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:
@@ -60,31 +60,31 @@ export const Posts: CollectionConfig = {
|
|||||||
|
|
||||||
The following options are available:
|
The following options are available:
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
|
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
|
||||||
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
|
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
|
||||||
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
|
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
|
||||||
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
||||||
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
||||||
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
|
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
|
||||||
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||||
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
||||||
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||||
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
|
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
|
||||||
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||||
| `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
|
| `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
|
||||||
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||||
| `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
|
| `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
|
||||||
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||||
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||||
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||||
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||||
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
|
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
|
||||||
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
|
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
|
||||||
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
|
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
|
||||||
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
|
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
|
||||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
||||||
|
|
||||||
_\* An asterisk denotes that a property is required._
|
_\* An asterisk denotes that a property is required._
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ export type Data = {
|
|||||||
export type Row = {
|
export type Row = {
|
||||||
blockType?: string
|
blockType?: string
|
||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
|
customComponents?: {
|
||||||
|
RowLabel?: React.ReactNode
|
||||||
|
}
|
||||||
id: string
|
id: string
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
|
lastRenderedPath?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FilterOptionsResult = {
|
export type FilterOptionsResult = {
|
||||||
@@ -34,7 +38,6 @@ export type FieldState = {
|
|||||||
Error?: React.ReactNode
|
Error?: React.ReactNode
|
||||||
Field?: React.ReactNode
|
Field?: React.ReactNode
|
||||||
Label?: React.ReactNode
|
Label?: React.ReactNode
|
||||||
RowLabels?: React.ReactNode[]
|
|
||||||
}
|
}
|
||||||
disableFormData?: boolean
|
disableFormData?: boolean
|
||||||
errorMessage?: string
|
errorMessage?: string
|
||||||
@@ -46,8 +49,16 @@ export type FieldState = {
|
|||||||
fieldSchema?: Field
|
fieldSchema?: Field
|
||||||
filterOptions?: FilterOptionsResult
|
filterOptions?: FilterOptionsResult
|
||||||
initialValue?: unknown
|
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
|
passesCondition?: boolean
|
||||||
requiresRender?: boolean
|
|
||||||
rows?: Row[]
|
rows?: Row[]
|
||||||
/**
|
/**
|
||||||
* The `serverPropsToIgnore` obj is used to prevent the various properties from being overridden across form state requests.
|
* 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
|
language?: keyof SupportedLanguages
|
||||||
locale?: string
|
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'
|
operation?: 'create' | 'update'
|
||||||
/*
|
/*
|
||||||
If true, will render field components within their state object
|
If true, will render field components within their state object
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export const uploadValidation = (
|
|||||||
const result = await fieldSchemasToFormState({
|
const result = await fieldSchemasToFormState({
|
||||||
id,
|
id,
|
||||||
collectionSlug: node.relationTo,
|
collectionSlug: node.relationTo,
|
||||||
|
|
||||||
data: node?.fields ?? {},
|
data: node?.fields ?? {},
|
||||||
documentData: data,
|
documentData: data,
|
||||||
fields: collection.fields,
|
fields: collection.fields,
|
||||||
|
|||||||
@@ -901,12 +901,12 @@ export {
|
|||||||
HTMLConverterFeature,
|
HTMLConverterFeature,
|
||||||
type HTMLConverterFeatureProps,
|
type HTMLConverterFeatureProps,
|
||||||
} from './features/converters/lexicalToHtml_deprecated/index.js'
|
} 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 { convertLexicalToMarkdown } from './features/converters/lexicalToMarkdown/index.js'
|
||||||
export { convertMarkdownToLexical } from './features/converters/markdownToLexical/index.js'
|
export { convertMarkdownToLexical } from './features/converters/markdownToLexical/index.js'
|
||||||
|
|
||||||
export { getPayloadPopulateFn } from './features/converters/utilities/payloadPopulateFn.js'
|
export { getPayloadPopulateFn } from './features/converters/utilities/payloadPopulateFn.js'
|
||||||
|
|
||||||
export { getRestPopulateFn } from './features/converters/utilities/restPopulateFn.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 { TestRecorderFeature } from './features/debug/testRecorder/server/index.js'
|
||||||
export { TreeViewFeature } from './features/debug/treeView/server/index.js'
|
export { TreeViewFeature } from './features/debug/treeView/server/index.js'
|
||||||
export { EXPERIMENTAL_TableFeature } from './features/experimental_table/server/index.js'
|
export { EXPERIMENTAL_TableFeature } from './features/experimental_table/server/index.js'
|
||||||
|
|||||||
@@ -110,10 +110,10 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
customComponents: { AfterInput, BeforeInput, Description, Error, Label, RowLabels } = {},
|
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
|
||||||
disabled,
|
disabled,
|
||||||
errorPaths,
|
errorPaths,
|
||||||
rows: rowsData = [],
|
rows = [],
|
||||||
showError,
|
showError,
|
||||||
valid,
|
valid,
|
||||||
value,
|
value,
|
||||||
@@ -173,12 +173,12 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
|
|||||||
(collapsed: boolean) => {
|
(collapsed: boolean) => {
|
||||||
const { collapsedIDs, updatedRows } = toggleAllRows({
|
const { collapsedIDs, updatedRows } = toggleAllRows({
|
||||||
collapsed,
|
collapsed,
|
||||||
rows: rowsData,
|
rows,
|
||||||
})
|
})
|
||||||
setDocFieldPreferences(path, { collapsed: collapsedIDs })
|
setDocFieldPreferences(path, { collapsed: collapsedIDs })
|
||||||
dispatchFields({ type: 'SET_ALL_ROWS_COLLAPSED', path, updatedRows })
|
dispatchFields({ type: 'SET_ALL_ROWS_COLLAPSED', path, updatedRows })
|
||||||
},
|
},
|
||||||
[dispatchFields, path, rowsData, setDocFieldPreferences],
|
[dispatchFields, path, rows, setDocFieldPreferences],
|
||||||
)
|
)
|
||||||
|
|
||||||
const setCollapse = useCallback(
|
const setCollapse = useCallback(
|
||||||
@@ -186,22 +186,22 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
|
|||||||
const { collapsedIDs, updatedRows } = extractRowsAndCollapsedIDs({
|
const { collapsedIDs, updatedRows } = extractRowsAndCollapsedIDs({
|
||||||
collapsed,
|
collapsed,
|
||||||
rowID,
|
rowID,
|
||||||
rows: rowsData,
|
rows,
|
||||||
})
|
})
|
||||||
|
|
||||||
dispatchFields({ type: 'SET_ROW_COLLAPSED', path, updatedRows })
|
dispatchFields({ type: 'SET_ROW_COLLAPSED', path, updatedRows })
|
||||||
setDocFieldPreferences(path, { collapsed: collapsedIDs })
|
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 fieldErrorCount = errorPaths.length
|
||||||
const fieldHasErrors = submitted && errorPaths.length > 0
|
const fieldHasErrors = submitted && errorPaths.length > 0
|
||||||
|
|
||||||
const showRequired = (readOnly || disabled) && rowsData.length === 0
|
const showRequired = (readOnly || disabled) && rows.length === 0
|
||||||
const showMinRows = rowsData.length < minRows || (required && rowsData.length === 0)
|
const showMinRows = rows.length < minRows || (required && rows.length === 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -242,7 +242,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
|
|||||||
<ErrorPill count={fieldErrorCount} i18n={i18n} withMessage />
|
<ErrorPill count={fieldErrorCount} i18n={i18n} withMessage />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{rowsData?.length > 0 && (
|
{rows?.length > 0 && (
|
||||||
<ul className={`${baseClass}__header-actions`}>
|
<ul className={`${baseClass}__header-actions`}>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
@@ -272,13 +272,13 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
|
|||||||
</header>
|
</header>
|
||||||
<NullifyLocaleField fieldValue={value} localized={localized} path={path} />
|
<NullifyLocaleField fieldValue={value} localized={localized} path={path} />
|
||||||
{BeforeInput}
|
{BeforeInput}
|
||||||
{(rowsData?.length > 0 || (!valid && (showRequired || showMinRows))) && (
|
{(rows?.length > 0 || (!valid && (showRequired || showMinRows))) && (
|
||||||
<DraggableSortable
|
<DraggableSortable
|
||||||
className={`${baseClass}__draggable-rows`}
|
className={`${baseClass}__draggable-rows`}
|
||||||
ids={rowsData.map((row) => row.id)}
|
ids={rows.map((row) => row.id)}
|
||||||
onDragEnd={({ moveFromIndex, moveToIndex }) => moveRow(moveFromIndex, moveToIndex)}
|
onDragEnd={({ moveFromIndex, moveToIndex }) => moveRow(moveFromIndex, moveToIndex)}
|
||||||
>
|
>
|
||||||
{rowsData.map((rowData, i) => {
|
{rows.map((rowData, i) => {
|
||||||
const { id: rowID, isLoading } = rowData
|
const { id: rowID, isLoading } = rowData
|
||||||
|
|
||||||
const rowPath = `${path}.${i}`
|
const rowPath = `${path}.${i}`
|
||||||
@@ -297,7 +297,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
|
|||||||
<ArrayRow
|
<ArrayRow
|
||||||
{...draggableSortableItemProps}
|
{...draggableSortableItemProps}
|
||||||
addRow={addRow}
|
addRow={addRow}
|
||||||
CustomRowLabel={RowLabels?.[i]}
|
CustomRowLabel={rows?.[i]?.customComponents?.RowLabel}
|
||||||
duplicateRow={duplicateRow}
|
duplicateRow={duplicateRow}
|
||||||
errorCount={rowErrorCount}
|
errorCount={rowErrorCount}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
@@ -313,7 +313,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
|
|||||||
readOnly={readOnly || disabled}
|
readOnly={readOnly || disabled}
|
||||||
removeRow={removeRow}
|
removeRow={removeRow}
|
||||||
row={rowData}
|
row={rowData}
|
||||||
rowCount={rowsData?.length}
|
rowCount={rows?.length}
|
||||||
rowIndex={i}
|
rowIndex={i}
|
||||||
schemaPath={schemaPath}
|
schemaPath={schemaPath}
|
||||||
setCollapse={setCollapse}
|
setCollapse={setCollapse}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
customComponents: { AfterInput, BeforeInput, Description, Error, Label, RowLabels } = {},
|
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
|
||||||
disabled,
|
disabled,
|
||||||
errorPaths,
|
errorPaths,
|
||||||
rows = [],
|
rows = [],
|
||||||
@@ -293,7 +293,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
|
|||||||
hasMaxRows={hasMaxRows}
|
hasMaxRows={hasMaxRows}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isSortable={isSortable}
|
isSortable={isSortable}
|
||||||
Label={RowLabels?.[i]}
|
Label={rows?.[i]?.customComponents?.RowLabel}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
moveRow={moveRow}
|
moveRow={moveRow}
|
||||||
parentPath={path}
|
parentPath={path}
|
||||||
|
|||||||
@@ -53,24 +53,14 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
|
|||||||
[`${path}.${rowIndex}.id`]: {
|
[`${path}.${rowIndex}.id`]: {
|
||||||
initialValue: newRow.id,
|
initialValue: newRow.id,
|
||||||
passesCondition: true,
|
passesCondition: true,
|
||||||
requiresRender: true,
|
|
||||||
valid: true,
|
valid: true,
|
||||||
value: newRow.id,
|
value: newRow.id,
|
||||||
},
|
},
|
||||||
[path]: {
|
[path]: {
|
||||||
...state[path],
|
...state[path],
|
||||||
disableFormData: true,
|
disableFormData: true,
|
||||||
requiresRender: true,
|
|
||||||
rows: withNewRow,
|
rows: withNewRow,
|
||||||
value: siblingRows.length,
|
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 { remainingFields, rows } = separateRows(path, state)
|
||||||
const rowsMetadata = [...(state[path].rows || [])]
|
const rowsMetadata = [...(state[path].rows || [])]
|
||||||
|
|
||||||
const duplicateRowMetadata = deepCopyObjectSimple(rowsMetadata[rowIndex])
|
const duplicateRowMetadata = deepCopyObjectSimpleWithoutReactComponents(
|
||||||
|
rowsMetadata[rowIndex],
|
||||||
|
)
|
||||||
|
|
||||||
if (duplicateRowMetadata.id) {
|
if (duplicateRowMetadata.id) {
|
||||||
duplicateRowMetadata.id = new ObjectId().toHexString()
|
duplicateRowMetadata.id = new ObjectId().toHexString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const duplicateRowState = deepCopyObjectSimpleWithoutReactComponents(rows[rowIndex])
|
const duplicateRowState = deepCopyObjectSimpleWithoutReactComponents(rows[rowIndex])
|
||||||
|
|
||||||
if (duplicateRowState.id) {
|
if (duplicateRowState.id) {
|
||||||
duplicateRowState.id.value = new ObjectId().toHexString()
|
duplicateRowState.id.value = new ObjectId().toHexString()
|
||||||
duplicateRowState.id.initialValue = new ObjectId().toHexString()
|
duplicateRowState.id.initialValue = new ObjectId().toHexString()
|
||||||
@@ -177,17 +171,8 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
|
|||||||
[path]: {
|
[path]: {
|
||||||
...state[path],
|
...state[path],
|
||||||
disableFormData: true,
|
disableFormData: true,
|
||||||
requiresRender: true,
|
|
||||||
rows: rowsMetadata,
|
rows: rowsMetadata,
|
||||||
value: rows.length,
|
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),
|
...flattenRows(path, topLevelRows),
|
||||||
[path]: {
|
[path]: {
|
||||||
...state[path],
|
...state[path],
|
||||||
requiresRender: true,
|
|
||||||
rows: rowsWithinField,
|
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
|
return newState
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,17 +227,8 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
|
|||||||
[path]: {
|
[path]: {
|
||||||
...state[path],
|
...state[path],
|
||||||
disableFormData: rows.length > 0,
|
disableFormData: rows.length > 0,
|
||||||
requiresRender: true,
|
|
||||||
rows: rowsMetadata,
|
rows: rowsMetadata,
|
||||||
value: rows.length,
|
value: rows.length,
|
||||||
...(state[path]?.requiresRender === true
|
|
||||||
? {
|
|
||||||
serverPropsToIgnore: [
|
|
||||||
...(state[path]?.serverPropsToIgnore || []),
|
|
||||||
'requiresRender',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: state[path]?.serverPropsToIgnore || []),
|
|
||||||
},
|
},
|
||||||
...flattenRows(path, rows),
|
...flattenRows(path, rows),
|
||||||
}
|
}
|
||||||
@@ -323,14 +268,6 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
|
|||||||
disableFormData: true,
|
disableFormData: true,
|
||||||
rows: rowsMetadata,
|
rows: rowsMetadata,
|
||||||
value: siblingRows.length,
|
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 newRows: unknown[] = getDataByPath(path) || []
|
||||||
const rowIndex = rowIndexArg === undefined ? newRows.length : rowIndexArg
|
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.
|
// This performs no form state request, as the debounced onChange effect will do that for us.
|
||||||
dispatchFields({
|
dispatchFields({
|
||||||
type: 'ADD_ROW',
|
type: 'ADD_ROW',
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export const mergeServerFormState = ({
|
|||||||
'errorMessage',
|
'errorMessage',
|
||||||
'errorPaths',
|
'errorPaths',
|
||||||
'customComponents',
|
'customComponents',
|
||||||
'requiresRender',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if (acceptValues) {
|
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
|
* 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
|
* 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 {
|
import type {
|
||||||
|
BuildFormStateArgs,
|
||||||
ClientFieldSchemaMap,
|
ClientFieldSchemaMap,
|
||||||
Data,
|
Data,
|
||||||
DocumentPreferences,
|
DocumentPreferences,
|
||||||
@@ -9,6 +10,7 @@ import type {
|
|||||||
FormState,
|
FormState,
|
||||||
FormStateWithoutComponents,
|
FormStateWithoutComponents,
|
||||||
PayloadRequest,
|
PayloadRequest,
|
||||||
|
Row,
|
||||||
SanitizedFieldPermissions,
|
SanitizedFieldPermissions,
|
||||||
SanitizedFieldsPermissions,
|
SanitizedFieldsPermissions,
|
||||||
SelectMode,
|
SelectMode,
|
||||||
@@ -68,6 +70,7 @@ export type AddFieldStatePromiseArgs = {
|
|||||||
*/
|
*/
|
||||||
includeSchema?: boolean
|
includeSchema?: boolean
|
||||||
indexPath: string
|
indexPath: string
|
||||||
|
mockRSCs?: BuildFormStateArgs['mockRSCs']
|
||||||
/**
|
/**
|
||||||
* Whether to omit parent fields in the state. @default false
|
* Whether to omit parent fields in the state. @default false
|
||||||
*/
|
*/
|
||||||
@@ -122,6 +125,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
fullData,
|
fullData,
|
||||||
includeSchema = false,
|
includeSchema = false,
|
||||||
indexPath,
|
indexPath,
|
||||||
|
mockRSCs,
|
||||||
omitParents = false,
|
omitParents = false,
|
||||||
operation,
|
operation,
|
||||||
parentPath,
|
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
|
let fieldPermissions: SanitizedFieldPermissions = true
|
||||||
|
|
||||||
const fieldState: FieldState = {}
|
const fieldState: FieldState = {}
|
||||||
|
|
||||||
|
if (lastRenderedPath) {
|
||||||
|
fieldState.lastRenderedPath = lastRenderedPath
|
||||||
|
}
|
||||||
|
|
||||||
if (passesCondition === false) {
|
if (passesCondition === false) {
|
||||||
fieldState.passesCondition = false
|
fieldState.passesCondition = false
|
||||||
}
|
}
|
||||||
@@ -289,6 +297,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
forceFullValue,
|
forceFullValue,
|
||||||
fullData,
|
fullData,
|
||||||
includeSchema,
|
includeSchema,
|
||||||
|
mockRSCs,
|
||||||
omitParents,
|
omitParents,
|
||||||
operation,
|
operation,
|
||||||
parentIndexPath: '',
|
parentIndexPath: '',
|
||||||
@@ -299,7 +308,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
fieldPermissions === true ? fieldPermissions : fieldPermissions?.fields || {},
|
fieldPermissions === true ? fieldPermissions : fieldPermissions?.fields || {},
|
||||||
preferences,
|
preferences,
|
||||||
previousFormState,
|
previousFormState,
|
||||||
renderAllFields: requiresRender,
|
renderAllFields,
|
||||||
renderFieldFn,
|
renderFieldFn,
|
||||||
req,
|
req,
|
||||||
select: typeof arraySelect === 'object' ? arraySelect : undefined,
|
select: typeof arraySelect === 'object' ? arraySelect : undefined,
|
||||||
@@ -314,17 +323,25 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
acc.rows = []
|
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,
|
id: row.id,
|
||||||
isLoading: false,
|
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 collapsedRowIDsFromPrefs = preferences?.fields?.[path]?.collapsed
|
||||||
|
|
||||||
const collapsed = (() => {
|
const collapsed = (() => {
|
||||||
// First, check if `previousFormState` has a matching row
|
|
||||||
const previousRow = previousRows.find((prevRow) => prevRow.id === row.id)
|
|
||||||
if (previousRow) {
|
if (previousRow) {
|
||||||
return previousRow.collapsed ?? false
|
return previousRow.collapsed ?? false
|
||||||
}
|
}
|
||||||
@@ -357,8 +374,6 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
fieldState.rows = rows
|
fieldState.rows = rows
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldState.requiresRender = false
|
|
||||||
|
|
||||||
// Add values to field state
|
// Add values to field state
|
||||||
if (data[field.name] !== null) {
|
if (data[field.name] !== null) {
|
||||||
fieldState.value = forceFullValue ? arrayValue : arrayValue.length
|
fieldState.value = forceFullValue ? arrayValue : arrayValue.length
|
||||||
@@ -468,6 +483,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
forceFullValue,
|
forceFullValue,
|
||||||
fullData,
|
fullData,
|
||||||
includeSchema,
|
includeSchema,
|
||||||
|
mockRSCs,
|
||||||
omitParents,
|
omitParents,
|
||||||
operation,
|
operation,
|
||||||
parentIndexPath: '',
|
parentIndexPath: '',
|
||||||
@@ -482,7 +498,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
: parentPermissions?.[field.name]?.blocks?.[block.slug]?.fields || {},
|
: parentPermissions?.[field.name]?.blocks?.[block.slug]?.fields || {},
|
||||||
preferences,
|
preferences,
|
||||||
previousFormState,
|
previousFormState,
|
||||||
renderAllFields: requiresRender,
|
renderAllFields,
|
||||||
renderFieldFn,
|
renderFieldFn,
|
||||||
req,
|
req,
|
||||||
select: typeof blockSelect === 'object' ? blockSelect : undefined,
|
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,
|
id: row.id,
|
||||||
blockType: row.blockType,
|
blockType: row.blockType,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if (previousRow?.lastRenderedPath) {
|
||||||
|
newRow.lastRenderedPath = previousRow.lastRenderedPath
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.rowMetadata.push(newRow)
|
||||||
|
|
||||||
const collapsedRowIDs = preferences?.fields?.[path]?.collapsed
|
const collapsedRowIDs = preferences?.fields?.[path]?.collapsed
|
||||||
|
|
||||||
@@ -536,10 +563,6 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
|
|
||||||
fieldState.rows = rowMetadata
|
fieldState.rows = rowMetadata
|
||||||
|
|
||||||
// Unset requiresRender
|
|
||||||
// so it will be removed from form state
|
|
||||||
fieldState.requiresRender = false
|
|
||||||
|
|
||||||
// Add field to state
|
// Add field to state
|
||||||
if (!omitParents && (!filter || filter(args))) {
|
if (!omitParents && (!filter || filter(args))) {
|
||||||
state[path] = fieldState
|
state[path] = fieldState
|
||||||
@@ -570,6 +593,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
forceFullValue,
|
forceFullValue,
|
||||||
fullData,
|
fullData,
|
||||||
includeSchema,
|
includeSchema,
|
||||||
|
mockRSCs,
|
||||||
omitParents,
|
omitParents,
|
||||||
operation,
|
operation,
|
||||||
parentIndexPath: '',
|
parentIndexPath: '',
|
||||||
@@ -709,6 +733,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
|
|
||||||
await iterateFields({
|
await iterateFields({
|
||||||
id,
|
id,
|
||||||
|
mockRSCs,
|
||||||
select,
|
select,
|
||||||
selectMode,
|
selectMode,
|
||||||
// passthrough parent functionality
|
// passthrough parent functionality
|
||||||
@@ -816,6 +841,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
forceFullValue,
|
forceFullValue,
|
||||||
fullData,
|
fullData,
|
||||||
includeSchema,
|
includeSchema,
|
||||||
|
mockRSCs,
|
||||||
omitParents,
|
omitParents,
|
||||||
operation,
|
operation,
|
||||||
parentIndexPath: isNamedTab ? '' : tabIndexPath,
|
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 fieldState = state[path]
|
||||||
|
|
||||||
const fieldConfig = fieldSchemaMap.get(schemaPath)
|
const fieldConfig = fieldSchemaMap.get(schemaPath)
|
||||||
|
|
||||||
if (!fieldConfig) {
|
if (!fieldConfig && !mockRSCs) {
|
||||||
if (schemaPath.endsWith('.blockType')) {
|
if (schemaPath.endsWith('.blockType')) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
@@ -873,6 +899,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
fieldState,
|
fieldState,
|
||||||
formState: state,
|
formState: state,
|
||||||
indexPath,
|
indexPath,
|
||||||
|
lastRenderedPath,
|
||||||
|
mockRSCs,
|
||||||
operation,
|
operation,
|
||||||
parentPath,
|
parentPath,
|
||||||
parentSchemaPath,
|
parentSchemaPath,
|
||||||
@@ -880,6 +908,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
permissions: fieldPermissions,
|
permissions: fieldPermissions,
|
||||||
preferences,
|
preferences,
|
||||||
previousFieldState: previousFormState?.[path],
|
previousFieldState: previousFormState?.[path],
|
||||||
|
renderAllFields,
|
||||||
req,
|
req,
|
||||||
schemaPath,
|
schemaPath,
|
||||||
siblingData: data,
|
siblingData: data,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
BuildFormStateArgs,
|
||||||
ClientFieldSchemaMap,
|
ClientFieldSchemaMap,
|
||||||
Data,
|
Data,
|
||||||
DocumentPreferences,
|
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.
|
* the initial block data here, which will be used as `blockData` for the top-level fields, until the first block is encountered.
|
||||||
*/
|
*/
|
||||||
initialBlockData?: Data
|
initialBlockData?: Data
|
||||||
|
mockRSCs?: BuildFormStateArgs['mockRSCs']
|
||||||
operation?: 'create' | 'update'
|
operation?: 'create' | 'update'
|
||||||
permissions: SanitizedFieldsPermissions
|
permissions: SanitizedFieldsPermissions
|
||||||
preferences: DocumentPreferences
|
preferences: DocumentPreferences
|
||||||
@@ -86,6 +88,7 @@ export const fieldSchemasToFormState = async ({
|
|||||||
fields,
|
fields,
|
||||||
fieldSchemaMap,
|
fieldSchemaMap,
|
||||||
initialBlockData,
|
initialBlockData,
|
||||||
|
mockRSCs,
|
||||||
operation,
|
operation,
|
||||||
permissions,
|
permissions,
|
||||||
preferences,
|
preferences,
|
||||||
@@ -139,6 +142,7 @@ export const fieldSchemasToFormState = async ({
|
|||||||
fields,
|
fields,
|
||||||
fieldSchemaMap,
|
fieldSchemaMap,
|
||||||
fullData,
|
fullData,
|
||||||
|
mockRSCs,
|
||||||
operation,
|
operation,
|
||||||
parentIndexPath: '',
|
parentIndexPath: '',
|
||||||
parentPassesCondition: true,
|
parentPassesCondition: true,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
BuildFormStateArgs,
|
||||||
ClientFieldSchemaMap,
|
ClientFieldSchemaMap,
|
||||||
Data,
|
Data,
|
||||||
DocumentPreferences,
|
DocumentPreferences,
|
||||||
@@ -46,6 +47,7 @@ type Args = {
|
|||||||
* Whether the field schema should be included in the state. @default false
|
* Whether the field schema should be included in the state. @default false
|
||||||
*/
|
*/
|
||||||
includeSchema?: boolean
|
includeSchema?: boolean
|
||||||
|
mockRSCs?: BuildFormStateArgs['mockRSCs']
|
||||||
/**
|
/**
|
||||||
* Whether to omit parent fields in the state. @default false
|
* Whether to omit parent fields in the state. @default false
|
||||||
*/
|
*/
|
||||||
@@ -94,6 +96,7 @@ export const iterateFields = async ({
|
|||||||
forceFullValue = false,
|
forceFullValue = false,
|
||||||
fullData,
|
fullData,
|
||||||
includeSchema = false,
|
includeSchema = false,
|
||||||
|
mockRSCs,
|
||||||
omitParents = false,
|
omitParents = false,
|
||||||
operation,
|
operation,
|
||||||
parentIndexPath,
|
parentIndexPath,
|
||||||
@@ -180,6 +183,7 @@ export const iterateFields = async ({
|
|||||||
fullData,
|
fullData,
|
||||||
includeSchema,
|
includeSchema,
|
||||||
indexPath,
|
indexPath,
|
||||||
|
mockRSCs,
|
||||||
omitParents,
|
omitParents,
|
||||||
operation,
|
operation,
|
||||||
parentIndexPath,
|
parentIndexPath,
|
||||||
|
|||||||
@@ -34,16 +34,25 @@ export const renderField: RenderFieldMethod = ({
|
|||||||
fieldState,
|
fieldState,
|
||||||
formState,
|
formState,
|
||||||
indexPath,
|
indexPath,
|
||||||
|
lastRenderedPath,
|
||||||
|
mockRSCs,
|
||||||
operation,
|
operation,
|
||||||
parentPath,
|
parentPath,
|
||||||
parentSchemaPath,
|
parentSchemaPath,
|
||||||
path,
|
path,
|
||||||
permissions,
|
permissions,
|
||||||
preferences,
|
preferences,
|
||||||
|
renderAllFields,
|
||||||
req,
|
req,
|
||||||
schemaPath,
|
schemaPath,
|
||||||
siblingData,
|
siblingData,
|
||||||
}) => {
|
}) => {
|
||||||
|
const requiresRender = renderAllFields || !lastRenderedPath || lastRenderedPath !== path
|
||||||
|
|
||||||
|
if (!requiresRender && fieldConfig.type !== 'array' && fieldConfig.type !== 'blocks') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const clientField = clientFieldSchemaMap
|
const clientField = clientFieldSchemaMap
|
||||||
? (clientFieldSchemaMap.get(schemaPath) as ClientField)
|
? (clientFieldSchemaMap.get(schemaPath) as ClientField)
|
||||||
: createClientField({
|
: createClientField({
|
||||||
@@ -53,10 +62,6 @@ export const renderField: RenderFieldMethod = ({
|
|||||||
importMap: req.payload.importMap,
|
importMap: req.payload.importMap,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (fieldIsHiddenOrDisabled(clientField)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientProps: ClientComponentProps & Partial<FieldPaths> = {
|
const clientProps: ClientComponentProps & Partial<FieldPaths> = {
|
||||||
field: clientField,
|
field: clientField,
|
||||||
path,
|
path,
|
||||||
@@ -97,6 +102,112 @@ export const renderField: RenderFieldMethod = ({
|
|||||||
user: req.user,
|
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.
|
* Only create the `customComponents` object if needed.
|
||||||
* This will prevent unnecessary data from being transferred to the client.
|
* This will prevent unnecessary data from being transferred to the client.
|
||||||
@@ -114,70 +225,6 @@ export const renderField: RenderFieldMethod = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (fieldConfig.type) {
|
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': {
|
case 'richText': {
|
||||||
if (!fieldConfig?.editor) {
|
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
|
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 = {}
|
fieldConfig.admin.components = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldState.customComponents.Field = (
|
fieldState.customComponents.Field = !mockRSCs ? (
|
||||||
<WatchCondition path={path}>
|
<WatchCondition path={path}>
|
||||||
{RenderServerComponent({
|
{RenderServerComponent({
|
||||||
clientProps,
|
clientProps,
|
||||||
@@ -204,6 +251,8 @@ export const renderField: RenderFieldMethod = ({
|
|||||||
serverProps,
|
serverProps,
|
||||||
})}
|
})}
|
||||||
</WatchCondition>
|
</WatchCondition>
|
||||||
|
) : (
|
||||||
|
'Mock'
|
||||||
)
|
)
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -219,13 +268,15 @@ export const renderField: RenderFieldMethod = ({
|
|||||||
|
|
||||||
const Component = fieldConfig.admin.components[key]
|
const Component = fieldConfig.admin.components[key]
|
||||||
|
|
||||||
fieldState.customComponents[key] = RenderServerComponent({
|
fieldState.customComponents[key] = !mockRSCs
|
||||||
clientProps,
|
? RenderServerComponent({
|
||||||
Component,
|
clientProps,
|
||||||
importMap: req.payload.importMap,
|
Component,
|
||||||
key: `field.admin.components.${key}`,
|
importMap: req.payload.importMap,
|
||||||
serverProps,
|
key: `field.admin.components.${key}`,
|
||||||
})
|
serverProps,
|
||||||
|
})
|
||||||
|
: 'Mock'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -241,7 +292,7 @@ export const renderField: RenderFieldMethod = ({
|
|||||||
'description' in fieldConfig.admin &&
|
'description' in fieldConfig.admin &&
|
||||||
typeof fieldConfig.admin?.description === 'function'
|
typeof fieldConfig.admin?.description === 'function'
|
||||||
) {
|
) {
|
||||||
fieldState.customComponents.Description = (
|
fieldState.customComponents.Description = !mockRSCs ? (
|
||||||
<FieldDescription
|
<FieldDescription
|
||||||
description={fieldConfig.admin?.description({
|
description={fieldConfig.admin?.description({
|
||||||
i18n: req.i18n,
|
i18n: req.i18n,
|
||||||
@@ -249,62 +300,74 @@ export const renderField: RenderFieldMethod = ({
|
|||||||
})}
|
})}
|
||||||
path={path}
|
path={path}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
'Mock'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldConfig.admin?.components) {
|
if (fieldConfig.admin?.components) {
|
||||||
if ('afterInput' in fieldConfig.admin.components) {
|
if ('afterInput' in fieldConfig.admin.components) {
|
||||||
fieldState.customComponents.AfterInput = RenderServerComponent({
|
fieldState.customComponents.AfterInput = !mockRSCs
|
||||||
clientProps,
|
? RenderServerComponent({
|
||||||
Component: fieldConfig.admin.components.afterInput,
|
clientProps,
|
||||||
importMap: req.payload.importMap,
|
Component: fieldConfig.admin.components.afterInput,
|
||||||
key: 'field.admin.components.afterInput',
|
importMap: req.payload.importMap,
|
||||||
serverProps,
|
key: 'field.admin.components.afterInput',
|
||||||
})
|
serverProps,
|
||||||
|
})
|
||||||
|
: 'Mock'
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('beforeInput' in fieldConfig.admin.components) {
|
if ('beforeInput' in fieldConfig.admin.components) {
|
||||||
fieldState.customComponents.BeforeInput = RenderServerComponent({
|
fieldState.customComponents.BeforeInput = !mockRSCs
|
||||||
clientProps,
|
? RenderServerComponent({
|
||||||
Component: fieldConfig.admin.components.beforeInput,
|
clientProps,
|
||||||
importMap: req.payload.importMap,
|
Component: fieldConfig.admin.components.beforeInput,
|
||||||
key: 'field.admin.components.beforeInput',
|
importMap: req.payload.importMap,
|
||||||
serverProps,
|
key: 'field.admin.components.beforeInput',
|
||||||
})
|
serverProps,
|
||||||
|
})
|
||||||
|
: 'Mock'
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('Description' in fieldConfig.admin.components) {
|
if ('Description' in fieldConfig.admin.components) {
|
||||||
fieldState.customComponents.Description = RenderServerComponent({
|
fieldState.customComponents.Description = !mockRSCs
|
||||||
clientProps,
|
? RenderServerComponent({
|
||||||
Component: fieldConfig.admin.components.Description,
|
clientProps,
|
||||||
importMap: req.payload.importMap,
|
Component: fieldConfig.admin.components.Description,
|
||||||
key: 'field.admin.components.Description',
|
importMap: req.payload.importMap,
|
||||||
serverProps,
|
key: 'field.admin.components.Description',
|
||||||
})
|
serverProps,
|
||||||
|
})
|
||||||
|
: 'Mock'
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('Error' in fieldConfig.admin.components) {
|
if ('Error' in fieldConfig.admin.components) {
|
||||||
fieldState.customComponents.Error = RenderServerComponent({
|
fieldState.customComponents.Error = !mockRSCs
|
||||||
clientProps,
|
? RenderServerComponent({
|
||||||
Component: fieldConfig.admin.components.Error,
|
clientProps,
|
||||||
importMap: req.payload.importMap,
|
Component: fieldConfig.admin.components.Error,
|
||||||
key: 'field.admin.components.Error',
|
importMap: req.payload.importMap,
|
||||||
serverProps,
|
key: 'field.admin.components.Error',
|
||||||
})
|
serverProps,
|
||||||
|
})
|
||||||
|
: 'Mock'
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('Label' in fieldConfig.admin.components) {
|
if ('Label' in fieldConfig.admin.components) {
|
||||||
fieldState.customComponents.Label = RenderServerComponent({
|
fieldState.customComponents.Label = !mockRSCs
|
||||||
clientProps,
|
? RenderServerComponent({
|
||||||
Component: fieldConfig.admin.components.Label,
|
clientProps,
|
||||||
importMap: req.payload.importMap,
|
Component: fieldConfig.admin.components.Label,
|
||||||
key: 'field.admin.components.Label',
|
importMap: req.payload.importMap,
|
||||||
serverProps,
|
key: 'field.admin.components.Label',
|
||||||
})
|
serverProps,
|
||||||
|
})
|
||||||
|
: 'Mock'
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('Field' in fieldConfig.admin.components) {
|
if ('Field' in fieldConfig.admin.components) {
|
||||||
fieldState.customComponents.Field = (
|
fieldState.customComponents.Field = !mockRSCs ? (
|
||||||
<WatchCondition path={path}>
|
<WatchCondition path={path}>
|
||||||
{RenderServerComponent({
|
{RenderServerComponent({
|
||||||
clientProps,
|
clientProps,
|
||||||
@@ -314,6 +377,8 @@ export const renderField: RenderFieldMethod = ({
|
|||||||
serverProps,
|
serverProps,
|
||||||
})}
|
})}
|
||||||
</WatchCondition>
|
</WatchCondition>
|
||||||
|
) : (
|
||||||
|
'Mock'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export type RenderFieldArgs = {
|
|||||||
formState: FormState
|
formState: FormState
|
||||||
id?: number | string
|
id?: number | string
|
||||||
indexPath: string
|
indexPath: string
|
||||||
|
lastRenderedPath: string
|
||||||
|
mockRSCs?: boolean
|
||||||
operation: Operation
|
operation: Operation
|
||||||
parentPath: string
|
parentPath: string
|
||||||
parentSchemaPath: string
|
parentSchemaPath: string
|
||||||
@@ -28,6 +30,7 @@ export type RenderFieldArgs = {
|
|||||||
permissions: SanitizedFieldPermissions
|
permissions: SanitizedFieldPermissions
|
||||||
preferences: DocumentPreferences
|
preferences: DocumentPreferences
|
||||||
previousFieldState: FieldState
|
previousFieldState: FieldState
|
||||||
|
renderAllFields: boolean
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
schemaPath: string
|
schemaPath: string
|
||||||
siblingData: Data
|
siblingData: Data
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export type IListQueryContext = {
|
|||||||
data: PaginatedDocs
|
data: PaginatedDocs
|
||||||
defaultLimit?: number
|
defaultLimit?: number
|
||||||
defaultSort?: Sort
|
defaultSort?: Sort
|
||||||
orderableFieldName?: string
|
|
||||||
modified: boolean
|
modified: boolean
|
||||||
|
orderableFieldName?: string
|
||||||
query: ListQuery
|
query: ListQuery
|
||||||
refineListData: (args: ListQuery, setModified?: boolean) => Promise<void>
|
refineListData: (args: ListQuery, setModified?: boolean) => Promise<void>
|
||||||
setModified: (modified: boolean) => void
|
setModified: (modified: boolean) => void
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export const buildFormState = async (
|
|||||||
globalSlug,
|
globalSlug,
|
||||||
initialBlockData,
|
initialBlockData,
|
||||||
initialBlockFormState,
|
initialBlockFormState,
|
||||||
|
mockRSCs,
|
||||||
operation,
|
operation,
|
||||||
renderAllFields,
|
renderAllFields,
|
||||||
req,
|
req,
|
||||||
@@ -205,6 +206,7 @@ export const buildFormState = async (
|
|||||||
fields,
|
fields,
|
||||||
fieldSchemaMap: schemaMap,
|
fieldSchemaMap: schemaMap,
|
||||||
initialBlockData: blockData,
|
initialBlockData: blockData,
|
||||||
|
mockRSCs,
|
||||||
operation,
|
operation,
|
||||||
permissions: docPermissions?.fields || {},
|
permissions: docPermissions?.fields || {},
|
||||||
preferences: docPreferences || { fields: {} },
|
preferences: docPreferences || { fields: {} },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { MigrateDownArgs, MigrateUpArgs} from '@payloadcms/db-sqlite';
|
import type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/db-sqlite'
|
||||||
|
|
||||||
import { sql } from '@payloadcms/db-sqlite'
|
import { sql } from '@payloadcms/db-sqlite'
|
||||||
|
|
||||||
|
|||||||
5
test/form-state/collections/Posts/ArrayRowLabel.tsx
Normal file
5
test/form-state/collections/Posts/ArrayRowLabel.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const ArrayRowLabel = () => {
|
||||||
|
return <p>This is a custom component</p>
|
||||||
|
}
|
||||||
@@ -69,6 +69,11 @@ export const PostsCollection: CollectionConfig = {
|
|||||||
{
|
{
|
||||||
name: 'array',
|
name: 'array',
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
RowLabel: './collections/Posts/ArrayRowLabel.js#ArrayRowLabel',
|
||||||
|
},
|
||||||
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'richText',
|
name: 'richText',
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||||
import { TEST_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
import { TEST_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||||
|
import { postsSlug } from './collections/Posts/index.js'
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@@ -36,7 +37,7 @@ test.describe('Form State', () => {
|
|||||||
test.beforeAll(async ({ browser }, testInfo) => {
|
test.beforeAll(async ({ browser }, testInfo) => {
|
||||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||||
;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname }))
|
;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname }))
|
||||||
postsUrl = new AdminUrlUtil(serverURL, 'posts')
|
postsUrl = new AdminUrlUtil(serverURL, postsSlug)
|
||||||
|
|
||||||
context = await browser.newContext()
|
context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
@@ -146,6 +147,64 @@ test.describe('Form State', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should send `lastRenderedPath` only when necessary', async () => {
|
||||||
|
await page.goto(postsUrl.create)
|
||||||
|
const field = page.locator('#field-title')
|
||||||
|
await field.fill('Test')
|
||||||
|
|
||||||
|
// The `array` itself SHOULD have a `lastRenderedPath` because it was rendered on initial load
|
||||||
|
await assertRequestBody<{ args: { formState: FormState } }[]>(page, {
|
||||||
|
action: await page.locator('#field-array .array-field__add-row').click(),
|
||||||
|
url: postsUrl.create,
|
||||||
|
expect: (body) =>
|
||||||
|
Boolean(
|
||||||
|
body?.[0]?.args?.formState?.['array'] && body[0].args.formState['array'].lastRenderedPath,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url() === postsUrl.create &&
|
||||||
|
response.status() === 200 &&
|
||||||
|
response.headers()['content-type'] === 'text/x-component',
|
||||||
|
)
|
||||||
|
|
||||||
|
// The `array` itself SHOULD still have a `lastRenderedPath`
|
||||||
|
// The rich text field in the first row SHOULD ALSO have a `lastRenderedPath` bc it was rendered in the first request
|
||||||
|
await assertRequestBody<{ args: { formState: FormState } }[]>(page, {
|
||||||
|
action: await page.locator('#field-array .array-field__add-row').click(),
|
||||||
|
url: postsUrl.create,
|
||||||
|
expect: (body) =>
|
||||||
|
Boolean(
|
||||||
|
body?.[0]?.args?.formState?.['array'] &&
|
||||||
|
body[0].args.formState['array'].lastRenderedPath &&
|
||||||
|
body[0].args.formState['array.0.richText']?.lastRenderedPath,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url() === postsUrl.create &&
|
||||||
|
response.status() === 200 &&
|
||||||
|
response.headers()['content-type'] === 'text/x-component',
|
||||||
|
)
|
||||||
|
|
||||||
|
// The `array` itself SHOULD still have a `lastRenderedPath`
|
||||||
|
// The rich text field in the first row SHOULD ALSO have a `lastRenderedPath` bc it was rendered in the first request
|
||||||
|
// The rich text field in the second row SHOULD ALSO have a `lastRenderedPath` bc it was rendered in the second request
|
||||||
|
await assertRequestBody<{ args: { formState: FormState } }[]>(page, {
|
||||||
|
action: await page.locator('#field-array .array-field__add-row').click(),
|
||||||
|
url: postsUrl.create,
|
||||||
|
expect: (body) =>
|
||||||
|
Boolean(
|
||||||
|
body?.[0]?.args?.formState?.['array'] &&
|
||||||
|
body[0].args.formState['array'].lastRenderedPath &&
|
||||||
|
body[0].args.formState['array.0.richText']?.lastRenderedPath &&
|
||||||
|
body[0].args.formState['array.1.richText']?.lastRenderedPath,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('should queue onChange functions', async () => {
|
test('should queue onChange functions', async () => {
|
||||||
await page.goto(postsUrl.create)
|
await page.goto(postsUrl.create)
|
||||||
const field = page.locator('#field-title')
|
const field = page.locator('#field-title')
|
||||||
@@ -182,7 +241,7 @@ test.describe('Form State', () => {
|
|||||||
await cdpSession.detach()
|
await cdpSession.detach()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not cause nested custom fields to disappear when queuing form state (1)', async () => {
|
test('should not cause nested custom components to disappear when adding a row then editing a field', async () => {
|
||||||
await page.goto(postsUrl.create)
|
await page.goto(postsUrl.create)
|
||||||
const field = page.locator('#field-title')
|
const field = page.locator('#field-title')
|
||||||
await field.fill('Test')
|
await field.fill('Test')
|
||||||
@@ -193,23 +252,12 @@ test.describe('Form State', () => {
|
|||||||
delay: 'Slow 3G',
|
delay: 'Slow 3G',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add a row and immediately type into another field
|
|
||||||
// Test that the rich text field within the row does not disappear
|
|
||||||
await assertNetworkRequests(
|
await assertNetworkRequests(
|
||||||
page,
|
page,
|
||||||
postsUrl.create,
|
postsUrl.create,
|
||||||
async () => {
|
async () => {
|
||||||
// Ensure `requiresRender` is `true` is set for the first request
|
await page.locator('#field-array .array-field__add-row').click()
|
||||||
await assertRequestBody<{ args: { formState: FormState } }[]>(page, {
|
await page.locator('#field-title').fill('Test 2')
|
||||||
action: page.locator('#field-array .array-field__add-row').click(),
|
|
||||||
expect: (body) => body[0]?.args?.formState?.array?.requiresRender === true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Ensure `requiresRender` is `false` for the second request
|
|
||||||
await assertRequestBody<{ args: { formState: FormState } }[]>(page, {
|
|
||||||
action: page.locator('#field-title').fill('Title 2'),
|
|
||||||
expect: (body) => body[0]?.args?.formState?.array?.requiresRender === false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// use `waitForSelector` to ensure the element doesn't appear and then disappear
|
// use `waitForSelector` to ensure the element doesn't appear and then disappear
|
||||||
// eslint-disable-next-line playwright/no-wait-for-selector
|
// eslint-disable-next-line playwright/no-wait-for-selector
|
||||||
@@ -237,7 +285,7 @@ test.describe('Form State', () => {
|
|||||||
await cdpSession.detach()
|
await cdpSession.detach()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not cause nested custom fields to disappear when queuing form state (2)', async () => {
|
test('should not cause nested custom components to disappear when adding rows back-to-back', async () => {
|
||||||
await page.goto(postsUrl.create)
|
await page.goto(postsUrl.create)
|
||||||
const field = page.locator('#field-title')
|
const field = page.locator('#field-title')
|
||||||
await field.fill('Test')
|
await field.fill('Test')
|
||||||
@@ -254,17 +302,8 @@ test.describe('Form State', () => {
|
|||||||
page,
|
page,
|
||||||
postsUrl.create,
|
postsUrl.create,
|
||||||
async () => {
|
async () => {
|
||||||
// Ensure `requiresRender` is `true` is set for the first request
|
await page.locator('#field-array .array-field__add-row').click()
|
||||||
await assertRequestBody<{ args: { formState: FormState } }[]>(page, {
|
await page.locator('#field-array .array-field__add-row').click()
|
||||||
action: page.locator('#field-array .array-field__add-row').click(),
|
|
||||||
expect: (body) => body[0]?.args?.formState?.array?.requiresRender === true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Ensure `requiresRender` is `true` is set for the second request
|
|
||||||
await assertRequestBody<{ args: { formState: FormState } }[]>(page, {
|
|
||||||
action: page.locator('#field-array .array-field__add-row').click(),
|
|
||||||
expect: (body) => body[0]?.args?.formState?.array?.requiresRender === true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// use `waitForSelector` to ensure the element doesn't appear and then disappear
|
// use `waitForSelector` to ensure the element doesn't appear and then disappear
|
||||||
// eslint-disable-next-line playwright/no-wait-for-selector
|
// eslint-disable-next-line playwright/no-wait-for-selector
|
||||||
@@ -292,12 +331,6 @@ test.describe('Form State', () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ensure `requiresRender` is `false` for the third request
|
|
||||||
await assertRequestBody<{ args: { formState: FormState } }[]>(page, {
|
|
||||||
action: page.locator('#field-title').fill('Title 2'),
|
|
||||||
expect: (body) => body[0]?.args?.formState?.array?.requiresRender === false,
|
|
||||||
})
|
|
||||||
|
|
||||||
await cdpSession.send('Network.emulateNetworkConditions', {
|
await cdpSession.send('Network.emulateNetworkConditions', {
|
||||||
offline: false,
|
offline: false,
|
||||||
latency: 0,
|
latency: 0,
|
||||||
|
|||||||
@@ -21,11 +21,8 @@ const filename = fileURLToPath(import.meta.url)
|
|||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
describe('Form State', () => {
|
describe('Form State', () => {
|
||||||
// --__--__--__--__--__--__--__--__--__
|
|
||||||
// Boilerplate test setup/teardown
|
|
||||||
// --__--__--__--__--__--__--__--__--__
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
;({ payload, restClient } = await initPayloadInt(dirname, undefined, true))
|
||||||
|
|
||||||
const data = await restClient
|
const data = await restClient
|
||||||
.POST('/users/login', {
|
.POST('/users/login', {
|
||||||
@@ -57,6 +54,7 @@ describe('Form State', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { state } = await buildFormState({
|
const { state } = await buildFormState({
|
||||||
|
mockRSCs: true,
|
||||||
id: postData.id,
|
id: postData.id,
|
||||||
collectionSlug: postsSlug,
|
collectionSlug: postsSlug,
|
||||||
data: postData,
|
data: postData,
|
||||||
@@ -95,7 +93,6 @@ describe('Form State', () => {
|
|||||||
validateUsingEvent: {},
|
validateUsingEvent: {},
|
||||||
blocks: {
|
blocks: {
|
||||||
initialValue: 0,
|
initialValue: 0,
|
||||||
requiresRender: false,
|
|
||||||
rows: [],
|
rows: [],
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
@@ -113,6 +110,7 @@ describe('Form State', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { state } = await buildFormState({
|
const { state } = await buildFormState({
|
||||||
|
mockRSCs: true,
|
||||||
id: postData.id,
|
id: postData.id,
|
||||||
collectionSlug: postsSlug,
|
collectionSlug: postsSlug,
|
||||||
data: postData,
|
data: postData,
|
||||||
@@ -134,9 +132,89 @@ describe('Form State', () => {
|
|||||||
title: {
|
title: {
|
||||||
value: postData.title,
|
value: postData.title,
|
||||||
initialValue: postData.title,
|
initialValue: postData.title,
|
||||||
|
lastRenderedPath: 'title',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it.todo('should skip validation if specified')
|
it('should not render custom components when `lastRenderedPath` exists', async () => {
|
||||||
|
const req = await createLocalReq({ user }, payload)
|
||||||
|
|
||||||
|
const { state: stateWithRow } = await buildFormState({
|
||||||
|
mockRSCs: true,
|
||||||
|
collectionSlug: postsSlug,
|
||||||
|
formState: {
|
||||||
|
array: {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'array.0.id': {
|
||||||
|
value: '123',
|
||||||
|
initialValue: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
docPermissions: undefined,
|
||||||
|
docPreferences: {
|
||||||
|
fields: {},
|
||||||
|
},
|
||||||
|
documentFormState: undefined,
|
||||||
|
operation: 'update',
|
||||||
|
renderAllFields: false,
|
||||||
|
req,
|
||||||
|
schemaPath: postsSlug,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure that row 1 _DOES_ return with rendered components
|
||||||
|
expect(stateWithRow?.['array.0.richText']?.lastRenderedPath).toStrictEqual('array.0.richText')
|
||||||
|
expect(stateWithRow?.['array.0.richText']?.customComponents?.Field).toBeDefined()
|
||||||
|
|
||||||
|
const { state: stateWithTitle } = await buildFormState({
|
||||||
|
mockRSCs: true,
|
||||||
|
collectionSlug: postsSlug,
|
||||||
|
formState: {
|
||||||
|
array: {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '456',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'array.0.id': {
|
||||||
|
value: '123',
|
||||||
|
initialValue: '123',
|
||||||
|
},
|
||||||
|
'array.0.richText': {
|
||||||
|
lastRenderedPath: 'array.0.richText',
|
||||||
|
},
|
||||||
|
'array.1.id': {
|
||||||
|
value: '456',
|
||||||
|
initialValue: '456',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
docPermissions: undefined,
|
||||||
|
docPreferences: {
|
||||||
|
fields: {},
|
||||||
|
},
|
||||||
|
documentFormState: undefined,
|
||||||
|
operation: 'update',
|
||||||
|
renderAllFields: false,
|
||||||
|
schemaPath: postsSlug,
|
||||||
|
req,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure that row 1 _DOES NOT_ return with rendered components
|
||||||
|
expect(stateWithTitle?.['array.0.richText']).toHaveProperty('lastRenderedPath')
|
||||||
|
expect(stateWithTitle?.['array.0.richText']).not.toHaveProperty('customComponents')
|
||||||
|
|
||||||
|
// Ensure that row 2 _DOES_ return with rendered components
|
||||||
|
expect(stateWithTitle?.['array.1.richText']).toHaveProperty('lastRenderedPath')
|
||||||
|
expect(stateWithTitle?.['array.1.richText']).toHaveProperty('customComponents')
|
||||||
|
expect(stateWithTitle?.['array.1.richText']?.customComponents?.Field).toBeDefined()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,15 +2,38 @@ import type { Page } from '@playwright/test'
|
|||||||
|
|
||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper function to assert the body of a network request.
|
||||||
|
* This is useful for reading the body of a request and testing whether it is correct.
|
||||||
|
* For example, if you have a form that submits data to an API, you can use this function to
|
||||||
|
* assert that the data being sent is correct.
|
||||||
|
* @param page The Playwright page
|
||||||
|
* @param options Options
|
||||||
|
* @param options.action The action to perform that will trigger the request
|
||||||
|
* @param options.expect A function to run after the request is made to assert the request body
|
||||||
|
* @returns The request body
|
||||||
|
* @example
|
||||||
|
* const requestBody = await assertRequestBody(page, {
|
||||||
|
* action: page.click('button'),
|
||||||
|
* expect: (requestBody) => expect(requestBody.foo).toBe('bar')
|
||||||
|
* })
|
||||||
|
*/
|
||||||
export const assertRequestBody = async <T>(
|
export const assertRequestBody = async <T>(
|
||||||
page: Page,
|
page: Page,
|
||||||
options: {
|
options: {
|
||||||
action: Promise<void> | void
|
action: Promise<void> | void
|
||||||
expect?: (requestBody: T) => boolean | Promise<boolean>
|
expect?: (requestBody: T) => boolean | Promise<boolean>
|
||||||
|
requestMethod?: string
|
||||||
|
url: string
|
||||||
},
|
},
|
||||||
): Promise<T | undefined> => {
|
): Promise<T | undefined> => {
|
||||||
const [request] = await Promise.all([
|
const [request] = await Promise.all([
|
||||||
page.waitForRequest((request) => request.method() === 'POST'), // Adjust condition as needed
|
page.waitForRequest((request) =>
|
||||||
|
Boolean(
|
||||||
|
request.url().startsWith(options.url) &&
|
||||||
|
(request.method() === options.requestMethod || 'POST'),
|
||||||
|
),
|
||||||
|
),
|
||||||
await options.action,
|
await options.action,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
86
test/helpers/e2e/assertResponseBody.ts
Normal file
86
test/helpers/e2e/assertResponseBody.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
|
function parseRSC(rscText: string) {
|
||||||
|
// Next.js streams use special delimiters like "\n"
|
||||||
|
const chunks = rscText.split('\n').filter((line) => line.trim() !== '')
|
||||||
|
|
||||||
|
// find the chunk starting with '1:', remove the '1:' prefix and parse the rest
|
||||||
|
const match = chunks.find((chunk) => chunk.startsWith('1:'))
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const jsonString = match.slice(2).trim()
|
||||||
|
if (jsonString) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(jsonString)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to parse JSON:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper function to assert the response of a network request.
|
||||||
|
* This is useful for reading the response of a request and testing whether it is correct.
|
||||||
|
* For example, if you have a form that submits data to an API, you can use this function to
|
||||||
|
* assert that the data sent back is correct.
|
||||||
|
* @param page The Playwright page
|
||||||
|
* @param options Options
|
||||||
|
* @param options.action The action to perform that will trigger the request
|
||||||
|
* @param options.expect A function to run after the request is made to assert the response body
|
||||||
|
* @param options.url The URL to match in the network requests
|
||||||
|
* @returns The request body
|
||||||
|
* @example
|
||||||
|
* const responseBody = await assertResponseBody(page, {
|
||||||
|
* action: page.click('button'),
|
||||||
|
* expect: (responseBody) => expect(responseBody.foo).toBe('bar')
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
export const assertResponseBody = async <T>(
|
||||||
|
page: Page,
|
||||||
|
options: {
|
||||||
|
action: Promise<void> | void
|
||||||
|
expect?: (requestBody: T) => boolean | Promise<boolean>
|
||||||
|
requestMethod?: string
|
||||||
|
responseContentType?: string
|
||||||
|
url?: string
|
||||||
|
},
|
||||||
|
): Promise<T | undefined> => {
|
||||||
|
const [response] = await Promise.all([
|
||||||
|
page.waitForResponse((response) =>
|
||||||
|
Boolean(
|
||||||
|
response.url().includes(options.url || '') &&
|
||||||
|
response.status() === 200 &&
|
||||||
|
response
|
||||||
|
.headers()
|
||||||
|
['content-type']?.includes(options.responseContentType || 'application/json'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
await options.action,
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
throw new Error('No response received')
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseBody = await response.text()
|
||||||
|
const responseType = response.headers()['content-type']?.split(';')[0]
|
||||||
|
|
||||||
|
let parsedBody: T = undefined as T
|
||||||
|
|
||||||
|
if (responseType === 'text/x-component') {
|
||||||
|
parsedBody = parseRSC(responseBody)
|
||||||
|
} else if (typeof responseBody === 'string') {
|
||||||
|
parsedBody = JSON.parse(responseBody) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.expect === 'function') {
|
||||||
|
expect(await options.expect(parsedBody)).toBeTruthy()
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedBody
|
||||||
|
}
|
||||||
@@ -10,8 +10,10 @@ import { updatePostStep1, updatePostStep2 } from './runners/updatePost.js'
|
|||||||
import { clearAndSeedEverything } from './seed.js'
|
import { clearAndSeedEverything } from './seed.js'
|
||||||
import { externalWorkflow } from './workflows/externalWorkflow.js'
|
import { externalWorkflow } from './workflows/externalWorkflow.js'
|
||||||
import { inlineTaskTestWorkflow } from './workflows/inlineTaskTest.js'
|
import { inlineTaskTestWorkflow } from './workflows/inlineTaskTest.js'
|
||||||
|
import { inlineTaskTestDelayedWorkflow } from './workflows/inlineTaskTestDelayed.js'
|
||||||
import { longRunningWorkflow } from './workflows/longRunning.js'
|
import { longRunningWorkflow } from './workflows/longRunning.js'
|
||||||
import { noRetriesSetWorkflow } from './workflows/noRetriesSet.js'
|
import { noRetriesSetWorkflow } from './workflows/noRetriesSet.js'
|
||||||
|
import { parallelTaskWorkflow } from './workflows/parallelTaskWorkflow.js'
|
||||||
import { retries0Workflow } from './workflows/retries0.js'
|
import { retries0Workflow } from './workflows/retries0.js'
|
||||||
import { retriesBackoffTestWorkflow } from './workflows/retriesBackoffTest.js'
|
import { retriesBackoffTestWorkflow } from './workflows/retriesBackoffTest.js'
|
||||||
import { retriesRollbackTestWorkflow } from './workflows/retriesRollbackTest.js'
|
import { retriesRollbackTestWorkflow } from './workflows/retriesRollbackTest.js'
|
||||||
@@ -24,8 +26,6 @@ import { updatePostJSONWorkflow } from './workflows/updatePostJSON.js'
|
|||||||
import { workflowAndTasksRetriesUndefinedWorkflow } from './workflows/workflowAndTasksRetriesUndefined.js'
|
import { workflowAndTasksRetriesUndefinedWorkflow } from './workflows/workflowAndTasksRetriesUndefined.js'
|
||||||
import { workflowRetries2TasksRetries0Workflow } from './workflows/workflowRetries2TasksRetries0.js'
|
import { workflowRetries2TasksRetries0Workflow } from './workflows/workflowRetries2TasksRetries0.js'
|
||||||
import { workflowRetries2TasksRetriesUndefinedWorkflow } from './workflows/workflowRetries2TasksRetriesUndefined.js'
|
import { workflowRetries2TasksRetriesUndefinedWorkflow } from './workflows/workflowRetries2TasksRetriesUndefined.js'
|
||||||
import { inlineTaskTestDelayedWorkflow } from './workflows/inlineTaskTestDelayed.js'
|
|
||||||
import { parallelTaskWorkflow } from './workflows/parallelTaskWorkflow.js'
|
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|||||||
@@ -1390,7 +1390,7 @@ describe('Queues', () => {
|
|||||||
limit: amount,
|
limit: amount,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
})
|
})
|
||||||
expect(simpleDocs.docs.length).toBe(amount)
|
expect(simpleDocs.docs).toHaveLength(amount)
|
||||||
|
|
||||||
// Ensure all docs are created (= all tasks are run once)
|
// Ensure all docs are created (= all tasks are run once)
|
||||||
for (let i = 1; i <= simpleDocs.docs.length; i++) {
|
for (let i = 1; i <= simpleDocs.docs.length; i++) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@payload-config": ["./test/form-state/config.ts"],
|
"@payload-config": ["./test/_community/config.ts"],
|
||||||
"@payloadcms/admin-bar": ["./packages/admin-bar/src"],
|
"@payloadcms/admin-bar": ["./packages/admin-bar/src"],
|
||||||
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
||||||
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
||||||
|
|||||||
Reference in New Issue
Block a user