fix(next): version view breaking for deeply nested tabs, rows and collapsibles (#11808)

Fixes #11458 

Some complex, nested fields were receiving incorrect field paths and
schema paths, leading to a `"Error: No client field found"` error.

This PR ensures field paths are calculated correctly, by matching it to
how they're calculated in payload hooks.
This commit is contained in:
Alessio Gravili
2025-03-24 14:57:36 -06:00
committed by GitHub
parent fb01b4046d
commit 3c4b3ee527
7 changed files with 194 additions and 26 deletions

View File

@@ -84,7 +84,7 @@ export const buildVersionFields = ({
const { indexPath, path, schemaPath } = getFieldPathsModified({ const { indexPath, path, schemaPath } = getFieldPathsModified({
field, field,
index: fieldIndex, index: fieldIndex,
parentIndexPath: 'name' in field ? '' : parentIndexPath, parentIndexPath,
parentPath, parentPath,
parentSchemaPath, parentSchemaPath,
}) })
@@ -253,15 +253,14 @@ const buildVersionField = ({
tabIndex++ tabIndex++
const isNamedTab = tabHasName(tab) const isNamedTab = tabHasName(tab)
const tabAsField = { ...tab, type: 'tab' }
const { const {
indexPath: tabIndexPath, indexPath: tabIndexPath,
path: tabPath, path: tabPath,
schemaPath: tabSchemaPath, schemaPath: tabSchemaPath,
} = getFieldPathsModified({ } = getFieldPathsModified({
field: { field: tabAsField,
...tab,
type: 'tab',
},
index: tabIndex, index: tabIndex,
parentIndexPath: indexPath, parentIndexPath: indexPath,
parentPath, parentPath,
@@ -280,8 +279,8 @@ const buildVersionField = ({
modifiedOnly, modifiedOnly,
parentIndexPath: isNamedTab ? '' : tabIndexPath, parentIndexPath: isNamedTab ? '' : tabIndexPath,
parentIsLocalized: parentIsLocalized || tab.localized, parentIsLocalized: parentIsLocalized || tab.localized,
parentPath: tabPath, parentPath: isNamedTab ? tabPath : path,
parentSchemaPath: tabSchemaPath, parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath,
req, req,
selectedLocales, selectedLocales,
versionSiblingData: 'name' in tab ? versionValue?.[tab.name] : versionValue, versionSiblingData: 'name' in tab ? versionValue?.[tab.name] : versionValue,
@@ -289,7 +288,7 @@ const buildVersionField = ({
label: tab.label, label: tab.label,
}) })
} }
} // At this point, we are dealing with a `row`, etc } // At this point, we are dealing with a `row`, `collapsible`, etc
else if ('fields' in field) { else if ('fields' in field) {
if (field.type === 'array' && versionValue) { if (field.type === 'array' && versionValue) {
const arrayValue = Array.isArray(versionValue) ? versionValue : [] const arrayValue = Array.isArray(versionValue) ? versionValue : []
@@ -328,8 +327,8 @@ const buildVersionField = ({
modifiedOnly, modifiedOnly,
parentIndexPath: 'name' in field ? '' : indexPath, parentIndexPath: 'name' in field ? '' : indexPath,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized), parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
parentPath: path, parentPath: 'name' in field ? path : parentPath,
parentSchemaPath: schemaPath, parentSchemaPath: 'name' in field ? schemaPath : parentSchemaPath,
req, req,
selectedLocales, selectedLocales,
versionSiblingData: versionValue as object, versionSiblingData: versionValue as object,

View File

@@ -42,19 +42,11 @@ export function getFieldPathsModified({
const parentPathToUse = parentIsUnnamed ? parentWithoutIndex : parentPath const parentPathToUse = parentIsUnnamed ? parentWithoutIndex : parentPath
const parentSchemaPathSegments = parentSchemaPath.split('.')
const parentSchemaIsUnnamed =
parentSchemaPathSegments[parentSchemaPathSegments.length - 1].startsWith('_index-')
const parentSchemaWithoutIndex = parentSchemaIsUnnamed
? parentSchemaPathSegments.slice(0, -1).join('.')
: parentSchemaPath
const parentSchemaPathToUse = parentSchemaIsUnnamed ? parentSchemaWithoutIndex : parentSchemaPath
if ('name' in field) { if ('name' in field) {
return { return {
indexPath: '', indexPath: '',
path: `${parentPathToUse ? parentPathToUse + '.' : ''}${field.name}`, path: `${parentPathToUse ? parentPathToUse + '.' : ''}${field.name}`,
schemaPath: `${parentSchemaPathToUse ? parentSchemaPathToUse + '.' : ''}${field.name}`, schemaPath: `${parentSchemaPath ? parentSchemaPath + '.' : ''}${field.name}`,
} }
} }
@@ -63,6 +55,6 @@ export function getFieldPathsModified({
return { return {
indexPath: `${parentIndexPath ? parentIndexPath + '-' : ''}${index}`, indexPath: `${parentIndexPath ? parentIndexPath + '-' : ''}${index}`,
path: `${parentPathToUse ? parentPathToUse + '.' : ''}${indexSuffix}`, path: `${parentPathToUse ? parentPathToUse + '.' : ''}${indexSuffix}`,
schemaPath: `${!parentIsUnnamed && parentSchemaPathToUse ? parentSchemaPathToUse + '.' : ''}${indexSuffix}`, schemaPath: `${!parentIsUnnamed && parentSchemaPath ? parentSchemaPath + '.' : ''}${indexSuffix}`,
} }
} }

View File

@@ -39,6 +39,64 @@ export const Diff: CollectionConfig = {
}, },
], ],
}, },
{
slug: 'CollapsibleBlock',
fields: [
{
type: 'collapsible',
label: 'Collapsible',
fields: [
{
type: 'collapsible',
label: 'Nested Collapsible',
fields: [
{
name: 'textInCollapsibleInCollapsibleBlock',
type: 'text',
},
],
},
{
type: 'row',
fields: [
{
name: 'textInRowInCollapsibleBlock',
type: 'text',
},
],
},
],
},
],
},
{
slug: 'TabsBlock',
fields: [
{
type: 'tabs',
tabs: [
{
name: 'namedTab1InBlock',
fields: [
{
name: 'textInNamedTab1InBlock',
type: 'text',
},
],
},
{
label: 'Unnamed Tab 2 In Block',
fields: [
{
name: 'textInUnnamedTab2InBlock',
type: 'text',
},
],
},
],
},
],
},
], ],
}, },
{ {

View File

@@ -1213,6 +1213,62 @@ describe('Versions', () => {
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(3)).toHaveText('textInBlock2') await expect(textInBlock.locator('tr').nth(1).locator('td').nth(3)).toHaveText('textInBlock2')
}) })
test('correctly renders diff for collapsibles within block fields', async () => {
await navigateToVersionFieldsDiff()
const textInBlock = page.locator(
'[data-field-path="blocks.1.textInCollapsibleInCollapsibleBlock"]',
)
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
'textInCollapsibleInCollapsibleBlock',
)
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
'textInCollapsibleInCollapsibleBlock2',
)
})
test('correctly renders diff for rows within block fields', async () => {
await navigateToVersionFieldsDiff()
const textInBlock = page.locator('[data-field-path="blocks.1.textInRowInCollapsibleBlock"]')
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
'textInRowInCollapsibleBlock',
)
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
'textInRowInCollapsibleBlock2',
)
})
test('correctly renders diff for named tabs within block fields', async () => {
await navigateToVersionFieldsDiff()
const textInBlock = page.locator(
'[data-field-path="blocks.2.namedTab1InBlock.textInNamedTab1InBlock"]',
)
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
'textInNamedTab1InBlock',
)
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
'textInNamedTab1InBlock2',
)
})
test('correctly renders diff for unnamed tabs within block fields', async () => {
await navigateToVersionFieldsDiff()
const textInBlock = page.locator('[data-field-path="blocks.2.textInUnnamedTab2InBlock"]')
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
'textInUnnamedTab2InBlock',
)
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
'textInUnnamedTab2InBlock2',
)
})
test('correctly renders diff for checkbox fields', async () => { test('correctly renders diff for checkbox fields', async () => {
await navigateToVersionFieldsDiff() await navigateToVersionFieldsDiff()

View File

@@ -54,6 +54,7 @@ export type SupportedTimezones =
| 'Asia/Singapore' | 'Asia/Singapore'
| 'Asia/Tokyo' | 'Asia/Tokyo'
| 'Asia/Seoul' | 'Asia/Seoul'
| 'Australia/Brisbane'
| 'Australia/Sydney' | 'Australia/Sydney'
| 'Pacific/Guam' | 'Pacific/Guam'
| 'Pacific/Noumea' | 'Pacific/Noumea'
@@ -312,12 +313,30 @@ export interface Diff {
}[] }[]
| null; | null;
blocks?: blocks?:
| (
| { | {
textInBlock?: string | null; textInBlock?: string | null;
id?: string | null; id?: string | null;
blockName?: string | null; blockName?: string | null;
blockType: 'TextBlock'; blockType: 'TextBlock';
}[] }
| {
textInCollapsibleInCollapsibleBlock?: string | null;
textInRowInCollapsibleBlock?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'CollapsibleBlock';
}
| {
namedTab1InBlock?: {
textInNamedTab1InBlock?: string | null;
};
textInUnnamedTab2InBlock?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'TabsBlock';
}
)[]
| null; | null;
checkbox?: boolean | null; checkbox?: boolean | null;
code?: string | null; code?: string | null;
@@ -772,6 +791,26 @@ export interface DiffSelect<T extends boolean = true> {
id?: T; id?: T;
blockName?: T; blockName?: T;
}; };
CollapsibleBlock?:
| T
| {
textInCollapsibleInCollapsibleBlock?: T;
textInRowInCollapsibleBlock?: T;
id?: T;
blockName?: T;
};
TabsBlock?:
| T
| {
namedTab1InBlock?:
| T
| {
textInNamedTab1InBlock?: T;
};
textInUnnamedTab2InBlock?: T;
id?: T;
blockName?: T;
};
}; };
checkbox?: T; checkbox?: T;
code?: T; code?: T;

