Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: copy to locale with localized arrays and blocks generate new IDs to prevent errors in postgres #10292

Merged
merged 6 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions packages/ui/src/utilities/copyDataFromLocale.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ObjectIdImport from 'bson-objectid'
import {
type CollectionSlug,
type Data,
Expand All @@ -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
Expand All @@ -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)
}
})
Expand All @@ -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':
Expand Down
11 changes: 11 additions & 0 deletions test/localization/collections/NestedToArrayAndBlock/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,16 @@ export const NestedToArrayAndBlock: CollectionConfig = {
},
],
},
{
name: 'topLevelArrayLocalized',
type: 'array',
localized: true,
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
}
2 changes: 1 addition & 1 deletion test/localization/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
117 changes: 115 additions & 2 deletions test/localization/int.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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', () => {
Expand Down
12 changes: 12 additions & 0 deletions test/localization/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,12 @@ export interface Nested {
id?: string | null;
}[]
| null;
topLevelArrayLocalized?:
| {
text?: string | null;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
Expand Down Expand Up @@ -1051,6 +1057,12 @@ export interface NestedSelect<T extends boolean = true> {
notLocalizedText?: T;
id?: T;
};
topLevelArrayLocalized?:
| T
| {
text?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
Expand Down