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])
|
User User? @relation(fields: [userId], references: [id])
|
||||||
userId String?
|
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
|
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
|
const { roles } = directiveArgs
|
||||||
applicationRequireAuth({ roles })
|
applicationRequireAuth({ context, roles })
|
||||||
}
|
}
|
||||||
|
|
||||||
const requireAuth = createValidatorDirective(schema, validate)
|
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
|
* @returns {boolean} - If the currentUser is authenticated
|
||||||
*/
|
*/
|
||||||
export const isAuthenticated = (): boolean => {
|
export const isAuthenticated = (context?): boolean => {
|
||||||
return !!context.currentUser
|
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
|
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
|
||||||
*/
|
*/
|
||||||
export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
|
export const requireAuth = ({
|
||||||
if (!isAuthenticated()) {
|
context,
|
||||||
|
roles,
|
||||||
|
}: {
|
||||||
|
context
|
||||||
|
roles?: AllowedRoles
|
||||||
|
}) => {
|
||||||
|
if (!isAuthenticated(context)) {
|
||||||
throw new AuthenticationError("You don't have permission to do that.")
|
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/auth-dbauth-setup": "8.4.0",
|
||||||
"@redwoodjs/core": "8.4.0",
|
"@redwoodjs/core": "8.4.0",
|
||||||
"@redwoodjs/project-config": "8.4.0",
|
"@redwoodjs/project-config": "8.4.0",
|
||||||
|
"@redwoodjs/realtime": "8.4.0",
|
||||||
|
"@redwoodjs/studio": "12",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.8"
|
"prettier-plugin-tailwindcss": "^0.6.8"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
// 'src/pages/HomePage/HomePage.js' -> HomePage
|
// 'src/pages/HomePage/HomePage.js' -> HomePage
|
||||||
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
|
// '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 ClientLayout from 'src/layouts/ClientLayout/ClientLayout'
|
||||||
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
|
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
|
||||||
@ -17,10 +17,13 @@ import { useAuth } from './auth'
|
|||||||
const Routes = () => {
|
const Routes = () => {
|
||||||
return (
|
return (
|
||||||
<Router useAuth={useAuth}>
|
<Router useAuth={useAuth}>
|
||||||
<Route path="/login" page={LoginPage} name="login" />
|
<PrivateSet unauthenticated="home">
|
||||||
<Route path="/signup" page={SignupPage} name="signup" />
|
<Set wrap={ScaffoldLayout} title="Settings" titleTo="settings" buttonLabel="New Setting" buttonTo="newSetting">
|
||||||
<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
|
<Route path="/settings/new" page={SettingNewSettingPage} name="newSetting" />
|
||||||
<Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />
|
<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">
|
<Set wrap={ScaffoldLayout} title="Roles" titleTo="roles" buttonLabel="New Role" buttonTo="newRole">
|
||||||
<Route path="/roles/new" page={RoleNewRolePage} name="newRole" />
|
<Route path="/roles/new" page={RoleNewRolePage} name="newRole" />
|
||||||
<Route path="/roles/{id}/edit" page={RoleEditRolePage} name="editRole" />
|
<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/{id}" page={AccountAccountPage} name="account" />
|
||||||
<Route path="/accounts" page={AccountAccountsPage} name="accounts" />
|
<Route path="/accounts" page={AccountAccountsPage} name="accounts" />
|
||||||
</Set>
|
</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}>
|
<Set wrap={ClientLayout}>
|
||||||
<Route path="/home" page={HomePage} name="home" />
|
<Route path="/home" page={HomePage} name="home" />
|
||||||
<Route path="/hero" page={HeroPage} name="hero" />
|
<Route path="/hero" page={HeroPage} name="hero" />
|
||||||
|
|||||||
@ -3,13 +3,17 @@ import { Link, routes } from '@redwoodjs/router'
|
|||||||
import { useAuth } from 'src/auth'
|
import { useAuth } from 'src/auth'
|
||||||
import ThemeChanger from 'src/components/ThemeChanger/ThemeChanger'
|
import ThemeChanger from 'src/components/ThemeChanger/ThemeChanger'
|
||||||
|
|
||||||
|
import SettingValue from '../Setting/SettingValue/SettingValue'
|
||||||
|
|
||||||
const HeaderBar = () => {
|
const HeaderBar = () => {
|
||||||
const { logOut } = useAuth()
|
const { logOut } = useAuth()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="navbar bg-base-100">
|
<div className="navbar bg-base-100">
|
||||||
<div className="flex-1">
|
<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>
|
||||||
<div className="flex-none">
|
<div className="flex-none">
|
||||||
<ul className="menu menu-horizontal px-1">
|
<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