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:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user