Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
493 lines
23 KiB
Plaintext
493 lines
23 KiB
Plaintext
import { MissingEditorProp } from '../../../errors/index.js';
|
|
import { validateBlocksFilterOptions } from '../../../index.js';
|
|
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js';
|
|
import { getTranslatedLabel } from '../../../utilities/getTranslatedLabel.js';
|
|
import { fieldAffectsData, fieldShouldBeLocalized, tabHasName } from '../../config/types.js';
|
|
import { getFieldPaths } from '../../getFieldPaths.js';
|
|
import { getExistingRowDoc } from './getExistingRowDoc.js';
|
|
import { traverseFields } from './traverseFields.js';
|
|
function buildFieldLabel(parentLabel, label) {
|
|
if (!label) {
|
|
return parentLabel;
|
|
}
|
|
const capitalizedLabel = label.charAt(0).toUpperCase() + label.slice(1);
|
|
return parentLabel && capitalizedLabel ? `${parentLabel} > ${capitalizedLabel}` : capitalizedLabel || parentLabel;
|
|
}
|
|
// This function is responsible for the following actions, in order:
|
|
// - Run condition
|
|
// - Execute field hooks
|
|
// - Validate data
|
|
// - Transform data for storage
|
|
// - beforeDuplicate hooks (if duplicate)
|
|
// - Unflatten locales
|
|
export const promise = async ({ id, blockData, collection, context, data, doc, docWithLocales, errors, field, fieldIndex, fieldLabelPath, global, mergeLocaleActions, operation, overrideAccess, parentIndexPath, parentIsLocalized, parentPath, parentSchemaPath, req, siblingData, siblingDoc, siblingDocWithLocales, siblingFields, skipValidation })=>{
|
|
const { indexPath, path, schemaPath } = getFieldPaths({
|
|
field,
|
|
index: fieldIndex,
|
|
parentIndexPath,
|
|
parentPath,
|
|
parentSchemaPath
|
|
});
|
|
const { localization } = req.payload.config;
|
|
const defaultLocale = localization ? localization?.defaultLocale : 'en';
|
|
const operationLocale = req.locale || defaultLocale;
|
|
const pathSegments = path ? path.split('.') : [];
|
|
const schemaPathSegments = schemaPath ? schemaPath.split('.') : [];
|
|
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : [];
|
|
const passesCondition = field.admin?.condition ? Boolean(field.admin.condition(data, siblingData, {
|
|
blockData: blockData,
|
|
operation,
|
|
path: pathSegments,
|
|
user: req.user
|
|
})) : true;
|
|
let skipValidationFromHere = skipValidation || !passesCondition;
|
|
if (fieldAffectsData(field)) {
|
|
// skip validation if the field is localized and the incoming data is null
|
|
if (fieldShouldBeLocalized({
|
|
field,
|
|
parentIsLocalized
|
|
}) && operationLocale !== defaultLocale) {
|
|
if ([
|
|
'array',
|
|
'blocks'
|
|
].includes(field.type) && siblingData[field.name] === null) {
|
|
skipValidationFromHere = true;
|
|
}
|
|
}
|
|
// Execute hooks
|
|
if ('hooks' in field && field.hooks?.beforeChange) {
|
|
for (const hook of field.hooks.beforeChange){
|
|
const hookedValue = await hook({
|
|
blockData,
|
|
collection,
|
|
context,
|
|
data,
|
|
field,
|
|
global,
|
|
indexPath: indexPathSegments,
|
|
operation,
|
|
originalDoc: doc,
|
|
path: pathSegments,
|
|
previousSiblingDoc: siblingDoc,
|
|
previousValue: siblingDoc[field.name],
|
|
req,
|
|
schemaPath: schemaPathSegments,
|
|
siblingData,
|
|
siblingDocWithLocales,
|
|
siblingFields: siblingFields,
|
|
value: siblingData[field.name]
|
|
});
|
|
if (hookedValue !== undefined) {
|
|
siblingData[field.name] = hookedValue;
|
|
}
|
|
}
|
|
}
|
|
// Validate
|
|
if (!skipValidationFromHere && 'validate' in field && field.validate) {
|
|
const valueToValidate = siblingData[field.name];
|
|
let jsonError;
|
|
if (field.type === 'json' && typeof siblingData[field.name] === 'string') {
|
|
try {
|
|
JSON.parse(siblingData[field.name]);
|
|
} catch (e) {
|
|
jsonError = e;
|
|
}
|
|
}
|
|
const validateFn = field.validate;
|
|
const validationResult = await validateFn(valueToValidate, {
|
|
...field,
|
|
id,
|
|
blockData: blockData,
|
|
collectionSlug: collection?.slug,
|
|
data: deepMergeWithSourceArrays(doc, data),
|
|
event: 'submit',
|
|
// @ts-expect-error
|
|
jsonError,
|
|
operation,
|
|
overrideAccess,
|
|
path: pathSegments,
|
|
preferences: {
|
|
fields: {}
|
|
},
|
|
previousValue: siblingDoc[field.name],
|
|
req,
|
|
siblingData: deepMergeWithSourceArrays(siblingDoc, siblingData)
|
|
});
|
|
if (typeof validationResult === 'string') {
|
|
let filterOptionsError = false;
|
|
if (field.type === 'blocks' && field.filterOptions) {
|
|
// Re-run filteroptions. If the validation error is due to filteroptions, we need to add error paths to all the blocks
|
|
// that are no longer valid
|
|
const validationResult = await validateBlocksFilterOptions({
|
|
id,
|
|
data,
|
|
filterOptions: field.filterOptions,
|
|
req,
|
|
siblingData,
|
|
value: siblingData[field.name]
|
|
});
|
|
if (validationResult?.invalidBlockSlugs?.length) {
|
|
filterOptionsError = true;
|
|
let rowIndex = -1;
|
|
for (const block of siblingData[field.name]){
|
|
rowIndex++;
|
|
if (validationResult.invalidBlockSlugs.includes(block.blockType)) {
|
|
const blockConfigOrSlug = (field.blockReferences ?? field.blocks).find((blockFromField)=>typeof blockFromField === 'string' ? blockFromField === block.blockType : blockFromField.slug === block.blockType);
|
|
const blockConfig = typeof blockConfigOrSlug !== 'string' ? blockConfigOrSlug : req.payload.config?.blocks?.[blockConfigOrSlug];
|
|
const blockLabelPath = field?.label === false ? fieldLabelPath : buildFieldLabel(fieldLabelPath, `${getTranslatedLabel(field?.label || field?.name, req.i18n)} > ${req.t('fields:block')} ${rowIndex + 1} (${getTranslatedLabel(blockConfig?.labels?.singular || block.blockType, req.i18n)})`);
|
|
errors.push({
|
|
label: blockLabelPath,
|
|
message: req.t('validation:invalidBlock', {
|
|
block: block.blockType
|
|
}),
|
|
path: `${path}.${rowIndex}.id`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!filterOptionsError) {
|
|
// If the error is due to block filterOptions, we want to push the errors for each individual block, not the blocks
|
|
// field itself => only push the error if the field is not a block field with validation failure due to filterOptions
|
|
const fieldLabel = buildFieldLabel(fieldLabelPath, getTranslatedLabel(field?.label || field?.name, req.i18n));
|
|
errors.push({
|
|
label: fieldLabel,
|
|
message: validationResult,
|
|
path
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// Push merge locale action if applicable
|
|
if (localization && fieldShouldBeLocalized({
|
|
field,
|
|
parentIsLocalized
|
|
})) {
|
|
mergeLocaleActions.push(()=>{
|
|
const localeData = {};
|
|
for (const locale of localization.localeCodes){
|
|
const fieldValue = locale === req.locale ? siblingData[field.name] : siblingDocWithLocales?.[field.name]?.[locale];
|
|
// update locale value if it's not undefined
|
|
if (typeof fieldValue !== 'undefined') {
|
|
localeData[locale] = fieldValue;
|
|
}
|
|
}
|
|
// If there are locales with data, set the data
|
|
if (Object.keys(localeData).length > 0) {
|
|
siblingData[field.name] = localeData;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
switch(field.type){
|
|
case 'array':
|
|
{
|
|
const rows = siblingData[field.name];
|
|
if (Array.isArray(rows)) {
|
|
const promises = [];
|
|
rows.forEach((row, rowIndex)=>{
|
|
promises.push(traverseFields({
|
|
id,
|
|
blockData,
|
|
collection,
|
|
context,
|
|
data,
|
|
doc,
|
|
docWithLocales,
|
|
errors,
|
|
fieldLabelPath: field?.label === false ? fieldLabelPath : buildFieldLabel(fieldLabelPath, `${getTranslatedLabel(field?.label || field?.name, req.i18n)} ${rowIndex + 1}`),
|
|
fields: field.fields,
|
|
global,
|
|
mergeLocaleActions,
|
|
operation,
|
|
overrideAccess,
|
|
parentIndexPath: '',
|
|
parentIsLocalized: parentIsLocalized || field.localized,
|
|
parentPath: path + '.' + rowIndex,
|
|
parentSchemaPath: schemaPath,
|
|
req,
|
|
siblingData: row,
|
|
siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
|
|
siblingDocWithLocales: getExistingRowDoc(row, siblingDocWithLocales?.[field.name]),
|
|
skipValidation: skipValidationFromHere
|
|
}));
|
|
});
|
|
await Promise.all(promises);
|
|
}
|
|
break;
|
|
}
|
|
case 'blocks':
|
|
{
|
|
const rows = siblingData[field.name];
|
|
if (Array.isArray(rows)) {
|
|
const promises = [];
|
|
rows.forEach((row, rowIndex)=>{
|
|
const rowSiblingDoc = getExistingRowDoc(row, siblingDoc[field.name]);
|
|
const rowSiblingDocWithLocales = getExistingRowDoc(row, siblingDocWithLocales ? siblingDocWithLocales[field.name] : {});
|
|
const blockTypeToMatch = row.blockType || rowSiblingDoc.blockType;
|
|
const block = req.payload.blocks[blockTypeToMatch] ?? (field.blockReferences ?? field.blocks).find((curBlock)=>typeof curBlock !== 'string' && curBlock.slug === blockTypeToMatch);
|
|
const blockLabelPath = field?.label === false ? fieldLabelPath : buildFieldLabel(fieldLabelPath, `${getTranslatedLabel(field?.label || field?.name, req.i18n)} > ${req.t('fields:block')} ${rowIndex + 1} (${getTranslatedLabel(block?.labels?.singular || blockTypeToMatch, req.i18n)})`);
|
|
if (block) {
|
|
promises.push(traverseFields({
|
|
id,
|
|
blockData: row,
|
|
collection,
|
|
context,
|
|
data,
|
|
doc,
|
|
docWithLocales,
|
|
errors,
|
|
fieldLabelPath: blockLabelPath,
|
|
fields: block.fields,
|
|
global,
|
|
mergeLocaleActions,
|
|
operation,
|
|
overrideAccess,
|
|
parentIndexPath: '',
|
|
parentIsLocalized: parentIsLocalized || field.localized,
|
|
parentPath: path + '.' + rowIndex,
|
|
parentSchemaPath: schemaPath + '.' + block.slug,
|
|
req,
|
|
siblingData: row,
|
|
siblingDoc: rowSiblingDoc,
|
|
siblingDocWithLocales: rowSiblingDocWithLocales,
|
|
skipValidation: skipValidationFromHere
|
|
}));
|
|
}
|
|
});
|
|
await Promise.all(promises);
|
|
}
|
|
break;
|
|
}
|
|
case 'collapsible':
|
|
case 'row':
|
|
{
|
|
await traverseFields({
|
|
id,
|
|
blockData,
|
|
collection,
|
|
context,
|
|
data,
|
|
doc,
|
|
docWithLocales,
|
|
errors,
|
|
fieldLabelPath: field.type === 'row' || field?.label === false ? fieldLabelPath : buildFieldLabel(fieldLabelPath, getTranslatedLabel(field?.label || field?.type, req.i18n)),
|
|
fields: field.fields,
|
|
global,
|
|
mergeLocaleActions,
|
|
operation,
|
|
overrideAccess,
|
|
parentIndexPath: indexPath,
|
|
parentIsLocalized,
|
|
parentPath,
|
|
parentSchemaPath: schemaPath,
|
|
req,
|
|
siblingData,
|
|
siblingDoc,
|
|
siblingDocWithLocales: siblingDocWithLocales,
|
|
skipValidation: skipValidationFromHere
|
|
});
|
|
break;
|
|
}
|
|
case 'group':
|
|
{
|
|
let groupSiblingData = siblingData;
|
|
let groupSiblingDoc = siblingDoc;
|
|
let groupSiblingDocWithLocales = siblingDocWithLocales;
|
|
const isNamedGroup = fieldAffectsData(field);
|
|
if (isNamedGroup) {
|
|
if (typeof siblingData[field.name] !== 'object') {
|
|
siblingData[field.name] = {};
|
|
}
|
|
if (typeof siblingDoc[field.name] !== 'object') {
|
|
siblingDoc[field.name] = {};
|
|
}
|
|
if (typeof siblingDocWithLocales[field.name] !== 'object') {
|
|
siblingDocWithLocales[field.name] = {};
|
|
}
|
|
if (typeof siblingData[field.name] !== 'object') {
|
|
siblingData[field.name] = {};
|
|
}
|
|
if (typeof siblingDoc[field.name] !== 'object') {
|
|
siblingDoc[field.name] = {};
|
|
}
|
|
if (typeof siblingDocWithLocales[field.name] !== 'object') {
|
|
siblingDocWithLocales[field.name] = {};
|
|
}
|
|
groupSiblingData = siblingData[field.name];
|
|
groupSiblingDoc = siblingDoc[field.name];
|
|
groupSiblingDocWithLocales = siblingDocWithLocales[field.name];
|
|
}
|
|
const fallbackLabel = field?.label || (isNamedGroup ? field.name : field?.type);
|
|
await traverseFields({
|
|
id,
|
|
blockData,
|
|
collection,
|
|
context,
|
|
data,
|
|
doc,
|
|
docWithLocales,
|
|
errors,
|
|
fieldLabelPath: field?.label === false ? fieldLabelPath : buildFieldLabel(fieldLabelPath, getTranslatedLabel(fallbackLabel, req.i18n)),
|
|
fields: field.fields,
|
|
global,
|
|
mergeLocaleActions,
|
|
operation,
|
|
overrideAccess,
|
|
parentIndexPath: isNamedGroup ? '' : indexPath,
|
|
parentIsLocalized: parentIsLocalized || field.localized,
|
|
parentPath: isNamedGroup ? path : parentPath,
|
|
parentSchemaPath: schemaPath,
|
|
req,
|
|
siblingData: groupSiblingData,
|
|
siblingDoc: groupSiblingDoc,
|
|
siblingDocWithLocales: groupSiblingDocWithLocales,
|
|
skipValidation: skipValidationFromHere
|
|
});
|
|
break;
|
|
}
|
|
case 'point':
|
|
{
|
|
// Transform point data for storage
|
|
if (Array.isArray(siblingData[field.name]) && siblingData[field.name][0] !== null && siblingData[field.name][1] !== null) {
|
|
siblingData[field.name] = {
|
|
type: 'Point',
|
|
coordinates: [
|
|
parseFloat(siblingData[field.name][0]),
|
|
parseFloat(siblingData[field.name][1])
|
|
]
|
|
};
|
|
}
|
|
break;
|
|
}
|
|
case 'richText':
|
|
{
|
|
if (!field?.editor) {
|
|
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
|
;
|
|
}
|
|
if (typeof field?.editor === 'function') {
|
|
throw new Error('Attempted to access unsanitized rich text editor.');
|
|
}
|
|
const editor = field?.editor;
|
|
if (editor?.hooks?.beforeChange?.length) {
|
|
for (const hook of editor.hooks.beforeChange){
|
|
const hookedValue = await hook({
|
|
collection,
|
|
context,
|
|
data,
|
|
docWithLocales,
|
|
errors,
|
|
field,
|
|
fieldLabelPath: field?.label === false ? fieldLabelPath : buildFieldLabel(fieldLabelPath, getTranslatedLabel(field?.label || field?.name, req.i18n)),
|
|
global,
|
|
indexPath: indexPathSegments,
|
|
mergeLocaleActions,
|
|
operation,
|
|
originalDoc: doc,
|
|
overrideAccess,
|
|
parentIsLocalized,
|
|
path: pathSegments,
|
|
previousSiblingDoc: siblingDoc,
|
|
previousValue: siblingDoc[field.name],
|
|
req,
|
|
schemaPath: schemaPathSegments,
|
|
siblingData,
|
|
siblingDocWithLocales,
|
|
skipValidation,
|
|
value: siblingData[field.name]
|
|
});
|
|
if (hookedValue !== undefined) {
|
|
siblingData[field.name] = hookedValue;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'tab':
|
|
{
|
|
let tabSiblingData = siblingData;
|
|
let tabSiblingDoc = siblingDoc;
|
|
let tabSiblingDocWithLocales = siblingDocWithLocales;
|
|
const isNamedTab = tabHasName(field);
|
|
if (isNamedTab) {
|
|
if (typeof siblingData[field.name] !== 'object') {
|
|
siblingData[field.name] = {};
|
|
}
|
|
if (typeof siblingDoc[field.name] !== 'object') {
|
|
siblingDoc[field.name] = {};
|
|
}
|
|
if (typeof siblingDocWithLocales[field.name] !== 'object') {
|
|
siblingDocWithLocales[field.name] = {};
|
|
}
|
|
tabSiblingData = siblingData[field.name];
|
|
tabSiblingDoc = siblingDoc[field.name];
|
|
tabSiblingDocWithLocales = siblingDocWithLocales[field.name];
|
|
}
|
|
await traverseFields({
|
|
id,
|
|
blockData,
|
|
collection,
|
|
context,
|
|
data,
|
|
doc,
|
|
docWithLocales,
|
|
errors,
|
|
fieldLabelPath: field?.label === false ? fieldLabelPath : buildFieldLabel(fieldLabelPath, getTranslatedLabel(field?.label || field.name, req.i18n)),
|
|
fields: field.fields,
|
|
global,
|
|
mergeLocaleActions,
|
|
operation,
|
|
overrideAccess,
|
|
parentIndexPath: isNamedTab ? '' : indexPath,
|
|
parentIsLocalized: parentIsLocalized || field.localized,
|
|
parentPath: isNamedTab ? path : parentPath,
|
|
parentSchemaPath: schemaPath,
|
|
req,
|
|
siblingData: tabSiblingData,
|
|
siblingDoc: tabSiblingDoc,
|
|
siblingDocWithLocales: tabSiblingDocWithLocales,
|
|
skipValidation: skipValidationFromHere
|
|
});
|
|
break;
|
|
}
|
|
case 'tabs':
|
|
{
|
|
await traverseFields({
|
|
id,
|
|
blockData,
|
|
collection,
|
|
context,
|
|
data,
|
|
doc,
|
|
docWithLocales,
|
|
errors,
|
|
fieldLabelPath: field?.label === false ? fieldLabelPath : buildFieldLabel(fieldLabelPath, getTranslatedLabel(field?.label || '', req.i18n)),
|
|
fields: field.tabs.map((tab)=>({
|
|
...tab,
|
|
type: 'tab'
|
|
})),
|
|
global,
|
|
mergeLocaleActions,
|
|
operation,
|
|
overrideAccess,
|
|
parentIndexPath: indexPath,
|
|
parentIsLocalized,
|
|
parentPath: path,
|
|
parentSchemaPath: schemaPath,
|
|
req,
|
|
siblingData,
|
|
siblingDoc,
|
|
siblingDocWithLocales: siblingDocWithLocales,
|
|
skipValidation: skipValidationFromHere
|
|
});
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
//# sourceMappingURL=promise.js.map |