diff --git a/src/fields/hooks/beforeChange/promise.ts b/src/fields/hooks/beforeChange/promise.ts index 5d7185b4f3..58fdb66731 100644 --- a/src/fields/hooks/beforeChange/promise.ts +++ b/src/fields/hooks/beforeChange/promise.ts @@ -3,10 +3,8 @@ import merge from 'deepmerge'; import { Field, fieldAffectsData, TabAsField, tabHasName } from '../../config/types'; import { Operation } from '../../../types'; import { PayloadRequest, RequestContext } from '../../../express/types'; -import getValueWithDefault from '../../getDefaultValue'; import { traverseFields } from './traverseFields'; import { getExistingRowDoc } from './getExistingRowDoc'; -import { cloneDataFromOriginalDoc } from './cloneDataFromOriginalDoc'; type Args = { data: Record @@ -28,8 +26,6 @@ type Args = { // This function is responsible for the following actions, in order: // - Run condition -// - Merge original document data into incoming data -// - Compute default values for undefined fields // - Execute field hooks // - Validate data // - Transform data for storage @@ -59,26 +55,6 @@ export const promise = async ({ const operationLocale = req.locale || defaultLocale; if (fieldAffectsData(field)) { - if (typeof siblingData[field.name] === 'undefined') { - // If no incoming data, but existing document data is found, merge it in - if (typeof siblingDoc[field.name] !== 'undefined') { - if (field.localized && typeof siblingDocWithLocales[field.name] === 'object' && siblingDocWithLocales[field.name] !== null) { - siblingData[field.name] = cloneDataFromOriginalDoc(siblingDocWithLocales[field.name][req.locale]); - } else { - siblingData[field.name] = cloneDataFromOriginalDoc(siblingDoc[field.name]); - } - - // Otherwise compute default value - } else if (typeof field.defaultValue !== 'undefined') { - siblingData[field.name] = await getValueWithDefault({ - value: siblingData[field.name], - defaultValue: field.defaultValue, - locale: req.locale, - user: req.user, - }); - } - } - // skip validation if the field is localized and the incoming data is null if (field.localized && operationLocale !== defaultLocale) { if (['array', 'blocks'].includes(field.type) && siblingData[field.name] === null) { diff --git a/src/fields/hooks/beforeValidate/promise.ts b/src/fields/hooks/beforeValidate/promise.ts index 5d6e0aa8bd..5f206dc3fc 100644 --- a/src/fields/hooks/beforeValidate/promise.ts +++ b/src/fields/hooks/beforeValidate/promise.ts @@ -1,6 +1,9 @@ /* eslint-disable no-param-reassign */ import { PayloadRequest, RequestContext } from '../../../express/types'; import { Field, fieldAffectsData, TabAsField, tabHasName, valueIsValueWithRelation } from '../../config/types'; +import getValueWithDefault from '../../getDefaultValue'; +import { cloneDataFromOriginalDoc } from '../beforeChange/cloneDataFromOriginalDoc'; +import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc'; import { traverseFields } from './traverseFields'; type Args = { @@ -20,6 +23,8 @@ type Args = { // - Sanitize incoming data // - Execute field hooks // - Execute field access control +// - Merge original document data into incoming data +// - Compute default values for undefined fields export const promise = async ({ data, @@ -189,6 +194,22 @@ export const promise = async ({ delete siblingData[field.name]; } } + + if (typeof siblingData[field.name] === 'undefined') { + // If no incoming data, but existing document data is found, merge it in + if (typeof siblingDoc[field.name] !== 'undefined') { + siblingData[field.name] = cloneDataFromOriginalDoc(siblingDoc[field.name]); + + // Otherwise compute default value + } else if (typeof field.defaultValue !== 'undefined') { + siblingData[field.name] = await getValueWithDefault({ + value: siblingData[field.name], + defaultValue: field.defaultValue, + locale: req.locale, + user: req.user, + }); + } + } } // Traverse subfields @@ -231,7 +252,7 @@ export const promise = async ({ overrideAccess, req, siblingData: row, - siblingDoc: siblingDoc[field.name]?.[i] || {}, + siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]), context, })); }); @@ -258,7 +279,7 @@ export const promise = async ({ overrideAccess, req, siblingData: row, - siblingDoc: siblingDoc[field.name]?.[i] || {}, + siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]), context, })); } @@ -291,8 +312,11 @@ export const promise = async ({ let tabSiblingData; let tabSiblingDoc; if (tabHasName(field)) { - tabSiblingData = typeof siblingData[field.name] === 'object' ? siblingData[field.name] : {}; - tabSiblingDoc = typeof siblingDoc[field.name] === 'object' ? siblingDoc[field.name] : {}; + if (typeof siblingData[field.name] !== 'object') siblingData[field.name] = {}; + if (typeof siblingDoc[field.name] !== 'object') siblingDoc[field.name] = {}; + + tabSiblingData = siblingData[field.name] as Record; + tabSiblingDoc = siblingDoc[field.name] as Record; } else { tabSiblingData = siblingData; tabSiblingDoc = siblingDoc; diff --git a/test/fields/collections/Text/index.ts b/test/fields/collections/Text/index.ts index 56a7220ebd..fd92475e38 100644 --- a/test/fields/collections/Text/index.ts +++ b/test/fields/collections/Text/index.ts @@ -57,6 +57,26 @@ const TextFields: CollectionConfig = { type: 'text', maxLength: 50000, }, + { + name: 'fieldWithDefaultValue', + type: 'text', + defaultValue: async () => { + const defaultValue = new Promise((resolve) => setTimeout(() => resolve('some-value'), 1000)); + + return defaultValue; + }, + }, + { + name: 'dependentOnFieldWithDefaultValue', + type: 'text', + hooks: { + beforeChange: [ + ({ data }) => { + return data?.fieldWithDefaultValue || ''; + }, + ], + }, + }, ], }; diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 5cc287230d..2266f560f8 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -47,6 +47,15 @@ describe('Fields', () => { expect(doc.defaultFunction).toEqual(defaultText); expect(doc.defaultAsync).toEqual(defaultText); }); + + it('should populate default values in beforeValidate hook', async () => { + const { fieldWithDefaultValue, dependentOnFieldWithDefaultValue } = await payload.create({ + collection: 'text-fields', + data: { text }, + }); + + await expect(fieldWithDefaultValue).toEqual(dependentOnFieldWithDefaultValue); + }); }); describe('timestamps', () => {