From 1afca33bbf3522e8dcb17580e9c4ffd5302abc2d Mon Sep 17 00:00:00 2001 From: jaccovdalfsen Date: Tue, 21 Jul 2026 14:43:08 +0200 Subject: [PATCH] fix(plugin-import-export): respect defaultVersionStatus/draft on update-mode imports processImportBatch never passed draft to payload.update() (or applied the defaultVersionStatus fallback) on the update-existing-doc path for importMode: 'update'/'upsert', so re-importing a versioned document always overwrote the published version live regardless of defaultVersionStatus or an incoming draft _status, since core's update operation only saves a draft when the draft argument is explicitly passed. Adds a regression test under the posts-imports-only suite (which already configures defaultVersionStatus: 'draft') asserting the published version is preserved and the update lands in a new draft version, plus published- version assertions on the existing upsert-mode test to cover the published default-status path. --- .../src/import/batchProcessor.ts | 16 +++- test/plugin-import-export/int.spec.ts | 76 +++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/packages/plugin-import-export/src/import/batchProcessor.ts b/packages/plugin-import-export/src/import/batchProcessor.ts index c2ca76f6b80..cbaf75bacba 100644 --- a/packages/plugin-import-export/src/import/batchProcessor.ts +++ b/packages/plugin-import-export/src/import/batchProcessor.ts @@ -356,6 +356,16 @@ async function processImportBatch({ delete updateData.createdAt delete updateData.updatedAt + // Only handle _status for versioned collections + let draftOption: boolean | undefined + if (collectionHasVersions) { + // Use defaultVersionStatus from config if _status not provided + const statusValue = updateData._status || options.defaultVersionStatus + const isPublished = statusValue !== 'draft' + draftOption = !isPublished + updateData._status = statusValue + } + // Check if we have multi-locale data and extract it const { flatData, hasMultiLocale, localeUpdates } = extractMultiLocaleData( updateData, @@ -389,7 +399,7 @@ async function processImportBatch({ collection: collectionSlug, data: flatData, depth: 0, - // Don't specify draft - this creates a new draft for versioned collections + draft: draftOption, overrideAccess: false, req: defaultLocaleReq, user, @@ -428,14 +438,12 @@ async function processImportBatch({ }) } - // Update the document - don't specify draft to let Payload handle versions properly - // This will create a new draft version for collections with versions enabled savedDocument = await req.payload.update({ id: existingDoc.id as number | string, collection: collectionSlug, data: updateData, depth: 0, - // Don't specify draft - this creates a new draft for versioned collections + draft: draftOption, overrideAccess: false, req, user, diff --git a/test/plugin-import-export/int.spec.ts b/test/plugin-import-export/int.spec.ts index 97ab2f1348d..a77f11db915 100644 --- a/test/plugin-import-export/int.spec.ts +++ b/test/plugin-import-export/int.spec.ts @@ -3298,6 +3298,13 @@ describe('@payloadcms/plugin-import-export', () => { expect(draftPage.title).toBe(`Upsert Test ${timestamp} Updated`) expect(draftPage.excerpt).toBe('updated') + // Pages has no defaultVersionStatus override, so it falls back to 'published' - + // the update is expected to apply directly to the published version too. + expect(publishedPage).toBeDefined() + expect(publishedPage._status).toBe('published') + expect(publishedPage.title).toBe(`Upsert Test ${timestamp} Updated`) + expect(publishedPage.excerpt).toBe('updated') + const newPages = await payload.find({ collection: 'pages', where: { @@ -6197,6 +6204,75 @@ describe('@payloadcms/plugin-import-export', () => { }, }) }) + + it('should preserve the published version and create a draft when updating with defaultVersionStatus draft', async () => { + const timestamp = Date.now() + const existingDoc = await payload.create({ + collection: 'posts-imports-only', + draft: false, + data: { + title: `Draft Status Update Test ${timestamp}`, + _status: 'published', + }, + }) + + const csvContent = + 'id,title\n' + `${existingDoc.id},"Draft Status Update Test ${timestamp} Updated"` + const csvBuffer = Buffer.from(csvContent) + + let importDoc = await payload.create({ + collection: 'imports', + user, + data: { + collectionSlug: 'posts-imports-only', + importMode: 'update', + matchField: 'id', + }, + file: { + data: csvBuffer, + mimetype: 'text/csv', + name: 'draft-status-update-test.csv', + size: csvBuffer.length, + }, + }) + + await payload.jobs.run() + + importDoc = await payload.findByID({ + collection: 'imports', + id: importDoc.id, + }) + + expect(importDoc.status).toBe('completed') + expect(importDoc.summary?.updated).toBe(1) + expect(importDoc.summary?.issues).toBe(0) + + // The row omitted `_status`, so the plugin's `defaultVersionStatus: 'draft'` + // config for this collection should apply - the published version must stay + // untouched and the incoming data should land in a new draft version instead. + const publishedDoc = await payload.findByID({ + collection: 'posts-imports-only', + id: existingDoc.id, + draft: false, + }) + + expect(publishedDoc._status).toBe('published') + expect(publishedDoc.title).toBe(`Draft Status Update Test ${timestamp}`) + + const draftDoc = await payload.findByID({ + collection: 'posts-imports-only', + id: existingDoc.id, + draft: true, + }) + + expect(draftDoc._status).toBe('draft') + expect(draftDoc.title).toBe(`Draft Status Update Test ${timestamp} Updated`) + + await payload.delete({ + collection: 'posts-imports-only', + id: existingDoc.id, + }) + }) }) })