View File

@@ -138,6 +138,18 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
blockType: 'TextBlock', blockType: 'TextBlock',
textInBlock: 'textInBlock', textInBlock: 'textInBlock',
}, },
{
blockType: 'CollapsibleBlock',
textInCollapsibleInCollapsibleBlock: 'textInCollapsibleInCollapsibleBlock',
textInRowInCollapsibleBlock: 'textInRowInCollapsibleBlock',
},
{
blockType: 'TabsBlock',
namedTab1InBlock: {
textInNamedTab1InBlock: 'textInNamedTab1InBlock',
},
textInUnnamedTab2InBlock: 'textInUnnamedTab2InBlock',
},
], ],
checkbox: true, checkbox: true,
code: 'code', code: 'code',
@@ -186,6 +198,18 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
blockType: 'TextBlock', blockType: 'TextBlock',
textInBlock: 'textInBlock2', textInBlock: 'textInBlock2',
}, },
{
blockType: 'CollapsibleBlock',
textInCollapsibleInCollapsibleBlock: 'textInCollapsibleInCollapsibleBlock2',
textInRowInCollapsibleBlock: 'textInRowInCollapsibleBlock2',
},
{
blockType: 'TabsBlock',
namedTab1InBlock: {
textInNamedTab1InBlock: 'textInNamedTab1InBlock2',
},
textInUnnamedTab2InBlock: 'textInUnnamedTab2InBlock2',
},
], ],
checkbox: false, checkbox: false,
code: 'code2', code: 'code2',