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 <jessica@trbl.design>
This commit is contained in:
Bjørn Nyborg
2025-04-29 10:23:40 +02:00
committed by GitHub
parent caae5986f5
commit 34ead72c85
2 changed files with 42 additions and 5 deletions

View File

@@ -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,

View File

@@ -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 })