fix: removes nested array field configs from array value (#3549)
* fix: array controls 'addBelow' was adding above
This commit is contained in:
@@ -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.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -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()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
1
test/fields/collections/Collapsible/shared.ts
Normal file
1
test/fields/collections/Collapsible/shared.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const collapsibleFieldsSlug = 'collapsible-fields'
|
||||||
@@ -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'
|
||||||
|
|||||||
48
test/nested-fields/e2e.spec.ts
Normal file
48
test/nested-fields/e2e.spec.ts
Normal 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,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user