Skip to content

Commit a231a05

Browse files
authored
fix(plugin-nested-docs): prevent phantom breadcrumb row (#13628)
When saving a doc and regenerating the breadcrumbs array, a phantom row will append itself to the end of the array on save. This is because of fixes made in #13551 changed the way we merge array and block rows from the server. To fix this we need to ensure that row IDs are consistent across form state invocations, i.e. the hooks that mutate the array rows _cannot_ discard the row IDs. Before: https://github.com/user-attachments/assets/db715801-b4fd-4114-b39b-8d9b37fad979 After: https://github.com/user-attachments/assets/6da63a31-cd5d-43c1-a15e-caddbc540d56 --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211175452200168
1 parent b99c324 commit a231a05

File tree

7 files changed

+73
-30
lines changed

7 files changed

+73
-30
lines changed

next-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3+
/// <reference path="./.next/types/routes.d.ts" />
34

45
// NOTE: This file should not be edited
56
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

packages/plugin-nested-docs/src/utilities/formatBreadcrumb.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@ import type { SanitizedCollectionConfig } from 'payload'
33
import type { Breadcrumb, GenerateLabel, GenerateURL } from '../types.js'
44

55
type Args = {
6+
/**
7+
* Existing breadcrumb, if any, to base the new breadcrumb on.
8+
* This ensures that row IDs are maintained across updates, etc.
9+
*/
10+
breadcrumb?: Breadcrumb
611
collection: SanitizedCollectionConfig
712
docs: Record<string, unknown>[]
813
generateLabel?: GenerateLabel
914
generateURL?: GenerateURL
1015
}
16+
1117
export const formatBreadcrumb = ({
18+
breadcrumb,
1219
collection,
1320
docs,
1421
generateLabel,
@@ -32,6 +39,7 @@ export const formatBreadcrumb = ({
3239
}
3340

3441
return {
42+
...(breadcrumb || {}),
3543
doc: lastDoc.id as string,
3644
label,
3745
url,

packages/plugin-nested-docs/src/utilities/populateBreadcrumbs.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ export const populateBreadcrumbs = async ({
2626
req,
2727
}: Args): Promise<Data> => {
2828
const newData = data
29+
2930
const currentDocument = {
3031
...originalDoc,
3132
...data,
33+
id: originalDoc?.id ?? data?.id,
3234
}
35+
3336
const allParentDocuments: Document[] = await getAllParentDocuments(
3437
req,
3538
{
@@ -41,14 +44,11 @@ export const populateBreadcrumbs = async ({
4144
currentDocument,
4245
)
4346

44-
if (originalDoc?.id) {
45-
currentDocument.id = originalDoc?.id
46-
}
47-
4847
allParentDocuments.push(currentDocument)
4948

5049
const breadcrumbs = allParentDocuments.map((_, i) =>
5150
formatBreadcrumb({
51+
breadcrumb: currentDocument[breadcrumbsFieldName]?.[i],
5252
collection,
5353
docs: allParentDocuments.slice(0, i + 1),
5454
generateLabel,

test/form-state/collections/Posts/index.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,24 +76,10 @@ export const PostsCollection: CollectionConfig = {
7676
name: 'array',
7777
type: 'array',
7878
admin: {
79-
description: 'If there is no value, a default row will be added by a beforeChange hook',
8079
components: {
8180
RowLabel: './collections/Posts/ArrayRowLabel.js#ArrayRowLabel',
8281
},
8382
},
84-
hooks: {
85-
beforeChange: [
86-
({ value }) =>
87-
!value?.length
88-
? [
89-
{
90-
defaultTextField: 'This is a computed value.',
91-
customTextField: 'This is a computed value.',
92-
},
93-
]
94-
: value,
95-
],
96-
},
9783
fields: [
9884
{
9985
name: 'customTextField',
@@ -111,5 +97,31 @@ export const PostsCollection: CollectionConfig = {
11197
},
11298
],
11399
},
100+
{
101+
name: 'computedArray',
102+
type: 'array',
103+
admin: {
104+
description:
105+
'If there is no value, a default row will be added by a beforeChange hook. Otherwise, modifies the rows on save.',
106+
},
107+
hooks: {
108+
beforeChange: [
109+
({ value }) =>
110+
!value?.length
111+
? [
112+
{
113+
text: 'This is a computed value.',
114+
},
115+
]
116+
: value,
117+
],
118+
},
119+
fields: [
120+
{
121+
name: 'text',
122+
type: 'text',
123+
},
124+
],
125+
},
114126
],
115127
}

test/form-state/e2e.spec.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -312,22 +312,18 @@ test.describe('Form State', () => {
312312

313313
// Now test array rows, as their merge logic is different
314314

315-
await page.locator('#field-array #array-row-0').isVisible()
315+
await page.locator('#field-computedArray #computedArray-row-0').isVisible()
316316

317-
await removeArrayRow(page, { fieldName: 'array' })
317+
await removeArrayRow(page, { fieldName: 'computedArray' })
318318

319-
await page.locator('#field-array .array-row-0').isHidden()
319+
await page.locator('#field-computedArray #computedArray-row-0').isHidden()
320320

321321
await saveDocAndAssert(page)
322322

323-
await expect(page.locator('#field-array #array-row-0')).toBeVisible()
324-
325-
await expect(
326-
page.locator('#field-array #array-row-0 #field-array__0__customTextField'),
327-
).toHaveValue('This is a computed value.')
323+
await expect(page.locator('#field-computedArray #computedArray-row-0')).toBeVisible()
328324

329325
await expect(
330-
page.locator('#field-array #array-row-0 #field-array__0__defaultTextField'),
326+
page.locator('#field-computedArray #computedArray-row-0 #field-computedArray__0__text'),
331327
).toHaveValue('This is a computed value.')
332328
})
333329

test/form-state/payload-types.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,22 @@ export interface Post {
144144
}
145145
)[]
146146
| null;
147-
/**
148-
* If there is no value, a default row will be added by a beforeChange hook
149-
*/
150147
array?:
151148
| {
152149
customTextField?: string | null;
153150
defaultTextField?: string | null;
154151
id?: string | null;
155152
}[]
156153
| null;
154+
/**
155+
* If there is no value, a default row will be added by a beforeChange hook. Otherwise, modifies the rows on save.
156+
*/
157+
computedArray?:
158+
| {
159+
text?: string | null;
160+
id?: string | null;
161+
}[]
162+
| null;
157163
updatedAt: string;
158164
createdAt: string;
159165
}
@@ -288,6 +294,12 @@ export interface PostsSelect<T extends boolean = true> {
288294
defaultTextField?: T;
289295
id?: T;
290296
};
297+
computedArray?:
298+
| T
299+
| {
300+
text?: T;
301+
id?: T;
302+
};
291303
updatedAt?: T;
292304
createdAt?: T;
293305
}

test/plugin-nested-docs/payload-types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ export interface User {
178178
hash?: string | null;
179179
loginAttempts?: number | null;
180180
lockUntil?: string | null;
181+
sessions?:
182+
| {
183+
id: string;
184+
createdAt?: string | null;
185+
expiresAt: string;
186+
}[]
187+
| null;
181188
password?: string | null;
182189
}
183190
/**
@@ -295,6 +302,13 @@ export interface UsersSelect<T extends boolean = true> {
295302
hash?: T;
296303
loginAttempts?: T;
297304
lockUntil?: T;
305+
sessions?:
306+
| T
307+
| {
308+
id?: T;
309+
createdAt?: T;
310+
expiresAt?: T;
311+
};
298312
}
299313
/**
300314
* This interface was referenced by `Config`'s JSON-Schema

0 commit comments

Comments
 (0)