fix: copy to locale with localized arrays and blocks generate new IDs to prevent errors in postgres (#10292)
Fixes https://github.com/payloadcms/payload/issues/10093
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import ObjectIdImport from 'bson-objectid'
|
||||||
import {
|
import {
|
||||||
type CollectionSlug,
|
type CollectionSlug,
|
||||||
type Data,
|
type Data,
|
||||||
@@ -7,6 +8,9 @@ import {
|
|||||||
} from 'payload'
|
} from 'payload'
|
||||||
import { fieldAffectsData, tabHasName } from 'payload/shared'
|
import { fieldAffectsData, tabHasName } from 'payload/shared'
|
||||||
|
|
||||||
|
const ObjectId = (ObjectIdImport.default ||
|
||||||
|
ObjectIdImport) as unknown as typeof ObjectIdImport.default
|
||||||
|
|
||||||
export type CopyDataFromLocaleArgs = {
|
export type CopyDataFromLocaleArgs = {
|
||||||
collectionSlug?: CollectionSlug
|
collectionSlug?: CollectionSlug
|
||||||
docID?: number | string
|
docID?: number | string
|
||||||
@@ -33,10 +37,15 @@ function iterateFields(fields: Field[], fromLocaleData: Data, toLocaleData: Data
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the field has a value but is not localized, loop over the data from target
|
// if the field has a value - loop over the data from target
|
||||||
if (!field.localized && field.name in toLocaleData) {
|
if (field.name in toLocaleData) {
|
||||||
toLocaleData[field.name].map((item: Data, index: number) => {
|
toLocaleData[field.name].map((item: Data, index: number) => {
|
||||||
if (fromLocaleData[field.name]?.[index]) {
|
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)
|
iterateFields(field.fields, fromLocaleData[field.name][index], item)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -55,18 +64,24 @@ function iterateFields(fields: Field[], fromLocaleData: Data, toLocaleData: Data
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the field has a value but is not localized, loop over the data from target
|
// if the field has a value - loop over the data from target
|
||||||
if (!field.localized && field.name in toLocaleData) {
|
if (field.name in toLocaleData) {
|
||||||
toLocaleData[field.name].map((blockData: Data, index: number) => {
|
toLocaleData[field.name].map((blockData: Data, index: number) => {
|
||||||
const blockFields = field.blocks.find(
|
const blockFields = field.blocks.find(
|
||||||
({ slug }) => slug === blockData.blockType,
|
({ slug }) => slug === blockData.blockType,
|
||||||
)?.fields
|
)?.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) {
|
if (blockFields?.length) {
|
||||||
iterateFields(blockFields, fromLocaleData[field.name][index], blockData)
|
iterateFields(blockFields, fromLocaleData[field.name][index], blockData)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
|
|||||||
@@ -47,5 +47,16 @@ export const NestedToArrayAndBlock: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'topLevelArrayLocalized',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ describe('Localization', () => {
|
|||||||
const nestedArrayURL = new AdminUrlUtil(serverURL, nestedToArrayAndBlockCollectionSlug)
|
const nestedArrayURL = new AdminUrlUtil(serverURL, nestedToArrayAndBlockCollectionSlug)
|
||||||
await page.goto(nestedArrayURL.create)
|
await page.goto(nestedArrayURL.create)
|
||||||
await changeLocale(page, 'ar')
|
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()
|
await addArrayRow.click()
|
||||||
|
|
||||||
const arrayField = page.locator('#field-topLevelArray__0__localizedText')
|
const arrayField = page.locator('#field-topLevelArray__0__localizedText')
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
|
import type { Payload, User, Where } from 'payload'
|
||||||
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { type Payload, type Where } from 'payload'
|
import { createLocalReq } from 'payload'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
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 { idToString } from '../helpers/idToString.js'
|
||||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||||
import { arrayCollectionSlug } from './collections/Array/index.js'
|
import { arrayCollectionSlug } from './collections/Array/index.js'
|
||||||
@@ -2451,6 +2462,108 @@ describe('Localization', () => {
|
|||||||
).rejects.toBeTruthy()
|
).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', () => {
|
describe('Localization with fallback false', () => {
|
||||||
|
|||||||
@@ -471,6 +471,12 @@ export interface Nested {
|
|||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
|
topLevelArrayLocalized?:
|
||||||
|
| {
|
||||||
|
text?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -1051,6 +1057,12 @@ export interface NestedSelect<T extends boolean = true> {
|
|||||||
notLocalizedText?: T;
|
notLocalizedText?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
|
topLevelArrayLocalized?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
text?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user