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..b6ccd93c7d5 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 @@ -118,7 +118,7 @@ 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, @@ -167,8 +167,31 @@ 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 +200,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 +222,7 @@ function Create(props: CreateProps) { } }, [ createOne, + fields, formValues, handleFormReset, localeText.createErrorMessage, diff --git a/packages/toolpad-core/src/Crud/types.ts b/packages/toolpad-core/src/Crud/types.ts index 248fc5fc94c..86d0f5b0457 100644 --- a/packages/toolpad-core/src/Crud/types.ts +++ b/packages/toolpad-core/src/Crud/types.ts @@ -46,13 +46,14 @@ 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; /** * 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']>; }