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:
2025-01-24 10:55:59 -05:00
parent aa60f69758
commit 90c6058d56
37 changed files with 146 additions and 564 deletions

View File

@@ -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: [

View File

@@ -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)

View 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)
})
)
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -1,4 +0,0 @@
import Dependencies
import Elementary
import SharedModels
@_exported import ViewController

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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
)
})
}
}

View File

@@ -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
}
}
}

View File

@@ -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))
}
}

View File

@@ -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
)
}
}

View File

@@ -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&amp;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>

View File

@@ -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&amp;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>

View File

@@ -1 +0,0 @@
<select name="createdForID" class="col-3"><option value="00000000-0000-0000-0000-000000000000">Testy Mctestface</option></select>

View File

@@ -1 +0,0 @@
<select name="createdForID" class="col-6" style="margin-left: 15px;"><option value="00000000-0000-0000-0000-000000000000">Testy Mctestface</option></select>

View File

@@ -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&amp;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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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&amp;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>

View File

@@ -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>

View File

@@ -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&amp;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>

View File

@@ -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&amp;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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1 +0,0 @@
<select name="vendorBranchID" class="col-4"><option value="00000000-0000-0000-0000-000000000001">Test - Mock</option></select>

View File

@@ -1 +0,0 @@
<select name="vendorBranchID" class="col-4" style="margin-left: 15px;"><option value="00000000-0000-0000-0000-000000000001">Test - Mock</option></select>

View File

@@ -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>

View File

@@ -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&amp;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>

View File

@@ -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>

View File

@@ -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&amp;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>

View File

@@ -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&amp;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>

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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&amp;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>

View File

@@ -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)
)
}
}