feat: form state select (#11689)

Implements a select-like API into the form state endpoint. This follows
the same spec as the Select API on existing Payload operations, but
works on form state rather than at the db level. This means you can send
the `select` argument through the form state handler, and it will only
process and return the fields you've explicitly identified.

This is especially useful when you only need to generate a partial form
state, for example within the bulk edit form where you select only a
subset of fields to edit. There is no need to iterate all fields of the
schema, generate default values for each, and return them all through
the network. This will also simplify and reduce the amount of
client-side processing required, where we longer need to strip
unselected fields before submission.
This commit is contained in:
Jacob Fletcher
2025-03-14 13:11:12 -04:00
committed by GitHub
parent 3c92fbd98d
commit 9ea8a7acf0
18 changed files with 377 additions and 66 deletions

View File

@@ -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'

View File

@@ -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
} & (

View File

@@ -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({

View File

@@ -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'

View File

@@ -101,8 +101,9 @@ type CreateLocalReq = (
export const createLocalReq: CreateLocalReq = async (
{ context, fallbackLocale, locale: localeArg, req = {} as PayloadRequest, urlSuffix, user },
payload,
) => {
): Promise<PayloadRequest> => {
const localization = payload.config?.localization
if (localization) {
const locale = localeArg === '*' ? 'all' : localeArg
const defaultLocale = localization.defaultLocale

View File

@@ -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 }
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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<Data> => {
await iterateFields({
@@ -26,6 +37,8 @@ export const calculateDefaultValues = async ({
fields,
locale,
req,
select,
selectMode,
siblingData: data,
user,
})

View File

@@ -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<T> = {
id?: number | string
locale: string | undefined
req: PayloadRequest
select?: SelectType
selectMode?: SelectMode
siblingData: Data
user: User
}
@@ -18,6 +20,8 @@ export const iterateFields = async <T>({
fields,
locale,
req,
select,
selectMode,
siblingData,
user,
}: Args<T>): Promise<void> => {
@@ -31,6 +35,8 @@ export const iterateFields = async <T>({
field,
locale,
req,
select,
selectMode,
siblingData,
user,
}),

View File

@@ -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<T> = {
id?: number | string
locale: string | undefined
req: PayloadRequest
select?: SelectType
selectMode?: SelectMode
siblingData: Data
user: User
}
@@ -22,9 +33,22 @@ export const defaultValuePromise = async <T>({
field,
locale,
req,
select,
selectMode,
siblingData,
user,
}: Args<T>): Promise<void> => {
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 <T>({
if (Array.isArray(rows)) {
const promises = []
const arraySelect = select?.[field.name]
rows.forEach((row) => {
promises.push(
@@ -63,6 +88,8 @@ export const defaultValuePromise = async <T>({
fields: field.fields,
locale,
req,
select: typeof arraySelect === 'object' ? arraySelect : undefined,
selectMode,
siblingData: row,
user,
}),
@@ -79,14 +106,22 @@ export const defaultValuePromise = async <T>({
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 <T>({
fields: block.fields,
locale,
req,
select: typeof blockSelect === 'object' ? blockSelect : undefined,
selectMode: blockSelectMode,
siblingData: row,
user,
}),
@@ -117,6 +154,8 @@ export const defaultValuePromise = async <T>({
fields: field.fields,
locale,
req,
select,
selectMode,
siblingData,
user,
})
@@ -130,12 +169,16 @@ export const defaultValuePromise = async <T>({
const groupData = siblingData[field.name] as Record<string, unknown>
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 <T>({
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<string, unknown>
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 <T>({
fields: field.fields,
locale,
req,
select: tabSelect,
selectMode,
siblingData: tabSiblingData,
user,
})
@@ -175,6 +230,8 @@ export const defaultValuePromise = async <T>({
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
locale,
req,
select,
selectMode,
siblingData,
user,
})

View File

@@ -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<FormState> => {
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,
})

View File

@@ -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,

View File

@@ -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,
})

View File

@@ -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()

View File

@@ -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<T extends boolean = true> {
disableListColumnText?: T;
disableListFilterText?: T;
sidebarField?: T;
validateUsingEvent?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;

View File

@@ -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')
})