feat(plugin-form-builder)!: update form builder plugin field overrides to use a function instead (#6497)

## Description

Changes the `fields` override for form builder plugin to use a function
instead so that we can actually override existing fields which currently
will not work.

```ts
//before
fields: [
  {
    name: 'custom',
    type: 'text',
  }
]

// current
fields: ({ defaultFields }) => {
  return [
    ...defaultFields,
    {
      name: 'custom',
      type: 'text',
    },
  ]
}
```

## Type of change

- [x] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
This commit is contained in:
Paul
2024-05-28 17:45:51 -03:00
committed by GitHub
parent b2662eeb1f
commit 7d0e909a30
5 changed files with 254 additions and 244 deletions

View File

@@ -1,4 +1,4 @@
import type { CollectionConfig } from 'payload/types' import type { CollectionConfig, Field } from 'payload/types'
import type { FormBuilderPluginConfig } from '../../types.js' import type { FormBuilderPluginConfig } from '../../types.js'
@@ -11,6 +11,74 @@ export const generateSubmissionCollection = (
): CollectionConfig => { ): CollectionConfig => {
const formSlug = formConfig?.formOverrides?.slug || 'forms' const formSlug = formConfig?.formOverrides?.slug || 'forms'
const defaultFields: Field[] = [
{
name: 'form',
type: 'relationship',
admin: {
readOnly: true,
},
relationTo: formSlug,
required: true,
validate: async (value, { req: { payload }, req }) => {
/* Don't run in the client side */
if (!payload) return true
if (payload) {
let _existingForm
try {
_existingForm = await payload.findByID({
id: value,
collection: formSlug,
req,
})
return true
} catch (error) {
return 'Cannot create this submission because this form does not exist.'
}
}
},
},
{
name: 'submissionData',
type: 'array',
admin: {
readOnly: true,
},
fields: [
{
name: 'field',
type: 'text',
required: true,
},
{
name: 'value',
type: 'text',
required: true,
validate: (value: unknown) => {
// TODO:
// create a validation function that dynamically
// relies on the field type and its options as configured.
// How to access sibling data from this field?
// Need the `name` of the field in order to validate it.
// Might not be possible to use this validation function.
// Instead, might need to do all validation in a `beforeValidate` collection hook.
if (typeof value !== 'undefined') {
return true
}
return 'This field is required.'
},
},
],
},
]
const newConfig: CollectionConfig = { const newConfig: CollectionConfig = {
...(formConfig?.formSubmissionOverrides || {}), ...(formConfig?.formSubmissionOverrides || {}),
slug: formConfig?.formSubmissionOverrides?.slug || 'form-submissions', slug: formConfig?.formSubmissionOverrides?.slug || 'form-submissions',
@@ -24,74 +92,11 @@ export const generateSubmissionCollection = (
...(formConfig?.formSubmissionOverrides?.admin || {}), ...(formConfig?.formSubmissionOverrides?.admin || {}),
enableRichTextRelationship: false, enableRichTextRelationship: false,
}, },
fields: [ fields:
{ formConfig?.formSubmissionOverrides?.fields &&
name: 'form', typeof formConfig?.formSubmissionOverrides?.fields === 'function'
type: 'relationship', ? formConfig?.formSubmissionOverrides?.fields({ defaultFields })
admin: { : defaultFields,
readOnly: true,
},
relationTo: formSlug,
required: true,
validate: async (value, { req: { payload }, req }) => {
/* Don't run in the client side */
if (!payload) return true
if (payload) {
let _existingForm
try {
_existingForm = await payload.findByID({
id: value,
collection: formSlug,
req,
})
return true
} catch (error) {
return 'Cannot create this submission because this form does not exist.'
}
}
},
},
{
name: 'submissionData',
type: 'array',
admin: {
readOnly: true,
},
fields: [
{
name: 'field',
type: 'text',
required: true,
},
{
name: 'value',
type: 'text',
required: true,
validate: (value: unknown) => {
// TODO:
// create a validation function that dynamically
// relies on the field type and its options as configured.
// How to access sibling data from this field?
// Need the `name` of the field in order to validate it.
// Might not be possible to use this validation function.
// Instead, might need to do all validation in a `beforeValidate` collection hook.
if (typeof value !== 'undefined') {
return true
}
return 'This field is required.'
},
},
],
},
...(formConfig?.formSubmissionOverrides?.fields || []),
],
hooks: { hooks: {
...(formConfig?.formSubmissionOverrides?.hooks || {}), ...(formConfig?.formSubmissionOverrides?.hooks || {}),
beforeChange: [ beforeChange: [

View File

@@ -64,6 +64,159 @@ export const generateFormCollection = (formConfig: FormBuilderPluginConfig): Col
} }
} }
const defaultFields: Field[] = [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'fields',
type: 'blocks',
blocks: Object.entries(formConfig?.fields || {})
.map(([fieldKey, fieldConfig]) => {
// let the config enable/disable fields with either boolean values or objects
if (fieldConfig !== false) {
const block = fields[fieldKey]
if (block === undefined && typeof fieldConfig === 'object') {
return fieldConfig
}
if (typeof block === 'object' && typeof fieldConfig === 'object') {
return merge<FieldConfig>(block, fieldConfig, {
arrayMerge: (_, sourceArray) => sourceArray,
})
}
if (typeof block === 'function') {
return block(fieldConfig)
}
return block
}
return null
})
.filter(Boolean) as Block[],
},
{
name: 'submitButtonLabel',
type: 'text',
localized: true,
},
{
name: 'confirmationType',
type: 'radio',
admin: {
description:
'Choose whether to display an on-page message or redirect to a different page after they submit the form.',
layout: 'horizontal',
},
defaultValue: 'message',
options: [
{
label: 'Message',
value: 'message',
},
{
label: 'Redirect',
value: 'redirect',
},
],
},
{
name: 'confirmationMessage',
type: 'richText',
admin: {
condition: (_, siblingData) => siblingData?.confirmationType === 'message',
},
localized: true,
required: true,
},
redirect,
{
name: 'emails',
type: 'array',
admin: {
description:
"Send custom emails when the form submits. Use comma separated lists to send the same email to multiple recipients. To reference a value from this form, wrap that field's name with double curly brackets, i.e. {{firstName}}.",
},
fields: [
{
type: 'row',
fields: [
{
name: 'emailTo',
type: 'text',
admin: {
placeholder: '"Email Sender" <sender@email.com>',
width: '100%',
},
label: 'Email To',
},
{
name: 'cc',
type: 'text',
admin: {
width: '50%',
},
label: 'CC',
},
{
name: 'bcc',
type: 'text',
admin: {
width: '50%',
},
label: 'BCC',
},
],
},
{
type: 'row',
fields: [
{
name: 'replyTo',
type: 'text',
admin: {
placeholder: '"Reply To" <reply-to@email.com>',
width: '50%',
},
label: 'Reply To',
},
{
name: 'emailFrom',
type: 'text',
admin: {
placeholder: '"Email From" <email-from@email.com>',
width: '50%',
},
label: 'Email From',
},
],
},
{
name: 'subject',
type: 'text',
defaultValue: "You've received a new message.",
label: 'Subject',
localized: true,
required: true,
},
{
name: 'message',
type: 'richText',
admin: {
description: 'Enter the message that should be sent in this email.',
},
label: 'Message',
localized: true,
},
],
},
]
const config: CollectionConfig = { const config: CollectionConfig = {
...(formConfig?.formOverrides || {}), ...(formConfig?.formOverrides || {}),
slug: formConfig?.formOverrides?.slug || 'forms', slug: formConfig?.formOverrides?.slug || 'forms',
@@ -76,159 +229,10 @@ export const generateFormCollection = (formConfig: FormBuilderPluginConfig): Col
useAsTitle: 'title', useAsTitle: 'title',
...(formConfig?.formOverrides?.admin || {}), ...(formConfig?.formOverrides?.admin || {}),
}, },
fields: [ fields:
{ formConfig?.formOverrides.fields && typeof formConfig?.formOverrides.fields === 'function'
name: 'title', ? formConfig?.formOverrides.fields({ defaultFields })
type: 'text', : defaultFields,
required: true,
},
{
name: 'fields',
type: 'blocks',
blocks: Object.entries(formConfig?.fields || {})
.map(([fieldKey, fieldConfig]) => {
// let the config enable/disable fields with either boolean values or objects
if (fieldConfig !== false) {
const block = fields[fieldKey]
if (block === undefined && typeof fieldConfig === 'object') {
return fieldConfig
}
if (typeof block === 'object' && typeof fieldConfig === 'object') {
return merge<FieldConfig>(block, fieldConfig, {
arrayMerge: (_, sourceArray) => sourceArray,
})
}
if (typeof block === 'function') {
return block(fieldConfig)
}
return block
}
return null
})
.filter(Boolean) as Block[],
},
{
name: 'submitButtonLabel',
type: 'text',
localized: true,
},
{
name: 'confirmationType',
type: 'radio',
admin: {
description:
'Choose whether to display an on-page message or redirect to a different page after they submit the form.',
layout: 'horizontal',
},
defaultValue: 'message',
options: [
{
label: 'Message',
value: 'message',
},
{
label: 'Redirect',
value: 'redirect',
},
],
},
{
name: 'confirmationMessage',
type: 'richText',
admin: {
condition: (_, siblingData) => siblingData?.confirmationType === 'message',
},
localized: true,
required: true,
},
redirect,
{
name: 'emails',
type: 'array',
admin: {
description:
"Send custom emails when the form submits. Use comma separated lists to send the same email to multiple recipients. To reference a value from this form, wrap that field's name with double curly brackets, i.e. {{firstName}}.",
},
fields: [
{
type: 'row',
fields: [
{
name: 'emailTo',
type: 'text',
admin: {
placeholder: '"Email Sender" <sender@email.com>',
width: '100%',
},
label: 'Email To',
},
{
name: 'cc',
type: 'text',
admin: {
width: '50%',
},
label: 'CC',
},
{
name: 'bcc',
type: 'text',
admin: {
width: '50%',
},
label: 'BCC',
},
],
},
{
type: 'row',
fields: [
{
name: 'replyTo',
type: 'text',
admin: {
placeholder: '"Reply To" <reply-to@email.com>',
width: '50%',
},
label: 'Reply To',
},
{
name: 'emailFrom',
type: 'text',
admin: {
placeholder: '"Email From" <email-from@email.com>',
width: '50%',
},
label: 'Email From',
},
],
},
{
name: 'subject',
type: 'text',
defaultValue: "You've received a new message.",
label: 'Subject',
localized: true,
required: true,
},
{
name: 'message',
type: 'richText',
admin: {
description: 'Enter the message that should be sent in this email.',
},
label: 'Message',
localized: true,
},
],
},
...(formConfig?.formOverrides?.fields || []),
],
} }
return config return config

