fix(plugin-nested-docs): crumbs not syncing on non-versioned collections (#13629)

Fixes https://github.com/payloadcms/payload/issues/13563

When using the nested docs plugin with collections that do not have
drafts enabled. It was not syncing breadcrumbs from parent changes.

The root cause was returning early on any document that did not meet
`doc._status !== 'published'` check, which was **_always_** the case for
non-draft collections.

### Before
```ts
if (doc._status !== 'published') {
  return
}
```
### After
```ts
if (collection.versions.drafts && doc._status !== 'published') {
  return
}
```
This commit is contained in:
Jarrod Flesch
2025-08-28 15:43:31 -04:00
committed by GitHub
parent c1b4960795
commit 4600c94cac
8 changed files with 121 additions and 102 deletions

View File

@@ -0,0 +1,19 @@
import type { CollectionBeforeChangeHook } from 'payload'
import type { NestedDocsPluginConfig } from '../types.js'
import { populateBreadcrumbs } from '../utilities/populateBreadcrumbs.js'
export const populateBreadcrumbsBeforeChange =
(pluginConfig: NestedDocsPluginConfig): CollectionBeforeChangeHook =>
async ({ collection, data, originalDoc, req }) =>
populateBreadcrumbs({
breadcrumbsFieldName: pluginConfig.breadcrumbsFieldSlug,
collection,
data,
generateLabel: pluginConfig.generateLabel,
generateURL: pluginConfig.generateURL,
originalDoc,
parentFieldName: pluginConfig.parentFieldSlug,
req,
})

View File

@@ -1,10 +1,4 @@
import type {
CollectionAfterChangeHook,
CollectionConfig,
JsonObject,
PayloadRequest,
ValidationError,
} from 'payload'
import type { CollectionAfterChangeHook, JsonObject, ValidationError } from 'payload'
import { APIError, ValidationErrorName } from 'payload'
@@ -12,21 +6,16 @@ import type { NestedDocsPluginConfig } from '../types.js'
import { populateBreadcrumbs } from '../utilities/populateBreadcrumbs.js'
type ResaveArgs = {
collection: CollectionConfig
doc: JsonObject
draft: boolean
pluginConfig: NestedDocsPluginConfig
req: PayloadRequest
}
export const resaveChildren =
(pluginConfig: NestedDocsPluginConfig): CollectionAfterChangeHook =>
async ({ collection, doc, req }) => {
if (collection.versions.drafts && doc._status !== 'published') {
// If the parent is a draft, don't resave children
return
}
const resave = async ({ collection, doc, draft, pluginConfig, req }: ResaveArgs) => {
const parentSlug = pluginConfig?.parentFieldSlug || 'parent'
const parentSlug = pluginConfig?.parentFieldSlug || 'parent'
if (draft) {
// If the parent is a draft, don't resave children
return
} else {
const initialDraftChildren = await req.payload.find({
collection: collection.slug,
depth: 0,
@@ -74,7 +63,7 @@ const resave = async ({ collection, doc, draft, pluginConfig, req }: ResaveArgs)
})
})
if (sortedChildren) {
if (sortedChildren.length) {
try {
for (const child of sortedChildren) {
const isDraft = child._status !== 'published'
@@ -82,7 +71,14 @@ const resave = async ({ collection, doc, draft, pluginConfig, req }: ResaveArgs)
await req.payload.update({
id: child.id,
collection: collection.slug,
data: populateBreadcrumbs(req, pluginConfig, collection, child),
data: populateBreadcrumbs({
collection,
data: child,
generateLabel: pluginConfig.generateLabel,
generateURL: pluginConfig.generateURL,
parentFieldName: pluginConfig.parentFieldSlug,
req,
}),
depth: 0,
draft: isDraft,
locale: req.locale,
@@ -106,19 +102,6 @@ const resave = async ({ collection, doc, draft, pluginConfig, req }: ResaveArgs)
}
}
}
}
}
export const resaveChildren =
(pluginConfig: NestedDocsPluginConfig, collection: CollectionConfig): CollectionAfterChangeHook =>
async ({ doc, req }) => {
await resave({
collection,
doc,
draft: doc._status === 'published' ? false : true,
pluginConfig,
req,
})
return undefined
}

View File

@@ -1,4 +1,4 @@
import type { CollectionAfterChangeHook, CollectionConfig } from 'payload'
import type { CollectionAfterChangeHook } from 'payload'
import type { Breadcrumb, NestedDocsPluginConfig } from '../types.js'
@@ -6,8 +6,8 @@ import type { Breadcrumb, NestedDocsPluginConfig } from '../types.js'
// so that we can build its breadcrumbs with the newly created document's ID.
export const resaveSelfAfterCreate =
(pluginConfig: NestedDocsPluginConfig, collection: CollectionConfig): CollectionAfterChangeHook =>
async ({ doc, operation, req }) => {
(pluginConfig: NestedDocsPluginConfig): CollectionAfterChangeHook =>
async ({ collection, doc, operation, req }) => {
if (operation !== 'create') {
return undefined
}
@@ -16,11 +16,6 @@ export const resaveSelfAfterCreate =
const breadcrumbSlug = pluginConfig.breadcrumbsFieldSlug || 'breadcrumbs'
const breadcrumbs = doc[breadcrumbSlug] as unknown as Breadcrumb[]
const updateAsDraft =
typeof collection.versions === 'object' &&
collection.versions.drafts &&
doc._status !== 'published'
try {
await payload.update({
id: doc.id,
@@ -33,7 +28,7 @@ export const resaveSelfAfterCreate =
})) || [],
},
depth: 0,
draft: updateAsDraft,
draft: collection.versions.drafts && doc._status !== 'published',
locale,
req,
})

View File

@@ -5,10 +5,10 @@ import type { NestedDocsPluginConfig } from './types.js'
import { createBreadcrumbsField } from './fields/breadcrumbs.js'
import { createParentField } from './fields/parent.js'
import { parentFilterOptions } from './fields/parentFilterOptions.js'
import { populateBreadcrumbsBeforeChange } from './hooks/populateBreadcrumbsBeforeChange.js'
import { resaveChildren } from './hooks/resaveChildren.js'
import { resaveSelfAfterCreate } from './hooks/resaveSelfAfterCreate.js'
import { getParents } from './utilities/getParents.js'
import { populateBreadcrumbs } from './utilities/populateBreadcrumbs.js'
export { createBreadcrumbsField, createParentField, getParents }
@@ -53,13 +53,12 @@ export const nestedDocsPlugin =
hooks: {
...(collection.hooks || {}),
afterChange: [
resaveChildren(pluginConfig, collection),
resaveSelfAfterCreate(pluginConfig, collection),
resaveChildren(pluginConfig),
resaveSelfAfterCreate(pluginConfig),
...(collection?.hooks?.afterChange || []),
],
beforeChange: [
async ({ data, originalDoc, req }) =>
populateBreadcrumbs(req, pluginConfig, collection, data, originalDoc),
populateBreadcrumbsBeforeChange(pluginConfig),
...(collection?.hooks?.beforeChange || []),
],
},

View File

@@ -1,23 +1,30 @@
import type { CollectionConfig } from 'payload'
import type { SanitizedCollectionConfig } from 'payload'
import type { Breadcrumb, NestedDocsPluginConfig } from '../types.js'
import type { Breadcrumb, GenerateLabel, GenerateURL } from '../types.js'
export const formatBreadcrumb = (
pluginConfig: NestedDocsPluginConfig,
collection: CollectionConfig,
docs: Array<Record<string, unknown>>,
): Breadcrumb => {
type Args = {
collection: SanitizedCollectionConfig
docs: Record<string, unknown>[]
generateLabel?: GenerateLabel
generateURL?: GenerateURL
}
export const formatBreadcrumb = ({
collection,
docs,
generateLabel,
generateURL,
}: Args): Breadcrumb => {
let url: string | undefined = undefined
let label: string
const lastDoc = docs[docs.length - 1]!
if (typeof pluginConfig?.generateURL === 'function') {
url = pluginConfig.generateURL(docs, lastDoc)
if (typeof generateURL === 'function') {
url = generateURL(docs, lastDoc)
}
if (typeof pluginConfig?.generateLabel === 'function') {
label = pluginConfig.generateLabel(docs, lastDoc)
if (typeof generateLabel === 'function') {
label = generateLabel(docs, lastDoc)
} else {
const title = collection.admin?.useAsTitle ? lastDoc[collection.admin.useAsTitle] : ''

View File

@@ -1,14 +1,14 @@
import type { CollectionConfig, PayloadRequest } from 'payload'
import type { CollectionConfig, Document, PayloadRequest } from 'payload'
import type { NestedDocsPluginConfig } from '../types.js'
export const getParents = async (
req: PayloadRequest,
pluginConfig: NestedDocsPluginConfig,
pluginConfig: Pick<NestedDocsPluginConfig, 'generateLabel' | 'generateURL' | 'parentFieldSlug'>,
collection: CollectionConfig,
doc: Record<string, unknown>,
docs: Array<Record<string, unknown>> = [],
): Promise<Array<Record<string, unknown>>> => {
): Promise<Document[]> => {
const parentSlug = pluginConfig?.parentFieldSlug || 'parent'
const parent = doc[parentSlug]
let retrievedParent: null | Record<string, unknown> = null

View File

@@ -1,42 +1,62 @@
import type { CollectionConfig } from 'payload'
import type { Data, Document, PayloadRequest, SanitizedCollectionConfig } from 'payload'
import type { NestedDocsPluginConfig } from '../types.js'
import type { GenerateLabel, GenerateURL } from '../types.js'
import { formatBreadcrumb } from './formatBreadcrumb.js'
import { getParents } from './getParents.js'
import { getParents as getAllParentDocuments } from './getParents.js'
export const populateBreadcrumbs = async (
req: any,
pluginConfig: NestedDocsPluginConfig,
collection: CollectionConfig,
data: any,
originalDoc?: any,
): Promise<any> => {
type Args = {
breadcrumbsFieldName?: string
collection: SanitizedCollectionConfig
data: Data
generateLabel?: GenerateLabel
generateURL?: GenerateURL
originalDoc?: Document
parentFieldName?: string
req: PayloadRequest
}
export const populateBreadcrumbs = async ({
breadcrumbsFieldName = 'breadcrumbs',
collection,
data,
generateLabel,
generateURL,
originalDoc,
parentFieldName,
req,
}: Args): Promise<Data> => {
const newData = data
const breadcrumbDocs = [
...(await getParents(req, pluginConfig, collection, {
...originalDoc,
...data,
})),
]
const currentDocBreadcrumb = {
const currentDocument = {
...originalDoc,
...data,
}
if (originalDoc?.id) {
currentDocBreadcrumb.id = originalDoc?.id
}
breadcrumbDocs.push(currentDocBreadcrumb)
const breadcrumbs = breadcrumbDocs.map((_, i) =>
formatBreadcrumb(pluginConfig, collection, breadcrumbDocs.slice(0, i + 1)),
const allParentDocuments: Document[] = await getAllParentDocuments(
req,
{
generateLabel,
generateURL,
parentFieldSlug: parentFieldName,
},
collection,
currentDocument,
)
return {
...newData,
[pluginConfig?.breadcrumbsFieldSlug || 'breadcrumbs']: breadcrumbs,
if (originalDoc?.id) {
currentDocument.id = originalDoc?.id
}
allParentDocuments.push(currentDocument)
const breadcrumbs = allParentDocuments.map((_, i) =>
formatBreadcrumb({
collection,
docs: allParentDocuments.slice(0, i + 1),
generateLabel,
generateURL,
}),
)
newData[breadcrumbsFieldName] = breadcrumbs
return newData
}

View File

@@ -179,19 +179,15 @@ describe('@payloadcms/plugin-nested-docs', () => {
})
.then(({ docs }) => docs[0])
if (!updatedChild) {
return
}
// breadcrumbs should be updated
expect(updatedChild.breadcrumbs).toHaveLength(2)
expect(updatedChild!.breadcrumbs).toHaveLength(2)
expect(updatedChild.breadcrumbs?.[0]?.url).toStrictEqual('/parent-updated')
expect(updatedChild.breadcrumbs?.[1]?.url).toStrictEqual('/parent-updated/child')
expect(updatedChild!.breadcrumbs?.[0]?.url).toStrictEqual('/parent-updated')
expect(updatedChild!.breadcrumbs?.[1]?.url).toStrictEqual('/parent-updated/child')
// no other data should be affected
expect(updatedChild.title).toEqual('child doc')
expect(updatedChild.slug).toEqual('child')
expect(updatedChild!.title).toEqual('child doc')
expect(updatedChild!.slug).toEqual('child')
})
})