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:
@@ -183,6 +183,17 @@ function mergeData(
|
|||||||
return toLocaleData
|
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) => {
|
export const copyDataFromLocaleHandler = async (args: CopyDataFromLocaleArgs) => {
|
||||||
const { req } = args
|
const { req } = args
|
||||||
|
|
||||||
@@ -288,7 +299,8 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
|
|||||||
throw new Error(`Error fetching data from locale "${toLocale}"`)
|
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
|
return globalSlug
|
||||||
? await payload.updateGlobal({
|
? await payload.updateGlobal({
|
||||||
@@ -296,8 +308,8 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
|
|||||||
data: overrideData
|
data: overrideData
|
||||||
? fromLocaleDataWithoutID
|
? fromLocaleDataWithoutID
|
||||||
: mergeData(
|
: mergeData(
|
||||||
fromLocaleData.value,
|
fromLocaleDataWithoutID,
|
||||||
toLocaleData.value,
|
toLocaleDataWithoutID,
|
||||||
globals[globalSlug].config.fields,
|
globals[globalSlug].config.fields,
|
||||||
req,
|
req,
|
||||||
false,
|
false,
|
||||||
@@ -313,8 +325,8 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
|
|||||||
data: overrideData
|
data: overrideData
|
||||||
? fromLocaleDataWithoutID
|
? fromLocaleDataWithoutID
|
||||||
: mergeData(
|
: mergeData(
|
||||||
fromLocaleData.value,
|
fromLocaleDataWithoutID,
|
||||||
toLocaleData.value,
|
toLocaleDataWithoutID,
|
||||||
collections[collectionSlug].config.fields,
|
collections[collectionSlug].config.fields,
|
||||||
req,
|
req,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||||
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.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 { nestedToArrayAndBlockCollectionSlug } from './collections/NestedToArrayAndBlock/index.js'
|
||||||
import { richTextSlug } from './collections/RichText/index.js'
|
import { richTextSlug } from './collections/RichText/index.js'
|
||||||
import {
|
import {
|
||||||
@@ -427,6 +428,30 @@ describe('Localization', () => {
|
|||||||
await expect(arrayField).toHaveValue(sampleText)
|
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 () => {
|
test('should default source locale to current locale', async () => {
|
||||||
await changeLocale(page, spanishLocale)
|
await changeLocale(page, spanishLocale)
|
||||||
await createAndSaveDoc(page, url, { title })
|
await createAndSaveDoc(page, url, { title })
|
||||||
|
|||||||
Reference in New Issue
Block a user