feat: Working on adding updates to project form, it's currently not loading an existing project.
This commit is contained in:
@@ -6739,6 +6739,9 @@
|
|||||||
.justify-center {
|
.justify-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.justify-end {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
.gap-4 {
|
.gap-4 {
|
||||||
gap: calc(var(--spacing) * 4);
|
gap: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
@@ -6770,6 +6773,13 @@
|
|||||||
margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
|
margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.space-x-6 {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
--tw-space-x-reverse: 0;
|
||||||
|
margin-inline-start: calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse));
|
||||||
|
margin-inline-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)));
|
||||||
|
}
|
||||||
|
}
|
||||||
.overflow-x-auto {
|
.overflow-x-auto {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ extension DatabaseClient {
|
|||||||
public var delete: @Sendable (Project.ID) async throws -> Void
|
public var delete: @Sendable (Project.ID) async throws -> Void
|
||||||
public var get: @Sendable (Project.ID) async throws -> Project?
|
public var get: @Sendable (Project.ID) async throws -> Project?
|
||||||
public var fetch: @Sendable (User.ID, PageRequest) async throws -> Page<Project>
|
public var fetch: @Sendable (User.ID, PageRequest) async throws -> Page<Project>
|
||||||
|
public var update: @Sendable (Project.Update) async throws -> Project
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +41,16 @@ extension DatabaseClient.Projects: TestDependencyKey {
|
|||||||
.filter(\.$user.$id == userID)
|
.filter(\.$user.$id == userID)
|
||||||
.paginate(request)
|
.paginate(request)
|
||||||
.map { try $0.toDTO() }
|
.map { try $0.toDTO() }
|
||||||
|
},
|
||||||
|
update: { updates in
|
||||||
|
guard let model = try await ProjectModel.find(updates.id, on: database) else {
|
||||||
|
throw NotFoundError()
|
||||||
|
}
|
||||||
|
try updates.validate()
|
||||||
|
if model.applyUpdates(updates) {
|
||||||
|
try await model.save(on: database)
|
||||||
|
}
|
||||||
|
return try model.toDTO()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -78,6 +89,37 @@ extension Project.Create {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Project.Update {
|
||||||
|
|
||||||
|
func validate() throws(ValidationError) {
|
||||||
|
if let name {
|
||||||
|
guard !name.isEmpty else {
|
||||||
|
throw ValidationError("Project name should not be empty.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let streetAddress {
|
||||||
|
guard !streetAddress.isEmpty else {
|
||||||
|
throw ValidationError("Project street address should not be empty.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let city {
|
||||||
|
guard !city.isEmpty else {
|
||||||
|
throw ValidationError("Project city should not be empty.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let state {
|
||||||
|
guard !state.isEmpty else {
|
||||||
|
throw ValidationError("Project state should not be empty.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let zipCode {
|
||||||
|
guard !zipCode.isEmpty else {
|
||||||
|
throw ValidationError("Project zipCode should not be empty.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension Project {
|
extension Project {
|
||||||
struct Migrate: AsyncMigration {
|
struct Migrate: AsyncMigration {
|
||||||
let name = "CreateProject"
|
let name = "CreateProject"
|
||||||
@@ -155,7 +197,6 @@ final class ProjectModel: Model, @unchecked Sendable {
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.streetAddress = streetAddress
|
self.streetAddress = streetAddress
|
||||||
self.city = city
|
self.city = city
|
||||||
self.city = city
|
|
||||||
self.state = state
|
self.state = state
|
||||||
self.zipCode = zipCode
|
self.zipCode = zipCode
|
||||||
$user.id = userID
|
$user.id = userID
|
||||||
@@ -175,4 +216,29 @@ final class ProjectModel: Model, @unchecked Sendable {
|
|||||||
updatedAt: updatedAt!
|
updatedAt: updatedAt!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyUpdates(_ updates: Project.Update) -> Bool {
|
||||||
|
var hasUpdates = false
|
||||||
|
if let name = updates.name, name != self.name {
|
||||||
|
hasUpdates = true
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
if let streetAddress = updates.streetAddress, streetAddress != self.streetAddress {
|
||||||
|
hasUpdates = true
|
||||||
|
self.streetAddress = streetAddress
|
||||||
|
}
|
||||||
|
if let city = updates.city, city != self.city {
|
||||||
|
hasUpdates = true
|
||||||
|
self.city = city
|
||||||
|
}
|
||||||
|
if let state = updates.state, state != self.state {
|
||||||
|
hasUpdates = true
|
||||||
|
self.state = state
|
||||||
|
}
|
||||||
|
if let zipCode = updates.zipCode, zipCode != self.zipCode {
|
||||||
|
hasUpdates = true
|
||||||
|
self.zipCode = zipCode
|
||||||
|
}
|
||||||
|
return hasUpdates
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,32 @@ extension Project {
|
|||||||
self.zipCode = zipCode
|
self.zipCode = zipCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct Update: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let id: Project.ID
|
||||||
|
public let name: String?
|
||||||
|
public let streetAddress: String?
|
||||||
|
public let city: String?
|
||||||
|
public let state: String?
|
||||||
|
public let zipCode: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: Project.ID,
|
||||||
|
name: String? = nil,
|
||||||
|
streetAddress: String? = nil,
|
||||||
|
city: String? = nil,
|
||||||
|
state: String? = nil,
|
||||||
|
zipCode: String? = nil
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.streetAddress = streetAddress
|
||||||
|
self.city = city
|
||||||
|
self.state = state
|
||||||
|
self.zipCode = zipCode
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|||||||
@@ -35,9 +35,10 @@ extension SiteRoute.View {
|
|||||||
case create(Project.Create)
|
case create(Project.Create)
|
||||||
case delete(id: Project.ID)
|
case delete(id: Project.ID)
|
||||||
case detail(Project.ID, DetailRoute)
|
case detail(Project.ID, DetailRoute)
|
||||||
case form(dismiss: Bool = false)
|
case form(id: Project.ID? = nil, dismiss: Bool = false)
|
||||||
case index
|
case index
|
||||||
case page(PageRequest)
|
case page(PageRequest)
|
||||||
|
case update(Project.Update)
|
||||||
|
|
||||||
public static func page(page: Int, per limit: Int) -> Self {
|
public static func page(page: Int, per limit: Int) -> Self {
|
||||||
.page(.init(page: page, per: limit))
|
.page(.init(page: page, per: limit))
|
||||||
@@ -81,6 +82,9 @@ extension SiteRoute.View {
|
|||||||
}
|
}
|
||||||
Method.get
|
Method.get
|
||||||
Query {
|
Query {
|
||||||
|
Optionally {
|
||||||
|
Field("id", default: nil) { Project.ID.parser() }
|
||||||
|
}
|
||||||
Field("dismiss", default: false) { Bool.parser() }
|
Field("dismiss", default: false) { Bool.parser() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,6 +104,31 @@ extension SiteRoute.View {
|
|||||||
}
|
}
|
||||||
.map(.memberwise(PageRequest.init))
|
.map(.memberwise(PageRequest.init))
|
||||||
}
|
}
|
||||||
|
Route(.case(Self.update)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.patch
|
||||||
|
Body {
|
||||||
|
FormData {
|
||||||
|
Field("id") { Project.ID.parser() }
|
||||||
|
Optionally {
|
||||||
|
Field("name", .string)
|
||||||
|
}
|
||||||
|
Optionally {
|
||||||
|
Field("streetAddress", .string)
|
||||||
|
}
|
||||||
|
Optionally {
|
||||||
|
Field("city", .string)
|
||||||
|
}
|
||||||
|
Optionally {
|
||||||
|
Field("state", .string)
|
||||||
|
}
|
||||||
|
Optionally {
|
||||||
|
Field("zipCode", .string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map(.memberwise(Project.Update.init))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,14 +65,7 @@ public struct EditButton: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var body: some HTML<HTMLTag.button> {
|
public var body: some HTML<HTMLTag.button> {
|
||||||
button(
|
button(.class("btn btn-success dark:text-white"), .type(type)) {
|
||||||
.class(
|
|
||||||
"""
|
|
||||||
btn btn-primary
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
.type(type)
|
|
||||||
) {
|
|
||||||
div(.class("flex")) {
|
div(.class("flex")) {
|
||||||
if let title {
|
if let title {
|
||||||
span(.class("pe-2")) { title }
|
span(.class("pe-2")) { title }
|
||||||
|
|||||||
@@ -85,8 +85,16 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
let projects = try await database.projects.fetch(user.id, page)
|
let projects = try await database.projects.fetch(user.id, page)
|
||||||
return ProjectsTable(userID: user.id, projects: projects)
|
return ProjectsTable(userID: user.id, projects: projects)
|
||||||
|
|
||||||
case .form(let dismiss):
|
case .form(let id, let dismiss):
|
||||||
return ProjectForm(dismiss: dismiss)
|
request.logger.debug("Project form: \(id != nil ? "Fetching project for: \(id!)" : "N/A")")
|
||||||
|
var project: Project? = nil
|
||||||
|
if let id, dismiss == false {
|
||||||
|
project = try await database.projects.get(id)
|
||||||
|
}
|
||||||
|
request.logger.debug(
|
||||||
|
project == nil ? "No project found" : "Showing form for existing project"
|
||||||
|
)
|
||||||
|
return ProjectForm(dismiss: dismiss, project: project)
|
||||||
|
|
||||||
case .create(let form):
|
case .create(let form):
|
||||||
let project = try await database.projects.create(user.id, form)
|
let project = try await database.projects.create(user.id, form)
|
||||||
@@ -97,11 +105,16 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
try await database.projects.delete(id)
|
try await database.projects.delete(id)
|
||||||
return EmptyHTML()
|
return EmptyHTML()
|
||||||
|
|
||||||
|
case .update(let form):
|
||||||
|
let project = try await database.projects.update(form)
|
||||||
|
return ProjectView(projectID: project.id, activeTab: .project)
|
||||||
|
|
||||||
case .detail(let projectID, let route):
|
case .detail(let projectID, let route):
|
||||||
switch route {
|
switch route {
|
||||||
case .index(let tab):
|
case .index(let tab):
|
||||||
return ProjectView(projectID: projectID, activeTab: tab)
|
return request.view {
|
||||||
// return try await defaultDetailView(projectID: projectID, activeTab: tab)
|
ProjectView(projectID: projectID, activeTab: tab)
|
||||||
|
}
|
||||||
case .equipment(let route):
|
case .equipment(let route):
|
||||||
return try await route.renderView(on: request, projectID: projectID)
|
return try await route.renderView(on: request, projectID: projectID)
|
||||||
case .frictionRate(let route):
|
case .frictionRate(let route):
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ struct ProjectDetail: HTML, Sendable {
|
|||||||
h1(.class("text-2xl font-bold")) { "Project" }
|
h1(.class("text-2xl font-bold")) { "Project" }
|
||||||
EditButton()
|
EditButton()
|
||||||
.attributes(
|
.attributes(
|
||||||
.hx.get(route: .project(.form(dismiss: false))),
|
.hx.get(route: .project(.form(id: project.id, dismiss: false))),
|
||||||
.hx.target("#projectForm"),
|
.hx.target("#projectForm"),
|
||||||
.hx.swap(.outerHTML)
|
.hx.swap(.outerHTML)
|
||||||
)
|
)
|
||||||
@@ -54,6 +54,6 @@ struct ProjectDetail: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div(.id("projectForm")) {}
|
ProjectForm(dismiss: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,16 @@ struct ProjectForm: HTML, Sendable {
|
|||||||
h1(.class("text-3xl font-bold pb-6 ps-2")) { "Project" }
|
h1(.class("text-3xl font-bold pb-6 ps-2")) { "Project" }
|
||||||
form(
|
form(
|
||||||
.class("space-y-4 p-4"),
|
.class("space-y-4 p-4"),
|
||||||
.method(.post),
|
project == nil
|
||||||
.action(route: .project(.index))
|
? .hx.post(route: .project(.index))
|
||||||
|
: .hx.patch(route: .project(.index)),
|
||||||
|
.hx.target("body"),
|
||||||
|
.hx.swap(.outerHTML)
|
||||||
) {
|
) {
|
||||||
|
if let project {
|
||||||
|
input(.class("hidden"), .name("id"), .value("\(project.id)"))
|
||||||
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
label(.for("name")) { "Name" }
|
label(.for("name")) { "Name" }
|
||||||
Input(id: "name", placeholder: "Name")
|
Input(id: "name", placeholder: "Name")
|
||||||
@@ -32,27 +39,25 @@ struct ProjectForm: HTML, Sendable {
|
|||||||
div {
|
div {
|
||||||
label(.for("streetAddress")) { "Address" }
|
label(.for("streetAddress")) { "Address" }
|
||||||
Input(id: "streetAddress", placeholder: "Street Address")
|
Input(id: "streetAddress", placeholder: "Street Address")
|
||||||
.attributes(.type(.text), .required)
|
.attributes(.type(.text), .required, .value(project?.streetAddress))
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
label(.for("city")) { "City" }
|
label(.for("city")) { "City" }
|
||||||
Input(id: "city", placeholder: "City")
|
Input(id: "city", placeholder: "City")
|
||||||
.attributes(.type(.text), .required)
|
.attributes(.type(.text), .required, .value(project?.city))
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
label(.for("state")) { "State" }
|
label(.for("state")) { "State" }
|
||||||
Input(id: "state", placeholder: "State")
|
Input(id: "state", placeholder: "State")
|
||||||
.attributes(.type(.text), .required)
|
.attributes(.type(.text), .required, .value(project?.state))
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
label(.for("zipCode")) { "Zip" }
|
label(.for("zipCode")) { "Zip" }
|
||||||
Input(id: "zipCode", placeholder: "Zip code")
|
Input(id: "zipCode", placeholder: "Zip code")
|
||||||
.attributes(.type(.text), .required)
|
.attributes(.type(.text), .required, .value(project?.zipCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
div(.class("flex justify-end space-x-6")) {
|
||||||
div {}
|
|
||||||
div(.class("space-x-4")) {
|
|
||||||
CancelButton()
|
CancelButton()
|
||||||
.attributes(
|
.attributes(
|
||||||
.hx.get(route: .project(.form(dismiss: true))),
|
.hx.get(route: .project(.form(dismiss: true))),
|
||||||
@@ -64,6 +69,5 @@ struct ProjectForm: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ struct RoomsView: HTML, Sendable {
|
|||||||
.class("badge badge-outline badge-success badge-xl"))
|
.class("badge badge-outline badge-success badge-xl"))
|
||||||
}
|
}
|
||||||
td {}
|
td {}
|
||||||
|
td {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,10 +88,11 @@ struct RoomsView: HTML, Sendable {
|
|||||||
Number(room.registerCount)
|
Number(room.registerCount)
|
||||||
}
|
}
|
||||||
td {
|
td {
|
||||||
Row {
|
div(.class("flex justify-end space-x-6")) {
|
||||||
TrashButton()
|
TrashButton()
|
||||||
.attributes(
|
.attributes(
|
||||||
.hx.delete(route: .project(.detail(room.projectID, .rooms(.delete(id: room.id))))),
|
.hx.delete(
|
||||||
|
route: .project(.detail(room.projectID, .rooms(.delete(id: room.id))))),
|
||||||
.hx.target("closest tr"),
|
.hx.target("closest tr"),
|
||||||
.hx.confirm("Are you sure?")
|
.hx.confirm("Are you sure?")
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user