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