From dc43d28a895555f9b4565bcea1972a18c8d3cf94 Mon Sep 17 00:00:00 2001 From: Graeme Ross Date: Sun, 6 Oct 2024 17:12:25 +0100 Subject: [PATCH] Create schema and scaffold --- .../migration.sql | 44 ++ api/db/migrations/migration_lock.toml | 3 + api/db/schema.prisma | 45 +- api/src/graphql/accounts.sdl.ts | 37 ++ api/src/graphql/contactAddresses.sdl.ts | 46 ++ api/src/graphql/roles.sdl.ts | 29 ++ api/src/graphql/users.sdl.ts | 34 ++ .../services/accounts/accounts.scenarios.ts | 37 ++ api/src/services/accounts/accounts.test.ts | 61 +++ api/src/services/accounts/accounts.ts | 50 +++ .../contactAddresses.scenarios.ts | 29 ++ .../contactAddresses/contactAddresses.test.ts | 79 ++++ .../contactAddresses/contactAddresses.ts | 48 +++ api/src/services/roles/roles.scenarios.ts | 11 + api/src/services/roles/roles.test.ts | 49 +++ api/src/services/roles/roles.ts | 42 ++ api/src/services/users/users.scenarios.ts | 41 ++ api/src/services/users/users.test.ts | 55 +++ api/src/services/users/users.ts | 48 +++ web/package.json | 1 + web/src/App.tsx | 2 + web/src/Routes.tsx | 35 +- .../components/Account/Account/Account.tsx | 98 +++++ .../Account/AccountCell/AccountCell.tsx | 40 ++ .../Account/AccountForm/AccountForm.tsx | 117 ++++++ .../components/Account/Accounts/Accounts.tsx | 102 +++++ .../Account/AccountsCell/AccountsCell.tsx | 46 ++ .../EditAccountCell/EditAccountCell.tsx | 89 ++++ .../Account/NewAccount/NewAccount.tsx | 55 +++ .../ContactAddress/ContactAddress.tsx | 106 +++++ .../ContactAddressCell/ContactAddressCell.tsx | 48 +++ .../ContactAddressForm/ContactAddressForm.tsx | 161 +++++++ .../ContactAddresses/ContactAddresses.tsx | 108 +++++ .../ContactAddressesCell.tsx | 53 +++ .../EditContactAddressCell.tsx | 98 +++++ .../NewContactAddress/NewContactAddress.tsx | 55 +++ .../Role/EditRoleCell/EditRoleCell.tsx | 74 ++++ web/src/components/Role/NewRole/NewRole.tsx | 52 +++ web/src/components/Role/Role/Role.tsx | 90 ++++ web/src/components/Role/RoleCell/RoleCell.tsx | 34 ++ web/src/components/Role/RoleForm/RoleForm.tsx | 82 ++++ web/src/components/Role/Roles/Roles.tsx | 98 +++++ .../components/Role/RolesCell/RolesCell.tsx | 43 ++ .../User/EditUserCell/EditUserCell.tsx | 76 ++++ web/src/components/User/NewUser/NewUser.tsx | 52 +++ web/src/components/User/User/User.tsx | 94 +++++ web/src/components/User/UserCell/UserCell.tsx | 35 ++ web/src/components/User/UserForm/UserForm.tsx | 101 +++++ web/src/components/User/Users/Users.tsx | 100 +++++ .../components/User/UsersCell/UsersCell.tsx | 44 ++ .../ClientLayout/ClientLayout.stories.tsx | 13 + .../ClientLayout/ClientLayout.test.tsx | 14 + web/src/layouts/ClientLayout/ClientLayout.tsx | 25 ++ .../layouts/ScaffoldLayout/ScaffoldLayout.tsx | 37 ++ web/src/lib/formatters.test.tsx | 192 +++++++++ web/src/lib/formatters.tsx | 58 +++ .../pages/Account/AccountPage/AccountPage.tsx | 11 + .../Account/AccountsPage/AccountsPage.tsx | 7 + .../EditAccountPage/EditAccountPage.tsx | 11 + .../Account/NewAccountPage/NewAccountPage.tsx | 7 + .../ContactAddressPage/ContactAddressPage.tsx | 11 + .../ContactAddressesPage.tsx | 7 + .../EditContactAddressPage.tsx | 11 + .../NewContactAddressPage.tsx | 7 + web/src/pages/HomePage/HomePage.stories.tsx | 13 + web/src/pages/HomePage/HomePage.test.tsx | 14 + web/src/pages/HomePage/HomePage.tsx | 21 + .../pages/Role/EditRolePage/EditRolePage.tsx | 11 + .../pages/Role/NewRolePage/NewRolePage.tsx | 7 + web/src/pages/Role/RolePage/RolePage.tsx | 11 + web/src/pages/Role/RolesPage/RolesPage.tsx | 7 + .../pages/User/EditUserPage/EditUserPage.tsx | 11 + .../pages/User/NewUserPage/NewUserPage.tsx | 7 + web/src/pages/User/UserPage/UserPage.tsx | 11 + web/src/pages/User/UsersPage/UsersPage.tsx | 7 + web/src/scaffold.css | 397 ++++++++++++++++++ yarn.lock | 1 + 77 files changed, 3899 insertions(+), 7 deletions(-) create mode 100644 api/db/migrations/20241006155320_initial_setup/migration.sql create mode 100644 api/db/migrations/migration_lock.toml create mode 100644 api/src/graphql/accounts.sdl.ts create mode 100644 api/src/graphql/contactAddresses.sdl.ts create mode 100644 api/src/graphql/roles.sdl.ts create mode 100644 api/src/graphql/users.sdl.ts create mode 100644 api/src/services/accounts/accounts.scenarios.ts create mode 100644 api/src/services/accounts/accounts.test.ts create mode 100644 api/src/services/accounts/accounts.ts create mode 100644 api/src/services/contactAddresses/contactAddresses.scenarios.ts create mode 100644 api/src/services/contactAddresses/contactAddresses.test.ts create mode 100644 api/src/services/contactAddresses/contactAddresses.ts create mode 100644 api/src/services/roles/roles.scenarios.ts create mode 100644 api/src/services/roles/roles.test.ts create mode 100644 api/src/services/roles/roles.ts create mode 100644 api/src/services/users/users.scenarios.ts create mode 100644 api/src/services/users/users.test.ts create mode 100644 api/src/services/users/users.ts create mode 100644 web/src/components/Account/Account/Account.tsx create mode 100644 web/src/components/Account/AccountCell/AccountCell.tsx create mode 100644 web/src/components/Account/AccountForm/AccountForm.tsx create mode 100644 web/src/components/Account/Accounts/Accounts.tsx create mode 100644 web/src/components/Account/AccountsCell/AccountsCell.tsx create mode 100644 web/src/components/Account/EditAccountCell/EditAccountCell.tsx create mode 100644 web/src/components/Account/NewAccount/NewAccount.tsx create mode 100644 web/src/components/ContactAddress/ContactAddress/ContactAddress.tsx create mode 100644 web/src/components/ContactAddress/ContactAddressCell/ContactAddressCell.tsx create mode 100644 web/src/components/ContactAddress/ContactAddressForm/ContactAddressForm.tsx create mode 100644 web/src/components/ContactAddress/ContactAddresses/ContactAddresses.tsx create mode 100644 web/src/components/ContactAddress/ContactAddressesCell/ContactAddressesCell.tsx create mode 100644 web/src/components/ContactAddress/EditContactAddressCell/EditContactAddressCell.tsx create mode 100644 web/src/components/ContactAddress/NewContactAddress/NewContactAddress.tsx create mode 100644 web/src/components/Role/EditRoleCell/EditRoleCell.tsx create mode 100644 web/src/components/Role/NewRole/NewRole.tsx create mode 100644 web/src/components/Role/Role/Role.tsx create mode 100644 web/src/components/Role/RoleCell/RoleCell.tsx create mode 100644 web/src/components/Role/RoleForm/RoleForm.tsx create mode 100644 web/src/components/Role/Roles/Roles.tsx create mode 100644 web/src/components/Role/RolesCell/RolesCell.tsx create mode 100644 web/src/components/User/EditUserCell/EditUserCell.tsx create mode 100644 web/src/components/User/NewUser/NewUser.tsx create mode 100644 web/src/components/User/User/User.tsx create mode 100644 web/src/components/User/UserCell/UserCell.tsx create mode 100644 web/src/components/User/UserForm/UserForm.tsx create mode 100644 web/src/components/User/Users/Users.tsx create mode 100644 web/src/components/User/UsersCell/UsersCell.tsx create mode 100644 web/src/layouts/ClientLayout/ClientLayout.stories.tsx create mode 100644 web/src/layouts/ClientLayout/ClientLayout.test.tsx create mode 100644 web/src/layouts/ClientLayout/ClientLayout.tsx create mode 100644 web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx create mode 100644 web/src/lib/formatters.test.tsx create mode 100644 web/src/lib/formatters.tsx create mode 100644 web/src/pages/Account/AccountPage/AccountPage.tsx create mode 100644 web/src/pages/Account/AccountsPage/AccountsPage.tsx create mode 100644 web/src/pages/Account/EditAccountPage/EditAccountPage.tsx create mode 100644 web/src/pages/Account/NewAccountPage/NewAccountPage.tsx create mode 100644 web/src/pages/ContactAddress/ContactAddressPage/ContactAddressPage.tsx create mode 100644 web/src/pages/ContactAddress/ContactAddressesPage/ContactAddressesPage.tsx create mode 100644 web/src/pages/ContactAddress/EditContactAddressPage/EditContactAddressPage.tsx create mode 100644 web/src/pages/ContactAddress/NewContactAddressPage/NewContactAddressPage.tsx create mode 100644 web/src/pages/HomePage/HomePage.stories.tsx create mode 100644 web/src/pages/HomePage/HomePage.test.tsx create mode 100644 web/src/pages/HomePage/HomePage.tsx create mode 100644 web/src/pages/Role/EditRolePage/EditRolePage.tsx create mode 100644 web/src/pages/Role/NewRolePage/NewRolePage.tsx create mode 100644 web/src/pages/Role/RolePage/RolePage.tsx create mode 100644 web/src/pages/Role/RolesPage/RolesPage.tsx create mode 100644 web/src/pages/User/EditUserPage/EditUserPage.tsx create mode 100644 web/src/pages/User/NewUserPage/NewUserPage.tsx create mode 100644 web/src/pages/User/UserPage/UserPage.tsx create mode 100644 web/src/pages/User/UsersPage/UsersPage.tsx create mode 100644 web/src/scaffold.css diff --git a/api/db/migrations/20241006155320_initial_setup/migration.sql b/api/db/migrations/20241006155320_initial_setup/migration.sql new file mode 100644 index 0000000..f3c2a3d --- /dev/null +++ b/api/db/migrations/20241006155320_initial_setup/migration.sql @@ -0,0 +1,44 @@ +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL PRIMARY KEY, + "active" BOOLEAN NOT NULL DEFAULT true, + "PaidUp" BOOLEAN NOT NULL DEFAULT true, + "contactAddressId" TEXT NOT NULL, + "userId" TEXT, + CONSTRAINT "Account_contactAddressId_fkey" FOREIGN KEY ("contactAddressId") REFERENCES "ContactAddress" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ContactAddress" ( + "id" TEXT NOT NULL PRIMARY KEY, + "Name" TEXT NOT NULL, + "Address1" TEXT NOT NULL, + "Address2" TEXT NOT NULL, + "Address3" TEXT NOT NULL, + "Town" TEXT NOT NULL, + "PostalCode" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "email" TEXT NOT NULL, + "contactAddressId" TEXT NOT NULL, + CONSTRAINT "User_contactAddressId_fkey" FOREIGN KEY ("contactAddressId") REFERENCES "ContactAddress" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Role" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "userId" TEXT, + CONSTRAINT "Role_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_userId_key" ON "User"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name"); diff --git a/api/db/migrations/migration_lock.toml b/api/db/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/api/db/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/api/db/schema.prisma b/api/db/schema.prisma index 8c86666..0cdeb80 100644 --- a/api/db/schema.prisma +++ b/api/db/schema.prisma @@ -16,9 +16,44 @@ generator client { // Define your own datamodels here and run `yarn redwood prisma migrate dev` // to create migrations for them and apply to your dev DB. -// TODO: Please remove the following example: -model UserExample { - id Int @id @default(autoincrement()) - email String @unique - name String? + +model Account { + id String @id @default(cuid()) + address ContactAddress @relation(fields: [contactAddressId], references: [id]) + active Boolean @default(true) //Are we still supporting this pendant + // We will give a years(?) grace + PaidUp Boolean @default(true) //Have we received a subscription payment this month + + contactAddressId String + User User? @relation(fields: [userId], references: [id]) + userId String? +} + +model ContactAddress { + id String @id @default(cuid()) + Name String // name of person who is using the pendant + Address1 String + Address2 String + Address3 String + Town String + PostalCode String + Account Account[] + User User[] +} + +model User { + id String @id @default(cuid()) + userId String @unique + email String + address ContactAddress @relation(fields: [contactAddressId], references: [id]) + contactAddressId String + accounts Account[] + roles Role[] +} + +model Role { + id String @id @default(cuid()) + name String @unique + User User? @relation(fields: [userId], references: [id]) + userId String? } diff --git a/api/src/graphql/accounts.sdl.ts b/api/src/graphql/accounts.sdl.ts new file mode 100644 index 0000000..8e6763e --- /dev/null +++ b/api/src/graphql/accounts.sdl.ts @@ -0,0 +1,37 @@ +export const schema = gql` + type Account { + id: String! + address: ContactAddress! + active: Boolean! + PaidUp: Boolean! + contactAddressId: String! + User: User + userId: String + } + + type Query { + accounts: [Account!]! @requireAuth + account(id: String!): Account @requireAuth + } + + input CreateAccountInput { + active: Boolean! + PaidUp: Boolean! + contactAddressId: String! + userId: String + } + + input UpdateAccountInput { + active: Boolean + PaidUp: Boolean + contactAddressId: String + userId: String + } + + type Mutation { + createAccount(input: CreateAccountInput!): Account! @requireAuth + updateAccount(id: String!, input: UpdateAccountInput!): Account! + @requireAuth + deleteAccount(id: String!): Account! @requireAuth + } +` diff --git a/api/src/graphql/contactAddresses.sdl.ts b/api/src/graphql/contactAddresses.sdl.ts new file mode 100644 index 0000000..0fc22e5 --- /dev/null +++ b/api/src/graphql/contactAddresses.sdl.ts @@ -0,0 +1,46 @@ +export const schema = gql` + type ContactAddress { + id: String! + Name: String! + Address1: String! + Address2: String! + Address3: String! + Town: String! + PostalCode: String! + Account: [Account]! + User: [User]! + } + + type Query { + contactAddresses: [ContactAddress!]! @requireAuth + contactAddress(id: String!): ContactAddress @requireAuth + } + + input CreateContactAddressInput { + Name: String! + Address1: String! + Address2: String! + Address3: String! + Town: String! + PostalCode: String! + } + + input UpdateContactAddressInput { + Name: String + Address1: String + Address2: String + Address3: String + Town: String + PostalCode: String + } + + type Mutation { + createContactAddress(input: CreateContactAddressInput!): ContactAddress! + @requireAuth + updateContactAddress( + id: String! + input: UpdateContactAddressInput! + ): ContactAddress! @requireAuth + deleteContactAddress(id: String!): ContactAddress! @requireAuth + } +` diff --git a/api/src/graphql/roles.sdl.ts b/api/src/graphql/roles.sdl.ts new file mode 100644 index 0000000..616130f --- /dev/null +++ b/api/src/graphql/roles.sdl.ts @@ -0,0 +1,29 @@ +export const schema = gql` + type Role { + id: String! + name: String! + User: User + userId: String + } + + type Query { + roles: [Role!]! @requireAuth + role(id: String!): Role @requireAuth + } + + input CreateRoleInput { + name: String! + userId: String + } + + input UpdateRoleInput { + name: String + userId: String + } + + type Mutation { + createRole(input: CreateRoleInput!): Role! @requireAuth + updateRole(id: String!, input: UpdateRoleInput!): Role! @requireAuth + deleteRole(id: String!): Role! @requireAuth + } +` diff --git a/api/src/graphql/users.sdl.ts b/api/src/graphql/users.sdl.ts new file mode 100644 index 0000000..338d67b --- /dev/null +++ b/api/src/graphql/users.sdl.ts @@ -0,0 +1,34 @@ +export const schema = gql` + type User { + id: String! + userId: String! + email: String! + address: ContactAddress! + contactAddressId: String! + accounts: [Account]! + roles: [Role]! + } + + type Query { + users: [User!]! @requireAuth + user(id: String!): User @requireAuth + } + + input CreateUserInput { + userId: String! + email: String! + contactAddressId: String! + } + + input UpdateUserInput { + userId: String + email: String + contactAddressId: String + } + + type Mutation { + createUser(input: CreateUserInput!): User! @requireAuth + updateUser(id: String!, input: UpdateUserInput!): User! @requireAuth + deleteUser(id: String!): User! @requireAuth + } +` diff --git a/api/src/services/accounts/accounts.scenarios.ts b/api/src/services/accounts/accounts.scenarios.ts new file mode 100644 index 0000000..4da7f14 --- /dev/null +++ b/api/src/services/accounts/accounts.scenarios.ts @@ -0,0 +1,37 @@ +import type { Prisma, Account } from '@prisma/client' +import type { ScenarioData } from '@redwoodjs/testing/api' + +export const standard = defineScenario({ + account: { + one: { + data: { + address: { + create: { + Name: 'String', + Address1: 'String', + Address2: 'String', + Address3: 'String', + Town: 'String', + PostalCode: 'String', + }, + }, + }, + }, + two: { + data: { + address: { + create: { + Name: 'String', + Address1: 'String', + Address2: 'String', + Address3: 'String', + Town: 'String', + PostalCode: 'String', + }, + }, + }, + }, + }, +}) + +export type StandardScenario = ScenarioData diff --git a/api/src/services/accounts/accounts.test.ts b/api/src/services/accounts/accounts.test.ts new file mode 100644 index 0000000..1cc3013 --- /dev/null +++ b/api/src/services/accounts/accounts.test.ts @@ -0,0 +1,61 @@ +import type { Account } from '@prisma/client' + +import { + accounts, + account, + createAccount, + updateAccount, + deleteAccount, +} from './accounts' +import type { StandardScenario } from './accounts.scenarios' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-services +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('accounts', () => { + scenario('returns all accounts', async (scenario: StandardScenario) => { + const result = await accounts() + + expect(result.length).toEqual(Object.keys(scenario.account).length) + }) + + scenario('returns a single account', async (scenario: StandardScenario) => { + const result = await account({ id: scenario.account.one.id }) + + expect(result).toEqual(scenario.account.one) + }) + + scenario('creates a account', async (scenario: StandardScenario) => { + const result = await createAccount({ + input: { contactAddressId: scenario.account.two.contactAddressId }, + }) + + expect(result.contactAddressId).toEqual( + scenario.account.two.contactAddressId + ) + }) + + scenario('updates a account', async (scenario: StandardScenario) => { + const original = (await account({ id: scenario.account.one.id })) as Account + const result = await updateAccount({ + id: original.id, + input: { contactAddressId: scenario.account.two.contactAddressId }, + }) + + expect(result.contactAddressId).toEqual( + scenario.account.two.contactAddressId + ) + }) + + scenario('deletes a account', async (scenario: StandardScenario) => { + const original = (await deleteAccount({ + id: scenario.account.one.id, + })) as Account + const result = await account({ id: original.id }) + + expect(result).toEqual(null) + }) +}) diff --git a/api/src/services/accounts/accounts.ts b/api/src/services/accounts/accounts.ts new file mode 100644 index 0000000..7c96ac6 --- /dev/null +++ b/api/src/services/accounts/accounts.ts @@ -0,0 +1,50 @@ +import type { + QueryResolvers, + MutationResolvers, + AccountRelationResolvers, +} from 'types/graphql' + +import { db } from 'src/lib/db' + +export const accounts: QueryResolvers['accounts'] = () => { + return db.account.findMany() +} + +export const account: QueryResolvers['account'] = ({ id }) => { + return db.account.findUnique({ + where: { id }, + }) +} + +export const createAccount: MutationResolvers['createAccount'] = ({ + input, +}) => { + return db.account.create({ + data: input, + }) +} + +export const updateAccount: MutationResolvers['updateAccount'] = ({ + id, + input, +}) => { + return db.account.update({ + data: input, + where: { id }, + }) +} + +export const deleteAccount: MutationResolvers['deleteAccount'] = ({ id }) => { + return db.account.delete({ + where: { id }, + }) +} + +export const Account: AccountRelationResolvers = { + address: (_obj, { root }) => { + return db.account.findUnique({ where: { id: root?.id } }).address() + }, + User: (_obj, { root }) => { + return db.account.findUnique({ where: { id: root?.id } }).User() + }, +} diff --git a/api/src/services/contactAddresses/contactAddresses.scenarios.ts b/api/src/services/contactAddresses/contactAddresses.scenarios.ts new file mode 100644 index 0000000..322e9b0 --- /dev/null +++ b/api/src/services/contactAddresses/contactAddresses.scenarios.ts @@ -0,0 +1,29 @@ +import type { Prisma, ContactAddress } from '@prisma/client' +import type { ScenarioData } from '@redwoodjs/testing/api' + +export const standard = defineScenario({ + contactAddress: { + one: { + data: { + Name: 'String', + Address1: 'String', + Address2: 'String', + Address3: 'String', + Town: 'String', + PostalCode: 'String', + }, + }, + two: { + data: { + Name: 'String', + Address1: 'String', + Address2: 'String', + Address3: 'String', + Town: 'String', + PostalCode: 'String', + }, + }, + }, +}) + +export type StandardScenario = ScenarioData diff --git a/api/src/services/contactAddresses/contactAddresses.test.ts b/api/src/services/contactAddresses/contactAddresses.test.ts new file mode 100644 index 0000000..de773a1 --- /dev/null +++ b/api/src/services/contactAddresses/contactAddresses.test.ts @@ -0,0 +1,79 @@ +import type { ContactAddress } from '@prisma/client' + +import { + contactAddresses, + contactAddress, + createContactAddress, + updateContactAddress, + deleteContactAddress, +} from './contactAddresses' +import type { StandardScenario } from './contactAddresses.scenarios' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-services +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('contactAddresses', () => { + scenario( + 'returns all contactAddresses', + async (scenario: StandardScenario) => { + const result = await contactAddresses() + + expect(result.length).toEqual(Object.keys(scenario.contactAddress).length) + } + ) + + scenario( + 'returns a single contactAddress', + async (scenario: StandardScenario) => { + const result = await contactAddress({ + id: scenario.contactAddress.one.id, + }) + + expect(result).toEqual(scenario.contactAddress.one) + } + ) + + scenario('creates a contactAddress', async () => { + const result = await createContactAddress({ + input: { + Name: 'String', + Address1: 'String', + Address2: 'String', + Address3: 'String', + Town: 'String', + PostalCode: 'String', + }, + }) + + expect(result.Name).toEqual('String') + expect(result.Address1).toEqual('String') + expect(result.Address2).toEqual('String') + expect(result.Address3).toEqual('String') + expect(result.Town).toEqual('String') + expect(result.PostalCode).toEqual('String') + }) + + scenario('updates a contactAddress', async (scenario: StandardScenario) => { + const original = (await contactAddress({ + id: scenario.contactAddress.one.id, + })) as ContactAddress + const result = await updateContactAddress({ + id: original.id, + input: { Name: 'String2' }, + }) + + expect(result.Name).toEqual('String2') + }) + + scenario('deletes a contactAddress', async (scenario: StandardScenario) => { + const original = (await deleteContactAddress({ + id: scenario.contactAddress.one.id, + })) as ContactAddress + const result = await contactAddress({ id: original.id }) + + expect(result).toEqual(null) + }) +}) diff --git a/api/src/services/contactAddresses/contactAddresses.ts b/api/src/services/contactAddresses/contactAddresses.ts new file mode 100644 index 0000000..b1c4669 --- /dev/null +++ b/api/src/services/contactAddresses/contactAddresses.ts @@ -0,0 +1,48 @@ +import type { + QueryResolvers, + MutationResolvers, + ContactAddressRelationResolvers, +} from 'types/graphql' + +import { db } from 'src/lib/db' + +export const contactAddresses: QueryResolvers['contactAddresses'] = () => { + return db.contactAddress.findMany() +} + +export const contactAddress: QueryResolvers['contactAddress'] = ({ id }) => { + return db.contactAddress.findUnique({ + where: { id }, + }) +} + +export const createContactAddress: MutationResolvers['createContactAddress'] = + ({ input }) => { + return db.contactAddress.create({ + data: input, + }) + } + +export const updateContactAddress: MutationResolvers['updateContactAddress'] = + ({ id, input }) => { + return db.contactAddress.update({ + data: input, + where: { id }, + }) + } + +export const deleteContactAddress: MutationResolvers['deleteContactAddress'] = + ({ id }) => { + return db.contactAddress.delete({ + where: { id }, + }) + } + +export const ContactAddress: ContactAddressRelationResolvers = { + Account: (_obj, { root }) => { + return db.contactAddress.findUnique({ where: { id: root?.id } }).Account() + }, + User: (_obj, { root }) => { + return db.contactAddress.findUnique({ where: { id: root?.id } }).User() + }, +} diff --git a/api/src/services/roles/roles.scenarios.ts b/api/src/services/roles/roles.scenarios.ts new file mode 100644 index 0000000..8f79ebb --- /dev/null +++ b/api/src/services/roles/roles.scenarios.ts @@ -0,0 +1,11 @@ +import type { Prisma, Role } from '@prisma/client' +import type { ScenarioData } from '@redwoodjs/testing/api' + +export const standard = defineScenario({ + role: { + one: { data: { name: 'String3272615' } }, + two: { data: { name: 'String6245503' } }, + }, +}) + +export type StandardScenario = ScenarioData diff --git a/api/src/services/roles/roles.test.ts b/api/src/services/roles/roles.test.ts new file mode 100644 index 0000000..776ffda --- /dev/null +++ b/api/src/services/roles/roles.test.ts @@ -0,0 +1,49 @@ +import type { Role } from '@prisma/client' + +import { roles, role, createRole, updateRole, deleteRole } from './roles' +import type { StandardScenario } from './roles.scenarios' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-services +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('roles', () => { + scenario('returns all roles', async (scenario: StandardScenario) => { + const result = await roles() + + expect(result.length).toEqual(Object.keys(scenario.role).length) + }) + + scenario('returns a single role', async (scenario: StandardScenario) => { + const result = await role({ id: scenario.role.one.id }) + + expect(result).toEqual(scenario.role.one) + }) + + scenario('creates a role', async () => { + const result = await createRole({ + input: { name: 'String8217040' }, + }) + + expect(result.name).toEqual('String8217040') + }) + + scenario('updates a role', async (scenario: StandardScenario) => { + const original = (await role({ id: scenario.role.one.id })) as Role + const result = await updateRole({ + id: original.id, + input: { name: 'String11018312' }, + }) + + expect(result.name).toEqual('String11018312') + }) + + scenario('deletes a role', async (scenario: StandardScenario) => { + const original = (await deleteRole({ id: scenario.role.one.id })) as Role + const result = await role({ id: original.id }) + + expect(result).toEqual(null) + }) +}) diff --git a/api/src/services/roles/roles.ts b/api/src/services/roles/roles.ts new file mode 100644 index 0000000..0f0fac8 --- /dev/null +++ b/api/src/services/roles/roles.ts @@ -0,0 +1,42 @@ +import type { + QueryResolvers, + MutationResolvers, + RoleRelationResolvers, +} from 'types/graphql' + +import { db } from 'src/lib/db' + +export const roles: QueryResolvers['roles'] = () => { + return db.role.findMany() +} + +export const role: QueryResolvers['role'] = ({ id }) => { + return db.role.findUnique({ + where: { id }, + }) +} + +export const createRole: MutationResolvers['createRole'] = ({ input }) => { + return db.role.create({ + data: input, + }) +} + +export const updateRole: MutationResolvers['updateRole'] = ({ id, input }) => { + return db.role.update({ + data: input, + where: { id }, + }) +} + +export const deleteRole: MutationResolvers['deleteRole'] = ({ id }) => { + return db.role.delete({ + where: { id }, + }) +} + +export const Role: RoleRelationResolvers = { + User: (_obj, { root }) => { + return db.role.findUnique({ where: { id: root?.id } }).User() + }, +} diff --git a/api/src/services/users/users.scenarios.ts b/api/src/services/users/users.scenarios.ts new file mode 100644 index 0000000..913bc33 --- /dev/null +++ b/api/src/services/users/users.scenarios.ts @@ -0,0 +1,41 @@ +import type { Prisma, User } from '@prisma/client' +import type { ScenarioData } from '@redwoodjs/testing/api' + +export const standard = defineScenario({ + user: { + one: { + data: { + userId: 'String1137501', + email: 'String', + address: { + create: { + Name: 'String', + Address1: 'String', + Address2: 'String', + Address3: 'String', + Town: 'String', + PostalCode: 'String', + }, + }, + }, + }, + two: { + data: { + userId: 'String7145010', + email: 'String', + address: { + create: { + Name: 'String', + Address1: 'String', + Address2: 'String', + Address3: 'String', + Town: 'String', + PostalCode: 'String', + }, + }, + }, + }, + }, +}) + +export type StandardScenario = ScenarioData diff --git a/api/src/services/users/users.test.ts b/api/src/services/users/users.test.ts new file mode 100644 index 0000000..cbaafae --- /dev/null +++ b/api/src/services/users/users.test.ts @@ -0,0 +1,55 @@ +import type { User } from '@prisma/client' + +import { users, user, createUser, updateUser, deleteUser } from './users' +import type { StandardScenario } from './users.scenarios' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-services +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('users', () => { + scenario('returns all users', async (scenario: StandardScenario) => { + const result = await users() + + expect(result.length).toEqual(Object.keys(scenario.user).length) + }) + + scenario('returns a single user', async (scenario: StandardScenario) => { + const result = await user({ id: scenario.user.one.id }) + + expect(result).toEqual(scenario.user.one) + }) + + scenario('creates a user', async (scenario: StandardScenario) => { + const result = await createUser({ + input: { + userId: 'String7795978', + email: 'String', + contactAddressId: scenario.user.two.contactAddressId, + }, + }) + + expect(result.userId).toEqual('String7795978') + expect(result.email).toEqual('String') + expect(result.contactAddressId).toEqual(scenario.user.two.contactAddressId) + }) + + scenario('updates a user', async (scenario: StandardScenario) => { + const original = (await user({ id: scenario.user.one.id })) as User + const result = await updateUser({ + id: original.id, + input: { userId: 'String60929082' }, + }) + + expect(result.userId).toEqual('String60929082') + }) + + scenario('deletes a user', async (scenario: StandardScenario) => { + const original = (await deleteUser({ id: scenario.user.one.id })) as User + const result = await user({ id: original.id }) + + expect(result).toEqual(null) + }) +}) diff --git a/api/src/services/users/users.ts b/api/src/services/users/users.ts new file mode 100644 index 0000000..66d463a --- /dev/null +++ b/api/src/services/users/users.ts @@ -0,0 +1,48 @@ +import type { + QueryResolvers, + MutationResolvers, + UserRelationResolvers, +} from 'types/graphql' + +import { db } from 'src/lib/db' + +export const users: QueryResolvers['users'] = () => { + return db.user.findMany() +} + +export const user: QueryResolvers['user'] = ({ id }) => { + return db.user.findUnique({ + where: { id }, + }) +} + +export const createUser: MutationResolvers['createUser'] = ({ input }) => { + return db.user.create({ + data: input, + }) +} + +export const updateUser: MutationResolvers['updateUser'] = ({ id, input }) => { + return db.user.update({ + data: input, + where: { id }, + }) +} + +export const deleteUser: MutationResolvers['deleteUser'] = ({ id }) => { + return db.user.delete({ + where: { id }, + }) +} + +export const User: UserRelationResolvers = { + address: (_obj, { root }) => { + return db.user.findUnique({ where: { id: root?.id } }).address() + }, + accounts: (_obj, { root }) => { + return db.user.findUnique({ where: { id: root?.id } }).accounts() + }, + roles: (_obj, { root }) => { + return db.user.findUnique({ where: { id: root?.id } }).roles() + }, +} diff --git a/web/package.json b/web/package.json index d8209fd..6cca1dd 100644 --- a/web/package.json +++ b/web/package.json @@ -14,6 +14,7 @@ "@redwoodjs/forms": "8.3.0", "@redwoodjs/router": "8.3.0", "@redwoodjs/web": "8.3.0", + "humanize-string": "2.1.0", "react": "18.3.1", "react-dom": "18.3.1" }, diff --git a/web/src/App.tsx b/web/src/App.tsx index 340c30b..6d20074 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -6,6 +6,8 @@ import { RedwoodApolloProvider } from '@redwoodjs/web/apollo' import FatalErrorPage from 'src/pages/FatalErrorPage' import './index.css' +import './scaffold.css' + interface AppProps { children?: ReactNode diff --git a/web/src/Routes.tsx b/web/src/Routes.tsx index 2c8f02a..75395eb 100644 --- a/web/src/Routes.tsx +++ b/web/src/Routes.tsx @@ -7,12 +7,43 @@ // 'src/pages/HomePage/HomePage.js' -> HomePage // 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage -import { Router, Route } from '@redwoodjs/router' +import { Router, Route, Set } from '@redwoodjs/router' + +import ScaffoldLayout from 'src/layouts/ScaffoldLayout' + +import ClientLayout from 'src/layouts/ClientLayout/ClientLayout' const Routes = () => { return ( - + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/web/src/components/Account/Account/Account.tsx b/web/src/components/Account/Account/Account.tsx new file mode 100644 index 0000000..71e47ec --- /dev/null +++ b/web/src/components/Account/Account/Account.tsx @@ -0,0 +1,98 @@ +import type { + DeleteAccountMutation, + DeleteAccountMutationVariables, + FindAccountById, +} from 'types/graphql' + +import { Link, routes, navigate } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import { checkboxInputTag } from 'src/lib/formatters' + +const DELETE_ACCOUNT_MUTATION: TypedDocumentNode< + DeleteAccountMutation, + DeleteAccountMutationVariables +> = gql` + mutation DeleteAccountMutation($id: String!) { + deleteAccount(id: $id) { + id + } + } +` + +interface Props { + account: NonNullable +} + +const Account = ({ account }: Props) => { + const [deleteAccount] = useMutation(DELETE_ACCOUNT_MUTATION, { + onCompleted: () => { + toast.success('Account deleted') + navigate(routes.accounts()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onDeleteClick = (id: DeleteAccountMutationVariables['id']) => { + if (confirm('Are you sure you want to delete account ' + id + '?')) { + deleteAccount({ variables: { id } }) + } + } + + return ( + <> +
+
+

