Create schema and scaffold
This commit is contained in:
parent
db2e273f25
commit
dc43d28a89
44
api/db/migrations/20241006155320_initial_setup/migration.sql
Normal file
44
api/db/migrations/20241006155320_initial_setup/migration.sql
Normal 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");
|
||||
3
api/db/migrations/migration_lock.toml
Normal file
3
api/db/migrations/migration_lock.toml
Normal 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"
|
||||
@ -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?
|
||||
}
|
||||
|
||||
37
api/src/graphql/accounts.sdl.ts
Normal file
37
api/src/graphql/accounts.sdl.ts
Normal 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
|
||||
}
|
||||
`
|
||||
46
api/src/graphql/contactAddresses.sdl.ts
Normal file
46
api/src/graphql/contactAddresses.sdl.ts
Normal 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
|
||||
}
|
||||
`
|
||||
29
api/src/graphql/roles.sdl.ts
Normal file
29
api/src/graphql/roles.sdl.ts
Normal 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
|
||||
}
|
||||
`
|
||||
34
api/src/graphql/users.sdl.ts
Normal file
34
api/src/graphql/users.sdl.ts
Normal 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
|
||||
}
|
||||
`
|
||||
37
api/src/services/accounts/accounts.scenarios.ts
Normal file
37
api/src/services/accounts/accounts.scenarios.ts
Normal 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'>
|
||||
61
api/src/services/accounts/accounts.test.ts
Normal file
61
api/src/services/accounts/accounts.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
50
api/src/services/accounts/accounts.ts
Normal file
50
api/src/services/accounts/accounts.ts
Normal 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()
|
||||
},
|
||||
}
|
||||
@ -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'>
|
||||
79
api/src/services/contactAddresses/contactAddresses.test.ts
Normal file
79
api/src/services/contactAddresses/contactAddresses.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
48
api/src/services/contactAddresses/contactAddresses.ts
Normal file
48
api/src/services/contactAddresses/contactAddresses.ts
Normal 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()
|
||||
},
|
||||
}
|
||||
11
api/src/services/roles/roles.scenarios.ts
Normal file
11
api/src/services/roles/roles.scenarios.ts
Normal 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'>
|
||||
49
api/src/services/roles/roles.test.ts
Normal file
49
api/src/services/roles/roles.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
42
api/src/services/roles/roles.ts
Normal file
42
api/src/services/roles/roles.ts
Normal 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()
|
||||
},
|
||||
}
|
||||
41
api/src/services/users/users.scenarios.ts
Normal file
41
api/src/services/users/users.scenarios.ts
Normal 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'>
|
||||
55
api/src/services/users/users.test.ts
Normal file
55
api/src/services/users/users.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
48
api/src/services/users/users.ts
Normal file
48
api/src/services/users/users.ts
Normal 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()
|
||||
},
|
||||
}
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
98
web/src/components/Account/Account/Account.tsx
Normal file
98
web/src/components/Account/Account/Account.tsx
Normal 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
|
||||
40
web/src/components/Account/AccountCell/AccountCell.tsx
Normal file
40
web/src/components/Account/AccountCell/AccountCell.tsx
Normal 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} />
|
||||
}
|
||||
117
web/src/components/Account/AccountForm/AccountForm.tsx
Normal file
117
web/src/components/Account/AccountForm/AccountForm.tsx
Normal 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
|
||||
102
web/src/components/Account/Accounts/Accounts.tsx
Normal file
102
web/src/components/Account/Accounts/Accounts.tsx
Normal 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> </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
|
||||
46
web/src/components/Account/AccountsCell/AccountsCell.tsx
Normal file
46
web/src/components/Account/AccountsCell/AccountsCell.tsx
Normal 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} />
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
55
web/src/components/Account/NewAccount/NewAccount.tsx
Normal file
55
web/src/components/Account/NewAccount/NewAccount.tsx
Normal 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
|
||||
@ -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
|
||||
@ -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} />
|
||||
}
|
||||
@ -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
|
||||
@ -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> </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
|
||||
@ -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} />
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
74
web/src/components/Role/EditRoleCell/EditRoleCell.tsx
Normal file
74
web/src/components/Role/EditRoleCell/EditRoleCell.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
52
web/src/components/Role/NewRole/NewRole.tsx
Normal file
52
web/src/components/Role/NewRole/NewRole.tsx
Normal 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
|
||||
90
web/src/components/Role/Role/Role.tsx
Normal file
90
web/src/components/Role/Role/Role.tsx
Normal 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
|
||||
34
web/src/components/Role/RoleCell/RoleCell.tsx
Normal file
34
web/src/components/Role/RoleCell/RoleCell.tsx
Normal 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} />
|
||||
}
|
||||
82
web/src/components/Role/RoleForm/RoleForm.tsx
Normal file
82
web/src/components/Role/RoleForm/RoleForm.tsx
Normal 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
|
||||
98
web/src/components/Role/Roles/Roles.tsx
Normal file
98
web/src/components/Role/Roles/Roles.tsx
Normal 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> </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
|
||||
43
web/src/components/Role/RolesCell/RolesCell.tsx
Normal file
43
web/src/components/Role/RolesCell/RolesCell.tsx
Normal 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} />
|
||||
}
|
||||
76
web/src/components/User/EditUserCell/EditUserCell.tsx
Normal file
76
web/src/components/User/EditUserCell/EditUserCell.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
52
web/src/components/User/NewUser/NewUser.tsx
Normal file
52
web/src/components/User/NewUser/NewUser.tsx
Normal 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
|
||||
94
web/src/components/User/User/User.tsx
Normal file
94
web/src/components/User/User/User.tsx
Normal 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
|
||||
35
web/src/components/User/UserCell/UserCell.tsx
Normal file
35
web/src/components/User/UserCell/UserCell.tsx
Normal 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} />
|
||||
}
|
||||
101
web/src/components/User/UserForm/UserForm.tsx
Normal file
101
web/src/components/User/UserForm/UserForm.tsx
Normal 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
|
||||
100
web/src/components/User/Users/Users.tsx
Normal file
100
web/src/components/User/Users/Users.tsx
Normal 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> </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
|
||||
44
web/src/components/User/UsersCell/UsersCell.tsx
Normal file
44
web/src/components/User/UsersCell/UsersCell.tsx
Normal 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} />
|
||||
}
|
||||
13
web/src/layouts/ClientLayout/ClientLayout.stories.tsx
Normal file
13
web/src/layouts/ClientLayout/ClientLayout.stories.tsx
Normal 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 = {}
|
||||
14
web/src/layouts/ClientLayout/ClientLayout.test.tsx
Normal file
14
web/src/layouts/ClientLayout/ClientLayout.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
25
web/src/layouts/ClientLayout/ClientLayout.tsx
Normal file
25
web/src/layouts/ClientLayout/ClientLayout.tsx
Normal 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
|
||||
37
web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
Normal file
37
web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
Normal 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
|
||||
192
web/src/lib/formatters.test.tsx
Normal file
192
web/src/lib/formatters.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
58
web/src/lib/formatters.tsx
Normal file
58
web/src/lib/formatters.tsx
Normal 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 />
|
||||
}
|
||||
11
web/src/pages/Account/AccountPage/AccountPage.tsx
Normal file
11
web/src/pages/Account/AccountPage/AccountPage.tsx
Normal 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
|
||||
7
web/src/pages/Account/AccountsPage/AccountsPage.tsx
Normal file
7
web/src/pages/Account/AccountsPage/AccountsPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import AccountsCell from 'src/components/Account/AccountsCell'
|
||||
|
||||
const AccountsPage = () => {
|
||||
return <AccountsCell />
|
||||
}
|
||||
|
||||
export default AccountsPage
|
||||
11
web/src/pages/Account/EditAccountPage/EditAccountPage.tsx
Normal file
11
web/src/pages/Account/EditAccountPage/EditAccountPage.tsx
Normal 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
|
||||
7
web/src/pages/Account/NewAccountPage/NewAccountPage.tsx
Normal file
7
web/src/pages/Account/NewAccountPage/NewAccountPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import NewAccount from 'src/components/Account/NewAccount'
|
||||
|
||||
const NewAccountPage = () => {
|
||||
return <NewAccount />
|
||||
}
|
||||
|
||||
export default NewAccountPage
|
||||
@ -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
|
||||
@ -0,0 +1,7 @@
|
||||
import ContactAddressesCell from 'src/components/ContactAddress/ContactAddressesCell'
|
||||
|
||||
const ContactAddressesPage = () => {
|
||||
return <ContactAddressesCell />
|
||||
}
|
||||
|
||||
export default ContactAddressesPage
|
||||
@ -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
|
||||
@ -0,0 +1,7 @@
|
||||
import NewContactAddress from 'src/components/ContactAddress/NewContactAddress'
|
||||
|
||||
const NewContactAddressPage = () => {
|
||||
return <NewContactAddress />
|
||||
}
|
||||
|
||||
export default NewContactAddressPage
|
||||
13
web/src/pages/HomePage/HomePage.stories.tsx
Normal file
13
web/src/pages/HomePage/HomePage.stories.tsx
Normal 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 = {}
|
||||
14
web/src/pages/HomePage/HomePage.test.tsx
Normal file
14
web/src/pages/HomePage/HomePage.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
21
web/src/pages/HomePage/HomePage.tsx
Normal file
21
web/src/pages/HomePage/HomePage.tsx
Normal 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
|
||||
11
web/src/pages/Role/EditRolePage/EditRolePage.tsx
Normal file
11
web/src/pages/Role/EditRolePage/EditRolePage.tsx
Normal 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
|
||||
7
web/src/pages/Role/NewRolePage/NewRolePage.tsx
Normal file
7
web/src/pages/Role/NewRolePage/NewRolePage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import NewRole from 'src/components/Role/NewRole'
|
||||
|
||||
const NewRolePage = () => {
|
||||
return <NewRole />
|
||||
}
|
||||
|
||||
export default NewRolePage
|
||||
11
web/src/pages/Role/RolePage/RolePage.tsx
Normal file
11
web/src/pages/Role/RolePage/RolePage.tsx
Normal 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
|
||||
7
web/src/pages/Role/RolesPage/RolesPage.tsx
Normal file
7
web/src/pages/Role/RolesPage/RolesPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import RolesCell from 'src/components/Role/RolesCell'
|
||||
|
||||
const RolesPage = () => {
|
||||
return <RolesCell />
|
||||
}
|
||||
|
||||
export default RolesPage
|
||||
11
web/src/pages/User/EditUserPage/EditUserPage.tsx
Normal file
11
web/src/pages/User/EditUserPage/EditUserPage.tsx
Normal 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
|
||||
7
web/src/pages/User/NewUserPage/NewUserPage.tsx
Normal file
7
web/src/pages/User/NewUserPage/NewUserPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import NewUser from 'src/components/User/NewUser'
|
||||
|
||||
const NewUserPage = () => {
|
||||
return <NewUser />
|
||||
}
|
||||
|
||||
export default NewUserPage
|
||||
11
web/src/pages/User/UserPage/UserPage.tsx
Normal file
11
web/src/pages/User/UserPage/UserPage.tsx
Normal 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
|
||||
7
web/src/pages/User/UsersPage/UsersPage.tsx
Normal file
7
web/src/pages/User/UsersPage/UsersPage.tsx
Normal 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
397
web/src/scaffold.css
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user