diff --git a/packages/ui/src/utilities/copyDataFromLocale.ts b/packages/ui/src/utilities/copyDataFromLocale.ts index 9ca06288992..6eaa1980d37 100644 --- a/packages/ui/src/utilities/copyDataFromLocale.ts +++ b/packages/ui/src/utilities/copyDataFromLocale.ts @@ -1,3 +1,4 @@ +import ObjectIdImport from 'bson-objectid' import { type CollectionSlug, type Data, @@ -7,6 +8,9 @@ import { } from 'payload' import { fieldAffectsData, tabHasName } from 'payload/shared' +const ObjectId = (ObjectIdImport.default || + ObjectIdImport) as unknown as typeof ObjectIdImport.default + export type CopyDataFromLocaleArgs = { collectionSlug?: CollectionSlug docID?: number | string @@ -33,10 +37,15 @@ function iterateFields(fields: Field[], fromLocaleData: Data, toLocaleData: Data break } - // if the field has a value but is not localized, loop over the data from target - if (!field.localized && field.name in toLocaleData) { + // if the field has a value - loop over the data from target + if (field.name in toLocaleData) { toLocaleData[field.name].map((item: Data, index: number) => { if (fromLocaleData[field.name]?.[index]) { + // Generate new IDs if the field is localized to prevent errors with relational DBs. + if (field.localized) { + toLocaleData[field.name][index].id = new ObjectId().toHexString() + } + iterateFields(field.fields, fromLocaleData[field.name][index], item) } }) @@ -55,18 +64,24 @@ function iterateFields(fields: Field[], fromLocaleData: Data, toLocaleData: Data break } - // if the field has a value but is not localized, loop over the data from target - if (!field.localized && field.name in toLocaleData) { + // if the field has a value - loop over the data from target + if (field.name in toLocaleData) { toLocaleData[field.name].map((blockData: Data, index: number) => { const blockFields = field.blocks.find( ({ slug }) => slug === blockData.blockType, )?.fields + // Generate new IDs if the field is localized to prevent errors with relational DBs. + if (field.localized) { + toLocaleData[field.name][index].id = new ObjectId().toHexString() + } + if (blockFields?.length) { iterateFields(blockFields, fromLocaleData[field.name][index], blockData) } }) } + break case 'checkbox': diff --git a/test/localization/collections/NestedToArrayAndBlock/index.ts b/test/localization/collections/NestedToArrayAndBlock/index.ts index 06171c6befd..080dddfc8cf 100644 --- a/test/localization/collections/NestedToArrayAndBlock/index.ts +++ b/test/localization/collections/NestedToArrayAndBlock/index.ts @@ -47,5 +47,16 @@ export const NestedToArrayAndBlock: CollectionConfig = { }, ], }, + { + name: 'topLevelArrayLocalized', + type: 'array', + localized: true, + fields: [ + { + name: 'text', + type: 'text', + }, + ], + }, ], } diff --git a/test/localization/e2e.spec.ts b/test/localization/e2e.spec.ts index 869ad08972c..00cc6df75fd 100644 --- a/test/localization/e2e.spec.ts +++ b/test/localization/e2e.spec.ts @@ -315,7 +315,7 @@ describe('Localization', () => { const nestedArrayURL = new AdminUrlUtil(serverURL, nestedToArrayAndBlockCollectionSlug) await page.goto(nestedArrayURL.create) await changeLocale(page, 'ar') - const addArrayRow = page.locator('.array-field__add-row') + const addArrayRow = page.locator('#field-topLevelArray .array-field__add-row') await addArrayRow.click() const arrayField = page.locator('#field-topLevelArray__0__localizedText') diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index e260cae9827..1912c7be998 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -1,10 +1,21 @@ +import type { Payload, User, Where } from 'payload' + import path from 'path' -import { type Payload, type Where } from 'payload' +import { createLocalReq } from 'payload' import { fileURLToPath } from 'url' import type { NextRESTClient } from '../helpers/NextRESTClient.js' -import type { LocalizedPost, LocalizedSort, WithLocalizedRelationship } from './payload-types.js' +import type { + LocalizedPost, + LocalizedSort, + Nested, + WithLocalizedRelationship, +} from './payload-types.js' + +import { devUser } from '../credentials.js' +// eslint-disable-next-line payload/no-relative-monorepo-imports +import { copyDataFromLocaleHandler } from '../../packages/ui/src/utilities/copyDataFromLocale.js' import { idToString } from '../helpers/idToString.js' import { initPayloadInt } from '../helpers/initPayloadInt.js' import { arrayCollectionSlug } from './collections/Array/index.js' @@ -2451,6 +2462,108 @@ describe('Localization', () => { ).rejects.toBeTruthy() }) }) + + describe('Copying To Locale', () => { + let user: User + + beforeAll(async () => { + user = ( + await payload.find({ + collection: 'users', + where: { + email: { + equals: devUser.email, + }, + }, + }) + ).docs[0] as unknown as User + + user['collection'] = 'users' + }) + + it('should copy to locale', async () => { + const doc = await payload.create({ + collection: 'localized-posts', + data: { + title: 'Hello', + group: { + children: 'Children', + }, + unique: 'unique-field', + localizedCheckbox: true, + }, + }) + + const req = await createLocalReq({ user }, payload) + + const res = (await copyDataFromLocaleHandler({ + fromLocale: 'en', + req, + toLocale: 'es', + docID: doc.id, + collectionSlug: 'localized-posts', + })) as LocalizedPost + + expect(res.title).toBe('Hello') + expect(res.group.children).toBe('Children') + expect(res.unique).toBe('unique-field') + expect(res.localizedCheckbox).toBe(true) + }) + + it('should copy localized nested to arrays', async () => { + const doc = await payload.create({ + collection: 'nested', + locale: 'en', + data: { + topLevelArray: [ + { + localizedText: 'some-localized-text', + notLocalizedText: 'some-not-localized-text', + }, + ], + }, + }) + + const req = await createLocalReq({ user }, payload) + + const res = (await copyDataFromLocaleHandler({ + fromLocale: 'en', + req, + toLocale: 'es', + docID: doc.id, + collectionSlug: 'nested', + })) as Nested + + expect(res.topLevelArray[0].localizedText).toBe('some-localized-text') + expect(res.topLevelArray[0].notLocalizedText).toBe('some-not-localized-text') + }) + + it('should copy localized arrays', async () => { + const doc = await payload.create({ + collection: 'nested', + locale: 'en', + data: { + topLevelArrayLocalized: [ + { + text: 'some-text', + }, + ], + }, + }) + + const req = await createLocalReq({ user }, payload) + + const res = (await copyDataFromLocaleHandler({ + fromLocale: 'en', + req, + toLocale: 'es', + docID: doc.id, + collectionSlug: 'nested', + })) as Nested + + expect(res.topLevelArrayLocalized[0].text).toBe('some-text') + }) + }) }) describe('Localization with fallback false', () => { diff --git a/test/localization/payload-types.ts b/test/localization/payload-types.ts index c2e41412cd3..091f2240421 100644 --- a/test/localization/payload-types.ts +++ b/test/localization/payload-types.ts @@ -471,6 +471,12 @@ export interface Nested { id?: string | null; }[] | null; + topLevelArrayLocalized?: + | { + text?: string | null; + id?: string | null; + }[] + | null; updatedAt: string; createdAt: string; } @@ -1051,6 +1057,12 @@ export interface NestedSelect { notLocalizedText?: T; id?: T; }; + topLevelArrayLocalized?: + | T + | { + text?: T; + id?: T; + }; updatedAt?: T; createdAt?: T; }