From 4dfb2d24bb1edd9ea7d6e177a388a25ff1a45fd6 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 21 May 2025 10:34:21 -0700 Subject: [PATCH] feat(plugin-form-builder): add new date field (#12416) Adds a new date field to take submission values for. It can help form serialisers render the right input for this kind of field as the submissions themselves don't do any validation right now. Disabled by default as to not cause any conflicts with existing projects potentially inserting their own date blocks. Can be enabled like this ```ts formBuilderPlugin({ fields: { date: true } }) ``` --- docs/plugins/form-builder.mdx | 49 +++++++++++++++ .../src/collections/Forms/fields.ts | 50 ++++++++++++++++ packages/plugin-form-builder/src/types.ts | 12 ++++ test/plugin-form-builder/config.ts | 20 +++++++ test/plugin-form-builder/e2e.spec.ts | 57 ++++++++++++++++-- test/plugin-form-builder/payload-types.ts | 26 ++++++++ test/plugin-form-builder/seed/index.ts | 59 +++++++++++++++++++ 7 files changed, 269 insertions(+), 4 deletions(-) diff --git a/docs/plugins/form-builder.mdx b/docs/plugins/form-builder.mdx index 7c1ceb2056..4c09c4429a 100644 --- a/docs/plugins/form-builder.mdx +++ b/docs/plugins/form-builder.mdx @@ -85,6 +85,7 @@ formBuilderPlugin({ checkbox: true, number: true, message: true, + date: false, payment: false, }, }) @@ -349,6 +350,18 @@ Maps to a `checkbox` input on your front-end. Used to collect a boolean value. | `width` | string | The width of the field on the front-end. | | `required` | checkbox | Whether or not the field is required when submitted. | +### Date + +Maps to a `date` input on your front-end. Used to collect a date value. + +| Property | Type | Description | +| -------------- | -------- | ---------------------------------------------------- | +| `name` | string | The name of the field. | +| `label` | string | The label of the field. | +| `defaultValue` | date | The default value of the field. | +| `width` | string | The width of the field on the front-end. | +| `required` | checkbox | Whether or not the field is required when submitted. | + ### Number Maps to a `number` input on your front-end. Used to collect a number. @@ -421,6 +434,42 @@ formBuilderPlugin({ }) ``` +### Customizing the date field default value + +You can custommise the default value of the date field and any other aspects of the date block in this way. +Note that the end submission source will be responsible for the timezone of the date. Payload only stores the date in UTC format. + +```ts +import { fields as formFields } from '@payloadcms/plugin-form-builder' + +// payload.config.ts +formBuilderPlugin({ + fields: { + // date: true, // just enable it without any customizations + date: { + ...formFields.date, + fields: [ + ...(formFields.date && 'fields' in formFields.date + ? formFields.date.fields.map((field) => { + if ('name' in field && field.name === 'defaultValue') { + return { + ...field, + timezone: true, // optionally enable timezone + admin: { + ...field.admin, + description: 'This is a date field', + }, + } + } + return field + }) + : []), + ], + }, + }, +}) +``` + ## Email This plugin relies on the [email configuration](../email/overview) defined in your Payload configuration. It will read from your config and attempt to send your emails using the credentials provided. diff --git a/packages/plugin-form-builder/src/collections/Forms/fields.ts b/packages/plugin-form-builder/src/collections/Forms/fields.ts index 7a6fe29dee..bcf5c49afc 100644 --- a/packages/plugin-form-builder/src/collections/Forms/fields.ts +++ b/packages/plugin-form-builder/src/collections/Forms/fields.ts @@ -487,6 +487,55 @@ const Checkbox: Block = { }, } +const Date: Block = { + slug: 'date', + fields: [ + { + type: 'row', + fields: [ + { + ...name, + admin: { + width: '50%', + }, + }, + { + ...label, + admin: { + width: '50%', + }, + }, + ], + }, + { + type: 'row', + fields: [ + { + ...width, + admin: { + width: '50%', + }, + }, + { + ...required, + admin: { + width: '50%', + }, + }, + ], + }, + { + name: 'defaultValue', + type: 'date', + label: 'Default Value', + }, + ], + labels: { + plural: 'Date Fields', + singular: 'Date', + }, +} + const Payment = (fieldConfig: PaymentFieldConfig): Block => { let paymentProcessorField = null if (fieldConfig?.paymentProcessor) { @@ -669,6 +718,7 @@ const Message: Block = { export const fields = { checkbox: Checkbox, country: Country, + date: Date, email: Email, message: Message, number: Number, diff --git a/packages/plugin-form-builder/src/types.ts b/packages/plugin-form-builder/src/types.ts index 0d5e1746d1..8ffc678f76 100644 --- a/packages/plugin-form-builder/src/types.ts +++ b/packages/plugin-form-builder/src/types.ts @@ -33,6 +33,7 @@ export interface FieldsConfig { [key: string]: boolean | FieldConfig | undefined checkbox?: boolean | FieldConfig country?: boolean | FieldConfig + date?: boolean | FieldConfig email?: boolean | FieldConfig message?: boolean | FieldConfig number?: boolean | FieldConfig @@ -146,6 +147,16 @@ export interface EmailField { width?: number } +export interface DateField { + blockName?: string + blockType: 'date' + defaultValue?: string + label?: string + name: string + required?: boolean + width?: number +} + export interface StateField { blockName?: string blockType: 'state' @@ -185,6 +196,7 @@ export interface MessageField { export type FormFieldBlock = | CheckboxField | CountryField + | DateField | EmailField | MessageField | PaymentField diff --git a/test/plugin-form-builder/config.ts b/test/plugin-form-builder/config.ts index 93d52c839c..8e2c9dc012 100644 --- a/test/plugin-form-builder/config.ts +++ b/test/plugin-form-builder/config.ts @@ -74,6 +74,26 @@ export default buildConfigWithDefaults({ singular: 'Custom Text Field', }, }, + date: { + ...formFields.date, + fields: [ + ...(formFields.date && 'fields' in formFields.date + ? formFields.date.fields.map((field) => { + if ('name' in field && field.name === 'defaultValue') { + return { + ...field, + timezone: true, + admin: { + ...field.admin, + description: 'This is a date field', + }, + } as Field + } + return field + }) + : []), + ], + }, // payment: { // paymentProcessor: { // options: [ diff --git a/test/plugin-form-builder/e2e.spec.ts b/test/plugin-form-builder/e2e.spec.ts index 7479d1b7d7..f51416706e 100644 --- a/test/plugin-form-builder/e2e.spec.ts +++ b/test/plugin-form-builder/e2e.spec.ts @@ -46,7 +46,7 @@ test.describe('Form Builder Plugin', () => { timeout: POLL_TOPASS_TIMEOUT, }) - const titleCell = page.locator('.row-1 .cell-title a') + const titleCell = page.locator('.row-2 .cell-title a') await expect(titleCell).toHaveText('Contact Form') const href = await titleCell.getAttribute('href') @@ -92,10 +92,10 @@ test.describe('Form Builder Plugin', () => { timeout: POLL_TOPASS_TIMEOUT, }) - const idCell = page.locator('.row-1 .cell-id a') - const href = await idCell.getAttribute('href') + const firstSubmissionCell = page.locator('.table .cell-id a').last() + const href = await firstSubmissionCell.getAttribute('href') - await idCell.click() + await firstSubmissionCell.click() await expect(() => expect(page.url()).toContain(href)).toPass({ timeout: POLL_TOPASS_TIMEOUT, }) @@ -109,6 +109,11 @@ test.describe('Form Builder Plugin', () => { test('can create form submission', async () => { const { docs } = await payload.find({ collection: 'forms', + where: { + title: { + contains: 'Contact', + }, + }, }) const createdSubmission = await payload.create({ @@ -137,5 +142,49 @@ test.describe('Form Builder Plugin', () => { await expect(page.locator('#field-submissionData__0__value')).toHaveValue('New tester') await expect(page.locator('#field-submissionData__1__value')).toHaveValue('new@example.com') }) + + test('can create form submission - with date field', async () => { + const { docs } = await payload.find({ + collection: 'forms', + where: { + title: { + contains: 'Booking', + }, + }, + }) + + const createdSubmission = await payload.create({ + collection: 'form-submissions', + data: { + form: docs[0].id, + submissionData: [ + { + field: 'name', + value: 'New tester', + }, + { + field: 'email', + value: 'new@example.com', + }, + { + field: 'date', + value: '2025-10-01T00:00:00.000Z', + }, + ], + }, + }) + + await page.goto(submissionsUrl.edit(createdSubmission.id)) + + await expect(() => expect(page.url()).toContain(createdSubmission.id)).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + + await expect(page.locator('#field-submissionData__0__value')).toHaveValue('New tester') + await expect(page.locator('#field-submissionData__1__value')).toHaveValue('new@example.com') + await expect(page.locator('#field-submissionData__2__value')).toHaveValue( + '2025-10-01T00:00:00.000Z', + ) + }) }) }) diff --git a/test/plugin-form-builder/payload-types.ts b/test/plugin-form-builder/payload-types.ts index 08cd5427a8..a4cf4a6412 100644 --- a/test/plugin-form-builder/payload-types.ts +++ b/test/plugin-form-builder/payload-types.ts @@ -270,6 +270,20 @@ export interface Form { blockName?: string | null; blockType: 'color'; } + | { + name: string; + label?: string | null; + width?: number | null; + required?: boolean | null; + /** + * This is a date field + */ + defaultValue?: string | null; + defaultValue_tz?: SupportedTimezones; + id?: string | null; + blockName?: string | null; + blockType: 'date'; + } )[] | null; submitButtonLabel?: string | null; @@ -615,6 +629,18 @@ export interface FormsSelect { id?: T; blockName?: T; }; + date?: + | T + | { + name?: T; + label?: T; + width?: T; + required?: T; + defaultValue?: T; + defaultValue_tz?: T; + id?: T; + blockName?: T; + }; }; submitButtonLabel?: T; confirmationType?: T; diff --git a/test/plugin-form-builder/seed/index.ts b/test/plugin-form-builder/seed/index.ts index a508889208..826413e0dd 100644 --- a/test/plugin-form-builder/seed/index.ts +++ b/test/plugin-form-builder/seed/index.ts @@ -78,6 +78,65 @@ export const seed = async (payload: Payload): Promise => { }, }) + const { id: dateFormID } = await payload.create({ + collection: formsSlug, + data: { + confirmationType: 'message', + confirmationMessage: { + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'Confirmed', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + textFormat: 0, + textStyle: '', + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'root', + version: 1, + }, + }, + fields: [ + { + name: 'name', + blockType: 'text', + label: 'Name', + required: true, + }, + { + name: 'email', + blockType: 'email', + label: 'Email', + required: true, + }, + { + name: 'date', + width: null, + required: null, + blockType: 'date', + }, + ], + title: 'Booking Form', + }, + }) + await payload.create({ collection: formSubmissionsSlug, data: {