Continuation of #11867. When rendering custom fields nested within arrays or blocks, such as the Lexical rich text editor which is treated as a custom field, these fields will sometimes disappear when form state requests are invoked sequentially. This is especially reproducible on slow networks. This is different from the previous PR in that this issue is caused by adding _rows_ back-to-back, whereas the previous issue was caused when adding a single row followed by a change to another field. Here's a screen recording demonstrating the issue: https://github.com/user-attachments/assets/5ecfa9ec-b747-49ed-8618-df282e64519d The problem is that `requiresRender` is never sent in the form state request for row 2. This is because the [task queue](https://github.com/payloadcms/payload/pull/11579) processes tasks within a single `useEffect`. This forces React to batch the results of these tasks into a single rendering cycle. So if request 1 sets state that request 2 relies on, request 2 will never use that state since they'll execute within the same lifecycle. Here's a play-by-play of the current behavior: 1. The "add row" event is dispatched a. This sets `requiresRender: true` in form state 1. A form state request is sent with `requiresRender: true` 1. While that request is processing, another "add row" event is dispatched a. This sets `requiresRender: true` in form state b. This adds a form state request into the queue 1. The initial form state request finishes a. This sets `requiresRender: false` in form state 1. The next form state request that was queued up in 3b is sent with `requiresRender: false` a. THIS IS EXPECTED, BUT SHOULD ACTUALLY BE `true`!! To fix this this, we need to ensure that the `requiresRender` property is persisted into the second request instead of overridden. To do this, we can add a new `serverPropsToIgnore` to form state which is read when the processing results from the server. So if `requiresRender` exists in `serverPropsToIgnore`, we do not merge it. This works because we actually mutate form state in between requests. So request 2 can read the results from request 1 without going through an additional rendering cycle. Here's a play-by-play of the fix: 1. The "add row" event is dispatched a. This sets `requiresRender: true` in form state b. This adds a task in the queue to mutate form state with `requiresRender: true` 1. A form state request is sent with `requiresRender: true` 1. While that request is processing, another "add row" event is dispatched a. This sets `requiresRender: true` in form state AND `serverPropsToIgnore: [ "requiresRender" ]` c. This adds a form state request into the queue 1. The initial form state request finishes a. This returns `requiresRender: false` from the form state endpoint BUT IS IGNORED 1. The next form state request that was queued up in 3c is sent with `requiresRender: true`
887 lines
25 KiB
TypeScript
887 lines
25 KiB
TypeScript
import type {
|
|
ClientFieldSchemaMap,
|
|
Data,
|
|
DocumentPreferences,
|
|
Field,
|
|
FieldSchemaMap,
|
|
FieldState,
|
|
FlattenedBlock,
|
|
FormState,
|
|
FormStateWithoutComponents,
|
|
PayloadRequest,
|
|
SanitizedFieldPermissions,
|
|
SanitizedFieldsPermissions,
|
|
SelectMode,
|
|
SelectType,
|
|
Validate,
|
|
} from 'payload'
|
|
|
|
import ObjectIdImport from 'bson-objectid'
|
|
import { getBlockSelect } from 'payload'
|
|
import {
|
|
deepCopyObjectSimple,
|
|
fieldAffectsData,
|
|
fieldHasSubFields,
|
|
fieldIsHiddenOrDisabled,
|
|
fieldIsID,
|
|
fieldIsLocalized,
|
|
getFieldPaths,
|
|
tabHasName,
|
|
} from 'payload/shared'
|
|
|
|
import type { RenderFieldMethod } from './types.js'
|
|
|
|
import { resolveFilterOptions } from '../../utilities/resolveFilterOptions.js'
|
|
import { iterateFields } from './iterateFields.js'
|
|
|
|
const ObjectId = (ObjectIdImport.default ||
|
|
ObjectIdImport) as unknown as typeof ObjectIdImport.default
|
|
|
|
export type AddFieldStatePromiseArgs = {
|
|
addErrorPathToParent: (fieldPath: string) => void
|
|
/**
|
|
* if all parents are localized, then the field is localized
|
|
*/
|
|
anyParentLocalized?: boolean
|
|
/**
|
|
* Data of the nearest parent block, or undefined
|
|
*/
|
|
blockData: Data | undefined
|
|
clientFieldSchemaMap?: ClientFieldSchemaMap
|
|
collectionSlug?: string
|
|
data: Data
|
|
field: Field
|
|
fieldIndex: number
|
|
fieldSchemaMap: FieldSchemaMap
|
|
/**
|
|
* You can use this to filter down to only `localized` fields that require translation (type: text, textarea, etc.). Another plugin might want to look for only `point` type fields to do some GIS function. With the filter function you can go in like a surgeon.
|
|
*/
|
|
filter?: (args: AddFieldStatePromiseArgs) => boolean
|
|
/**
|
|
* Force the value of fields like arrays or blocks to be the full value instead of the length @default false
|
|
*/
|
|
forceFullValue?: boolean
|
|
fullData: Data
|
|
id: number | string
|
|
/**
|
|
* Whether the field schema should be included in the state
|
|
*/
|
|
includeSchema?: boolean
|
|
indexPath: string
|
|
/**
|
|
* Whether to omit parent fields in the state. @default false
|
|
*/
|
|
omitParents?: boolean
|
|
operation: 'create' | 'update'
|
|
parentIndexPath: string
|
|
parentPath: string
|
|
parentPermissions: SanitizedFieldsPermissions
|
|
parentSchemaPath: string
|
|
passesCondition: boolean
|
|
path: string
|
|
preferences: DocumentPreferences
|
|
previousFormState: FormState
|
|
renderAllFields: boolean
|
|
renderFieldFn: RenderFieldMethod
|
|
/**
|
|
* Req is used for validation and defaultValue calculation. If you don't need validation,
|
|
* just create your own req and pass in the locale and the user
|
|
*/
|
|
req: PayloadRequest
|
|
schemaPath: string
|
|
select?: SelectType
|
|
selectMode?: SelectMode
|
|
/**
|
|
* Whether to skip checking the field's condition. @default false
|
|
*/
|
|
skipConditionChecks?: boolean
|
|
/**
|
|
* Whether to skip validating the field. @default false
|
|
*/
|
|
skipValidation?: boolean
|
|
state: FormStateWithoutComponents
|
|
}
|
|
|
|
/**
|
|
* 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: AddFieldStatePromiseArgs): Promise<void> => {
|
|
const {
|
|
id,
|
|
addErrorPathToParent: addErrorPathToParentArg,
|
|
anyParentLocalized = false,
|
|
blockData,
|
|
clientFieldSchemaMap,
|
|
collectionSlug,
|
|
data,
|
|
field,
|
|
fieldSchemaMap,
|
|
filter,
|
|
forceFullValue = false,
|
|
fullData,
|
|
includeSchema = false,
|
|
indexPath,
|
|
omitParents = false,
|
|
operation,
|
|
parentPath,
|
|
parentPermissions,
|
|
parentSchemaPath,
|
|
passesCondition,
|
|
path,
|
|
preferences,
|
|
previousFormState,
|
|
renderAllFields,
|
|
renderFieldFn,
|
|
req,
|
|
schemaPath,
|
|
select,
|
|
selectMode,
|
|
skipConditionChecks = false,
|
|
skipValidation = false,
|
|
state,
|
|
} = args
|
|
|
|
if (!args.clientFieldSchemaMap && args.renderFieldFn) {
|
|
console.warn(
|
|
'clientFieldSchemaMap is not passed to addFieldStatePromise - this will reduce performance',
|
|
)
|
|
}
|
|
|
|
const requiresRender = renderAllFields || previousFormState?.[path]?.requiresRender
|
|
|
|
let fieldPermissions: SanitizedFieldPermissions = true
|
|
|
|
const fieldState: FieldState = {}
|
|
|
|
if (passesCondition === false) {
|
|
fieldState.passesCondition = false
|
|
}
|
|
|
|
if (includeSchema) {
|
|
fieldState.fieldSchema = field
|
|
}
|
|
|
|
if (fieldAffectsData(field) && !fieldIsHiddenOrDisabled(field)) {
|
|
fieldPermissions =
|
|
parentPermissions === true
|
|
? parentPermissions
|
|
: deepCopyObjectSimple(parentPermissions?.[field.name])
|
|
|
|
let hasPermission: boolean =
|
|
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 = field.validate
|
|
|
|
let validationResult: string | true = 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,
|
|
})
|
|
}
|
|
}
|
|
|
|
const addErrorPathToParent = (errorPath: string) => {
|
|
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, i: number) => {
|
|
const parentPath = path + '.' + i
|
|
row.id = row?.id || new ObjectId().toHexString()
|
|
|
|
if (!omitParents && (!filter || filter(args))) {
|
|
const idKey = parentPath + '.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,
|
|
omitParents,
|
|
operation,
|
|
parentIndexPath: '',
|
|
parentPassesCondition: passesCondition,
|
|
parentPath,
|
|
parentSchemaPath: schemaPath,
|
|
permissions:
|
|
fieldPermissions === true ? fieldPermissions : fieldPermissions?.fields || {},
|
|
preferences,
|
|
previousFormState,
|
|
renderAllFields: requiresRender,
|
|
renderFieldFn,
|
|
req,
|
|
select: typeof arraySelect === 'object' ? arraySelect : undefined,
|
|
selectMode,
|
|
skipConditionChecks,
|
|
skipValidation,
|
|
state,
|
|
}),
|
|
)
|
|
|
|
if (!acc.rows) {
|
|
acc.rows = []
|
|
}
|
|
|
|
acc.rows.push({
|
|
id: row.id,
|
|
})
|
|
|
|
const previousRows = previousFormState?.[path]?.rows || []
|
|
const collapsedRowIDsFromPrefs = preferences?.fields?.[path]?.collapsed
|
|
|
|
const collapsed = (() => {
|
|
// First, check if `previousFormState` has a matching row
|
|
const previousRow = previousRows.find((prevRow) => prevRow.id === row.id)
|
|
if (previousRow) {
|
|
return previousRow.collapsed ?? false
|
|
}
|
|
|
|
// If previousFormState is undefined, check preferences
|
|
if (collapsedRowIDsFromPrefs !== undefined) {
|
|
return collapsedRowIDsFromPrefs.includes(row.id) // Check if collapsed in preferences
|
|
}
|
|
|
|
// If neither exists, fallback to `field.admin.initCollapsed`
|
|
return field.admin.initCollapsed
|
|
})()
|
|
|
|
if (collapsed) {
|
|
acc.rows[acc.rows.length - 1].collapsed = collapsed
|
|
}
|
|
|
|
return acc
|
|
},
|
|
{
|
|
promises: [],
|
|
rows: undefined,
|
|
},
|
|
)
|
|
|
|
// Wait for all promises and update fields with the results
|
|
await Promise.all(promises)
|
|
|
|
if (rows) {
|
|
fieldState.rows = rows
|
|
}
|
|
|
|
fieldState.requiresRender = false
|
|
|
|
// 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] : []
|
|
|
|
const { promises, rowMetadata } = blocksValue.reduce(
|
|
(acc, row, i: number) => {
|
|
const blockTypeToMatch: string = row.blockType
|
|
|
|
const block =
|
|
req.payload.blocks[blockTypeToMatch] ??
|
|
((field.blockReferences ?? field.blocks).find(
|
|
(blockType) => typeof blockType !== 'string' && blockType.slug === blockTypeToMatch,
|
|
) as FlattenedBlock | undefined)
|
|
|
|
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 parentPath = path + '.' + i
|
|
|
|
if (block) {
|
|
row.id = row?.id || new ObjectId().toHexString()
|
|
|
|
if (!omitParents && (!filter || filter(args))) {
|
|
// Handle block `id` field
|
|
const idKey = parentPath + '.id'
|
|
|
|
state[idKey] = {
|
|
initialValue: row.id,
|
|
value: row.id,
|
|
}
|
|
|
|
if (includeSchema) {
|
|
state[idKey].fieldSchema = includeSchema
|
|
? block.fields.find((blockField) => fieldIsID(blockField))
|
|
: undefined
|
|
}
|
|
|
|
// Handle `blockType` field
|
|
const fieldKey = parentPath + '.blockType'
|
|
|
|
state[fieldKey] = {
|
|
initialValue: row.blockType,
|
|
value: row.blockType,
|
|
}
|
|
|
|
if (includeSchema) {
|
|
state[fieldKey].fieldSchema = block.fields.find(
|
|
(blockField) => 'name' in blockField && blockField.name === 'blockType',
|
|
)
|
|
}
|
|
|
|
// Handle `blockName` field
|
|
const blockNameKey = parentPath + '.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,
|
|
omitParents,
|
|
operation,
|
|
parentIndexPath: '',
|
|
parentPassesCondition: passesCondition,
|
|
parentPath,
|
|
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,
|
|
renderAllFields: requiresRender,
|
|
renderFieldFn,
|
|
req,
|
|
select: typeof blockSelect === 'object' ? blockSelect : undefined,
|
|
selectMode: blockSelectMode,
|
|
skipConditionChecks,
|
|
skipValidation,
|
|
state,
|
|
}),
|
|
)
|
|
|
|
acc.rowMetadata.push({
|
|
id: row.id,
|
|
blockType: row.blockType,
|
|
})
|
|
|
|
const collapsedRowIDs = preferences?.fields?.[path]?.collapsed
|
|
|
|
const collapsed =
|
|
collapsedRowIDs === undefined
|
|
? field.admin.initCollapsed
|
|
: collapsedRowIDs.includes(row.id)
|
|
|
|
if (collapsed) {
|
|
acc.rowMetadata[acc.rowMetadata.length - 1].collapsed = collapsed
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
// Unset requiresRender
|
|
// so it will be removed from form state
|
|
fieldState.requiresRender = false
|
|
|
|
// 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,
|
|
omitParents,
|
|
operation,
|
|
parentIndexPath: '',
|
|
parentPassesCondition: passesCondition,
|
|
parentPath: path,
|
|
parentSchemaPath: schemaPath,
|
|
permissions:
|
|
typeof fieldPermissions === 'boolean' ? fieldPermissions : fieldPermissions?.fields,
|
|
preferences,
|
|
previousFormState,
|
|
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
|
|
}
|
|
|
|
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, etc)
|
|
|
|
if (!filter || filter(args)) {
|
|
state[path] = {
|
|
disableFormData: true,
|
|
}
|
|
|
|
if (passesCondition === false) {
|
|
state[path].passesCondition = false
|
|
}
|
|
}
|
|
|
|
await iterateFields({
|
|
id,
|
|
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,
|
|
parentSchemaPath,
|
|
permissions: parentPermissions, // TODO: Verify this is correct
|
|
preferences,
|
|
previousFormState,
|
|
renderAllFields,
|
|
renderFieldFn,
|
|
req,
|
|
skipConditionChecks,
|
|
skipValidation,
|
|
state,
|
|
})
|
|
} else if (field.type === 'tabs') {
|
|
const promises = field.tabs.map((tab, tabIndex) => {
|
|
const isNamedTab = tabHasName(tab)
|
|
let tabSelect: SelectType | undefined
|
|
|
|
const {
|
|
indexPath: tabIndexPath,
|
|
path: tabPath,
|
|
schemaPath: tabSchemaPath,
|
|
} = getFieldPaths({
|
|
field: {
|
|
...tab,
|
|
type: 'tab',
|
|
},
|
|
index: tabIndex,
|
|
parentIndexPath: indexPath,
|
|
parentPath,
|
|
parentSchemaPath,
|
|
})
|
|
|
|
let childPermissions: SanitizedFieldsPermissions = undefined
|
|
|
|
if (isNamedTab) {
|
|
if (parentPermissions === true) {
|
|
childPermissions = true
|
|
} else {
|
|
const tabPermissions = parentPermissions?.[tab.name]
|
|
if (tabPermissions === true) {
|
|
childPermissions = true
|
|
} else {
|
|
childPermissions = tabPermissions?.fields
|
|
}
|
|
}
|
|
|
|
if (typeof select?.[tab.name] === 'object') {
|
|
tabSelect = select?.[tab.name] as SelectType
|
|
}
|
|
} 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 tab.admin?.condition === 'function') {
|
|
tabPassesCondition = tab.admin.condition(fullData, data, {
|
|
blockData,
|
|
path: pathSegments,
|
|
user: req.user,
|
|
})
|
|
}
|
|
|
|
if (tab?.id) {
|
|
state[tab.id] = {
|
|
passesCondition: tabPassesCondition,
|
|
}
|
|
}
|
|
|
|
return iterateFields({
|
|
id,
|
|
addErrorPathToParent: addErrorPathToParentArg,
|
|
anyParentLocalized: tab.localized || anyParentLocalized,
|
|
blockData,
|
|
clientFieldSchemaMap,
|
|
collectionSlug,
|
|
data: isNamedTab ? data?.[tab.name] || {} : data,
|
|
fields: tab.fields,
|
|
fieldSchemaMap,
|
|
filter,
|
|
forceFullValue,
|
|
fullData,
|
|
includeSchema,
|
|
omitParents,
|
|
operation,
|
|
parentIndexPath: isNamedTab ? '' : tabIndexPath,
|
|
parentPassesCondition: tabPassesCondition,
|
|
parentPath: isNamedTab ? tabPath : parentPath,
|
|
parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath,
|
|
permissions: childPermissions,
|
|
preferences,
|
|
previousFormState,
|
|
renderAllFields,
|
|
renderFieldFn,
|
|
req,
|
|
select: tabSelect,
|
|
selectMode,
|
|
skipConditionChecks,
|
|
skipValidation,
|
|
state,
|
|
})
|
|
})
|
|
|
|
await Promise.all(promises)
|
|
} else if (field.type === 'ui') {
|
|
if (!filter || filter(args)) {
|
|
state[path] = fieldState
|
|
state[path].disableFormData = true
|
|
}
|
|
}
|
|
|
|
if (requiresRender && renderFieldFn && !fieldIsHiddenOrDisabled(field)) {
|
|
const fieldState = state[path]
|
|
|
|
const fieldConfig = fieldSchemaMap.get(schemaPath)
|
|
|
|
if (!fieldConfig) {
|
|
if (schemaPath.endsWith('.blockType')) {
|
|
return
|
|
} else {
|
|
throw new Error(`Field config not found for ${schemaPath}`)
|
|
}
|
|
}
|
|
|
|
if (!fieldState) {
|
|
// Some fields (ie `Tab`) do not live in form state
|
|
// therefore we cannot attach customComponents to them
|
|
return
|
|
}
|
|
|
|
renderFieldFn({
|
|
id,
|
|
clientFieldSchemaMap,
|
|
collectionSlug,
|
|
data: fullData,
|
|
fieldConfig: fieldConfig as Field,
|
|
fieldSchemaMap,
|
|
fieldState,
|
|
formState: state,
|
|
indexPath,
|
|
operation,
|
|
parentPath,
|
|
parentSchemaPath,
|
|
path,
|
|
permissions: fieldPermissions,
|
|
preferences,
|
|
previousFieldState: previousFormState?.[path],
|
|
req,
|
|
schemaPath,
|
|
siblingData: data,
|
|
})
|
|
}
|
|
}
|