feat: Adds multi-step form to generate equivalent lengths for a project.
This commit is contained in:
@@ -22,6 +22,8 @@
|
|||||||
--breakpoint-lg: 64rem;
|
--breakpoint-lg: 64rem;
|
||||||
--container-lg: 32rem;
|
--container-lg: 32rem;
|
||||||
--container-xl: 36rem;
|
--container-xl: 36rem;
|
||||||
|
--container-4xl: 56rem;
|
||||||
|
--container-7xl: 80rem;
|
||||||
--text-sm: 0.875rem;
|
--text-sm: 0.875rem;
|
||||||
--text-sm--line-height: calc(1.25 / 0.875);
|
--text-sm--line-height: calc(1.25 / 0.875);
|
||||||
--text-xl: 1.25rem;
|
--text-xl: 1.25rem;
|
||||||
@@ -5270,6 +5272,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.mx-auto {
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
.file-input-ghost {
|
.file-input-ghost {
|
||||||
@layer daisyui.l1.l2 {
|
@layer daisyui.l1.l2 {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -6534,9 +6539,21 @@
|
|||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.w-screen {
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
.max-w-\[280px\] {
|
.max-w-\[280px\] {
|
||||||
max-width: 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 {
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ private let viewRouteMiddleware: [any Middleware] = [
|
|||||||
extension SiteRoute.View {
|
extension SiteRoute.View {
|
||||||
var middleware: [any Middleware]? {
|
var middleware: [any Middleware]? {
|
||||||
switch self {
|
switch self {
|
||||||
case .project,
|
case .project:
|
||||||
.effectiveLength:
|
|
||||||
return viewRouteMiddleware
|
return viewRouteMiddleware
|
||||||
case .login, .signup:
|
case .login, .signup:
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -119,6 +119,20 @@ private func siteHandler(
|
|||||||
@Dependency(\.apiController) var apiController
|
@Dependency(\.apiController) var apiController
|
||||||
@Dependency(\.viewController) var viewController
|
@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 {
|
switch route {
|
||||||
case .api(let route):
|
case .api(let route):
|
||||||
return try await apiController.respond(route, request: request)
|
return try await apiController.respond(route, request: request)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
// TODO: Move to ManualDCore
|
||||||
public struct ValidationError: Error {
|
public struct ValidationError: Error {
|
||||||
public let message: String
|
public let message: String
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ extension SiteRoute {
|
|||||||
case login(LoginRoute)
|
case login(LoginRoute)
|
||||||
case signup(SignupRoute)
|
case signup(SignupRoute)
|
||||||
case project(ProjectRoute)
|
case project(ProjectRoute)
|
||||||
case effectiveLength(EffectiveLengthRoute)
|
|
||||||
|
|
||||||
public static let router = OneOf {
|
public static let router = OneOf {
|
||||||
Route(.case(Self.login)) {
|
Route(.case(Self.login)) {
|
||||||
@@ -23,9 +22,6 @@ extension SiteRoute {
|
|||||||
Route(.case(Self.project)) {
|
Route(.case(Self.project)) {
|
||||||
SiteRoute.View.ProjectRoute.router
|
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 {
|
public enum DetailRoute: Equatable, Sendable {
|
||||||
case index(tab: Tab = .default)
|
case index(tab: Tab = .default)
|
||||||
case equipment(EquipmentInfoRoute)
|
case equipment(EquipmentInfoRoute)
|
||||||
|
case equivalentLength(EquivalentLengthRoute)
|
||||||
case frictionRate(FrictionRateRoute)
|
case frictionRate(FrictionRateRoute)
|
||||||
case rooms(RoomRoute)
|
case rooms(RoomRoute)
|
||||||
|
|
||||||
@@ -163,6 +160,9 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
Route(.case(Self.equipment)) {
|
Route(.case(Self.equipment)) {
|
||||||
EquipmentInfoRoute.router
|
EquipmentInfoRoute.router
|
||||||
}
|
}
|
||||||
|
Route(.case(Self.equivalentLength)) {
|
||||||
|
EquivalentLengthRoute.router
|
||||||
|
}
|
||||||
Route(.case(Self.frictionRate)) {
|
Route(.case(Self.frictionRate)) {
|
||||||
FrictionRateRoute.router
|
FrictionRateRoute.router
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
public enum Tab: String, CaseIterable, Equatable, Sendable {
|
public enum Tab: String, CaseIterable, Equatable, Sendable {
|
||||||
case project
|
case project
|
||||||
case rooms
|
case rooms
|
||||||
case effectiveLength
|
case equivalentLength
|
||||||
case frictionRate
|
case frictionRate
|
||||||
case ductSizing
|
case ductSizing
|
||||||
|
|
||||||
@@ -373,13 +373,12 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension SiteRoute.View {
|
public enum EquivalentLengthRoute: Equatable, Sendable {
|
||||||
public enum EffectiveLengthRoute: Equatable, Sendable {
|
|
||||||
case field(FieldType, style: EffectiveLength.EffectiveLengthType? = nil)
|
case field(FieldType, style: EffectiveLength.EffectiveLengthType? = nil)
|
||||||
case form(dismiss: Bool = false)
|
case form(dismiss: Bool = false)
|
||||||
case index
|
case index
|
||||||
|
case submit(FormStep)
|
||||||
|
|
||||||
static let rootPath = "effective-lengths"
|
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 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 {
|
public enum FieldType: String, CaseIterable, Equatable, Sendable {
|
||||||
case straightLength
|
case straightLength
|
||||||
case group
|
case group
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FormStep: String, CaseIterable, Equatable, Sendable {
|
|
||||||
case nameAndType
|
|
||||||
case straightLengths
|
|
||||||
case groups
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,10 +39,6 @@ extension ViewController.Request {
|
|||||||
}
|
}
|
||||||
case .project(let route):
|
case .project(let route):
|
||||||
return try await route.renderView(on: self)
|
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:
|
default:
|
||||||
// FIX: FIX
|
// FIX: FIX
|
||||||
return _render(isHtmxRequest: false) {
|
return _render(isHtmxRequest: false) {
|
||||||
@@ -117,6 +113,8 @@ extension SiteRoute.View.ProjectRoute {
|
|||||||
}
|
}
|
||||||
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 .equivalentLength(let route):
|
||||||
|
return try await route.renderView(on: request, projectID: projectID)
|
||||||
case .frictionRate(let route):
|
case .frictionRate(let route):
|
||||||
return try await route.renderView(on: request, projectID: projectID)
|
return try await route.renderView(on: request, projectID: projectID)
|
||||||
case .rooms(let route):
|
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 {
|
switch self {
|
||||||
case .index:
|
case .index:
|
||||||
return _render(isHtmxRequest: isHtmxRequest, active: .effectiveLength) {
|
return request.view {
|
||||||
EffectiveLengthsView(effectiveLengths: EffectiveLength.mocks)
|
ProjectView(projectID: projectID, activeTab: .equivalentLength)
|
||||||
}
|
}
|
||||||
case .form(let dismiss):
|
case .form(let dismiss):
|
||||||
return EffectiveLengthForm(dismiss: dismiss)
|
return EffectiveLengthForm(projectID: projectID, dismiss: dismiss)
|
||||||
|
|
||||||
case .field(let type, let style):
|
case .field(let type, let style):
|
||||||
switch type {
|
switch type {
|
||||||
@@ -258,8 +261,28 @@ extension SiteRoute.View.EffectiveLengthRoute {
|
|||||||
// FIX:
|
// FIX:
|
||||||
return GroupField(style: style ?? .supply)
|
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func _render<C: HTML>(
|
private func _render<C: HTML>(
|
||||||
|
|||||||
@@ -9,34 +9,100 @@ import Styleguide
|
|||||||
// Currently when the select field is changed it doesn't change the group
|
// Currently when the select field is changed it doesn't change the group
|
||||||
// I can get it to add a new one.
|
// I can get it to add a new one.
|
||||||
|
|
||||||
|
// TODO: Add back buttons / capability??
|
||||||
|
|
||||||
struct EffectiveLengthForm: HTML, Sendable {
|
struct EffectiveLengthForm: HTML, Sendable {
|
||||||
|
static let id = "equivalentLengthForm"
|
||||||
|
|
||||||
|
let projectID: Project.ID
|
||||||
let dismiss: Bool
|
let dismiss: Bool
|
||||||
let type: EffectiveLength.EffectiveLengthType
|
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.dismiss = dismiss
|
||||||
self.type = type
|
self.type = type
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
ModalForm(id: "effectiveLengthForm", dismiss: dismiss) {
|
ModalForm(id: Self.id, dismiss: dismiss) {
|
||||||
h1(.class("text-2xl font-bold")) { "Effective Length" }
|
h1(.class("text-2xl font-bold")) { "Effective Length" }
|
||||||
form(.class("space-y-4 p-4")) {
|
div(.id("formStep")) {
|
||||||
div {
|
StepOne(projectID: projectID, effectiveLength: nil)
|
||||||
label(.for("name")) { "Name" }
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)"))
|
||||||
|
}
|
||||||
Input(id: "name", placeholder: "Name")
|
Input(id: "name", placeholder: "Name")
|
||||||
.attributes(.type(.text), .required, .autofocus)
|
.attributes(.type(.text), .required, .autofocus, .value(effectiveLength?.name))
|
||||||
|
|
||||||
|
GroupTypeSelect(projectID: projectID, selected: effectiveLength?.type ?? .supply)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
div {}
|
||||||
|
SubmitButton(title: "Next")
|
||||||
}
|
}
|
||||||
div {
|
|
||||||
label(.for("type")) { "Type" }
|
|
||||||
GroupTypeSelect(selected: type)
|
|
||||||
.attributes(.class("w-full border rounded-md"))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
Row {
|
||||||
Label { "Straigth Lengths" }
|
Label { "Straigth Lengths" }
|
||||||
button(
|
button(
|
||||||
.type(.button),
|
.type(.button),
|
||||||
.hx.get(route: .effectiveLength(.field(.straightLength))),
|
.hx.get(
|
||||||
|
route: .project(.detail(projectID, .equivalentLength(.field(.straightLength))))
|
||||||
|
),
|
||||||
.hx.target("#straightLengths"),
|
.hx.target("#straightLengths"),
|
||||||
.hx.swap(.beforeEnd)
|
.hx.swap(.beforeEnd)
|
||||||
) {
|
) {
|
||||||
@@ -47,11 +113,50 @@ struct EffectiveLengthForm: HTML, Sendable {
|
|||||||
StraightLengthField()
|
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 {
|
Row {
|
||||||
Label { "Groups" }
|
Label { "Groups" }
|
||||||
button(
|
button(
|
||||||
.type(.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.target("#groups"),
|
||||||
.hx.swap(.beforeEnd)
|
.hx.swap(.beforeEnd)
|
||||||
) {
|
) {
|
||||||
@@ -59,24 +164,15 @@ struct EffectiveLengthForm: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
div(.id("groups"), .class("space-y-4")) {
|
div(.id("groups"), .class("space-y-4")) {
|
||||||
GroupField(style: type)
|
GroupField(style: stepTwo.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
div {}
|
div {}
|
||||||
div(.class("space-x-4")) {
|
|
||||||
CancelButton()
|
|
||||||
.attributes(
|
|
||||||
.hx.get(route: .effectiveLength(.form(dismiss: true))),
|
|
||||||
.hx.target("#effectiveLengthForm"),
|
|
||||||
.hx.swap(.outerHTML)
|
|
||||||
)
|
|
||||||
SubmitButton()
|
SubmitButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StraightLengthField: HTML, Sendable {
|
struct StraightLengthField: HTML, Sendable {
|
||||||
@@ -87,13 +183,17 @@ struct StraightLengthField: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some HTML<HTMLTag.div> {
|
var body: some HTML<HTMLTag.div> {
|
||||||
div(.class("pb-4")) {
|
Row {
|
||||||
Input(
|
Input(
|
||||||
name: "straightLengths[]",
|
name: "straightLengths",
|
||||||
placeholder: "Length"
|
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 {
|
var body: some HTML {
|
||||||
Row {
|
Row {
|
||||||
// Input(name: "group[][group]", placeholder: "Group")
|
|
||||||
// .attributes(.type(.number), .min("0"))
|
|
||||||
GroupSelect(style: style)
|
GroupSelect(style: style)
|
||||||
Input(name: "group[][letter]", placeholder: "Letter")
|
Input(name: "group[letter]", placeholder: "Letter")
|
||||||
.attributes(.type(.text))
|
.attributes(.type(.text), .autofocus, .required)
|
||||||
Input(name: "group[][length]", placeholder: "Length")
|
Input(name: "group[length]", placeholder: "Length")
|
||||||
.attributes(.type(.number), .min("0"))
|
.attributes(.type(.number), .min("0"), .required)
|
||||||
Input(name: "group[][quantity]", placeholder: "Quantity")
|
Input(name: "group[quantity]", placeholder: "Quantity")
|
||||||
.attributes(.type(.number), .min("1"), .value("1"))
|
.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 {
|
var body: some HTML {
|
||||||
select(
|
select(
|
||||||
.name("group")
|
.name("group[group]"),
|
||||||
|
.class("select")
|
||||||
) {
|
) {
|
||||||
for value in style.selectOptions {
|
for value in style.selectOptions {
|
||||||
option(.value("\(value)")) { "\(value)" }
|
option(.value("\(value)")) { "\(value)" }
|
||||||
@@ -135,17 +236,14 @@ struct GroupSelect: HTML, Sendable {
|
|||||||
|
|
||||||
struct GroupTypeSelect: HTML, Sendable {
|
struct GroupTypeSelect: HTML, Sendable {
|
||||||
|
|
||||||
var selected: EffectiveLength.EffectiveLengthType
|
let projectID: Project.ID
|
||||||
|
let selected: EffectiveLength.EffectiveLengthType
|
||||||
|
|
||||||
var body: some HTML<HTMLTag.select> {
|
var body: some HTML<HTMLTag.select> {
|
||||||
select(.name("type"), .id("type")) {
|
select(.class("select"), .name("type"), .id("type")) {
|
||||||
for value in EffectiveLength.EffectiveLengthType.allCases {
|
for value in EffectiveLength.EffectiveLengthType.allCases {
|
||||||
option(
|
option(
|
||||||
.value("\(value.rawValue)"),
|
.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 }
|
) { value.title }
|
||||||
.attributes(.selected, when: value == selected)
|
.attributes(.selected, when: value == selected)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Styleguide
|
|||||||
|
|
||||||
struct EffectiveLengthsView: HTML, Sendable {
|
struct EffectiveLengthsView: HTML, Sendable {
|
||||||
|
|
||||||
|
let projectID: Project.ID
|
||||||
let effectiveLengths: [EffectiveLength]
|
let effectiveLengths: [EffectiveLength]
|
||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
@@ -12,20 +13,9 @@ struct EffectiveLengthsView: HTML, Sendable {
|
|||||||
.class("m-4")
|
.class("m-4")
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
h1(.class("text-2xl font-bold")) { "Effective Lengths" }
|
h1(.class("text-2xl font-bold")) { "Equivalent Lengths" }
|
||||||
PlusButton()
|
PlusButton()
|
||||||
.attributes(
|
.attributes(.showModal(id: EffectiveLengthForm.id))
|
||||||
.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(.class("pb-6"))
|
.attributes(.class("pb-6"))
|
||||||
|
|
||||||
@@ -38,7 +28,7 @@ struct EffectiveLengthsView: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EffectiveLengthForm(dismiss: true)
|
EffectiveLengthForm(projectID: projectID, dismiss: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable
|
|||||||
script(.src("/js/main.js")) {}
|
script(.src("/js/main.js")) {}
|
||||||
link(.rel(.stylesheet), .href("/css/output.css"))
|
link(.rel(.stylesheet), .href("/css/output.css"))
|
||||||
link(.rel(.icon), .href("/images/favicon.ico"), .custom(name: "type", value: "image/x-icon"))
|
link(.rel(.icon), .href("/images/favicon.ico"), .custom(name: "type", value: "image/x-icon"))
|
||||||
|
HTMLRaw(
|
||||||
|
"""
|
||||||
|
<script src="https://unpkg.com/htmx-remove@latest"
|
||||||
|
integrity="sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT"
|
||||||
|
crossorigin="anonymous">
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some HTML {
|
public var body: some HTML {
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ struct ProjectView: HTML, Sendable {
|
|||||||
sensibleHeatRatio: database.projects.getSensibleHeatRatio(projectID)
|
sensibleHeatRatio: database.projects.getSensibleHeatRatio(projectID)
|
||||||
)
|
)
|
||||||
|
|
||||||
case .effectiveLength:
|
case .equivalentLength:
|
||||||
try await EffectiveLengthsView(
|
try await EffectiveLengthsView(
|
||||||
|
projectID: projectID,
|
||||||
effectiveLengths: database.effectiveLength.fetch(projectID)
|
effectiveLengths: database.effectiveLength.fetch(projectID)
|
||||||
)
|
)
|
||||||
case .frictionRate:
|
case .frictionRate:
|
||||||
@@ -103,11 +104,19 @@ struct Sidebar: HTML {
|
|||||||
)
|
)
|
||||||
.attributes(.data("active", value: active == .project ? "true" : "false"))
|
.attributes(.data("active", value: active == .project ? "true" : "false"))
|
||||||
|
|
||||||
row(title: "Rooms", icon: .doorClosed, route: .project(.detail(projectID, .rooms(.index))))
|
row(
|
||||||
|
title: "Rooms",
|
||||||
|
icon: .doorClosed,
|
||||||
|
route: .project(.detail(projectID, .rooms(.index)))
|
||||||
|
)
|
||||||
.attributes(.data("active", value: active == .rooms ? "true" : "false"))
|
.attributes(.data("active", value: active == .rooms ? "true" : "false"))
|
||||||
|
|
||||||
row(title: "Equivalent Lengths", icon: .rulerDimensionLine, route: .effectiveLength(.index))
|
row(
|
||||||
.attributes(.data("active", value: active == .effectiveLength ? "true" : "false"))
|
title: "Equivalent Lengths",
|
||||||
|
icon: .rulerDimensionLine,
|
||||||
|
route: .project(.detail(projectID, .equivalentLength(.index)))
|
||||||
|
)
|
||||||
|
.attributes(.data("active", value: active == .equivalentLength ? "true" : "false"))
|
||||||
|
|
||||||
row(
|
row(
|
||||||
title: "Friction Rate",
|
title: "Friction Rate",
|
||||||
|
|||||||
@@ -72,4 +72,34 @@ struct ProjectRouteTests {
|
|||||||
let route = try router.parse(&request)
|
let route = try router.parse(&request)
|
||||||
#expect(route == .project(.index))
|
#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])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user