From f8bed406702dbae30f103301e2a2e7c1beac7f42 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Wed, 7 Jan 2026 11:56:04 -0500 Subject: [PATCH] feat: Adds multi-step form to generate equivalent lengths for a project. --- Public/css/output.css | 17 ++ .../App/Middleware/ViewRoute+middleware.swift | 3 +- Sources/App/configure.swift | 14 ++ Sources/DatabaseClient/Errors.swift | 1 + Sources/ManualDCore/Routes/ViewRoute.swift | 161 +++++++++++++-- .../EquivalentLengthForm+extensions.swift | 46 +++++ Sources/ViewController/Live.swift | 41 +++- .../EffectiveLength/EffectiveLengthForm.swift | 184 ++++++++++++++---- .../EffectiveLengthsView.swift | 18 +- Sources/ViewController/Views/MainPage.swift | 8 + .../Views/Project/ProjectView.swift | 19 +- Tests/ApiRouteTests/ProjectRouteTests.swift | 30 +++ 12 files changed, 450 insertions(+), 92 deletions(-) create mode 100644 Sources/ViewController/Extensions/EquivalentLengthForm+extensions.swift diff --git a/Public/css/output.css b/Public/css/output.css index 1cf99c7..291d89f 100644 --- a/Public/css/output.css +++ b/Public/css/output.css @@ -22,6 +22,8 @@ --breakpoint-lg: 64rem; --container-lg: 32rem; --container-xl: 36rem; + --container-4xl: 56rem; + --container-7xl: 80rem; --text-sm: 0.875rem; --text-sm--line-height: calc(1.25 / 0.875); --text-xl: 1.25rem; @@ -5270,6 +5272,9 @@ } } } + .mx-auto { + margin-inline: auto; + } .file-input-ghost { @layer daisyui.l1.l2 { background-color: transparent; @@ -6534,9 +6539,21 @@ .w-full { width: 100%; } + .w-screen { + width: 100vw; + } .max-w-\[280px\] { max-width: 280px; } + .max-w-lg { + max-width: var(--container-lg); + } + .max-w-screen-lg { + max-width: var(--breakpoint-lg); + } + .max-w-xl { + max-width: var(--container-xl); + } .flex-none { flex: none; } diff --git a/Sources/App/Middleware/ViewRoute+middleware.swift b/Sources/App/Middleware/ViewRoute+middleware.swift index f31749c..0c3023f 100644 --- a/Sources/App/Middleware/ViewRoute+middleware.swift +++ b/Sources/App/Middleware/ViewRoute+middleware.swift @@ -14,8 +14,7 @@ private let viewRouteMiddleware: [any Middleware] = [ extension SiteRoute.View { var middleware: [any Middleware]? { switch self { - case .project, - .effectiveLength: + case .project: return viewRouteMiddleware case .login, .signup: return nil diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index ebb6a8c..00923e7 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -119,6 +119,20 @@ private func siteHandler( @Dependency(\.apiController) var apiController @Dependency(\.viewController) var viewController + request.logger.debug("Site Handler: Route: \(route)") + request.logger.debug("Content: \(request.content)") + // + // // HACK: Can't get arrays to decode currently. + if let content = try? request.content.decode( + SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepTwo.self + ) { + request.logger.debug("Site Handler: Got step two: \(content)") + // return try await viewController.respond( + // route: .project(.detail(content.projectID, .equivalentLength(.submit(.two(content))))), + // request: request + // ) + } + switch route { case .api(let route): return try await apiController.respond(route, request: request) diff --git a/Sources/DatabaseClient/Errors.swift b/Sources/DatabaseClient/Errors.swift index 76e148e..6c0a0f4 100644 --- a/Sources/DatabaseClient/Errors.swift +++ b/Sources/DatabaseClient/Errors.swift @@ -1,5 +1,6 @@ import Foundation +// TODO: Move to ManualDCore public struct ValidationError: Error { public let message: String diff --git a/Sources/ManualDCore/Routes/ViewRoute.swift b/Sources/ManualDCore/Routes/ViewRoute.swift index 9a55273..e6c5a2c 100644 --- a/Sources/ManualDCore/Routes/ViewRoute.swift +++ b/Sources/ManualDCore/Routes/ViewRoute.swift @@ -11,7 +11,6 @@ extension SiteRoute { case login(LoginRoute) case signup(SignupRoute) case project(ProjectRoute) - case effectiveLength(EffectiveLengthRoute) public static let router = OneOf { Route(.case(Self.login)) { @@ -23,9 +22,6 @@ extension SiteRoute { Route(.case(Self.project)) { SiteRoute.View.ProjectRoute.router } - Route(.case(Self.effectiveLength)) { - SiteRoute.View.EffectiveLengthRoute.router - } } } } @@ -148,6 +144,7 @@ extension SiteRoute.View.ProjectRoute { public enum DetailRoute: Equatable, Sendable { case index(tab: Tab = .default) case equipment(EquipmentInfoRoute) + case equivalentLength(EquivalentLengthRoute) case frictionRate(FrictionRateRoute) case rooms(RoomRoute) @@ -163,6 +160,9 @@ extension SiteRoute.View.ProjectRoute { Route(.case(Self.equipment)) { EquipmentInfoRoute.router } + Route(.case(Self.equivalentLength)) { + EquivalentLengthRoute.router + } Route(.case(Self.frictionRate)) { FrictionRateRoute.router } @@ -174,7 +174,7 @@ extension SiteRoute.View.ProjectRoute { public enum Tab: String, CaseIterable, Equatable, Sendable { case project case rooms - case effectiveLength + case equivalentLength case frictionRate case ductSizing @@ -373,13 +373,12 @@ extension SiteRoute.View.ProjectRoute { } } } -} -extension SiteRoute.View { - public enum EffectiveLengthRoute: Equatable, Sendable { + public enum EquivalentLengthRoute: Equatable, Sendable { case field(FieldType, style: EffectiveLength.EffectiveLengthType? = nil) case form(dismiss: Bool = false) case index + case submit(FormStep) static let rootPath = "effective-lengths" @@ -413,20 +412,144 @@ extension SiteRoute.View { } } } + Route(.case(Self.submit)) { + Path { rootPath } + Method.post + FormStep.router + } } - } -} -extension SiteRoute.View.EffectiveLengthRoute { - public enum FieldType: String, CaseIterable, Equatable, Sendable { - case straightLength - case group - } + public enum FormStep: Equatable, Sendable { + case one(StepOne) + case two(StepTwo) + case three(StepThree) + + static let router = OneOf { + Route(.case(Self.one)) { + Path { + Key.stepOne.rawValue + } + Body { + FormData { + Optionally { + Field("id", default: nil) { EffectiveLength.ID.parser() } + } + Field("name", .string) + Field("type") { EffectiveLength.EffectiveLengthType.parser() } + } + .map(.memberwise(StepOne.init)) + } + } + Route(.case(Self.two)) { + Path { + Key.stepTwo.rawValue + } + Body { + FormData { + Optionally { + Field("id", default: nil) { EffectiveLength.ID.parser() } + } + Field("name", .string) + Field("type") { EffectiveLength.EffectiveLengthType.parser() } + Many { + Field("straightLengths") { + Int.parser() + } + } + } + .map(.memberwise(StepTwo.init)) + } + } + Route(.case(Self.three)) { + Path { + Key.stepThree.rawValue + } + Body { + FormData { + Optionally { + Field("id", default: nil) { EffectiveLength.ID.parser() } + } + Field("name", .string) + Field("type") { EffectiveLength.EffectiveLengthType.parser() } + Many { + Field("straightLengths") { + Int.parser() + } + } + Many { + Field("group[group]") { + Int.parser() + } + } + Many { + Field("group[letter]", .string) + } + Many { + Field("group[length]") { + Int.parser() + } + + } + Many { + Field("group[quantity]") { + Int.parser() + } + } + } + .map(.memberwise(StepThree.init)) + } + } + } + + public enum Key: String, CaseIterable, Codable, Equatable, Sendable { + case stepOne + case stepTwo + case stepThree + } + } + + public struct StepOne: Codable, Equatable, Sendable { + public let id: EffectiveLength.ID? + public let name: String + public let type: EffectiveLength.EffectiveLengthType + } + + public struct StepTwo: Codable, Equatable, Sendable { + + public let id: EffectiveLength.ID? + public let name: String + public let type: EffectiveLength.EffectiveLengthType + public let straightLengths: [Int] + + public init( + id: EffectiveLength.ID? = nil, + name: String, + type: EffectiveLength.EffectiveLengthType, + straightLengths: [Int] + ) { + self.id = id + self.name = name + self.type = type + self.straightLengths = straightLengths + } + } + + public struct StepThree: Codable, Equatable, Sendable { + public let id: EffectiveLength.ID? + public let name: String + public let type: EffectiveLength.EffectiveLengthType + public let straightLengths: [Int] + public let groupGroups: [Int] + public let groupLetters: [String] + public let groupLengths: [Int] + public let groupQuantities: [Int] + } + + public enum FieldType: String, CaseIterable, Equatable, Sendable { + case straightLength + case group + } - public enum FormStep: String, CaseIterable, Equatable, Sendable { - case nameAndType - case straightLengths - case groups } } diff --git a/Sources/ViewController/Extensions/EquivalentLengthForm+extensions.swift b/Sources/ViewController/Extensions/EquivalentLengthForm+extensions.swift new file mode 100644 index 0000000..b7aabf8 --- /dev/null +++ b/Sources/ViewController/Extensions/EquivalentLengthForm+extensions.swift @@ -0,0 +1,46 @@ +import DatabaseClient +import ManualDCore + +extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree { + + func validate() throws(ValidationError) { + guard groupGroups.count == groupLengths.count, + groupGroups.count == groupLetters.count, + groupGroups.count == groupQuantities.count + else { + throw ValidationError("Equivalent length form group counts are not equal.") + } + } + + var groups: [EffectiveLength.Group] { + var groups = [EffectiveLength.Group]() + for (n, group) in groupGroups.enumerated() { + groups.append( + .init( + group: group, + letter: groupLetters[n], + value: Double(groupLengths[n]), + quantity: groupQuantities[n] + ) + ) + } + return groups + + } +} + +extension EffectiveLength.Create { + + init( + form: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree, + projectID: Project.ID + ) { + self.init( + projectID: projectID, + name: form.name, + type: form.type, + straightLengths: form.straightLengths, + groups: form.groups + ) + } +} diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index a80bfb5..406cb39 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -39,10 +39,6 @@ extension ViewController.Request { } case .project(let route): return try await route.renderView(on: self) - case .effectiveLength(let route): - return try await route.renderView(isHtmxRequest: isHtmxRequest) - // case .user(let route): - // return try await route.renderView(isHtmxRequest: isHtmxRequest) default: // FIX: FIX return _render(isHtmxRequest: false) { @@ -117,6 +113,8 @@ extension SiteRoute.View.ProjectRoute { } case .equipment(let route): return try await route.renderView(on: request, projectID: projectID) + case .equivalentLength(let route): + return try await route.renderView(on: request, projectID: projectID) case .frictionRate(let route): return try await route.renderView(on: request, projectID: projectID) case .rooms(let route): @@ -239,16 +237,21 @@ extension SiteRoute.View.ProjectRoute.FrictionRateRoute.FormType { } } -extension SiteRoute.View.EffectiveLengthRoute { +extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute { + + func renderView( + on request: ViewController.Request, + projectID: Project.ID + ) async throws -> AnySendableHTML { + @Dependency(\.database) var database - func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML { switch self { case .index: - return _render(isHtmxRequest: isHtmxRequest, active: .effectiveLength) { - EffectiveLengthsView(effectiveLengths: EffectiveLength.mocks) + return request.view { + ProjectView(projectID: projectID, activeTab: .equivalentLength) } case .form(let dismiss): - return EffectiveLengthForm(dismiss: dismiss) + return EffectiveLengthForm(projectID: projectID, dismiss: dismiss) case .field(let type, let style): switch type { @@ -258,7 +261,27 @@ extension SiteRoute.View.EffectiveLengthRoute { // FIX: return GroupField(style: style ?? .supply) } + + case .submit(let step): + switch step { + case .one(let stepOne): + return EffectiveLengthForm.StepTwo( + projectID: projectID, stepOne: stepOne, effectiveLength: nil + ) + case .two(let stepTwo): + request.logger.debug("ViewController: Got step two...") + return EffectiveLengthForm.StepThree( + projectID: projectID, effectiveLength: nil, stepTwo: stepTwo + ) + case .three(let stepThree): + request.logger.debug("ViewController: Got step three: \(stepThree)") + try stepThree.validate() + _ = try await database.effectiveLength.create(.init(form: stepThree, projectID: projectID)) + return ProjectView(projectID: projectID, activeTab: .equivalentLength) + + } } + } } diff --git a/Sources/ViewController/Views/EffectiveLength/EffectiveLengthForm.swift b/Sources/ViewController/Views/EffectiveLength/EffectiveLengthForm.swift index 8cba94e..b737eaa 100644 --- a/Sources/ViewController/Views/EffectiveLength/EffectiveLengthForm.swift +++ b/Sources/ViewController/Views/EffectiveLength/EffectiveLengthForm.swift @@ -9,34 +9,100 @@ import Styleguide // Currently when the select field is changed it doesn't change the group // I can get it to add a new one. +// TODO: Add back buttons / capability?? + struct EffectiveLengthForm: HTML, Sendable { + static let id = "equivalentLengthForm" + + let projectID: Project.ID let dismiss: Bool let type: EffectiveLength.EffectiveLengthType - init(dismiss: Bool, type: EffectiveLength.EffectiveLengthType = .supply) { + init( + projectID: Project.ID, + dismiss: Bool, + type: EffectiveLength.EffectiveLengthType = .supply + ) { + self.projectID = projectID self.dismiss = dismiss self.type = type } var body: some HTML { - ModalForm(id: "effectiveLengthForm", dismiss: dismiss) { + ModalForm(id: Self.id, dismiss: dismiss) { h1(.class("text-2xl font-bold")) { "Effective Length" } - form(.class("space-y-4 p-4")) { - div { - label(.for("name")) { "Name" } - Input(id: "name", placeholder: "Name") - .attributes(.type(.text), .required, .autofocus) + div(.id("formStep")) { + StepOne(projectID: projectID, effectiveLength: nil) + } + } + } + + struct StepOne: HTML, Sendable { + let projectID: Project.ID + let effectiveLength: EffectiveLength? + + var route: String { + let baseRoute = SiteRoute.View.router.path( + for: .project(.detail(projectID, .equivalentLength(.index))) + ) + return "\(baseRoute)/stepOne" + } + + var body: some HTML { + form( + .class("space-y-4"), + .hx.post(route), + .hx.target("#formStep"), + .hx.swap(.innerHTML) + ) { + if let id = effectiveLength?.id { + input(.class("hidden"), .name("id"), .value("\(id)")) } - div { - label(.for("type")) { "Type" } - GroupTypeSelect(selected: type) - .attributes(.class("w-full border rounded-md")) + Input(id: "name", placeholder: "Name") + .attributes(.type(.text), .required, .autofocus, .value(effectiveLength?.name)) + + GroupTypeSelect(projectID: projectID, selected: effectiveLength?.type ?? .supply) + + Row { + div {} + SubmitButton(title: "Next") } + } + } + } + + struct StepTwo: HTML, Sendable { + let projectID: Project.ID + let stepOne: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepOne + let effectiveLength: EffectiveLength? + + var route: String { + let baseRoute = SiteRoute.View.router.path( + for: .project(.detail(projectID, .equivalentLength(.index))) + ) + return "\(baseRoute)/stepTwo" + } + + var body: some HTML { + form( + .class("space-y-4"), + .hx.post(route), + .hx.target("#formStep"), + .hx.swap(.innerHTML) + ) { + if let id = effectiveLength?.id { + input(.class("hidden"), .name("id"), .value("\(id)")) + } + input(.class("hidden"), .name("name"), .value(stepOne.name)) + input(.class("hidden"), .name("type"), .value(stepOne.type.rawValue)) + Row { Label { "Straigth Lengths" } button( .type(.button), - .hx.get(route: .effectiveLength(.field(.straightLength))), + .hx.get( + route: .project(.detail(projectID, .equivalentLength(.field(.straightLength)))) + ), .hx.target("#straightLengths"), .hx.swap(.beforeEnd) ) { @@ -47,11 +113,50 @@ struct EffectiveLengthForm: HTML, Sendable { StraightLengthField() } + Row { + div {} + SubmitButton(title: "Next") + } + } + } + } + + struct StepThree: HTML, Sendable { + let projectID: Project.ID + let effectiveLength: EffectiveLength? + let stepTwo: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepTwo + + var route: String { + let baseRoute = SiteRoute.View.router.path( + for: .project(.detail(projectID, .equivalentLength(.index))) + ) + return "\(baseRoute)/stepThree" + } + + var body: some HTML { + form( + .class("space-y-4"), + .hx.post(route), + .hx.target("body"), + .hx.swap(.outerHTML) + ) { + if let id = effectiveLength?.id { + input(.class("hidden"), .name("id"), .value("\(id)")) + } + input(.class("hidden"), .name("name"), .value(stepTwo.name)) + input(.class("hidden"), .name("type"), .value(stepTwo.type.rawValue)) + for length in stepTwo.straightLengths { + input(.class("hidden"), .name("straightLengths"), .value("\(length)")) + } + Row { Label { "Groups" } button( .type(.button), - .hx.get(route: .effectiveLength(.field(.group, style: type))), + .hx.get( + route: .project( + .detail(projectID, .equivalentLength(.field(.group, style: stepTwo.type)))) + ), .hx.target("#groups"), .hx.swap(.beforeEnd) ) { @@ -59,20 +164,11 @@ struct EffectiveLengthForm: HTML, Sendable { } } div(.id("groups"), .class("space-y-4")) { - GroupField(style: type) + GroupField(style: stepTwo.type) } - Row { div {} - div(.class("space-x-4")) { - CancelButton() - .attributes( - .hx.get(route: .effectiveLength(.form(dismiss: true))), - .hx.target("#effectiveLengthForm"), - .hx.swap(.outerHTML) - ) - SubmitButton() - } + SubmitButton() } } } @@ -87,13 +183,17 @@ struct StraightLengthField: HTML, Sendable { } var body: some HTML { - div(.class("pb-4")) { + Row { Input( - name: "straightLengths[]", + name: "straightLengths", placeholder: "Length" ) - .attributes(.type(.number), .min("0")) + .attributes(.type(.number), .min("0"), .autofocus, .required) + + TrashButton() + .attributes(.data("remove", value: "true")) } + .attributes(.hx.ext("remove")) } } @@ -103,17 +203,17 @@ struct GroupField: HTML, Sendable { var body: some HTML { Row { - // Input(name: "group[][group]", placeholder: "Group") - // .attributes(.type(.number), .min("0")) GroupSelect(style: style) - Input(name: "group[][letter]", placeholder: "Letter") - .attributes(.type(.text)) - Input(name: "group[][length]", placeholder: "Length") - .attributes(.type(.number), .min("0")) - Input(name: "group[][quantity]", placeholder: "Quantity") - .attributes(.type(.number), .min("1"), .value("1")) + Input(name: "group[letter]", placeholder: "Letter") + .attributes(.type(.text), .autofocus, .required) + Input(name: "group[length]", placeholder: "Length") + .attributes(.type(.number), .min("0"), .required) + Input(name: "group[quantity]", placeholder: "Quantity") + .attributes(.type(.number), .min("1"), .value("1"), .required) + TrashButton() + .attributes(.data("remove", value: "true")) } - .attributes(.class("space-x-2")) + .attributes(.class("space-x-2"), .hx.ext("remove")) } } @@ -123,7 +223,8 @@ struct GroupSelect: HTML, Sendable { var body: some HTML { select( - .name("group") + .name("group[group]"), + .class("select") ) { for value in style.selectOptions { option(.value("\(value)")) { "\(value)" } @@ -135,17 +236,14 @@ struct GroupSelect: HTML, Sendable { struct GroupTypeSelect: HTML, Sendable { - var selected: EffectiveLength.EffectiveLengthType + let projectID: Project.ID + let selected: EffectiveLength.EffectiveLengthType var body: some HTML { - select(.name("type"), .id("type")) { + select(.class("select"), .name("type"), .id("type")) { for value in EffectiveLength.EffectiveLengthType.allCases { option( .value("\(value.rawValue)"), - .hx.get(route: .effectiveLength(.field(.group, style: value))), - .hx.target("#groups"), - .hx.swap(.beforeEnd), - .hx.trigger(.event(.change).from("#type")) ) { value.title } .attributes(.selected, when: value == selected) } diff --git a/Sources/ViewController/Views/EffectiveLength/EffectiveLengthsView.swift b/Sources/ViewController/Views/EffectiveLength/EffectiveLengthsView.swift index 6ef27d3..76f4d41 100644 --- a/Sources/ViewController/Views/EffectiveLength/EffectiveLengthsView.swift +++ b/Sources/ViewController/Views/EffectiveLength/EffectiveLengthsView.swift @@ -5,6 +5,7 @@ import Styleguide struct EffectiveLengthsView: HTML, Sendable { + let projectID: Project.ID let effectiveLengths: [EffectiveLength] var body: some HTML { @@ -12,20 +13,9 @@ struct EffectiveLengthsView: HTML, Sendable { .class("m-4") ) { Row { - h1(.class("text-2xl font-bold")) { "Effective Lengths" } + h1(.class("text-2xl font-bold")) { "Equivalent Lengths" } PlusButton() - .attributes( - .hx.get(route: .effectiveLength(.form(dismiss: false))), - .hx.target("#effectiveLengthForm"), - .hx.swap(.outerHTML) - ) - // button( - // .hx.get(route: .effectiveLength(.form(dismiss: false))), - // .hx.target("#effectiveLengthForm"), - // .hx.swap(.outerHTML) - // ) { - // Icon(.circlePlus) - // } + .attributes(.showModal(id: EffectiveLengthForm.id)) } .attributes(.class("pb-6")) @@ -38,7 +28,7 @@ struct EffectiveLengthsView: HTML, Sendable { } } - EffectiveLengthForm(dismiss: true) + EffectiveLengthForm(projectID: projectID, dismiss: true) } } diff --git a/Sources/ViewController/Views/MainPage.swift b/Sources/ViewController/Views/MainPage.swift index 0732c11..a6d3d9c 100644 --- a/Sources/ViewController/Views/MainPage.swift +++ b/Sources/ViewController/Views/MainPage.swift @@ -23,6 +23,14 @@ public struct MainPage: SendableHTMLDocument where Inner: Sendable script(.src("/js/main.js")) {} link(.rel(.stylesheet), .href("/css/output.css")) link(.rel(.icon), .href("/images/favicon.ico"), .custom(name: "type", value: "image/x-icon")) + HTMLRaw( + """ + + """ + ) } public var body: some HTML { diff --git a/Sources/ViewController/Views/Project/ProjectView.swift b/Sources/ViewController/Views/Project/ProjectView.swift index 97f866a..0db196a 100644 --- a/Sources/ViewController/Views/Project/ProjectView.swift +++ b/Sources/ViewController/Views/Project/ProjectView.swift @@ -42,8 +42,9 @@ struct ProjectView: HTML, Sendable { sensibleHeatRatio: database.projects.getSensibleHeatRatio(projectID) ) - case .effectiveLength: + case .equivalentLength: try await EffectiveLengthsView( + projectID: projectID, effectiveLengths: database.effectiveLength.fetch(projectID) ) case .frictionRate: @@ -103,11 +104,19 @@ struct Sidebar: HTML { ) .attributes(.data("active", value: active == .project ? "true" : "false")) - row(title: "Rooms", icon: .doorClosed, route: .project(.detail(projectID, .rooms(.index)))) - .attributes(.data("active", value: active == .rooms ? "true" : "false")) + row( + title: "Rooms", + icon: .doorClosed, + route: .project(.detail(projectID, .rooms(.index))) + ) + .attributes(.data("active", value: active == .rooms ? "true" : "false")) - row(title: "Equivalent Lengths", icon: .rulerDimensionLine, route: .effectiveLength(.index)) - .attributes(.data("active", value: active == .effectiveLength ? "true" : "false")) + row( + title: "Equivalent Lengths", + icon: .rulerDimensionLine, + route: .project(.detail(projectID, .equivalentLength(.index))) + ) + .attributes(.data("active", value: active == .equivalentLength ? "true" : "false")) row( title: "Friction Rate", diff --git a/Tests/ApiRouteTests/ProjectRouteTests.swift b/Tests/ApiRouteTests/ProjectRouteTests.swift index 1865f8f..3102064 100644 --- a/Tests/ApiRouteTests/ProjectRouteTests.swift +++ b/Tests/ApiRouteTests/ProjectRouteTests.swift @@ -72,4 +72,34 @@ struct ProjectRouteTests { let route = try router.parse(&request) #expect(route == .project(.index)) } + + @Test + func formData() throws { + let p = Body { + FormData { + Optionally { + Field("id", default: nil) { EffectiveLength.ID.parser() } + } + Field("projectID") { Project.ID.parser() } + Field("name", .string) + Field("type") { EffectiveLength.EffectiveLengthType.parser() } + Many { + Field("straightLengths") { + Int.parser() + } + } + } + .map(.memberwise(SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepTwo.init)) + } + + var request = URLRequestData( + body: .init( + "projectID=15062A72-7AB5-4F15-9B1F-74A4BFA53CBB&name=Test&type=supply&straightLengths=20&straightLengths=10" + .utf8 + ) + ) + let value = try p.parse(&request) + print(value) + #expect(value.straightLengths == [20, 10]) + } }