From 7d0e909a301c2f2d1b348e46986785328293d90f Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 28 May 2024 17:45:51 -0300 Subject: [PATCH] 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) --- .../src/collections/FormSubmissions/index.ts | 143 ++++---- .../src/collections/Forms/index.ts | 306 +++++++++--------- packages/plugin-form-builder/src/index.ts | 14 - packages/plugin-form-builder/src/types.ts | 5 +- test/plugin-form-builder/config.ts | 26 +- 5 files changed, 252 insertions(+), 242 deletions(-) diff --git a/packages/plugin-form-builder/src/collections/FormSubmissions/index.ts b/packages/plugin-form-builder/src/collections/FormSubmissions/index.ts index dee29e48cd1..0d6e802768c 100644 --- a/packages/plugin-form-builder/src/collections/FormSubmissions/index.ts +++ b/packages/plugin-form-builder/src/collections/FormSubmissions/index.ts @@ -1,4 +1,4 @@ -import type { CollectionConfig } from 'payload/types' +import type { CollectionConfig, Field } from 'payload/types' import type { FormBuilderPluginConfig } from '../../types.js' @@ -11,6 +11,74 @@ export const generateSubmissionCollection = ( ): CollectionConfig => { 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 = { ...(formConfig?.formSubmissionOverrides || {}), slug: formConfig?.formSubmissionOverrides?.slug || 'form-submissions', @@ -24,74 +92,11 @@ export const generateSubmissionCollection = ( ...(formConfig?.formSubmissionOverrides?.admin || {}), enableRichTextRelationship: false, }, - fields: [ - { - 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.' - }, - }, - ], - }, - ...(formConfig?.formSubmissionOverrides?.fields || []), - ], + fields: + formConfig?.formSubmissionOverrides?.fields && + typeof formConfig?.formSubmissionOverrides?.fields === 'function' + ? formConfig?.formSubmissionOverrides?.fields({ defaultFields }) + : defaultFields, hooks: { ...(formConfig?.formSubmissionOverrides?.hooks || {}), beforeChange: [ diff --git a/packages/plugin-form-builder/src/collections/Forms/index.ts b/packages/plugin-form-builder/src/collections/Forms/index.ts index d182a780a6e..fb3d0ea3329 100644 --- a/packages/plugin-form-builder/src/collections/Forms/index.ts +++ b/packages/plugin-form-builder/src/collections/Forms/index.ts @@ -64,171 +64,175 @@ export const generateFormCollection = (formConfig: FormBuilderPluginConfig): Col } } - const config: CollectionConfig = { - ...(formConfig?.formOverrides || {}), - slug: formConfig?.formOverrides?.slug || 'forms', - access: { - read: () => true, - ...(formConfig?.formOverrides?.access || {}), - }, - admin: { - enableRichTextRelationship: false, - useAsTitle: 'title', - ...(formConfig?.formOverrides?.admin || {}), + const defaultFields: Field[] = [ + { + name: 'title', + type: 'text', + required: true, }, - fields: [ - { - 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(block, fieldConfig, { - arrayMerge: (_, sourceArray) => sourceArray, - }) - } - - if (typeof block === 'function') { - return block(fieldConfig) - } - - return block + { + 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 } - return null - }) - .filter(Boolean) as Block[], - }, - { - name: 'submitButtonLabel', - type: 'text', - localized: true, + if (typeof block === 'object' && typeof fieldConfig === 'object') { + return merge(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', }, - { - 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', }, - defaultValue: 'message', - options: [ - { - label: 'Message', - value: 'message', - }, - { - label: 'Redirect', - value: 'redirect', - }, - ], - }, - { - name: 'confirmationMessage', - type: 'richText', - admin: { - condition: (_, siblingData) => siblingData?.confirmationType === 'message', + { + label: 'Redirect', + value: 'redirect', }, - localized: true, - required: true, + ], + }, + { + name: 'confirmationMessage', + type: 'richText', + admin: { + condition: (_, siblingData) => siblingData?.confirmationType === 'message', }, - 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" ', - width: '100%', - }, - label: 'Email To', + 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" ', + width: '100%', }, - { - name: 'cc', - type: 'text', - admin: { - width: '50%', - }, - label: 'CC', + label: 'Email To', + }, + { + name: 'cc', + type: 'text', + admin: { + width: '50%', }, - { - name: 'bcc', - type: 'text', - admin: { - width: '50%', - }, - label: 'BCC', + label: 'CC', + }, + { + name: 'bcc', + type: 'text', + admin: { + width: '50%', }, - ], - }, - { - type: 'row', - fields: [ - { - name: 'replyTo', - type: 'text', - admin: { - placeholder: '"Reply To" ', - width: '50%', - }, - label: 'Reply To', + label: 'BCC', + }, + ], + }, + { + type: 'row', + fields: [ + { + name: 'replyTo', + type: 'text', + admin: { + placeholder: '"Reply To" ', + width: '50%', }, - { - name: 'emailFrom', - type: 'text', - admin: { - placeholder: '"Email From" ', - width: '50%', - }, - label: 'Email From', + label: 'Reply To', + }, + { + name: 'emailFrom', + type: 'text', + admin: { + placeholder: '"Email From" ', + width: '50%', }, - ], - }, - { - 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: 'Email From', }, - label: 'Message', - localized: true, + ], + }, + { + 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.', }, - ], - }, - ...(formConfig?.formOverrides?.fields || []), - ], + label: 'Message', + localized: true, + }, + ], + }, + ] + + const config: CollectionConfig = { + ...(formConfig?.formOverrides || {}), + slug: formConfig?.formOverrides?.slug || 'forms', + access: { + read: () => true, + ...(formConfig?.formOverrides?.access || {}), + }, + admin: { + enableRichTextRelationship: false, + useAsTitle: 'title', + ...(formConfig?.formOverrides?.admin || {}), + }, + fields: + formConfig?.formOverrides.fields && typeof formConfig?.formOverrides.fields === 'function' + ? formConfig?.formOverrides.fields({ defaultFields }) + : defaultFields, } return config diff --git a/packages/plugin-form-builder/src/index.ts b/packages/plugin-form-builder/src/index.ts index d63c8be6339..d6f78ea8c1a 100644 --- a/packages/plugin-form-builder/src/index.ts +++ b/packages/plugin-form-builder/src/index.ts @@ -30,20 +30,6 @@ export const formBuilderPlugin = return { ...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: [ ...(config?.collections || []), generateFormCollection(formConfig), diff --git a/packages/plugin-form-builder/src/types.ts b/packages/plugin-form-builder/src/types.ts index 9cd2b7d6905..44db8911a63 100644 --- a/packages/plugin-form-builder/src/types.ts +++ b/packages/plugin-form-builder/src/types.ts @@ -39,12 +39,13 @@ export interface FieldsConfig { export type BeforeEmail = (emails: FormattedEmail[]) => FormattedEmail[] | Promise export type HandlePayment = (data: any) => void +export type FieldsOverride = (args: { defaultFields: Field[] }) => Field[] export type FormBuilderPluginConfig = { beforeEmail?: BeforeEmail fields?: FieldsConfig - formOverrides?: Partial - formSubmissionOverrides?: Partial + formOverrides?: Partial> & { fields: FieldsOverride } + formSubmissionOverrides?: Partial> & { fields: FieldsOverride } handlePayment?: HandlePayment redirectRelationships?: string[] } diff --git a/test/plugin-form-builder/config.ts b/test/plugin-form-builder/config.ts index af29e3eec65..cc0b04f9ca4 100644 --- a/test/plugin-form-builder/config.ts +++ b/test/plugin-form-builder/config.ts @@ -73,12 +73,26 @@ export default buildConfigWithDefaults({ // singular: 'Contact Form', // plural: 'Contact Forms' // }, - fields: [ - { - name: 'custom', - type: 'text', - }, - ], + fields: ({ defaultFields }) => { + return [ + ...defaultFields, + { + name: 'custom', + type: 'text', + }, + ] + }, + }, + formSubmissionOverrides: { + fields: ({ defaultFields }) => { + return [ + ...defaultFields, + { + name: 'custom', + type: 'text', + }, + ] + }, }, redirectRelationships: ['pages'], }),