From 988d0a4b08e1228bb358bb133bcb05dbce7f55ab Mon Sep 17 00:00:00 2001 From: James Date: Fri, 2 Apr 2021 11:15:28 -0400 Subject: [PATCH] feat: remembers conditional field values after removing / readding --- demo/collections/Conditions.ts | 18 ++++++++++++++ package.json | 2 +- .../forms/Form/buildStateFromSchema.ts | 19 ++++++++++++--- src/admin/components/forms/Form/index.tsx | 24 +++++++++++++++---- src/admin/components/forms/Form/types.ts | 3 +++ .../forms/field-types/Array/Array.tsx | 2 ++ .../forms/field-types/Blocks/Blocks.tsx | 2 ++ .../forms/field-types/Checkbox/index.tsx | 2 ++ .../forms/field-types/Code/Code.tsx | 2 ++ .../forms/field-types/DateTime/index.tsx | 2 ++ .../forms/field-types/Email/index.tsx | 2 ++ .../forms/field-types/Number/index.tsx | 2 ++ .../forms/field-types/RadioGroup/index.tsx | 2 ++ .../forms/field-types/Relationship/index.tsx | 2 ++ .../forms/field-types/RichText/RichText.tsx | 2 ++ .../forms/field-types/Select/index.tsx | 2 ++ .../forms/field-types/Text/index.tsx | 2 ++ .../forms/field-types/Textarea/index.tsx | 2 ++ .../forms/field-types/Upload/index.tsx | 2 ++ .../components/forms/useFieldType/index.tsx | 20 +++++++--------- .../components/forms/useFieldType/types.ts | 3 ++- .../components/forms/withCondition/index.tsx | 10 ++++---- src/fields/config/types.ts | 4 +++- 23 files changed, 105 insertions(+), 26 deletions(-) diff --git a/demo/collections/Conditions.ts b/demo/collections/Conditions.ts index b8b6ac5bdf..45e90687de 100644 --- a/demo/collections/Conditions.ts +++ b/demo/collections/Conditions.ts @@ -1,4 +1,8 @@ import { PayloadCollectionConfig } from '../../src/collections/config/types'; +import Email from '../blocks/Email'; +import Quote from '../blocks/Quote'; +import NumberBlock from '../blocks/Number'; +import CallToAction from '../blocks/CallToAction'; const Conditions: PayloadCollectionConfig = { slug: 'conditions', @@ -49,6 +53,20 @@ const Conditions: PayloadCollectionConfig = { condition: (_, siblings) => (siblings.number > 20 && siblings.enableTest === true) || (siblings.number < 20 && siblings.enableTest === false), }, }, + { + name: 'blocks', + label: 'Blocks', + labels: { + singular: 'Block', + plural: 'Blocks', + }, + type: 'blocks', + blocks: [Email, NumberBlock, Quote, CallToAction], + required: true, + admin: { + condition: (_, siblings) => siblings?.enableTest === true, + }, + }, ], }; diff --git a/package.json b/package.json index 191f36b9d1..997bb7caac 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "build:tsc": "tsc --p tsconfig.admin.json && tsc --p tsconfig.server.json", "build:components": "webpack --config dist/webpack/components.config.js", "build": "yarn copyfiles && yarn build:tsc && yarn build:components", - "build:watch": "nodemon --watch 'src/**' --ext 'ts' --exec 'yarn build:tsc'", + "build:watch": "nodemon --watch 'src/**' --ext 'ts,tsx' --exec 'yarn build:tsc'", "demo:build:analyze": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.ts PAYLOAD_ANALYZE_BUNDLE=true node dist/bin/build", "demo:build": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.ts node dist/bin/build", "dev": "cross-env PAYLOAD_CONFIG_PATH=demo/payload.config.ts nodemon", diff --git a/src/admin/components/forms/Form/buildStateFromSchema.ts b/src/admin/components/forms/Form/buildStateFromSchema.ts index d549786577..2f3726ed01 100644 --- a/src/admin/components/forms/Form/buildStateFromSchema.ts +++ b/src/admin/components/forms/Form/buildStateFromSchema.ts @@ -1,10 +1,22 @@ import { Field as FieldSchema } from '../../../../fields/config/types'; import { Fields, Field, Data } from './types'; -const buildValidationPromise = async (fieldState: Field, field: FieldSchema) => { +const buildValidationPromise = async (fieldState: Field, field: FieldSchema, fullData: Data = {}, data: Data = {}) => { const validatedFieldState = fieldState; - const validationResult = typeof field.validate === 'function' ? await field.validate(fieldState.value, field) : true; + let passesConditionalLogic = true; + + if (field?.admin?.condition) { + passesConditionalLogic = await field.admin.condition(fullData, data); + } + + let validationResult: boolean | string = true; + + if (!passesConditionalLogic) { + validationResult = true; + } else if (typeof field.validate === 'function') { + validationResult = await field.validate(fieldState.value, field); + } if (typeof validationResult === 'string') { validatedFieldState.errorMessage = validationResult; @@ -26,9 +38,10 @@ const buildStateFromSchema = async (fieldSchema: FieldSchema[], fullData: Data = initialValue: value, valid: true, validate: field.validate, + condition: field?.admin?.condition, }; - validationPromises.push(buildValidationPromise(fieldState, field)); + validationPromises.push(buildValidationPromise(fieldState, field, fullData, data)); return fieldState; }; diff --git a/src/admin/components/forms/Form/index.tsx b/src/admin/components/forms/Form/index.tsx index 469a6b891b..01895147fe 100644 --- a/src/admin/components/forms/Form/index.tsx +++ b/src/admin/components/forms/Form/index.tsx @@ -67,11 +67,27 @@ const Form: React.FC = (props) => { const validatedFieldState = {}; let isValid = true; - const validationPromises = Object.entries(contextRef.current.fields).map(async ([path, field]) => { - const validatedField = { ...field }; + const data = contextRef.current.getData(); - validatedField.valid = true; - const validationResult = typeof field.validate === 'function' ? await field.validate(field.value) : true; + const validationPromises = Object.entries(contextRef.current.fields).map(async ([path, field]) => { + const validatedField = { + ...field, + valid: true, + }; + + const siblingData = contextRef.current.getSiblingData(path); + + let passesConditionalLogic = true; + + if (typeof field?.condition === 'function') { + passesConditionalLogic = await field.condition(data, siblingData); + } + + let validationResult: boolean | string = true; + + if (passesConditionalLogic && typeof field.validate === 'function') { + validationResult = await field.validate(field.value); + } if (typeof validationResult === 'string') { validatedField.errorMessage = validationResult; diff --git a/src/admin/components/forms/Form/types.ts b/src/admin/components/forms/Form/types.ts index 3f7da71787..adcf3eb844 100644 --- a/src/admin/components/forms/Form/types.ts +++ b/src/admin/components/forms/Form/types.ts @@ -1,3 +1,5 @@ +import { Condition } from '../../../../fields/config/types'; + export type Field = { value: unknown initialValue: unknown @@ -7,6 +9,7 @@ export type Field = { disableFormData?: boolean ignoreWhileFlattening?: boolean stringify?: boolean + condition?: Condition } export type Fields = { diff --git a/src/admin/components/forms/field-types/Array/Array.tsx b/src/admin/components/forms/field-types/Array/Array.tsx index ff14405d35..6ad6afa893 100644 --- a/src/admin/components/forms/field-types/Array/Array.tsx +++ b/src/admin/components/forms/field-types/Array/Array.tsx @@ -35,6 +35,7 @@ const ArrayFieldType: React.FC = (props) => { permissions, admin: { readOnly, + condition, }, } = props; @@ -62,6 +63,7 @@ const ArrayFieldType: React.FC = (props) => { validate: memoizedValidate, disableFormData, ignoreWhileFlattening: true, + condition, }); const addRow = useCallback(async (rowIndex) => { diff --git a/src/admin/components/forms/field-types/Blocks/Blocks.tsx b/src/admin/components/forms/field-types/Blocks/Blocks.tsx index 8e286bbe69..7d8ce05128 100644 --- a/src/admin/components/forms/field-types/Blocks/Blocks.tsx +++ b/src/admin/components/forms/field-types/Blocks/Blocks.tsx @@ -41,6 +41,7 @@ const Blocks: React.FC = (props) => { permissions, admin: { readOnly, + condition, }, } = props; @@ -72,6 +73,7 @@ const Blocks: React.FC = (props) => { validate: memoizedValidate, disableFormData, ignoreWhileFlattening: true, + condition, }); const addRow = useCallback(async (rowIndex, blockType) => { diff --git a/src/admin/components/forms/field-types/Checkbox/index.tsx b/src/admin/components/forms/field-types/Checkbox/index.tsx index dc67255b6d..88b628df7f 100644 --- a/src/admin/components/forms/field-types/Checkbox/index.tsx +++ b/src/admin/components/forms/field-types/Checkbox/index.tsx @@ -23,6 +23,7 @@ const Checkbox: React.FC = (props) => { readOnly, style, width, + condition, } = {}, } = props; @@ -42,6 +43,7 @@ const Checkbox: React.FC = (props) => { path, validate: memoizedValidate, disableFormData, + condition, }); return ( diff --git a/src/admin/components/forms/field-types/Code/Code.tsx b/src/admin/components/forms/field-types/Code/Code.tsx index 72b30027df..77f279cd2d 100644 --- a/src/admin/components/forms/field-types/Code/Code.tsx +++ b/src/admin/components/forms/field-types/Code/Code.tsx @@ -23,6 +23,7 @@ const Code: React.FC = (props) => { style, width, language, + condition, } = {}, label, minLength, @@ -53,6 +54,7 @@ const Code: React.FC = (props) => { path, validate: memoizedValidate, enableDebouncedValue: true, + condition, }); const classes = [ diff --git a/src/admin/components/forms/field-types/DateTime/index.tsx b/src/admin/components/forms/field-types/DateTime/index.tsx index 49fb0861b5..ae37e7ae3f 100644 --- a/src/admin/components/forms/field-types/DateTime/index.tsx +++ b/src/admin/components/forms/field-types/DateTime/index.tsx @@ -25,6 +25,7 @@ const DateTime: React.FC = (props) => { style, width, date, + condition, } = {}, } = props; @@ -43,6 +44,7 @@ const DateTime: React.FC = (props) => { } = useFieldType({ path, validate: memoizedValidate, + condition, }); const classes = [ diff --git a/src/admin/components/forms/field-types/Email/index.tsx b/src/admin/components/forms/field-types/Email/index.tsx index cd5e9a42be..ddd3bb4ac9 100644 --- a/src/admin/components/forms/field-types/Email/index.tsx +++ b/src/admin/components/forms/field-types/Email/index.tsx @@ -20,6 +20,7 @@ const Email: React.FC = (props) => { width, placeholder, autoComplete, + condition, } = {}, label, } = props; @@ -35,6 +36,7 @@ const Email: React.FC = (props) => { path, validate: memoizedValidate, enableDebouncedValue: true, + condition, }); const { diff --git a/src/admin/components/forms/field-types/Number/index.tsx b/src/admin/components/forms/field-types/Number/index.tsx index 7c55f5622a..a84cae676b 100644 --- a/src/admin/components/forms/field-types/Number/index.tsx +++ b/src/admin/components/forms/field-types/Number/index.tsx @@ -23,6 +23,7 @@ const NumberField: React.FC = (props) => { width, step, placeholder, + condition, } = {}, } = props; @@ -42,6 +43,7 @@ const NumberField: React.FC = (props) => { path, validate: memoizedValidate, enableDebouncedValue: true, + condition, }); const handleChange = useCallback((e) => { diff --git a/src/admin/components/forms/field-types/RadioGroup/index.tsx b/src/admin/components/forms/field-types/RadioGroup/index.tsx index 6b223a1a97..7d75f12118 100644 --- a/src/admin/components/forms/field-types/RadioGroup/index.tsx +++ b/src/admin/components/forms/field-types/RadioGroup/index.tsx @@ -25,6 +25,7 @@ const RadioGroup: React.FC = (props) => { layout = 'horizontal', style, width, + condition, } = {}, options, } = props; @@ -44,6 +45,7 @@ const RadioGroup: React.FC = (props) => { } = useFieldType({ path, validate: memoizedValidate, + condition, }); const classes = [ diff --git a/src/admin/components/forms/field-types/Relationship/index.tsx b/src/admin/components/forms/field-types/Relationship/index.tsx index d7fc55d9d0..3744d8e1e5 100644 --- a/src/admin/components/forms/field-types/Relationship/index.tsx +++ b/src/admin/components/forms/field-types/Relationship/index.tsx @@ -34,6 +34,7 @@ const Relationship: React.FC = (props) => { readOnly, style, width, + condition, } = {}, } = props; @@ -70,6 +71,7 @@ const Relationship: React.FC = (props) => { } = useFieldType({ path: path || name, validate: memoizedValidate, + condition, }); const addOptions = useCallback((data, relation) => { diff --git a/src/admin/components/forms/field-types/RichText/RichText.tsx b/src/admin/components/forms/field-types/RichText/RichText.tsx index 9dafc0b960..2bc11ee2bb 100644 --- a/src/admin/components/forms/field-types/RichText/RichText.tsx +++ b/src/admin/components/forms/field-types/RichText/RichText.tsx @@ -41,6 +41,7 @@ const RichText: React.FC = (props) => { style, width, placeholder, + condition, } = {}, } = props; @@ -101,6 +102,7 @@ const RichText: React.FC = (props) => { path, validate: memoizedValidate, stringify: true, + condition, }); const { diff --git a/src/admin/components/forms/field-types/Select/index.tsx b/src/admin/components/forms/field-types/Select/index.tsx index 210d2d9972..9a5ae8e914 100644 --- a/src/admin/components/forms/field-types/Select/index.tsx +++ b/src/admin/components/forms/field-types/Select/index.tsx @@ -36,6 +36,7 @@ const Select: React.FC = (props) => { readOnly, style, width, + condition, } = {}, } = props; @@ -56,6 +57,7 @@ const Select: React.FC = (props) => { } = useFieldType({ path, validate: memoizedValidate, + condition, }); const classes = [ diff --git a/src/admin/components/forms/field-types/Text/index.tsx b/src/admin/components/forms/field-types/Text/index.tsx index 97c96cd9b7..792e95803a 100644 --- a/src/admin/components/forms/field-types/Text/index.tsx +++ b/src/admin/components/forms/field-types/Text/index.tsx @@ -20,6 +20,7 @@ const Text: React.FC = (props) => { readOnly, style, width, + condition, } = {}, } = props; @@ -28,6 +29,7 @@ const Text: React.FC = (props) => { const fieldType = useFieldType({ path, validate, + condition, enableDebouncedValue: true, }); diff --git a/src/admin/components/forms/field-types/Textarea/index.tsx b/src/admin/components/forms/field-types/Textarea/index.tsx index a3805b5946..22192690a5 100644 --- a/src/admin/components/forms/field-types/Textarea/index.tsx +++ b/src/admin/components/forms/field-types/Textarea/index.tsx @@ -20,6 +20,7 @@ const Textarea: React.FC = (props) => { width, placeholder, rows, + condition, } = {}, label, minLength, @@ -42,6 +43,7 @@ const Textarea: React.FC = (props) => { path, validate: memoizedValidate, enableDebouncedValue: true, + condition, }); const classes = [ diff --git a/src/admin/components/forms/field-types/Upload/index.tsx b/src/admin/components/forms/field-types/Upload/index.tsx index 16450047c1..fc72c333ab 100644 --- a/src/admin/components/forms/field-types/Upload/index.tsx +++ b/src/admin/components/forms/field-types/Upload/index.tsx @@ -30,6 +30,7 @@ const Upload: React.FC = (props) => { readOnly, style, width, + condition, } = {}, label, validate = upload, @@ -51,6 +52,7 @@ const Upload: React.FC = (props) => { const fieldType = useFieldType({ path, validate: memoizedValidate, + condition, }); const { diff --git a/src/admin/components/forms/useFieldType/index.tsx b/src/admin/components/forms/useFieldType/index.tsx index f8d39cfc70..ac032359fd 100644 --- a/src/admin/components/forms/useFieldType/index.tsx +++ b/src/admin/components/forms/useFieldType/index.tsx @@ -15,6 +15,7 @@ const useFieldType = (options: Options): FieldType => { disableFormData, ignoreWhileFlattening, stringify, + condition, } = options; const formContext = useForm(); @@ -46,14 +47,15 @@ const useFieldType = (options: Options): FieldType => { const sendField = useCallback(async (valueToSend) => { const fieldToDispatch = { path, + stringify, + disableFormData, + ignoreWhileFlattening, + initialValue, + validate, + condition, value: valueToSend, valid: false, errorMessage: undefined, - stringify: false, - disableFormData: false, - ignoreWhileFlattening: false, - initialValue: undefined, - validate: undefined, }; const validationResult = typeof validate === 'function' ? await validate(valueToSend) : true; @@ -65,14 +67,8 @@ const useFieldType = (options: Options): FieldType => { fieldToDispatch.valid = validationResult; } - fieldToDispatch.stringify = stringify; - fieldToDispatch.disableFormData = disableFormData; - fieldToDispatch.ignoreWhileFlattening = ignoreWhileFlattening; - fieldToDispatch.initialValue = initialValue; - fieldToDispatch.validate = validate; - dispatchFields(fieldToDispatch); - }, [path, dispatchFields, validate, disableFormData, ignoreWhileFlattening, initialValue, stringify]); + }, [path, dispatchFields, validate, disableFormData, ignoreWhileFlattening, initialValue, stringify, condition]); // Method to return from `useFieldType`, used to // update internal field values from field component(s) diff --git a/src/admin/components/forms/useFieldType/types.ts b/src/admin/components/forms/useFieldType/types.ts index 6803bb9cd4..f67ab8558f 100644 --- a/src/admin/components/forms/useFieldType/types.ts +++ b/src/admin/components/forms/useFieldType/types.ts @@ -1,4 +1,4 @@ -import { Validate } from '../../../../fields/config/types'; +import { Validate, Condition } from '../../../../fields/config/types'; export type Options = { path: string @@ -7,6 +7,7 @@ export type Options = { disableFormData?: boolean ignoreWhileFlattening?: boolean stringify?: boolean + condition?: Condition } export type FieldType = { diff --git a/src/admin/components/forms/withCondition/index.tsx b/src/admin/components/forms/withCondition/index.tsx index 8ba4e82742..fb33b750de 100644 --- a/src/admin/components/forms/withCondition/index.tsx +++ b/src/admin/components/forms/withCondition/index.tsx @@ -36,13 +36,15 @@ const withCondition =

>(Field: React.Component const field = getField(path); const siblingData = getSiblingData(path); const passesCondition = condition ? condition(data, siblingData) : true; - const fieldExists = Boolean(field); useEffect(() => { - if (!passesCondition && fieldExists) { - dispatchFields({ type: 'REMOVE', path }); + if (!passesCondition) { + dispatchFields({ + ...field, + valid: true, + }); } - }, [dispatchFields, passesCondition, path, fieldExists]); + }, [passesCondition, field, dispatchFields]); if (passesCondition) { return ; diff --git a/src/fields/config/types.ts b/src/fields/config/types.ts index 20b67f6ac6..450b3ef4eb 100644 --- a/src/fields/config/types.ts +++ b/src/fields/config/types.ts @@ -22,13 +22,15 @@ export type FieldAccess = (args: { siblingData: Record }) => Promise | boolean; +export type Condition = (data: Record, siblingData: Record) => boolean + type Admin = { position?: string; width?: string; style?: CSSProperties; readOnly?: boolean; disabled?: boolean; - condition?: (...args: any[]) => any | void; + condition?: Condition; components?: { [key: string]: React.ComponentType }; hidden?: boolean }