Skip to content

Commit f470699

Browse files
fix: draft doc validation when duplicating docs (#15816)
### What? This PR defaults the `draft` value to true when it is not set during a duplicate operation. ### Why? It validates the required fields even if the user is trying to duplicate a draft doc. The regression was introduced when [`packages/payload/src/collections/endpoints/duplicate.ts`](https://github.com/payloadcms/payload/blob/v3.77.0/packages/payload/src/collections/endpoints/duplicate.ts) was refactored to use the shared `parseParams` utility instead of manually parsing query parameters. **v3.62.0 (working)** — In [v3.62.0 of `duplicate.ts`](https://github.com/payloadcms/payload/blob/v3.62.0/packages/payload/src/collections/endpoints/duplicate.ts), the `draft` parameter was explicitly defaulted to `true`: ```typescript // draft defaults to true, unless explicitly set requested as false // to prevent the newly duplicated document from being published const draft = searchParams.get('draft') !== 'false' ``` **v3.77.0 (broken)** — In [v3.77.0 of `duplicate.ts`](https://github.com/payloadcms/payload/blob/v3.77.0/packages/payload/src/collections/endpoints/duplicate.ts), the endpoint uses [`parseParams`](https://github.com/payloadcms/payload/blob/v3.77.0/packages/payload/src/utilities/parseParams/index.ts): ```typescript const { depth, draft, populate, select, selectedLocales } = parseParams(req.query) ``` `parseParams` only sets `draft` to `true` if the query string explicitly contains `?draft=true`. The admin UI's [`DuplicateDocument`](https://github.com/payloadcms/payload/blob/v3.77.0/packages/ui/src/elements/DuplicateDocument/index.tsx) component does **not** send `?draft=true` as a query parameter, so `draft` is `undefined`. The UI does send `{ _status: 'draft' }` in the request body, but that does not influence the `draft` flag. In [`createOperation`](https://github.com/payloadcms/payload/blob/v3.77.0/packages/payload/src/collections/operations/create.ts), the validation skip logic depends entirely on the `draft` argument: ```typescript const isSavingDraft = Boolean(draft && hasDraftsEnabled(collectionConfig) && !publishAllLocales) // ... skipValidation: isSavingDraft && !hasDraftValidationEnabled(collectionConfig), ``` Since `draft` is `undefined`, `isSavingDraft` is `false`, `skipValidation` is `false`, and full validation runs — rejecting the duplicate when any required fields are empty. ### How? By setting the `draft` value as `true` by default when it is not set during a duplicate operation: Fixes #15815 --------- Co-authored-by: German Jablonski <GermanJablo@users.noreply.github.com>
1 parent f0498f2 commit f470699

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

packages/payload/src/collections/endpoints/duplicate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { duplicateOperation } from '../operations/duplicate.js'
1111
export const duplicateHandler: PayloadHandler = async (req) => {
1212
const { id, collection } = getRequestCollectionWithID(req)
1313

14-
const { depth, draft, populate, select, selectedLocales } = parseParams(req.query)
14+
const { depth, draft = true, populate, select, selectedLocales } = parseParams(req.query)
1515

1616
const doc = await duplicateOperation({
1717
id,

test/versions/int.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,63 @@ describe('Versions', () => {
476476
})
477477

478478
expect(duplicatedDoc._status).toBe('draft')
479+
480+
await payload.delete({ collection: draftCollectionSlug, id: originalDoc.id })
481+
await payload.delete({ collection: draftCollectionSlug, id: duplicatedDoc.id })
482+
})
483+
484+
it('should duplicate a draft document with empty required fields via local API', async () => {
485+
const originalDoc = await payload.create({
486+
collection: draftCollectionSlug,
487+
data: {
488+
title: 'Draft with partial data',
489+
_status: 'draft',
490+
},
491+
draft: true,
492+
})
493+
494+
// description is required but missing — duplicate should still succeed as a draft
495+
const duplicatedDoc = await payload.duplicate({
496+
id: originalDoc.id,
497+
collection: draftCollectionSlug,
498+
draft: true,
499+
})
500+
501+
expect(duplicatedDoc._status).toBe('draft')
502+
expect(duplicatedDoc.id).not.toEqual(originalDoc.id)
503+
expect(duplicatedDoc.title).toContain('Draft with partial data')
504+
505+
await payload.delete({ collection: draftCollectionSlug, id: originalDoc.id })
506+
await payload.delete({ collection: draftCollectionSlug, id: duplicatedDoc.id })
507+
})
508+
509+
it('should duplicate a draft document with empty required fields via REST API without explicit draft param', async () => {
510+
const originalDoc = await payload.create({
511+
collection: draftCollectionSlug,
512+
data: {
513+
title: 'REST draft partial',
514+
_status: 'draft',
515+
},
516+
draft: true,
517+
})
518+
519+
// Mimics the admin UI: POST to /:collection/:id/duplicate
520+
// with { _status: 'draft' } in body and NO draft query parameter
521+
const response = await restClient.POST(
522+
`/${draftCollectionSlug}/${originalDoc.id}/duplicate`,
523+
{
524+
body: JSON.stringify({ _status: 'draft' }),
525+
},
526+
)
527+
528+
const { doc } = await response.json()
529+
530+
expect(response.status).toBe(200)
531+
expect(doc._status).toBe('draft')
532+
expect(doc.id).not.toEqual(originalDoc.id)
533+
534+
await payload.delete({ collection: draftCollectionSlug, id: originalDoc.id })
535+
await payload.delete({ collection: draftCollectionSlug, id: doc.id })
479536
})
480537
})
481538

0 commit comments

Comments
 (0)