View File

@@ -30,20 +30,6 @@ export const formBuilderPlugin =
return { return {
...config, ...config,
// admin: {
// ...config.admin,
// webpack: (webpackConfig) => ({
// ...webpackConfig,
// resolve: {
// ...webpackConfig.resolve,
// alias: {
// ...webpackConfig.resolve.alias,
// [path.resolve(__dirname, 'collections/FormSubmissions/hooks/sendEmail.ts')]: path.resolve(__dirname, 'mocks/serverModule.js'),
// [path.resolve(__dirname, 'collections/FormSubmissions/hooks/createCharge.ts')]: path.resolve(__dirname, 'mocks/serverModule.js'),
// },
// },
// })
// },
collections: [ collections: [
...(config?.collections || []), ...(config?.collections || []),
generateFormCollection(formConfig), generateFormCollection(formConfig),

View File

@@ -39,12 +39,13 @@ export interface FieldsConfig {
export type BeforeEmail = (emails: FormattedEmail[]) => FormattedEmail[] | Promise<FormattedEmail[]> export type BeforeEmail = (emails: FormattedEmail[]) => FormattedEmail[] | Promise<FormattedEmail[]>
export type HandlePayment = (data: any) => void export type HandlePayment = (data: any) => void
export type FieldsOverride = (args: { defaultFields: Field[] }) => Field[]
export type FormBuilderPluginConfig = { export type FormBuilderPluginConfig = {
beforeEmail?: BeforeEmail beforeEmail?: BeforeEmail
fields?: FieldsConfig fields?: FieldsConfig
formOverrides?: Partial<CollectionConfig> formOverrides?: Partial<Omit<CollectionConfig, 'fields'>> & { fields: FieldsOverride }
formSubmissionOverrides?: Partial<CollectionConfig> formSubmissionOverrides?: Partial<Omit<CollectionConfig, 'fields'>> & { fields: FieldsOverride }
handlePayment?: HandlePayment handlePayment?: HandlePayment
redirectRelationships?: string[] redirectRelationships?: string[]
} }

View File

@@ -73,12 +73,26 @@ export default buildConfigWithDefaults({
// singular: 'Contact Form', // singular: 'Contact Form',
// plural: 'Contact Forms' // plural: 'Contact Forms'
// }, // },
fields: [ fields: ({ defaultFields }) => {
{ return [
name: 'custom', ...defaultFields,
type: 'text', {
}, name: 'custom',
], type: 'text',
},
]
},
},
formSubmissionOverrides: {
fields: ({ defaultFields }) => {
return [
...defaultFields,
{
name: 'custom',
type: 'text',
},
]
},
}, },
redirectRelationships: ['pages'], redirectRelationships: ['pages'],
}), }),