feat: Removes old tests, fixes authentication middleware not working, view routes updated to not have delete routes and uses api routes for delete methods.
This commit is contained in:
@@ -54,18 +54,18 @@ let package = Package(
|
|||||||
],
|
],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings
|
||||||
),
|
),
|
||||||
.testTarget(
|
// .testTarget(
|
||||||
name: "AppTests",
|
// name: "AppTests",
|
||||||
dependencies: [
|
// dependencies: [
|
||||||
.target(name: "App"),
|
// .target(name: "App"),
|
||||||
.target(name: "HtmlSnapshotTesting"),
|
// .target(name: "HtmlSnapshotTesting"),
|
||||||
.product(name: "XCTVapor", package: "vapor")
|
// .product(name: "XCTVapor", package: "vapor")
|
||||||
],
|
// ],
|
||||||
resources: [
|
// resources: [
|
||||||
.copy("__Snapshots__")
|
// .copy("__Snapshots__")
|
||||||
],
|
// ],
|
||||||
swiftSettings: swiftSettings
|
// swiftSettings: swiftSettings
|
||||||
),
|
// ),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "ViewRouteTests",
|
name: "ViewRouteTests",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
@@ -115,13 +115,6 @@ let package = Package(
|
|||||||
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
|
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
|
||||||
name: "HtmlSnapshotTestingTests",
|
|
||||||
dependencies: [
|
|
||||||
.target(name: "App"),
|
|
||||||
.target(name: "HtmlSnapshotTesting")
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.target(
|
.target(
|
||||||
name: "SharedModels",
|
name: "SharedModels",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import Vapor
|
|||||||
private let apiMiddleware: [any Middleware] = [
|
private let apiMiddleware: [any Middleware] = [
|
||||||
UserPasswordAuthenticator(),
|
UserPasswordAuthenticator(),
|
||||||
UserTokenAuthenticator(),
|
UserTokenAuthenticator(),
|
||||||
|
UserSessionAuthenticator(),
|
||||||
User.guardMiddleware()
|
User.guardMiddleware()
|
||||||
]
|
]
|
||||||
|
|
||||||
extension ApiRoute {
|
extension ApiRoute {
|
||||||
var middleware: [any Middleware]? { apiMiddleware }
|
var middleware: [any Middleware]? { apiMiddleware }
|
||||||
|
|
||||||
func handle(request: Request) async throws -> any AsyncResponseEncodable {
|
func respond(request: Request) async throws -> any AsyncResponseEncodable {
|
||||||
switch self {
|
switch self {
|
||||||
case let .employee(route):
|
case let .employee(route):
|
||||||
return try await route.handleApiRequest(request: request)
|
return try await route.handleApiRequest(request: request)
|
||||||
|
|||||||
53
Sources/App/Extensions/ViewController+respond.swift
Normal file
53
Sources/App/Extensions/ViewController+respond.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import Elementary
|
||||||
|
import SharedModels
|
||||||
|
import Vapor
|
||||||
|
import VaporElementary
|
||||||
|
import ViewController
|
||||||
|
|
||||||
|
extension ViewController {
|
||||||
|
func respond(route: ViewRoute, request: Vapor.Request) async throws -> any AsyncResponseEncodable {
|
||||||
|
let html = try await view(
|
||||||
|
for: route,
|
||||||
|
isHtmxRequest: request.isHtmxRequest,
|
||||||
|
logger: request.logger,
|
||||||
|
authenticate: { request.session.authenticate($0) }
|
||||||
|
)
|
||||||
|
return AnyHTMLResponse(value: html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-adapted from `HTMLResponse` in the VaporElementary package to work with any html types
|
||||||
|
// returned from the view controller.
|
||||||
|
struct AnyHTMLResponse: AsyncResponseEncodable {
|
||||||
|
|
||||||
|
public var chunkSize: Int
|
||||||
|
public var headers: HTTPHeaders = ["Content-Type": "text/html; charset=utf-8"]
|
||||||
|
var value: _SendableAnyHTMLBox
|
||||||
|
|
||||||
|
init(chunkSize: Int = 1024, additionalHeaders: HTTPHeaders = [:], value: AnySendableHTML) {
|
||||||
|
self.chunkSize = chunkSize
|
||||||
|
if additionalHeaders.contains(name: .contentType) {
|
||||||
|
self.headers = additionalHeaders
|
||||||
|
} else {
|
||||||
|
headers.add(contentsOf: additionalHeaders)
|
||||||
|
}
|
||||||
|
self.value = .init(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeResponse(for request: Request) async throws -> Response {
|
||||||
|
Response(
|
||||||
|
status: .ok,
|
||||||
|
headers: headers,
|
||||||
|
body: .init(asyncStream: { [value, chunkSize] writer in
|
||||||
|
guard let html = value.tryTake() else {
|
||||||
|
assertionFailure("Non-sendable HTML value consumed more than once")
|
||||||
|
request.logger.error("Non-sendable HTML value consumed more than once")
|
||||||
|
throw Abort(.internalServerError)
|
||||||
|
}
|
||||||
|
try await writer.writeHTML(html, chunkSize: chunkSize)
|
||||||
|
try await writer.write(.end)
|
||||||
|
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,23 @@
|
|||||||
import DatabaseClientLive
|
import DatabaseClientLive
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import Vapor
|
import Vapor
|
||||||
|
import ViewControllerLive
|
||||||
|
|
||||||
// Taken from discussions page on `swift-dependencies`.
|
// Taken from discussions page on `swift-dependencies`.
|
||||||
|
|
||||||
// TODO: Pass dependencies to set into this middleware.
|
|
||||||
struct DependenciesMiddleware: AsyncMiddleware {
|
struct DependenciesMiddleware: AsyncMiddleware {
|
||||||
|
|
||||||
private let values: DependencyValues.Continuation
|
private let values: DependencyValues.Continuation
|
||||||
private let database: DatabaseClient
|
private let database: DatabaseClient
|
||||||
|
private let viewController: ViewController
|
||||||
|
|
||||||
init(
|
init(
|
||||||
database: DatabaseClient
|
database: DatabaseClient,
|
||||||
|
viewController: ViewController = .liveValue
|
||||||
) {
|
) {
|
||||||
self.values = withEscapedDependencies { $0 }
|
self.values = withEscapedDependencies { $0 }
|
||||||
self.database = database
|
self.database = database
|
||||||
|
self.viewController = viewController
|
||||||
}
|
}
|
||||||
|
|
||||||
func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
|
func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
|
||||||
@@ -22,6 +25,7 @@ struct DependenciesMiddleware: AsyncMiddleware {
|
|||||||
try await withDependencies {
|
try await withDependencies {
|
||||||
$0.database = database
|
$0.database = database
|
||||||
$0.dateFormatter = .liveValue
|
$0.dateFormatter = .liveValue
|
||||||
|
$0.viewController = viewController
|
||||||
} operation: {
|
} operation: {
|
||||||
try await next.respond(to: request)
|
try await next.respond(to: request)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ extension SharedModels.ViewRoute {
|
|||||||
|
|
||||||
var middleware: [any Middleware]? {
|
var middleware: [any Middleware]? {
|
||||||
switch self {
|
switch self {
|
||||||
case .index: return viewProtectedMiddleware
|
// case .index: return viewProtectedMiddleware
|
||||||
case let .employee(route): return route.middleware
|
case let .employee(route): return route.middleware
|
||||||
case .login: return nil
|
case .login: return nil
|
||||||
case let .purchaseOrder(route): return route.middleware
|
case let .purchaseOrder(route): return route.middleware
|
||||||
@@ -48,6 +48,11 @@ public func configure(
|
|||||||
|
|
||||||
app.middleware.use(DependenciesMiddleware(database: databaseClient))
|
app.middleware.use(DependenciesMiddleware(database: databaseClient))
|
||||||
|
|
||||||
|
// Redirect the index path to purchase order route.
|
||||||
|
app.get { req in
|
||||||
|
req.redirect(to: ViewRoute.router.path(for: .purchaseOrder(.index)))
|
||||||
|
}
|
||||||
|
|
||||||
app.mount(
|
app.mount(
|
||||||
SiteRoute.router,
|
SiteRoute.router,
|
||||||
middleware: {
|
middleware: {
|
||||||
@@ -79,7 +84,6 @@ extension SiteRoute {
|
|||||||
case .health:
|
case .health:
|
||||||
return nil
|
return nil
|
||||||
case let .view(route):
|
case let .view(route):
|
||||||
// return nil
|
|
||||||
return route.middleware
|
return route.middleware
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,62 +94,13 @@ func siteHandler(
|
|||||||
request: Request,
|
request: Request,
|
||||||
route: SiteRoute
|
route: SiteRoute
|
||||||
) async throws -> any AsyncResponseEncodable {
|
) async throws -> any AsyncResponseEncodable {
|
||||||
|
@Dependency(\.viewController) var viewController
|
||||||
switch route {
|
switch route {
|
||||||
case let .api(route):
|
case let .api(route):
|
||||||
return try await route.handle(request: request)
|
return try await route.respond(request: request)
|
||||||
case .health:
|
case .health:
|
||||||
return HTTPStatus.ok
|
return HTTPStatus.ok
|
||||||
case let .view(route):
|
case let .view(route):
|
||||||
return try await route.respond(request: request)
|
return try await viewController.respond(route: route, request: request)
|
||||||
// return try await route.handle(request: request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ViewRoute {
|
|
||||||
func respond(request: Request) async throws -> any AsyncResponseEncodable {
|
|
||||||
if self == .index {
|
|
||||||
return request.redirect(to: ViewRoute.router.path(for: .purchaseOrder(.index)))
|
|
||||||
} else {
|
|
||||||
let html = try await view(isHtmxRequest: request.isHtmxRequest, authenticate: { request.auth.login($0) })
|
|
||||||
// Delete routes return nil, but are valid routes.
|
|
||||||
guard let html else {
|
|
||||||
return HTTPStatus.ok
|
|
||||||
}
|
|
||||||
return AnyHTMLResponse(value: html)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AnyHTMLResponse: AsyncResponseEncodable {
|
|
||||||
|
|
||||||
public var chunkSize: Int
|
|
||||||
public var headers: HTTPHeaders = ["Content-Type": "text/html; charset=utf-8"]
|
|
||||||
var value: _SendableAnyHTMLBox
|
|
||||||
|
|
||||||
init(chunkSize: Int = 1024, additionalHeaders: HTTPHeaders = [:], value: any HTML & Sendable) {
|
|
||||||
self.chunkSize = chunkSize
|
|
||||||
if additionalHeaders.contains(name: .contentType) {
|
|
||||||
self.headers = additionalHeaders
|
|
||||||
} else {
|
|
||||||
headers.add(contentsOf: additionalHeaders)
|
|
||||||
}
|
|
||||||
self.value = .init(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeResponse(for request: Request) async throws -> Response {
|
|
||||||
Response(
|
|
||||||
status: .ok,
|
|
||||||
headers: headers,
|
|
||||||
body: .init(asyncStream: { [value, chunkSize] writer in
|
|
||||||
guard let html = value.tryTake() else {
|
|
||||||
assertionFailure("Non-sendable HTML value consumed more than once")
|
|
||||||
request.logger.error("Non-sendable HTML value consumed more than once")
|
|
||||||
throw Abort(.internalServerError)
|
|
||||||
}
|
|
||||||
try await writer.writeHTML(html, chunkSize: chunkSize)
|
|
||||||
try await writer.write(.end)
|
|
||||||
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
import Dependencies
|
|
||||||
import Elementary
|
|
||||||
import SharedModels
|
|
||||||
@_exported import ViewController
|
|
||||||
@@ -2,10 +2,9 @@ import CasePathsCore
|
|||||||
import Foundation
|
import Foundation
|
||||||
@preconcurrency import URLRouting
|
@preconcurrency import URLRouting
|
||||||
|
|
||||||
// TODO: Remove `delete` routes from views and use api routes.
|
|
||||||
public enum ViewRoute: Sendable, Equatable {
|
public enum ViewRoute: Sendable, Equatable {
|
||||||
|
|
||||||
case index
|
// case index
|
||||||
case employee(EmployeeRoute)
|
case employee(EmployeeRoute)
|
||||||
case login(LoginRoute)
|
case login(LoginRoute)
|
||||||
case purchaseOrder(PurchaseOrderRoute)
|
case purchaseOrder(PurchaseOrderRoute)
|
||||||
@@ -14,9 +13,9 @@ public enum ViewRoute: Sendable, Equatable {
|
|||||||
case vendorBranch(VendorBranchRoute)
|
case vendorBranch(VendorBranchRoute)
|
||||||
|
|
||||||
public static let router = OneOf {
|
public static let router = OneOf {
|
||||||
Route(.case(Self.index)) {
|
// Route(.case(Self.index)) {
|
||||||
Method.get
|
// Method.get
|
||||||
}
|
// }
|
||||||
Route(.case(Self.employee)) { EmployeeRoute.router }
|
Route(.case(Self.employee)) { EmployeeRoute.router }
|
||||||
Route(.case(Self.login)) { LoginRoute.router }
|
Route(.case(Self.login)) { LoginRoute.router }
|
||||||
Route(.case(Self.purchaseOrder)) { PurchaseOrderRoute.router }
|
Route(.case(Self.purchaseOrder)) { PurchaseOrderRoute.router }
|
||||||
@@ -31,7 +30,6 @@ public extension ViewRoute {
|
|||||||
|
|
||||||
enum EmployeeRoute: Sendable, Equatable {
|
enum EmployeeRoute: Sendable, Equatable {
|
||||||
case create(Employee.Create)
|
case create(Employee.Create)
|
||||||
case delete(id: Employee.ID)
|
|
||||||
case form
|
case form
|
||||||
case get(id: Employee.ID)
|
case get(id: Employee.ID)
|
||||||
case index
|
case index
|
||||||
@@ -53,10 +51,6 @@ public extension ViewRoute {
|
|||||||
.map(.memberwise(Employee.Create.init))
|
.map(.memberwise(Employee.Create.init))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Route(.case(Self.delete(id:))) {
|
|
||||||
Path { rootPath; Employee.ID.parser() }
|
|
||||||
Method.delete
|
|
||||||
}
|
|
||||||
Route(.case(Self.get(id:))) {
|
Route(.case(Self.get(id:))) {
|
||||||
Path { rootPath; Employee.ID.parser() }
|
Path { rootPath; Employee.ID.parser() }
|
||||||
Method.get
|
Method.get
|
||||||
@@ -147,7 +141,6 @@ public extension ViewRoute {
|
|||||||
public extension ViewRoute {
|
public extension ViewRoute {
|
||||||
enum PurchaseOrderRoute: Sendable, Equatable {
|
enum PurchaseOrderRoute: Sendable, Equatable {
|
||||||
case create(PurchaseOrder.Create)
|
case create(PurchaseOrder.Create)
|
||||||
case delete(id: PurchaseOrder.ID)
|
|
||||||
case form
|
case form
|
||||||
case get(id: PurchaseOrder.ID)
|
case get(id: PurchaseOrder.ID)
|
||||||
case index
|
case index
|
||||||
@@ -174,11 +167,6 @@ public extension ViewRoute {
|
|||||||
.map(.memberwise(PurchaseOrder.Create.init))
|
.map(.memberwise(PurchaseOrder.Create.init))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Route(.case(Self.delete(id:))) {
|
|
||||||
Path { rootPath; Digits() }
|
|
||||||
Method.delete
|
|
||||||
}
|
|
||||||
|
|
||||||
Route(.case(Self.form)) {
|
Route(.case(Self.form)) {
|
||||||
Path { rootPath; "create" }
|
Path { rootPath; "create" }
|
||||||
Method.get
|
Method.get
|
||||||
@@ -275,7 +263,6 @@ public extension ViewRoute {
|
|||||||
public extension ViewRoute {
|
public extension ViewRoute {
|
||||||
enum UserRoute: Sendable, Equatable {
|
enum UserRoute: Sendable, Equatable {
|
||||||
case create(User.Create)
|
case create(User.Create)
|
||||||
case delete(id: User.ID)
|
|
||||||
case form
|
case form
|
||||||
case get(id: User.ID)
|
case get(id: User.ID)
|
||||||
case index
|
case index
|
||||||
@@ -297,10 +284,6 @@ public extension ViewRoute {
|
|||||||
.map(.memberwise(User.Create.init))
|
.map(.memberwise(User.Create.init))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Route(.case(Self.delete(id:))) {
|
|
||||||
Path { rootPath; User.ID.parser() }
|
|
||||||
Method.delete
|
|
||||||
}
|
|
||||||
Route(.case(Self.form)) {
|
Route(.case(Self.form)) {
|
||||||
Path { rootPath; "create" }
|
Path { rootPath; "create" }
|
||||||
Method.get
|
Method.get
|
||||||
@@ -335,7 +318,6 @@ public extension ViewRoute {
|
|||||||
|
|
||||||
enum VendorRoute: Sendable, Equatable {
|
enum VendorRoute: Sendable, Equatable {
|
||||||
case create(Vendor.Create)
|
case create(Vendor.Create)
|
||||||
case delete(id: Vendor.ID)
|
|
||||||
case form
|
case form
|
||||||
case get(id: Vendor.ID)
|
case get(id: Vendor.ID)
|
||||||
case index
|
case index
|
||||||
@@ -354,10 +336,6 @@ public extension ViewRoute {
|
|||||||
.map(.memberwise(Vendor.Create.init))
|
.map(.memberwise(Vendor.Create.init))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Route(.case(Self.delete(id:))) {
|
|
||||||
Path { rootPath; Vendor.ID.parser() }
|
|
||||||
Method.delete
|
|
||||||
}
|
|
||||||
Route(.case(Self.get(id:))) {
|
Route(.case(Self.get(id:))) {
|
||||||
Path { rootPath; Vendor.ID.parser() }
|
Path { rootPath; Vendor.ID.parser() }
|
||||||
Method.get
|
Method.get
|
||||||
@@ -389,7 +367,6 @@ public extension ViewRoute {
|
|||||||
|
|
||||||
enum VendorBranchRoute: Sendable, Equatable {
|
enum VendorBranchRoute: Sendable, Equatable {
|
||||||
case create(VendorBranch.Create)
|
case create(VendorBranch.Create)
|
||||||
case delete(id: VendorBranch.ID)
|
|
||||||
case index(for: Vendor.ID? = nil)
|
case index(for: Vendor.ID? = nil)
|
||||||
case select(context: ViewRoute.SelectContext)
|
case select(context: ViewRoute.SelectContext)
|
||||||
|
|
||||||
@@ -405,10 +382,6 @@ public extension ViewRoute {
|
|||||||
.map(.memberwise(VendorBranch.Create.init))
|
.map(.memberwise(VendorBranch.Create.init))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Route(.case(Self.delete(id:))) {
|
|
||||||
Path { "vendors"; "branches"; VendorBranch.ID.parser() }
|
|
||||||
Method.delete
|
|
||||||
}
|
|
||||||
Route(.case(Self.index(for:))) {
|
Route(.case(Self.index(for:))) {
|
||||||
Path { "vendors"; "branches" }
|
Path { "vendors"; "branches" }
|
||||||
Method.get
|
Method.get
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Dependencies
|
import Dependencies
|
||||||
import DependenciesMacros
|
import DependenciesMacros
|
||||||
import Elementary
|
import Elementary
|
||||||
|
import Logging
|
||||||
import SharedModels
|
import SharedModels
|
||||||
|
|
||||||
public extension DependencyValues {
|
public extension DependencyValues {
|
||||||
@@ -10,19 +11,41 @@ public extension DependencyValues {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public typealias AnySendableHTML = (any HTML & Sendable)
|
||||||
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct ViewController: Sendable {
|
public struct ViewController: Sendable {
|
||||||
public typealias AuthenticateHandler = @Sendable (User) -> Void
|
public typealias AuthenticateHandler = @Sendable (User) -> Void
|
||||||
|
|
||||||
public var view: @Sendable (ViewRoute, Bool, @escaping AuthenticateHandler) async throws -> (any HTML)?
|
public var view: @Sendable (Request) async throws -> AnySendableHTML
|
||||||
|
|
||||||
@Sendable
|
@Sendable
|
||||||
public func view(
|
public func view(
|
||||||
for route: ViewRoute,
|
for route: ViewRoute,
|
||||||
isHtmxRequest: Bool,
|
isHtmxRequest: Bool,
|
||||||
|
logger: Logger,
|
||||||
authenticate: @escaping AuthenticateHandler
|
authenticate: @escaping AuthenticateHandler
|
||||||
) async throws -> (any HTML)? {
|
) async throws -> AnySendableHTML {
|
||||||
try await view(route, isHtmxRequest, authenticate)
|
try await view(.init(route, isHtmxRequest: isHtmxRequest, authenticate: authenticate, logger: logger))
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Request: Sendable {
|
||||||
|
public let route: ViewRoute
|
||||||
|
public let isHtmxRequest: Bool
|
||||||
|
public let authenticate: AuthenticateHandler
|
||||||
|
public let logger: Logger
|
||||||
|
|
||||||
|
public init(
|
||||||
|
_ route: ViewRoute,
|
||||||
|
isHtmxRequest: Bool,
|
||||||
|
authenticate: @escaping AuthenticateHandler,
|
||||||
|
logger: Logger
|
||||||
|
) {
|
||||||
|
self.route = route
|
||||||
|
self.isHtmxRequest = isHtmxRequest
|
||||||
|
self.authenticate = authenticate
|
||||||
|
self.logger = logger
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ import SharedModels
|
|||||||
|
|
||||||
extension ViewController: DependencyKey {
|
extension ViewController: DependencyKey {
|
||||||
public static var liveValue: ViewController {
|
public static var liveValue: ViewController {
|
||||||
.init(view: { route, isHtmxRequest, authenticate in
|
.init(view: { request in
|
||||||
try await route.view(isHtmxRequest: isHtmxRequest, authenticate: authenticate)
|
try await request.route.view(
|
||||||
|
isHtmxRequest: request.isHtmxRequest,
|
||||||
|
logger: request.logger,
|
||||||
|
authenticate: request.authenticate
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import DatabaseClient
|
import DatabaseClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import Elementary
|
import Elementary
|
||||||
|
import Logging
|
||||||
import SharedModels
|
import SharedModels
|
||||||
import Vapor
|
import Vapor
|
||||||
|
import ViewController
|
||||||
public typealias AnySendableHTML = (any HTML & Sendable)
|
|
||||||
|
|
||||||
public extension SharedModels.ViewRoute {
|
public extension SharedModels.ViewRoute {
|
||||||
|
|
||||||
func view(
|
func view(
|
||||||
isHtmxRequest: Bool,
|
isHtmxRequest: Bool,
|
||||||
|
logger: Logger,
|
||||||
authenticate: @escaping @Sendable (User) -> Void
|
authenticate: @escaping @Sendable (User) -> Void
|
||||||
) async throws -> AnySendableHTML? {
|
) async throws -> AnySendableHTML {
|
||||||
@Dependency(\.database.users) var users
|
@Dependency(\.database.users) var users
|
||||||
switch self {
|
switch self {
|
||||||
case .index:
|
// case .index:
|
||||||
// return request.redirect(to: Self.router.path(for: .purchaseOrder(.index)))
|
// // This get's redirected to purchase-orders route in the app / site handler.
|
||||||
return nil
|
// return nil
|
||||||
|
|
||||||
case let .employee(route):
|
case let .employee(route):
|
||||||
return try await route.view(isHtmxRequest: isHtmxRequest)
|
return try await route.view(isHtmxRequest: isHtmxRequest)
|
||||||
@@ -31,7 +32,7 @@ public extension SharedModels.ViewRoute {
|
|||||||
let token = try await users.login(.init(username: login.username, password: login.password))
|
let token = try await users.login(.init(username: login.username, password: login.password))
|
||||||
let user = try await users.get(token.userID)!
|
let user = try await users.get(token.userID)!
|
||||||
authenticate(user)
|
authenticate(user)
|
||||||
// request.logger.info("Logged in next: \(login.next ?? "N/A")")
|
logger.info("Logged in next: \(login.next ?? "N/A")")
|
||||||
return MainPage.loggedIn(next: login.next)
|
return MainPage.loggedIn(next: login.next)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ extension SharedModels.ViewRoute.EmployeeRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML? {
|
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||||
@Dependency(\.database.employees) var employees
|
@Dependency(\.database.employees) var employees
|
||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
@@ -83,17 +84,10 @@ extension SharedModels.ViewRoute.EmployeeRoute {
|
|||||||
throw Abort(.badRequest, reason: "Employee id not found.")
|
throw Abort(.badRequest, reason: "Employee id not found.")
|
||||||
}
|
}
|
||||||
return try await render(mainPage, isHtmxRequest, EmployeeForm(employee: employee))
|
return try await render(mainPage, isHtmxRequest, EmployeeForm(employee: employee))
|
||||||
// return try await request.render(mainPage: mainPage) {
|
|
||||||
// EmployeeForm(employee: employee)
|
|
||||||
// }
|
|
||||||
|
|
||||||
case let .create(employee):
|
case let .create(employee):
|
||||||
return try await EmployeeTable.Row(employee: employees.create(employee))
|
return try await EmployeeTable.Row(employee: employees.create(employee))
|
||||||
|
|
||||||
case let .delete(id: id):
|
|
||||||
try await employees.delete(id)
|
|
||||||
return nil
|
|
||||||
|
|
||||||
case let .update(id: id, updates: updates):
|
case let .update(id: id, updates: updates):
|
||||||
return try await EmployeeTable.Row(employee: employees.update(id, updates))
|
return try await EmployeeTable.Row(employee: employees.update(id, updates))
|
||||||
}
|
}
|
||||||
@@ -115,7 +109,7 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML? {
|
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||||
@Dependency(\.database.purchaseOrders) var purchaseOrders
|
@Dependency(\.database.purchaseOrders) var purchaseOrders
|
||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
@@ -130,10 +124,6 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute {
|
|||||||
case let .create(purchaseOrder):
|
case let .create(purchaseOrder):
|
||||||
return try await PurchaseOrderTable.Row(purchaseOrder: purchaseOrders.create(purchaseOrder))
|
return try await PurchaseOrderTable.Row(purchaseOrder: purchaseOrders.create(purchaseOrder))
|
||||||
|
|
||||||
case let .delete(id: id):
|
|
||||||
try await purchaseOrders.delete(id)
|
|
||||||
return nil
|
|
||||||
|
|
||||||
case .index:
|
case .index:
|
||||||
return try await mainPage(PurchaseOrderForm())
|
return try await mainPage(PurchaseOrderForm())
|
||||||
|
|
||||||
@@ -198,7 +188,7 @@ extension SharedModels.ViewRoute.UserRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML? {
|
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||||
@Dependency(\.database.users) var users
|
@Dependency(\.database.users) var users
|
||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
@@ -208,10 +198,6 @@ extension SharedModels.ViewRoute.UserRoute {
|
|||||||
case let .create(user):
|
case let .create(user):
|
||||||
return try await UserTable.Row(user: users.create(user))
|
return try await UserTable.Row(user: users.create(user))
|
||||||
|
|
||||||
case let .delete(id: id):
|
|
||||||
try await users.delete(id)
|
|
||||||
return nil
|
|
||||||
|
|
||||||
case .index:
|
case .index:
|
||||||
return try await mainPage(UserDetail(user: nil))
|
return try await mainPage(UserDetail(user: nil))
|
||||||
|
|
||||||
@@ -240,16 +226,14 @@ extension SharedModels.ViewRoute.VendorRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML? {
|
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||||
@Dependency(\.database) var database
|
@Dependency(\.database) var database
|
||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
case .form:
|
case .form:
|
||||||
// html = VendorForm(.float(shouldShow: true))
|
|
||||||
return try await render(mainPage, isHtmxRequest, VendorForm(.float(shouldShow: true)))
|
return try await render(mainPage, isHtmxRequest, VendorForm(.float(shouldShow: true)))
|
||||||
|
|
||||||
case .index:
|
case .index:
|
||||||
// return VendorForm()
|
|
||||||
return try await mainPage(VendorForm())
|
return try await mainPage(VendorForm())
|
||||||
|
|
||||||
case let .create(vendor):
|
case let .create(vendor):
|
||||||
@@ -262,10 +246,6 @@ extension SharedModels.ViewRoute.VendorRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .delete(id: id):
|
|
||||||
try await database.vendors.delete(id)
|
|
||||||
return nil
|
|
||||||
|
|
||||||
case let .get(id: id):
|
case let .get(id: id):
|
||||||
guard let vendor = try await database.vendors.get(id, .withBranches) else {
|
guard let vendor = try await database.vendors.get(id, .withBranches) else {
|
||||||
throw Abort(.badRequest, reason: "Vendor not found.")
|
throw Abort(.badRequest, reason: "Vendor not found.")
|
||||||
@@ -282,7 +262,7 @@ extension SharedModels.ViewRoute.VendorRoute {
|
|||||||
|
|
||||||
extension SharedModels.ViewRoute.VendorBranchRoute {
|
extension SharedModels.ViewRoute.VendorBranchRoute {
|
||||||
|
|
||||||
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML? {
|
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||||
@Dependency(\.database) var database
|
@Dependency(\.database) var database
|
||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
@@ -300,10 +280,6 @@ extension SharedModels.ViewRoute.VendorBranchRoute {
|
|||||||
|
|
||||||
case let .create(branch):
|
case let .create(branch):
|
||||||
return try await VendorBranchList.Row(branch: database.vendorBranches.create(branch))
|
return try await VendorBranchList.Row(branch: database.vendorBranches.create(branch))
|
||||||
|
|
||||||
case let .delete(id: id):
|
|
||||||
try await database.vendorBranches.delete(id)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ extension HTMLAttribute.hx {
|
|||||||
put(SharedModels.ViewRoute.router.path(for: route))
|
put(SharedModels.ViewRoute.router.path(for: route))
|
||||||
}
|
}
|
||||||
|
|
||||||
static func delete(route: SharedModels.ViewRoute) -> HTMLAttribute {
|
static func delete(route: SharedModels.ApiRoute) -> HTMLAttribute {
|
||||||
delete(SharedModels.ViewRoute.router.path(for: route))
|
delete(SharedModels.ApiRoute.router.path(for: route))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,341 +0,0 @@
|
|||||||
@testable import App
|
|
||||||
import DatabaseClient
|
|
||||||
import Dependencies
|
|
||||||
import HtmlSnapshotTesting
|
|
||||||
import SharedModels
|
|
||||||
import SnapshotTesting
|
|
||||||
import Vapor
|
|
||||||
import XCTVapor
|
|
||||||
|
|
||||||
final class ViewSnapshotTests: XCTestCase {
|
|
||||||
|
|
||||||
var app: Application!
|
|
||||||
let router = ViewRoute.router
|
|
||||||
|
|
||||||
override func setUp() {
|
|
||||||
app = Application(.testing)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func invokeTest() {
|
|
||||||
withSnapshotTesting(record: .missing) {
|
|
||||||
super.invokeTest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tearDown() {
|
|
||||||
app.shutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
func testEmployeeViews() async throws {
|
|
||||||
try await withDependencies {
|
|
||||||
$0.database.employees = .mock
|
|
||||||
} operation: {
|
|
||||||
@Dependency(\.database) var database
|
|
||||||
|
|
||||||
try await configure(app, makeDatabaseClient: { _ in database })
|
|
||||||
|
|
||||||
try await app.test(.GET, router.path(for: .employee(.index))) { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
|
|
||||||
try await app.test(.GET, router.path(for: .employee(.form))) { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
|
|
||||||
for context in SharedModels.ViewRoute.SelectContext.allCases {
|
|
||||||
try app.test(.GET, router.path(for: .employee(.select(context: context)))) { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try app.test(.GET, router.path(for: .employee(.get(id: UUID(0))))) { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
|
|
||||||
try app.test(.POST, router.path(for: .employee(.index)), beforeRequest: { req in
|
|
||||||
req.body = ByteBuffer(string: "firstName=Testy&lastName=McTestface")
|
|
||||||
}, afterResponse: { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
})
|
|
||||||
|
|
||||||
try app.test(.PUT, router.path(for: .employee(.update(id: UUID(0), updates: .mock))), beforeRequest: { req in
|
|
||||||
req.body = ByteBuffer(string: "firstName=Testy&lastName=McTestface")
|
|
||||||
}, afterResponse: { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: These need to come after mocks are generated.
|
|
||||||
// func testPurchaseOrderIndex() async throws {
|
|
||||||
// try await configure(app)
|
|
||||||
// try await app.test(.GET, router.path(for: .purchaseOrder(.index))) { res in
|
|
||||||
// assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
func testUserViews() async throws {
|
|
||||||
try await withDependencies {
|
|
||||||
$0.database.users = .mock
|
|
||||||
} operation: {
|
|
||||||
@Dependency(\.database) var database
|
|
||||||
|
|
||||||
try await configure(app, makeDatabaseClient: { _ in database })
|
|
||||||
|
|
||||||
try app.test(.GET, router.path(for: .user(.form))) { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
|
|
||||||
try app.test(.POST, router.path(for: .user(.index))) { req in
|
|
||||||
req.body = ByteBuffer(string: "username=test&email=test@test.com&password=super-secret&confirmPassword=super-secret")
|
|
||||||
} afterResponse: { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
|
|
||||||
try app.test(.GET, router.path(for: .user(.index))) { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
|
|
||||||
try app.test(.GET, router.path(for: .user(.get(id: UUID(0))))) { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
|
|
||||||
try app.test(.PATCH, router.path(for: .user(.update(id: UUID(0), updates: .mock)))) { req in
|
|
||||||
req.body = .init(string: "username=test&email=test@test.com")
|
|
||||||
} afterResponse: { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testVendorViews() async throws {
|
|
||||||
try await withDependencies {
|
|
||||||
$0.database.vendors = .mock
|
|
||||||
} operation: {
|
|
||||||
@Dependency(\.database) var database
|
|
||||||
|
|
||||||
try await configure(app, makeDatabaseClient: { _ in database })
|
|
||||||
|
|
||||||
try app.test(.GET, router.path(for: .vendor(.form))) { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
|
|
||||||
try app.test(.POST, router.path(for: .vendor(.index))) { req in
|
|
||||||
req.body = ByteBuffer(string: "name=Test")
|
|
||||||
} afterResponse: { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
|
|
||||||
try app.test(.GET, router.path(for: .vendor(.index))) { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
|
|
||||||
try app.test(.GET, router.path(for: .vendor(.get(id: UUID(0))))) { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
|
|
||||||
try app.test(.PUT, router.path(for: .vendor(.update(id: UUID(0), updates: .mock)))) { req in
|
|
||||||
req.body = .init(string: "name=Test")
|
|
||||||
} afterResponse: { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testVendorBranchViews() async throws {
|
|
||||||
try await withDependencies {
|
|
||||||
$0.database.vendorBranches = .mock
|
|
||||||
} operation: {
|
|
||||||
@Dependency(\.database) var database
|
|
||||||
|
|
||||||
try await configure(app, makeDatabaseClient: { _ in database })
|
|
||||||
|
|
||||||
try app.test(.GET, router.path(for: .vendorBranch(.index(for: UUID(0))))) { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
|
|
||||||
for context in SharedModels.ViewRoute.SelectContext.allCases {
|
|
||||||
try app.test(.GET, router.path(for: .vendorBranch(.select(context: context)))) { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try app.test(.POST, router.path(for: .vendorBranch(.create(.mock)))) { req in
|
|
||||||
req.body = .init(string: "name=Test&vendorID=\(UUID(0))")
|
|
||||||
} afterResponse: { res in
|
|
||||||
assertSnapshot(of: res.body.string, as: .html)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DatabaseClient.Employees {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(
|
|
||||||
create: { _ in .mock },
|
|
||||||
delete: { _ in },
|
|
||||||
fetchAll: { _ in [Employee.mock] },
|
|
||||||
get: { _ in Employee.mock },
|
|
||||||
update: { _, _ in Employee.mock }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DatabaseClient.Users {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(
|
|
||||||
count: { 1 },
|
|
||||||
create: { _ in User.mock },
|
|
||||||
delete: { _ in },
|
|
||||||
fetchAll: { [User.mock] },
|
|
||||||
get: { _ in User.mock },
|
|
||||||
login: { _ in User.Token.mock },
|
|
||||||
logout: { _ in },
|
|
||||||
token: { _ in User.Token.mock },
|
|
||||||
update: { _, _ in User.mock }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DatabaseClient.Vendors {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(
|
|
||||||
create: { _ in Vendor.mock },
|
|
||||||
delete: { _ in },
|
|
||||||
fetchAll: { _ in [Vendor.mock] },
|
|
||||||
get: { _, _ in Vendor.mock },
|
|
||||||
update: { _, _, _ in Vendor.mock }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DatabaseClient.VendorBranches {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(
|
|
||||||
create: { _ in VendorBranch.mock },
|
|
||||||
delete: { _ in },
|
|
||||||
fetchAll: { _ in [VendorBranch.mock] },
|
|
||||||
fetchAllWithDetail: { [VendorBranch.Detail.mock] },
|
|
||||||
get: { _ in VendorBranch.mock },
|
|
||||||
update: { _, _ in VendorBranch.mock }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Date {
|
|
||||||
static var mock: Self {
|
|
||||||
Date(timeIntervalSince1970: 1_234_567_890)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Employee {
|
|
||||||
static var mock: Self {
|
|
||||||
Employee(
|
|
||||||
id: UUID(0),
|
|
||||||
createdAt: Date(timeIntervalSince1970: 1_234_567_890),
|
|
||||||
firstName: "Testy",
|
|
||||||
lastName: "McTestface",
|
|
||||||
updatedAt: Date(timeIntervalSince1970: 1_234_567_890)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Employee.Create {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(firstName: "Testy", lastName: "McTestface")
|
|
||||||
}
|
|
||||||
|
|
||||||
func employeeMock() -> Employee {
|
|
||||||
@Dependency(\.date.now) var now
|
|
||||||
return .init(
|
|
||||||
id: UUID(0),
|
|
||||||
createdAt: Date(timeIntervalSince1970: 1_234_567_890),
|
|
||||||
firstName: firstName,
|
|
||||||
lastName: lastName,
|
|
||||||
updatedAt: Date(timeIntervalSince1970: 1_234_567_890)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Employee.Update {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(firstName: "Testy", lastName: "McTestface", active: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension User {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(id: UUID(0), email: "test@example.com", username: "test")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension User.Create {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(username: "test", email: "test@example.com", password: "super-secret", confirmPassword: "super-secret")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension User.Token {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(id: UUID(1), userID: UUID(0), value: "test-token")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension User.Update {
|
|
||||||
static var mock: Self {
|
|
||||||
User.Update(username: "test", email: "test@test.com")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Vendor {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(id: UUID(0), name: "Test", branches: nil, createdAt: .mock, updatedAt: .mock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Vendor.Create {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(name: "Test")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Vendor.Update {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(name: "Test")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VendorBranch {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(id: UUID(1), name: "Mock", vendorID: UUID(0), createdAt: .mock, updatedAt: .mock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VendorBranch.Create {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(name: "Mock", vendorID: UUID(0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VendorBranch.Detail {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(id: UUID(1), name: "Mock", vendor: .mock, createdAt: .mock, updatedAt: .mock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PurchaseOrder {
|
|
||||||
static var mock: Self {
|
|
||||||
.init(
|
|
||||||
id: 1,
|
|
||||||
workOrder: 12245,
|
|
||||||
materials: "foo",
|
|
||||||
customer: "Testy McTestface",
|
|
||||||
truckStock: true,
|
|
||||||
createdBy: .mock,
|
|
||||||
createdFor: .mock,
|
|
||||||
vendorBranch: .mock,
|
|
||||||
createdAt: .mock,
|
|
||||||
updatedAt: .mock
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><html lang="en"><head><title>Purchase Orders</title><meta charset="UTF-8"><script src="https://unpkg.com/htmx.org@2.0.4"></script><script src="/js/main.js"></script><link rel="stylesheet" href="/css/main.css"><link rel="icon" href="/images/favicon.ico" type="image/x-icon"></head><body><header class="header"><div id="logo">HHE - Purchase Orders</div><div class="sidepanel" id="sidepanel"><a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a><div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div><a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click">Logout</a></div><button class="openbtn" onclick="openSidepanel()"><img src="/images/menu.svg" style="width: 30px;, height: 30px;"></button></header><div class="container" style="padding: 20px 20px;"><h1>Employees</h1><br><p class="secondary"><i>Employees are who purchase orders can be issued to.</i></p><br></div><div class="container"><div id="float" class="" style="display: hidden;"></div><table><thead><tr><th>Name</th><th style="width: 100px;"><button class="btn btn-add" style="padding: 0px 10px;" hx-get="/employees/create" hx-target="#float" hx-swap="outerHTML transition:true swap:0.5s">+</button></th></tr></thead><tbody id="employee-table"><tr id="employee-00000000-0000-0000-0000-000000000000"><td>Testy Mctestface</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/employees/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML transition:true swap:0.5s">〉</button></td></tr></tbody></table></div></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><html lang="en"><head><title>Purchase Orders</title><meta charset="UTF-8"><script src="https://unpkg.com/htmx.org@2.0.4"></script><script src="/js/main.js"></script><link rel="stylesheet" href="/css/main.css"><link rel="icon" href="/images/favicon.ico" type="image/x-icon"></head><body><header class="header"><div id="logo">HHE - Purchase Orders</div><div class="sidepanel" id="sidepanel"><a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a><div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div><a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click">Logout</a></div><button class="openbtn" onclick="openSidepanel()"><img src="/images/menu.svg" style="width: 30px;, height: 30px;"></button></header><div class="container" style="padding: 20px 20px;"><h1>Employees</h1><br><p class="secondary"><i>Employees are who purchase orders can be issued to.</i></p><br></div><div class="container"><div id="float" class="float" style="display: block;"><div class="btn-row"><button class="btn-close" onclick="toggleContent('float'); window.location.href='/employees';">x</button></div><form hx-post="/employees" hx-target="#employee-table" hx-swap="beforeend transition:true swap:0.5s" hx-on::after-request="if(event.detail.successful) toggleContent('float'); window.location.href='/employees';"><div class="row"><input type="text" class="col-5" name="firstName" value="" placeholder="First Name" required><div class="col-2"></div><input type="text" class="col-5" name="lastName" value="" placeholder="Last Name" required></div><div class="btn-row"><button type="submit" class="btn-primary">Create</button></div></form></div><table><thead><tr><th>Name</th><th style="width: 100px;"><button class="btn btn-add" style="padding: 0px 10px;" hx-get="/employees/create" hx-target="#float" hx-swap="outerHTML transition:true swap:0.5s">+</button></th></tr></thead><tbody id="employee-table"><tr id="employee-00000000-0000-0000-0000-000000000000"><td>Testy Mctestface</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/employees/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML transition:true swap:0.5s">〉</button></td></tr></tbody></table></div></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<select name="createdForID" class="col-3"><option value="00000000-0000-0000-0000-000000000000">Testy Mctestface</option></select>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<select name="createdForID" class="col-6" style="margin-left: 15px;"><option value="00000000-0000-0000-0000-000000000000">Testy Mctestface</option></select>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><html lang="en"><head><title>Purchase Orders</title><meta charset="UTF-8"><script src="https://unpkg.com/htmx.org@2.0.4"></script><script src="/js/main.js"></script><link rel="stylesheet" href="/css/main.css"><link rel="icon" href="/images/favicon.ico" type="image/x-icon"></head><body><header class="header"><div id="logo">HHE - Purchase Orders</div><div class="sidepanel" id="sidepanel"><a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a><div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div><a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click">Logout</a></div><button class="openbtn" onclick="openSidepanel()"><img src="/images/menu.svg" style="width: 30px;, height: 30px;"></button></header><div class="container" style="padding: 20px 20px;"><h1>Employees</h1><br><p class="secondary"><i>Employees are who purchase orders can be issued to.</i></p><br></div><div class="container"><div id="float" class="float" style="display: block;"><div class="btn-row"><button class="btn-close" onclick="toggleContent('float'); window.location.href='/employees';">x</button></div><form hx-put="/employees/00000000-0000-0000-0000-000000000000" hx-target="#employee-00000000-0000-0000-0000-000000000000" hx-swap="outerHTML transition:true swap:0.5s" hx-on::after-request="if(event.detail.successful) toggleContent('float'); window.location.href='/employees';"><div class="row"><input type="text" class="col-5" name="firstName" value="Testy" placeholder="First Name" required><div class="col-2"></div><input type="text" class="col-5" name="lastName" value="McTestface" placeholder="Last Name" required></div><div class="btn-row"><button type="submit" class="btn-primary">Update</button><button class="danger" hx-confirm="Are you sure you want to delete this employee?" hx-delete="/employees/00000000-0000-0000-0000-000000000000" hx-target="#employee-00000000-0000-0000-0000-000000000000" hx-swap="outerHTML transition:true swap:1s">Delete</button></div></form></div><table><thead><tr><th>Name</th><th style="width: 100px;"><button class="btn btn-add" style="padding: 0px 10px;" hx-get="/employees/create" hx-target="#float" hx-swap="outerHTML transition:true swap:0.5s">+</button></th></tr></thead><tbody id="employee-table"><tr id="employee-00000000-0000-0000-0000-000000000000"><td>Testy Mctestface</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/employees/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML transition:true swap:0.5s">〉</button></td></tr></tbody></table></div></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<tr id="employee-00000000-0000-0000-0000-000000000000"><td>Testy Mctestface</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/employees/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML transition:true swap:0.5s">〉</button></td></tr>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<tr id="employee-00000000-0000-0000-0000-000000000000"><td>Testy Mctestface</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/employees/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML transition:true swap:0.5s">〉</button></td></tr>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><html lang="en"><head><title>Purchase Orders</title><meta charset="UTF-8"><script src="https://unpkg.com/htmx.org@2.0.4"></script><script src="/js/main.js"></script><link rel="stylesheet" href="/css/main.css"><link rel="icon" href="/images/favicon.ico" type="image/x-icon"></head><body><header class="header"><div id="logo">HHE - Purchase Orders</div><div class="sidepanel" id="sidepanel"><a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a><div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div><a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click">Logout</a></div><button class="openbtn" onclick="openSidepanel()"><img src="/images/menu.svg" style="width: 30px;, height: 30px;"></button></header><div class="container" style="padding: 20px 20px;"><h1>Users</h1><br><p class="secondary"><i>Users are who can login and issue purchase orders for employees.</i></p><br></div><div class="container"><div id="float" class="float" style="display: block;"><div class="btn-row"><button class="btn-close" onclick="toggleContent('float');">x</button></div><form id="user-form" class="user-form" hx-post="/users" hx-push-url="false" hx-target="#user-table" hx-swap="afterbegin transition:true swap:0.5s" hx-on::after-request="if(event.detail.successful) this.reset(); toggleContent('float');"><div class="row"><input type="text" id="username" name="username" placeholder="Username" autofocus required></div><div class="row"><input type="email" id="email" name="email" placeholder="Email" required></div><div class="row"><input type="password" id="password" name="password" placeholder="Password" required></div><div class="row"><input type="password" id="confirmPassword" name="confirmPassword" placeholder="Confirm Password" required></div><div class="row"><button type="submit" class="btn-primary">Create</button></div></form></div><table><thead><tr><th>Username</th><th>Email</th><th style="width: 50px;"><button class="btn btn-add" hx-get="/users/create" hx-target="#float" hx-swap="outerHTML">+</button></th></tr></thead><tbody id="user-table"><tr id="user-00000000-0000-0000-0000-000000000000"><td>test</td><td>test@example.com</td><td><button class="btn-detail" hx-get="/users/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-swap="outerHTML" hx-push-url="true">〉</button></td></tr></tbody></table></div></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<tr id="user-00000000-0000-0000-0000-000000000000"><td>test</td><td>test@example.com</td><td><button class="btn-detail" hx-get="/users/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-swap="outerHTML" hx-push-url="true">〉</button></td></tr>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><html lang="en"><head><title>Purchase Orders</title><meta charset="UTF-8"><script src="https://unpkg.com/htmx.org@2.0.4"></script><script src="/js/main.js"></script><link rel="stylesheet" href="/css/main.css"><link rel="icon" href="/images/favicon.ico" type="image/x-icon"></head><body><header class="header"><div id="logo">HHE - Purchase Orders</div><div class="sidepanel" id="sidepanel"><a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a><div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div><a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click">Logout</a></div><button class="openbtn" onclick="openSidepanel()"><img src="/images/menu.svg" style="width: 30px;, height: 30px;"></button></header><div class="container" style="padding: 20px 20px;"><h1>Users</h1><br><p class="secondary"><i>Users are who can login and issue purchase orders for employees.</i></p><br></div><div class="container"><div id="float" class="" style="display: hidden;"></div><table><thead><tr><th>Username</th><th>Email</th><th style="width: 50px;"><button class="btn btn-add" hx-get="/users/create" hx-target="#float" hx-swap="outerHTML">+</button></th></tr></thead><tbody id="user-table"><tr id="user-00000000-0000-0000-0000-000000000000"><td>test</td><td>test@example.com</td><td><button class="btn-detail" hx-get="/users/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-swap="outerHTML" hx-push-url="true">〉</button></td></tr></tbody></table></div></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><html lang="en"><head><title>Purchase Orders</title><meta charset="UTF-8"><script src="https://unpkg.com/htmx.org@2.0.4"></script><script src="/js/main.js"></script><link rel="stylesheet" href="/css/main.css"><link rel="icon" href="/images/favicon.ico" type="image/x-icon"></head><body><header class="header"><div id="logo">HHE - Purchase Orders</div><div class="sidepanel" id="sidepanel"><a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a><div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div><a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click">Logout</a></div><button class="openbtn" onclick="openSidepanel()"><img src="/images/menu.svg" style="width: 30px;, height: 30px;"></button></header><div class="container" style="padding: 20px 20px;"><h1>Users</h1><br><p class="secondary"><i>Users are who can login and issue purchase orders for employees.</i></p><br></div><div class="container"><div id="float" class="float" style="display: block;"><div class="btn-row"><button class="btn-close" onclick="toggleContent('float'); window.location.href='/users';">x</button></div><form hx-post="/users/00000000-0000-0000-0000-000000000000" hx-swap="outerHTML" hx-target="#user-00000000-0000-0000-0000-000000000000" hx-on::after-request="toggleContent('float');"><div class="row"><label for="username" class="col-2"><span class="label">Username:</span></label><input class="col-4" type="text" id="username" name="username" value="test" required><label for="email" class="col-2"><span class="label">Email:</span></label><input class="col-4" type="email" id="email" name="email" value="test@example.com" required></div><div class="row"><span class="label col-2">Created:</span><span class="date col-4"></span><span class="label col-2">Updated:</span><span class="date col-4"></span></div><div class="btn-row user-buttons"><button type="submit" class="btn-secondary">Update</button><button class="danger" hx-delete="/users/00000000-0000-0000-0000-000000000000" hx-trigger="click" hx-swap="outerHTML" hx-target="#user-00000000-0000-0000-0000-000000000000" hx-confirm="Are you sure you want to delete this user?" hx-on::after-request="toggleContent('float'); window.location.href='/users';">Delete</button></div></form></div><table><thead><tr><th>Username</th><th>Email</th><th style="width: 50px;"><button class="btn btn-add" hx-get="/users/create" hx-target="#float" hx-swap="outerHTML">+</button></th></tr></thead><tbody id="user-table"><tr id="user-00000000-0000-0000-0000-000000000000"><td>test</td><td>test@example.com</td><td><button class="btn-detail" hx-get="/users/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-swap="outerHTML" hx-push-url="true">〉</button></td></tr></tbody></table></div></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<tr id="user-00000000-0000-0000-0000-000000000000"><td>test</td><td>test@example.com</td><td><button class="btn-detail" hx-get="/users/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-swap="outerHTML" hx-push-url="true">〉</button></td></tr>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<ul id="branch-list"><li id="branch-00000000-0000-0000-0000-000000000001" class="branch-row"><span class="label">Mock</span><button class="btn" hx-delete="/vendors/branches/00000000-0000-0000-0000-000000000001" hx-target="#branch-00000000-0000-0000-0000-000000000001" hx-swap="outerHTML transition:true swap:0.5s"><img src="/images/trash-can.svg" width="30" height="30" style="margin-top: 5px;"></button></li></ul>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<select name="vendorBranchID" class="col-4"><option value="00000000-0000-0000-0000-000000000001">Test - Mock</option></select>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<select name="vendorBranchID" class="col-4" style="margin-left: 15px;"><option value="00000000-0000-0000-0000-000000000001">Test - Mock</option></select>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<li id="branch-00000000-0000-0000-0000-000000000001" class="branch-row"><span class="label">Mock</span><button class="btn" hx-delete="/vendors/branches/00000000-0000-0000-0000-000000000001" hx-target="#branch-00000000-0000-0000-0000-000000000001" hx-swap="outerHTML transition:true swap:0.5s"><img src="/images/trash-can.svg" width="30" height="30" style="margin-top: 5px;"></button></li>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><html lang="en"><head><title>Purchase Orders</title><meta charset="UTF-8"><script src="https://unpkg.com/htmx.org@2.0.4"></script><script src="/js/main.js"></script><link rel="stylesheet" href="/css/main.css"><link rel="icon" href="/images/favicon.ico" type="image/x-icon"></head><body><header class="header"><div id="logo">HHE - Purchase Orders</div><div class="sidepanel" id="sidepanel"><a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a><div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div><a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click">Logout</a></div><button class="openbtn" onclick="openSidepanel()"><img src="/images/menu.svg" style="width: 30px;, height: 30px;"></button></header><div class="container" style="padding: 20px 20px;"><h1>Vendors</h1><br><p class="secondary"><i>Vendors are where purchase orders can be issued to.</i></p><br></div><div class="container" id="content"><div id="float" class="float" style="display: block;"><div class="btn-row"><button class="btn-close" onclick="toggleContent('float');">x</button></div><form id="vendor-form" hx-post="/vendors" hx-target="#content" hx-swap="outerHTML"><div class="row"><input type="text" class="col-9" id="vendor-name" name="name" value="" placeholder="Vendor Name" hx-post="/vendors" hx-trigger="keyup changed delay:500ms" required><button type="submit" class="btn-primary" style="float: right">Create</button></div></form></div><table><thead><tr><th>Name</th><th>Branches</th><th style="width: 100px;"><button class="btn btn-add" style="padding: 0px 10px;" hx-get="/vendors/create" hx-target="#float" hx-swap="outerHTML">+</button></th></tr></thead><tbody id="vendor-table"><tr id="vendor-00000000-0000-0000-0000-000000000000"><td>Test</td><td>(0) Branches</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML">〉</button></td></tr></tbody></table></div></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<div class="container" id="content"><div id="float" class="float" style="display: block;"><div class="btn-row"><button class="btn-close" onclick="toggleContent('float');" hx-get="/vendors" hx-push-url="true" hx-target="body" hx-swap="outerHTML">x</button></div><form id="vendor-form" hx-put="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#content" hx-swap="outerHTML"><div class="row"><input type="text" class="col-9" id="vendor-name" name="name" value="Test" placeholder="Vendor Name" hx-put="/vendors/00000000-0000-0000-0000-000000000000" hx-trigger="keyup changed delay:500ms" required><button class="danger" style="font-size: 1.25em; padding: 10px 20px; border-radius: 10px;" hx-delete="/vendors/00000000-0000-0000-0000-000000000000" hx-confirm="Are you sure you want to delete this vendor?" hx-target="#vendor-00000000-0000-0000-0000-000000000000" hx-swap="outerHTML transition:true swap:1s" hx-on::after-request="if(event.detail.successful) toggleContent('float'); window.location.href='/vendors';">Delete</button><button type="submit" class="btn-primary" style="float: right">Update</button></div></form><h2 style="margin-left: 20px; font-size: 1.5em;" class="label">Branches</h2><form id="branch-form" hx-post="/vendors/branches" hx-target="#branch-list" hx-swap="beforeend" hx-on::after-request="if(event.detail.successful) this.reset();"><input type="hidden" name="vendorID" value="00000000-0000-0000-0000-000000000000"><input type="text" class="col-9" name="name" placeholder="Add branch..." required hx-trigger="keyup changed delay:800ms"><button type="submit" class="btn-secondary" style="float: right; padding: 10px 50px;" hx-target="#branch-list" hx-swap="beforeend">+</button></form><div hx-get="/vendors/branches?vendorID=00000000-0000-0000-0000-000000000000" hx-target="this" hx-indicator=".hx-indicator" hx-trigger="revealed"><img src="/images/spinner.svg" width="30" height="30" class="hx-indicator"></div></div><table><thead><tr><th>Name</th><th>Branches</th><th style="width: 100px;"><button class="btn btn-add" style="padding: 0px 10px;" hx-get="/vendors/create" hx-target="#float" hx-swap="outerHTML">+</button></th></tr></thead><tbody id="vendor-table"><tr id="vendor-00000000-0000-0000-0000-000000000000"><td>Test</td><td>(0) Branches</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML">〉</button></td></tr></tbody></table></div>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><html lang="en"><head><title>Purchase Orders</title><meta charset="UTF-8"><script src="https://unpkg.com/htmx.org@2.0.4"></script><script src="/js/main.js"></script><link rel="stylesheet" href="/css/main.css"><link rel="icon" href="/images/favicon.ico" type="image/x-icon"></head><body><header class="header"><div id="logo">HHE - Purchase Orders</div><div class="sidepanel" id="sidepanel"><a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a><div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div><a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click">Logout</a></div><button class="openbtn" onclick="openSidepanel()"><img src="/images/menu.svg" style="width: 30px;, height: 30px;"></button></header><div class="container" style="padding: 20px 20px;"><h1>Vendors</h1><br><p class="secondary"><i>Vendors are where purchase orders can be issued to.</i></p><br></div><div class="container" id="content"><div id="float" class="" style="display: hidden;"></div><table><thead><tr><th>Name</th><th>Branches</th><th style="width: 100px;"><button class="btn btn-add" style="padding: 0px 10px;" hx-get="/vendors/create" hx-target="#float" hx-swap="outerHTML">+</button></th></tr></thead><tbody id="vendor-table"><tr id="vendor-00000000-0000-0000-0000-000000000000"><td>Test</td><td>(0) Branches</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML">〉</button></td></tr></tbody></table></div></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><html lang="en"><head><title>Purchase Orders</title><meta charset="UTF-8"><script src="https://unpkg.com/htmx.org@2.0.4"></script><script src="/js/main.js"></script><link rel="stylesheet" href="/css/main.css"><link rel="icon" href="/images/favicon.ico" type="image/x-icon"></head><body><header class="header"><div id="logo">HHE - Purchase Orders</div><div class="sidepanel" id="sidepanel"><a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a><div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div><a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click">Logout</a></div><button class="openbtn" onclick="openSidepanel()"><img src="/images/menu.svg" style="width: 30px;, height: 30px;"></button></header><div class="container" style="padding: 20px 20px;"><h1>Vendors</h1><br><p class="secondary"><i>Vendors are where purchase orders can be issued to.</i></p><br></div><div class="container" id="content"><div id="float" class="float" style="display: block;"><div class="btn-row"><button class="btn-close" onclick="toggleContent('float');" hx-get="/vendors" hx-push-url="true" hx-target="body" hx-swap="outerHTML">x</button></div><form id="vendor-form" hx-put="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#content" hx-swap="outerHTML"><div class="row"><input type="text" class="col-9" id="vendor-name" name="name" value="Test" placeholder="Vendor Name" hx-put="/vendors/00000000-0000-0000-0000-000000000000" hx-trigger="keyup changed delay:500ms" required><button class="danger" style="font-size: 1.25em; padding: 10px 20px; border-radius: 10px;" hx-delete="/vendors/00000000-0000-0000-0000-000000000000" hx-confirm="Are you sure you want to delete this vendor?" hx-target="#vendor-00000000-0000-0000-0000-000000000000" hx-swap="outerHTML transition:true swap:1s" hx-on::after-request="if(event.detail.successful) toggleContent('float'); window.location.href='/vendors';">Delete</button><button type="submit" class="btn-primary" style="float: right">Update</button></div></form><h2 style="margin-left: 20px; font-size: 1.5em;" class="label">Branches</h2><form id="branch-form" hx-post="/vendors/branches" hx-target="#branch-list" hx-swap="beforeend" hx-on::after-request="if(event.detail.successful) this.reset();"><input type="hidden" name="vendorID" value="00000000-0000-0000-0000-000000000000"><input type="text" class="col-9" name="name" placeholder="Add branch..." required hx-trigger="keyup changed delay:800ms"><button type="submit" class="btn-secondary" style="float: right; padding: 10px 50px;" hx-target="#branch-list" hx-swap="beforeend">+</button></form><div hx-get="/vendors/branches?vendorID=00000000-0000-0000-0000-000000000000" hx-target="this" hx-indicator=".hx-indicator" hx-trigger="revealed"><img src="/images/spinner.svg" width="30" height="30" class="hx-indicator"></div></div><table><thead><tr><th>Name</th><th>Branches</th><th style="width: 100px;"><button class="btn btn-add" style="padding: 0px 10px;" hx-get="/vendors/create" hx-target="#float" hx-swap="outerHTML">+</button></th></tr></thead><tbody id="vendor-table"><tr id="vendor-00000000-0000-0000-0000-000000000000"><td>Test</td><td>(0) Branches</td><td><button class="btn-detail" style="padding-left: 15px;" hx-get="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#float" hx-push-url="true" hx-swap="outerHTML">〉</button></td></tr></tbody></table></div></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<div id="float" class="float" style="display: block;"><div class="btn-row"><button class="btn-close" onclick="toggleContent('float');" hx-get="/vendors" hx-push-url="true" hx-target="body" hx-swap="outerHTML">x</button></div><form id="vendor-form" hx-put="/vendors/00000000-0000-0000-0000-000000000000" hx-target="#content" hx-swap="outerHTML"><div class="row"><input type="text" class="col-9" id="vendor-name" name="name" value="Test" placeholder="Vendor Name" hx-put="/vendors/00000000-0000-0000-0000-000000000000" hx-trigger="keyup changed delay:500ms" required><button class="danger" style="font-size: 1.25em; padding: 10px 20px; border-radius: 10px;" hx-delete="/vendors/00000000-0000-0000-0000-000000000000" hx-confirm="Are you sure you want to delete this vendor?" hx-target="#vendor-00000000-0000-0000-0000-000000000000" hx-swap="outerHTML transition:true swap:1s" hx-on::after-request="if(event.detail.successful) toggleContent('float'); window.location.href='/vendors';">Delete</button><button type="submit" class="btn-primary" style="float: right">Update</button></div></form><h2 style="margin-left: 20px; font-size: 1.5em;" class="label">Branches</h2><form id="branch-form" hx-post="/vendors/branches" hx-target="#branch-list" hx-swap="beforeend" hx-on::after-request="if(event.detail.successful) this.reset();"><input type="hidden" name="vendorID" value="00000000-0000-0000-0000-000000000000"><input type="text" class="col-9" name="name" placeholder="Add branch..." required hx-trigger="keyup changed delay:800ms"><button type="submit" class="btn-secondary" style="float: right; padding: 10px 50px;" hx-target="#branch-list" hx-swap="beforeend">+</button></form><div hx-get="/vendors/branches?vendorID=00000000-0000-0000-0000-000000000000" hx-target="this" hx-indicator=".hx-indicator" hx-trigger="revealed"><img src="/images/spinner.svg" width="30" height="30" class="hx-indicator"></div></div>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
@testable import App
|
|
||||||
import Elementary
|
|
||||||
import HtmlSnapshotTesting
|
|
||||||
import SnapshotTesting
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
final class SnapshotTestingTests: XCTestCase {
|
|
||||||
func testSimple() {
|
|
||||||
let doc = MainPage.loggedIn(next: nil)
|
|
||||||
assertSnapshot(of: doc, as: .html)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>Purchase Orders</title>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
||||||
<script src="/js/main.js"></script>
|
|
||||||
<link rel="stylesheet" href="/css/main.css">
|
|
||||||
<link rel="icon" href="/images/favicon.ico" type="image/x-icon">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header class="header">
|
|
||||||
<div id="logo">HHE - Purchase Orders</div>
|
|
||||||
<div class="sidepanel" id="sidepanel">
|
|
||||||
<a href="javascript:void(0)" class="closebtn" onclick="closeSidepanel()">x</a><a hx-get="/purchase-orders?page=1&limit=50" hx-target="body" hx-push-url="true">Purchase Orders</a><a hx-get="/users" hx-target="body" hx-push-url="true">Users</a><a hx-get="/employees" hx-target="body" hx-push-url="true">Employees</a><a hx-get="/vendors" hx-target="body" hx-push-url="true">Vendors</a>
|
|
||||||
<div style="border-bottom: 1px solid grey; margin-bottom: 5px;"></div>
|
|
||||||
Logout<a hx-post="/logout" hx-target="#content" hx-swap="outerHTML" hx-trigger="click"></a>
|
|
||||||
</div>
|
|
||||||
<button class="openbtn" onclick="openSidepanel()">
|
|
||||||
<img src="/images/menu.svg" style="width: 30px;, height: 30px;">
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
<div class="container" style="padding: 20px 20px;">
|
|
||||||
<h1>Purchase Orders</h1>
|
|
||||||
<br>
|
|
||||||
<p class="secondary"><i></i></p>
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
<div hx-get="/purchase-orders" hx-push-url="true" hx-target="body" hx-trigger="revealed" hx-indicator=".hx-indicator">
|
|
||||||
<img src="/images/spinner.svg" width="30" height="30" class="hx-indicator">
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -182,7 +182,12 @@ struct ViewControllerTests {
|
|||||||
extension ViewController {
|
extension ViewController {
|
||||||
|
|
||||||
func render(_ route: ViewRoute) async throws -> String {
|
func render(_ route: ViewRoute) async throws -> String {
|
||||||
guard let html = try await view(for: route, isHtmxRequest: true, authenticate: { _ in }) else {
|
guard let html = try await view(
|
||||||
|
for: route,
|
||||||
|
isHtmxRequest: true,
|
||||||
|
logger: .init(label: "tests"),
|
||||||
|
authenticate: { _ in }
|
||||||
|
) else {
|
||||||
throw TestError()
|
throw TestError()
|
||||||
}
|
}
|
||||||
return html.renderFormatted()
|
return html.renderFormatted()
|
||||||
@@ -380,6 +385,12 @@ extension PurchaseOrder {
|
|||||||
|
|
||||||
extension PurchaseOrder.Create {
|
extension PurchaseOrder.Create {
|
||||||
static var mock: Self {
|
static var mock: Self {
|
||||||
.init(materials: "bar", customer: "Testy McTestface", createdByID: UUID(0), createdForID: UUID(0), vendorBranchID: UUID(0))
|
.init(
|
||||||
|
materials: "bar",
|
||||||
|
customer: "Testy McTestface",
|
||||||
|
createdByID: UUID(0),
|
||||||
|
createdForID: UUID(0),
|
||||||
|
vendorBranchID: UUID(0)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user