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
|
||||
),
|
||||
.testTarget(
|
||||
name: "AppTests",
|
||||
dependencies: [
|
||||
.target(name: "App"),
|
||||
.target(name: "HtmlSnapshotTesting"),
|
||||
.product(name: "XCTVapor", package: "vapor")
|
||||
],
|
||||
resources: [
|
||||
.copy("__Snapshots__")
|
||||
],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
// .testTarget(
|
||||
// name: "AppTests",
|
||||
// dependencies: [
|
||||
// .target(name: "App"),
|
||||
// .target(name: "HtmlSnapshotTesting"),
|
||||
// .product(name: "XCTVapor", package: "vapor")
|
||||
// ],
|
||||
// resources: [
|
||||
// .copy("__Snapshots__")
|
||||
// ],
|
||||
// swiftSettings: swiftSettings
|
||||
// ),
|
||||
.testTarget(
|
||||
name: "ViewRouteTests",
|
||||
dependencies: [
|
||||
@@ -115,13 +115,6 @@ let package = Package(
|
||||
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "HtmlSnapshotTestingTests",
|
||||
dependencies: [
|
||||
.target(name: "App"),
|
||||
.target(name: "HtmlSnapshotTesting")
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "SharedModels",
|
||||
dependencies: [
|
||||
|
||||
@@ -7,13 +7,14 @@ import Vapor
|
||||
private let apiMiddleware: [any Middleware] = [
|
||||
UserPasswordAuthenticator(),
|
||||
UserTokenAuthenticator(),
|
||||
UserSessionAuthenticator(),
|
||||
User.guardMiddleware()
|
||||
]
|
||||
|
||||
extension ApiRoute {
|
||||
var middleware: [any Middleware]? { apiMiddleware }
|
||||
|
||||
func handle(request: Request) async throws -> any AsyncResponseEncodable {
|
||||
func respond(request: Request) async throws -> any AsyncResponseEncodable {
|
||||
switch self {
|
||||
case let .employee(route):
|
||||
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 Dependencies
|
||||
import Vapor
|
||||
import ViewControllerLive
|
||||
|
||||
// Taken from discussions page on `swift-dependencies`.
|
||||
|
||||
// TODO: Pass dependencies to set into this middleware.
|
||||
struct DependenciesMiddleware: AsyncMiddleware {
|
||||
|
||||
private let values: DependencyValues.Continuation
|
||||
private let database: DatabaseClient
|
||||
private let viewController: ViewController
|
||||
|
||||
init(
|
||||
database: DatabaseClient
|
||||
database: DatabaseClient,
|
||||
viewController: ViewController = .liveValue
|
||||
) {
|
||||
self.values = withEscapedDependencies { $0 }
|
||||
self.database = database
|
||||
self.viewController = viewController
|
||||
}
|
||||
|
||||
func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
|
||||
@@ -22,6 +25,7 @@ struct DependenciesMiddleware: AsyncMiddleware {
|
||||
try await withDependencies {
|
||||
$0.database = database
|
||||
$0.dateFormatter = .liveValue
|
||||
$0.viewController = viewController
|
||||
} operation: {
|
||||
try await next.respond(to: request)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ extension SharedModels.ViewRoute {
|
||||
|
||||
var middleware: [any Middleware]? {
|
||||
switch self {
|
||||
case .index: return viewProtectedMiddleware
|
||||
// case .index: return viewProtectedMiddleware
|
||||
case let .employee(route): return route.middleware
|
||||
case .login: return nil
|
||||
case let .purchaseOrder(route): return route.middleware
|
||||
@@ -48,6 +48,11 @@ public func configure(
|
||||
|
||||
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(
|
||||
SiteRoute.router,
|
||||
middleware: {
|
||||
@@ -79,7 +84,6 @@ extension SiteRoute {
|
||||
case .health:
|
||||
return nil
|
||||
case let .view(route):
|
||||
// return nil
|
||||
return route.middleware
|
||||
}
|
||||
}
|
||||
@@ -90,62 +94,13 @@ func siteHandler(
|
||||
request: Request,
|
||||
route: SiteRoute
|
||||
) async throws -> any AsyncResponseEncodable {
|
||||
@Dependency(\.viewController) var viewController
|
||||
switch route {
|
||||
case let .api(route):
|
||||
return try await route.handle(request: request)
|
||||
return try await route.respond(request: request)
|
||||
case .health:
|
||||
return HTTPStatus.ok
|
||||
case let .view(route):
|
||||
return try await route.respond(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)
|
||||
|
||||
})
|
||||
)
|
||||
return try await viewController.respond(route: route, request: request)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import Dependencies
|
||||
import Elementary
|
||||
import SharedModels
|
||||
@_exported import ViewController
|
||||
@@ -2,10 +2,9 @@ import CasePathsCore
|
||||
import Foundation
|
||||
@preconcurrency import URLRouting
|
||||
|
||||
// TODO: Remove `delete` routes from views and use api routes.
|
||||
public enum ViewRoute: Sendable, Equatable {
|
||||
|
||||
case index
|
||||
// case index
|
||||
case employee(EmployeeRoute)
|
||||
case login(LoginRoute)
|
||||
case purchaseOrder(PurchaseOrderRoute)
|
||||
@@ -14,9 +13,9 @@ public enum ViewRoute: Sendable, Equatable {
|
||||
case vendorBranch(VendorBranchRoute)
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.index)) {
|
||||
Method.get
|
||||
}
|
||||
// Route(.case(Self.index)) {
|
||||
// Method.get
|
||||
// }
|
||||
Route(.case(Self.employee)) { EmployeeRoute.router }
|
||||
Route(.case(Self.login)) { LoginRoute.router }
|
||||
Route(.case(Self.purchaseOrder)) { PurchaseOrderRoute.router }
|
||||
@@ -31,7 +30,6 @@ public extension ViewRoute {
|
||||
|
||||
enum EmployeeRoute: Sendable, Equatable {
|
||||
case create(Employee.Create)
|
||||
case delete(id: Employee.ID)
|
||||
case form
|
||||
case get(id: Employee.ID)
|
||||
case index
|
||||
@@ -53,10 +51,6 @@ public extension ViewRoute {
|
||||
.map(.memberwise(Employee.Create.init))
|
||||
}
|
||||
}
|
||||
Route(.case(Self.delete(id:))) {
|
||||
Path { rootPath; Employee.ID.parser() }
|
||||
Method.delete
|
||||
}
|
||||
Route(.case(Self.get(id:))) {
|
||||
Path { rootPath; Employee.ID.parser() }
|
||||
Method.get
|
||||
@@ -147,7 +141,6 @@ public extension ViewRoute {
|
||||
public extension ViewRoute {
|
||||
enum PurchaseOrderRoute: Sendable, Equatable {
|
||||
case create(PurchaseOrder.Create)
|
||||
case delete(id: PurchaseOrder.ID)
|
||||
case form
|
||||
case get(id: PurchaseOrder.ID)
|
||||
case index
|
||||
@@ -174,11 +167,6 @@ public extension ViewRoute {
|
||||
.map(.memberwise(PurchaseOrder.Create.init))
|
||||
}
|
||||
}
|
||||
Route(.case(Self.delete(id:))) {
|
||||
Path { rootPath; Digits() }
|
||||
Method.delete
|
||||
}
|
||||
|
||||
Route(.case(Self.form)) {
|
||||
Path { rootPath; "create" }
|
||||
Method.get
|
||||
@@ -275,7 +263,6 @@ public extension ViewRoute {
|
||||
public extension ViewRoute {
|
||||
enum UserRoute: Sendable, Equatable {
|
||||
case create(User.Create)
|
||||
case delete(id: User.ID)
|
||||
case form
|
||||
case get(id: User.ID)
|
||||
case index
|
||||
@@ -297,10 +284,6 @@ public extension ViewRoute {
|
||||
.map(.memberwise(User.Create.init))
|
||||
}
|
||||
}
|
||||
Route(.case(Self.delete(id:))) {
|
||||
Path { rootPath; User.ID.parser() }
|
||||
Method.delete
|
||||
}
|
||||
Route(.case(Self.form)) {
|
||||
Path { rootPath; "create" }
|
||||
Method.get
|
||||
@@ -335,7 +318,6 @@ public extension ViewRoute {
|
||||
|
||||
enum VendorRoute: Sendable, Equatable {
|
||||
case create(Vendor.Create)
|
||||
case delete(id: Vendor.ID)
|
||||
case form
|
||||
case get(id: Vendor.ID)
|
||||
case index
|
||||
@@ -354,10 +336,6 @@ public extension ViewRoute {
|
||||
.map(.memberwise(Vendor.Create.init))
|
||||
}
|
||||
}
|
||||
Route(.case(Self.delete(id:))) {
|
||||
Path { rootPath; Vendor.ID.parser() }
|
||||
Method.delete
|
||||
}
|
||||
Route(.case(Self.get(id:))) {
|
||||
Path { rootPath; Vendor.ID.parser() }
|
||||
Method.get
|
||||
@@ -389,7 +367,6 @@ public extension ViewRoute {
|
||||
|
||||
enum VendorBranchRoute: Sendable, Equatable {
|
||||
case create(VendorBranch.Create)
|
||||
case delete(id: VendorBranch.ID)
|
||||
case index(for: Vendor.ID? = nil)
|
||||
case select(context: ViewRoute.SelectContext)
|
||||
|
||||
@@ -405,10 +382,6 @@ public extension ViewRoute {
|
||||
.map(.memberwise(VendorBranch.Create.init))
|
||||
}
|
||||
}
|
||||
Route(.case(Self.delete(id:))) {
|
||||
Path { "vendors"; "branches"; VendorBranch.ID.parser() }
|
||||
Method.delete
|
||||
}
|
||||
Route(.case(Self.index(for:))) {
|
||||
Path { "vendors"; "branches" }
|
||||
Method.get
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import Elementary
|
||||
import Logging
|
||||
import SharedModels
|
||||
|
||||
public extension DependencyValues {
|
||||
@@ -10,19 +11,41 @@ public extension DependencyValues {
|
||||
}
|
||||
}
|
||||
|
||||
public typealias AnySendableHTML = (any HTML & Sendable)
|
||||
|
||||
@DependencyClient
|
||||
public struct ViewController: Sendable {
|
||||
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
|
||||
public func view(
|
||||
for route: ViewRoute,
|
||||
isHtmxRequest: Bool,
|
||||
logger: Logger,
|
||||
authenticate: @escaping AuthenticateHandler
|
||||
) async throws -> (any HTML)? {
|
||||
try await view(route, isHtmxRequest, authenticate)
|
||||
) async throws -> AnySendableHTML {
|
||||
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 {
|
||||
public static var liveValue: ViewController {
|
||||
.init(view: { route, isHtmxRequest, authenticate in
|
||||
try await route.view(isHtmxRequest: isHtmxRequest, authenticate: authenticate)
|
||||
.init(view: { request in
|
||||
try await request.route.view(
|
||||
isHtmxRequest: request.isHtmxRequest,
|
||||
logger: request.logger,
|
||||
authenticate: request.authenticate
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import Elementary
|
||||
import Logging
|
||||
import SharedModels
|
||||
import Vapor
|
||||
|
||||
public typealias AnySendableHTML = (any HTML & Sendable)
|
||||
import ViewController
|
||||
|
||||
public extension SharedModels.ViewRoute {
|
||||
|
||||
func view(
|
||||
isHtmxRequest: Bool,
|
||||
logger: Logger,
|
||||
authenticate: @escaping @Sendable (User) -> Void
|
||||
) async throws -> AnySendableHTML? {
|
||||
) async throws -> AnySendableHTML {
|
||||
@Dependency(\.database.users) var users
|
||||
switch self {
|
||||
case .index:
|
||||
// return request.redirect(to: Self.router.path(for: .purchaseOrder(.index)))
|
||||
return nil
|
||||
// case .index:
|
||||
// // This get's redirected to purchase-orders route in the app / site handler.
|
||||
// return nil
|
||||
|
||||
case let .employee(route):
|
||||
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 user = try await users.get(token.userID)!
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
switch self {
|
||||
@@ -83,17 +84,10 @@ extension SharedModels.ViewRoute.EmployeeRoute {
|
||||
throw Abort(.badRequest, reason: "Employee id not found.")
|
||||
}
|
||||
return try await render(mainPage, isHtmxRequest, EmployeeForm(employee: employee))
|
||||
// return try await request.render(mainPage: mainPage) {
|
||||
// EmployeeForm(employee: employee)
|
||||
// }
|
||||
|
||||
case let .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):
|
||||
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
|
||||
|
||||
switch self {
|
||||
@@ -130,10 +124,6 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute {
|
||||
case let .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:
|
||||
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
|
||||
|
||||
switch self {
|
||||
@@ -208,10 +198,6 @@ extension SharedModels.ViewRoute.UserRoute {
|
||||
case let .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:
|
||||
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
|
||||
|
||||
switch self {
|
||||
case .form:
|
||||
// html = VendorForm(.float(shouldShow: true))
|
||||
return try await render(mainPage, isHtmxRequest, VendorForm(.float(shouldShow: true)))
|
||||
|
||||
case .index:
|
||||
// return VendorForm()
|
||||
return try await mainPage(VendorForm())
|
||||
|
||||
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):
|
||||
guard let vendor = try await database.vendors.get(id, .withBranches) else {
|
||||
throw Abort(.badRequest, reason: "Vendor not found.")
|
||||
@@ -282,7 +262,7 @@ extension SharedModels.ViewRoute.VendorRoute {
|
||||
|
||||
extension SharedModels.ViewRoute.VendorBranchRoute {
|
||||
|
||||
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML? {
|
||||
func view(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||
@Dependency(\.database) var database
|
||||
|
||||
switch self {
|
||||
@@ -300,10 +280,6 @@ extension SharedModels.ViewRoute.VendorBranchRoute {
|
||||
|
||||
case let .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))
|
||||
}
|
||||
|
||||
static func delete(route: SharedModels.ViewRoute) -> HTMLAttribute {
|
||||
delete(SharedModels.ViewRoute.router.path(for: route))
|
||||
static func delete(route: SharedModels.ApiRoute) -> HTMLAttribute {
|
||||
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 {
|
||||
|
||||
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()
|
||||
}
|
||||
return html.renderFormatted()
|
||||
@@ -380,6 +385,12 @@ extension PurchaseOrder {
|
||||
|
||||
extension PurchaseOrder.Create {
|
||||
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