From 34ead72c8527ed2d8bb53d126ad29e0dede8bccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Nyborg?= Date: Tue, 29 Apr 2025 10:23:40 +0200 Subject: [PATCH] fix(ui): copyToLocale should not pass any id's to avoid duplicates (#11887) ### What? Using the `Copy To Locale` function causes validation errors on content with `id` fields in postgres, since these should be unique. ``` key not found: error:valueMustBeUnique key not found: error:followingFieldsInvalid [13:11:29] ERROR: There was an error copying data from "en" to "de" err: { "type": "ValidationError", "message": "error:followingFieldsInvalid id", "stack": ValidationError: error:followingFieldsInvalid id ``` ### Why? In `packages/ui/src/utilities/copyDataFromLocale.ts` we are passing all data from `fromLocaleData` including the `id` fields, which causes duplicates on fields with unique id's like `Blocks` and `Arrays`. ### How? To resolve this i implemented a function that recursively remove any `id` field on the passed data. ### Fixes - https://github.com/payloadcms/payload/issues/10684 - https://discord.com/channels/967097582721572934/1351497930984521800 --------- Co-authored-by: Jessica Chowdhury --- .../ui/src/utilities/copyDataFromLocale.ts | 22 ++++++++++++---- test/localization/e2e.spec.ts | 25 +++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/utilities/copyDataFromLocale.ts b/packages/ui/src/utilities/copyDataFromLocale.ts index c934b7f0c1..5bf9711c38 100644 --- a/packages/ui/src/utilities/copyDataFromLocale.ts +++ b/packages/ui/src/utilities/copyDataFromLocale.ts @@ -183,6 +183,17 @@ function mergeData( return toLocaleData } +function removeIds(data: Data): Data { + if (Array.isArray(data)) { + return data.map(removeIds) + } + if (typeof data === 'object' && data !== null) { + const { id: _id, ...rest } = data + return Object.fromEntries(Object.entries(rest).map(([key, value]) => [key, removeIds(value)])) + } + return data +} + export const copyDataFromLocaleHandler = async (args: CopyDataFromLocaleArgs) => { const { req } = args @@ -288,7 +299,8 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => { throw new Error(`Error fetching data from locale "${toLocale}"`) } - const { id, ...fromLocaleDataWithoutID } = fromLocaleData.value + const fromLocaleDataWithoutID = removeIds(fromLocaleData.value) + const toLocaleDataWithoutID = removeIds(toLocaleData.value) return globalSlug ? await payload.updateGlobal({ @@ -296,8 +308,8 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => { data: overrideData ? fromLocaleDataWithoutID : mergeData( - fromLocaleData.value, - toLocaleData.value, + fromLocaleDataWithoutID, + toLocaleDataWithoutID, globals[globalSlug].config.fields, req, false, @@ -313,8 +325,8 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => { data: overrideData ? fromLocaleDataWithoutID : mergeData( - fromLocaleData.value, - toLocaleData.value, + fromLocaleDataWithoutID, + toLocaleDataWithoutID, collections[collectionSlug].config.fields, req, false, diff --git a/test/localization/e2e.spec.ts b/test/localization/e2e.spec.ts index 1916677181..b1f1884301 100644 --- a/test/localization/e2e.spec.ts +++ b/test/localization/e2e.spec.ts @@ -26,6 +26,7 @@ import { import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js' +import { blocksCollectionSlug } from './collections/Blocks/index.js' import { nestedToArrayAndBlockCollectionSlug } from './collections/NestedToArrayAndBlock/index.js' import { richTextSlug } from './collections/RichText/index.js' import { @@ -427,6 +428,30 @@ describe('Localization', () => { await expect(arrayField).toHaveValue(sampleText) }) + test('should copy block to locale', async () => { + const sampleText = 'Copy this text' + const blocksCollection = new AdminUrlUtil(serverURL, blocksCollectionSlug) + await page.goto(blocksCollection.create) + await changeLocale(page, 'pt') + const addBlock = page.locator('.blocks-field__drawer-toggler') + await addBlock.click() + const selectBlock = page.locator('.blocks-drawer__block button') + await selectBlock.click() + const addContentButton = page.locator('#field-content__0__content button') + await addContentButton.click() + await selectBlock.click() + const textField = page.locator('#field-content__0__content__0__text') + await expect(textField).toBeVisible() + await textField.fill(sampleText) + await saveDocAndAssert(page) + + await openCopyToLocaleDrawer(page) + await setToLocale(page, 'English') + await runCopy(page) + + await expect(textField).toHaveValue(sampleText) + }) + test('should default source locale to current locale', async () => { await changeLocale(page, spanishLocale) await createAndSaveDoc(page, url, { title })