Skip to content

Commit 43b8de6

Browse files
fix(ui): prevent data loss in copy-to-locale with drafts (#16073)
## Summary Fixes two related bugs in copyDataFromLocale that cause data loss when using the "Copy to Locale" feature with drafts/autosave enabled: 1. 500 error with draft-only documents: When a document only exists as a draft (never published), copy-to-locale failed with "Error fetching data from locale" because findByID without draft: true can't find draft-only documents. 2. Published content overwritten: When the source locale has both published and draft versions with different content, copying to another locale would overwrite the source locale's published content with the draft content. ### Root Cause The copyDataFromLocale function was: - Fetching documents without draft: true, failing on draft-only docs - Calling update() without draft: true, which creates a new published version that snapshots all locales - but using draft data instead of published data for the source locale ### Fix Added draft: true to: - All findByID/findGlobal calls (4 places) - allows finding draft-only documents - All update/updateGlobal calls (2 places) - creates draft instead of published, preserving existing published content ### Behavior Change Copy-to-locale now creates a draft in the target locale instead of immediately publishing. This is arguably better UX since users can review translations before publishing. ## Test Plan - Added integration tests for copy-to-locale with autosave/drafts - Added integration test for draft-only documents - Added integration test verifying published content isn't overwritten - Added e2e test for full UI flow
1 parent b2a8917 commit 43b8de6

3 files changed

Lines changed: 248 additions & 0 deletions

File tree

packages/ui/src/utilities/copyDataFromLocale.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
249249
? payload.findGlobal({
250250
slug: globalSlug,
251251
depth: 0,
252+
draft: true,
252253
locale: fromLocale,
253254
overrideAccess: false,
254255
user,
@@ -258,6 +259,7 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
258259
id: docID,
259260
collection: collectionSlug,
260261
depth: 0,
262+
draft: true,
261263
joins: false,
262264
locale: fromLocale,
263265
overrideAccess: false,
@@ -268,6 +270,7 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
268270
? payload.findGlobal({
269271
slug: globalSlug,
270272
depth: 0,
273+
draft: true,
271274
locale: toLocale,
272275
overrideAccess: false,
273276
user,
@@ -277,6 +280,7 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
277280
id: docID,
278281
collection: collectionSlug,
279282
depth: 0,
283+
draft: true,
280284
joins: false,
281285
locale: toLocale,
282286
overrideAccess: false,
@@ -310,6 +314,7 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
310314
? await payload.updateGlobal({
311315
slug: globalSlug,
312316
data,
317+
draft: true,
313318
locale: toLocale,
314319
overrideAccess: false,
315320
req,
@@ -319,6 +324,7 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
319324
id: docID,
320325
collection: collectionSlug,
321326
data,
327+
draft: true,
322328
locale: toLocale,
323329
overrideAccess: false,
324330
req,

test/localization/e2e.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,50 @@ describe('Localization', () => {
567567
// Should show error
568568
await expect(page.locator('.payload-toast-container .toast-error')).toBeVisible()
569569
})
570+
571+
test('should not lose source locale data when copying with autosave enabled', async () => {
572+
// This tests that copy-to-locale doesn't cause data loss
573+
// when operating on a collection with autosave enabled (blocks-fields)
574+
575+
// Create a document with blocks content in en locale
576+
await changeLocale(page, defaultLocale)
577+
await page.goto(urlBlocks.create)
578+
579+
// Fill in the title
580+
const titleField = page.locator('#field-title')
581+
await titleField.fill('English Block Title')
582+
583+
// Add a block with content
584+
await addBlock({ page, fieldName: 'content', blockToSelect: 'Block Inside Block' })
585+
const blockTextField = page.locator('#field-content__0__text')
586+
await blockTextField.fill('English block text content')
587+
588+
// Wait for autosave to complete
589+
await waitForAutoSaveToRunAndComplete(page)
590+
591+
// Verify autosave worked
592+
await expect(titleField).toHaveValue('English Block Title')
593+
await expect(blockTextField).toHaveValue('English block text content')
594+
595+
// Copy to Spanish locale
596+
await openCopyToLocaleDrawer(page)
597+
await setToLocale(page, 'Spanish')
598+
await runCopy({ page, toLocale: spanishLocale })
599+
600+
// Wait for the form to be ready after copy/navigation
601+
await waitForFormReady(page)
602+
603+
// Verify Spanish locale has the copied data
604+
await expect(page.locator('#field-title')).toHaveValue('English Block Title')
605+
606+
// CRITICAL: Switch back to English and verify data is NOT lost
607+
await changeLocale(page, defaultLocale)
608+
609+
await expect(page.locator('#field-title')).toHaveValue('English Block Title')
610+
await expect(page.locator('#field-content__0__text')).toHaveValue(
611+
'English block text content',
612+
)
613+
})
570614
})
571615

572616
describe('locale change', () => {

test/localization/int.spec.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3117,6 +3117,204 @@ describe('Localization', () => {
31173117
// The source data should remain unchanged
31183118
expect(refreshedDoc.topLevelArrayLocalized?.[0]?.text).toBe('some-text')
31193119
})
3120+
3121+
it('should copy to locale without losing data when autosave and drafts are enabled', async () => {
3122+
// The blocks-fields collection has versions.drafts.autosave: true
3123+
// This test verifies that copyToLocale doesn't cause data loss
3124+
// when operating on a collection with autosave enabled
3125+
3126+
// Create a document with content in en locale
3127+
const doc = await payload.create({
3128+
collection: 'blocks-fields',
3129+
locale: 'en',
3130+
data: {
3131+
title: 'English Title',
3132+
content: [
3133+
{
3134+
blockType: 'blockInsideBlock',
3135+
text: 'English block text',
3136+
content: [
3137+
{
3138+
blockType: 'textBlock',
3139+
text: 'Nested English text',
3140+
},
3141+
],
3142+
},
3143+
],
3144+
},
3145+
})
3146+
3147+
// Add content to Spanish locale separately
3148+
await payload.update({
3149+
collection: 'blocks-fields',
3150+
id: doc.id,
3151+
locale: 'es',
3152+
data: {
3153+
title: 'Spanish Title',
3154+
content: [
3155+
{
3156+
blockType: 'blockInsideBlock',
3157+
text: 'Spanish block text',
3158+
},
3159+
],
3160+
},
3161+
})
3162+
3163+
// Verify initial state - English data should exist
3164+
const enDocBefore = await payload.findByID({
3165+
id: doc.id,
3166+
collection: 'blocks-fields',
3167+
locale: 'en',
3168+
})
3169+
3170+
expect(enDocBefore.title).toBe('English Title')
3171+
expect(enDocBefore.content?.[0]?.text).toBe('English block text')
3172+
3173+
// Copy data from en to es
3174+
const req = await createLocalReq({ user }, payload)
3175+
3176+
await copyDataFromLocaleHandler({
3177+
fromLocale: 'en',
3178+
req,
3179+
toLocale: 'es',
3180+
docID: doc.id,
3181+
collectionSlug: 'blocks-fields',
3182+
overrideData: true,
3183+
})
3184+
3185+
// CRITICAL: Verify English data is NOT lost after copy operation
3186+
const enDocAfter = await payload.findByID({
3187+
id: doc.id,
3188+
collection: 'blocks-fields',
3189+
locale: 'en',
3190+
})
3191+
3192+
expect(enDocAfter.title).toBe('English Title')
3193+
expect(enDocAfter.content?.[0]?.text).toBe('English block text')
3194+
expect(enDocAfter.content?.[0]?.content?.[0]?.text).toBe('Nested English text')
3195+
3196+
// Verify Spanish locale received the copied data (as a draft)
3197+
const esDocAfter = await payload.findByID({
3198+
id: doc.id,
3199+
collection: 'blocks-fields',
3200+
locale: 'es',
3201+
draft: true,
3202+
})
3203+
3204+
expect(esDocAfter.title).toBe('English Title')
3205+
expect(esDocAfter.content?.[0]?.text).toBe('English block text')
3206+
})
3207+
3208+
it('should copy to locale without losing draft data when autosave is enabled', async () => {
3209+
// Create a document with draft content
3210+
const doc = await payload.create({
3211+
collection: 'blocks-fields',
3212+
locale: 'en',
3213+
draft: true,
3214+
data: {
3215+
title: 'Draft English Title',
3216+
content: [
3217+
{
3218+
blockType: 'blockInsideBlock',
3219+
text: 'Draft block text',
3220+
},
3221+
],
3222+
},
3223+
})
3224+
3225+
// Verify draft exists
3226+
const draftBefore = await payload.findByID({
3227+
id: doc.id,
3228+
collection: 'blocks-fields',
3229+
locale: 'en',
3230+
draft: true,
3231+
})
3232+
3233+
expect(draftBefore.title).toBe('Draft English Title')
3234+
3235+
// Copy draft data to another locale
3236+
const req = await createLocalReq({ user }, payload)
3237+
3238+
await copyDataFromLocaleHandler({
3239+
fromLocale: 'en',
3240+
req,
3241+
toLocale: 'es',
3242+
docID: doc.id,
3243+
collectionSlug: 'blocks-fields',
3244+
})
3245+
3246+
// Verify the source draft is not lost
3247+
const draftAfter = await payload.findByID({
3248+
id: doc.id,
3249+
collection: 'blocks-fields',
3250+
locale: 'en',
3251+
draft: true,
3252+
})
3253+
3254+
expect(draftAfter.title).toBe('Draft English Title')
3255+
expect(draftAfter.content?.[0]?.text).toBe('Draft block text')
3256+
})
3257+
3258+
it('should not overwrite published content when source has both published and draft versions', async () => {
3259+
// Create published doc in en
3260+
const doc = await payload.create({
3261+
collection: 'blocks-fields',
3262+
locale: 'en',
3263+
data: {
3264+
title: 'Published EN',
3265+
},
3266+
})
3267+
3268+
// Create draft with different content
3269+
await payload.update({
3270+
collection: 'blocks-fields',
3271+
id: doc.id,
3272+
locale: 'en',
3273+
draft: true,
3274+
data: {
3275+
title: 'Draft EN',
3276+
},
3277+
})
3278+
3279+
// Verify both published and draft exist with different content
3280+
const enPublishedBefore = await payload.findByID({
3281+
id: doc.id,
3282+
collection: 'blocks-fields',
3283+
locale: 'en',
3284+
draft: false,
3285+
})
3286+
const enDraftBefore = await payload.findByID({
3287+
id: doc.id,
3288+
collection: 'blocks-fields',
3289+
locale: 'en',
3290+
draft: true,
3291+
})
3292+
3293+
expect(enPublishedBefore.title).toBe('Published EN')
3294+
expect(enDraftBefore.title).toBe('Draft EN')
3295+
3296+
// Copy to another locale using the actual handler
3297+
const req = await createLocalReq({ user }, payload)
3298+
3299+
await copyDataFromLocaleHandler({
3300+
fromLocale: 'en',
3301+
req,
3302+
toLocale: 'es',
3303+
docID: doc.id,
3304+
collectionSlug: 'blocks-fields',
3305+
overrideData: true,
3306+
})
3307+
3308+
// Verify published content in source locale is NOT overwritten
3309+
const enPublishedAfter = await payload.findByID({
3310+
id: doc.id,
3311+
collection: 'blocks-fields',
3312+
locale: 'en',
3313+
draft: false,
3314+
})
3315+
3316+
expect(enPublishedAfter.title).toBe('Published EN')
3317+
})
31203318
})
31213319

31223320
describe('Multiple fallback locales', () => {

0 commit comments

Comments
 (0)