perf(ui): significantly optimize form state component rendering, up to 96% smaller and 75% faster (#11946)

Significantly optimizes the component rendering strategy within the form
state endpoint by precisely rendering only the fields that require it.
This cuts down on server processing and network response sizes when
invoking form state requests **that manipulate array and block rows
which contain server components**, such as rich text fields, custom row
labels, etc. (results listed below).

Here's a breakdown of the issue:

Previously, when manipulating array and block fields, _all_ rows would
render any server components that might exist within them, including
rich text fields. This means that subsequent changes to these fields
would potentially _re-render_ those same components even if they don't
require it.

For example, if you have an array field with a rich text field within
it, adding the first row would cause the rich text field to render,
which is expected. However, when you add a second row, the rich text
field within the first row would render again unnecessarily along with
the new row.

This is especially noticeable for fields with many rows, where every
single row processes its server components and returns RSC data. And
this does not only affect nested rich text fields, but any custom
component defined on the field level, as these are handled in the same
way.

The reason this was necessary in the first place was to ensure that the
server components receive the proper data when they are rendered, such
as the row index and the row's data. Changing one of these rows could
cause the server component to receive the wrong data if it was not
freshly rendered.

While this is still a requirement that rows receive up-to-date props, it
is no longer necessary to render everything.

Here's a breakdown of the actual fix:

This change ensures that only the fields that are actually being
manipulated will be rendered, rather than all rows. The existing rows
will remain in memory on the client, while the newly rendered components
will return from the server. For example, if you add a new row to an
array field, only the new row will render its server components.

To do this, we send the path of the field that is being manipulated to
the server. The server can then use this path to determine for itself
which fields have already been rendered and which ones need required
rendering.

## Results

The following results were gathered by booting up the `form-state` test
suite and seeding 100 array rows, each containing a rich text field. To
invoke a form state request, we navigate to a document within the
"posts" collection, then add a new array row to the list. The result is
then saved to the file system for comparison.

| Test Suite | Collection | Number of Rows | Before | After | Percentage
Change |
|------|------|---------|--------|--------|--------|
| `form-state` | `posts` | 101 | 1.9MB / 266ms | 80KB / 70ms | ~96%
smaller / ~75% faster |

---------

Co-authored-by: James <james@trbl.design>
Co-authored-by: Alessio Gravili <alessio@gravili.de>
This commit is contained in:
Jacob Fletcher
2025-04-03 12:27:14 -04:00
committed by GitHub
parent 8880d705e3
commit e87521a376
26 changed files with 585 additions and 295 deletions

View File

@@ -13,8 +13,12 @@ export type Data = {
export type Row = {
blockType?: string
collapsed?: boolean
customComponents?: {
RowLabel?: React.ReactNode
}
id: string
isLoading?: boolean
lastRenderedPath?: string
}
export type FilterOptionsResult = {
@@ -34,7 +38,6 @@ export type FieldState = {
Error?: React.ReactNode
Field?: React.ReactNode
Label?: React.ReactNode
RowLabels?: React.ReactNode[]
}
disableFormData?: boolean
errorMessage?: string
@@ -46,8 +49,16 @@ export type FieldState = {
fieldSchema?: Field
filterOptions?: FilterOptionsResult
initialValue?: unknown
/**
* The path of the field when its custom components were last rendered.
* This is used to denote if a field has been rendered, and if so,
* what path it was rendered under last.
*
* If this path is undefined, or, if it is different
* from the current path of a given field, the field's components will be re-rendered.
*/
lastRenderedPath?: string
passesCondition?: boolean
requiresRender?: boolean
rows?: Row[]
/**
* The `serverPropsToIgnore` obj is used to prevent the various properties from being overridden across form state requests.
@@ -95,6 +106,13 @@ export type BuildFormStateArgs = {
*/
language?: keyof SupportedLanguages
locale?: string
/**
* If true, will not render RSCs and instead return a simple string in their place.
* This is useful for environments that lack RSC support, such as Jest.
* Form state can still be built, but any server components will be omitted.
* @default false
*/
mockRSCs?: boolean
operation?: 'create' | 'update'
/*
If true, will render field components within their state object

View File

@@ -46,7 +46,6 @@ export const uploadValidation = (
const result = await fieldSchemasToFormState({
id,
collectionSlug: node.relationTo,
data: node?.fields ?? {},
documentData: data,
fields: collection.fields,

View File

@@ -901,12 +901,12 @@ export {
HTMLConverterFeature,
type HTMLConverterFeatureProps,
} from './features/converters/lexicalToHtml_deprecated/index.js'
export { DebugJsxConverterFeature } from './features/debug/jsxConverter/server/index.js'
export { convertLexicalToMarkdown } from './features/converters/lexicalToMarkdown/index.js'
export { convertMarkdownToLexical } from './features/converters/markdownToLexical/index.js'
export { getPayloadPopulateFn } from './features/converters/utilities/payloadPopulateFn.js'
export { getRestPopulateFn } from './features/converters/utilities/restPopulateFn.js'
export { DebugJsxConverterFeature } from './features/debug/jsxConverter/server/index.js'
export { TestRecorderFeature } from './features/debug/testRecorder/server/index.js'
export { TreeViewFeature } from './features/debug/treeView/server/index.js'
export { EXPERIMENTAL_TableFeature } from './features/experimental_table/server/index.js'

View File

@@ -110,10 +110,10 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
)
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label, RowLabels } = {},
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
errorPaths,
rows: rowsData = [],
rows = [],
showError,
valid,
value,
@@ -173,12 +173,12 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
(collapsed: boolean) => {
const { collapsedIDs, updatedRows } = toggleAllRows({
collapsed,
rows: rowsData,
rows,
})
setDocFieldPreferences(path, { collapsed: collapsedIDs })
dispatchFields({ type: 'SET_ALL_ROWS_COLLAPSED', path, updatedRows })
},
[dispatchFields, path, rowsData, setDocFieldPreferences],
[dispatchFields, path, rows, setDocFieldPreferences],
)
const setCollapse = useCallback(
@@ -186,22 +186,22 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
const { collapsedIDs, updatedRows } = extractRowsAndCollapsedIDs({
collapsed,
rowID,
rows: rowsData,
rows,
})
dispatchFields({ type: 'SET_ROW_COLLAPSED', path, updatedRows })
setDocFieldPreferences(path, { collapsed: collapsedIDs })
},
[dispatchFields, path, rowsData, setDocFieldPreferences],
[dispatchFields, path, rows, setDocFieldPreferences],
)
const hasMaxRows = maxRows && rowsData.length >= maxRows
const hasMaxRows = maxRows && rows.length >= maxRows
const fieldErrorCount = errorPaths.length
const fieldHasErrors = submitted && errorPaths.length > 0
const showRequired = (readOnly || disabled) && rowsData.length === 0
const showMinRows = rowsData.length < minRows || (required && rowsData.length === 0)
const showRequired = (readOnly || disabled) && rows.length === 0
const showMinRows = rows.length < minRows || (required && rows.length === 0)
return (
<div
@@ -242,7 +242,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
<ErrorPill count={fieldErrorCount} i18n={i18n} withMessage />
)}
</div>
{rowsData?.length > 0 && (
{rows?.length > 0 && (
<ul className={`${baseClass}__header-actions`}>
<li>
<button
@@ -272,13 +272,13 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
</header>
<NullifyLocaleField fieldValue={value} localized={localized} path={path} />
{BeforeInput}
{(rowsData?.length > 0 || (!valid && (showRequired || showMinRows))) && (
{(rows?.length > 0 || (!valid && (showRequired || showMinRows))) && (
<DraggableSortable
className={`${baseClass}__draggable-rows`}
ids={rowsData.map((row) => row.id)}
ids={rows.map((row) => row.id)}
onDragEnd={({ moveFromIndex, moveToIndex }) => moveRow(moveFromIndex, moveToIndex)}
>
{rowsData.map((rowData, i) => {
{rows.map((rowData, i) => {
const { id: rowID, isLoading } = rowData
const rowPath = `${path}.${i}`
@@ -297,7 +297,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
<ArrayRow
{...draggableSortableItemProps}
addRow={addRow}
CustomRowLabel={RowLabels?.[i]}
CustomRowLabel={rows?.[i]?.customComponents?.RowLabel}
duplicateRow={duplicateRow}
errorCount={rowErrorCount}
fields={fields}
@@ -313,7 +313,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
readOnly={readOnly || disabled}
removeRow={removeRow}
row={rowData}
rowCount={rowsData?.length}
rowCount={rows?.length}
rowIndex={i}
schemaPath={schemaPath}
setCollapse={setCollapse}

View File

@@ -98,7 +98,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
)
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label, RowLabels } = {},
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
disabled,
errorPaths,
rows = [],
@@ -293,7 +293,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
hasMaxRows={hasMaxRows}
isLoading={isLoading}
isSortable={isSortable}
Label={RowLabels?.[i]}
Label={rows?.[i]?.customComponents?.RowLabel}
labels={labels}
moveRow={moveRow}
parentPath={path}

View File

@@ -53,24 +53,14 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
[`${path}.${rowIndex}.id`]: {
initialValue: newRow.id,
passesCondition: true,
requiresRender: true,
valid: true,
value: newRow.id,
},
[path]: {
...state[path],
disableFormData: true,
requiresRender: true,
rows: withNewRow,
value: siblingRows.length,
...(state[path]?.requiresRender === true
? {
serverPropsToIgnore: [
...(state[path]?.serverPropsToIgnore || []),
'requiresRender',
],
}
: state[path]?.serverPropsToIgnore || []),
},
}
@@ -144,12 +134,16 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
const { remainingFields, rows } = separateRows(path, state)
const rowsMetadata = [...(state[path].rows || [])]
const duplicateRowMetadata = deepCopyObjectSimple(rowsMetadata[rowIndex])
const duplicateRowMetadata = deepCopyObjectSimpleWithoutReactComponents(
rowsMetadata[rowIndex],
)
if (duplicateRowMetadata.id) {
duplicateRowMetadata.id = new ObjectId().toHexString()
}
const duplicateRowState = deepCopyObjectSimpleWithoutReactComponents(rows[rowIndex])
if (duplicateRowState.id) {
duplicateRowState.id.value = new ObjectId().toHexString()
duplicateRowState.id.initialValue = new ObjectId().toHexString()
@@ -177,17 +171,8 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
[path]: {
...state[path],
disableFormData: true,
requiresRender: true,
rows: rowsMetadata,
value: rows.length,
...(state[path]?.requiresRender === true
? {
serverPropsToIgnore: [
...(state[path]?.serverPropsToIgnore || []),
'requiresRender',
],
}
: state[path]?.serverPropsToIgnore || ([] as any)),
},
}
@@ -214,41 +199,10 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
...flattenRows(path, topLevelRows),
[path]: {
...state[path],
requiresRender: true,
rows: rowsWithinField,
...(state[path]?.requiresRender === true
? {
serverPropsToIgnore: [
...(state[path]?.serverPropsToIgnore || []),
'requiresRender',
],
}
: state[path]?.serverPropsToIgnore || ([] as any)),
},
}
// Do the same for custom components, i.e. `array.customComponents.RowLabels[0]` -> `array.customComponents.RowLabels[1]`
// Do this _after_ initializing `newState` to avoid adding the `customComponents` key to the state if it doesn't exist
if (newState[path]?.customComponents?.RowLabels) {
const customComponents = {
...newState[path].customComponents,
RowLabels: [...newState[path].customComponents.RowLabels],
}
// Ensure the array grows if necessary
if (moveToIndex >= customComponents.RowLabels.length) {
customComponents.RowLabels.length = moveToIndex + 1
}
const copyOfMovingLabel = customComponents.RowLabels[moveFromIndex]
customComponents.RowLabels.splice(moveFromIndex, 1)
customComponents.RowLabels.splice(moveToIndex, 0, copyOfMovingLabel)
newState[path].customComponents = customComponents
}
return newState
}
@@ -273,17 +227,8 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
[path]: {
...state[path],
disableFormData: rows.length > 0,
requiresRender: true,
rows: rowsMetadata,
value: rows.length,
...(state[path]?.requiresRender === true
? {
serverPropsToIgnore: [
...(state[path]?.serverPropsToIgnore || []),
'requiresRender',
],
}
: state[path]?.serverPropsToIgnore || []),
},
...flattenRows(path, rows),
}
@@ -323,14 +268,6 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
disableFormData: true,
rows: rowsMetadata,
value: siblingRows.length,
...(state[path]?.requiresRender === true
? {
serverPropsToIgnore: [
...(state[path]?.serverPropsToIgnore || []),
'requiresRender',
],
}
: state[path]?.serverPropsToIgnore || []),
},
}

View File

@@ -596,7 +596,7 @@ export const Form: React.FC<FormProps> = (props) => {
const newRows: unknown[] = getDataByPath(path) || []
const rowIndex = rowIndexArg === undefined ? newRows.length : rowIndexArg
// dispatch ADD_ROW that sets requiresRender: true and adds a blank row to local form state.
// dispatch ADD_ROW adds a blank row to local form state.
// This performs no form state request, as the debounced onChange effect will do that for us.
dispatchFields({
type: 'ADD_ROW',

View File

@@ -34,7 +34,6 @@ export const mergeServerFormState = ({
'errorMessage',
'errorPaths',
'customComponents',
'requiresRender',
]
if (acceptValues) {
@@ -76,7 +75,7 @@ export const mergeServerFormState = ({
}
/**
* Need to intelligently merge the rows array to ensure no rows are lost or added while the request was pending
* Need to intelligently merge the rows array to ensure changes to local state are not lost while the request was pending
* For example, the server response could come back with a row which has been deleted on the client
* Loop over the incoming rows, if it exists in client side form state, merge in any new properties from the server
*/

View File

@@ -1,4 +1,5 @@
import type {
BuildFormStateArgs,
ClientFieldSchemaMap,
Data,
DocumentPreferences,
@@ -9,6 +10,7 @@ import type {
FormState,
FormStateWithoutComponents,
PayloadRequest,
Row,
SanitizedFieldPermissions,
SanitizedFieldsPermissions,
SelectMode,
@@ -68,6 +70,7 @@ export type AddFieldStatePromiseArgs = {
*/
includeSchema?: boolean
indexPath: string
mockRSCs?: BuildFormStateArgs['mockRSCs']
/**
* Whether to omit parent fields in the state. @default false
*/
@@ -122,6 +125,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
fullData,
includeSchema = false,
indexPath,
mockRSCs,
omitParents = false,
operation,
parentPath,
@@ -148,12 +152,16 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
)
}
const requiresRender = renderAllFields || previousFormState?.[path]?.requiresRender
const lastRenderedPath = previousFormState?.[path]?.lastRenderedPath
let fieldPermissions: SanitizedFieldPermissions = true
const fieldState: FieldState = {}
if (lastRenderedPath) {
fieldState.lastRenderedPath = lastRenderedPath
}
if (passesCondition === false) {
fieldState.passesCondition = false
}
@@ -289,6 +297,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
forceFullValue,
fullData,
includeSchema,
mockRSCs,
omitParents,
operation,
parentIndexPath: '',
@@ -299,7 +308,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
fieldPermissions === true ? fieldPermissions : fieldPermissions?.fields || {},
preferences,
previousFormState,
renderAllFields: requiresRender,
renderAllFields,
renderFieldFn,
req,
select: typeof arraySelect === 'object' ? arraySelect : undefined,
@@ -314,17 +323,25 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
acc.rows = []
}
acc.rows.push({
const previousRows = previousFormState?.[path]?.rows || []
// First, check if `previousFormState` has a matching row
const previousRow: Row = previousRows.find((prevRow) => prevRow.id === row.id)
const newRow: Row = {
id: row.id,
isLoading: false,
})
}
if (previousRow?.lastRenderedPath) {
newRow.lastRenderedPath = previousRow.lastRenderedPath
}
acc.rows.push(newRow)
const previousRows = previousFormState?.[path]?.rows || []
const collapsedRowIDsFromPrefs = preferences?.fields?.[path]?.collapsed
const collapsed = (() => {
// First, check if `previousFormState` has a matching row
const previousRow = previousRows.find((prevRow) => prevRow.id === row.id)
if (previousRow) {
return previousRow.collapsed ?? false
}
@@ -357,8 +374,6 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
fieldState.rows = rows
}
fieldState.requiresRender = false
// Add values to field state
if (data[field.name] !== null) {
fieldState.value = forceFullValue ? arrayValue : arrayValue.length
@@ -468,6 +483,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
forceFullValue,
fullData,
includeSchema,
mockRSCs,
omitParents,
operation,
parentIndexPath: '',
@@ -482,7 +498,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
: parentPermissions?.[field.name]?.blocks?.[block.slug]?.fields || {},
preferences,
previousFormState,
renderAllFields: requiresRender,
renderAllFields,
renderFieldFn,
req,
select: typeof blockSelect === 'object' ? blockSelect : undefined,
@@ -493,11 +509,22 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
}),
)
acc.rowMetadata.push({
const previousRows = previousFormState?.[path]?.rows || []
// First, check if `previousFormState` has a matching row
const previousRow: Row = previousRows.find((prevRow) => prevRow.id === row.id)
const newRow: Row = {
id: row.id,
blockType: row.blockType,
isLoading: false,
})
}
if (previousRow?.lastRenderedPath) {
newRow.lastRenderedPath = previousRow.lastRenderedPath
}
acc.rowMetadata.push(newRow)
const collapsedRowIDs = preferences?.fields?.[path]?.collapsed
@@ -536,10 +563,6 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
fieldState.rows = rowMetadata
// Unset requiresRender
// so it will be removed from form state
fieldState.requiresRender = false
// Add field to state
if (!omitParents && (!filter || filter(args))) {
state[path] = fieldState
@@ -570,6 +593,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
forceFullValue,
fullData,
includeSchema,
mockRSCs,
omitParents,
operation,
parentIndexPath: '',
@@ -709,6 +733,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
await iterateFields({
id,
mockRSCs,
select,
selectMode,
// passthrough parent functionality
@@ -816,6 +841,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
forceFullValue,
fullData,
includeSchema,
mockRSCs,
omitParents,
operation,
parentIndexPath: isNamedTab ? '' : tabIndexPath,
@@ -844,12 +870,12 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
}
}
if (requiresRender && renderFieldFn && !fieldIsHiddenOrDisabled(field)) {
if (renderFieldFn && !fieldIsHiddenOrDisabled(field)) {
const fieldState = state[path]
const fieldConfig = fieldSchemaMap.get(schemaPath)
if (!fieldConfig) {
if (!fieldConfig && !mockRSCs) {
if (schemaPath.endsWith('.blockType')) {
return
} else {
@@ -873,6 +899,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
fieldState,
formState: state,
indexPath,
lastRenderedPath,
mockRSCs,
operation,
parentPath,
parentSchemaPath,
@@ -880,6 +908,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
permissions: fieldPermissions,
preferences,
previousFieldState: previousFormState?.[path],
renderAllFields,
req,
schemaPath,
siblingData: data,

View File

@@ -1,4 +1,5 @@
import type {
BuildFormStateArgs,
ClientFieldSchemaMap,
Data,
DocumentPreferences,
@@ -56,6 +57,7 @@ type Args = {
* the initial block data here, which will be used as `blockData` for the top-level fields, until the first block is encountered.
*/
initialBlockData?: Data
mockRSCs?: BuildFormStateArgs['mockRSCs']
operation?: 'create' | 'update'
permissions: SanitizedFieldsPermissions
preferences: DocumentPreferences
@@ -86,6 +88,7 @@ export const fieldSchemasToFormState = async ({
fields,
fieldSchemaMap,
initialBlockData,
mockRSCs,
operation,
permissions,
preferences,
@@ -139,6 +142,7 @@ export const fieldSchemasToFormState = async ({
fields,
fieldSchemaMap,
fullData,
mockRSCs,
operation,
parentIndexPath: '',
parentPassesCondition: true,

View File

@@ -1,4 +1,5 @@
import type {
BuildFormStateArgs,
ClientFieldSchemaMap,
Data,
DocumentPreferences,
@@ -46,6 +47,7 @@ type Args = {
* Whether the field schema should be included in the state. @default false
*/
includeSchema?: boolean
mockRSCs?: BuildFormStateArgs['mockRSCs']
/**
* Whether to omit parent fields in the state. @default false
*/
@@ -94,6 +96,7 @@ export const iterateFields = async ({
forceFullValue = false,
fullData,
includeSchema = false,
mockRSCs,
omitParents = false,
operation,
parentIndexPath,
@@ -180,6 +183,7 @@ export const iterateFields = async ({
fullData,
includeSchema,
indexPath,
mockRSCs,
omitParents,
operation,
parentIndexPath,

View File

@@ -34,16 +34,25 @@ export const renderField: RenderFieldMethod = ({
fieldState,
formState,
indexPath,
lastRenderedPath,
mockRSCs,
operation,
parentPath,
parentSchemaPath,
path,
permissions,
preferences,
renderAllFields,
req,
schemaPath,
siblingData,
}) => {
const requiresRender = renderAllFields || !lastRenderedPath || lastRenderedPath !== path
if (!requiresRender && fieldConfig.type !== 'array' && fieldConfig.type !== 'blocks') {
return
}
const clientField = clientFieldSchemaMap
? (clientFieldSchemaMap.get(schemaPath) as ClientField)
: createClientField({
@@ -53,10 +62,6 @@ export const renderField: RenderFieldMethod = ({
importMap: req.payload.importMap,
})
if (fieldIsHiddenOrDisabled(clientField)) {
return
}
const clientProps: ClientComponentProps & Partial<FieldPaths> = {
field: clientField,
path,
@@ -97,6 +102,112 @@ export const renderField: RenderFieldMethod = ({
user: req.user,
}
switch (fieldConfig.type) {
case 'array': {
fieldState?.rows?.forEach((row, rowIndex) => {
const rowLastRenderedPath = row.lastRenderedPath
const rowPath = `${path}.${rowIndex}`
const rowRequiresRender =
renderAllFields || !rowLastRenderedPath || rowLastRenderedPath !== rowPath
if (!rowRequiresRender) {
return
}
row.lastRenderedPath = rowPath
if (fieldConfig.admin?.components && 'RowLabel' in fieldConfig.admin.components) {
if (!row.customComponents) {
row.customComponents = {}
}
row.customComponents.RowLabel = !mockRSCs
? RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.RowLabel,
importMap: req.payload.importMap,
key: `${rowIndex}`,
serverProps: {
...serverProps,
rowLabel: `${getTranslation(fieldConfig.labels.singular, req.i18n)} ${String(
rowIndex + 1,
).padStart(2, '0')}`,
rowNumber: rowIndex + 1,
},
})
: 'Mock'
}
})
break
}
case 'blocks': {
fieldState?.rows?.forEach((row, rowIndex) => {
const rowLastRenderedPath = row.lastRenderedPath
const rowPath = `${path}.${rowIndex}`
const rowRequiresRender =
renderAllFields || !rowLastRenderedPath || rowLastRenderedPath !== rowPath
if (!rowRequiresRender) {
return
}
row.lastRenderedPath = rowPath
const blockTypeToMatch: string = row.blockType
const blockConfig =
req.payload.blocks[blockTypeToMatch] ??
((fieldConfig.blockReferences ?? fieldConfig.blocks).find(
(block) => typeof block !== 'string' && block.slug === blockTypeToMatch,
) as FlattenedBlock | undefined)
if (blockConfig.admin?.components && 'Label' in blockConfig.admin.components) {
if (!fieldState.rows[rowIndex]?.customComponents) {
fieldState.rows[rowIndex].customComponents = {}
}
fieldState.rows[rowIndex].customComponents.RowLabel = !mockRSCs
? RenderServerComponent({
clientProps,
Component: blockConfig.admin.components.Label,
importMap: req.payload.importMap,
key: `${rowIndex}`,
serverProps: {
...serverProps,
blockType: row.blockType,
rowLabel: `${getTranslation(blockConfig.labels.singular, req.i18n)} ${String(
rowIndex + 1,
).padStart(2, '0')}`,
rowNumber: rowIndex + 1,
},
})
: 'Mock'
}
})
break
}
}
if (!requiresRender) {
return
}
/**
* Set the lastRenderedPath equal to the new path of the field
*/
fieldState.lastRenderedPath = path
if (fieldIsHiddenOrDisabled(clientField)) {
return
}
/**
* Only create the `customComponents` object if needed.
* This will prevent unnecessary data from being transferred to the client.
@@ -114,70 +225,6 @@ export const renderField: RenderFieldMethod = ({
}
switch (fieldConfig.type) {
case 'array': {
fieldState?.rows?.forEach((row, rowIndex) => {
if (fieldConfig.admin?.components && 'RowLabel' in fieldConfig.admin.components) {
if (!fieldState.customComponents.RowLabels) {
fieldState.customComponents.RowLabels = []
}
fieldState.customComponents.RowLabels[rowIndex] = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.RowLabel,
importMap: req.payload.importMap,
key: `${rowIndex}`,
serverProps: {
...serverProps,
rowLabel: `${getTranslation(fieldConfig.labels.singular, req.i18n)} ${String(
rowIndex + 1,
).padStart(2, '0')}`,
rowNumber: rowIndex + 1,
},
})
}
})
break
}
case 'blocks': {
fieldState?.rows?.forEach((row, rowIndex) => {
const blockTypeToMatch: string = row.blockType
const blockConfig =
req.payload.blocks[blockTypeToMatch] ??
((fieldConfig.blockReferences ?? fieldConfig.blocks).find(
(block) => typeof block !== 'string' && block.slug === blockTypeToMatch,
) as FlattenedBlock | undefined)
if (blockConfig.admin?.components && 'Label' in blockConfig.admin.components) {
if (!fieldState.customComponents) {
fieldState.customComponents = {}
}
if (!fieldState.customComponents.RowLabels) {
fieldState.customComponents.RowLabels = []
}
fieldState.customComponents.RowLabels[rowIndex] = RenderServerComponent({
clientProps,
Component: blockConfig.admin.components.Label,
importMap: req.payload.importMap,
key: `${rowIndex}`,
serverProps: {
...serverProps,
blockType: row.blockType,
rowLabel: `${getTranslation(blockConfig.labels.singular, req.i18n)} ${String(
rowIndex + 1,
).padStart(2, '0')}`,
rowNumber: rowIndex + 1,
},
})
}
})
break
}
case 'richText': {
if (!fieldConfig?.editor) {
throw new MissingEditorProp(fieldConfig) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
@@ -195,7 +242,7 @@ export const renderField: RenderFieldMethod = ({
fieldConfig.admin.components = {}
}
fieldState.customComponents.Field = (
fieldState.customComponents.Field = !mockRSCs ? (
<WatchCondition path={path}>
{RenderServerComponent({
clientProps,
@@ -204,6 +251,8 @@ export const renderField: RenderFieldMethod = ({
serverProps,
})}
</WatchCondition>
) : (
'Mock'
)
break
@@ -219,13 +268,15 @@ export const renderField: RenderFieldMethod = ({
const Component = fieldConfig.admin.components[key]
fieldState.customComponents[key] = RenderServerComponent({
clientProps,
Component,
importMap: req.payload.importMap,
key: `field.admin.components.${key}`,
serverProps,
})
fieldState.customComponents[key] = !mockRSCs
? RenderServerComponent({
clientProps,
Component,
importMap: req.payload.importMap,
key: `field.admin.components.${key}`,
serverProps,
})
: 'Mock'
}
}
break
@@ -241,7 +292,7 @@ export const renderField: RenderFieldMethod = ({
'description' in fieldConfig.admin &&
typeof fieldConfig.admin?.description === 'function'
) {
fieldState.customComponents.Description = (
fieldState.customComponents.Description = !mockRSCs ? (
<FieldDescription
description={fieldConfig.admin?.description({
i18n: req.i18n,
@@ -249,62 +300,74 @@ export const renderField: RenderFieldMethod = ({
})}
path={path}
/>
) : (
'Mock'
)
}
if (fieldConfig.admin?.components) {
if ('afterInput' in fieldConfig.admin.components) {
fieldState.customComponents.AfterInput = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.afterInput,
importMap: req.payload.importMap,
key: 'field.admin.components.afterInput',
serverProps,
})
fieldState.customComponents.AfterInput = !mockRSCs
? RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.afterInput,
importMap: req.payload.importMap,
key: 'field.admin.components.afterInput',
serverProps,
})
: 'Mock'
}
if ('beforeInput' in fieldConfig.admin.components) {
fieldState.customComponents.BeforeInput = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.beforeInput,
importMap: req.payload.importMap,
key: 'field.admin.components.beforeInput',
serverProps,
})
fieldState.customComponents.BeforeInput = !mockRSCs
? RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.beforeInput,
importMap: req.payload.importMap,
key: 'field.admin.components.beforeInput',
serverProps,
})
: 'Mock'
}
if ('Description' in fieldConfig.admin.components) {
fieldState.customComponents.Description = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Description,
importMap: req.payload.importMap,
key: 'field.admin.components.Description',
serverProps,
})
fieldState.customComponents.Description = !mockRSCs
? RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Description,
importMap: req.payload.importMap,
key: 'field.admin.components.Description',
serverProps,
})
: 'Mock'
}
if ('Error' in fieldConfig.admin.components) {
fieldState.customComponents.Error = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Error,
importMap: req.payload.importMap,
key: 'field.admin.components.Error',
serverProps,
})
fieldState.customComponents.Error = !mockRSCs
? RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Error,
importMap: req.payload.importMap,
key: 'field.admin.components.Error',
serverProps,
})
: 'Mock'
}
if ('Label' in fieldConfig.admin.components) {
fieldState.customComponents.Label = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Label,
importMap: req.payload.importMap,
key: 'field.admin.components.Label',
serverProps,
})
fieldState.customComponents.Label = !mockRSCs
? RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Label,
importMap: req.payload.importMap,
key: 'field.admin.components.Label',
serverProps,
})
: 'Mock'
}
if ('Field' in fieldConfig.admin.components) {
fieldState.customComponents.Field = (
fieldState.customComponents.Field = !mockRSCs ? (
<WatchCondition path={path}>
{RenderServerComponent({
clientProps,
@@ -314,6 +377,8 @@ export const renderField: RenderFieldMethod = ({
serverProps,
})}
</WatchCondition>
) : (
'Mock'
)
}
}

View File

@@ -21,6 +21,8 @@ export type RenderFieldArgs = {
formState: FormState
id?: number | string
indexPath: string
lastRenderedPath: string
mockRSCs?: boolean
operation: Operation
parentPath: string
parentSchemaPath: string
@@ -28,6 +30,7 @@ export type RenderFieldArgs = {
permissions: SanitizedFieldPermissions
preferences: DocumentPreferences
previousFieldState: FieldState
renderAllFields: boolean
req: PayloadRequest
schemaPath: string
siblingData: Data

View File

@@ -41,8 +41,8 @@ export type IListQueryContext = {
data: PaginatedDocs
defaultLimit?: number
defaultSort?: Sort
orderableFieldName?: string
modified: boolean
orderableFieldName?: string
query: ListQuery
refineListData: (args: ListQuery, setModified?: boolean) => Promise<void>
setModified: (modified: boolean) => void

View File

@@ -107,6 +107,7 @@ export const buildFormState = async (
globalSlug,
initialBlockData,
initialBlockFormState,
mockRSCs,
operation,
renderAllFields,
req,
@@ -205,6 +206,7 @@ export const buildFormState = async (
fields,
fieldSchemaMap: schemaMap,
initialBlockData: blockData,
mockRSCs,
operation,
permissions: docPermissions?.fields || {},
preferences: docPreferences || { fields: {} },