diff --git a/jest.config.js b/jest.config.js index 39283f8ad..defee6f01 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,6 +13,9 @@ const esModules = [ 'path-exists', 'qs-esm', 'uint8array-extras', + '@faceless-ui/window-info', + '@faceless-ui/modal', + '@faceless-ui/scroll-info', ].join('|') import path from 'path' diff --git a/packages/payload/src/admin/forms/Form.ts b/packages/payload/src/admin/forms/Form.ts index 7ed86c105..94242f093 100644 --- a/packages/payload/src/admin/forms/Form.ts +++ b/packages/payload/src/admin/forms/Form.ts @@ -4,7 +4,7 @@ import type { SanitizedDocumentPermissions } from '../../auth/types.js' import type { Field, 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' +import type { PayloadRequest, SelectType, Where } from '../../types/index.js' export type Data = { [key: string]: any @@ -91,6 +91,7 @@ export type BuildFormStateArgs = { req: PayloadRequest returnLockStatus?: boolean schemaPath: string + select?: SelectType skipValidation?: boolean updateLastEdited?: boolean } & ( diff --git a/packages/payload/src/fields/hooks/afterRead/promise.ts b/packages/payload/src/fields/hooks/afterRead/promise.ts index 11abfa439..fd2b0d144 100644 --- a/packages/payload/src/fields/hooks/afterRead/promise.ts +++ b/packages/payload/src/fields/hooks/afterRead/promise.ts @@ -13,6 +13,8 @@ import type { import type { Block, Field, TabAsField } from '../../config/types.js' import { MissingEditorProp } from '../../../errors/index.js' +import { getBlockSelect } from '../../../utilities/getBlockSelect.js' +import { stripUnselectedFields } from '../../../utilities/stripUnselectedFields.js' import { fieldAffectsData, fieldShouldBeLocalized, tabHasName } from '../../config/types.js' import { getDefaultValue } from '../../getDefaultValue.js' import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' @@ -122,20 +124,16 @@ export const promise = async ({ delete siblingDoc[field.name] } - // Strip unselected fields - if (fieldAffectsData(field) && select && selectMode && path !== 'id') { - if (selectMode === 'include') { - if (!select[field.name]) { - delete siblingDoc[field.name] - return - } - } + if (path !== 'id') { + const shouldContinue = stripUnselectedFields({ + field, + select, + selectMode, + siblingDoc, + }) - if (selectMode === 'exclude') { - if (select[field.name] === false) { - delete siblingDoc[field.name] - return - } + if (!shouldContinue) { + return } } @@ -454,8 +452,6 @@ export const promise = async ({ case 'blocks': { const rows = siblingDoc[field.name] - let blocksSelect = select?.[field.name] - if (Array.isArray(rows)) { rows.forEach((row, rowIndex) => { const blockTypeToMatch = (row as JsonObject).blockType @@ -466,37 +462,11 @@ export const promise = async ({ (curBlock) => typeof curBlock !== 'string' && curBlock.slug === blockTypeToMatch, ) as Block | undefined) - let blockSelectMode = selectMode - - if (typeof blocksSelect === 'object') { - blocksSelect = { - ...blocksSelect, - } - - // sanitize blocks: {cta: false} to blocks: {cta: {id: true, blockType: true}} - if (selectMode === 'exclude' && blocksSelect[block.slug] === false) { - blockSelectMode = 'include' - blocksSelect[block.slug] = { - id: true, - blockType: true, - } - } else if (selectMode === 'include') { - if (!blocksSelect[block.slug]) { - blocksSelect[block.slug] = {} - } - - if (typeof blocksSelect[block.slug] === 'object') { - blocksSelect[block.slug] = { - ...(blocksSelect[block.slug] as object), - } - - blocksSelect[block.slug]['id'] = true - blocksSelect[block.slug]['blockType'] = true - } - } - } - - const blockSelect = blocksSelect?.[block.slug] + const { blockSelect, blockSelectMode } = getBlockSelect({ + block, + select: select?.[field.name], + selectMode, + }) if (block) { traverseFields({ diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 6ec77d618..d26c0b95f 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -1458,6 +1458,7 @@ export { flattenAllFields } from './utilities/flattenAllFields.js' export { default as flattenTopLevelFields } from './utilities/flattenTopLevelFields.js' export { formatErrors } from './utilities/formatErrors.js' export { formatLabels, formatNames, toWords } from './utilities/formatLabels.js' +export { getBlockSelect } from './utilities/getBlockSelect.js' export { getCollectionIDFieldTypes } from './utilities/getCollectionIDFieldTypes.js' export { getObjectDotNotation } from './utilities/getObjectDotNotation.js' export { getRequestLanguage } from './utilities/getRequestLanguage.js' @@ -1477,6 +1478,7 @@ export { sanitizeFallbackLocale } from './utilities/sanitizeFallbackLocale.js' export { sanitizeJoinParams } from './utilities/sanitizeJoinParams.js' export { sanitizePopulateParam } from './utilities/sanitizePopulateParam.js' export { sanitizeSelectParam } from './utilities/sanitizeSelectParam.js' +export { stripUnselectedFields } from './utilities/stripUnselectedFields.js' export { traverseFields } from './utilities/traverseFields.js' export type { TraverseFieldsCallback } from './utilities/traverseFields.js' export { buildVersionCollectionFields } from './versions/buildCollectionFields.js' diff --git a/packages/payload/src/utilities/createLocalReq.ts b/packages/payload/src/utilities/createLocalReq.ts index 9ee2a980b..d5840fa18 100644 --- a/packages/payload/src/utilities/createLocalReq.ts +++ b/packages/payload/src/utilities/createLocalReq.ts @@ -101,8 +101,9 @@ type CreateLocalReq = ( export const createLocalReq: CreateLocalReq = async ( { context, fallbackLocale, locale: localeArg, req = {} as PayloadRequest, urlSuffix, user }, payload, -) => { +): Promise => { const localization = payload.config?.localization + if (localization) { const locale = localeArg === '*' ? 'all' : localeArg const defaultLocale = localization.defaultLocale diff --git a/packages/payload/src/utilities/getBlockSelect.ts b/packages/payload/src/utilities/getBlockSelect.ts new file mode 100644 index 000000000..13c8a71d0 --- /dev/null +++ b/packages/payload/src/utilities/getBlockSelect.ts @@ -0,0 +1,54 @@ +import type { Block } from '../fields/config/types.js' +import type { SelectMode, SelectType } from '../types/index.js' + +/** + * This is used for the Select API to determine the select level of a block. + * It will ensure that `id` and `blockType` are always included in the select object. + * @returns { blockSelect: boolean | SelectType, blockSelectMode: SelectMode } + */ +export const getBlockSelect = ({ + block, + select, + selectMode, +}: { + block: Block + select: SelectType[string] + selectMode: SelectMode +}): { blockSelect: boolean | SelectType; blockSelectMode: SelectMode } => { + if (typeof select === 'object') { + let blockSelectMode = selectMode + + const blocksSelect = { + ...select, + } + + let blockSelect = blocksSelect[block.slug] + + // sanitize `{ blocks: { cta: false }}` to `{ blocks: { cta: { id: true, blockType: true }}}` + if (selectMode === 'exclude' && blockSelect === false) { + blockSelectMode = 'include' + + blockSelect = { + id: true, + blockType: true, + } + } else if (selectMode === 'include') { + if (!blockSelect) { + blockSelect = {} + } + + if (typeof blockSelect === 'object') { + blockSelect = { + ...blockSelect, + } + + blockSelect['id'] = true + blockSelect['blockType'] = true + } + } + + return { blockSelect, blockSelectMode } + } + + return { blockSelect: select, blockSelectMode: selectMode } +} diff --git a/packages/payload/src/utilities/stripUnselectedFields.ts b/packages/payload/src/utilities/stripUnselectedFields.ts new file mode 100644 index 000000000..0a4dc053e --- /dev/null +++ b/packages/payload/src/utilities/stripUnselectedFields.ts @@ -0,0 +1,43 @@ +import type { Data } from '../admin/types.js' +import type { Field, TabAsField } from '../fields/config/types.js' +import type { SelectMode, SelectType } from '../types/index.js' + +import { fieldAffectsData } from '../fields/config/types.js' + +/** + * This is used for the Select API to strip out fields that are not selected. + * It will mutate the given data object and determine if your recursive function should continue to run. + * It is used within the `afterRead` hook as well as `getFormState`. + * @returns boolean - whether or not the recursive function should continue + */ +export const stripUnselectedFields = ({ + field, + select, + selectMode, + siblingDoc, +}: { + field: Field | TabAsField + select: SelectType + selectMode: SelectMode + siblingDoc: Data +}): boolean => { + let shouldContinue = true + + if (fieldAffectsData(field) && select && selectMode && field.name) { + if (selectMode === 'include') { + if (!select[field.name]) { + delete siblingDoc[field.name] + shouldContinue = false + } + } + + if (selectMode === 'exclude') { + if (select[field.name] === false) { + delete siblingDoc[field.name] + shouldContinue = false + } + } + } + + return shouldContinue +} diff --git a/packages/ui/src/forms/Form/index.tsx b/packages/ui/src/forms/Form/index.tsx index f14ae390e..309d256f7 100644 --- a/packages/ui/src/forms/Form/index.tsx +++ b/packages/ui/src/forms/Form/index.tsx @@ -10,7 +10,7 @@ import { reduceFieldsToValues, wait, } from 'payload/shared' -import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' +import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react' import { toast } from 'sonner' import type { diff --git a/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts b/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts index 10034234c..5760eb7c4 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts @@ -11,10 +11,13 @@ import type { PayloadRequest, SanitizedFieldPermissions, SanitizedFieldsPermissions, + SelectMode, + SelectType, Validate, } from 'payload' import ObjectIdImport from 'bson-objectid' +import { getBlockSelect } from 'payload' import { deepCopyObjectSimple, fieldAffectsData, @@ -86,6 +89,8 @@ export type AddFieldStatePromiseArgs = { */ req: PayloadRequest schemaPath: string + select?: SelectType + selectMode?: SelectMode /** * Whether to skip checking the field's condition. @default false */ @@ -130,6 +135,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom renderFieldFn, req, schemaPath, + select, + selectMode, skipConditionChecks = false, skipValidation = false, state, @@ -247,6 +254,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom 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 @@ -293,6 +302,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom renderAllFields: requiresRender, renderFieldFn, req, + select: typeof arraySelect === 'object' ? arraySelect : undefined, + selectMode, skipConditionChecks, skipValidation, state, @@ -373,6 +384,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom 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( @@ -385,6 +397,12 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom ) } + const { blockSelect, blockSelectMode } = getBlockSelect({ + block, + select: select?.[field.name], + selectMode, + }) + const parentPath = path + '.' + i if (block) { @@ -468,6 +486,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom renderAllFields: requiresRender, renderFieldFn, req, + select: typeof blockSelect === 'object' ? blockSelect : undefined, + selectMode: blockSelectMode, skipConditionChecks, skipValidation, state, @@ -534,6 +554,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom state[path] = fieldState } + const groupSelect = select?.[field.name] + await iterateFields({ id, addErrorPathToParent, @@ -561,6 +583,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom renderAllFields, renderFieldFn, req, + select: typeof groupSelect === 'object' ? groupSelect : undefined, + selectMode, skipConditionChecks, skipValidation, state, @@ -685,6 +709,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom await iterateFields({ id, + select, + selectMode, // passthrough parent functionality addErrorPathToParent: addErrorPathToParentArg, anyParentLocalized: fieldIsLocalized(field) || anyParentLocalized, @@ -717,6 +743,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom } else if (field.type === 'tabs') { const promises = field.tabs.map((tab, tabIndex) => { const isNamedTab = tabHasName(tab) + let tabSelect: SelectType | undefined const { indexPath: tabIndexPath, @@ -746,8 +773,13 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom 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('.') : [] @@ -796,6 +828,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom renderAllFields, renderFieldFn, req, + select: tabSelect, + selectMode, skipConditionChecks, skipValidation, state, diff --git a/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/index.ts b/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/index.ts index a9287deda..95cf90f25 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/index.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/index.ts @@ -1,4 +1,11 @@ -import type { Data, Field as FieldSchema, PayloadRequest, User } from 'payload' +import type { + Data, + Field as FieldSchema, + PayloadRequest, + SelectMode, + SelectType, + User, +} from 'payload' import { iterateFields } from './iterateFields.js' @@ -8,6 +15,8 @@ type Args = { id?: number | string locale: string | undefined req: PayloadRequest + select?: SelectType + selectMode?: SelectMode siblingData: Data user: User } @@ -18,6 +27,8 @@ export const calculateDefaultValues = async ({ fields, locale, req, + select, + selectMode, user, }: Args): Promise => { await iterateFields({ @@ -26,6 +37,8 @@ export const calculateDefaultValues = async ({ fields, locale, req, + select, + selectMode, siblingData: data, user, }) diff --git a/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/iterateFields.ts b/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/iterateFields.ts index 91505a593..ad9426c86 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/iterateFields.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/iterateFields.ts @@ -1,4 +1,4 @@ -import type { Data, Field, PayloadRequest, TabAsField, User } from 'payload' +import type { Data, Field, PayloadRequest, SelectMode, SelectType, TabAsField, User } from 'payload' import { defaultValuePromise } from './promise.js' @@ -8,6 +8,8 @@ type Args = { id?: number | string locale: string | undefined req: PayloadRequest + select?: SelectType + selectMode?: SelectMode siblingData: Data user: User } @@ -18,6 +20,8 @@ export const iterateFields = async ({ fields, locale, req, + select, + selectMode, siblingData, user, }: Args): Promise => { @@ -31,6 +35,8 @@ export const iterateFields = async ({ field, locale, req, + select, + selectMode, siblingData, user, }), diff --git a/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/promise.ts b/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/promise.ts index ec5c03585..25b37c6b3 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/promise.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/calculateDefaultValues/promise.ts @@ -1,6 +1,15 @@ -import type { Data, Field, FlattenedBlock, PayloadRequest, TabAsField, User } from 'payload' +import type { + Data, + Field, + FlattenedBlock, + PayloadRequest, + SelectMode, + SelectType, + TabAsField, + User, +} from 'payload' -import { getDefaultValue } from 'payload' +import { getBlockSelect, getDefaultValue, stripUnselectedFields } from 'payload' import { fieldAffectsData, tabHasName } from 'payload/shared' import { iterateFields } from './iterateFields.js' @@ -11,6 +20,8 @@ type Args = { id?: number | string locale: string | undefined req: PayloadRequest + select?: SelectType + selectMode?: SelectMode siblingData: Data user: User } @@ -22,9 +33,22 @@ export const defaultValuePromise = async ({ field, locale, req, + select, + selectMode, siblingData, user, }: Args): Promise => { + const shouldContinue = stripUnselectedFields({ + field, + select, + selectMode, + siblingDoc: siblingData, + }) + + if (!shouldContinue) { + return + } + if (fieldAffectsData(field)) { if ( typeof siblingData[field.name] === 'undefined' && @@ -54,6 +78,7 @@ export const defaultValuePromise = async ({ if (Array.isArray(rows)) { const promises = [] + const arraySelect = select?.[field.name] rows.forEach((row) => { promises.push( @@ -63,6 +88,8 @@ export const defaultValuePromise = async ({ fields: field.fields, locale, req, + select: typeof arraySelect === 'object' ? arraySelect : undefined, + selectMode, siblingData: row, user, }), @@ -79,14 +106,22 @@ export const defaultValuePromise = async ({ if (Array.isArray(rows)) { const promises = [] + rows.forEach((row) => { 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) + const { blockSelect, blockSelectMode } = getBlockSelect({ + block, + select: select?.[field.name], + selectMode, + }) + if (block) { row.blockType = blockTypeToMatch @@ -97,6 +132,8 @@ export const defaultValuePromise = async ({ fields: block.fields, locale, req, + select: typeof blockSelect === 'object' ? blockSelect : undefined, + selectMode: blockSelectMode, siblingData: row, user, }), @@ -117,6 +154,8 @@ export const defaultValuePromise = async ({ fields: field.fields, locale, req, + select, + selectMode, siblingData, user, }) @@ -130,12 +169,16 @@ export const defaultValuePromise = async ({ const groupData = siblingData[field.name] as Record + const groupSelect = select?.[field.name] + await iterateFields({ id, data, fields: field.fields, locale, req, + select: typeof groupSelect === 'object' ? groupSelect : undefined, + selectMode, siblingData: groupData, user, }) @@ -145,14 +188,24 @@ export const defaultValuePromise = async ({ case 'tab': { let tabSiblingData - if (tabHasName(field)) { + + const isNamedTab = tabHasName(field) + + let tabSelect: SelectType | undefined + + if (isNamedTab) { if (typeof siblingData[field.name] !== 'object') { siblingData[field.name] = {} } tabSiblingData = siblingData[field.name] as Record + + if (typeof select?.[field.name] === 'object') { + tabSelect = select?.[field.name] as SelectType + } } else { tabSiblingData = siblingData + tabSelect = select } await iterateFields({ @@ -161,6 +214,8 @@ export const defaultValuePromise = async ({ fields: field.fields, locale, req, + select: tabSelect, + selectMode, siblingData: tabSiblingData, user, }) @@ -175,6 +230,8 @@ export const defaultValuePromise = async ({ fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), locale, req, + select, + selectMode, siblingData, user, }) diff --git a/packages/ui/src/forms/fieldSchemasToFormState/index.tsx b/packages/ui/src/forms/fieldSchemasToFormState/index.tsx index 0ce6efaa9..31f9ef809 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/index.tsx +++ b/packages/ui/src/forms/fieldSchemasToFormState/index.tsx @@ -8,6 +8,8 @@ import type { FormStateWithoutComponents, PayloadRequest, SanitizedFieldsPermissions, + SelectMode, + SelectType, } from 'payload' import type { RenderFieldMethod } from './types.js' @@ -70,6 +72,8 @@ type Args = { renderFieldFn?: RenderFieldMethod req: PayloadRequest schemaPath: string + select?: SelectType + selectMode?: SelectMode skipValidation?: boolean } @@ -90,6 +94,8 @@ export const fieldSchemasToFormState = async ({ renderFieldFn, req, schemaPath, + select, + selectMode, skipValidation, }: Args): Promise => { if (!clientFieldSchemaMap && renderFieldFn) { @@ -109,6 +115,8 @@ export const fieldSchemasToFormState = async ({ fields, locale: req.locale, req, + select, + selectMode, siblingData: dataWithDefaultValues, user: req.user, }) @@ -142,6 +150,8 @@ export const fieldSchemasToFormState = async ({ renderAllFields, renderFieldFn, req, + select, + selectMode, skipValidation, state, }) diff --git a/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts b/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts index 82206b545..a10a764ed 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts @@ -8,8 +8,11 @@ import type { FormStateWithoutComponents, PayloadRequest, SanitizedFieldsPermissions, + SelectMode, + SelectType, } from 'payload' +import { stripUnselectedFields } from 'payload' import { getFieldPaths } from 'payload/shared' import type { AddFieldStatePromiseArgs } from './addFieldStatePromise.js' @@ -61,6 +64,8 @@ type Args = { renderAllFields: boolean renderFieldFn: RenderFieldMethod req: PayloadRequest + select?: SelectType + selectMode?: SelectMode /** * Whether to skip checking the field's condition. @default false */ @@ -101,6 +106,8 @@ export const iterateFields = async ({ renderAllFields, renderFieldFn: renderFieldFn, req, + select, + selectMode, skipConditionChecks = false, skipValidation = false, state = {}, @@ -118,6 +125,19 @@ export const iterateFields = async ({ parentSchemaPath, }) + if (path !== 'id') { + const shouldContinue = stripUnselectedFields({ + field, + select, + selectMode, + siblingDoc: data, + }) + + if (!shouldContinue) { + return + } + } + const pathSegments = path ? path.split('.') : [] if (!skipConditionChecks) { @@ -174,6 +194,8 @@ export const iterateFields = async ({ renderFieldFn, req, schemaPath, + select, + selectMode, skipConditionChecks, skipValidation, state, diff --git a/packages/ui/src/utilities/buildFormState.ts b/packages/ui/src/utilities/buildFormState.ts index fdb7dd646..8127be5d3 100644 --- a/packages/ui/src/utilities/buildFormState.ts +++ b/packages/ui/src/utilities/buildFormState.ts @@ -1,7 +1,7 @@ import type { BuildFormStateArgs, ClientConfig, ClientUser, ErrorResult, FormState } from 'payload' import { formatErrors } from 'payload' -import { reduceFieldsToValues } from 'payload/shared' +import { getSelectMode, reduceFieldsToValues } from 'payload/shared' import { fieldSchemasToFormState } from '../forms/fieldSchemasToFormState/index.js' import { renderField } from '../forms/fieldSchemasToFormState/renderField.js' @@ -117,10 +117,13 @@ export const buildFormState = async ( }, returnLockStatus, schemaPath = collectionSlug || globalSlug, + select, skipValidation, updateLastEdited, } = args + const selectMode = select ? getSelectMode(select) : undefined + let data = incomingData if (!collectionSlug && !globalSlug) { @@ -210,6 +213,8 @@ export const buildFormState = async ( renderFieldFn: renderField, req, schemaPath, + select, + selectMode, skipValidation, }) diff --git a/test/admin/e2e/general/e2e.spec.ts b/test/admin/e2e/general/e2e.spec.ts index bb3bf670e..5840c6789 100644 --- a/test/admin/e2e/general/e2e.spec.ts +++ b/test/admin/e2e/general/e2e.spec.ts @@ -855,6 +855,7 @@ describe('General', () => { test('should not override un-edited values in bulk edit if it has a defaultValue', async () => { await deleteAllPosts() const post1Title = 'Post' + const postData = { title: 'Post', arrayOfFields: [ @@ -879,6 +880,7 @@ describe('General', () => { ], defaultValueField: 'not the default value', } + const updatedPostTitle = `${post1Title} (Updated)` await createPost(postData) await page.goto(postsUrl.list) @@ -890,10 +892,8 @@ describe('General', () => { hasText: exactText('Title'), }) - await expect(titleOption).toBeVisible() await titleOption.click() const titleInput = page.locator('#field-title') - await expect(titleInput).toBeVisible() await titleInput.fill(updatedPostTitle) await page.locator('.form-submit button[type="submit"].edit-many__publish').click() diff --git a/test/admin/payload-types.ts b/test/admin/payload-types.ts index 3185c8188..81586b26a 100644 --- a/test/admin/payload-types.ts +++ b/test/admin/payload-types.ts @@ -269,10 +269,6 @@ export interface Post { * This is a very long description that takes many characters to complete and hopefully will wrap instead of push the sidebar open, lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum voluptates. Quisquam, voluptatum voluptates. */ sidebarField?: string | null; - /** - * This field should only validate on submit. Try typing "Not allowed" and submitting the form. - */ - validateUsingEvent?: string | null; updatedAt: string; createdAt: string; _status?: ('draft' | 'published') | null; @@ -719,7 +715,6 @@ export interface PostsSelect { disableListColumnText?: T; disableListFilterText?: T; sidebarField?: T; - validateUsingEvent?: T; updatedAt?: T; createdAt?: T; _status?: T; diff --git a/test/form-state/int.spec.ts b/test/form-state/int.spec.ts index 8f21bd02e..532cbc809 100644 --- a/test/form-state/int.spec.ts +++ b/test/form-state/int.spec.ts @@ -1,6 +1,8 @@ -import type { Payload } from 'payload' +import type { Payload, User } from 'payload' +import { buildFormState } from '@payloadcms/ui/utilities/buildFormState' import path from 'path' +import { createLocalReq } from 'payload' import { fileURLToPath } from 'url' import type { NextRESTClient } from '../helpers/NextRESTClient.js' @@ -12,6 +14,7 @@ import { postsSlug } from './collections/Posts/index.js' let payload: Payload let token: string let restClient: NextRESTClient +let user: User const { email, password } = devUser const filename = fileURLToPath(import.meta.url) @@ -22,8 +25,7 @@ describe('Form State', () => { // Boilerplate test setup/teardown // --__--__--__--__--__--__--__--__--__ beforeAll(async () => { - const initialized = await initPayloadInt(dirname) - ;({ payload, restClient } = initialized) + ;({ payload, restClient } = await initPayloadInt(dirname)) const data = await restClient .POST('/users/login', { @@ -35,6 +37,7 @@ describe('Form State', () => { .then((res) => res.json()) token = data.token + user = data.user }) afterAll(async () => { @@ -43,5 +46,97 @@ describe('Form State', () => { } }) - it.todo('should execute form state endpoint') + it('should build entire form state', async () => { + const req = await createLocalReq({ user }, payload) + + const postData = await payload.create({ + collection: postsSlug, + data: { + title: 'Test Post', + }, + }) + + const { state } = await buildFormState({ + id: postData.id, + collectionSlug: postsSlug, + data: postData, + docPermissions: { + create: true, + delete: true, + fields: true, + read: true, + readVersions: true, + update: true, + }, + docPreferences: { + fields: {}, + }, + documentFormState: undefined, + operation: 'update', + renderAllFields: false, + req, + schemaPath: postsSlug, + }) + + expect(state).toMatchObject({ + title: { + value: postData.title, + initialValue: postData.title, + }, + updatedAt: { + value: postData.updatedAt, + initialValue: postData.updatedAt, + }, + createdAt: { + value: postData.createdAt, + initialValue: postData.createdAt, + }, + renderTracker: {}, + validateUsingEvent: {}, + blocks: { + initialValue: 0, + requiresRender: false, + rows: [], + value: 0, + }, + }) + }) + + it('should use `select` to build partial form state with only specified fields', async () => { + const req = await createLocalReq({ user }, payload) + + const postData = await payload.create({ + collection: postsSlug, + data: { + title: 'Test Post', + }, + }) + + const { state } = await buildFormState({ + id: postData.id, + collectionSlug: postsSlug, + data: postData, + docPermissions: undefined, + docPreferences: { + fields: {}, + }, + documentFormState: undefined, + operation: 'update', + renderAllFields: false, + req, + schemaPath: postsSlug, + select: { + title: true, + }, + }) + + expect(state).toStrictEqual({ + title: { + value: postData.title, + initialValue: postData.title, + }, + }) + }) + + it.todo('should skip validation if specified') })