fix: removes nested array field configs from array value (#3549)

* fix: array controls 'addBelow' was adding above
This commit is contained in:
Jarrod Flesch
2023-10-10 15:55:00 -04:00
committed by GitHub
parent a42e84bbb2
commit af892ecb0e
11 changed files with 95 additions and 38 deletions

View File

@@ -347,7 +347,7 @@ The `useForm` hook returns an object with the following properties: |
value: <strong><code>rowIndex</code></strong>, value: <strong><code>rowIndex</code></strong>,
}, },
{ {
value: "The index of the row to add", value: "The index of the row to add. If omitted, the row will be added to the end of the array.",
}, },
], ],
[ [

View File

@@ -66,7 +66,7 @@ export const ArrayAction: React.FC<Props> = ({
<PopupList.Button <PopupList.Button
className={`${baseClass}__action ${baseClass}__add`} className={`${baseClass}__action ${baseClass}__add`}
onClick={() => { onClick={() => {
addRow(index) addRow(index + 1)
close() close()
}} }}
> >

View File

@@ -132,7 +132,9 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
} }
case 'ADD_ROW': { case 'ADD_ROW': {
const { blockType, path, rowIndex, subFieldState } = action const { blockType, path, rowIndex: rowIndexFromArgs, subFieldState } = action
const rowIndex =
typeof rowIndexFromArgs === 'number' ? rowIndexFromArgs : state[path]?.rows?.length || 0
const rowsMetadata = [...(state[path]?.rows || [])] const rowsMetadata = [...(state[path]?.rows || [])]
rowsMetadata.splice( rowsMetadata.splice(
@@ -155,19 +157,22 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
} }
} }
const { remainingFields, rows } = separateRows(path, state) // add new row to array _field state_
const { remainingFields, rows: siblingRows } = separateRows(path, state)
siblingRows.splice(rowIndex, 0, subFieldState)
// actual form state (value saved in db) // add new row to array _value_
rows.splice(rowIndex, 0, subFieldState) const currentValue = (Array.isArray(state[path]?.value) ? state[path]?.value : []) as Fields[]
const newValue = currentValue.splice(rowIndex, 0, reduceFieldsToValues(subFieldState, true))
const newState: Fields = { const newState: Fields = {
...remainingFields, ...remainingFields,
...flattenRows(path, rows), ...flattenRows(path, siblingRows),
[path]: { [path]: {
...state[path], ...state[path],
disableFormData: true, disableFormData: true,
rows: rowsMetadata, rows: rowsMetadata,
value: rows, value: newValue,
}, },
} }
@@ -176,8 +181,8 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
case 'REPLACE_ROW': { case 'REPLACE_ROW': {
const { blockType, path, rowIndex: rowIndexArg, subFieldState } = action const { blockType, path, rowIndex: rowIndexArg, subFieldState } = action
const { remainingFields, rows } = separateRows(path, state) const { remainingFields, rows: siblingRows } = separateRows(path, state)
const rowIndex = Math.max(0, Math.min(rowIndexArg, rows?.length - 1 || 0)) const rowIndex = Math.max(0, Math.min(rowIndexArg, siblingRows?.length - 1 || 0))
const rowsMetadata = [...(state[path]?.rows || [])] const rowsMetadata = [...(state[path]?.rows || [])]
rowsMetadata[rowIndex] = { rowsMetadata[rowIndex] = {
@@ -195,17 +200,21 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
} }
} }
// replace form field state // replace form _field state_
rows[rowIndex] = subFieldState siblingRows[rowIndex] = subFieldState
// replace array _value_
const newValue = Array.isArray(state[path]?.value) ? state[path]?.value : []
newValue[rowIndex] = reduceFieldsToValues(subFieldState, true)
const newState: Fields = { const newState: Fields = {
...remainingFields, ...remainingFields,
...flattenRows(path, rows), ...flattenRows(path, siblingRows),
[path]: { [path]: {
...state[path], ...state[path],
disableFormData: true, disableFormData: true,
rows: rowsMetadata, rows: rowsMetadata,
value: rows, value: newValue,
}, },
} }

View File

@@ -431,7 +431,7 @@ const Form: React.FC<Props> = (props) => {
[], [],
) )
const getRowConfigByPath = React.useCallback( const getRowSchemaByPath = React.useCallback(
({ blockType, path }: { blockType?: string; path: string }) => { ({ blockType, path }: { blockType?: string; path: string }) => {
const rowConfig = traverseRowConfigs({ const rowConfig = traverseRowConfigs({
fieldConfig: collection?.fields || global?.fields, fieldConfig: collection?.fields || global?.fields,
@@ -449,23 +449,24 @@ const Form: React.FC<Props> = (props) => {
const addFieldRow: Context['addFieldRow'] = useCallback( const addFieldRow: Context['addFieldRow'] = useCallback(
async ({ data, path, rowIndex }) => { async ({ data, path, rowIndex }) => {
const preferences = await getDocPreferences() const preferences = await getDocPreferences()
const fieldConfig = getRowConfigByPath({ const rowSchema = getRowSchemaByPath({
blockType: data?.blockType, blockType: data?.blockType,
path, path,
}) })
if (fieldConfig) { if (rowSchema) {
const subFieldState = await buildStateFromSchema({ const subFieldState = await buildStateFromSchema({
id, id,
config, config,
data, data,
fieldSchema: fieldConfig, fieldSchema: rowSchema,
locale, locale,
operation, operation,
preferences, preferences,
t, t,
user, user,
}) })
dispatchFields({ dispatchFields({
blockType: data?.blockType, blockType: data?.blockType,
path, path,
@@ -475,11 +476,11 @@ const Form: React.FC<Props> = (props) => {
}) })
} }
}, },
[dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath, config], [dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowSchemaByPath, config],
) )
const removeFieldRow: Context['removeFieldRow'] = useCallback( const removeFieldRow: Context['removeFieldRow'] = useCallback(
async ({ path, rowIndex }) => { ({ path, rowIndex }) => {
dispatchFields({ path, rowIndex, type: 'REMOVE_ROW' }) dispatchFields({ path, rowIndex, type: 'REMOVE_ROW' })
}, },
[dispatchFields], [dispatchFields],
@@ -488,17 +489,17 @@ const Form: React.FC<Props> = (props) => {
const replaceFieldRow: Context['replaceFieldRow'] = useCallback( const replaceFieldRow: Context['replaceFieldRow'] = useCallback(
async ({ data, path, rowIndex }) => { async ({ data, path, rowIndex }) => {
const preferences = await getDocPreferences() const preferences = await getDocPreferences()
const fieldConfig = getRowConfigByPath({ const rowSchema = getRowSchemaByPath({
blockType: data?.blockType, blockType: data?.blockType,
path, path,
}) })
if (fieldConfig) { if (rowSchema) {
const subFieldState = await buildStateFromSchema({ const subFieldState = await buildStateFromSchema({
id, id,
config, config,
data, data,
fieldSchema: fieldConfig, fieldSchema: rowSchema,
locale, locale,
operation, operation,
preferences, preferences,
@@ -514,7 +515,7 @@ const Form: React.FC<Props> = (props) => {
}) })
} }
}, },
[dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath, config], [dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowSchemaByPath, config],
) )
const getFields = useCallback(() => contextRef.current.fields, [contextRef]) const getFields = useCallback(() => contextRef.current.fields, [contextRef])

View File

@@ -12,9 +12,9 @@ export const separateRows = (path: string, fields: Fields): Result => {
const newRows = incomingRows const newRows = incomingRows
if (fieldPath.indexOf(`${path}.`) === 0) { if (fieldPath.indexOf(`${path}.`) === 0) {
const index = Number(fieldPath.replace(`${path}.`, '').split('.')[0]) const [rowIndex] = fieldPath.replace(`${path}.`, '').split('.')
if (!newRows[index]) newRows[index] = {} if (!newRows[rowIndex]) newRows[rowIndex] = {}
newRows[index][fieldPath.replace(`${path}.${String(index)}.`, '')] = { ...field } newRows[rowIndex][fieldPath.replace(`${path}.${String(rowIndex)}.`, '')] = { ...field }
} else { } else {
remainingFields[fieldPath] = field remainingFields[fieldPath] = field
} }

View File

@@ -174,7 +174,10 @@ export type Context = {
}: { }: {
data?: Data data?: Data
path: string path: string
rowIndex: number /*
* by default the new row will be added to the end of the list
*/
rowIndex?: number
}) => Promise<void> }) => Promise<void>
buildRowErrors: () => void buildRowErrors: () => void
createFormData: CreateFormData createFormData: CreateFormData
@@ -190,7 +193,7 @@ export type Context = {
getField: GetField getField: GetField
getFields: GetFields getFields: GetFields
getSiblingData: GetSiblingData getSiblingData: GetSiblingData
removeFieldRow: ({ path, rowIndex }: { path: string; rowIndex: number }) => Promise<void> removeFieldRow: ({ path, rowIndex }: { path: string; rowIndex: number }) => void
replaceFieldRow: ({ replaceFieldRow: ({
data, data,
path, path,

View File

@@ -10,8 +10,6 @@ export const AddCustomBlocks: React.FC = () => {
const { addFieldRow, replaceFieldRow } = useForm() const { addFieldRow, replaceFieldRow } = useForm()
const { value } = useField({ path: 'customBlocks' }) const { value } = useField({ path: 'customBlocks' })
const nextIndex = Array.isArray(value) ? value.length : 0
return ( return (
<div className={baseClass}> <div className={baseClass}>
<div className={`${baseClass}__blocks-grid`}> <div className={`${baseClass}__blocks-grid`}>
@@ -21,7 +19,6 @@ export const AddCustomBlocks: React.FC = () => {
addFieldRow({ addFieldRow({
data: { block1Title: 'Block 1: Prefilled Title', blockType: 'block-1' }, data: { block1Title: 'Block 1: Prefilled Title', blockType: 'block-1' },
path: 'customBlocks', path: 'customBlocks',
rowIndex: nextIndex,
}) })
} }
type="button" type="button"
@@ -35,7 +32,6 @@ export const AddCustomBlocks: React.FC = () => {
addFieldRow({ addFieldRow({
data: { block2Title: 'Block 2: Prefilled Title', blockType: 'block-2' }, data: { block2Title: 'Block 2: Prefilled Title', blockType: 'block-2' },
path: 'customBlocks', path: 'customBlocks',
rowIndex: nextIndex,
}) })
} }
type="button" type="button"
@@ -51,12 +47,12 @@ export const AddCustomBlocks: React.FC = () => {
replaceFieldRow({ replaceFieldRow({
data: { block1Title: 'REPLACED BLOCK', blockType: 'block-1' }, data: { block1Title: 'REPLACED BLOCK', blockType: 'block-1' },
path: 'customBlocks', path: 'customBlocks',
rowIndex: nextIndex - 1, rowIndex: (Array.isArray(value) ? value.length : 0) - 1,
}) })
} }
type="button" type="button"
> >
Replace Block {nextIndex} Replace Block {Array.isArray(value) ? value.length : 0}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,7 @@
import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types'
import { CollapsibleLabelComponent } from './LabelComponent' import { CollapsibleLabelComponent } from './LabelComponent'
import { collapsibleFieldsSlug } from './shared'
export const collapsibleFieldsSlug = 'collapsible-fields'
const CollapsibleFields: CollectionConfig = { const CollapsibleFields: CollectionConfig = {
slug: collapsibleFieldsSlug, slug: collapsibleFieldsSlug,

View File

@@ -0,0 +1 @@
export const collapsibleFieldsSlug = 'collapsible-fields'

View File

@@ -9,7 +9,7 @@ import wait from '../../packages/payload/src/utilities/wait'
import { saveDocAndAssert, saveDocHotkeyAndAssert } from '../helpers' import { saveDocAndAssert, saveDocHotkeyAndAssert } from '../helpers'
import { AdminUrlUtil } from '../helpers/adminUrlUtil' import { AdminUrlUtil } from '../helpers/adminUrlUtil'
import { initPayloadE2E } from '../helpers/configHelpers' import { initPayloadE2E } from '../helpers/configHelpers'
import { collapsibleFieldsSlug } from './collections/Collapsible' import { collapsibleFieldsSlug } from './collections/Collapsible/shared'
import { jsonDoc } from './collections/JSON' import { jsonDoc } from './collections/JSON'
import { numberDoc } from './collections/Number' import { numberDoc } from './collections/Number'
import { pointFieldsSlug } from './collections/Point' import { pointFieldsSlug } from './collections/Point'

View File

@@ -0,0 +1,48 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { saveDocAndAssert } from '../helpers'
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
import { initPayloadTest } from '../helpers/configHelpers'
const { beforeAll, describe } = test
let url: AdminUrlUtil
const slug = 'nested-fields'
let page: Page
describe('Nested Fields', () => {
beforeAll(async ({ browser }) => {
const { serverURL } = await initPayloadTest({
__dirname,
init: {
local: false,
},
})
url = new AdminUrlUtil(serverURL, slug)
const context = await browser.newContext()
page = await context.newPage()
})
test('should save deeply nested fields', async () => {
const assertionValue = 'sample block value'
await page.goto(url.create)
await page.locator('#field-array > button').click()
await page.locator('#field-array__0__group__namedTab__blocks > button').click()
await page.locator('button[title="Block With Field"]').click()
await page.locator('#field-array__0__group__namedTab__blocks__0__text').fill(assertionValue)
await saveDocAndAssert(page)
await expect(page.locator('#field-array__0__group__namedTab__blocks__0__text')).toHaveValue(
assertionValue,
)
})
})