Files
klz-cables.com/.pnpm-store/v10/files/94/79ee2fd90ed2bbea53b9bd603773c15adf67a158c898a31692b5399a8927d3deab478015eefc23516e4c0c1a8e36e4b2e64060b6e8722be280d07383351be5
Marc Mintel 5397309103
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
fix(products): fix breadcrumbs and product filtering (backport from main)
2026-02-24 16:04:21 +01:00

771 lines
27 KiB
Plaintext

import ObjectIdImport from 'bson-objectid';
import { getBlockSelect, stripUnselectedFields, validateBlocksFilterOptions } from 'payload';
import { deepCopyObjectSimple, fieldAffectsData, fieldHasSubFields, fieldIsHiddenOrDisabled, fieldIsID, fieldIsLocalized, tabHasName } from 'payload/shared';
import { resolveFilterOptions } from '../../utilities/resolveFilterOptions.js';
import { isRowCollapsed } from './isRowCollapsed.js';
import { iterateFields } from './iterateFields.js';
const ObjectId = 'default' in ObjectIdImport ? ObjectIdImport.default : ObjectIdImport;
/**
* Flattens the fields schema and fields data.
* The output is the field path (e.g. array.0.name) mapped to a FormField object.
*/
export const addFieldStatePromise = async args => {
const {
id,
addErrorPathToParent: addErrorPathToParentArg,
anyParentLocalized = false,
blockData,
clientFieldSchemaMap,
collectionSlug,
data,
field,
fieldSchemaMap,
filter,
forceFullValue = false,
fullData,
includeSchema = false,
indexPath,
mockRSCs,
omitParents = false,
operation,
parentPath,
parentPermissions,
parentSchemaPath,
passesCondition,
path,
preferences,
previousFormState,
readOnly,
renderAllFields,
renderFieldFn,
req,
schemaPath,
select,
selectMode,
skipConditionChecks = false,
skipValidation = false,
state
} = args;
if (!args.clientFieldSchemaMap && args.renderFieldFn) {
// eslint-disable-next-line no-console
console.warn('clientFieldSchemaMap is not passed to addFieldStatePromise - this will reduce performance');
}
let fieldPermissions = true;
const fieldState = {};
const lastRenderedPath = previousFormState?.[path]?.lastRenderedPath;
// Append only if true to avoid sending '$undefined' through the network
if (lastRenderedPath) {
fieldState.lastRenderedPath = lastRenderedPath;
}
// If we're rendering all fields, no need to flag this as added by server
const addedByServer = !renderAllFields && !previousFormState?.[path];
// Append only if true to avoid sending '$undefined' through the network
if (addedByServer) {
fieldState.addedByServer = true;
}
// Append only if true to avoid sending '$undefined' through the network
if (passesCondition === false) {
fieldState.passesCondition = false;
}
// Append only if true to avoid sending '$undefined' through the network
if (includeSchema) {
fieldState.fieldSchema = field;
}
if (fieldAffectsData(field) && !fieldIsHiddenOrDisabled(field) && field.type !== 'tab') {
fieldPermissions = parentPermissions === true ? parentPermissions : deepCopyObjectSimple(parentPermissions?.[field.name]);
let hasPermission = fieldPermissions === true || deepCopyObjectSimple(fieldPermissions?.read);
if (typeof field?.access?.read === 'function') {
hasPermission = await field.access.read({
id,
blockData,
data: fullData,
req,
siblingData: data
});
} else {
hasPermission = true;
}
if (!hasPermission) {
return;
}
const validate = 'validate' in field ? field.validate : undefined;
let validationResult = true;
if (typeof validate === 'function' && !skipValidation && passesCondition) {
let jsonError;
if (field.type === 'json' && typeof data[field.name] === 'string') {
try {
JSON.parse(data[field.name]);
} catch (e) {
jsonError = e;
}
}
try {
validationResult = await validate(data?.[field.name], {
...field,
id,
blockData,
collectionSlug,
data: fullData,
event: 'onChange',
// @AlessioGr added `jsonError` in https://github.com/payloadcms/payload/commit/c7ea62a39473408c3ea912c4fbf73e11be4b538d
// @ts-expect-error-next-line
jsonError,
operation,
preferences,
previousValue: previousFormState?.[path]?.initialValue,
req,
siblingData: data
});
} catch (err) {
validationResult = `Error validating field at path: ${path}`;
req.payload.logger.error({
err,
msg: validationResult
});
}
}
/**
* This function adds the error **path** to the current field and all its parents. If a field is invalid, all its parents are also invalid.
* It does not add the error **message** to the current field, as that shouldn't apply to all parents.
* This is done separately below.
*/
const addErrorPathToParent = errorPath => {
if (typeof addErrorPathToParentArg === 'function') {
addErrorPathToParentArg(errorPath);
}
if (!fieldState.errorPaths) {
fieldState.errorPaths = [];
}
if (!fieldState.errorPaths.includes(errorPath)) {
fieldState.errorPaths.push(errorPath);
fieldState.valid = false;
}
};
if (typeof validationResult === 'string') {
fieldState.errorMessage = validationResult;
fieldState.valid = false;
addErrorPathToParent(path);
}
switch (field.type) {
case 'array':
{
const arrayValue = Array.isArray(data[field.name]) ? data[field.name] : [];
const arraySelect = select?.[field.name];
const {
promises,
rows
} = arrayValue.reduce((acc, row, rowIndex) => {
const rowPath = path + '.' + rowIndex;
row.id = row?.id || new ObjectId().toHexString();
if (!omitParents && (!filter || filter(args))) {
const idKey = rowPath + '.id';
state[idKey] = {
initialValue: row.id,
value: row.id
};
if (includeSchema) {
state[idKey].fieldSchema = field.fields.find(field => fieldIsID(field));
}
}
acc.promises.push(iterateFields({
id,
addErrorPathToParent,
anyParentLocalized: field.localized || anyParentLocalized,
blockData,
clientFieldSchemaMap,
collectionSlug,
data: row,
fields: field.fields,
fieldSchemaMap,
filter,
forceFullValue,
fullData,
includeSchema,
mockRSCs,
omitParents,
operation,
parentIndexPath: '',
parentPassesCondition: passesCondition,
parentPath: rowPath,
parentSchemaPath: schemaPath,
permissions: fieldPermissions === true ? fieldPermissions : fieldPermissions?.fields || {},
preferences,
previousFormState,
readOnly,
renderAllFields,
renderFieldFn,
req,
select: typeof arraySelect === 'object' ? arraySelect : undefined,
selectMode,
skipConditionChecks,
skipValidation,
state
}));
if (!acc.rows) {
acc.rows = [];
}
// First, check if `previousFormState` has a matching row
const previousRow = (previousFormState?.[path]?.rows || []).find(prevRow => prevRow.id === row.id);
const newRow = {
id: row.id,
isLoading: false
};
if (previousRow?.lastRenderedPath) {
newRow.lastRenderedPath = previousRow.lastRenderedPath;
}
// add addedByServer flag
if (!previousRow) {
newRow.addedByServer = true;
}
const isCollapsed = isRowCollapsed({
collapsedPrefs: preferences?.fields?.[path]?.collapsed,
field,
previousRow,
row
});
if (isCollapsed) {
newRow.collapsed = true;
}
acc.rows.push(newRow);
return acc;
}, {
promises: [],
rows: []
});
// Wait for all promises and update fields with the results
await Promise.all(promises);
if (rows) {
fieldState.rows = rows;
}
// Add values to field state
if (data[field.name] !== null) {
fieldState.value = forceFullValue ? arrayValue : arrayValue.length;
fieldState.initialValue = forceFullValue ? arrayValue : arrayValue.length;
if (arrayValue.length > 0) {
fieldState.disableFormData = true;
}
}
// Add field to state
if (!omitParents && (!filter || filter(args))) {
state[path] = fieldState;
}
break;
}
case 'blocks':
{
const blocksValue = Array.isArray(data[field.name]) ? data[field.name] : [];
// Handle blocks filterOptions
let filterOptionsValidationResult = null;
if (field.filterOptions) {
filterOptionsValidationResult = await validateBlocksFilterOptions({
id,
data: fullData,
filterOptions: field.filterOptions,
req,
siblingData: data,
value: data[field.name]
});
fieldState.blocksFilterOptions = filterOptionsValidationResult.allowedBlockSlugs;
}
const {
promises,
rowMetadata
} = blocksValue.reduce((acc, row, i) => {
const blockTypeToMatch = row.blockType;
const block = req.payload.blocks[blockTypeToMatch] ?? (field.blockReferences ?? field.blocks).find(blockType => typeof blockType !== 'string' && blockType.slug === blockTypeToMatch);
if (!block) {
throw new Error(`Block with type "${row.blockType}" was found in block data, but no block with that type is defined in the config for field with schema path ${schemaPath}.`);
}
const {
blockSelect,
blockSelectMode
} = getBlockSelect({
block,
select: select?.[field.name],
selectMode
});
const rowPath = path + '.' + i;
if (block) {
row.id = row?.id || new ObjectId().toHexString();
if (!omitParents && (!filter || filter(args))) {
// Handle block `id` field
const idKey = rowPath + '.id';
state[idKey] = {
initialValue: row.id,
value: row.id
};
// If the blocks field fails filterOptions validation, add error paths to the individual blocks that are no longer allowed
if (filterOptionsValidationResult?.invalidBlockSlugs?.length && filterOptionsValidationResult.invalidBlockSlugs.includes(row.blockType)) {
state[idKey].errorMessage = req.t('validation:invalidBlock', {
block: row.blockType
});
state[idKey].valid = false;
addErrorPathToParent(idKey);
// If the error is due to block filterOptions, we want the blocks field (fieldState) to include all the filterOptions-related
// error paths for each sub-block, not for the validation result of the block itself. Otherwise, say there are 2 invalid blocks,
// the blocks field will have 3 instead of 2 error paths - one for itself, and one for each invalid block.
// Instead, we want only the 2 error paths for the individual, invalid blocks.
fieldState.errorPaths = fieldState.errorPaths.filter(errorPath => errorPath !== path);
}
if (includeSchema) {
state[idKey].fieldSchema = includeSchema ? block.fields.find(blockField => fieldIsID(blockField)) : undefined;
}
// Handle `blockType` field
const fieldKey = rowPath + '.blockType';
state[fieldKey] = {
initialValue: row.blockType,
value: row.blockType
};
if (addedByServer) {
state[fieldKey].addedByServer = addedByServer;
}
if (includeSchema) {
state[fieldKey].fieldSchema = block.fields.find(blockField => 'name' in blockField && blockField.name === 'blockType');
}
// Handle `blockName` field
const blockNameKey = rowPath + '.blockName';
state[blockNameKey] = {};
if (row.blockName) {
state[blockNameKey].initialValue = row.blockName;
state[blockNameKey].value = row.blockName;
}
if (includeSchema) {
state[blockNameKey].fieldSchema = block.fields.find(blockField => 'name' in blockField && blockField.name === 'blockName');
}
}
acc.promises.push(iterateFields({
id,
addErrorPathToParent,
anyParentLocalized: field.localized || anyParentLocalized,
blockData: row,
clientFieldSchemaMap,
collectionSlug,
data: row,
fields: block.fields,
fieldSchemaMap,
filter,
forceFullValue,
fullData,
includeSchema,
mockRSCs,
omitParents,
operation,
parentIndexPath: '',
parentPassesCondition: passesCondition,
parentPath: rowPath,
parentSchemaPath: schemaPath + '.' + block.slug,
permissions: fieldPermissions === true ? fieldPermissions : parentPermissions?.[field.name]?.blocks?.[block.slug] === true ? true : parentPermissions?.[field.name]?.blocks?.[block.slug]?.fields || {},
preferences,
previousFormState,
readOnly,
renderAllFields,
renderFieldFn,
req,
select: typeof blockSelect === 'object' ? blockSelect : undefined,
selectMode: blockSelectMode,
skipConditionChecks,
skipValidation,
state
}));
// First, check if `previousFormState` has a matching row
const previousRow = (previousFormState?.[path]?.rows || []).find(prevRow => prevRow.id === row.id);
const newRow = {
id: row.id,
blockType: row.blockType,
isLoading: false
};
if (previousRow?.lastRenderedPath) {
newRow.lastRenderedPath = previousRow.lastRenderedPath;
}
acc.rowMetadata.push(newRow);
const isCollapsed = isRowCollapsed({
collapsedPrefs: preferences?.fields?.[path]?.collapsed,
field,
previousRow,
row
});
if (isCollapsed) {
acc.rowMetadata[acc.rowMetadata.length - 1].collapsed = true;
}
}
return acc;
}, {
promises: [],
rowMetadata: []
});
await Promise.all(promises);
// Add values to field state
if (data[field.name] === null) {
fieldState.value = null;
fieldState.initialValue = null;
} else {
fieldState.value = forceFullValue ? blocksValue : blocksValue.length;
fieldState.initialValue = forceFullValue ? blocksValue : blocksValue.length;
if (blocksValue.length > 0) {
fieldState.disableFormData = true;
}
}
fieldState.rows = rowMetadata;
// Add field to state
if (!omitParents && (!filter || filter(args))) {
state[path] = fieldState;
}
break;
}
case 'group':
{
if (!filter || filter(args)) {
fieldState.disableFormData = true;
state[path] = fieldState;
}
const groupSelect = select?.[field.name];
await iterateFields({
id,
addErrorPathToParent,
anyParentLocalized: field.localized || anyParentLocalized,
blockData,
clientFieldSchemaMap,
collectionSlug,
data: data?.[field.name] || {},
fields: field.fields,
fieldSchemaMap,
filter,
forceFullValue,
fullData,
includeSchema,
mockRSCs,
omitParents,
operation,
parentIndexPath: '',
parentPassesCondition: passesCondition,
parentPath: path,
parentSchemaPath: schemaPath,
permissions: typeof fieldPermissions === 'boolean' ? fieldPermissions : fieldPermissions?.fields,
preferences,
previousFormState,
readOnly,
renderAllFields,
renderFieldFn,
req,
select: typeof groupSelect === 'object' ? groupSelect : undefined,
selectMode,
skipConditionChecks,
skipValidation,
state
});
break;
}
case 'relationship':
case 'upload':
{
if (field.filterOptions) {
if (typeof field.filterOptions === 'object') {
if (typeof field.relationTo === 'string') {
fieldState.filterOptions = {
[field.relationTo]: field.filterOptions
};
} else {
fieldState.filterOptions = field.relationTo.reduce((acc, relation) => {
acc[relation] = field.filterOptions;
return acc;
}, {});
}
}
if (typeof field.filterOptions === 'function') {
const query = await resolveFilterOptions(field.filterOptions, {
id,
blockData,
data: fullData,
relationTo: field.relationTo,
req,
siblingData: data,
user: req.user
});
fieldState.filterOptions = query;
}
}
if (field.hasMany) {
const relationshipValue = Array.isArray(data[field.name]) ? data[field.name].map(relationship => {
if (Array.isArray(field.relationTo)) {
return {
relationTo: relationship.relationTo,
value: relationship.value && typeof relationship.value === 'object' ? relationship.value?.id : relationship.value
};
}
if (typeof relationship === 'object' && relationship !== null) {
return relationship.id;
}
return relationship;
}) : undefined;
fieldState.value = relationshipValue;
fieldState.initialValue = relationshipValue;
} else if (Array.isArray(field.relationTo)) {
if (data[field.name] && typeof data[field.name] === 'object' && 'relationTo' in data[field.name] && 'value' in data[field.name]) {
const value = typeof data[field.name]?.value === 'object' && data[field.name]?.value && 'id' in data[field.name].value ? data[field.name].value.id : data[field.name].value;
const relationshipValue = {
relationTo: data[field.name]?.relationTo,
value
};
fieldState.value = relationshipValue;
fieldState.initialValue = relationshipValue;
}
} else {
const relationshipValue = data[field.name] && typeof data[field.name] === 'object' && 'id' in data[field.name] ? data[field.name].id : data[field.name];
fieldState.value = relationshipValue;
fieldState.initialValue = relationshipValue;
}
if (!filter || filter(args)) {
state[path] = fieldState;
}
break;
}
case 'select':
{
if (typeof field.filterOptions === 'function') {
fieldState.selectFilterOptions = field.filterOptions({
data: fullData,
options: field.options,
req,
siblingData: data
});
}
if (data[field.name] !== undefined) {
fieldState.value = data[field.name];
fieldState.initialValue = data[field.name];
}
if (!filter || filter(args)) {
state[path] = fieldState;
}
break;
}
default:
{
if (data[field.name] !== undefined) {
fieldState.value = data[field.name];
fieldState.initialValue = data[field.name];
}
// Add field to state
if (!filter || filter(args)) {
state[path] = fieldState;
}
break;
}
}
} else if (fieldHasSubFields(field) && !fieldAffectsData(field)) {
// Handle field types that do not use names (row, collapsible, unnamed group etc)
if (!filter || filter(args)) {
state[path] = {
disableFormData: true
};
if (passesCondition === false) {
state[path].passesCondition = false;
}
}
await iterateFields({
id,
mockRSCs,
select,
selectMode,
// passthrough parent functionality
addErrorPathToParent: addErrorPathToParentArg,
anyParentLocalized: fieldIsLocalized(field) || anyParentLocalized,
blockData,
clientFieldSchemaMap,
collectionSlug,
data,
fields: field.fields,
fieldSchemaMap,
filter,
forceFullValue,
fullData,
includeSchema,
omitParents,
operation,
parentIndexPath: indexPath,
parentPassesCondition: passesCondition,
parentPath: path,
parentSchemaPath: schemaPath,
permissions: parentPermissions,
preferences,
previousFormState,
readOnly,
renderAllFields,
renderFieldFn,
req,
skipConditionChecks,
skipValidation,
state
});
} else if (field.type === 'tab') {
const isNamedTab = tabHasName(field);
let tabSelect;
const tabField = {
...field,
type: 'tab'
};
let childPermissions = undefined;
if (isNamedTab) {
const shouldContinue = stripUnselectedFields({
field: tabField,
select,
selectMode,
siblingDoc: data?.[field.name] || {}
});
if (!shouldContinue) {
return;
}
if (parentPermissions === true) {
childPermissions = true;
} else {
const tabPermissions = parentPermissions?.[field.name];
if (tabPermissions === true) {
childPermissions = true;
} else {
childPermissions = tabPermissions?.fields;
}
}
if (typeof select?.[field.name] === 'object') {
tabSelect = select?.[field.name];
}
} else {
childPermissions = parentPermissions;
tabSelect = select;
}
const pathSegments = path ? path.split('.') : [];
// If passesCondition is false then this should always result to false
// If the tab has no admin.condition provided then fallback to passesCondition and let that decide the result
let tabPassesCondition = passesCondition;
if (passesCondition && typeof field.admin?.condition === 'function') {
tabPassesCondition = field.admin.condition(fullData, data, {
blockData,
operation,
path: pathSegments,
user: req.user
});
}
if (field?.id) {
state[field.id] = {
passesCondition: tabPassesCondition
};
}
return iterateFields({
id,
addErrorPathToParent: addErrorPathToParentArg,
anyParentLocalized: field.localized || anyParentLocalized,
blockData,
clientFieldSchemaMap,
collectionSlug,
data: isNamedTab ? data?.[field.name] || {} : data,
fields: field.fields,
fieldSchemaMap,
filter,
forceFullValue,
fullData,
includeSchema,
mockRSCs,
omitParents,
operation,
parentIndexPath: indexPath,
parentPassesCondition: tabPassesCondition,
parentPath: path,
parentSchemaPath: schemaPath,
permissions: childPermissions,
preferences,
previousFormState,
readOnly,
renderAllFields,
renderFieldFn,
req,
select: tabSelect,
selectMode,
skipConditionChecks,
skipValidation,
state
});
} else if (field.type === 'tabs') {
return iterateFields({
id,
addErrorPathToParent: addErrorPathToParentArg,
anyParentLocalized: fieldIsLocalized(field) || anyParentLocalized,
blockData,
clientFieldSchemaMap,
collectionSlug,
data,
fields: field.tabs.map(tab => ({
...tab,
type: 'tab'
})),
fieldSchemaMap,
filter,
forceFullValue,
fullData,
includeSchema,
omitParents,
operation,
parentIndexPath: indexPath,
parentPassesCondition: passesCondition,
parentPath: path,
parentSchemaPath: schemaPath,
permissions: parentPermissions,
preferences,
previousFormState,
renderAllFields,
renderFieldFn,
req,
select,
selectMode,
skipConditionChecks,
skipValidation,
state
});
} else if (field.type === 'ui') {
if (!filter || filter(args)) {
state[path] = fieldState;
state[path].disableFormData = true;
}
}
if (renderFieldFn && !fieldIsHiddenOrDisabled(field)) {
const fieldConfig = fieldSchemaMap.get(schemaPath);
if (!fieldConfig && !mockRSCs) {
if (schemaPath.endsWith('.blockType')) {
return;
} else {
throw new Error(`Field config not found for ${schemaPath}`);
}
}
if (!state[path]) {
// Some fields (ie `Tab`) do not live in form state
// therefore we cannot attach customComponents to them
return;
}
if (addedByServer) {
state[path].addedByServer = addedByServer;
}
renderFieldFn({
id,
clientFieldSchemaMap,
collectionSlug,
data: fullData,
fieldConfig: fieldConfig,
fieldSchemaMap,
fieldState: state[path],
formState: state,
indexPath,
lastRenderedPath,
mockRSCs,
operation,
parentPath,
parentSchemaPath,
path,
permissions: fieldPermissions,
preferences,
previousFieldState: previousFormState?.[path],
readOnly,
renderAllFields,
req,
schemaPath,
siblingData: data
});
}
};
//# sourceMappingURL=addFieldStatePromise.js.map