diff --git a/apps/blocks/src/lib/app-kit/resource-kit.ts b/apps/blocks/src/lib/app-kit/resource-kit.ts index b4dcf13..31e0cc5 100644 --- a/apps/blocks/src/lib/app-kit/resource-kit.ts +++ b/apps/blocks/src/lib/app-kit/resource-kit.ts @@ -138,9 +138,9 @@ export function ContactPage({ id }: { id: string }) { }, { name: 'ResourceField.createDefault', - type: "'now'", + type: "'now' | { value }", behavior: - 'Prefills a date or datetime control from the reader’s local clock when a create form opens. An explicit form default takes precedence, and edit forms read the stored value.', + 'Prefills a create form: \'now\' sets a date or datetime control from the reader’s local clock, and { value } sets any control to that value, such as the column’s database default. An explicit form default takes precedence, and edit forms read the stored value.', }, { name: 'ResourceSource', diff --git a/packages/ui/src/components/resource-kit/form-values.ts b/packages/ui/src/components/resource-kit/form-values.ts index 8f36d33..566b48e 100644 --- a/packages/ui/src/components/resource-kit/form-values.ts +++ b/packages/ui/src/components/resource-kit/form-values.ts @@ -64,6 +64,8 @@ export function initialFormValues( if (!record && source === undefined && field.createDefault === 'now' && (field.input === 'date' || field.input === 'datetime')) { localNow ??= toLocalDateTime(new Date()); values[field.name] = field.input === 'date' ? localNow.slice(0, 10) : localNow; + } else if (!record && source === undefined && typeof field.createDefault === 'object') { + values[field.name] = toFormValue(field, field.createDefault.value); } else { values[field.name] = toFormValue(field, source); } diff --git a/packages/ui/src/components/resource-kit/types.ts b/packages/ui/src/components/resource-kit/types.ts index b4440e0..7a30522 100644 --- a/packages/ui/src/components/resource-kit/types.ts +++ b/packages/ui/src/components/resource-kit/types.ts @@ -33,8 +33,12 @@ export type ResourceField = { /** List and detail display; defaults from `input`. */ format?: FieldFormat; required?: boolean; - /** Prefill a new date or datetime field from the local clock. Explicit form defaults take precedence. */ - createDefault?: 'now'; + /** + * What a create form starts on: `'now'` prefills a date or datetime field from + * the local clock; `{ value }` prefills any field with that value (a column's + * database default, say). Explicit form defaults take precedence. + */ + createDefault?: 'now' | { value: string | number | boolean }; /** Shown, never written (e.g. createdAt). */ readOnly?: boolean; /** Never shown (e.g. ownerId). */ diff --git a/packages/ui/test/resource-kit.test.tsx b/packages/ui/test/resource-kit.test.tsx index 57cfa66..7371507 100644 --- a/packages/ui/test/resource-kit.test.tsx +++ b/packages/ui/test/resource-kit.test.tsx @@ -312,6 +312,41 @@ describe('Resource Kit', () => { } }); + it('starts a create form on each value default, and leaves edit forms on the stored values', async () => { + const { resources, tasks } = workspace(); + const defaulted = resources.map((resource) => resource.name === 'tasks' ? { + ...resource, + formFields: [...resource.formFields, 'priority'], + fields: { + ...resource.fields, + estimate: { ...resource.fields.estimate, createDefault: { value: 3 } }, + priority: { + name: 'priority', + label: 'Priority', + input: 'select' as const, + options: [{ value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }], + createDefault: { value: 'medium' }, + }, + }, + } : resource); + await render( {}} defaultValues={{ projectId: 'p1' }} />, defaulted); + await settle(); + + expect(field('Estimate').value).toBe('3'); + expect(text(field('Priority'))).toContain('Medium'); + await type(field('Title'), 'Kickoff'); + await submit(); + expect(tasks.create).toHaveBeenCalledWith(expect.objectContaining({ estimate: 3, priority: 'medium' })); + + await act(async () => root!.unmount()); + container!.remove(); + const existing = await tasks.get('t1'); + await render( {}} />, defaulted); + await settle(); + expect(field('Estimate').value).toBe(String(existing?.estimate ?? '')); + expect(text(field('Priority'))).not.toContain('Medium'); + }); + it('creates a related record from a detail view with the parent filled in, mapped values, and a refreshed list', async () => { const { resources, tasks } = workspace(); await render(, resources);