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:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
} & (
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
54
packages/payload/src/utilities/getBlockSelect.ts
Normal file
54
packages/payload/src/utilities/getBlockSelect.ts
Normal 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 }
|
||||
}
|
||||
43
packages/payload/src/utilities/stripUnselectedFields.ts
Normal file
43
packages/payload/src/utilities/stripUnselectedFields.ts
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user