setup settings cell and modified a query and acell to return a specific value
This commit is contained in:
parent
3f3df221c8
commit
6221af88cf
8
api/db/migrations/20241016200819_settings/migration.sql
Normal file
8
api/db/migrations/20241016200819_settings/migration.sql
Normal file
@ -0,0 +1,8 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Setting" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"enabled" BOOLEAN NOT NULL,
|
||||
"group" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL
|
||||
);
|
||||
@ -68,3 +68,11 @@ model Role {
|
||||
User User? @relation(fields: [userId], references: [id])
|
||||
userId String?
|
||||
}
|
||||
|
||||
model Setting {
|
||||
id Int @id @default(autoincrement())
|
||||
enabled Boolean
|
||||
group String
|
||||
name String
|
||||
value String
|
||||
}
|
||||
|
||||
@ -13,11 +13,13 @@ export const schema = gql`
|
||||
directive @requireAuth(roles: [String]) on FIELD_DEFINITION
|
||||
`
|
||||
|
||||
type RequireAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
|
||||
type RequireAuthValidate = ValidatorDirectiveFunc<{
|
||||
roles?: string[]
|
||||
}>
|
||||
|
||||
const validate: RequireAuthValidate = ({ directiveArgs }) => {
|
||||
const validate: RequireAuthValidate = ({ context, directiveArgs }) => {
|
||||
const { roles } = directiveArgs
|
||||
applicationRequireAuth({ roles })
|
||||
applicationRequireAuth({ context, roles })
|
||||
}
|
||||
|
||||
const requireAuth = createValidatorDirective(schema, validate)
|
||||
|
||||
35
api/src/graphql/settings.sdl.ts
Normal file
35
api/src/graphql/settings.sdl.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export const schema = gql`
|
||||
type Setting {
|
||||
id: Int!
|
||||
enabled: Boolean!
|
||||
group(group: String = "default"): String!
|
||||
name(name: String): String!
|
||||
value: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
settings: [Setting!]! @requireAuth
|
||||
setting(id: Int!): Setting @skipAuth
|
||||
value(name: String, group: String): [Setting!] @skipAuth
|
||||
}
|
||||
|
||||
input CreateSettingInput {
|
||||
enabled: Boolean!
|
||||
group: String!
|
||||
name: String!
|
||||
value: String!
|
||||
}
|
||||
|
||||
input UpdateSettingInput {
|
||||
enabled: Boolean
|
||||
group: String
|
||||
name: String
|
||||
value: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createSetting(input: CreateSettingInput!): Setting! @requireAuth
|
||||
updateSetting(id: Int!, input: UpdateSettingInput!): Setting! @requireAuth
|
||||
deleteSetting(id: Int!): Setting! @requireAuth
|
||||
}
|
||||
`
|
||||
@ -45,7 +45,7 @@ export const getCurrentUser = async (session: Decoded) => {
|
||||
*
|
||||
* @returns {boolean} - If the currentUser is authenticated
|
||||
*/
|
||||
export const isAuthenticated = (): boolean => {
|
||||
export const isAuthenticated = (context?): boolean => {
|
||||
return !!context.currentUser
|
||||
}
|
||||
|
||||
@ -110,8 +110,14 @@ export const hasRole = (roles: AllowedRoles): boolean => {
|
||||
*
|
||||
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
|
||||
*/
|
||||
export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
|
||||
if (!isAuthenticated()) {
|
||||
export const requireAuth = ({
|
||||
context,
|
||||
roles,
|
||||
}: {
|
||||
context
|
||||
roles?: AllowedRoles
|
||||
}) => {
|
||||
if (!isAuthenticated(context)) {
|
||||
throw new AuthenticationError("You don't have permission to do that.")
|
||||
}
|
||||
|
||||
|
||||
15
api/src/services/settings/settings.scenarios.ts
Normal file
15
api/src/services/settings/settings.scenarios.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { Prisma, Setting } from '@prisma/client'
|
||||
import type { ScenarioData } from '@redwoodjs/testing/api'
|
||||
|
||||
export const standard = defineScenario<Prisma.SettingCreateArgs>({
|
||||
setting: {
|
||||
one: {
|
||||
data: { enabled: true, group: 'String', name: 'String', value: 'String' },
|
||||
},
|
||||
two: {
|
||||
data: { enabled: true, group: 'String', name: 'String', value: 'String' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type StandardScenario = ScenarioData<Setting, 'setting'>
|
||||
65
api/src/services/settings/settings.test.ts
Normal file
65
api/src/services/settings/settings.test.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import type { Setting } from '@prisma/client'
|
||||
|
||||
import {
|
||||
settings,
|
||||
setting,
|
||||
createSetting,
|
||||
updateSetting,
|
||||
deleteSetting,
|
||||
} from './settings'
|
||||
import type { StandardScenario } from './settings.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('settings', () => {
|
||||
scenario('returns all settings', async (scenario: StandardScenario) => {
|
||||
const result = await settings()
|
||||
|
||||
expect(result.length).toEqual(Object.keys(scenario.setting).length)
|
||||
})
|
||||
|
||||
scenario('returns a single setting', async (scenario: StandardScenario) => {
|
||||
const result = await setting({ id: scenario.setting.one.id })
|
||||
|
||||
expect(result).toEqual(scenario.setting.one)
|
||||
})
|
||||
|
||||
scenario('creates a setting', async () => {
|
||||
const result = await createSetting({
|
||||
input: {
|
||||
enabled: true,
|
||||
group: 'String',
|
||||
name: 'String',
|
||||
value: 'String',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.enabled).toEqual(true)
|
||||
expect(result.group).toEqual('String')
|
||||
expect(result.name).toEqual('String')
|
||||
expect(result.value).toEqual('String')
|
||||
})
|
||||
|
||||
scenario('updates a setting', async (scenario: StandardScenario) => {
|
||||
const original = (await setting({ id: scenario.setting.one.id })) as Setting
|
||||
const result = await updateSetting({
|
||||
id: original.id,
|
||||
input: { enabled: false },
|
||||
})
|
||||
|
||||
expect(result.enabled).toEqual(false)
|
||||
})
|
||||
|
||||
scenario('deletes a setting', async (scenario: StandardScenario) => {
|
||||
const original = (await deleteSetting({
|
||||
id: scenario.setting.one.id,
|
||||
})) as Setting
|
||||
const result = await setting({ id: original.id })
|
||||
|
||||
expect(result).toEqual(null)
|
||||
})
|
||||
})
|
||||
44
api/src/services/settings/settings.ts
Normal file
44
api/src/services/settings/settings.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { QueryResolvers, MutationResolvers } from 'types/graphql'
|
||||
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const settings: QueryResolvers['settings'] = () => {
|
||||
return db.setting.findMany()
|
||||
}
|
||||
|
||||
export const setting: QueryResolvers['setting'] = ({ id }) => {
|
||||
return db.setting.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const value: QueryResolvers['value'] = ({ name, group }) => {
|
||||
const values = db.setting.findMany({
|
||||
where: { AND: [{ name: name }, { group: group }] },
|
||||
})
|
||||
return values
|
||||
}
|
||||
|
||||
export const createSetting: MutationResolvers['createSetting'] = ({
|
||||
input,
|
||||
}) => {
|
||||
return db.setting.create({
|
||||
data: input,
|
||||
})
|
||||
}
|
||||
|
||||
export const updateSetting: MutationResolvers['updateSetting'] = ({
|
||||
id,
|
||||
input,
|
||||
}) => {
|
||||
return db.setting.update({
|
||||
data: input,
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteSetting: MutationResolvers['deleteSetting'] = ({ id }) => {
|
||||
return db.setting.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
@ -10,6 +10,8 @@
|
||||
"@redwoodjs/auth-dbauth-setup": "8.4.0",
|
||||
"@redwoodjs/core": "8.4.0",
|
||||
"@redwoodjs/project-config": "8.4.0",
|
||||
"@redwoodjs/realtime": "8.4.0",
|
||||
"@redwoodjs/studio": "12",
|
||||
"prettier-plugin-tailwindcss": "^0.6.8"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
// 'src/pages/HomePage/HomePage.js' -> HomePage
|
||||
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
|
||||
|
||||
import { Router, Route, Set } from '@redwoodjs/router'
|
||||
import { Router, Route, PrivateSet, Set } from '@redwoodjs/router'
|
||||
|
||||
import ClientLayout from 'src/layouts/ClientLayout/ClientLayout'
|
||||
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
|
||||
@ -17,10 +17,13 @@ import { useAuth } from './auth'
|
||||
const Routes = () => {
|
||||
return (
|
||||
<Router useAuth={useAuth}>
|
||||
<Route path="/login" page={LoginPage} name="login" />
|
||||
<Route path="/signup" page={SignupPage} name="signup" />
|
||||
<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
|
||||
<Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />
|
||||
<PrivateSet unauthenticated="home">
|
||||
<Set wrap={ScaffoldLayout} title="Settings" titleTo="settings" buttonLabel="New Setting" buttonTo="newSetting">
|
||||
<Route path="/settings/new" page={SettingNewSettingPage} name="newSetting" />
|
||||
<Route path="/settings/{id:Int}/edit" page={SettingEditSettingPage} name="editSetting" />
|
||||
<Route path="/settings/{id:Int}" page={SettingSettingPage} name="setting" />
|
||||
<Route path="/settings" page={SettingSettingsPage} name="settings" />
|
||||
</Set>
|
||||
<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" />
|
||||
@ -45,6 +48,11 @@ const Routes = () => {
|
||||
<Route path="/accounts/{id}" page={AccountAccountPage} name="account" />
|
||||
<Route path="/accounts" page={AccountAccountsPage} name="accounts" />
|
||||
</Set>
|
||||
</PrivateSet>
|
||||
<Route path="/login" page={LoginPage} name="login" />
|
||||
<Route path="/signup" page={SignupPage} name="signup" />
|
||||
<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
|
||||
<Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />
|
||||
<Set wrap={ClientLayout}>
|
||||
<Route path="/home" page={HomePage} name="home" />
|
||||
<Route path="/hero" page={HeroPage} name="hero" />
|
||||
|
||||
@ -3,13 +3,17 @@ import { Link, routes } from '@redwoodjs/router'
|
||||
import { useAuth } from 'src/auth'
|
||||
import ThemeChanger from 'src/components/ThemeChanger/ThemeChanger'
|
||||
|
||||
import SettingValue from '../Setting/SettingValue/SettingValue'
|
||||
|
||||
const HeaderBar = () => {
|
||||
const { logOut } = useAuth()
|
||||
|
||||
return (
|
||||
<div className="navbar bg-base-100">
|
||||
<div className="flex-1">
|
||||
<a className="btn btn-ghost text-xl">Pendantator</a>
|
||||
<Link className="btn btn-ghost text-xl" to="${routes.home()}">
|
||||
<SettingValue name="title" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<ul className="menu menu-horizontal px-1">
|
||||
|
||||
@ -0,0 +1,89 @@
|
||||
import type {
|
||||
EditSettingById,
|
||||
UpdateSettingInput,
|
||||
UpdateSettingMutationVariables,
|
||||
} 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 SettingForm from 'src/components/Setting/SettingForm'
|
||||
|
||||
export const QUERY: TypedDocumentNode<EditSettingById> = gql`
|
||||
query EditSettingById($id: Int!) {
|
||||
setting: setting(id: $id) {
|
||||
id
|
||||
enabled
|
||||
group
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const UPDATE_SETTING_MUTATION: TypedDocumentNode<
|
||||
EditSettingById,
|
||||
UpdateSettingMutationVariables
|
||||
> = gql`
|
||||
mutation UpdateSettingMutation($id: Int!, $input: UpdateSettingInput!) {
|
||||
updateSetting(id: $id, input: $input) {
|
||||
id
|
||||
enabled
|
||||
group
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Failure = ({ error }: CellFailureProps) => (
|
||||
<div className="rw-cell-error">{error?.message}</div>
|
||||
)
|
||||
|
||||
export const Success = ({ setting }: CellSuccessProps<EditSettingById>) => {
|
||||
const [updateSetting, { loading, error }] = useMutation(
|
||||
UPDATE_SETTING_MUTATION,
|
||||
{
|
||||
onCompleted: () => {
|
||||
toast.success('Setting updated')
|
||||
navigate(routes.settings())
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const onSave = (
|
||||
input: UpdateSettingInput,
|
||||
id: EditSettingById['setting']['id']
|
||||
) => {
|
||||
updateSetting({ variables: { id, input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">
|
||||
Edit Setting {setting?.id}
|
||||
</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<SettingForm
|
||||
setting={setting}
|
||||
onSave={onSave}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
55
web/src/components/Setting/NewSetting/NewSetting.tsx
Normal file
55
web/src/components/Setting/NewSetting/NewSetting.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import type {
|
||||
CreateSettingMutation,
|
||||
CreateSettingInput,
|
||||
CreateSettingMutationVariables,
|
||||
} 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 SettingForm from 'src/components/Setting/SettingForm'
|
||||
|
||||
const CREATE_SETTING_MUTATION: TypedDocumentNode<
|
||||
CreateSettingMutation,
|
||||
CreateSettingMutationVariables
|
||||
> = gql`
|
||||
mutation CreateSettingMutation($input: CreateSettingInput!) {
|
||||
createSetting(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const NewSetting = () => {
|
||||
const [createSetting, { loading, error }] = useMutation(
|
||||
CREATE_SETTING_MUTATION,
|
||||
{
|
||||
onCompleted: () => {
|
||||
toast.success('Setting created')
|
||||
navigate(routes.settings())
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const onSave = (input: CreateSettingInput) => {
|
||||
createSetting({ variables: { input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">New Setting</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<SettingForm onSave={onSave} loading={loading} error={error} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewSetting
|
||||
98
web/src/components/Setting/Setting/Setting.tsx
Normal file
98
web/src/components/Setting/Setting/Setting.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import type {
|
||||
DeleteSettingMutation,
|
||||
DeleteSettingMutationVariables,
|
||||
FindSettingById,
|
||||
} 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_SETTING_MUTATION: TypedDocumentNode<
|
||||
DeleteSettingMutation,
|
||||
DeleteSettingMutationVariables
|
||||
> = gql`
|
||||
mutation DeleteSettingMutation($id: Int!) {
|
||||
deleteSetting(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
interface Props {
|
||||
setting: NonNullable<FindSettingById['setting']>
|
||||
}
|
||||
|
||||
const Setting = ({ setting }: Props) => {
|
||||
const [deleteSetting] = useMutation(DELETE_SETTING_MUTATION, {
|
||||
onCompleted: () => {
|
||||
toast.success('Setting deleted')
|
||||
navigate(routes.settings())
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
})
|
||||
|
||||
const onDeleteClick = (id: DeleteSettingMutationVariables['id']) => {
|
||||
if (confirm('Are you sure you want to delete setting ' + id + '?')) {
|
||||
deleteSetting({ variables: { id } })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">
|
||||
Setting {setting.id} Detail
|
||||
</h2>
|
||||
</header>
|
||||
<table className="rw-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<td>{setting.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Enabled</th>
|
||||
<td>{checkboxInputTag(setting.enabled)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Group</th>
|
||||
<td>{setting.group}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td>{setting.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Value</th>
|
||||
<td>{setting.value}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<nav className="rw-button-group">
|
||||
<Link
|
||||
to={routes.editSetting({ id: setting.id })}
|
||||
className="rw-button rw-button-blue"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="rw-button rw-button-red"
|
||||
onClick={() => onDeleteClick(setting.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Setting
|
||||
40
web/src/components/Setting/SettingCell/SettingCell.tsx
Normal file
40
web/src/components/Setting/SettingCell/SettingCell.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import type { FindSettingById, FindSettingByIdVariables } from 'types/graphql'
|
||||
|
||||
import type {
|
||||
CellSuccessProps,
|
||||
CellFailureProps,
|
||||
TypedDocumentNode,
|
||||
} from '@redwoodjs/web'
|
||||
|
||||
import Setting from 'src/components/Setting/Setting'
|
||||
|
||||
export const QUERY: TypedDocumentNode<
|
||||
FindSettingById,
|
||||
FindSettingByIdVariables
|
||||
> = gql`
|
||||
query FindSettingById($id: Int!) {
|
||||
setting: setting(id: $id) {
|
||||
id
|
||||
enabled
|
||||
group
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => <div>Setting not found</div>
|
||||
|
||||
export const Failure = ({
|
||||
error,
|
||||
}: CellFailureProps<FindSettingByIdVariables>) => (
|
||||
<div className="rw-cell-error">{error?.message}</div>
|
||||
)
|
||||
|
||||
export const Success = ({
|
||||
setting,
|
||||
}: CellSuccessProps<FindSettingById, FindSettingByIdVariables>) => {
|
||||
return <Setting setting={setting} />
|
||||
}
|
||||
119
web/src/components/Setting/SettingForm/SettingForm.tsx
Normal file
119
web/src/components/Setting/SettingForm/SettingForm.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import type { EditSettingById, UpdateSettingInput } from 'types/graphql'
|
||||
|
||||
import type { RWGqlError } from '@redwoodjs/forms'
|
||||
import {
|
||||
Form,
|
||||
FormError,
|
||||
FieldError,
|
||||
Label,
|
||||
CheckboxField,
|
||||
TextField,
|
||||
Submit,
|
||||
} from '@redwoodjs/forms'
|
||||
|
||||
type FormSetting = NonNullable<EditSettingById['setting']>
|
||||
|
||||
interface SettingFormProps {
|
||||
setting?: EditSettingById['setting']
|
||||
onSave: (data: UpdateSettingInput, id?: FormSetting['id']) => void
|
||||
error: RWGqlError
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const SettingForm = (props: SettingFormProps) => {
|
||||
const onSubmit = (data: FormSetting) => {
|
||||
props.onSave(data, props?.setting?.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-form-wrapper">
|
||||
<Form<FormSetting> 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="enabled"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Enabled
|
||||
</Label>
|
||||
|
||||
<CheckboxField
|
||||
name="enabled"
|
||||
defaultChecked={props.setting?.enabled}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
/>
|
||||
|
||||
<FieldError name="enabled" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="group"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Group
|
||||
</Label>
|
||||
|
||||
<TextField
|
||||
name="group"
|
||||
defaultValue={props.setting?.group}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
|
||||
<FieldError name="group" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="name"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Name
|
||||
</Label>
|
||||
|
||||
<TextField
|
||||
name="name"
|
||||
defaultValue={props.setting?.name}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
|
||||
<FieldError name="name" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="value"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Value
|
||||
</Label>
|
||||
|
||||
<TextField
|
||||
name="value"
|
||||
defaultValue={props.setting?.value}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
|
||||
<FieldError name="value" 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 SettingForm
|
||||
34
web/src/components/Setting/SettingValue/SettingValue.tsx
Normal file
34
web/src/components/Setting/SettingValue/SettingValue.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
|
||||
const QUERY_VALUE = gql`
|
||||
query value($name: String, $group: String) {
|
||||
value(name: $name, group: $group) {
|
||||
group
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
`
|
||||
interface SettingValueProps {
|
||||
name: string
|
||||
group?: string
|
||||
}
|
||||
|
||||
const SettingValue = ({ name, group = 'default' }: SettingValueProps) => {
|
||||
const { loading, error, data } = useQuery(QUERY_VALUE, {
|
||||
variables: { name, group },
|
||||
})
|
||||
|
||||
if (loading) return null
|
||||
|
||||
if (error) return 'Error! ' + error
|
||||
|
||||
let value = name + ' not found in group ' + group
|
||||
if (data.value.length >= 1) {
|
||||
value = data.value[0].value
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export default SettingValue
|
||||
102
web/src/components/Setting/Settings/Settings.tsx
Normal file
102
web/src/components/Setting/Settings/Settings.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import type {
|
||||
DeleteSettingMutation,
|
||||
DeleteSettingMutationVariables,
|
||||
FindSettings,
|
||||
} 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/Setting/SettingsCell'
|
||||
import { checkboxInputTag, truncate } from 'src/lib/formatters'
|
||||
|
||||
const DELETE_SETTING_MUTATION: TypedDocumentNode<
|
||||
DeleteSettingMutation,
|
||||
DeleteSettingMutationVariables
|
||||
> = gql`
|
||||
mutation DeleteSettingMutation($id: Int!) {
|
||||
deleteSetting(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const SettingsList = ({ settings }: FindSettings) => {
|
||||
const [deleteSetting] = useMutation(DELETE_SETTING_MUTATION, {
|
||||
onCompleted: () => {
|
||||
toast.success('Setting 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: DeleteSettingMutationVariables['id']) => {
|
||||
if (confirm('Are you sure you want to delete setting ' + id + '?')) {
|
||||
deleteSetting({ variables: { id } })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment rw-table-wrapper-responsive">
|
||||
<table className="rw-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Enabled</th>
|
||||
<th>Group</th>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{settings.map((setting) => (
|
||||
<tr key={setting.id}>
|
||||
<td>{truncate(setting.id)}</td>
|
||||
<td>{checkboxInputTag(setting.enabled)}</td>
|
||||
<td>{truncate(setting.group)}</td>
|
||||
<td>{truncate(setting.name)}</td>
|
||||
<td>{truncate(setting.value)}</td>
|
||||
<td>
|
||||
<nav className="rw-table-actions">
|
||||
<Link
|
||||
to={routes.setting({ id: setting.id })}
|
||||
title={'Show setting ' + setting.id + ' detail'}
|
||||
className="rw-button rw-button-small"
|
||||
>
|
||||
Show
|
||||
</Link>
|
||||
<Link
|
||||
to={routes.editSetting({ id: setting.id })}
|
||||
title={'Edit setting ' + setting.id}
|
||||
className="rw-button rw-button-small rw-button-blue"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
title={'Delete setting ' + setting.id}
|
||||
className="rw-button rw-button-small rw-button-red"
|
||||
onClick={() => onDeleteClick(setting.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</nav>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsList
|
||||
46
web/src/components/Setting/SettingsCell/SettingsCell.tsx
Normal file
46
web/src/components/Setting/SettingsCell/SettingsCell.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import type { FindSettings, FindSettingsVariables } from 'types/graphql'
|
||||
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
import type {
|
||||
CellSuccessProps,
|
||||
CellFailureProps,
|
||||
TypedDocumentNode,
|
||||
} from '@redwoodjs/web'
|
||||
|
||||
import Settings from 'src/components/Setting/Settings'
|
||||
|
||||
export const QUERY: TypedDocumentNode<FindSettings, FindSettingsVariables> =
|
||||
gql`
|
||||
query FindSettings {
|
||||
settings {
|
||||
id
|
||||
enabled
|
||||
group
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => {
|
||||
return (
|
||||
<div className="rw-text-center">
|
||||
No settings yet.{' '}
|
||||
<Link to={routes.newSetting()} className="rw-link">
|
||||
Create one?
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Failure = ({ error }: CellFailureProps<FindSettings>) => (
|
||||
<div className="rw-cell-error">{error?.message}</div>
|
||||
)
|
||||
|
||||
export const Success = ({
|
||||
settings,
|
||||
}: CellSuccessProps<FindSettings, FindSettingsVariables>) => {
|
||||
return <Settings settings={settings} />
|
||||
}
|
||||
11
web/src/pages/Setting/EditSettingPage/EditSettingPage.tsx
Normal file
11
web/src/pages/Setting/EditSettingPage/EditSettingPage.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import EditSettingCell from 'src/components/Setting/EditSettingCell'
|
||||
|
||||
type SettingPageProps = {
|
||||
id: number
|
||||
}
|
||||
|
||||
const EditSettingPage = ({ id }: SettingPageProps) => {
|
||||
return <EditSettingCell id={id} />
|
||||
}
|
||||
|
||||
export default EditSettingPage
|
||||
7
web/src/pages/Setting/NewSettingPage/NewSettingPage.tsx
Normal file
7
web/src/pages/Setting/NewSettingPage/NewSettingPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import NewSetting from 'src/components/Setting/NewSetting'
|
||||
|
||||
const NewSettingPage = () => {
|
||||
return <NewSetting />
|
||||
}
|
||||
|
||||
export default NewSettingPage
|
||||
11
web/src/pages/Setting/SettingPage/SettingPage.tsx
Normal file
11
web/src/pages/Setting/SettingPage/SettingPage.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import SettingCell from 'src/components/Setting/SettingCell'
|
||||
|
||||
type SettingPageProps = {
|
||||
id: number
|
||||
}
|
||||
|
||||
const SettingPage = ({ id }: SettingPageProps) => {
|
||||
return <SettingCell id={id} />
|
||||
}
|
||||
|
||||
export default SettingPage
|
||||
7
web/src/pages/Setting/SettingsPage/SettingsPage.tsx
Normal file
7
web/src/pages/Setting/SettingsPage/SettingsPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import SettingsCell from 'src/components/Setting/SettingsCell'
|
||||
|
||||
const SettingsPage = () => {
|
||||
return <SettingsCell />
|
||||
}
|
||||
|
||||
export default SettingsPage
|
||||
Loading…
Reference in New Issue
Block a user