Skip to content

Commit 0554b50

Browse files
wingding12cursoragentJarrodMFlesch
authored
fix(plugin-nested-docs): await populateBreadcrumbs in resaveChildren (#15582)
Fixes #14943 ## Summary The async `populateBreadcrumbs()` function was not being awaited in `resaveChildren`. This caused `payload.update()` to receive a Promise instead of the child document's data, so updates couldn't preserve the `_status` field and defaulted to the main document's draft status. When a parent was saved, all child updates created draft versions instead of preserving the published/draft distinction. Querying with `draft: false` then returned draft data, making the published version "disappear." The fix adds `await` to the `populateBreadcrumbs()` call. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
1 parent be6c443 commit 0554b50

2 files changed

Lines changed: 202 additions & 2 deletions

File tree

packages/plugin-nested-docs/src/hooks/resaveChildren.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const resaveChildren =
7171
await req.payload.update({
7272
id: child.id,
7373
collection: collection.slug,
74-
data: populateBreadcrumbs({
74+
data: await populateBreadcrumbs({
7575
collection,
7676
data: child,
7777
generateLabel: pluginConfig.generateLabel,

test/plugin-nested-docs/int.spec.ts

Lines changed: 201 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ArrayField, Payload, RelationshipField } from 'payload'
22

33
import path from 'path'
44
import { fileURLToPath } from 'url'
5-
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
5+
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
66

77
import type { Page } from './payload-types.js'
88

@@ -192,6 +192,206 @@ describe('@payloadcms/plugin-nested-docs', () => {
192192
})
193193
})
194194

195+
describe('versions', () => {
196+
const createdPageIDs: (number | string)[] = []
197+
198+
afterEach(async () => {
199+
// Clean up in reverse order (children before parents)
200+
for (const id of [...createdPageIDs].reverse()) {
201+
await payload.delete({ collection: 'pages', id })
202+
}
203+
createdPageIDs.length = 0
204+
})
205+
206+
it('should preserve published version of child when parent is saved and child has unpublished draft', async () => {
207+
// Step 1: Create parent page and publish it
208+
const parentDoc = await payload.create({
209+
collection: 'pages',
210+
data: {
211+
title: 'Version Parent',
212+
slug: 'version-parent',
213+
_status: 'published',
214+
},
215+
})
216+
createdPageIDs.push(parentDoc.id)
217+
218+
// Step 2: Create child page and publish it
219+
const childDoc = await payload.create({
220+
collection: 'pages',
221+
data: {
222+
title: 'Version Child',
223+
slug: 'version-child',
224+
parent: parentDoc.id,
225+
_status: 'published',
226+
},
227+
})
228+
createdPageIDs.push(childDoc.id)
229+
230+
// Verify initial published state
231+
const initialPublished = await payload.findByID({
232+
id: childDoc.id,
233+
collection: 'pages',
234+
draft: false,
235+
})
236+
expect(initialPublished._status).toBe('published')
237+
expect(initialPublished.breadcrumbs).toHaveLength(2)
238+
239+
// Step 3: Make unpublished changes to child (creates a draft version)
240+
await payload.update({
241+
id: childDoc.id,
242+
collection: 'pages',
243+
data: {
244+
title: 'Version Child Draft Edit',
245+
},
246+
draft: true,
247+
})
248+
249+
// Step 4: Re-publish the parent (triggers resaveChildren)
250+
await payload.update({
251+
id: parentDoc.id,
252+
collection: 'pages',
253+
data: {
254+
title: 'Version Parent Updated',
255+
slug: 'version-parent-updated',
256+
_status: 'published',
257+
},
258+
})
259+
260+
// Step 5: Verify the child's published version is still accessible
261+
const publishedChild = await payload.findByID({
262+
id: childDoc.id,
263+
collection: 'pages',
264+
draft: false,
265+
})
266+
267+
expect(publishedChild).toBeDefined()
268+
expect(publishedChild._status).toBe('published')
269+
expect(publishedChild.breadcrumbs).toHaveLength(2)
270+
expect(publishedChild.breadcrumbs?.[0]?.url).toBe('/version-parent-updated')
271+
272+
// Step 6: Verify the draft version is also still accessible
273+
const draftChild = await payload.findByID({
274+
id: childDoc.id,
275+
collection: 'pages',
276+
draft: true,
277+
})
278+
279+
expect(draftChild).toBeDefined()
280+
expect(draftChild.title).toBe('Version Child Draft Edit')
281+
})
282+
283+
it('should update breadcrumbs for draft-only children when parent is saved', async () => {
284+
const parentDoc = await payload.create({
285+
collection: 'pages',
286+
data: {
287+
title: 'Draft Parent',
288+
slug: 'draft-parent',
289+
_status: 'published',
290+
},
291+
})
292+
createdPageIDs.push(parentDoc.id)
293+
294+
// Create a child that is never published (draft-only)
295+
const draftChild = await payload.create({
296+
collection: 'pages',
297+
data: {
298+
title: 'Draft Only Child',
299+
slug: 'draft-only-child',
300+
parent: parentDoc.id,
301+
_status: 'draft',
302+
},
303+
})
304+
createdPageIDs.push(draftChild.id)
305+
306+
expect(draftChild._status).toBe('draft')
307+
308+
// Update the parent
309+
await payload.update({
310+
id: parentDoc.id,
311+
collection: 'pages',
312+
data: {
313+
title: 'Draft Parent Updated',
314+
slug: 'draft-parent-updated',
315+
_status: 'published',
316+
},
317+
})
318+
319+
// Draft-only child should have updated breadcrumbs
320+
const updatedDraftChild = await payload.findByID({
321+
id: draftChild.id,
322+
collection: 'pages',
323+
draft: true,
324+
})
325+
326+
expect(updatedDraftChild.breadcrumbs).toHaveLength(2)
327+
expect(updatedDraftChild.breadcrumbs?.[0]?.url).toBe('/draft-parent-updated')
328+
})
329+
330+
it('should update breadcrumbs for both published and draft versions when parent changes', async () => {
331+
const parent = await payload.create({
332+
collection: 'pages',
333+
data: {
334+
title: 'Breadcrumb Parent',
335+
slug: 'breadcrumb-parent',
336+
_status: 'published',
337+
},
338+
})
339+
createdPageIDs.push(parent.id)
340+
341+
const child = await payload.create({
342+
collection: 'pages',
343+
data: {
344+
title: 'Breadcrumb Child',
345+
slug: 'breadcrumb-child',
346+
parent: parent.id,
347+
_status: 'published',
348+
},
349+
})
350+
createdPageIDs.push(child.id)
351+
352+
// Create draft edit on child
353+
await payload.update({
354+
id: child.id,
355+
collection: 'pages',
356+
data: {
357+
title: 'Breadcrumb Child Draft',
358+
},
359+
draft: true,
360+
})
361+
362+
// Update parent slug
363+
await payload.update({
364+
id: parent.id,
365+
collection: 'pages',
366+
data: {
367+
slug: 'breadcrumb-parent-updated',
368+
_status: 'published',
369+
},
370+
})
371+
372+
// Published child has updated breadcrumbs and is accessible
373+
const published = await payload.findByID({
374+
id: child.id,
375+
collection: 'pages',
376+
draft: false,
377+
})
378+
379+
expect(published._status).toBe('published')
380+
expect(published.breadcrumbs?.[0]?.url).toBe('/breadcrumb-parent-updated')
381+
382+
// Draft child also has updated breadcrumbs
383+
const draft = await payload.findByID({
384+
id: child.id,
385+
collection: 'pages',
386+
draft: true,
387+
})
388+
389+
expect(draft._status).toBe('draft')
390+
expect(draft.title).toBe('Breadcrumb Child Draft')
391+
expect(draft.breadcrumbs?.[0]?.url).toBe('/breadcrumb-parent-updated')
392+
})
393+
})
394+
195395
describe('overrides', () => {
196396
let collection
197397
beforeAll(() => {

0 commit comments

Comments
 (0)