+ Account {account.id} Detail +

+
+ + + + + + + + + + + + + + + + + + + + + + + +
Id{account.id}
Active{checkboxInputTag(account.active)}
Paid up{checkboxInputTag(account.PaidUp)}
Contact address id{account.contactAddressId}
User id{account.userId}
+
+ + + ) +} + +export default Account diff --git a/web/src/components/Account/AccountCell/AccountCell.tsx b/web/src/components/Account/AccountCell/AccountCell.tsx new file mode 100644 index 0000000..480d34a --- /dev/null +++ b/web/src/components/Account/AccountCell/AccountCell.tsx @@ -0,0 +1,40 @@ +import type { FindAccountById, FindAccountByIdVariables } from 'types/graphql' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import Account from 'src/components/Account/Account' + +export const QUERY: TypedDocumentNode< + FindAccountById, + FindAccountByIdVariables +> = gql` + query FindAccountById($id: String!) { + account: account(id: $id) { + id + active + PaidUp + contactAddressId + userId + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
Account not found
+ +export const Failure = ({ + error, +}: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + account, +}: CellSuccessProps) => { + return +} diff --git a/web/src/components/Account/AccountForm/AccountForm.tsx b/web/src/components/Account/AccountForm/AccountForm.tsx new file mode 100644 index 0000000..ff0869e --- /dev/null +++ b/web/src/components/Account/AccountForm/AccountForm.tsx @@ -0,0 +1,117 @@ +import type { EditAccountById, UpdateAccountInput } from 'types/graphql' + +import type { RWGqlError } from '@redwoodjs/forms' +import { + Form, + FormError, + FieldError, + Label, + CheckboxField, + TextField, + Submit, +} from '@redwoodjs/forms' + +type FormAccount = NonNullable + +interface AccountFormProps { + account?: EditAccountById['account'] + onSave: (data: UpdateAccountInput, id?: FormAccount['id']) => void + error: RWGqlError + loading: boolean +} + +const AccountForm = (props: AccountFormProps) => { + const onSubmit = (data: FormAccount) => { + props.onSave(data, props?.account?.id) + } + + return ( +
+ onSubmit={onSubmit} error={props.error}> + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Save + +
+ +
+ ) +} + +export default AccountForm diff --git a/web/src/components/Account/Accounts/Accounts.tsx b/web/src/components/Account/Accounts/Accounts.tsx new file mode 100644 index 0000000..947c901 --- /dev/null +++ b/web/src/components/Account/Accounts/Accounts.tsx @@ -0,0 +1,102 @@ +import type { + DeleteAccountMutation, + DeleteAccountMutationVariables, + FindAccounts, +} from 'types/graphql' + +import { Link, routes } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import { QUERY } from 'src/components/Account/AccountsCell' +import { checkboxInputTag, truncate } from 'src/lib/formatters' + +const DELETE_ACCOUNT_MUTATION: TypedDocumentNode< + DeleteAccountMutation, + DeleteAccountMutationVariables +> = gql` + mutation DeleteAccountMutation($id: String!) { + deleteAccount(id: $id) { + id + } + } +` + +const AccountsList = ({ accounts }: FindAccounts) => { + const [deleteAccount] = useMutation(DELETE_ACCOUNT_MUTATION, { + onCompleted: () => { + toast.success('Account deleted') + }, + onError: (error) => { + toast.error(error.message) + }, + // This refetches the query on the list page. Read more about other ways to + // update the cache over here: + // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates + refetchQueries: [{ query: QUERY }], + awaitRefetchQueries: true, + }) + + const onDeleteClick = (id: DeleteAccountMutationVariables['id']) => { + if (confirm('Are you sure you want to delete account ' + id + '?')) { + deleteAccount({ variables: { id } }) + } + } + + return ( +
+ + + + + + + + + + + + + {accounts.map((account) => ( + + + + + + + + + ))} + +
IdActivePaid upContact address idUser id 
{truncate(account.id)}{checkboxInputTag(account.active)}{checkboxInputTag(account.PaidUp)}{truncate(account.contactAddressId)}{truncate(account.userId)} + +
+
+ ) +} + +export default AccountsList diff --git a/web/src/components/Account/AccountsCell/AccountsCell.tsx b/web/src/components/Account/AccountsCell/AccountsCell.tsx new file mode 100644 index 0000000..d2c5de1 --- /dev/null +++ b/web/src/components/Account/AccountsCell/AccountsCell.tsx @@ -0,0 +1,46 @@ +import type { FindAccounts, FindAccountsVariables } from 'types/graphql' + +import { Link, routes } from '@redwoodjs/router' +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import Accounts from 'src/components/Account/Accounts' + +export const QUERY: TypedDocumentNode = + gql` + query FindAccounts { + accounts { + id + active + PaidUp + contactAddressId + userId + } + } + ` + +export const Loading = () =>
Loading...
+ +export const Empty = () => { + return ( +
+ No accounts yet.{' '} + + Create one? + +
+ ) +} + +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + accounts, +}: CellSuccessProps) => { + return +} diff --git a/web/src/components/Account/EditAccountCell/EditAccountCell.tsx b/web/src/components/Account/EditAccountCell/EditAccountCell.tsx new file mode 100644 index 0000000..b6d8dc4 --- /dev/null +++ b/web/src/components/Account/EditAccountCell/EditAccountCell.tsx @@ -0,0 +1,89 @@ +import type { + EditAccountById, + UpdateAccountInput, + UpdateAccountMutationVariables, +} from 'types/graphql' + +import { navigate, routes } from '@redwoodjs/router' +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' +import { useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import AccountForm from 'src/components/Account/AccountForm' + +export const QUERY: TypedDocumentNode = gql` + query EditAccountById($id: String!) { + account: account(id: $id) { + id + active + PaidUp + contactAddressId + userId + } + } +` + +const UPDATE_ACCOUNT_MUTATION: TypedDocumentNode< + EditAccountById, + UpdateAccountMutationVariables +> = gql` + mutation UpdateAccountMutation($id: String!, $input: UpdateAccountInput!) { + updateAccount(id: $id, input: $input) { + id + active + PaidUp + contactAddressId + userId + } + } +` + +export const Loading = () =>
Loading...
+ +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ account }: CellSuccessProps) => { + const [updateAccount, { loading, error }] = useMutation( + UPDATE_ACCOUNT_MUTATION, + { + onCompleted: () => { + toast.success('Account updated') + navigate(routes.accounts()) + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + + const onSave = ( + input: UpdateAccountInput, + id: EditAccountById['account']['id'] + ) => { + updateAccount({ variables: { id, input } }) + } + + return ( +
+
+

+ Edit Account {account?.id} +

+
+
+ +
+
+ ) +} diff --git a/web/src/components/Account/NewAccount/NewAccount.tsx b/web/src/components/Account/NewAccount/NewAccount.tsx new file mode 100644 index 0000000..4e1d2bb --- /dev/null +++ b/web/src/components/Account/NewAccount/NewAccount.tsx @@ -0,0 +1,55 @@ +import type { + CreateAccountMutation, + CreateAccountInput, + CreateAccountMutationVariables, +} from 'types/graphql' + +import { navigate, routes } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import AccountForm from 'src/components/Account/AccountForm' + +const CREATE_ACCOUNT_MUTATION: TypedDocumentNode< + CreateAccountMutation, + CreateAccountMutationVariables +> = gql` + mutation CreateAccountMutation($input: CreateAccountInput!) { + createAccount(input: $input) { + id + } + } +` + +const NewAccount = () => { + const [createAccount, { loading, error }] = useMutation( + CREATE_ACCOUNT_MUTATION, + { + onCompleted: () => { + toast.success('Account created') + navigate(routes.accounts()) + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + + const onSave = (input: CreateAccountInput) => { + createAccount({ variables: { input } }) + } + + return ( +
+
+

New Account

+
+
+ +
+
+ ) +} + +export default NewAccount diff --git a/web/src/components/ContactAddress/ContactAddress/ContactAddress.tsx b/web/src/components/ContactAddress/ContactAddress/ContactAddress.tsx new file mode 100644 index 0000000..2563182 --- /dev/null +++ b/web/src/components/ContactAddress/ContactAddress/ContactAddress.tsx @@ -0,0 +1,106 @@ +import type { + DeleteContactAddressMutation, + DeleteContactAddressMutationVariables, + FindContactAddressById, +} from 'types/graphql' + +import { Link, routes, navigate } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import {} from 'src/lib/formatters' + +const DELETE_CONTACT_ADDRESS_MUTATION: TypedDocumentNode< + DeleteContactAddressMutation, + DeleteContactAddressMutationVariables +> = gql` + mutation DeleteContactAddressMutation($id: String!) { + deleteContactAddress(id: $id) { + id + } + } +` + +interface Props { + contactAddress: NonNullable +} + +const ContactAddress = ({ contactAddress }: Props) => { + const [deleteContactAddress] = useMutation(DELETE_CONTACT_ADDRESS_MUTATION, { + onCompleted: () => { + toast.success('ContactAddress deleted') + navigate(routes.contactAddresses()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onDeleteClick = (id: DeleteContactAddressMutationVariables['id']) => { + if (confirm('Are you sure you want to delete contactAddress ' + id + '?')) { + deleteContactAddress({ variables: { id } }) + } + } + + return ( + <> +
+
+

+ ContactAddress {contactAddress.id} Detail +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Id{contactAddress.id}
Name{contactAddress.Name}
Address1{contactAddress.Address1}
Address2{contactAddress.Address2}
Address3{contactAddress.Address3}
Town{contactAddress.Town}
Postal code{contactAddress.PostalCode}
+
+ + + ) +} + +export default ContactAddress diff --git a/web/src/components/ContactAddress/ContactAddressCell/ContactAddressCell.tsx b/web/src/components/ContactAddress/ContactAddressCell/ContactAddressCell.tsx new file mode 100644 index 0000000..3cd1454 --- /dev/null +++ b/web/src/components/ContactAddress/ContactAddressCell/ContactAddressCell.tsx @@ -0,0 +1,48 @@ +import type { + FindContactAddressById, + FindContactAddressByIdVariables, +} from 'types/graphql' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import ContactAddress from 'src/components/ContactAddress/ContactAddress' + +export const QUERY: TypedDocumentNode< + FindContactAddressById, + FindContactAddressByIdVariables +> = gql` + query FindContactAddressById($id: String!) { + contactAddress: contactAddress(id: $id) { + id + Name + Address1 + Address2 + Address3 + Town + PostalCode + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
ContactAddress not found
+ +export const Failure = ({ + error, +}: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + contactAddress, +}: CellSuccessProps< + FindContactAddressById, + FindContactAddressByIdVariables +>) => { + return +} diff --git a/web/src/components/ContactAddress/ContactAddressForm/ContactAddressForm.tsx b/web/src/components/ContactAddress/ContactAddressForm/ContactAddressForm.tsx new file mode 100644 index 0000000..3e82b77 --- /dev/null +++ b/web/src/components/ContactAddress/ContactAddressForm/ContactAddressForm.tsx @@ -0,0 +1,161 @@ +import type { + EditContactAddressById, + UpdateContactAddressInput, +} from 'types/graphql' + +import type { RWGqlError } from '@redwoodjs/forms' +import { + Form, + FormError, + FieldError, + Label, + TextField, + Submit, +} from '@redwoodjs/forms' + +type FormContactAddress = NonNullable + +interface ContactAddressFormProps { + contactAddress?: EditContactAddressById['contactAddress'] + onSave: ( + data: UpdateContactAddressInput, + id?: FormContactAddress['id'] + ) => void + error: RWGqlError + loading: boolean +} + +const ContactAddressForm = (props: ContactAddressFormProps) => { + const onSubmit = (data: FormContactAddress) => { + props.onSave(data, props?.contactAddress?.id) + } + + return ( +
+ onSubmit={onSubmit} error={props.error}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Save + +
+ +
+ ) +} + +export default ContactAddressForm diff --git a/web/src/components/ContactAddress/ContactAddresses/ContactAddresses.tsx b/web/src/components/ContactAddress/ContactAddresses/ContactAddresses.tsx new file mode 100644 index 0000000..41995f2 --- /dev/null +++ b/web/src/components/ContactAddress/ContactAddresses/ContactAddresses.tsx @@ -0,0 +1,108 @@ +import type { + DeleteContactAddressMutation, + DeleteContactAddressMutationVariables, + FindContactAddresses, +} from 'types/graphql' + +import { Link, routes } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import { QUERY } from 'src/components/ContactAddress/ContactAddressesCell' +import { truncate } from 'src/lib/formatters' + +const DELETE_CONTACT_ADDRESS_MUTATION: TypedDocumentNode< + DeleteContactAddressMutation, + DeleteContactAddressMutationVariables +> = gql` + mutation DeleteContactAddressMutation($id: String!) { + deleteContactAddress(id: $id) { + id + } + } +` + +const ContactAddressesList = ({ contactAddresses }: FindContactAddresses) => { + const [deleteContactAddress] = useMutation(DELETE_CONTACT_ADDRESS_MUTATION, { + onCompleted: () => { + toast.success('ContactAddress deleted') + }, + onError: (error) => { + toast.error(error.message) + }, + // This refetches the query on the list page. Read more about other ways to + // update the cache over here: + // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates + refetchQueries: [{ query: QUERY }], + awaitRefetchQueries: true, + }) + + const onDeleteClick = (id: DeleteContactAddressMutationVariables['id']) => { + if (confirm('Are you sure you want to delete contactAddress ' + id + '?')) { + deleteContactAddress({ variables: { id } }) + } + } + + return ( +
+ + + + + + + + + + + + + + + {contactAddresses.map((contactAddress) => ( + + + + + + + + + + + ))} + +
IdNameAddress1Address2Address3TownPostal code 
{truncate(contactAddress.id)}{truncate(contactAddress.Name)}{truncate(contactAddress.Address1)}{truncate(contactAddress.Address2)}{truncate(contactAddress.Address3)}{truncate(contactAddress.Town)}{truncate(contactAddress.PostalCode)} + +
+
+ ) +} + +export default ContactAddressesList diff --git a/web/src/components/ContactAddress/ContactAddressesCell/ContactAddressesCell.tsx b/web/src/components/ContactAddress/ContactAddressesCell/ContactAddressesCell.tsx new file mode 100644 index 0000000..329081f --- /dev/null +++ b/web/src/components/ContactAddress/ContactAddressesCell/ContactAddressesCell.tsx @@ -0,0 +1,53 @@ +import type { + FindContactAddresses, + FindContactAddressesVariables, +} from 'types/graphql' + +import { Link, routes } from '@redwoodjs/router' +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import ContactAddresses from 'src/components/ContactAddress/ContactAddresses' + +export const QUERY: TypedDocumentNode< + FindContactAddresses, + FindContactAddressesVariables +> = gql` + query FindContactAddresses { + contactAddresses { + id + Name + Address1 + Address2 + Address3 + Town + PostalCode + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () => { + return ( +
+ No contactAddresses yet.{' '} + + Create one? + +
+ ) +} + +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + contactAddresses, +}: CellSuccessProps) => { + return +} diff --git a/web/src/components/ContactAddress/EditContactAddressCell/EditContactAddressCell.tsx b/web/src/components/ContactAddress/EditContactAddressCell/EditContactAddressCell.tsx new file mode 100644 index 0000000..e1d6cec --- /dev/null +++ b/web/src/components/ContactAddress/EditContactAddressCell/EditContactAddressCell.tsx @@ -0,0 +1,98 @@ +import type { + EditContactAddressById, + UpdateContactAddressInput, + UpdateContactAddressMutationVariables, +} from 'types/graphql' + +import { navigate, routes } from '@redwoodjs/router' +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' +import { useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import ContactAddressForm from 'src/components/ContactAddress/ContactAddressForm' + +export const QUERY: TypedDocumentNode = gql` + query EditContactAddressById($id: String!) { + contactAddress: contactAddress(id: $id) { + id + Name + Address1 + Address2 + Address3 + Town + PostalCode + } + } +` + +const UPDATE_CONTACT_ADDRESS_MUTATION: TypedDocumentNode< + EditContactAddressById, + UpdateContactAddressMutationVariables +> = gql` + mutation UpdateContactAddressMutation( + $id: String! + $input: UpdateContactAddressInput! + ) { + updateContactAddress(id: $id, input: $input) { + id + Name + Address1 + Address2 + Address3 + Town + PostalCode + } + } +` + +export const Loading = () =>
Loading...
+ +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + contactAddress, +}: CellSuccessProps) => { + const [updateContactAddress, { loading, error }] = useMutation( + UPDATE_CONTACT_ADDRESS_MUTATION, + { + onCompleted: () => { + toast.success('ContactAddress updated') + navigate(routes.contactAddresses()) + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + + const onSave = ( + input: UpdateContactAddressInput, + id: EditContactAddressById['contactAddress']['id'] + ) => { + updateContactAddress({ variables: { id, input } }) + } + + return ( +
+
+

+ Edit ContactAddress {contactAddress?.id} +

+
+
+ +
+
+ ) +} diff --git a/web/src/components/ContactAddress/NewContactAddress/NewContactAddress.tsx b/web/src/components/ContactAddress/NewContactAddress/NewContactAddress.tsx new file mode 100644 index 0000000..1368a70 --- /dev/null +++ b/web/src/components/ContactAddress/NewContactAddress/NewContactAddress.tsx @@ -0,0 +1,55 @@ +import type { + CreateContactAddressMutation, + CreateContactAddressInput, + CreateContactAddressMutationVariables, +} from 'types/graphql' + +import { navigate, routes } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import ContactAddressForm from 'src/components/ContactAddress/ContactAddressForm' + +const CREATE_CONTACT_ADDRESS_MUTATION: TypedDocumentNode< + CreateContactAddressMutation, + CreateContactAddressMutationVariables +> = gql` + mutation CreateContactAddressMutation($input: CreateContactAddressInput!) { + createContactAddress(input: $input) { + id + } + } +` + +const NewContactAddress = () => { + const [createContactAddress, { loading, error }] = useMutation( + CREATE_CONTACT_ADDRESS_MUTATION, + { + onCompleted: () => { + toast.success('ContactAddress created') + navigate(routes.contactAddresses()) + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + + const onSave = (input: CreateContactAddressInput) => { + createContactAddress({ variables: { input } }) + } + + return ( +
+
+

New ContactAddress

+
+
+ +
+
+ ) +} + +export default NewContactAddress diff --git a/web/src/components/Role/EditRoleCell/EditRoleCell.tsx b/web/src/components/Role/EditRoleCell/EditRoleCell.tsx new file mode 100644 index 0000000..0b4eb02 --- /dev/null +++ b/web/src/components/Role/EditRoleCell/EditRoleCell.tsx @@ -0,0 +1,74 @@ +import type { + EditRoleById, + UpdateRoleInput, + UpdateRoleMutationVariables, +} from 'types/graphql' + +import { navigate, routes } from '@redwoodjs/router' +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' +import { useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import RoleForm from 'src/components/Role/RoleForm' + +export const QUERY: TypedDocumentNode = gql` + query EditRoleById($id: String!) { + role: role(id: $id) { + id + name + userId + } + } +` + +const UPDATE_ROLE_MUTATION: TypedDocumentNode< + EditRoleById, + UpdateRoleMutationVariables +> = gql` + mutation UpdateRoleMutation($id: String!, $input: UpdateRoleInput!) { + updateRole(id: $id, input: $input) { + id + name + userId + } + } +` + +export const Loading = () =>
Loading...
+ +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ role }: CellSuccessProps) => { + const [updateRole, { loading, error }] = useMutation(UPDATE_ROLE_MUTATION, { + onCompleted: () => { + toast.success('Role updated') + navigate(routes.roles()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onSave = (input: UpdateRoleInput, id: EditRoleById['role']['id']) => { + updateRole({ variables: { id, input } }) + } + + return ( +
+
+

+ Edit Role {role?.id} +

+
+
+ +
+
+ ) +} diff --git a/web/src/components/Role/NewRole/NewRole.tsx b/web/src/components/Role/NewRole/NewRole.tsx new file mode 100644 index 0000000..10d3152 --- /dev/null +++ b/web/src/components/Role/NewRole/NewRole.tsx @@ -0,0 +1,52 @@ +import type { + CreateRoleMutation, + CreateRoleInput, + CreateRoleMutationVariables, +} from 'types/graphql' + +import { navigate, routes } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import RoleForm from 'src/components/Role/RoleForm' + +const CREATE_ROLE_MUTATION: TypedDocumentNode< + CreateRoleMutation, + CreateRoleMutationVariables +> = gql` + mutation CreateRoleMutation($input: CreateRoleInput!) { + createRole(input: $input) { + id + } + } +` + +const NewRole = () => { + const [createRole, { loading, error }] = useMutation(CREATE_ROLE_MUTATION, { + onCompleted: () => { + toast.success('Role created') + navigate(routes.roles()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onSave = (input: CreateRoleInput) => { + createRole({ variables: { input } }) + } + + return ( +
+
+

New Role

+
+
+ +
+
+ ) +} + +export default NewRole diff --git a/web/src/components/Role/Role/Role.tsx b/web/src/components/Role/Role/Role.tsx new file mode 100644 index 0000000..0c23b42 --- /dev/null +++ b/web/src/components/Role/Role/Role.tsx @@ -0,0 +1,90 @@ +import type { + DeleteRoleMutation, + DeleteRoleMutationVariables, + FindRoleById, +} from 'types/graphql' + +import { Link, routes, navigate } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import {} from 'src/lib/formatters' + +const DELETE_ROLE_MUTATION: TypedDocumentNode< + DeleteRoleMutation, + DeleteRoleMutationVariables +> = gql` + mutation DeleteRoleMutation($id: String!) { + deleteRole(id: $id) { + id + } + } +` + +interface Props { + role: NonNullable +} + +const Role = ({ role }: Props) => { + const [deleteRole] = useMutation(DELETE_ROLE_MUTATION, { + onCompleted: () => { + toast.success('Role deleted') + navigate(routes.roles()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onDeleteClick = (id: DeleteRoleMutationVariables['id']) => { + if (confirm('Are you sure you want to delete role ' + id + '?')) { + deleteRole({ variables: { id } }) + } + } + + return ( + <> +
+
+

+ Role {role.id} Detail +

+
+ + + + + + + + + + + + + + + +
Id{role.id}
Name{role.name}
User id{role.userId}
+
+ + + ) +} + +export default Role diff --git a/web/src/components/Role/RoleCell/RoleCell.tsx b/web/src/components/Role/RoleCell/RoleCell.tsx new file mode 100644 index 0000000..15d1243 --- /dev/null +++ b/web/src/components/Role/RoleCell/RoleCell.tsx @@ -0,0 +1,34 @@ +import type { FindRoleById, FindRoleByIdVariables } from 'types/graphql' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import Role from 'src/components/Role/Role' + +export const QUERY: TypedDocumentNode = + gql` + query FindRoleById($id: String!) { + role: role(id: $id) { + id + name + userId + } + } + ` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
Role not found
+ +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + role, +}: CellSuccessProps) => { + return +} diff --git a/web/src/components/Role/RoleForm/RoleForm.tsx b/web/src/components/Role/RoleForm/RoleForm.tsx new file mode 100644 index 0000000..d6bdd43 --- /dev/null +++ b/web/src/components/Role/RoleForm/RoleForm.tsx @@ -0,0 +1,82 @@ +import type { EditRoleById, UpdateRoleInput } from 'types/graphql' + +import type { RWGqlError } from '@redwoodjs/forms' +import { + Form, + FormError, + FieldError, + Label, + TextField, + Submit, +} from '@redwoodjs/forms' + +type FormRole = NonNullable + +interface RoleFormProps { + role?: EditRoleById['role'] + onSave: (data: UpdateRoleInput, id?: FormRole['id']) => void + error: RWGqlError + loading: boolean +} + +const RoleForm = (props: RoleFormProps) => { + const onSubmit = (data: FormRole) => { + props.onSave(data, props?.role?.id) + } + + return ( +
+ onSubmit={onSubmit} error={props.error}> + + + + + + + + + + + + + + +
+ + Save + +
+ +
+ ) +} + +export default RoleForm diff --git a/web/src/components/Role/Roles/Roles.tsx b/web/src/components/Role/Roles/Roles.tsx new file mode 100644 index 0000000..5f42c36 --- /dev/null +++ b/web/src/components/Role/Roles/Roles.tsx @@ -0,0 +1,98 @@ +import type { + DeleteRoleMutation, + DeleteRoleMutationVariables, + FindRoles, +} from 'types/graphql' + +import { Link, routes } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import { QUERY } from 'src/components/Role/RolesCell' +import { truncate } from 'src/lib/formatters' + +const DELETE_ROLE_MUTATION: TypedDocumentNode< + DeleteRoleMutation, + DeleteRoleMutationVariables +> = gql` + mutation DeleteRoleMutation($id: String!) { + deleteRole(id: $id) { + id + } + } +` + +const RolesList = ({ roles }: FindRoles) => { + const [deleteRole] = useMutation(DELETE_ROLE_MUTATION, { + onCompleted: () => { + toast.success('Role deleted') + }, + onError: (error) => { + toast.error(error.message) + }, + // This refetches the query on the list page. Read more about other ways to + // update the cache over here: + // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates + refetchQueries: [{ query: QUERY }], + awaitRefetchQueries: true, + }) + + const onDeleteClick = (id: DeleteRoleMutationVariables['id']) => { + if (confirm('Are you sure you want to delete role ' + id + '?')) { + deleteRole({ variables: { id } }) + } + } + + return ( +
+ + + + + + + + + + + {roles.map((role) => ( + + + + + + + ))} + +
IdNameUser id 
{truncate(role.id)}{truncate(role.name)}{truncate(role.userId)} + +
+
+ ) +} + +export default RolesList diff --git a/web/src/components/Role/RolesCell/RolesCell.tsx b/web/src/components/Role/RolesCell/RolesCell.tsx new file mode 100644 index 0000000..d1284ed --- /dev/null +++ b/web/src/components/Role/RolesCell/RolesCell.tsx @@ -0,0 +1,43 @@ +import type { FindRoles, FindRolesVariables } from 'types/graphql' + +import { Link, routes } from '@redwoodjs/router' +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import Roles from 'src/components/Role/Roles' + +export const QUERY: TypedDocumentNode = gql` + query FindRoles { + roles { + id + name + userId + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () => { + return ( +
+ No roles yet.{' '} + + Create one? + +
+ ) +} + +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + roles, +}: CellSuccessProps) => { + return +} diff --git a/web/src/components/User/EditUserCell/EditUserCell.tsx b/web/src/components/User/EditUserCell/EditUserCell.tsx new file mode 100644 index 0000000..ab61a00 --- /dev/null +++ b/web/src/components/User/EditUserCell/EditUserCell.tsx @@ -0,0 +1,76 @@ +import type { + EditUserById, + UpdateUserInput, + UpdateUserMutationVariables, +} from 'types/graphql' + +import { navigate, routes } from '@redwoodjs/router' +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' +import { useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import UserForm from 'src/components/User/UserForm' + +export const QUERY: TypedDocumentNode = gql` + query EditUserById($id: String!) { + user: user(id: $id) { + id + userId + email + contactAddressId + } + } +` + +const UPDATE_USER_MUTATION: TypedDocumentNode< + EditUserById, + UpdateUserMutationVariables +> = gql` + mutation UpdateUserMutation($id: String!, $input: UpdateUserInput!) { + updateUser(id: $id, input: $input) { + id + userId + email + contactAddressId + } + } +` + +export const Loading = () =>
Loading...
+ +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ user }: CellSuccessProps) => { + const [updateUser, { loading, error }] = useMutation(UPDATE_USER_MUTATION, { + onCompleted: () => { + toast.success('User updated') + navigate(routes.users()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onSave = (input: UpdateUserInput, id: EditUserById['user']['id']) => { + updateUser({ variables: { id, input } }) + } + + return ( +
+
+

+ Edit User {user?.id} +

+
+
+ +
+
+ ) +} diff --git a/web/src/components/User/NewUser/NewUser.tsx b/web/src/components/User/NewUser/NewUser.tsx new file mode 100644 index 0000000..9e36c1b --- /dev/null +++ b/web/src/components/User/NewUser/NewUser.tsx @@ -0,0 +1,52 @@ +import type { + CreateUserMutation, + CreateUserInput, + CreateUserMutationVariables, +} from 'types/graphql' + +import { navigate, routes } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import UserForm from 'src/components/User/UserForm' + +const CREATE_USER_MUTATION: TypedDocumentNode< + CreateUserMutation, + CreateUserMutationVariables +> = gql` + mutation CreateUserMutation($input: CreateUserInput!) { + createUser(input: $input) { + id + } + } +` + +const NewUser = () => { + const [createUser, { loading, error }] = useMutation(CREATE_USER_MUTATION, { + onCompleted: () => { + toast.success('User created') + navigate(routes.users()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onSave = (input: CreateUserInput) => { + createUser({ variables: { input } }) + } + + return ( +
+
+

New User

+
+
+ +
+
+ ) +} + +export default NewUser diff --git a/web/src/components/User/User/User.tsx b/web/src/components/User/User/User.tsx new file mode 100644 index 0000000..e9de130 --- /dev/null +++ b/web/src/components/User/User/User.tsx @@ -0,0 +1,94 @@ +import type { + DeleteUserMutation, + DeleteUserMutationVariables, + FindUserById, +} from 'types/graphql' + +import { Link, routes, navigate } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import {} from 'src/lib/formatters' + +const DELETE_USER_MUTATION: TypedDocumentNode< + DeleteUserMutation, + DeleteUserMutationVariables +> = gql` + mutation DeleteUserMutation($id: String!) { + deleteUser(id: $id) { + id + } + } +` + +interface Props { + user: NonNullable +} + +const User = ({ user }: Props) => { + const [deleteUser] = useMutation(DELETE_USER_MUTATION, { + onCompleted: () => { + toast.success('User deleted') + navigate(routes.users()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onDeleteClick = (id: DeleteUserMutationVariables['id']) => { + if (confirm('Are you sure you want to delete user ' + id + '?')) { + deleteUser({ variables: { id } }) + } + } + + return ( + <> +
+
+

+ User {user.id} Detail +

+
+ + + + + + + + + + + + + + + + + + + +
Id{user.id}
User id{user.userId}
Email{user.email}
Contact address id{user.contactAddressId}
+
+ + + ) +} + +export default User diff --git a/web/src/components/User/UserCell/UserCell.tsx b/web/src/components/User/UserCell/UserCell.tsx new file mode 100644 index 0000000..8c236db --- /dev/null +++ b/web/src/components/User/UserCell/UserCell.tsx @@ -0,0 +1,35 @@ +import type { FindUserById, FindUserByIdVariables } from 'types/graphql' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import User from 'src/components/User/User' + +export const QUERY: TypedDocumentNode = + gql` + query FindUserById($id: String!) { + user: user(id: $id) { + id + userId + email + contactAddressId + } + } + ` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
User not found
+ +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + user, +}: CellSuccessProps) => { + return +} diff --git a/web/src/components/User/UserForm/UserForm.tsx b/web/src/components/User/UserForm/UserForm.tsx new file mode 100644 index 0000000..a0cea48 --- /dev/null +++ b/web/src/components/User/UserForm/UserForm.tsx @@ -0,0 +1,101 @@ +import type { EditUserById, UpdateUserInput } from 'types/graphql' + +import type { RWGqlError } from '@redwoodjs/forms' +import { + Form, + FormError, + FieldError, + Label, + TextField, + Submit, +} from '@redwoodjs/forms' + +type FormUser = NonNullable + +interface UserFormProps { + user?: EditUserById['user'] + onSave: (data: UpdateUserInput, id?: FormUser['id']) => void + error: RWGqlError + loading: boolean +} + +const UserForm = (props: UserFormProps) => { + const onSubmit = (data: FormUser) => { + props.onSave(data, props?.user?.id) + } + + return ( +
+ onSubmit={onSubmit} error={props.error}> + + + + + + + + + + + + + + + + + + + + +
+ + Save + +
+ +
+ ) +} + +export default UserForm diff --git a/web/src/components/User/Users/Users.tsx b/web/src/components/User/Users/Users.tsx new file mode 100644 index 0000000..0918bdd --- /dev/null +++ b/web/src/components/User/Users/Users.tsx @@ -0,0 +1,100 @@ +import type { + DeleteUserMutation, + DeleteUserMutationVariables, + FindUsers, +} from 'types/graphql' + +import { Link, routes } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import { QUERY } from 'src/components/User/UsersCell' +import { truncate } from 'src/lib/formatters' + +const DELETE_USER_MUTATION: TypedDocumentNode< + DeleteUserMutation, + DeleteUserMutationVariables +> = gql` + mutation DeleteUserMutation($id: String!) { + deleteUser(id: $id) { + id + } + } +` + +const UsersList = ({ users }: FindUsers) => { + const [deleteUser] = useMutation(DELETE_USER_MUTATION, { + onCompleted: () => { + toast.success('User deleted') + }, + onError: (error) => { + toast.error(error.message) + }, + // This refetches the query on the list page. Read more about other ways to + // update the cache over here: + // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates + refetchQueries: [{ query: QUERY }], + awaitRefetchQueries: true, + }) + + const onDeleteClick = (id: DeleteUserMutationVariables['id']) => { + if (confirm('Are you sure you want to delete user ' + id + '?')) { + deleteUser({ variables: { id } }) + } + } + + return ( +
+ + + + + + + + + + + + {users.map((user) => ( + + + + + + + + ))} + +
IdUser idEmailContact address id 
{truncate(user.id)}{truncate(user.userId)}{truncate(user.email)}{truncate(user.contactAddressId)} + +
+
+ ) +} + +export default UsersList diff --git a/web/src/components/User/UsersCell/UsersCell.tsx b/web/src/components/User/UsersCell/UsersCell.tsx new file mode 100644 index 0000000..1c4abad --- /dev/null +++ b/web/src/components/User/UsersCell/UsersCell.tsx @@ -0,0 +1,44 @@ +import type { FindUsers, FindUsersVariables } from 'types/graphql' + +import { Link, routes } from '@redwoodjs/router' +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import Users from 'src/components/User/Users' + +export const QUERY: TypedDocumentNode = gql` + query FindUsers { + users { + id + userId + email + contactAddressId + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () => { + return ( +
+ No users yet.{' '} + + Create one? + +
+ ) +} + +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + users, +}: CellSuccessProps) => { + return +} diff --git a/web/src/layouts/ClientLayout/ClientLayout.stories.tsx b/web/src/layouts/ClientLayout/ClientLayout.stories.tsx new file mode 100644 index 0000000..dec41f0 --- /dev/null +++ b/web/src/layouts/ClientLayout/ClientLayout.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import ClientLayout from './ClientLayout' + +const meta: Meta = { + component: ClientLayout, +} + +export default meta + +type Story = StoryObj + +export const Primary: Story = {} diff --git a/web/src/layouts/ClientLayout/ClientLayout.test.tsx b/web/src/layouts/ClientLayout/ClientLayout.test.tsx new file mode 100644 index 0000000..2215c55 --- /dev/null +++ b/web/src/layouts/ClientLayout/ClientLayout.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@redwoodjs/testing/web' + +import ClientLayout from './ClientLayout' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-pages-layouts + +describe('ClientLayout', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/web/src/layouts/ClientLayout/ClientLayout.tsx b/web/src/layouts/ClientLayout/ClientLayout.tsx new file mode 100644 index 0000000..07fab13 --- /dev/null +++ b/web/src/layouts/ClientLayout/ClientLayout.tsx @@ -0,0 +1,25 @@ +import { Link, routes } from '@redwoodjs/router' + +type ClientLayoutProps = { + children?: React.ReactNode +} + +const ClientLayout = ({ children }: ClientLayoutProps) => { + return ( + <> +
+

Redwood Blog

+ +
+
{children}
+ + ) +} + +export default ClientLayout diff --git a/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx b/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx new file mode 100644 index 0000000..f4daba6 --- /dev/null +++ b/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx @@ -0,0 +1,37 @@ +import { Link, routes } from '@redwoodjs/router' +import { Toaster } from '@redwoodjs/web/toast' + +type LayoutProps = { + title: string + titleTo: keyof typeof routes + buttonLabel: string + buttonTo: keyof typeof routes + children: React.ReactNode +} + +const ScaffoldLayout = ({ + title, + titleTo, + buttonLabel, + buttonTo, + children, +}: LayoutProps) => { + return ( +
+ +
+

+ + {title} + +

+ +
+
{buttonLabel} + +
+
{children}
+
+ ) +} + +export default ScaffoldLayout diff --git a/web/src/lib/formatters.test.tsx b/web/src/lib/formatters.test.tsx new file mode 100644 index 0000000..5659338 --- /dev/null +++ b/web/src/lib/formatters.test.tsx @@ -0,0 +1,192 @@ +import { render, waitFor, screen } from '@redwoodjs/testing/web' + +import { + formatEnum, + jsonTruncate, + truncate, + timeTag, + jsonDisplay, + checkboxInputTag, +} from './formatters' + +describe('formatEnum', () => { + it('handles nullish values', () => { + expect(formatEnum(null)).toEqual('') + expect(formatEnum('')).toEqual('') + expect(formatEnum(undefined)).toEqual('') + }) + + it('formats a list of values', () => { + expect( + formatEnum(['RED', 'ORANGE', 'YELLOW', 'GREEN', 'BLUE', 'VIOLET']) + ).toEqual('Red, Orange, Yellow, Green, Blue, Violet') + }) + + it('formats a single value', () => { + expect(formatEnum('DARK_BLUE')).toEqual('Dark blue') + }) + + it('returns an empty string for values of the wrong type (for JS projects)', () => { + // @ts-expect-error - Testing JS scenario + expect(formatEnum(5)).toEqual('') + }) +}) + +describe('truncate', () => { + it('truncates really long strings', () => { + expect(truncate('na '.repeat(1000) + 'batman').length).toBeLessThan(1000) + expect(truncate('na '.repeat(1000) + 'batman')).not.toMatch(/batman/) + }) + + it('does not modify short strings', () => { + expect(truncate('Short strinG')).toEqual('Short strinG') + }) + + it('adds ... to the end of truncated strings', () => { + expect(truncate('repeat'.repeat(1000))).toMatch(/\w\.\.\.$/) + }) + + it('accepts numbers', () => { + expect(truncate(123)).toEqual('123') + expect(truncate(0)).toEqual('0') + expect(truncate(0o000)).toEqual('0') + }) + + it('handles arguments of invalid type', () => { + // @ts-expect-error - Testing JS scenario + expect(truncate(false)).toEqual('false') + + expect(truncate(undefined)).toEqual('') + expect(truncate(null)).toEqual('') + }) +}) + +describe('jsonTruncate', () => { + it('truncates large json structures', () => { + expect( + jsonTruncate({ + foo: 'foo', + bar: 'bar', + baz: 'baz', + kittens: 'kittens meow', + bazinga: 'Sheldon', + nested: { + foobar: 'I have no imagination', + two: 'Second nested item', + }, + five: 5, + bool: false, + }) + ).toMatch(/.+\n.+\w\.\.\.$/s) + }) +}) + +describe('timeTag', () => { + it('renders a date', async () => { + render(
{timeTag(new Date('1970-08-20').toUTCString())}
) + + await waitFor(() => screen.getByText(/1970.*00:00:00/)) + }) + + it('can take an empty input string', async () => { + expect(timeTag('')).toEqual('') + }) +}) + +describe('jsonDisplay', () => { + it('produces the correct output', () => { + expect( + jsonDisplay({ + title: 'TOML Example (but in JSON)', + database: { + data: [['delta', 'phi'], [3.14]], + enabled: true, + ports: [8000, 8001, 8002], + temp_targets: { + case: 72.0, + cpu: 79.5, + }, + }, + owner: { + dob: '1979-05-27T07:32:00-08:00', + name: 'Tom Preston-Werner', + }, + servers: { + alpha: { + ip: '10.0.0.1', + role: 'frontend', + }, + beta: { + ip: '10.0.0.2', + role: 'backend', + }, + }, + }) + ).toMatchInlineSnapshot(` +
+        
+          {
+        "title": "TOML Example (but in JSON)",
+        "database": {
+          "data": [
+            [
+              "delta",
+              "phi"
+            ],
+            [
+              3.14
+            ]
+          ],
+          "enabled": true,
+          "ports": [
+            8000,
+            8001,
+            8002
+          ],
+          "temp_targets": {
+            "case": 72,
+            "cpu": 79.5
+          }
+        },
+        "owner": {
+          "dob": "1979-05-27T07:32:00-08:00",
+          "name": "Tom Preston-Werner"
+        },
+        "servers": {
+          "alpha": {
+            "ip": "10.0.0.1",
+            "role": "frontend"
+          },
+          "beta": {
+            "ip": "10.0.0.2",
+            "role": "backend"
+          }
+        }
+      }
+        
+      
+ `) + }) +}) + +describe('checkboxInputTag', () => { + it('can be checked', () => { + render(checkboxInputTag(true)) + expect(screen.getByRole('checkbox')).toBeChecked() + }) + + it('can be unchecked', () => { + render(checkboxInputTag(false)) + expect(screen.getByRole('checkbox')).not.toBeChecked() + }) + + it('is disabled when checked', () => { + render(checkboxInputTag(true)) + expect(screen.getByRole('checkbox')).toBeDisabled() + }) + + it('is disabled when unchecked', () => { + render(checkboxInputTag(false)) + expect(screen.getByRole('checkbox')).toBeDisabled() + }) +}) diff --git a/web/src/lib/formatters.tsx b/web/src/lib/formatters.tsx new file mode 100644 index 0000000..8ab9e80 --- /dev/null +++ b/web/src/lib/formatters.tsx @@ -0,0 +1,58 @@ +import React from 'react' + +import humanize from 'humanize-string' + +const MAX_STRING_LENGTH = 150 + +export const formatEnum = (values: string | string[] | null | undefined) => { + let output = '' + + if (Array.isArray(values)) { + const humanizedValues = values.map((value) => humanize(value)) + output = humanizedValues.join(', ') + } else if (typeof values === 'string') { + output = humanize(values) + } + + return output +} + +export const jsonDisplay = (obj: unknown) => { + return ( +
+      {JSON.stringify(obj, null, 2)}
+    
+ ) +} + +export const truncate = (value: string | number) => { + let output = value?.toString() ?? '' + + if (output.length > MAX_STRING_LENGTH) { + output = output.substring(0, MAX_STRING_LENGTH) + '...' + } + + return output +} + +export const jsonTruncate = (obj: unknown) => { + return truncate(JSON.stringify(obj, null, 2)) +} + +export const timeTag = (dateTime?: string) => { + let output: string | JSX.Element = '' + + if (dateTime) { + output = ( + + ) + } + + return output +} + +export const checkboxInputTag = (checked: boolean) => { + return +} diff --git a/web/src/pages/Account/AccountPage/AccountPage.tsx b/web/src/pages/Account/AccountPage/AccountPage.tsx new file mode 100644 index 0000000..c51ae23 --- /dev/null +++ b/web/src/pages/Account/AccountPage/AccountPage.tsx @@ -0,0 +1,11 @@ +import AccountCell from 'src/components/Account/AccountCell' + +type AccountPageProps = { + id: string +} + +const AccountPage = ({ id }: AccountPageProps) => { + return +} + +export default AccountPage diff --git a/web/src/pages/Account/AccountsPage/AccountsPage.tsx b/web/src/pages/Account/AccountsPage/AccountsPage.tsx new file mode 100644 index 0000000..caea9d7 --- /dev/null +++ b/web/src/pages/Account/AccountsPage/AccountsPage.tsx @@ -0,0 +1,7 @@ +import AccountsCell from 'src/components/Account/AccountsCell' + +const AccountsPage = () => { + return +} + +export default AccountsPage diff --git a/web/src/pages/Account/EditAccountPage/EditAccountPage.tsx b/web/src/pages/Account/EditAccountPage/EditAccountPage.tsx new file mode 100644 index 0000000..59d3200 --- /dev/null +++ b/web/src/pages/Account/EditAccountPage/EditAccountPage.tsx @@ -0,0 +1,11 @@ +import EditAccountCell from 'src/components/Account/EditAccountCell' + +type AccountPageProps = { + id: string +} + +const EditAccountPage = ({ id }: AccountPageProps) => { + return +} + +export default EditAccountPage diff --git a/web/src/pages/Account/NewAccountPage/NewAccountPage.tsx b/web/src/pages/Account/NewAccountPage/NewAccountPage.tsx new file mode 100644 index 0000000..d11803e --- /dev/null +++ b/web/src/pages/Account/NewAccountPage/NewAccountPage.tsx @@ -0,0 +1,7 @@ +import NewAccount from 'src/components/Account/NewAccount' + +const NewAccountPage = () => { + return +} + +export default NewAccountPage diff --git a/web/src/pages/ContactAddress/ContactAddressPage/ContactAddressPage.tsx b/web/src/pages/ContactAddress/ContactAddressPage/ContactAddressPage.tsx new file mode 100644 index 0000000..287eda6 --- /dev/null +++ b/web/src/pages/ContactAddress/ContactAddressPage/ContactAddressPage.tsx @@ -0,0 +1,11 @@ +import ContactAddressCell from 'src/components/ContactAddress/ContactAddressCell' + +type ContactAddressPageProps = { + id: string +} + +const ContactAddressPage = ({ id }: ContactAddressPageProps) => { + return +} + +export default ContactAddressPage diff --git a/web/src/pages/ContactAddress/ContactAddressesPage/ContactAddressesPage.tsx b/web/src/pages/ContactAddress/ContactAddressesPage/ContactAddressesPage.tsx new file mode 100644 index 0000000..097ca68 --- /dev/null +++ b/web/src/pages/ContactAddress/ContactAddressesPage/ContactAddressesPage.tsx @@ -0,0 +1,7 @@ +import ContactAddressesCell from 'src/components/ContactAddress/ContactAddressesCell' + +const ContactAddressesPage = () => { + return +} + +export default ContactAddressesPage diff --git a/web/src/pages/ContactAddress/EditContactAddressPage/EditContactAddressPage.tsx b/web/src/pages/ContactAddress/EditContactAddressPage/EditContactAddressPage.tsx new file mode 100644 index 0000000..f3db08f --- /dev/null +++ b/web/src/pages/ContactAddress/EditContactAddressPage/EditContactAddressPage.tsx @@ -0,0 +1,11 @@ +import EditContactAddressCell from 'src/components/ContactAddress/EditContactAddressCell' + +type ContactAddressPageProps = { + id: string +} + +const EditContactAddressPage = ({ id }: ContactAddressPageProps) => { + return +} + +export default EditContactAddressPage diff --git a/web/src/pages/ContactAddress/NewContactAddressPage/NewContactAddressPage.tsx b/web/src/pages/ContactAddress/NewContactAddressPage/NewContactAddressPage.tsx new file mode 100644 index 0000000..ea5ff8e --- /dev/null +++ b/web/src/pages/ContactAddress/NewContactAddressPage/NewContactAddressPage.tsx @@ -0,0 +1,7 @@ +import NewContactAddress from 'src/components/ContactAddress/NewContactAddress' + +const NewContactAddressPage = () => { + return +} + +export default NewContactAddressPage diff --git a/web/src/pages/HomePage/HomePage.stories.tsx b/web/src/pages/HomePage/HomePage.stories.tsx new file mode 100644 index 0000000..d9631ae --- /dev/null +++ b/web/src/pages/HomePage/HomePage.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import HomePage from './HomePage' + +const meta: Meta = { + component: HomePage, +} + +export default meta + +type Story = StoryObj + +export const Primary: Story = {} diff --git a/web/src/pages/HomePage/HomePage.test.tsx b/web/src/pages/HomePage/HomePage.test.tsx new file mode 100644 index 0000000..c684c7a --- /dev/null +++ b/web/src/pages/HomePage/HomePage.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@redwoodjs/testing/web' + +import HomePage from './HomePage' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-pages-layouts + +describe('HomePage', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/web/src/pages/HomePage/HomePage.tsx b/web/src/pages/HomePage/HomePage.tsx new file mode 100644 index 0000000..48e8208 --- /dev/null +++ b/web/src/pages/HomePage/HomePage.tsx @@ -0,0 +1,21 @@ +// import { Link, routes } from '@redwoodjs/router' +import { Metadata } from '@redwoodjs/web' + +const HomePage = () => { + return ( + <> + + +

HomePage

+

+ Find me in ./web/src/pages/HomePage/HomePage.tsx +

+ {/* + My default route is named `home`, link to me with: + `Home` + */} + + ) +} + +export default HomePage diff --git a/web/src/pages/Role/EditRolePage/EditRolePage.tsx b/web/src/pages/Role/EditRolePage/EditRolePage.tsx new file mode 100644 index 0000000..122576d --- /dev/null +++ b/web/src/pages/Role/EditRolePage/EditRolePage.tsx @@ -0,0 +1,11 @@ +import EditRoleCell from 'src/components/Role/EditRoleCell' + +type RolePageProps = { + id: string +} + +const EditRolePage = ({ id }: RolePageProps) => { + return +} + +export default EditRolePage diff --git a/web/src/pages/Role/NewRolePage/NewRolePage.tsx b/web/src/pages/Role/NewRolePage/NewRolePage.tsx new file mode 100644 index 0000000..d57626e --- /dev/null +++ b/web/src/pages/Role/NewRolePage/NewRolePage.tsx @@ -0,0 +1,7 @@ +import NewRole from 'src/components/Role/NewRole' + +const NewRolePage = () => { + return +} + +export default NewRolePage diff --git a/web/src/pages/Role/RolePage/RolePage.tsx b/web/src/pages/Role/RolePage/RolePage.tsx new file mode 100644 index 0000000..10f8203 --- /dev/null +++ b/web/src/pages/Role/RolePage/RolePage.tsx @@ -0,0 +1,11 @@ +import RoleCell from 'src/components/Role/RoleCell' + +type RolePageProps = { + id: string +} + +const RolePage = ({ id }: RolePageProps) => { + return +} + +export default RolePage diff --git a/web/src/pages/Role/RolesPage/RolesPage.tsx b/web/src/pages/Role/RolesPage/RolesPage.tsx new file mode 100644 index 0000000..1a0bcd4 --- /dev/null +++ b/web/src/pages/Role/RolesPage/RolesPage.tsx @@ -0,0 +1,7 @@ +import RolesCell from 'src/components/Role/RolesCell' + +const RolesPage = () => { + return +} + +export default RolesPage diff --git a/web/src/pages/User/EditUserPage/EditUserPage.tsx b/web/src/pages/User/EditUserPage/EditUserPage.tsx new file mode 100644 index 0000000..ac60089 --- /dev/null +++ b/web/src/pages/User/EditUserPage/EditUserPage.tsx @@ -0,0 +1,11 @@ +import EditUserCell from 'src/components/User/EditUserCell' + +type UserPageProps = { + id: string +} + +const EditUserPage = ({ id }: UserPageProps) => { + return +} + +export default EditUserPage diff --git a/web/src/pages/User/NewUserPage/NewUserPage.tsx b/web/src/pages/User/NewUserPage/NewUserPage.tsx new file mode 100644 index 0000000..c9b6dce --- /dev/null +++ b/web/src/pages/User/NewUserPage/NewUserPage.tsx @@ -0,0 +1,7 @@ +import NewUser from 'src/components/User/NewUser' + +const NewUserPage = () => { + return +} + +export default NewUserPage diff --git a/web/src/pages/User/UserPage/UserPage.tsx b/web/src/pages/User/UserPage/UserPage.tsx new file mode 100644 index 0000000..e320322 --- /dev/null +++ b/web/src/pages/User/UserPage/UserPage.tsx @@ -0,0 +1,11 @@ +import UserCell from 'src/components/User/UserCell' + +type UserPageProps = { + id: string +} + +const UserPage = ({ id }: UserPageProps) => { + return +} + +export default UserPage diff --git a/web/src/pages/User/UsersPage/UsersPage.tsx b/web/src/pages/User/UsersPage/UsersPage.tsx new file mode 100644 index 0000000..fb7b175 --- /dev/null +++ b/web/src/pages/User/UsersPage/UsersPage.tsx @@ -0,0 +1,7 @@ +import UsersCell from 'src/components/User/UsersCell' + +const UsersPage = () => { + return +} + +export default UsersPage diff --git a/web/src/scaffold.css b/web/src/scaffold.css new file mode 100644 index 0000000..3a6a215 --- /dev/null +++ b/web/src/scaffold.css @@ -0,0 +1,397 @@ +/* + normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +*/ + +.rw-scaffold *, +.rw-scaffold ::after, +.rw-scaffold ::before { + box-sizing: inherit; +} +.rw-scaffold main { + color: #4a5568; + display: block; +} +.rw-scaffold h1, +.rw-scaffold h2 { + margin: 0; +} +.rw-scaffold a { + background-color: transparent; +} +.rw-scaffold ul { + margin: 0; + padding: 0; +} +.rw-scaffold input { + font-family: inherit; + font-size: 100%; + overflow: visible; +} +.rw-scaffold input:-ms-input-placeholder { + color: #a0aec0; +} +.rw-scaffold input::-ms-input-placeholder { + color: #a0aec0; +} +.rw-scaffold input::placeholder { + color: #a0aec0; +} +.rw-scaffold table { + border-collapse: collapse; +} + +/* + Style +*/ + +.rw-scaffold, +.rw-toast { + background-color: #fff; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; +} +.rw-header { + display: flex; + justify-content: space-between; + padding: 1rem 2rem 1rem 2rem; +} +.rw-main { + margin-left: 1rem; + margin-right: 1rem; + padding-bottom: 1rem; +} +.rw-segment { + border-radius: 0.5rem; + border-width: 1px; + border-color: #e5e7eb; + overflow: hidden; + width: 100%; + scrollbar-color: #a1a1aa transparent; +} +.rw-segment::-webkit-scrollbar { + height: initial; +} +.rw-segment::-webkit-scrollbar-track { + background-color: transparent; + border-color: #e2e8f0; + border-style: solid; + border-radius: 0 0 10px 10px; + border-width: 1px 0 0 0; + padding: 2px; +} +.rw-segment::-webkit-scrollbar-thumb { + background-color: #a1a1aa; + background-clip: content-box; + border: 3px solid transparent; + border-radius: 10px; +} +.rw-segment-header { + background-color: #e2e8f0; + color: #4a5568; + padding: 0.75rem 1rem; +} +.rw-segment-main { + background-color: #f7fafc; + padding: 1rem; +} +.rw-link { + color: #4299e1; + text-decoration: underline; +} +.rw-link:hover { + color: #2b6cb0; +} +.rw-forgot-link { + font-size: 0.75rem; + color: #a0aec0; + text-align: right; + margin-top: 0.1rem; +} +.rw-forgot-link:hover { + font-size: 0.75rem; + color: #4299e1; +} +.rw-heading { + font-weight: 600; +} +.rw-heading.rw-heading-primary { + font-size: 1.25rem; +} +.rw-heading.rw-heading-secondary { + font-size: 0.875rem; +} +.rw-heading .rw-link { + color: #4a5568; + text-decoration: none; +} +.rw-heading .rw-link:hover { + color: #1a202c; + text-decoration: underline; +} +.rw-cell-error { + font-size: 90%; + font-weight: 600; +} +.rw-form-wrapper { + box-sizing: border-box; + font-size: 0.875rem; + margin-top: -1rem; +} +.rw-cell-error, +.rw-form-error-wrapper { + padding: 1rem; + background-color: #fff5f5; + color: #c53030; + border-width: 1px; + border-color: #feb2b2; + border-radius: 0.25rem; + margin: 1rem 0; +} +.rw-form-error-title { + margin-top: 0; + margin-bottom: 0; + font-weight: 600; +} +.rw-form-error-list { + margin-top: 0.5rem; + list-style-type: disc; + list-style-position: inside; +} +.rw-button { + border: none; + color: #718096; + cursor: pointer; + display: flex; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; + padding: 0.25rem 1rem; + text-transform: uppercase; + text-decoration: none; + letter-spacing: 0.025em; + border-radius: 0.25rem; + line-height: 2; + border: 0; +} +.rw-button:hover { + background-color: #718096; + color: #fff; +} +.rw-button.rw-button-small { + font-size: 0.75rem; + border-radius: 0.125rem; + padding: 0.25rem 0.5rem; + line-height: inherit; +} +.rw-button.rw-button-green { + background-color: #48bb78; + color: #fff; +} +.rw-button.rw-button-green:hover { + background-color: #38a169; + color: #fff; +} +.rw-button.rw-button-blue { + background-color: #3182ce; + color: #fff; +} +.rw-button.rw-button-blue:hover { + background-color: #2b6cb0; +} +.rw-button.rw-button-red { + background-color: #e53e3e; + color: #fff; +} +.rw-button.rw-button-red:hover { + background-color: #c53030; +} +.rw-button-icon { + font-size: 1.25rem; + line-height: 1; + margin-right: 0.25rem; +} +.rw-button-group { + display: flex; + justify-content: center; + margin: 0.75rem 0.5rem; +} +.rw-button-group .rw-button { + margin: 0 0.25rem; +} +.rw-form-wrapper .rw-button-group { + margin-top: 2rem; + margin-bottom: 0; +} +.rw-label { + display: block; + margin-top: 1.5rem; + color: #4a5568; + font-weight: 600; + text-align: left; +} +.rw-label.rw-label-error { + color: #c53030; +} +.rw-input { + display: block; + margin-top: 0.5rem; + width: 100%; + padding: 0.5rem; + border-width: 1px; + border-style: solid; + border-color: #e2e8f0; + color: #4a5568; + border-radius: 0.25rem; + outline: none; +} +.rw-check-radio-item-none { + color: #4a5568; +} +.rw-check-radio-items { + display: flex; + justify-items: center; +} +.rw-input[type='checkbox'] { + display: inline; + width: 1rem; + margin-left: 0; + margin-right: 0.5rem; + margin-top: 0.25rem; +} +.rw-input[type='radio'] { + display: inline; + width: 1rem; + margin-left: 0; + margin-right: 0.5rem; + margin-top: 0.25rem; +} +.rw-input:focus { + border-color: #a0aec0; +} +.rw-input-error { + border-color: #c53030; + color: #c53030; +} + +.rw-input-error:focus { + outline: none; + border-color: #c53030; + box-shadow: 0 0 5px #c53030; +} + +.rw-field-error { + display: block; + margin-top: 0.25rem; + font-weight: 600; + text-transform: uppercase; + font-size: 0.75rem; + color: #c53030; +} +.rw-table-wrapper-responsive { + overflow-x: auto; +} +.rw-table-wrapper-responsive .rw-table { + min-width: 48rem; +} +.rw-table { + table-layout: auto; + width: 100%; + font-size: 0.875rem; +} +.rw-table th, +.rw-table td { + padding: 0.75rem; +} +.rw-table td { + background-color: #ffffff; + color: #1a202c; +} +.rw-table tr:nth-child(odd) td, +.rw-table tr:nth-child(odd) th { + background-color: #f7fafc; +} +.rw-table thead tr { + color: #4a5568; +} +.rw-table th { + font-weight: 600; + text-align: left; +} +.rw-table thead th { + background-color: #e2e8f0; + text-align: left; +} +.rw-table tbody th { + text-align: right; +} +@media (min-width: 768px) { + .rw-table tbody th { + width: 20%; + } +} +.rw-table tbody tr { + border-top-width: 1px; +} +.rw-table input { + margin-left: 0; +} +.rw-table-actions { + display: flex; + justify-content: flex-end; + align-items: center; + height: 17px; + padding-right: 0.25rem; +} +.rw-table-actions .rw-button { + background-color: transparent; +} +.rw-table-actions .rw-button:hover { + background-color: #718096; + color: #fff; +} +.rw-table-actions .rw-button-blue { + color: #3182ce; +} +.rw-table-actions .rw-button-blue:hover { + background-color: #3182ce; + color: #fff; +} +.rw-table-actions .rw-button-red { + color: #e53e3e; +} +.rw-table-actions .rw-button-red:hover { + background-color: #e53e3e; + color: #fff; +} +.rw-text-center { + text-align: center; +} +.rw-login-container { + display: flex; + align-items: center; + justify-content: center; + width: 24rem; + margin: 4rem auto; + flex-wrap: wrap; +} +.rw-login-container .rw-form-wrapper { + width: 100%; + text-align: center; +} +.rw-login-link { + margin-top: 1rem; + color: #4a5568; + font-size: 90%; + text-align: center; + flex-basis: 100%; +} +.rw-webauthn-wrapper { + margin: 1.5rem 1rem 1rem; + line-height: 1.4; +} +.rw-webauthn-wrapper h2 { + font-size: 150%; + font-weight: bold; + margin-bottom: 1rem; +} diff --git a/yarn.lock b/yarn.lock index 92a5d77..5703936 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17001,6 +17001,7 @@ __metadata: "@redwoodjs/web": "npm:8.3.0" "@types/react": "npm:^18.2.55" "@types/react-dom": "npm:^18.2.19" + humanize-string: "npm:2.1.0" react: "npm:18.3.1" react-dom: "npm:18.3.1" languageName: unknown