diff --git a/packages/payload/src/admin/forms/Form.ts b/packages/payload/src/admin/forms/Form.ts index 263e97214e2..cfc5531e418 100644 --- a/packages/payload/src/admin/forms/Form.ts +++ b/packages/payload/src/admin/forms/Form.ts @@ -1,7 +1,7 @@ import { type SupportedLanguages } from '@payloadcms/translations' import type { SanitizedDocumentPermissions } from '../../auth/types.js' -import type { Field, Validate } from '../../fields/config/types.js' +import type { Field, TabAsField, Validate } from '../../fields/config/types.js' import type { TypedLocale } from '../../index.js' import type { DocumentPreferences } from '../../preferences/types.js' import type { PayloadRequest, Where } from '../../types/index.js' @@ -43,7 +43,7 @@ export type FieldState = { * The fieldSchema may be part of the form state if `includeSchema: true` is passed to buildFormState. * This will never be in the form state of the client. */ - fieldSchema?: Field + fieldSchema?: Field | TabAsField filterOptions?: FilterOptionsResult initialValue?: unknown passesCondition?: boolean diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index 039c1ce2afd..5ef0fc2f067 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -1683,7 +1683,7 @@ export type FieldWithManyClient = RelationshipFieldClient | SelectFieldClient export type FieldWithMaxDepth = RelationshipField | UploadField export type FieldWithMaxDepthClient = JoinFieldClient | RelationshipFieldClient | UploadFieldClient -export function fieldHasSubFields( +export function fieldHasSubFields( field: TField, ): field is TField & (TField extends ClientField ? FieldWithSubFieldsClient : FieldWithSubFields) { return ( diff --git a/packages/payload/src/fields/getFieldPaths.ts b/packages/payload/src/fields/getFieldPaths.ts index 56dd4b9b7d5..7f493f39e1a 100644 --- a/packages/payload/src/fields/getFieldPaths.ts +++ b/packages/payload/src/fields/getFieldPaths.ts @@ -31,30 +31,6 @@ export function getFieldPaths({ parentIndexPath, parentPath, parentSchemaPath, -}: Args): FieldPaths { - if ('name' in field) { - return { - indexPath: `${parentIndexPath ? parentIndexPath + '-' : ''}${index}`, - path: `${parentPath ? parentPath + '.' : ''}${field.name}`, - schemaPath: `${parentSchemaPath ? parentSchemaPath + '.' : ''}${field.name}`, - } - } - - const indexSuffix = `_index-${`${parentIndexPath ? parentIndexPath + '-' : ''}${index}`}` - - return { - indexPath: `${parentIndexPath ? parentIndexPath + '-' : ''}${index}`, - path: `${parentPath ? parentPath + '.' : ''}${indexSuffix}`, - schemaPath: `${parentSchemaPath ? parentSchemaPath + '.' : ''}${indexSuffix}`, - } -} - -export function getFieldPathsModified({ - field, - index, - parentIndexPath, - parentPath, - parentSchemaPath, }: Args): FieldPaths { const parentPathSegments = parentPath.split('.') diff --git a/packages/payload/src/fields/hooks/afterChange/promise.ts b/packages/payload/src/fields/hooks/afterChange/promise.ts index 7c1dbe6e0f3..54f7bbcf80d 100644 --- a/packages/payload/src/fields/hooks/afterChange/promise.ts +++ b/packages/payload/src/fields/hooks/afterChange/promise.ts @@ -7,7 +7,7 @@ import type { Field, TabAsField } from '../../config/types.js' import { MissingEditorProp } from '../../../errors/index.js' import { fieldAffectsData, tabHasName } from '../../config/types.js' -import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' +import { getFieldPaths } from '../../getFieldPaths.js' import { traverseFields } from './traverseFields.js' type Args = { diff --git a/packages/payload/src/fields/hooks/afterRead/promise.ts b/packages/payload/src/fields/hooks/afterRead/promise.ts index 5a36fdcf91f..0d618c5e9a8 100644 --- a/packages/payload/src/fields/hooks/afterRead/promise.ts +++ b/packages/payload/src/fields/hooks/afterRead/promise.ts @@ -14,7 +14,7 @@ import type { Field, TabAsField } from '../../config/types.js' import { MissingEditorProp } from '../../../errors/index.js' import { fieldAffectsData, tabHasName } from '../../config/types.js' import { getDefaultValue } from '../../getDefaultValue.js' -import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' +import { getFieldPaths } from '../../getFieldPaths.js' import { relationshipPopulationPromise } from './relationshipPopulationPromise.js' import { traverseFields } from './traverseFields.js' diff --git a/packages/payload/src/fields/hooks/beforeChange/promise.ts b/packages/payload/src/fields/hooks/beforeChange/promise.ts index 1e827684fd7..228e2d391a7 100644 --- a/packages/payload/src/fields/hooks/beforeChange/promise.ts +++ b/packages/payload/src/fields/hooks/beforeChange/promise.ts @@ -11,7 +11,7 @@ import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js' import { getLabelFromPath } from '../../../utilities/getLabelFromPath.js' import { getTranslatedLabel } from '../../../utilities/getTranslatedLabel.js' import { fieldAffectsData, tabHasName } from '../../config/types.js' -import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' +import { getFieldPaths } from '../../getFieldPaths.js' import { getExistingRowDoc } from './getExistingRowDoc.js' import { traverseFields } from './traverseFields.js' diff --git a/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts b/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts index cec3cac67d8..4763e6ec9f1 100644 --- a/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts +++ b/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts @@ -4,7 +4,7 @@ import type { JsonObject, PayloadRequest } from '../../../types/index.js' import type { Field, FieldHookArgs, TabAsField } from '../../config/types.js' import { fieldAffectsData } from '../../config/types.js' -import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' +import { getFieldPaths } from '../../getFieldPaths.js' import { runBeforeDuplicateHooks } from './runHook.js' import { traverseFields } from './traverseFields.js' diff --git a/packages/payload/src/fields/hooks/beforeValidate/promise.ts b/packages/payload/src/fields/hooks/beforeValidate/promise.ts index fede4caed97..dd0ac7ab57f 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/promise.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/promise.ts @@ -8,7 +8,7 @@ import type { Field, TabAsField } from '../../config/types.js' import { MissingEditorProp } from '../../../errors/index.js' import { fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types.js' import { getDefaultValue } from '../../getDefaultValue.js' -import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' +import { getFieldPaths } from '../../getFieldPaths.js' import { cloneDataFromOriginalDoc } from '../beforeChange/cloneDataFromOriginalDoc.js' import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc.js' import { traverseFields } from './traverseFields.js' diff --git a/packages/richtext-lexical/src/utilities/generateSchemaMap.ts b/packages/richtext-lexical/src/utilities/generateSchemaMap.ts index bf088d9170b..0a01bf36f0d 100644 --- a/packages/richtext-lexical/src/utilities/generateSchemaMap.ts +++ b/packages/richtext-lexical/src/utilities/generateSchemaMap.ts @@ -32,6 +32,7 @@ export const getGenerateSchemaMap = fields: field.fields, i18n, parentIndexPath: '', + parentPath: '', parentSchemaPath: `${schemaPath}.lexical_internal_feature.${featureKey}.${schemaKey}`, schemaMap, }) diff --git a/packages/richtext-slate/src/generateSchemaMap.ts b/packages/richtext-slate/src/generateSchemaMap.ts index 7c94eb46bd4..a2f9201c014 100644 --- a/packages/richtext-slate/src/generateSchemaMap.ts +++ b/packages/richtext-slate/src/generateSchemaMap.ts @@ -34,6 +34,7 @@ export const getGenerateSchemaMap = fields: args.admin?.link?.fields as Field[], i18n, parentIndexPath: '', + parentPath: '', parentSchemaPath: `${schemaPath}.${linkFieldsSchemaPath}`, schemaMap, }) @@ -68,6 +69,7 @@ export const getGenerateSchemaMap = fields: args?.admin?.upload?.collections[collection.slug]?.fields, i18n, parentIndexPath: '', + parentPath: '', parentSchemaPath: `${schemaPath}.${uploadFieldsSchemaPath}.${collection.slug}`, schemaMap, }) diff --git a/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts b/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts index cb73740a83b..eb408b0bde9 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts @@ -10,6 +10,7 @@ import type { PayloadRequest, SanitizedFieldPermissions, SanitizedFieldsPermissions, + TabAsField, Validate, } from 'payload' @@ -21,7 +22,6 @@ import { fieldIsHiddenOrDisabled, fieldIsID, fieldIsLocalized, - getFieldPaths, tabHasName, } from 'payload/shared' @@ -42,7 +42,7 @@ export type AddFieldStatePromiseArgs = { clientFieldSchemaMap?: ClientFieldSchemaMap collectionSlug?: string data: Data - field: Field + field: Field | TabAsField fieldIndex: number fieldSchemaMap: FieldSchemaMap /** @@ -168,7 +168,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom return } - const validate: Validate = field.validate + const validate: Validate = 'validate' in field ? field.validate : undefined let validationResult: string | true = true @@ -235,12 +235,13 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom const arrayValue = Array.isArray(data[field.name]) ? data[field.name] : [] const { promises, rows } = arrayValue.reduce( - (acc, row, i: number) => { - const parentPath = path + '.' + i + (acc, row, rowIndex: number) => { + const rowPath = path + '.' + rowIndex + row.id = row?.id || new ObjectId().toHexString() if (!omitParents && (!filter || filter(args))) { - const idKey = parentPath + '.id' + const idKey = rowPath + '.id' state[idKey] = { initialValue: row.id, @@ -270,7 +271,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom operation, parentIndexPath: '', parentPassesCondition: passesCondition, - parentPath, + parentPath: rowPath, parentSchemaPath: schemaPath, permissions: fieldPermissions === true ? fieldPermissions : fieldPermissions?.fields || {}, @@ -357,22 +358,23 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom const blocksValue = Array.isArray(data[field.name]) ? data[field.name] : [] const { promises, rowMetadata } = blocksValue.reduce( - (acc, row, i: number) => { + (acc, row, rowIndex: number) => { const block = field.blocks.find((blockType) => blockType.slug === row.blockType) + 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 parentPath = path + '.' + i + const rowPath = path + '.' + rowIndex if (block) { row.id = row?.id || new ObjectId().toHexString() if (!omitParents && (!filter || filter(args))) { // Handle block `id` field - const idKey = parentPath + '.id' + const idKey = rowPath + '.id' state[idKey] = { initialValue: row.id, @@ -386,7 +388,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom } // Handle `blockType` field - const fieldKey = parentPath + '.blockType' + const fieldKey = rowPath + '.blockType' state[fieldKey] = { initialValue: row.blockType, @@ -400,7 +402,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom } // Handle `blockName` field - const blockNameKey = parentPath + '.blockName' + const blockNameKey = rowPath + '.blockName' state[blockNameKey] = {} @@ -434,7 +436,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom operation, parentIndexPath: '', parentPassesCondition: passesCondition, - parentPath, + parentPath: rowPath, parentSchemaPath: schemaPath + '.' + block.slug, permissions: fieldPermissions === true @@ -546,6 +548,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom break } + case 'relationship': case 'upload': { if (field.filterOptions) { @@ -678,8 +681,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom operation, parentIndexPath: indexPath, parentPassesCondition: passesCondition, - parentPath, - parentSchemaPath, + parentPath: path, + parentSchemaPath: schemaPath, permissions: parentPermissions, // TODO: Verify this is correct preferences, previousFormState, @@ -690,74 +693,85 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom skipValidation, state, }) - } else if (field.type === 'tabs') { - const promises = field.tabs.map((tab, tabIndex) => { - const isNamedTab = tabHasName(tab) - - 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) { + } else if (field.type === 'tab') { + const isNamedTab = tabHasName(field) + + let childPermissions: SanitizedFieldsPermissions = undefined + + if (isNamedTab) { + if (parentPermissions === true) { + childPermissions = true + } else { + const tabPermissions = parentPermissions?.[field.name] + if (tabPermissions === true) { childPermissions = true } else { - const tabPermissions = parentPermissions?.[tab.name] - if (tabPermissions === true) { - childPermissions = true - } else { - childPermissions = tabPermissions?.fields - } + childPermissions = tabPermissions?.fields } - } else { - childPermissions = parentPermissions } + } else { + childPermissions = parentPermissions + } - return iterateFields({ - id, - addErrorPathToParent: addErrorPathToParentArg, - anyParentLocalized: tab.localized || anyParentLocalized, - clientFieldSchemaMap, - collectionSlug, - data: isNamedTab ? data?.[tab.name] || {} : data, - fields: tab.fields, - fieldSchemaMap, - filter, - forceFullValue, - fullData, - includeSchema, - omitParents, - operation, - parentIndexPath: isNamedTab ? '' : tabIndexPath, - parentPassesCondition: passesCondition, - parentPath: isNamedTab ? tabPath : parentPath, - parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath, - permissions: childPermissions, - preferences, - previousFormState, - renderAllFields, - renderFieldFn, - req, - skipConditionChecks, - skipValidation, - state, - }) + return iterateFields({ + id, + addErrorPathToParent: addErrorPathToParentArg, + anyParentLocalized: fieldIsLocalized(field) || anyParentLocalized, + clientFieldSchemaMap, + collectionSlug, + data: isNamedTab ? data?.[field.name] || {} : data, + fields: field.fields, + fieldSchemaMap, + filter, + forceFullValue, + fullData, + includeSchema, + omitParents, + operation, + parentIndexPath: isNamedTab ? '' : indexPath, + parentPassesCondition: passesCondition, + parentPath: isNamedTab ? path : parentPath, + parentSchemaPath: schemaPath, + permissions: childPermissions, + preferences, + previousFormState, + renderAllFields, + renderFieldFn, + req, + skipConditionChecks, + skipValidation, + state, + }) + } else if (field.type === 'tabs') { + return iterateFields({ + id, + addErrorPathToParent: addErrorPathToParentArg, + anyParentLocalized: fieldIsLocalized(field) || anyParentLocalized, + 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, + skipConditionChecks, + skipValidation, + state, }) - - await Promise.all(promises) } else if (field.type === 'ui') { if (!filter || filter(args)) { state[path] = fieldState diff --git a/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts b/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts index c26b4909f69..1c9a95914cf 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts @@ -2,12 +2,13 @@ import type { ClientFieldSchemaMap, Data, DocumentPreferences, - Field as FieldSchema, + Field, FieldSchemaMap, FormState, FormStateWithoutComponents, PayloadRequest, SanitizedFieldsPermissions, + TabAsField, } from 'payload' import { getFieldPaths } from 'payload/shared' @@ -26,7 +27,7 @@ type Args = { clientFieldSchemaMap?: ClientFieldSchemaMap collectionSlug?: string data: Data - fields: FieldSchema[] + fields: (Field | TabAsField)[] fieldSchemaMap: FieldSchemaMap filter?: (args: AddFieldStatePromiseArgs) => boolean /** @@ -108,7 +109,7 @@ export const iterateFields = async ({ const { indexPath, path, schemaPath } = getFieldPaths({ field, index: fieldIndex, - parentIndexPath: 'name' in field ? '' : parentIndexPath, + parentIndexPath, parentPath, parentSchemaPath, }) diff --git a/packages/ui/src/utilities/buildClientFieldSchemaMap/index.ts b/packages/ui/src/utilities/buildClientFieldSchemaMap/index.ts index 31762d1784c..4b4af24f555 100644 --- a/packages/ui/src/utilities/buildClientFieldSchemaMap/index.ts +++ b/packages/ui/src/utilities/buildClientFieldSchemaMap/index.ts @@ -62,6 +62,7 @@ export const buildClientFieldSchemaMap = (args: { fields: fieldsToSet, i18n, parentIndexPath: '', + parentPath: '', parentSchemaPath: collectionSlug, payload, schemaMap, @@ -81,6 +82,7 @@ export const buildClientFieldSchemaMap = (args: { fields: matchedGlobal.fields, i18n, parentIndexPath: '', + parentPath: '', parentSchemaPath: globalSlug, payload, schemaMap, diff --git a/packages/ui/src/utilities/buildClientFieldSchemaMap/traverseFields.ts b/packages/ui/src/utilities/buildClientFieldSchemaMap/traverseFields.ts index a238e1113e3..8415c263bb7 100644 --- a/packages/ui/src/utilities/buildClientFieldSchemaMap/traverseFields.ts +++ b/packages/ui/src/utilities/buildClientFieldSchemaMap/traverseFields.ts @@ -1,22 +1,24 @@ import type { I18n } from '@payloadcms/translations' - -import { - type ClientConfig, - type ClientField, - type ClientFieldSchemaMap, - createClientFields, - type Field, - type FieldSchemaMap, - type Payload, +import type { + ClientConfig, + ClientField, + ClientFieldSchemaMap, + Field, + FieldSchemaMap, + Payload, + TabAsFieldClient, } from 'payload' + +import { createClientFields } from 'payload' import { getFieldPaths, tabHasName } from 'payload/shared' type Args = { clientSchemaMap: ClientFieldSchemaMap config: ClientConfig - fields: ClientField[] + fields: (ClientField | TabAsFieldClient)[] i18n: I18n parentIndexPath: string + parentPath: string parentSchemaPath: string payload: Payload schemaMap: FieldSchemaMap @@ -28,16 +30,17 @@ export const traverseFields = ({ fields, i18n, parentIndexPath, + parentPath, parentSchemaPath, payload, schemaMap, }: Args) => { for (const [index, field] of fields.entries()) { - const { indexPath, schemaPath } = getFieldPaths({ + const { indexPath, path, schemaPath } = getFieldPaths({ field, index, - parentIndexPath: 'name' in field ? '' : parentIndexPath, - parentPath: '', + parentIndexPath, + parentPath, parentSchemaPath, }) @@ -45,21 +48,23 @@ export const traverseFields = ({ switch (field.type) { case 'array': - case 'group': + case 'group': { traverseFields({ clientSchemaMap, config, fields: field.fields, i18n, parentIndexPath: '', + parentPath: path + '.' + 0, // mock the row index parentSchemaPath: schemaPath, payload, schemaMap, }) break + } - case 'blocks': + case 'blocks': { field.blocks.map((block) => { const blockSchemaPath = `${schemaPath}.${block.slug}` @@ -70,26 +75,31 @@ export const traverseFields = ({ fields: block.fields, i18n, parentIndexPath: '', - parentSchemaPath: blockSchemaPath, + parentPath: path + '.' + 0, // mock the row index + parentSchemaPath: schemaPath + '.' + block.slug, payload, schemaMap, }) }) break + } + case 'collapsible': - case 'row': + case 'row': { traverseFields({ clientSchemaMap, config, fields: field.fields, i18n, parentIndexPath: indexPath, - parentSchemaPath, + parentPath, + parentSchemaPath: schemaPath, payload, schemaMap, }) break + } case 'richText': { // richText sub-fields are not part of the ClientConfig or the Config. @@ -132,36 +142,39 @@ export const traverseFields = ({ break } - case 'tabs': - field.tabs.map((tab, tabIndex) => { - const isNamedTab = tabHasName(tab) - - const { indexPath: tabIndexPath, schemaPath: tabSchemaPath } = getFieldPaths({ - field: { - ...tab, - type: 'tab', - }, - index: tabIndex, - parentIndexPath: indexPath, - parentPath: '', - parentSchemaPath, - }) + case 'tab': { + const isNamedTab = tabHasName(field) - clientSchemaMap.set(tabSchemaPath, tab) + traverseFields({ + clientSchemaMap, + config, + fields: field.fields, + i18n, + parentIndexPath: isNamedTab ? '' : indexPath, + parentPath: isNamedTab ? path : parentPath, + parentSchemaPath: schemaPath, + payload, + schemaMap, + }) - traverseFields({ - clientSchemaMap, - config, - fields: tab.fields, - i18n, - parentIndexPath: isNamedTab ? '' : tabIndexPath, - parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath, - payload, - schemaMap, - }) + break + } + + case 'tabs': { + traverseFields({ + clientSchemaMap, + config, + fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), + i18n, + parentIndexPath: indexPath, + parentPath: path, + parentSchemaPath: schemaPath, + payload, + schemaMap, }) break + } } } } diff --git a/packages/ui/src/utilities/buildFieldSchemaMap/index.ts b/packages/ui/src/utilities/buildFieldSchemaMap/index.ts index 342ea59274a..4ede748bd90 100644 --- a/packages/ui/src/utilities/buildFieldSchemaMap/index.ts +++ b/packages/ui/src/utilities/buildFieldSchemaMap/index.ts @@ -56,6 +56,7 @@ export const buildFieldSchemaMap = (args: { fields: fieldsToSet, i18n, parentIndexPath: '', + parentPath: '', parentSchemaPath: collectionSlug, schemaMap, }) @@ -73,6 +74,7 @@ export const buildFieldSchemaMap = (args: { fields: matchedGlobal.fields, i18n, parentIndexPath: '', + parentPath: '', parentSchemaPath: globalSlug, schemaMap, }) diff --git a/packages/ui/src/utilities/buildFieldSchemaMap/traverseFields.ts b/packages/ui/src/utilities/buildFieldSchemaMap/traverseFields.ts index cc620332d55..4877c731d82 100644 --- a/packages/ui/src/utilities/buildFieldSchemaMap/traverseFields.ts +++ b/packages/ui/src/utilities/buildFieldSchemaMap/traverseFields.ts @@ -1,14 +1,15 @@ import type { I18n } from '@payloadcms/translations' -import type { Field, FieldSchemaMap, SanitizedConfig } from 'payload' +import type { Field, FieldSchemaMap, SanitizedConfig, TabAsField } from 'payload' import { MissingEditorProp } from 'payload' import { getFieldPaths, tabHasName } from 'payload/shared' type Args = { config: SanitizedConfig - fields: Field[] + fields: (Field | TabAsField)[] i18n: I18n parentIndexPath: string + parentPath: string parentSchemaPath: string schemaMap: FieldSchemaMap } @@ -18,15 +19,16 @@ export const traverseFields = ({ fields, i18n, parentIndexPath, + parentPath, parentSchemaPath, schemaMap, }: Args) => { for (const [index, field] of fields.entries()) { - const { indexPath, schemaPath } = getFieldPaths({ + const { indexPath, path, schemaPath } = getFieldPaths({ field, index, - parentIndexPath: 'name' in field ? '' : parentIndexPath, - parentPath: '', + parentIndexPath, + parentPath, parentSchemaPath, }) @@ -40,13 +42,14 @@ export const traverseFields = ({ fields: field.fields, i18n, parentIndexPath: '', + parentPath: path + '.' + 0, // mock the row index parentSchemaPath: schemaPath, schemaMap, }) break - case 'blocks': + case 'blocks': { field.blocks.map((block) => { const blockSchemaPath = `${schemaPath}.${block.slug}` @@ -56,12 +59,15 @@ export const traverseFields = ({ fields: block.fields, i18n, parentIndexPath: '', - parentSchemaPath: blockSchemaPath, + parentPath: path + '.' + 0, // mock the row index + parentSchemaPath: schemaPath + '.' + block.slug, schemaMap, }) }) break + } + case 'collapsible': case 'row': traverseFields({ @@ -69,13 +75,14 @@ export const traverseFields = ({ fields: field.fields, i18n, parentIndexPath: indexPath, - parentSchemaPath, + parentPath, + parentSchemaPath: schemaPath, schemaMap, }) break - case 'richText': + 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 } @@ -95,35 +102,37 @@ export const traverseFields = ({ } break + } - case 'tabs': - field.tabs.map((tab, tabIndex) => { - const isNamedTab = tabHasName(tab) - - const { indexPath: tabIndexPath, schemaPath: tabSchemaPath } = getFieldPaths({ - field: { - ...tab, - type: 'tab', - }, - index: tabIndex, - parentIndexPath: indexPath, - parentPath: '', - parentSchemaPath, - }) + case 'tab': { + const isNamedTab = tabHasName(field) - schemaMap.set(tabSchemaPath, tab) + traverseFields({ + config, + fields: field.fields, + i18n, + parentIndexPath: isNamedTab ? '' : indexPath, + parentPath: isNamedTab ? path : parentPath, + parentSchemaPath: schemaPath, + schemaMap, + }) - traverseFields({ - config, - fields: tab.fields, - i18n, - parentIndexPath: isNamedTab ? '' : tabIndexPath, - parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath, - schemaMap, - }) + break + } + + case 'tabs': { + traverseFields({ + config, + fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), + i18n, + parentIndexPath: indexPath, + parentPath: path, + parentSchemaPath: schemaPath, + schemaMap, }) break + } } } } diff --git a/packages/ui/src/utilities/buildFormState.ts b/packages/ui/src/utilities/buildFormState.ts index 0b573b03d14..5c075e09a79 100644 --- a/packages/ui/src/utilities/buildFormState.ts +++ b/packages/ui/src/utilities/buildFormState.ts @@ -131,6 +131,8 @@ export const buildFormState = async ( i18n, }) + console.log('schemaMap', schemaMap) + const clientSchemaMap = getClientSchemaMap({ collectionSlug, config: getClientConfig({ config, i18n, importMap: req.payload.importMap }), diff --git a/test/hooks/collections/FieldPaths/index.ts b/test/field-paths/collections/FieldPaths/index.ts similarity index 100% rename from test/hooks/collections/FieldPaths/index.ts rename to test/field-paths/collections/FieldPaths/index.ts diff --git a/test/field-paths/config.ts b/test/field-paths/config.ts new file mode 100644 index 00000000000..c8833ba43f8 --- /dev/null +++ b/test/field-paths/config.ts @@ -0,0 +1,22 @@ +import { fileURLToPath } from 'node:url' +import path from 'path' +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) +import type { SanitizedConfig } from 'payload' + +import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' +import { FieldPaths } from './collections/FieldPaths/index.js' + +export const HooksConfig: Promise = buildConfigWithDefaults({ + admin: { + importMap: { + baseDir: path.resolve(dirname), + }, + }, + collections: [FieldPaths], + typescript: { + outputFile: path.resolve(dirname, 'payload-types.ts'), + }, +}) + +export default HooksConfig diff --git a/test/field-paths/e2e.spec.ts b/test/field-paths/e2e.spec.ts new file mode 100644 index 00000000000..7782ac6504b --- /dev/null +++ b/test/field-paths/e2e.spec.ts @@ -0,0 +1,49 @@ +import type { Page } from '@playwright/test' + +import { test } from '@playwright/test' +import path from 'path' +import { fileURLToPath } from 'url' + +import type { PayloadTestSDK } from '../helpers/sdk/index.js' +import type { Config } from './payload-types.js' + +import { ensureCompilationIsDone, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js' +import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' +import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' +import { TEST_TIMEOUT_LONG } from '../playwright.config.js' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +const { beforeAll, beforeEach, describe } = test + +let payload: PayloadTestSDK + +describe('Field Paths', () => { + let url: AdminUrlUtil + let beforeChangeURL: AdminUrlUtil + let page: Page + let serverURL: string + + beforeAll(async ({ browser }, testInfo) => { + testInfo.setTimeout(TEST_TIMEOUT_LONG) + ;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname })) + + url = new AdminUrlUtil(serverURL, 'before-validate') + beforeChangeURL = new AdminUrlUtil(serverURL, 'before-change-hooks') + + const context = await browser.newContext() + page = await context.newPage() + + initPageConsoleErrorCatch(page) + await ensureCompilationIsDone({ page, serverURL }) + }) + + beforeEach(async () => { + await ensureCompilationIsDone({ page, serverURL }) + }) + + test('should replace value with before validate response', async () => { + expect(true).toBe(true) + }) +}) diff --git a/test/field-paths/eslint.config.js b/test/field-paths/eslint.config.js new file mode 100644 index 00000000000..327c58a8e06 --- /dev/null +++ b/test/field-paths/eslint.config.js @@ -0,0 +1,19 @@ +import { rootParserOptions } from '../../eslint.config.js' +import testEslintConfig from '../eslint.config.js' + +/** @typedef {import('eslint').Linter.Config} Config */ + +/** @type {Config[]} */ +export const index = [ + ...testEslintConfig, + { + languageOptions: { + parserOptions: { + ...rootParserOptions, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +] + +export default index diff --git a/test/field-paths/int.spec.ts b/test/field-paths/int.spec.ts new file mode 100644 index 00000000000..1b6d25ef5da --- /dev/null +++ b/test/field-paths/int.spec.ts @@ -0,0 +1,177 @@ +import type { Payload, SanitizedConfig } from 'payload' + +import path from 'path' +import { fileURLToPath } from 'url' + +import { buildFieldSchemaMap } from '../../packages/ui/src/utilities/buildFieldSchemaMap/index.js' + +import { initPayloadInt } from '../helpers/initPayloadInt.js' +import { fieldPathsSlug } from './shared.js' +import { initI18n } from '@payloadcms/translations' + +let payload: Payload +let config: SanitizedConfig + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +describe('Field Paths', () => { + beforeAll(async () => { + const initResult = await initPayloadInt(dirname) + config = initResult.config + payload = initResult.payload + }) + + afterAll(async () => { + if (typeof payload.db.destroy === 'function') { + await payload.db.destroy() + } + }) + + describe('hooks', () => { + it('should pass correct field paths through field hooks', async () => { + const formatExpectedFieldPaths = ( + fieldIdentifier: string, + { + path, + schemaPath, + }: { + path: string[] + schemaPath: string[] + }, + ) => ({ + [`${fieldIdentifier}_beforeValidate_FieldPaths`]: { + path, + schemaPath, + }, + [`${fieldIdentifier}_beforeChange_FieldPaths`]: { + path, + schemaPath, + }, + [`${fieldIdentifier}_afterRead_FieldPaths`]: { + path, + schemaPath, + }, + [`${fieldIdentifier}_beforeDuplicate_FieldPaths`]: { + path, + schemaPath, + }, + }) + + const originalDoc = await payload.create({ + collection: fieldPathsSlug, + data: { + topLevelNamedField: 'Test', + array: [ + { + fieldWithinArray: 'Test', + nestedArray: [ + { + fieldWithinNestedArray: 'Test', + fieldWithinNestedRow: 'Test', + }, + ], + }, + ], + fieldWithinRow: 'Test', + fieldWithinUnnamedTab: 'Test', + namedTab: { + fieldWithinNamedTab: 'Test', + }, + fieldWithinNestedUnnamedTab: 'Test', + }, + }) + + // duplicate the doc to ensure that the beforeDuplicate hook is run + const doc = await payload.duplicate({ + id: originalDoc.id, + collection: fieldPathsSlug, + }) + + expect(doc).toMatchObject({ + ...formatExpectedFieldPaths('topLevelNamedField', { + path: ['topLevelNamedField'], + schemaPath: ['topLevelNamedField'], + }), + ...formatExpectedFieldPaths('fieldWithinArray', { + path: ['array', '0', 'fieldWithinArray'], + schemaPath: ['array', 'fieldWithinArray'], + }), + ...formatExpectedFieldPaths('fieldWithinNestedArray', { + path: ['array', '0', 'nestedArray', '0', 'fieldWithinNestedArray'], + schemaPath: ['array', 'nestedArray', 'fieldWithinNestedArray'], + }), + ...formatExpectedFieldPaths('fieldWithinRowWithinArray', { + path: ['array', '0', 'fieldWithinRowWithinArray'], + schemaPath: ['array', '_index-2', 'fieldWithinRowWithinArray'], + }), + ...formatExpectedFieldPaths('fieldWithinRow', { + path: ['fieldWithinRow'], + schemaPath: ['_index-2', 'fieldWithinRow'], + }), + ...formatExpectedFieldPaths('fieldWithinUnnamedTab', { + path: ['fieldWithinUnnamedTab'], + schemaPath: ['_index-3-0', 'fieldWithinUnnamedTab'], + }), + ...formatExpectedFieldPaths('fieldWithinNestedUnnamedTab', { + path: ['fieldWithinNestedUnnamedTab'], + schemaPath: ['_index-3-0-1-0', 'fieldWithinNestedUnnamedTab'], + }), + ...formatExpectedFieldPaths('fieldWithinNamedTab', { + path: ['namedTab', 'fieldWithinNamedTab'], + schemaPath: ['_index-3', 'namedTab', 'fieldWithinNamedTab'], + }), + }) + }) + }) + + describe('field schema map', () => { + it('should build a field schema map with correct field paths', async () => { + const i18n = await initI18n({ + config: config.i18n, + context: 'client', + language: 'en', + }) + + const { fieldSchemaMap } = await buildFieldSchemaMap({ + collectionSlug: fieldPathsSlug, + config, + i18n, + }) + + const fieldSchemaKeys: string[] = [] + + fieldSchemaMap.forEach((value, key) => { + if (key === fieldPathsSlug || key.endsWith('_FieldPaths')) { + return + } + + fieldSchemaKeys.push(key.replace(`${fieldPathsSlug}.`, '')) + }) + + expect(fieldSchemaKeys).toEqual([ + 'topLevelNamedField', + 'array', + 'array.fieldWithinArray', + 'array.nestedArray', + 'array.nestedArray.fieldWithinNestedArray', + 'array.nestedArray.id', + 'array._index-2', + 'array._index-2.fieldWithinRowWithinArray', // THIS ONE IS WRONG! + 'array.id', + '_index-2', + '_index-2.fieldWithinRow', // THIS ONE IS WRONG! + '_index-3', + '_index-3-0', + '_index-3-0.fieldWithinUnnamedTab', // THIS ONE IS WRONG! + '_index-3-0-1', + '_index-3-0-1-0', + '_index-3-0-1-0.fieldWithinNestedUnnamedTab', // THIS ONE IS WRONG + '_index-3.namedTab', // THIS ONE IS WRONG! + '_index-3.namedTab.fieldWithinNamedTab', // THIS ONE IS WRONG! + 'updatedAt', + 'createdAt', + ]) + }) + }) +}) diff --git a/test/field-paths/payload-types.ts b/test/field-paths/payload-types.ts new file mode 100644 index 00000000000..fcce0f4b9b4 --- /dev/null +++ b/test/field-paths/payload-types.ts @@ -0,0 +1,572 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * This file was automatically generated by Payload. + * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, + * and re-run `payload generate:types` to regenerate this file. + */ + +export interface Config { + auth: { + users: UserAuthOperations; + }; + collections: { + 'field-paths': FieldPath; + users: User; + 'payload-locked-documents': PayloadLockedDocument; + 'payload-preferences': PayloadPreference; + 'payload-migrations': PayloadMigration; + }; + collectionsJoins: {}; + collectionsSelect: { + 'field-paths': FieldPathsSelect | FieldPathsSelect; + users: UsersSelect | UsersSelect; + 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; + 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; + 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; + }; + db: { + defaultIDType: string; + }; + globals: {}; + globalsSelect: {}; + locale: null; + user: User & { + collection: 'users'; + }; + jobs: { + tasks: unknown; + workflows: unknown; + }; +} +export interface UserAuthOperations { + forgotPassword: { + email: string; + password: string; + }; + login: { + email: string; + password: string; + }; + registerFirstUser: { + email: string; + password: string; + }; + unlock: { + email: string; + password: string; + }; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "field-paths". + */ +export interface FieldPath { + id: string; + topLevelNamedField?: string | null; + array?: + | { + fieldWithinArray?: string | null; + nestedArray?: + | { + fieldWithinNestedArray?: string | null; + id?: string | null; + }[] + | null; + fieldWithinRowWithinArray?: string | null; + id?: string | null; + }[] + | null; + fieldWithinRow?: string | null; + fieldWithinUnnamedTab?: string | null; + fieldWithinNestedUnnamedTab?: string | null; + namedTab?: { + fieldWithinNamedTab?: string | null; + }; + topLevelNamedField_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + topLevelNamedField_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + topLevelNamedField_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + topLevelNamedField_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinArray_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinArray_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinArray_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinArray_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedArray_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedArray_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedArray_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedArray_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinRowWithinArray_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinRowWithinArray_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinRowWithinArray_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinRowWithinArray_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinRow_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinRow_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinRow_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinRow_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinUnnamedTab_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinUnnamedTab_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinUnnamedTab_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinUnnamedTab_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedUnnamedTab_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedUnnamedTab_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedUnnamedTab_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedUnnamedTab_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNamedTab_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNamedTab_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNamedTab_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNamedTab_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "users". + */ +export interface User { + id: string; + updatedAt: string; + createdAt: string; + email: string; + resetPasswordToken?: string | null; + resetPasswordExpiration?: string | null; + salt?: string | null; + hash?: string | null; + loginAttempts?: number | null; + lockUntil?: string | null; + password?: string | null; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-locked-documents". + */ +export interface PayloadLockedDocument { + id: string; + document?: + | ({ + relationTo: 'field-paths'; + value: string | FieldPath; + } | null) + | ({ + relationTo: 'users'; + value: string | User; + } | null); + globalSlug?: string | null; + user: { + relationTo: 'users'; + value: string | User; + }; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-preferences". + */ +export interface PayloadPreference { + id: string; + user: { + relationTo: 'users'; + value: string | User; + }; + key?: string | null; + value?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-migrations". + */ +export interface PayloadMigration { + id: string; + name?: string | null; + batch?: number | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "field-paths_select". + */ +export interface FieldPathsSelect { + topLevelNamedField?: T; + array?: + | T + | { + fieldWithinArray?: T; + nestedArray?: + | T + | { + fieldWithinNestedArray?: T; + id?: T; + }; + fieldWithinRowWithinArray?: T; + id?: T; + }; + fieldWithinRow?: T; + fieldWithinUnnamedTab?: T; + fieldWithinNestedUnnamedTab?: T; + namedTab?: + | T + | { + fieldWithinNamedTab?: T; + }; + topLevelNamedField_beforeValidate_FieldPaths?: T; + topLevelNamedField_beforeChange_FieldPaths?: T; + topLevelNamedField_afterRead_FieldPaths?: T; + topLevelNamedField_beforeDuplicate_FieldPaths?: T; + fieldWithinArray_beforeValidate_FieldPaths?: T; + fieldWithinArray_beforeChange_FieldPaths?: T; + fieldWithinArray_afterRead_FieldPaths?: T; + fieldWithinArray_beforeDuplicate_FieldPaths?: T; + fieldWithinNestedArray_beforeValidate_FieldPaths?: T; + fieldWithinNestedArray_beforeChange_FieldPaths?: T; + fieldWithinNestedArray_afterRead_FieldPaths?: T; + fieldWithinNestedArray_beforeDuplicate_FieldPaths?: T; + fieldWithinRowWithinArray_beforeValidate_FieldPaths?: T; + fieldWithinRowWithinArray_beforeChange_FieldPaths?: T; + fieldWithinRowWithinArray_afterRead_FieldPaths?: T; + fieldWithinRowWithinArray_beforeDuplicate_FieldPaths?: T; + fieldWithinRow_beforeValidate_FieldPaths?: T; + fieldWithinRow_beforeChange_FieldPaths?: T; + fieldWithinRow_afterRead_FieldPaths?: T; + fieldWithinRow_beforeDuplicate_FieldPaths?: T; + fieldWithinUnnamedTab_beforeValidate_FieldPaths?: T; + fieldWithinUnnamedTab_beforeChange_FieldPaths?: T; + fieldWithinUnnamedTab_afterRead_FieldPaths?: T; + fieldWithinUnnamedTab_beforeDuplicate_FieldPaths?: T; + fieldWithinNestedUnnamedTab_beforeValidate_FieldPaths?: T; + fieldWithinNestedUnnamedTab_beforeChange_FieldPaths?: T; + fieldWithinNestedUnnamedTab_afterRead_FieldPaths?: T; + fieldWithinNestedUnnamedTab_beforeDuplicate_FieldPaths?: T; + fieldWithinNamedTab_beforeValidate_FieldPaths?: T; + fieldWithinNamedTab_beforeChange_FieldPaths?: T; + fieldWithinNamedTab_afterRead_FieldPaths?: T; + fieldWithinNamedTab_beforeDuplicate_FieldPaths?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "users_select". + */ +export interface UsersSelect { + updatedAt?: T; + createdAt?: T; + email?: T; + resetPasswordToken?: T; + resetPasswordExpiration?: T; + salt?: T; + hash?: T; + loginAttempts?: T; + lockUntil?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-locked-documents_select". + */ +export interface PayloadLockedDocumentsSelect { + document?: T; + globalSlug?: T; + user?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-preferences_select". + */ +export interface PayloadPreferencesSelect { + user?: T; + key?: T; + value?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "payload-migrations_select". + */ +export interface PayloadMigrationsSelect { + name?: T; + batch?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "auth". + */ +export interface Auth { + [k: string]: unknown; +} + + +declare module 'payload' { + // @ts-ignore + export interface GeneratedTypes extends Config {} +} \ No newline at end of file diff --git a/test/field-paths/shared.ts b/test/field-paths/shared.ts new file mode 100644 index 00000000000..141f1aca639 --- /dev/null +++ b/test/field-paths/shared.ts @@ -0,0 +1,2 @@ +export const beforeValidateSlug = 'before-validate' +export const fieldPathsSlug = 'field-paths' diff --git a/test/field-paths/tsconfig.eslint.json b/test/field-paths/tsconfig.eslint.json new file mode 100644 index 00000000000..b34cc7afbb8 --- /dev/null +++ b/test/field-paths/tsconfig.eslint.json @@ -0,0 +1,13 @@ +{ + // extend your base config to share compilerOptions, etc + //"extends": "./tsconfig.json", + "compilerOptions": { + // ensure that nobody can accidentally use this config for a build + "noEmit": true + }, + "include": [ + // whatever paths you intend to lint + "./**/*.ts", + "./**/*.tsx" + ] +} diff --git a/test/field-paths/tsconfig.json b/test/field-paths/tsconfig.json new file mode 100644 index 00000000000..3c43903cfdd --- /dev/null +++ b/test/field-paths/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} diff --git a/test/hooks/config.ts b/test/hooks/config.ts index a3247abcaea..59f617f2818 100644 --- a/test/hooks/config.ts +++ b/test/hooks/config.ts @@ -13,7 +13,6 @@ import { BeforeValidateCollection } from './collections/BeforeValidate/index.js' import ChainingHooks from './collections/ChainingHooks/index.js' import ContextHooks from './collections/ContextHooks/index.js' import { DataHooks } from './collections/Data/index.js' -import { FieldPaths } from './collections/FieldPaths/index.js' import Hooks, { hooksSlug } from './collections/Hook/index.js' import NestedAfterReadHooks from './collections/NestedAfterReadHooks/index.js' import Relations from './collections/Relations/index.js' @@ -39,7 +38,6 @@ export const HooksConfig: Promise = buildConfigWithDefaults({ Relations, Users, DataHooks, - FieldPaths, ], globals: [DataHooksGlobal], endpoints: [ diff --git a/test/hooks/int.spec.ts b/test/hooks/int.spec.ts index 8d216098700..049c535311c 100644 --- a/test/hooks/int.spec.ts +++ b/test/hooks/int.spec.ts @@ -517,101 +517,6 @@ describe('Hooks', () => { expect(doc.field_globalAndField).toStrictEqual(globalAndFieldString + globalAndFieldString) }) - - it('should pass correct field paths through field hooks', async () => { - const formatExpectedFieldPaths = ( - fieldIdentifier: string, - { - path, - schemaPath, - }: { - path: string[] - schemaPath: string[] - }, - ) => ({ - [`${fieldIdentifier}_beforeValidate_FieldPaths`]: { - path, - schemaPath, - }, - [`${fieldIdentifier}_beforeChange_FieldPaths`]: { - path, - schemaPath, - }, - [`${fieldIdentifier}_afterRead_FieldPaths`]: { - path, - schemaPath, - }, - [`${fieldIdentifier}_beforeDuplicate_FieldPaths`]: { - path, - schemaPath, - }, - }) - - const originalDoc = await payload.create({ - collection: fieldPathsSlug, - data: { - topLevelNamedField: 'Test', - array: [ - { - fieldWithinArray: 'Test', - nestedArray: [ - { - fieldWithinNestedArray: 'Test', - fieldWithinNestedRow: 'Test', - }, - ], - }, - ], - fieldWithinRow: 'Test', - fieldWithinUnnamedTab: 'Test', - namedTab: { - fieldWithinNamedTab: 'Test', - }, - fieldWithinNestedUnnamedTab: 'Test', - }, - }) - - // duplicate the doc to ensure that the beforeDuplicate hook is run - const doc = await payload.duplicate({ - id: originalDoc.id, - collection: fieldPathsSlug, - }) - - expect(doc).toMatchObject({ - ...formatExpectedFieldPaths('topLevelNamedField', { - path: ['topLevelNamedField'], - schemaPath: ['topLevelNamedField'], - }), - ...formatExpectedFieldPaths('fieldWithinArray', { - path: ['array', '0', 'fieldWithinArray'], - schemaPath: ['array', 'fieldWithinArray'], - }), - ...formatExpectedFieldPaths('fieldWithinNestedArray', { - path: ['array', '0', 'nestedArray', '0', 'fieldWithinNestedArray'], - schemaPath: ['array', 'nestedArray', 'fieldWithinNestedArray'], - }), - ...formatExpectedFieldPaths('fieldWithinRowWithinArray', { - path: ['array', '0', 'fieldWithinRowWithinArray'], - schemaPath: ['array', '_index-2', 'fieldWithinRowWithinArray'], - }), - ...formatExpectedFieldPaths('fieldWithinRow', { - path: ['fieldWithinRow'], - schemaPath: ['_index-2', 'fieldWithinRow'], - }), - ...formatExpectedFieldPaths('fieldWithinUnnamedTab', { - path: ['fieldWithinUnnamedTab'], - schemaPath: ['_index-3-0', 'fieldWithinUnnamedTab'], - }), - ...formatExpectedFieldPaths('fieldWithinNestedUnnamedTab', { - path: ['fieldWithinNestedUnnamedTab'], - schemaPath: ['_index-3-0-1-0', 'fieldWithinNestedUnnamedTab'], - }), - ...formatExpectedFieldPaths('fieldWithinNamedTab', { - path: ['namedTab', 'fieldWithinNamedTab'], - schemaPath: ['_index-3', 'namedTab', 'fieldWithinNamedTab'], - }), - }) - }) }) describe('config level after error hook', () => { diff --git a/test/hooks/payload-types.ts b/test/hooks/payload-types.ts index 73c4857ba2a..7427e2bb24f 100644 --- a/test/hooks/payload-types.ts +++ b/test/hooks/payload-types.ts @@ -22,7 +22,6 @@ export interface Config { relations: Relation; 'hooks-users': HooksUser; 'data-hooks': DataHook; - 'field-paths': FieldPath; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; 'payload-migrations': PayloadMigration; @@ -40,7 +39,6 @@ export interface Config { relations: RelationsSelect | RelationsSelect; 'hooks-users': HooksUsersSelect | HooksUsersSelect; 'data-hooks': DataHooksSelect | DataHooksSelect; - 'field-paths': FieldPathsSelect | FieldPathsSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; @@ -238,286 +236,6 @@ export interface DataHook { updatedAt: string; createdAt: string; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "field-paths". - */ -export interface FieldPath { - id: string; - topLevelNamedField?: string | null; - array?: - | { - fieldWithinArray?: string | null; - nestedArray?: - | { - fieldWithinNestedArray?: string | null; - id?: string | null; - }[] - | null; - id?: string | null; - }[] - | null; - fieldWithinRow?: string | null; - fieldWithinUnnamedTab?: string | null; - fieldWithinNestedUnnamedTab?: string | null; - namedTab?: { - fieldWithinNamedTab?: string | null; - }; - topLevelNamedField_beforeValidate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - topLevelNamedField_beforeChange_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - topLevelNamedField_afterRead_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - topLevelNamedField_beforeDuplicate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinArray_beforeValidate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinArray_beforeChange_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinArray_afterRead_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinArray_beforeDuplicate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinNestedArray_beforeValidate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinNestedArray_beforeChange_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinNestedArray_afterRead_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinNestedArray_beforeDuplicate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinRow_beforeValidate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinRow_beforeChange_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinRow_afterRead_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinRow_beforeDuplicate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinUnnamedTab_beforeValidate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinUnnamedTab_beforeChange_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinUnnamedTab_afterRead_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinUnnamedTab_beforeDuplicate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinNestedUnnamedTab_beforeValidate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinNestedUnnamedTab_beforeChange_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinNestedUnnamedTab_afterRead_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinNestedUnnamedTab_beforeDuplicate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinNamedTab_beforeValidate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinNamedTab_beforeChange_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinNamedTab_afterRead_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - fieldWithinNamedTab_beforeDuplicate_FieldPaths?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - updatedAt: string; - createdAt: string; -} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents". @@ -568,10 +286,6 @@ export interface PayloadLockedDocument { | ({ relationTo: 'data-hooks'; value: string | DataHook; - } | null) - | ({ - relationTo: 'field-paths'; - value: string | FieldPath; } | null); globalSlug?: string | null; user: { @@ -756,63 +470,6 @@ export interface DataHooksSelect { updatedAt?: T; createdAt?: T; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "field-paths_select". - */ -export interface FieldPathsSelect { - topLevelNamedField?: T; - array?: - | T - | { - fieldWithinArray?: T; - nestedArray?: - | T - | { - fieldWithinNestedArray?: T; - id?: T; - }; - id?: T; - }; - fieldWithinRow?: T; - fieldWithinUnnamedTab?: T; - fieldWithinNestedUnnamedTab?: T; - namedTab?: - | T - | { - fieldWithinNamedTab?: T; - }; - topLevelNamedField_beforeValidate_FieldPaths?: T; - topLevelNamedField_beforeChange_FieldPaths?: T; - topLevelNamedField_afterRead_FieldPaths?: T; - topLevelNamedField_beforeDuplicate_FieldPaths?: T; - fieldWithinArray_beforeValidate_FieldPaths?: T; - fieldWithinArray_beforeChange_FieldPaths?: T; - fieldWithinArray_afterRead_FieldPaths?: T; - fieldWithinArray_beforeDuplicate_FieldPaths?: T; - fieldWithinNestedArray_beforeValidate_FieldPaths?: T; - fieldWithinNestedArray_beforeChange_FieldPaths?: T; - fieldWithinNestedArray_afterRead_FieldPaths?: T; - fieldWithinNestedArray_beforeDuplicate_FieldPaths?: T; - fieldWithinRow_beforeValidate_FieldPaths?: T; - fieldWithinRow_beforeChange_FieldPaths?: T; - fieldWithinRow_afterRead_FieldPaths?: T; - fieldWithinRow_beforeDuplicate_FieldPaths?: T; - fieldWithinUnnamedTab_beforeValidate_FieldPaths?: T; - fieldWithinUnnamedTab_beforeChange_FieldPaths?: T; - fieldWithinUnnamedTab_afterRead_FieldPaths?: T; - fieldWithinUnnamedTab_beforeDuplicate_FieldPaths?: T; - fieldWithinNestedUnnamedTab_beforeValidate_FieldPaths?: T; - fieldWithinNestedUnnamedTab_beforeChange_FieldPaths?: T; - fieldWithinNestedUnnamedTab_afterRead_FieldPaths?: T; - fieldWithinNestedUnnamedTab_beforeDuplicate_FieldPaths?: T; - fieldWithinNamedTab_beforeValidate_FieldPaths?: T; - fieldWithinNamedTab_beforeChange_FieldPaths?: T; - fieldWithinNamedTab_afterRead_FieldPaths?: T; - fieldWithinNamedTab_beforeDuplicate_FieldPaths?: T; - updatedAt?: T; - createdAt?: T; -} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents_select". diff --git a/tsconfig.base.json b/tsconfig.base.json index 35007f8ac1c..06a27a30e18 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,7 +31,7 @@ } ], "paths": { - "@payload-config": ["./test/versions/config.ts"], + "@payload-config": ["./test/hooks/config.ts"], "@payloadcms/live-preview": ["./packages/live-preview/src"], "@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"], "@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],