diff --git a/docs/plugins/form-builder.mdx b/docs/plugins/form-builder.mdx index 7c1ceb205..4c09c4429 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 7a6fe29de..bcf5c49af 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 0d5e1746d..8ffc678f7 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 93d52c839..8e2c9dc01 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 7479d1b7d..f51416706 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 08cd5427a..a4cf4a641 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 a50888920..826413e0d 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: {