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:
@@ -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,
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
const resave = async ({ collection, doc, draft, pluginConfig, req }: ResaveArgs) => {
|
||||
const parentSlug = pluginConfig?.parentFieldSlug || 'parent'
|
||||
|
||||
if (draft) {
|
||||
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
|
||||
} else {
|
||||
}
|
||||
|
||||
const parentSlug = pluginConfig?.parentFieldSlug || 'parent'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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 || []),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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] : ''
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user