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
   }
})
```
This commit is contained in:
Paul
2025-05-21 10:34:21 -07:00
committed by GitHub
parent 230128b92e
commit 4dfb2d24bb
7 changed files with 269 additions and 4 deletions

View File

@@ -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.

View File

@@ -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,

View File

@@ -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

View File

@@ -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: [

View File

@@ -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',
)
})
})
})

View File

@@ -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<T extends boolean = true> {
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;

View File

@@ -78,6 +78,65 @@ export const seed = async (payload: Payload): Promise<boolean> => {
},
})
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: {