Skip to content

Commit c7b3204

Browse files
authored
fix: copy to locale with localized arrays and blocks generate new IDs to prevent errors in postgres (#10292)
Fixes #10093
1 parent d68a1ea commit c7b3204

File tree

5 files changed

+158
-7
lines changed

5 files changed

+158
-7
lines changed

packages/ui/src/utilities/copyDataFromLocale.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ObjectIdImport from 'bson-objectid'
12
import {
23
type CollectionSlug,
34
type Data,
@@ -7,6 +8,9 @@ import {
78
} from 'payload'
89
import { fieldAffectsData, tabHasName } from 'payload/shared'
910

11+
const ObjectId = (ObjectIdImport.default ||
12+
ObjectIdImport) as unknown as typeof ObjectIdImport.default
13+
1014
export type CopyDataFromLocaleArgs = {
1115
collectionSlug?: CollectionSlug
1216
docID?: number | string
@@ -33,10 +37,15 @@ function iterateFields(fields: Field[], fromLocaleData: Data, toLocaleData: Data
3337
break
3438
}
3539

36-
// if the field has a value but is not localized, loop over the data from target
37-
if (!field.localized && field.name in toLocaleData) {
40+
// if the field has a value - loop over the data from target
41+
if (field.name in toLocaleData) {
3842
toLocaleData[field.name].map((item: Data, index: number) => {
3943
if (fromLocaleData[field.name]?.[index]) {
44+
// Generate new IDs if the field is localized to prevent errors with relational DBs.
45+
if (field.localized) {
46+
toLocaleData[field.name][index].id = new ObjectId().toHexString()
47+
}
48+
4049
iterateFields(field.fields, fromLocaleData[field.name][index], item)
4150
}
4251
})
@@ -55,18 +64,24 @@ function iterateFields(fields: Field[], fromLocaleData: Data, toLocaleData: Data
5564
break
5665
}
5766

58-
// if the field has a value but is not localized, loop over the data from target
59-
if (!field.localized && field.name in toLocaleData) {
67+
// if the field has a value - loop over the data from target
68+
if (field.name in toLocaleData) {
6069
toLocaleData[field.name].map((blockData: Data, index: number) => {
6170
const blockFields = field.blocks.find(
6271
({ slug }) => slug === blockData.blockType,
6372
)?.fields
6473

74+
// Generate new IDs if the field is localized to prevent errors with relational DBs.
75+
if (field.localized) {
76+
toLocaleData[field.name][index].id = new ObjectId().toHexString()
77+
}
78+
6579
if (blockFields?.length) {
6680
iterateFields(blockFields, fromLocaleData[field.name][index], blockData)
6781
}
6882
})
6983
}
84+
7085
break
7186

7287
case 'checkbox':

test/localization/collections/NestedToArrayAndBlock/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,16 @@ export const NestedToArrayAndBlock: CollectionConfig = {
4747
},
4848
],
4949
},
50+
{
51+
name: 'topLevelArrayLocalized',
52+
type: 'array',
53+
localized: true,
54+
fields: [
55+
{
56+
name: 'text',
57+
type: 'text',
58+
},
59+
],
60+
},
5061
],
5162
}

test/localization/e2e.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ describe('Localization', () => {
315315
const nestedArrayURL = new AdminUrlUtil(serverURL, nestedToArrayAndBlockCollectionSlug)
316316
await page.goto(nestedArrayURL.create)
317317
await changeLocale(page, 'ar')
318-
const addArrayRow = page.locator('.array-field__add-row')
318+
const addArrayRow = page.locator('#field-topLevelArray .array-field__add-row')
319319
await addArrayRow.click()
320320

321321
const arrayField = page.locator('#field-topLevelArray__0__localizedText')

test/localization/int.spec.ts

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1+
import type { Payload, User, Where } from 'payload'
2+
13
import path from 'path'
2-
import { type Payload, type Where } from 'payload'
4+
import { createLocalReq } from 'payload'
35
import { fileURLToPath } from 'url'
46

57
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
6-
import type { LocalizedPost, LocalizedSort, WithLocalizedRelationship } from './payload-types.js'
8+
import type {
9+
LocalizedPost,
10+
LocalizedSort,
11+
Nested,
12+
WithLocalizedRelationship,
13+
} from './payload-types.js'
14+
15+
import { devUser } from '../credentials.js'
716

17+
// eslint-disable-next-line payload/no-relative-monorepo-imports
18+
import { copyDataFromLocaleHandler } from '../../packages/ui/src/utilities/copyDataFromLocale.js'
819
import { idToString } from '../helpers/idToString.js'
920
import { initPayloadInt } from '../helpers/initPayloadInt.js'
1021
import { arrayCollectionSlug } from './collections/Array/index.js'
@@ -2451,6 +2462,108 @@ describe('Localization', () => {
24512462
).rejects.toBeTruthy()
24522463
})
24532464
})
2465+
2466+
describe('Copying To Locale', () => {
2467+
let user: User
2468+
2469+
beforeAll(async () => {
2470+
user = (
2471+
await payload.find({
2472+
collection: 'users',
2473+
where: {
2474+
email: {
2475+
equals: devUser.email,
2476+
},
2477+
},
2478+
})
2479+
).docs[0] as unknown as User
2480+
2481+
user['collection'] = 'users'
2482+
})
2483+
2484+
it('should copy to locale', async () => {
2485+
const doc = await payload.create({
2486+
collection: 'localized-posts',
2487+
data: {
2488+
title: 'Hello',
2489+
group: {
2490+
children: 'Children',
2491+
},
2492+
unique: 'unique-field',
2493+
localizedCheckbox: true,
2494+
},
2495+
})
2496+
2497+
const req = await createLocalReq({ user }, payload)
2498+
2499+
const res = (await copyDataFromLocaleHandler({
2500+
fromLocale: 'en',
2501+
req,
2502+
toLocale: 'es',
2503+
docID: doc.id,
2504+
collectionSlug: 'localized-posts',
2505+
})) as LocalizedPost
2506+
2507+
expect(res.title).toBe('Hello')
2508+
expect(res.group.children).toBe('Children')
2509+
expect(res.unique).toBe('unique-field')
2510+
expect(res.localizedCheckbox).toBe(true)
2511+
})
2512+
2513+
it('should copy localized nested to arrays', async () => {
2514+
const doc = await payload.create({
2515+
collection: 'nested',
2516+
locale: 'en',
2517+
data: {
2518+
topLevelArray: [
2519+
{
2520+
localizedText: 'some-localized-text',
2521+
notLocalizedText: 'some-not-localized-text',
2522+
},
2523+
],
2524+
},
2525+
})
2526+
2527+
const req = await createLocalReq({ user }, payload)
2528+
2529+
const res = (await copyDataFromLocaleHandler({
2530+
fromLocale: 'en',
2531+
req,
2532+
toLocale: 'es',
2533+
docID: doc.id,
2534+
collectionSlug: 'nested',
2535+
})) as Nested
2536+
2537+
expect(res.topLevelArray[0].localizedText).toBe('some-localized-text')
2538+
expect(res.topLevelArray[0].notLocalizedText).toBe('some-not-localized-text')
2539+
})
2540+
2541+
it('should copy localized arrays', async () => {
2542+
const doc = await payload.create({
2543+
collection: 'nested',
2544+
locale: 'en',
2545+
data: {
2546+
topLevelArrayLocalized: [
2547+
{
2548+
text: 'some-text',
2549+
},
2550+
],
2551+
},
2552+
})
2553+
2554+
const req = await createLocalReq({ user }, payload)
2555+
2556+
const res = (await copyDataFromLocaleHandler({
2557+
fromLocale: 'en',
2558+
req,
2559+
toLocale: 'es',
2560+
docID: doc.id,
2561+
collectionSlug: 'nested',
2562+
})) as Nested
2563+
2564+
expect(res.topLevelArrayLocalized[0].text).toBe('some-text')
2565+
})
2566+
})
24542567
})
24552568

24562569
describe('Localization with fallback false', () => {

test/localization/payload-types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,12 @@ export interface Nested {
471471
id?: string | null;
472472
}[]
473473
| null;
474+
topLevelArrayLocalized?:
475+
| {
476+
text?: string | null;
477+
id?: string | null;
478+
}[]
479+
| null;
474480
updatedAt: string;
475481
createdAt: string;
476482
}
@@ -1051,6 +1057,12 @@ export interface NestedSelect<T extends boolean = true> {
10511057
notLocalizedText?: T;
10521058
id?: T;
10531059
};
1060+
topLevelArrayLocalized?:
1061+
| T
1062+
| {
1063+
text?: T;
1064+
id?: T;
1065+
};
10541066
updatedAt?: T;
10551067
createdAt?: T;
10561068
}

0 commit comments

Comments
 (0)