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:
@@ -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
|
||||
|
||||
@@ -46,7 +46,6 @@ export const uploadValidation = (
|
||||
const result = await fieldSchemasToFormState({
|
||||
id,
|
||||
collectionSlug: node.relationTo,
|
||||
|
||||
data: node?.fields ?? {},
|
||||
documentData: data,
|
||||
fields: collection.fields,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 || []),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {} },
|
||||
|
||||
Reference in New Issue
Block a user