From 5085fa4beed8fab0b79156043220856f5b58a6e2 Mon Sep 17 00:00:00 2001 From: _Zaizen_ Date: Thu, 10 Jul 2025 09:48:28 +0200 Subject: [PATCH 1/4] Make createOne in crud not partial --- packages/toolpad-core/src/Crud/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolpad-core/src/Crud/types.ts b/packages/toolpad-core/src/Crud/types.ts index 248fc5fc94c..3a5aa917ab7 100644 --- a/packages/toolpad-core/src/Crud/types.ts +++ b/packages/toolpad-core/src/Crud/types.ts @@ -46,7 +46,7 @@ export interface DataSource { filterModel: GridFilterModel; }) => { items: D[]; itemCount: number } | Promise<{ items: D[]; itemCount: number }>; getOne?: (id: DataModelId) => D | Promise; - createOne?: (data: Partial>) => D | Promise; + createOne?: (data: OmitId) => D | Promise; updateOne?: (id: DataModelId, data: Partial>) => D | Promise; deleteOne?: (id: DataModelId) => void | Promise; /** From fcf14353919fec6f8730e7ee1e99ee4f9a1b6105 Mon Sep 17 00:00:00 2001 From: _Zaizen_ Date: Fri, 11 Jul 2025 10:17:28 +0200 Subject: [PATCH 2/4] Adapt code to support the non partial createOne --- .../auth-nextjs-themed/app/api/employees/route.ts | 2 +- .../src/pages/api/employees/index.ts | 2 +- .../core/crud-nextjs/src/app/api/employees/route.ts | 2 +- .../templates/nextjs/nextjs-app/employeesRoute.ts | 2 +- .../templates/nextjs/nextjs-pages/employeesRoute.ts | 2 +- packages/toolpad-core/src/Crud/Create.tsx | 12 ++++++------ packages/toolpad-core/src/Crud/CrudForm.tsx | 6 +++--- packages/toolpad-core/src/Crud/types.ts | 5 +++-- 8 files changed, 17 insertions(+), 16 deletions(-) diff --git a/examples/core/auth-nextjs-themed/app/api/employees/route.ts b/examples/core/auth-nextjs-themed/app/api/employees/route.ts index e65dd982a1f..09f8e6fbe43 100644 --- a/examples/core/auth-nextjs-themed/app/api/employees/route.ts +++ b/examples/core/auth-nextjs-themed/app/api/employees/route.ts @@ -77,7 +77,7 @@ export async function GET(req: NextRequest) { } export async function POST(req: NextRequest) { - const body: Partial> = await req.json(); + const body: OmitId = await req.json(); const employeesStore = getEmployeesStore(); diff --git a/examples/core/crud-nextjs-pages/src/pages/api/employees/index.ts b/examples/core/crud-nextjs-pages/src/pages/api/employees/index.ts index 1c013d43a14..bb3d556dc93 100644 --- a/examples/core/crud-nextjs-pages/src/pages/api/employees/index.ts +++ b/examples/core/crud-nextjs-pages/src/pages/api/employees/index.ts @@ -73,7 +73,7 @@ export async function getEmployees(req: NextApiRequest, res: NextApiResponse) { } export async function createEmployee(req: NextApiRequest, res: NextApiResponse) { - const body: Partial> = req.body; + const body: OmitId = req.body; const employeesStore = getEmployeesStore(); diff --git a/examples/core/crud-nextjs/src/app/api/employees/route.ts b/examples/core/crud-nextjs/src/app/api/employees/route.ts index 6ceb0c79d6a..b2103458109 100644 --- a/examples/core/crud-nextjs/src/app/api/employees/route.ts +++ b/examples/core/crud-nextjs/src/app/api/employees/route.ts @@ -77,7 +77,7 @@ export async function GET(req: NextRequest) { } export async function POST(req: NextRequest) { - const body: Partial> = await req.json(); + const body: OmitId = await req.json(); const employeesStore = getEmployeesStore(); diff --git a/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/employeesRoute.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/employeesRoute.ts index 09306f5b416..8f05b3ed912 100644 --- a/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/employeesRoute.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/employeesRoute.ts @@ -77,7 +77,7 @@ export async function GET(req: NextRequest) { } export async function POST(req: NextRequest) { - const body: Partial> = await req.json(); + const body: OmitId = await req.json(); const employeesStore = getEmployeesStore(); diff --git a/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/employeesRoute.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/employeesRoute.ts index 4768b4386de..04b48fb2c2b 100644 --- a/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/employeesRoute.ts +++ b/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/employeesRoute.ts @@ -73,7 +73,7 @@ export async function getEmployees(req: NextApiRequest, res: NextApiResponse) { } export async function createEmployee(req: NextApiRequest, res: NextApiResponse) { - const body: Partial> = req.body; + const body: OmitId = req.body; const employeesStore = getEmployeesStore(); diff --git a/packages/toolpad-core/src/Crud/Create.tsx b/packages/toolpad-core/src/Crud/Create.tsx index 8516b3d9961..7bbbfa05088 100644 --- a/packages/toolpad-core/src/Crud/Create.tsx +++ b/packages/toolpad-core/src/Crud/Create.tsx @@ -26,7 +26,7 @@ export interface CreateProps { /** * Callback fired when the form is successfully submitted. */ - onSubmitSuccess?: (formValues: Partial>) => void | Promise; + onSubmitSuccess?: (formValues: OmitId) => void | Promise; /** * Whether the form fields should reset after the form is submitted. * @default false @@ -109,7 +109,7 @@ function Create(props: CreateProps) { const activePage = useActivePage(); const [formState, setFormState] = React.useState<{ - values: Partial>; + values: OmitId; errors: Partial>; }>(() => ({ values: { @@ -118,17 +118,17 @@ function Create(props: CreateProps) { .filter(({ field, editable }) => field !== 'id' && editable !== false) .map(({ field, type }) => [ field, - type === 'boolean' ? (initialValues[field] ?? false) : initialValues[field], + type === 'boolean' ? (initialValues?.[field] ?? false) : initialValues?.[field], ]), ), ...initialValues, - }, + } as OmitId, errors: {}, })); const formValues = formState.values; const formErrors = formState.errors; - const setFormValues = React.useCallback((newFormValues: Partial>) => { + const setFormValues = React.useCallback((newFormValues: OmitId) => { setFormState((previousState) => ({ ...previousState, values: newFormValues, @@ -144,7 +144,7 @@ function Create(props: CreateProps) { const handleFormFieldChange = React.useCallback( (name: keyof D, value: DataFieldFormValue) => { - const validateField = async (values: Partial>) => { + const validateField = async (values: OmitId) => { if (validate) { const { issues } = await validate(values); setFormErrors({ diff --git a/packages/toolpad-core/src/Crud/CrudForm.tsx b/packages/toolpad-core/src/Crud/CrudForm.tsx index d82bcb172d0..fa45dda1d7e 100644 --- a/packages/toolpad-core/src/Crud/CrudForm.tsx +++ b/packages/toolpad-core/src/Crud/CrudForm.tsx @@ -24,7 +24,7 @@ import { CrudContext } from '../shared/context'; import type { DataField, DataFieldFormValue, DataModel, DataSource, OmitId } from './types'; interface CrudFormState { - values: Partial>; + values: OmitId | Partial>; errors: Partial>; } @@ -80,11 +80,11 @@ export interface CrudFormProps { /** * Callback fired when the form is submitted. */ - onSubmit: (formValues: Partial>) => void | Promise; + onSubmit: (formValues: OmitId | Partial>) => void | Promise; /** * Callback fired when the form is reset. */ - onReset?: (formValues: Partial>) => void | Promise; + onReset?: (formValues: OmitId | Partial>) => void | Promise; /** * The components used for each slot inside. * @default {} diff --git a/packages/toolpad-core/src/Crud/types.ts b/packages/toolpad-core/src/Crud/types.ts index 3a5aa917ab7..86d0f5b0457 100644 --- a/packages/toolpad-core/src/Crud/types.ts +++ b/packages/toolpad-core/src/Crud/types.ts @@ -51,8 +51,9 @@ export interface DataSource { deleteOne?: (id: DataModelId) => void | Promise; /** * Function to validate form values. Follows the Standard Schema `validate` function format (https://standardschema.dev/). + * Can validate either complete records (for create) or partial records (for update). */ validate?: ( - value: Partial>, - ) => ReturnType>>['~standard']['validate']>; + value: OmitId | Partial>, + ) => ReturnType | Partial>>['~standard']['validate']>; } From 713a2cb7517c2f0bbb3e365bc2072e656ad62b0b Mon Sep 17 00:00:00 2001 From: _Zaizen_ Date: Fri, 11 Jul 2025 11:39:28 +0200 Subject: [PATCH 3/4] Update --- packages/toolpad-core/src/Crud/Create.tsx | 30 ++++++++++++++++----- packages/toolpad-core/src/Crud/CrudForm.tsx | 6 ++--- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/toolpad-core/src/Crud/Create.tsx b/packages/toolpad-core/src/Crud/Create.tsx index 7bbbfa05088..fabebea142c 100644 --- a/packages/toolpad-core/src/Crud/Create.tsx +++ b/packages/toolpad-core/src/Crud/Create.tsx @@ -109,7 +109,7 @@ function Create(props: CreateProps) { const activePage = useActivePage(); const [formState, setFormState] = React.useState<{ - values: OmitId; + values: Partial>; errors: Partial>; }>(() => ({ values: { @@ -122,13 +122,13 @@ function Create(props: CreateProps) { ]), ), ...initialValues, - } as OmitId, + }, errors: {}, })); const formValues = formState.values; const formErrors = formState.errors; - const setFormValues = React.useCallback((newFormValues: OmitId) => { + const setFormValues = React.useCallback((newFormValues: Partial>) => { setFormState((previousState) => ({ ...previousState, values: newFormValues, @@ -144,7 +144,7 @@ function Create(props: CreateProps) { const handleFormFieldChange = React.useCallback( (name: keyof D, value: DataFieldFormValue) => { - const validateField = async (values: OmitId) => { + const validateField = async (values: Partial>) => { if (validate) { const { issues } = await validate(values); setFormErrors({ @@ -167,8 +167,23 @@ function Create(props: CreateProps) { }, [initialValues, setFormValues]); const handleFormSubmit = React.useCallback(async () => { + // Check if all required fields are present + const requiredFields = fields.filter(({ field, editable }) => field !== 'id' && editable !== false); + const missingFields = requiredFields.filter(({ field }) => formValues[field] === undefined || formValues[field] === null || formValues[field] === ''); + + if (missingFields.length > 0) { + const missingFieldErrors = Object.fromEntries( + missingFields.map(({ field, headerName }) => [field as keyof D, `${headerName || field} is required`]) + ) as Partial>; + setFormErrors(missingFieldErrors); + throw new Error('Required fields are missing'); + } + + // At this point, we know all required fields are present, so we can safely cast to OmitId + const completeFormValues = formValues as unknown as OmitId; + if (validate) { - const { issues } = await validate(formValues); + const { issues } = await validate(completeFormValues); if (issues && issues.length > 0) { setFormErrors(Object.fromEntries(issues.map((issue) => [issue.path?.[0], issue.message]))); throw new Error('Form validation failed'); @@ -177,14 +192,14 @@ function Create(props: CreateProps) { setFormErrors({}); try { - await createOne(formValues); + await createOne(completeFormValues); notifications.show(localeText.createSuccessMessage, { severity: 'success', autoHideDuration: 3000, }); if (onSubmitSuccess) { - await onSubmitSuccess(formValues); + await onSubmitSuccess(completeFormValues); } if (resetOnSubmit) { @@ -199,6 +214,7 @@ function Create(props: CreateProps) { } }, [ createOne, + fields, formValues, handleFormReset, localeText.createErrorMessage, diff --git a/packages/toolpad-core/src/Crud/CrudForm.tsx b/packages/toolpad-core/src/Crud/CrudForm.tsx index fa45dda1d7e..d82bcb172d0 100644 --- a/packages/toolpad-core/src/Crud/CrudForm.tsx +++ b/packages/toolpad-core/src/Crud/CrudForm.tsx @@ -24,7 +24,7 @@ import { CrudContext } from '../shared/context'; import type { DataField, DataFieldFormValue, DataModel, DataSource, OmitId } from './types'; interface CrudFormState { - values: OmitId | Partial>; + values: Partial>; errors: Partial>; } @@ -80,11 +80,11 @@ export interface CrudFormProps { /** * Callback fired when the form is submitted. */ - onSubmit: (formValues: OmitId | Partial>) => void | Promise; + onSubmit: (formValues: Partial>) => void | Promise; /** * Callback fired when the form is reset. */ - onReset?: (formValues: OmitId | Partial>) => void | Promise; + onReset?: (formValues: Partial>) => void | Promise; /** * The components used for each slot inside. * @default {} From 84de75bdf048b177e45c2dc715ae92b5027c7b71 Mon Sep 17 00:00:00 2001 From: _Zaizen_ Date: Fri, 11 Jul 2025 11:52:09 +0200 Subject: [PATCH 4/4] Fix format (?) --- packages/toolpad-core/src/Crud/Create.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/toolpad-core/src/Crud/Create.tsx b/packages/toolpad-core/src/Crud/Create.tsx index fabebea142c..b6ccd93c7d5 100644 --- a/packages/toolpad-core/src/Crud/Create.tsx +++ b/packages/toolpad-core/src/Crud/Create.tsx @@ -168,12 +168,20 @@ function Create(props: CreateProps) { const handleFormSubmit = React.useCallback(async () => { // Check if all required fields are present - const requiredFields = fields.filter(({ field, editable }) => field !== 'id' && editable !== false); - const missingFields = requiredFields.filter(({ field }) => formValues[field] === undefined || formValues[field] === null || formValues[field] === ''); - + const requiredFields = fields.filter( + ({ field, editable }) => field !== 'id' && editable !== false, + ); + const missingFields = requiredFields.filter( + ({ field }) => + formValues[field] === undefined || formValues[field] === null || formValues[field] === '', + ); + if (missingFields.length > 0) { const missingFieldErrors = Object.fromEntries( - missingFields.map(({ field, headerName }) => [field as keyof D, `${headerName || field} is required`]) + missingFields.map(({ field, headerName }) => [ + field as keyof D, + `${headerName || field} is required`, + ]), ) as Partial>; setFormErrors(missingFieldErrors); throw new Error('Required fields are missing');