feat: Updates to using ResultView to handle errors.

This commit is contained in:
2026-01-10 20:14:59 -05:00
parent 20065ebf10
commit 1446540109
3 changed files with 250 additions and 124 deletions

View File

@@ -54,6 +54,17 @@ extension ResultView {
Styleguide.ErrorView(error: error) Styleguide.ErrorView(error: error)
} }
} }
public init(
catching: @escaping @Sendable () async throws(E) -> V,
) async where ErrorView == Styleguide.ErrorView<E>, V == Void, ValueView == EmptyHTML {
await self.init(
result: .init(catching: catching),
onSuccess: { EmptyHTML() }
) { error in
Styleguide.ErrorView(error: error)
}
}
} }
extension ResultView: Sendable where Error: Sendable, ValueView: Sendable, ErrorView: Sendable {} extension ResultView: Sendable where Error: Sendable, ValueView: Sendable, ErrorView: Sendable {}

View File

@@ -55,7 +55,7 @@ extension ViewController: DependencyKey {
// FIX: Fix. // FIX: Fix.
public static let liveValue = Self( public static let liveValue = Self(
view: { request in view: { request in
try await request.render() await request.render()
} }
) )
} }

View File

@@ -3,10 +3,11 @@ import Dependencies
import Elementary import Elementary
import Foundation import Foundation
import ManualDCore import ManualDCore
import Styleguide
extension ViewController.Request { extension ViewController.Request {
func render() async throws -> AnySendableHTML { func render() async -> AnySendableHTML {
@Dependency(\.database) var database @Dependency(\.database) var database
@@ -22,9 +23,13 @@ extension ViewController.Request {
LoginForm(next: next) LoginForm(next: next)
} }
case .submit(let login): case .submit(let login):
let _ = try await authenticate(login) // let _ = try await authenticate(login)
return view { return await view {
LoggedIn(next: login.next) await ResultView {
try await authenticate(login)
} onSuccess: { _ in
LoggedIn(next: login.next)
}
} }
} }
case .signup(let route): case .signup(let route):
@@ -35,14 +40,20 @@ extension ViewController.Request {
} }
case .submit(let request): case .submit(let request):
// Create a new user and log them in. // Create a new user and log them in.
let user = try await createAndAuthenticate(request) return await view {
let projects = try await database.projects.fetch(user.id, .init(page: 1, per: 25)) await ResultView {
return view { let user = try await createAndAuthenticate(request)
ProjectsTable(userID: user.id, projects: projects) return (
user.id,
try await database.projects.fetch(user.id, .init(page: 1, per: 25))
)
} onSuccess: { (userID, projects) in
ProjectsTable(userID: userID, projects: projects)
}
} }
} }
case .project(let route): case .project(let route):
return try await route.renderView(on: self) return await route.renderView(on: self)
default: default:
// FIX: FIX // FIX: FIX
return _render(isHtmxRequest: false) { return _render(isHtmxRequest: false) {
@@ -59,6 +70,14 @@ extension ViewController.Request {
} }
} }
func view<C: HTML>(
@HTMLBuilder inner: () async -> C
) async -> AnySendableHTML where C: Sendable {
await _render(isHtmxRequest: isHtmxRequest, showSidebar: showSidebar) {
await inner()
}
}
var showSidebar: Bool { var showSidebar: Bool {
switch route { switch route {
case .login, .signup, .project(.page): case .login, .signup, .project(.page):
@@ -71,43 +90,70 @@ extension ViewController.Request {
extension SiteRoute.View.ProjectRoute { extension SiteRoute.View.ProjectRoute {
func renderView(on request: ViewController.Request) async throws -> AnySendableHTML { func renderView(on request: ViewController.Request) async -> AnySendableHTML {
@Dependency(\.database) var database @Dependency(\.database) var database
let user = try request.currentUser() // let user = try request.currentUser()
switch self { switch self {
case .index: case .index:
let projects = try await database.projects.fetchPage(userID: user.id) return await request.view {
return request.view { await ResultView {
ProjectsTable(userID: user.id, projects: projects) let user = try request.currentUser()
return try await (
user.id,
database.projects.fetchPage(userID: user.id)
)
} onSuccess: { (userID, projects) in
ProjectsTable(userID: userID, projects: projects)
}
} }
case .page(let page): case .page(let page):
let projects = try await database.projects.fetch(user.id, page) return await ResultView {
return ProjectsTable(userID: user.id, projects: projects) let user = try request.currentUser()
return try await (
user.id,
database.projects.fetch(user.id, page)
)
} onSuccess: { (userID, projects) in
ProjectsTable(userID: userID, projects: projects)
}
case .form(let id, let dismiss): case .form(let id, let dismiss):
request.logger.debug("Project form: \(id != nil ? "Fetching project for: \(id!)" : "N/A")") return await ResultView {
var project: Project? = nil var project: Project? = nil
if let id, dismiss == false { if let id, dismiss == false {
project = try await database.projects.get(id) project = try await database.projects.get(id)
}
return project
} onSuccess: { project in
ProjectForm(dismiss: dismiss, project: project)
} }
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) return await ResultView {
try await database.componentLoss.createDefaults(projectID: project.id) let user = try request.currentUser()
return ProjectView(projectID: project.id, activeTab: .rooms) let project = try await database.projects.create(user.id, form)
try await database.componentLoss.createDefaults(projectID: project.id)
return project.id
} onSuccess: { projectID in
ProjectView(projectID: projectID, activeTab: .rooms)
}
case .delete(let id): case .delete(let id):
try await database.projects.delete(id) return await ResultView {
return EmptyHTML() try await database.projects.delete(id)
} onSuccess: {
EmptyHTML()
}
case .update(let id, let form): case .update(let id, let form):
let project = try await database.projects.update(id, form) return await ResultView {
return ProjectView(projectID: project.id, activeTab: .project) try await database.projects.update(id, form).id
} onSuccess: { projectID in
return ProjectView(projectID: projectID, activeTab: .project)
}
case .detail(let projectID, let route): case .detail(let projectID, let route):
switch route { switch route {
@@ -116,17 +162,17 @@ extension SiteRoute.View.ProjectRoute {
ProjectView(projectID: projectID, activeTab: tab) ProjectView(projectID: projectID, activeTab: tab)
} }
case .componentLoss(let route): case .componentLoss(let route):
return try await route.renderView(on: request, projectID: projectID) return await route.renderView(on: request, projectID: projectID)
case .ductSizing(let route): case .ductSizing(let route):
return try await route.renderView(on: request, projectID: projectID) return await route.renderView(on: request, projectID: projectID)
case .equipment(let route): case .equipment(let route):
return try await route.renderView(on: request, projectID: projectID) return await route.renderView(on: request, projectID: projectID)
case .equivalentLength(let route): case .equivalentLength(let route):
return try await route.renderView(on: request, projectID: projectID) return 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 route.renderView(on: request, projectID: projectID)
case .rooms(let route): case .rooms(let route):
return try await route.renderView(on: request, projectID: projectID) return await route.renderView(on: request, projectID: projectID)
} }
} }
@@ -138,22 +184,34 @@ extension SiteRoute.View.ProjectRoute.EquipmentInfoRoute {
func renderView( func renderView(
on request: ViewController.Request, on request: ViewController.Request,
projectID: Project.ID projectID: Project.ID
) async throws -> AnySendableHTML { ) async -> AnySendableHTML {
@Dependency(\.database) var database @Dependency(\.database) var database
switch self { switch self {
case .index: case .index:
let equipment = try await database.equipment.fetch(projectID) return await ResultView {
return EquipmentInfoView(equipmentInfo: equipment, projectID: projectID) try await database.equipment.fetch(projectID)
} onSuccess: { equipment in
EquipmentInfoView(equipmentInfo: equipment, projectID: projectID)
}
case .form(let dismiss): case .form(let dismiss):
let equipment = try await database.equipment.fetch(projectID) return await ResultView {
return EquipmentInfoForm(dismiss: dismiss, projectID: projectID, equipmentInfo: equipment) try await database.equipment.fetch(projectID)
} onSuccess: { equipment in
EquipmentInfoForm(dismiss: dismiss, projectID: projectID, equipmentInfo: equipment)
}
case .submit(let form): case .submit(let form):
let equipment = try await database.equipment.create(form) return await ResultView {
return EquipmentInfoView(equipmentInfo: equipment, projectID: projectID) try await database.equipment.create(form)
} onSuccess: { equipment in
EquipmentInfoView(equipmentInfo: equipment, projectID: projectID)
}
case .update(let id, let updates): case .update(let id, let updates):
let equipment = try await database.equipment.update(id, updates) return await ResultView {
return EquipmentInfoView(equipmentInfo: equipment, projectID: projectID) try await database.equipment.update(id, updates)
} onSuccess: { equipment in
EquipmentInfoView(equipmentInfo: equipment, projectID: projectID)
}
} }
} }
} }
@@ -162,21 +220,26 @@ extension SiteRoute.View.ProjectRoute.RoomRoute {
func renderView( func renderView(
on request: ViewController.Request, on request: ViewController.Request,
projectID: Project.ID projectID: Project.ID
) async throws -> AnySendableHTML { ) async -> AnySendableHTML {
@Dependency(\.database) var database @Dependency(\.database) var database
switch self { switch self {
case .delete(let id): case .delete(let id):
try await database.rooms.delete(id) return await ResultView {
return EmptyHTML() try await database.rooms.delete(id)
}
case .form(let id, let dismiss): case .form(let id, let dismiss):
var room: Room? = nil return await ResultView {
if let id, dismiss == false { var room: Room? = nil
room = try await database.rooms.get(id) if let id, dismiss == false {
room = try await database.rooms.get(id)
}
return room
} onSuccess: { room in
RoomForm(dismiss: dismiss, projectID: projectID, room: room)
} }
return RoomForm(dismiss: dismiss, projectID: projectID, room: room)
case .index: case .index:
return request.view { return request.view {
@@ -184,40 +247,46 @@ extension SiteRoute.View.ProjectRoute.RoomRoute {
} }
case .submit(let form): case .submit(let form):
request.logger.debug("New room form submitted.") return await request.view {
// FIX: Just return a room row?? await ResultView {
let _ = try await database.rooms.create(form) request.logger.debug("New room form submitted.")
return request.view { // FIX: Just return a room row??
ProjectView(projectID: projectID, activeTab: .rooms) let _ = try await database.rooms.create(form)
} onSuccess: {
ProjectView(projectID: projectID, activeTab: .rooms)
}
} }
case .update(let id, let form): case .update(let id, let form):
let _ = try await database.rooms.update(id, form) return await ResultView {
return ProjectView(projectID: projectID, activeTab: .rooms) let _ = try await database.rooms.update(id, form)
} onSuccess: {
ProjectView(projectID: projectID, activeTab: .rooms)
}
case .updateSensibleHeatRatio(let form): case .updateSensibleHeatRatio(let form):
let _ = try await database.projects.update( return await request.view {
form.projectID, await ResultView {
.init(sensibleHeatRatio: form.sensibleHeatRatio) let _ = try await database.projects.update(
) form.projectID,
return request.view { .init(sensibleHeatRatio: form.sensibleHeatRatio)
ProjectView(projectID: projectID, activeTab: .rooms) )
} onSuccess: {
ProjectView(projectID: projectID, activeTab: .rooms)
}
} }
} }
} }
} }
extension SiteRoute.View.ProjectRoute.FrictionRateRoute { extension SiteRoute.View.ProjectRoute.FrictionRateRoute {
func renderView(on request: ViewController.Request, projectID: Project.ID) async throws func renderView(
-> AnySendableHTML on request: ViewController.Request,
{ projectID: Project.ID
@Dependency(\.database) var database ) -> AnySendableHTML {
switch self { switch self {
case .index: case .index:
// let equipment = try await database.equipment.fetch(projectID)
// let componentLosses = try await database.componentLoss.fetch(projectID)
return request.view { return request.view {
ProjectView(projectID: projectID, activeTab: .frictionRate) ProjectView(projectID: projectID, activeTab: .frictionRate)
} }
@@ -240,21 +309,31 @@ extension SiteRoute.View.ProjectRoute.ComponentLossRoute {
func renderView( func renderView(
on request: ViewController.Request, on request: ViewController.Request,
projectID: Project.ID projectID: Project.ID
) async throws -> AnySendableHTML { ) async -> AnySendableHTML {
@Dependency(\.database) var database @Dependency(\.database) var database
switch self { switch self {
case .index: case .index:
return EmptyHTML() return EmptyHTML()
case .delete(let id): case .delete(let id):
_ = try await database.componentLoss.delete(id) return await ResultView {
return EmptyHTML() _ = try await database.componentLoss.delete(id)
} onSuccess: {
EmptyHTML()
}
// return EmptyHTML()
case .submit(let form): case .submit(let form):
_ = try await database.componentLoss.create(form) return await ResultView {
return ProjectView(projectID: projectID, activeTab: .frictionRate) _ = try await database.componentLoss.create(form)
} onSuccess: {
ProjectView(projectID: projectID, activeTab: .frictionRate)
}
case .update(let id, let form): case .update(let id, let form):
_ = try await database.componentLoss.update(id, form) return await ResultView {
return ProjectView(projectID: projectID, activeTab: .frictionRate) _ = try await database.componentLoss.update(id, form)
} onSuccess: {
ProjectView(projectID: projectID, activeTab: .frictionRate)
}
} }
} }
} }
@@ -275,14 +354,15 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
func renderView( func renderView(
on request: ViewController.Request, on request: ViewController.Request,
projectID: Project.ID projectID: Project.ID
) async throws -> AnySendableHTML { ) async -> AnySendableHTML {
@Dependency(\.database) var database @Dependency(\.database) var database
switch self { switch self {
case .delete(let id): case .delete(let id):
try await database.effectiveLength.delete(id) return await ResultView {
return EmptyHTML() try await database.effectiveLength.delete(id)
}
case .index: case .index:
return request.view { return request.view {
@@ -301,35 +381,50 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
} }
case .update(let id, let form): case .update(let id, let form):
_ = try await database.effectiveLength.update(id, .init(form: form, projectID: projectID)) return await ResultView {
return ProjectView(projectID: projectID, activeTab: .equivalentLength) _ = try await database.effectiveLength.update(id, .init(form: form, projectID: projectID))
} onSuccess: {
ProjectView(projectID: projectID, activeTab: .equivalentLength)
}
case .submit(let step): case .submit(let step):
switch step { switch step {
case .one(let stepOne): case .one(let stepOne):
var effectiveLength: EffectiveLength? = nil return await ResultView {
if let id = stepOne.id { var effectiveLength: EffectiveLength? = nil
effectiveLength = try await database.effectiveLength.get(id) if let id = stepOne.id {
effectiveLength = try await database.effectiveLength.get(id)
}
return effectiveLength
} onSuccess: { effectiveLength in
EffectiveLengthForm.StepTwo(
projectID: projectID,
stepOne: stepOne,
effectiveLength: effectiveLength
)
} }
return EffectiveLengthForm.StepTwo(
projectID: projectID,
stepOne: stepOne,
effectiveLength: effectiveLength
)
case .two(let stepTwo): case .two(let stepTwo):
request.logger.debug("ViewController: Got step two...") return await ResultView {
var effectiveLength: EffectiveLength? = nil request.logger.debug("ViewController: Got step two...")
if let id = stepTwo.id { var effectiveLength: EffectiveLength? = nil
effectiveLength = try await database.effectiveLength.get(id) if let id = stepTwo.id {
effectiveLength = try await database.effectiveLength.get(id)
}
return effectiveLength
} onSuccess: { effectiveLength in
return EffectiveLengthForm.StepThree(
projectID: projectID, effectiveLength: effectiveLength, stepTwo: stepTwo
)
} }
return EffectiveLengthForm.StepThree(
projectID: projectID, effectiveLength: effectiveLength, stepTwo: stepTwo
)
case .three(let stepThree): case .three(let stepThree):
request.logger.debug("ViewController: Got step three: \(stepThree)") return await ResultView {
try stepThree.validate() request.logger.debug("ViewController: Got step three: \(stepThree)")
_ = try await database.effectiveLength.create(.init(form: stepThree, projectID: projectID)) try stepThree.validate()
return ProjectView(projectID: projectID, activeTab: .equivalentLength) _ = try await database.effectiveLength.create(
.init(form: stepThree, projectID: projectID))
} onSuccess: {
ProjectView(projectID: projectID, activeTab: .equivalentLength)
}
} }
} }
@@ -339,9 +434,10 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
extension SiteRoute.View.ProjectRoute.DuctSizingRoute { extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
func renderView(on request: ViewController.Request, projectID: Project.ID) async throws func renderView(
-> AnySendableHTML on request: ViewController.Request,
{ projectID: Project.ID
) async -> AnySendableHTML {
@Dependency(\.database) var database @Dependency(\.database) var database
@Dependency(\.manualD) var manualD @Dependency(\.manualD) var manualD
@@ -352,25 +448,31 @@ extension SiteRoute.View.ProjectRoute.DuctSizingRoute {
} }
case .deleteRectangularSize(let roomID, let rectangularSizeID): case .deleteRectangularSize(let roomID, let rectangularSizeID):
let room = try await database.rooms.deleteRectangularSize(roomID, rectangularSizeID) return await ResultView {
let container = try await database.calculateDuctSizes(projectID: projectID) let room = try await database.rooms.deleteRectangularSize(roomID, rectangularSizeID)
.filter({ $0.roomID == room.id }) return try await database.calculateDuctSizes(projectID: projectID)
.first! .filter({ $0.roomID == room.id })
return DuctSizingView.RoomRow(projectID: projectID, room: container) .first!
} onSuccess: { container in
DuctSizingView.RoomRow(projectID: projectID, room: container)
}
case .roomRectangularForm(let roomID, let form): case .roomRectangularForm(let roomID, let form):
let room = try await database.rooms.update( return await ResultView {
roomID, let room = try await database.rooms.update(
.init( roomID,
rectangularSizes: [ .init(
.init(id: form.id ?? .init(), register: form.register, height: form.height) rectangularSizes: [
] .init(id: form.id ?? .init(), register: form.register, height: form.height)
]
)
) )
) return try await database.calculateDuctSizes(projectID: projectID)
let container = try await database.calculateDuctSizes(projectID: projectID) .filter({ $0.roomID == room.id })
.filter({ $0.roomID == room.id }) .first!
.first! } onSuccess: { container in
return DuctSizingView.RoomRow(projectID: projectID, room: container) DuctSizingView.RoomRow(projectID: projectID, room: container)
}
} }
} }
} }
@@ -388,6 +490,19 @@ private func _render<C: HTML>(
return MainPage { inner } return MainPage { inner }
} }
private func _render<C: HTML>(
isHtmxRequest: Bool,
active activeTab: SiteRoute.View.ProjectRoute.DetailRoute.Tab = .rooms,
showSidebar: Bool = true,
@HTMLBuilder inner: () async -> C
) async -> AnySendableHTML where C: Sendable {
let inner = await inner()
if isHtmxRequest {
return inner
}
return MainPage { inner }
}
private func _render<C: HTML>( private func _render<C: HTML>(
isHtmxRequest: Bool, isHtmxRequest: Bool,
active activeTab: SiteRoute.View.ProjectRoute.DetailRoute.Tab = .rooms, active activeTab: SiteRoute.View.ProjectRoute.DetailRoute.Tab = .rooms,