Create schema and scaffold

This commit is contained in:
Graeme Ross 2024-10-06 17:12:25 +01:00
parent db2e273f25
commit dc43d28a89
77 changed files with 3899 additions and 7 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,37 @@
import type { Prisma, Account } from '@prisma/client'
import type { ScenarioData } from '@redwoodjs/testing/api'
export const standard = defineScenario<Prisma.AccountCreateArgs>({
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<Account, 'account'>

View File

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

View File

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

View File

@ -0,0 +1,29 @@
import type { Prisma, ContactAddress } from '@prisma/client'
import type { ScenarioData } from '@redwoodjs/testing/api'
export const standard = defineScenario<Prisma.ContactAddressCreateArgs>({
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<ContactAddress, 'contactAddress'>

View File

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

View File

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

View File

@ -0,0 +1,11 @@
import type { Prisma, Role } from '@prisma/client'
import type { ScenarioData } from '@redwoodjs/testing/api'
export const standard = defineScenario<Prisma.RoleCreateArgs>({
role: {
one: { data: { name: 'String3272615' } },
two: { data: { name: 'String6245503' } },
},
})
export type StandardScenario = ScenarioData<Role, 'role'>

View File

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

View File

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

View File

@ -0,0 +1,41 @@
import type { Prisma, User } from '@prisma/client'
import type { ScenarioData } from '@redwoodjs/testing/api'
export const standard = defineScenario<Prisma.UserCreateArgs>({
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<User, 'user'>

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (
<Router>
<Route notfound page={NotFoundPage} />
<Set wrap={ScaffoldLayout} title="Roles" titleTo="roles" buttonLabel="New Role" buttonTo="newRole">
<Route path="/roles/new" page={RoleNewRolePage} name="newRole" />
<Route path="/roles/{id}/edit" page={RoleEditRolePage} name="editRole" />
<Route path="/roles/{id}" page={RoleRolePage} name="role" />
<Route path="/roles" page={RoleRolesPage} name="roles" />
</Set>
<Set wrap={ScaffoldLayout} title="Users" titleTo="users" buttonLabel="New User" buttonTo="newUser">
<Route path="/users/new" page={UserNewUserPage} name="newUser" />
<Route path="/users/{id}/edit" page={UserEditUserPage} name="editUser" />
<Route path="/users/{id}" page={UserUserPage} name="user" />
<Route path="/users" page={UserUsersPage} name="users" />
</Set>
<Set wrap={ScaffoldLayout} title="ContactAddresses" titleTo="contactAddresses" buttonLabel="New ContactAddress" buttonTo="newContactAddress">
<Route path="/contact-addresses/new" page={ContactAddressNewContactAddressPage} name="newContactAddress" />
<Route path="/contact-addresses/{id}/edit" page={ContactAddressEditContactAddressPage} name="editContactAddress" />
<Route path="/contact-addresses/{id}" page={ContactAddressContactAddressPage} name="contactAddress" />
<Route path="/contact-addresses" page={ContactAddressContactAddressesPage} name="contactAddresses" />
</Set>
<Set wrap={ScaffoldLayout} title="Accounts" titleTo="accounts" buttonLabel="New Account" buttonTo="newAccount">
<Route path="/accounts/new" page={AccountNewAccountPage} name="newAccount" />
<Route path="/accounts/{id}/edit" page={AccountEditAccountPage} name="editAccount" />
<Route path="/accounts/{id}" page={AccountAccountPage} name="account" />
<Route path="/accounts" page={AccountAccountsPage} name="accounts" />
</Set>
<Set wrap={ClientLayout}>
<Route path="/home" page={HomePage} name="home" />
<Route notfound page={NotFoundPage} />
</Set>
</Router>
)
}

View File

@ -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<FindAccountById['account']>
}
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 (
<>
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Account {account.id} Detail
</h2>
</header>
<table className="rw-table">
<tbody>
<tr>
<th>Id</th>
<td>{account.id}</td>
</tr>
<tr>
<th>Active</th>
<td>{checkboxInputTag(account.active)}</td>
</tr>
<tr>
<th>Paid up</th>
<td>{checkboxInputTag(account.PaidUp)}</td>
</tr>
<tr>
<th>Contact address id</th>
<td>{account.contactAddressId}</td>
</tr>
<tr>
<th>User id</th>
<td>{account.userId}</td>
</tr>
</tbody>
</table>
</div>
<nav className="rw-button-group">
<Link
to={routes.editAccount({ id: account.id })}
className="rw-button rw-button-blue"
>
Edit
</Link>
<button
type="button"
className="rw-button rw-button-red"
onClick={() => onDeleteClick(account.id)}
>
Delete
</button>
</nav>
</>
)
}
export default Account

View File

@ -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 = () => <div>Loading...</div>
export const Empty = () => <div>Account not found</div>
export const Failure = ({
error,
}: CellFailureProps<FindAccountByIdVariables>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
account,
}: CellSuccessProps<FindAccountById, FindAccountByIdVariables>) => {
return <Account account={account} />
}

View File

@ -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<EditAccountById['account']>
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 (
<div className="rw-form-wrapper">
<Form<FormAccount> onSubmit={onSubmit} error={props.error}>
<FormError
error={props.error}
wrapperClassName="rw-form-error-wrapper"
titleClassName="rw-form-error-title"
listClassName="rw-form-error-list"
/>
<Label
name="active"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Active
</Label>
<CheckboxField
name="active"
defaultChecked={props.account?.active}
className="rw-input"
errorClassName="rw-input rw-input-error"
/>
<FieldError name="active" className="rw-field-error" />
<Label
name="PaidUp"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Paid up
</Label>
<CheckboxField
name="PaidUp"
defaultChecked={props.account?.PaidUp}
className="rw-input"
errorClassName="rw-input rw-input-error"
/>
<FieldError name="PaidUp" className="rw-field-error" />
<Label
name="contactAddressId"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Contact address id
</Label>
<TextField
name="contactAddressId"
defaultValue={props.account?.contactAddressId}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="contactAddressId" className="rw-field-error" />
<Label
name="userId"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
User id
</Label>
<TextField
name="userId"
defaultValue={props.account?.userId}
className="rw-input"
errorClassName="rw-input rw-input-error"
/>
<FieldError name="userId" className="rw-field-error" />
<div className="rw-button-group">
<Submit disabled={props.loading} className="rw-button rw-button-blue">
Save
</Submit>
</div>
</Form>
</div>
)
}
export default AccountForm

View File

@ -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 (
<div className="rw-segment rw-table-wrapper-responsive">
<table className="rw-table">
<thead>
<tr>
<th>Id</th>
<th>Active</th>
<th>Paid up</th>
<th>Contact address id</th>
<th>User id</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{accounts.map((account) => (
<tr key={account.id}>
<td>{truncate(account.id)}</td>
<td>{checkboxInputTag(account.active)}</td>
<td>{checkboxInputTag(account.PaidUp)}</td>
<td>{truncate(account.contactAddressId)}</td>
<td>{truncate(account.userId)}</td>
<td>
<nav className="rw-table-actions">
<Link
to={routes.account({ id: account.id })}
title={'Show account ' + account.id + ' detail'}
className="rw-button rw-button-small"
>
Show
</Link>
<Link
to={routes.editAccount({ id: account.id })}
title={'Edit account ' + account.id}
className="rw-button rw-button-small rw-button-blue"
>
Edit
</Link>
<button
type="button"
title={'Delete account ' + account.id}
className="rw-button rw-button-small rw-button-red"
onClick={() => onDeleteClick(account.id)}
>
Delete
</button>
</nav>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default AccountsList

View File

@ -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<FindAccounts, FindAccountsVariables> =
gql`
query FindAccounts {
accounts {
id
active
PaidUp
contactAddressId
userId
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => {
return (
<div className="rw-text-center">
No accounts yet.{' '}
<Link to={routes.newAccount()} className="rw-link">
Create one?
</Link>
</div>
)
}
export const Failure = ({ error }: CellFailureProps<FindAccounts>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
accounts,
}: CellSuccessProps<FindAccounts, FindAccountsVariables>) => {
return <Accounts accounts={accounts} />
}

View File

@ -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<EditAccountById> = 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 = () => <div>Loading...</div>
export const Failure = ({ error }: CellFailureProps) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({ account }: CellSuccessProps<EditAccountById>) => {
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 (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Edit Account {account?.id}
</h2>
</header>
<div className="rw-segment-main">
<AccountForm
account={account}
onSave={onSave}
error={error}
loading={loading}
/>
</div>
</div>
)
}

View File

@ -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 (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">New Account</h2>
</header>
<div className="rw-segment-main">
<AccountForm onSave={onSave} loading={loading} error={error} />
</div>
</div>
)
}
export default NewAccount

View File

@ -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<FindContactAddressById['contactAddress']>
}
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 (
<>
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
ContactAddress {contactAddress.id} Detail
</h2>
</header>
<table className="rw-table">
<tbody>
<tr>
<th>Id</th>
<td>{contactAddress.id}</td>
</tr>
<tr>
<th>Name</th>
<td>{contactAddress.Name}</td>
</tr>
<tr>
<th>Address1</th>
<td>{contactAddress.Address1}</td>
</tr>
<tr>
<th>Address2</th>
<td>{contactAddress.Address2}</td>
</tr>
<tr>
<th>Address3</th>
<td>{contactAddress.Address3}</td>
</tr>
<tr>
<th>Town</th>
<td>{contactAddress.Town}</td>
</tr>
<tr>
<th>Postal code</th>
<td>{contactAddress.PostalCode}</td>
</tr>
</tbody>
</table>
</div>
<nav className="rw-button-group">
<Link
to={routes.editContactAddress({ id: contactAddress.id })}
className="rw-button rw-button-blue"
>
Edit
</Link>
<button
type="button"
className="rw-button rw-button-red"
onClick={() => onDeleteClick(contactAddress.id)}
>
Delete
</button>
</nav>
</>
)
}
export default ContactAddress

View File

@ -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 = () => <div>Loading...</div>
export const Empty = () => <div>ContactAddress not found</div>
export const Failure = ({
error,
}: CellFailureProps<FindContactAddressByIdVariables>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
contactAddress,
}: CellSuccessProps<
FindContactAddressById,
FindContactAddressByIdVariables
>) => {
return <ContactAddress contactAddress={contactAddress} />
}

View File

@ -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<EditContactAddressById['contactAddress']>
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 (
<div className="rw-form-wrapper">
<Form<FormContactAddress> onSubmit={onSubmit} error={props.error}>
<FormError
error={props.error}
wrapperClassName="rw-form-error-wrapper"
titleClassName="rw-form-error-title"
listClassName="rw-form-error-list"
/>
<Label
name="Name"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Name
</Label>
<TextField
name="Name"
defaultValue={props.contactAddress?.Name}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="Name" className="rw-field-error" />
<Label
name="Address1"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Address1
</Label>
<TextField
name="Address1"
defaultValue={props.contactAddress?.Address1}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="Address1" className="rw-field-error" />
<Label
name="Address2"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Address2
</Label>
<TextField
name="Address2"
defaultValue={props.contactAddress?.Address2}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="Address2" className="rw-field-error" />
<Label
name="Address3"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Address3
</Label>
<TextField
name="Address3"
defaultValue={props.contactAddress?.Address3}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="Address3" className="rw-field-error" />
<Label
name="Town"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Town
</Label>
<TextField
name="Town"
defaultValue={props.contactAddress?.Town}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="Town" className="rw-field-error" />
<Label
name="PostalCode"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Postal code
</Label>
<TextField
name="PostalCode"
defaultValue={props.contactAddress?.PostalCode}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="PostalCode" className="rw-field-error" />
<div className="rw-button-group">
<Submit disabled={props.loading} className="rw-button rw-button-blue">
Save
</Submit>
</div>
</Form>
</div>
)
}
export default ContactAddressForm

View File

@ -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 (
<div className="rw-segment rw-table-wrapper-responsive">
<table className="rw-table">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Address1</th>
<th>Address2</th>
<th>Address3</th>
<th>Town</th>
<th>Postal code</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{contactAddresses.map((contactAddress) => (
<tr key={contactAddress.id}>
<td>{truncate(contactAddress.id)}</td>
<td>{truncate(contactAddress.Name)}</td>
<td>{truncate(contactAddress.Address1)}</td>
<td>{truncate(contactAddress.Address2)}</td>
<td>{truncate(contactAddress.Address3)}</td>
<td>{truncate(contactAddress.Town)}</td>
<td>{truncate(contactAddress.PostalCode)}</td>
<td>
<nav className="rw-table-actions">
<Link
to={routes.contactAddress({ id: contactAddress.id })}
title={
'Show contactAddress ' + contactAddress.id + ' detail'
}
className="rw-button rw-button-small"
>
Show
</Link>
<Link
to={routes.editContactAddress({ id: contactAddress.id })}
title={'Edit contactAddress ' + contactAddress.id}
className="rw-button rw-button-small rw-button-blue"
>
Edit
</Link>
<button
type="button"
title={'Delete contactAddress ' + contactAddress.id}
className="rw-button rw-button-small rw-button-red"
onClick={() => onDeleteClick(contactAddress.id)}
>
Delete
</button>
</nav>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default ContactAddressesList

View File

@ -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 = () => <div>Loading...</div>
export const Empty = () => {
return (
<div className="rw-text-center">
No contactAddresses yet.{' '}
<Link to={routes.newContactAddress()} className="rw-link">
Create one?
</Link>
</div>
)
}
export const Failure = ({ error }: CellFailureProps<FindContactAddresses>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
contactAddresses,
}: CellSuccessProps<FindContactAddresses, FindContactAddressesVariables>) => {
return <ContactAddresses contactAddresses={contactAddresses} />
}

View File

@ -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<EditContactAddressById> = 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 = () => <div>Loading...</div>
export const Failure = ({ error }: CellFailureProps) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
contactAddress,
}: CellSuccessProps<EditContactAddressById>) => {
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 (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Edit ContactAddress {contactAddress?.id}
</h2>
</header>
<div className="rw-segment-main">
<ContactAddressForm
contactAddress={contactAddress}
onSave={onSave}
error={error}
loading={loading}
/>
</div>
</div>
)
}

View File

@ -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 (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">New ContactAddress</h2>
</header>
<div className="rw-segment-main">
<ContactAddressForm onSave={onSave} loading={loading} error={error} />
</div>
</div>
)
}
export default NewContactAddress

View File

@ -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<EditRoleById> = 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 = () => <div>Loading...</div>
export const Failure = ({ error }: CellFailureProps) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({ role }: CellSuccessProps<EditRoleById>) => {
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 (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Edit Role {role?.id}
</h2>
</header>
<div className="rw-segment-main">
<RoleForm role={role} onSave={onSave} error={error} loading={loading} />
</div>
</div>
)
}

View File

@ -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 (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">New Role</h2>
</header>
<div className="rw-segment-main">
<RoleForm onSave={onSave} loading={loading} error={error} />
</div>
</div>
)
}
export default NewRole

View File

@ -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<FindRoleById['role']>
}
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 (
<>
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Role {role.id} Detail
</h2>
</header>
<table className="rw-table">
<tbody>
<tr>
<th>Id</th>
<td>{role.id}</td>
</tr>
<tr>
<th>Name</th>
<td>{role.name}</td>
</tr>
<tr>
<th>User id</th>
<td>{role.userId}</td>
</tr>
</tbody>
</table>
</div>
<nav className="rw-button-group">
<Link
to={routes.editRole({ id: role.id })}
className="rw-button rw-button-blue"
>
Edit
</Link>
<button
type="button"
className="rw-button rw-button-red"
onClick={() => onDeleteClick(role.id)}
>
Delete
</button>
</nav>
</>
)
}
export default Role

View File

@ -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<FindRoleById, FindRoleByIdVariables> =
gql`
query FindRoleById($id: String!) {
role: role(id: $id) {
id
name
userId
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Role not found</div>
export const Failure = ({ error }: CellFailureProps<FindRoleByIdVariables>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
role,
}: CellSuccessProps<FindRoleById, FindRoleByIdVariables>) => {
return <Role role={role} />
}

View File

@ -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<EditRoleById['role']>
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 (
<div className="rw-form-wrapper">
<Form<FormRole> onSubmit={onSubmit} error={props.error}>
<FormError
error={props.error}
wrapperClassName="rw-form-error-wrapper"
titleClassName="rw-form-error-title"
listClassName="rw-form-error-list"
/>
<Label
name="name"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Name
</Label>
<TextField
name="name"
defaultValue={props.role?.name}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="name" className="rw-field-error" />
<Label
name="userId"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
User id
</Label>
<TextField
name="userId"
defaultValue={props.role?.userId}
className="rw-input"
errorClassName="rw-input rw-input-error"
/>
<FieldError name="userId" className="rw-field-error" />
<div className="rw-button-group">
<Submit disabled={props.loading} className="rw-button rw-button-blue">
Save
</Submit>
</div>
</Form>
</div>
)
}
export default RoleForm

View File

@ -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 (
<div className="rw-segment rw-table-wrapper-responsive">
<table className="rw-table">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>User id</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{roles.map((role) => (
<tr key={role.id}>
<td>{truncate(role.id)}</td>
<td>{truncate(role.name)}</td>
<td>{truncate(role.userId)}</td>
<td>
<nav className="rw-table-actions">
<Link
to={routes.role({ id: role.id })}
title={'Show role ' + role.id + ' detail'}
className="rw-button rw-button-small"
>
Show
</Link>
<Link
to={routes.editRole({ id: role.id })}
title={'Edit role ' + role.id}
className="rw-button rw-button-small rw-button-blue"
>
Edit
</Link>
<button
type="button"
title={'Delete role ' + role.id}
className="rw-button rw-button-small rw-button-red"
onClick={() => onDeleteClick(role.id)}
>
Delete
</button>
</nav>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default RolesList

View File

@ -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<FindRoles, FindRolesVariables> = gql`
query FindRoles {
roles {
id
name
userId
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => {
return (
<div className="rw-text-center">
No roles yet.{' '}
<Link to={routes.newRole()} className="rw-link">
Create one?
</Link>
</div>
)
}
export const Failure = ({ error }: CellFailureProps<FindRoles>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
roles,
}: CellSuccessProps<FindRoles, FindRolesVariables>) => {
return <Roles roles={roles} />
}

View File

@ -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<EditUserById> = 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 = () => <div>Loading...</div>
export const Failure = ({ error }: CellFailureProps) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({ user }: CellSuccessProps<EditUserById>) => {
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 (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Edit User {user?.id}
</h2>
</header>
<div className="rw-segment-main">
<UserForm user={user} onSave={onSave} error={error} loading={loading} />
</div>
</div>
)
}

View File

@ -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 (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">New User</h2>
</header>
<div className="rw-segment-main">
<UserForm onSave={onSave} loading={loading} error={error} />
</div>
</div>
)
}
export default NewUser

View File

@ -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<FindUserById['user']>
}
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 (
<>
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
User {user.id} Detail
</h2>
</header>
<table className="rw-table">
<tbody>
<tr>
<th>Id</th>
<td>{user.id}</td>
</tr>
<tr>
<th>User id</th>
<td>{user.userId}</td>
</tr>
<tr>
<th>Email</th>
<td>{user.email}</td>
</tr>
<tr>
<th>Contact address id</th>
<td>{user.contactAddressId}</td>
</tr>
</tbody>
</table>
</div>
<nav className="rw-button-group">
<Link
to={routes.editUser({ id: user.id })}
className="rw-button rw-button-blue"
>
Edit
</Link>
<button
type="button"
className="rw-button rw-button-red"
onClick={() => onDeleteClick(user.id)}
>
Delete
</button>
</nav>
</>
)
}
export default User

View File

@ -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<FindUserById, FindUserByIdVariables> =
gql`
query FindUserById($id: String!) {
user: user(id: $id) {
id
userId
email
contactAddressId
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>User not found</div>
export const Failure = ({ error }: CellFailureProps<FindUserByIdVariables>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
user,
}: CellSuccessProps<FindUserById, FindUserByIdVariables>) => {
return <User user={user} />
}

View File

@ -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<EditUserById['user']>
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 (
<div className="rw-form-wrapper">
<Form<FormUser> onSubmit={onSubmit} error={props.error}>
<FormError
error={props.error}
wrapperClassName="rw-form-error-wrapper"
titleClassName="rw-form-error-title"
listClassName="rw-form-error-list"
/>
<Label
name="userId"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
User id
</Label>
<TextField
name="userId"
defaultValue={props.user?.userId}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="userId" className="rw-field-error" />
<Label
name="email"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Email
</Label>
<TextField
name="email"
defaultValue={props.user?.email}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="email" className="rw-field-error" />
<Label
name="contactAddressId"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Contact address id
</Label>
<TextField
name="contactAddressId"
defaultValue={props.user?.contactAddressId}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="contactAddressId" className="rw-field-error" />
<div className="rw-button-group">
<Submit disabled={props.loading} className="rw-button rw-button-blue">
Save
</Submit>
</div>
</Form>
</div>
)
}
export default UserForm

View File

@ -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 (
<div className="rw-segment rw-table-wrapper-responsive">
<table className="rw-table">
<thead>
<tr>
<th>Id</th>
<th>User id</th>
<th>Email</th>
<th>Contact address id</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td>{truncate(user.id)}</td>
<td>{truncate(user.userId)}</td>
<td>{truncate(user.email)}</td>
<td>{truncate(user.contactAddressId)}</td>
<td>
<nav className="rw-table-actions">
<Link
to={routes.user({ id: user.id })}
title={'Show user ' + user.id + ' detail'}
className="rw-button rw-button-small"
>
Show
</Link>
<Link
to={routes.editUser({ id: user.id })}
title={'Edit user ' + user.id}
className="rw-button rw-button-small rw-button-blue"
>
Edit
</Link>
<button
type="button"
title={'Delete user ' + user.id}
className="rw-button rw-button-small rw-button-red"
onClick={() => onDeleteClick(user.id)}
>
Delete
</button>
</nav>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default UsersList

View File

@ -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<FindUsers, FindUsersVariables> = gql`
query FindUsers {
users {
id
userId
email
contactAddressId
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => {
return (
<div className="rw-text-center">
No users yet.{' '}
<Link to={routes.newUser()} className="rw-link">
Create one?
</Link>
</div>
)
}
export const Failure = ({ error }: CellFailureProps<FindUsers>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
users,
}: CellSuccessProps<FindUsers, FindUsersVariables>) => {
return <Users users={users} />
}

View File

@ -0,0 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react'
import ClientLayout from './ClientLayout'
const meta: Meta<typeof ClientLayout> = {
component: ClientLayout,
}
export default meta
type Story = StoryObj<typeof ClientLayout>
export const Primary: Story = {}

View File

@ -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(<ClientLayout />)
}).not.toThrow()
})
})

View File

@ -0,0 +1,25 @@
import { Link, routes } from '@redwoodjs/router'
type ClientLayoutProps = {
children?: React.ReactNode
}
const ClientLayout = ({ children }: ClientLayoutProps) => {
return (
<>
<header>
<h1>Redwood Blog</h1>
<nav>
<ul>
<li>
<Link to={routes.home()}>Home</Link>
</li>
</ul>
</nav>
</header>
<main>{children}</main>
</>
)
}
export default ClientLayout

View File

@ -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 (
<div className="rw-scaffold">
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
<header className="rw-header">
<h1 className="rw-heading rw-heading-primary">
<Link to={routes[titleTo]()} className="rw-link">
{title}
</Link>
</h1>
<Link to={routes[buttonTo]()} className="rw-button rw-button-green">
<div className="rw-button-icon">+</div> {buttonLabel}
</Link>
</header>
<main className="rw-main">{children}</main>
</div>
)
}
export default ScaffoldLayout

View File

@ -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(<div>{timeTag(new Date('1970-08-20').toUTCString())}</div>)
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(`
<pre>
<code>
{
"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"
}
}
}
</code>
</pre>
`)
})
})
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()
})
})

View File

@ -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 (
<pre>
<code>{JSON.stringify(obj, null, 2)}</code>
</pre>
)
}
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 = (
<time dateTime={dateTime} title={dateTime}>
{new Date(dateTime).toUTCString()}
</time>
)
}
return output
}
export const checkboxInputTag = (checked: boolean) => {
return <input type="checkbox" checked={checked} disabled />
}

View File

@ -0,0 +1,11 @@
import AccountCell from 'src/components/Account/AccountCell'
type AccountPageProps = {
id: string
}
const AccountPage = ({ id }: AccountPageProps) => {
return <AccountCell id={id} />
}
export default AccountPage

View File

@ -0,0 +1,7 @@
import AccountsCell from 'src/components/Account/AccountsCell'
const AccountsPage = () => {
return <AccountsCell />
}
export default AccountsPage

View File

@ -0,0 +1,11 @@
import EditAccountCell from 'src/components/Account/EditAccountCell'
type AccountPageProps = {
id: string
}
const EditAccountPage = ({ id }: AccountPageProps) => {
return <EditAccountCell id={id} />
}
export default EditAccountPage

View File

@ -0,0 +1,7 @@
import NewAccount from 'src/components/Account/NewAccount'
const NewAccountPage = () => {
return <NewAccount />
}
export default NewAccountPage

View File

@ -0,0 +1,11 @@
import ContactAddressCell from 'src/components/ContactAddress/ContactAddressCell'
type ContactAddressPageProps = {
id: string
}
const ContactAddressPage = ({ id }: ContactAddressPageProps) => {
return <ContactAddressCell id={id} />
}
export default ContactAddressPage

View File

@ -0,0 +1,7 @@
import ContactAddressesCell from 'src/components/ContactAddress/ContactAddressesCell'
const ContactAddressesPage = () => {
return <ContactAddressesCell />
}
export default ContactAddressesPage

View File

@ -0,0 +1,11 @@
import EditContactAddressCell from 'src/components/ContactAddress/EditContactAddressCell'
type ContactAddressPageProps = {
id: string
}
const EditContactAddressPage = ({ id }: ContactAddressPageProps) => {
return <EditContactAddressCell id={id} />
}
export default EditContactAddressPage

View File

@ -0,0 +1,7 @@
import NewContactAddress from 'src/components/ContactAddress/NewContactAddress'
const NewContactAddressPage = () => {
return <NewContactAddress />
}
export default NewContactAddressPage

View File

@ -0,0 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react'
import HomePage from './HomePage'
const meta: Meta<typeof HomePage> = {
component: HomePage,
}
export default meta
type Story = StoryObj<typeof HomePage>
export const Primary: Story = {}

View File

@ -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(<HomePage />)
}).not.toThrow()
})
})

View File

@ -0,0 +1,21 @@
// import { Link, routes } from '@redwoodjs/router'
import { Metadata } from '@redwoodjs/web'
const HomePage = () => {
return (
<>
<Metadata title="Home" description="Home page" />
<h1>HomePage</h1>
<p>
Find me in <code>./web/src/pages/HomePage/HomePage.tsx</code>
</p>
{/*
My default route is named `home`, link to me with:
`<Link to={routes.home()}>Home</Link>`
*/}
</>
)
}
export default HomePage

View File

@ -0,0 +1,11 @@
import EditRoleCell from 'src/components/Role/EditRoleCell'
type RolePageProps = {
id: string
}
const EditRolePage = ({ id }: RolePageProps) => {
return <EditRoleCell id={id} />
}
export default EditRolePage

View File

@ -0,0 +1,7 @@
import NewRole from 'src/components/Role/NewRole'
const NewRolePage = () => {
return <NewRole />
}
export default NewRolePage

View File

@ -0,0 +1,11 @@
import RoleCell from 'src/components/Role/RoleCell'
type RolePageProps = {
id: string
}
const RolePage = ({ id }: RolePageProps) => {
return <RoleCell id={id} />
}
export default RolePage

View File

@ -0,0 +1,7 @@
import RolesCell from 'src/components/Role/RolesCell'
const RolesPage = () => {
return <RolesCell />
}
export default RolesPage

View File

@ -0,0 +1,11 @@
import EditUserCell from 'src/components/User/EditUserCell'
type UserPageProps = {
id: string
}
const EditUserPage = ({ id }: UserPageProps) => {
return <EditUserCell id={id} />
}
export default EditUserPage

View File

@ -0,0 +1,7 @@
import NewUser from 'src/components/User/NewUser'
const NewUserPage = () => {
return <NewUser />
}
export default NewUserPage

View File

@ -0,0 +1,11 @@
import UserCell from 'src/components/User/UserCell'
type UserPageProps = {
id: string
}
const UserPage = ({ id }: UserPageProps) => {
return <UserCell id={id} />
}
export default UserPage

View File

@ -0,0 +1,7 @@
import UsersCell from 'src/components/User/UsersCell'
const UsersPage = () => {
return <UsersCell />
}
export default UsersPage

397
web/src/scaffold.css Normal file
View File

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

View File

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