fix: duplicating localized nested arrays (#8220)

Fixes an issue where duplicating documents in Postgres / SQLite would
crash because of a foreign key constraint / unique ID issue when you
have nested arrays / blocks within localized arrays / blocks.

We now run `beforeDuplicate` against all locales prior to
`beforeValidate` and `beforeChange` hooks.

This PR also fixes a series of issues in Postgres / SQLite where you
have localized groups / named tabs, and then arrays / blocks within the
localized groups / named tabs.
This commit is contained in:
James Mikrut
2024-09-14 22:51:31 -04:00
committed by GitHub
parent 8fc2c43190
commit 5873a3db06
19 changed files with 592 additions and 84 deletions

View File

@@ -200,7 +200,7 @@ user-friendly.
The `beforeDuplicate` field hook is called on each locale (when using localization), when duplicating a document. It may be used when documents having the
exact same properties may cause issue. This gives you a way to avoid duplicate names on `unique`, `required` fields or when external systems expect non-repeating values on documents.
This hook gets called after `beforeChange` hooks are called and before the document is saved to the database.
This hook gets called before the `beforeValidate` and `beforeChange` hooks are called.
By Default, unique and required text fields Payload will append "- Copy" to the original document value. The default is not added if your field has its own, you must return non-unique values from your beforeDuplicate hook to avoid errors or enable the `disableDuplicate` option on the collection.
Here is an example of a number field with a hook that increments the number to avoid unique constraint errors when duplicating a document:

View File

@@ -166,7 +166,8 @@ export const traverseFields = ({
if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
hasLocalizedManyTextField = true
@@ -199,7 +200,8 @@ export const traverseFields = ({
if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
hasLocalizedManyNumberField = true
@@ -279,7 +281,8 @@ export const traverseFields = ({
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
baseColumns.locale = text('locale', { enum: locales }).notNull()
@@ -365,7 +368,8 @@ export const traverseFields = ({
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
baseColumns._locale = text('_locale', { enum: locales }).notNull()
@@ -503,7 +507,8 @@ export const traverseFields = ({
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
baseColumns._locale = text('_locale', { enum: locales }).notNull()

View File

@@ -172,7 +172,8 @@ export const traverseFields = ({
if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
hasLocalizedManyTextField = true
@@ -205,7 +206,8 @@ export const traverseFields = ({
if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
hasLocalizedManyNumberField = true
@@ -300,7 +302,8 @@ export const traverseFields = ({
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
@@ -382,7 +385,8 @@ export const traverseFields = ({
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
@@ -516,7 +520,8 @@ export const traverseFields = ({
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()

View File

@@ -489,6 +489,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
valuesToTransform.push({
ref: localizedFieldData,
table: {
...table,
...localeRow,
},
})
@@ -526,7 +527,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
relationships,
table,
texts,
withinArrayOrBlockLocale,
withinArrayOrBlockLocale: locale || withinArrayOrBlockLocale,
})
if ('_order' in ref) {

View File

@@ -14,6 +14,7 @@ import { APIError, Forbidden, NotFound } from '../../errors/index.js'
import { afterChange } from '../../fields/hooks/afterChange/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { beforeChange } from '../../fields/hooks/beforeChange/index.js'
import { beforeDuplicate } from '../../fields/hooks/beforeDuplicate/index.js'
import { beforeValidate } from '../../fields/hooks/beforeValidate/index.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
@@ -93,7 +94,7 @@ export const duplicateOperation = async <TSlug extends CollectionSlug>(
where: combineQueries({ id: { equals: id } }, accessResults),
}
const docWithLocales = await getLatestCollectionVersion({
let docWithLocales = await getLatestCollectionVersion({
id,
config: collectionConfig,
payload,
@@ -112,6 +113,15 @@ export const duplicateOperation = async <TSlug extends CollectionSlug>(
delete docWithLocales.createdAt
delete docWithLocales.id
docWithLocales = await beforeDuplicate({
id,
collection: collectionConfig,
context: req.context,
doc: docWithLocales,
overrideAccess,
req,
})
// for version enabled collections, override the current status with draft, unless draft is explicitly set to false
if (shouldSaveDraft) {
docWithLocales._status = 'draft'
@@ -205,7 +215,6 @@ export const duplicateOperation = async <TSlug extends CollectionSlug>(
data,
doc: originalDoc,
docWithLocales,
duplicate: true,
global: null,
operation,
req,

View File

@@ -1,18 +0,0 @@
import ObjectIdImport from 'bson-objectid'
import type { FieldHook } from '../config/types.js'
const ObjectId = (ObjectIdImport.default ||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
/**
* Arrays and Blocks need to clear ids beforeDuplicate
*/
export const baseBeforeDuplicateArrays: FieldHook = ({ value }) => {
if (value) {
value = value.map((item) => {
item.id = new ObjectId().toHexString()
return item
})
return value
}
}

View File

@@ -25,6 +25,11 @@ export const baseIDField: TextField = {
return value
},
],
beforeDuplicate: [
() => {
return new ObjectId().toHexString()
},
],
},
label: 'ID',
}

View File

@@ -12,7 +12,6 @@ import {
} from '../../errors/index.js'
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
import { baseBeforeDuplicateArrays } from '../baseFields/baseBeforeDuplicateArrays.js'
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
import { baseIDField } from '../baseFields/baseIDField.js'
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
@@ -131,15 +130,6 @@ export const sanitizeFields = async ({
if (field.type === 'array' && field.fields) {
field.fields.push(baseIDField)
if (field.localized) {
if (!field.hooks) {
field.hooks = {}
}
if (!field.hooks.beforeDuplicate) {
field.hooks.beforeDuplicate = []
}
field.hooks.beforeDuplicate.push(baseBeforeDuplicateArrays)
}
}
if ((field.type === 'blocks' || field.type === 'array') && field.label) {
@@ -220,15 +210,6 @@ export const sanitizeFields = async ({
}
if (field.type === 'blocks' && field.blocks) {
if (field.localized) {
if (!field.hooks) {
field.hooks = {}
}
if (!field.hooks.beforeDuplicate) {
field.hooks.beforeDuplicate = []
}
field.hooks.beforeDuplicate.push(baseBeforeDuplicateArrays)
}
for (const block of field.blocks) {
if (block._sanitized === true) {
continue

View File

@@ -12,7 +12,6 @@ type Args<T extends JsonObject> = {
data: T
doc: T
docWithLocales: JsonObject
duplicate?: boolean
global: null | SanitizedGlobalConfig
id?: number | string
operation: Operation
@@ -26,7 +25,6 @@ type Args<T extends JsonObject> = {
* - Execute field hooks
* - Validate data
* - Transform data for storage
* - beforeDuplicate hooks (if duplicate)
* - Unflatten locales. The input `data` is the normal document for one locale. The output result will become the document with locales.
*/
export const beforeChange = async <T extends JsonObject>({
@@ -36,7 +34,6 @@ export const beforeChange = async <T extends JsonObject>({
data: incomingData,
doc,
docWithLocales,
duplicate = false,
global,
operation,
req,
@@ -53,7 +50,6 @@ export const beforeChange = async <T extends JsonObject>({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: collection?.fields || global?.fields,
global,

View File

@@ -8,7 +8,6 @@ import { MissingEditorProp } from '../../../errors/index.js'
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
import { fieldAffectsData, tabHasName } from '../../config/types.js'
import { getFieldPaths } from '../../getFieldPaths.js'
import { beforeDuplicate } from './beforeDuplicate.js'
import { getExistingRowDoc } from './getExistingRowDoc.js'
import { traverseFields } from './traverseFields.js'
@@ -18,7 +17,6 @@ type Args = {
data: JsonObject
doc: JsonObject
docWithLocales: JsonObject
duplicate: boolean
errors: { field: string; message: string }[]
field: Field | TabAsField
global: null | SanitizedGlobalConfig
@@ -55,7 +53,6 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
field,
global,
@@ -176,16 +173,11 @@ export const promise = async ({
const localeData = await localization.localeCodes.reduce(
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
const localizedValues = await localizedValuesPromise
let fieldValue =
const fieldValue =
locale === req.locale
? siblingData[field.name]
: siblingDocWithLocales?.[field.name]?.[locale]
if (duplicate && field.hooks?.beforeDuplicate?.length) {
beforeDuplicateArgs.value = fieldValue
fieldValue = await beforeDuplicate(beforeDuplicateArgs)
}
// const result = await localizedValues
// update locale value if it's not undefined
if (typeof fieldValue !== 'undefined') {
@@ -205,10 +197,6 @@ export const promise = async ({
siblingData[field.name] = localeData
}
})
} else if (duplicate && field.hooks?.beforeDuplicate?.length) {
mergeLocaleActions.push(async () => {
siblingData[field.name] = await beforeDuplicate(beforeDuplicateArgs)
})
}
}
@@ -250,7 +238,6 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: field.fields,
global,
@@ -282,7 +269,6 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: field.fields,
global,
@@ -332,7 +318,6 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: block.fields,
global,
@@ -365,7 +350,6 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: field.fields,
global,
@@ -411,7 +395,6 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: field.fields,
global,
@@ -437,7 +420,6 @@ export const promise = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
global,
@@ -474,7 +456,6 @@ export const promise = async ({
context,
data,
docWithLocales,
duplicate,
errors,
field,
global,

View File

@@ -17,7 +17,6 @@ type Args = {
* The original data with locales (not modified by any hooks)
*/
docWithLocales: JsonObject
duplicate: boolean
errors: { field: string; message: string }[]
fields: (Field | TabAsField)[]
global: null | SanitizedGlobalConfig
@@ -54,7 +53,6 @@ export const traverseFields = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
fields,
global,
@@ -79,7 +77,6 @@ export const traverseFields = async ({
data,
doc,
docWithLocales,
duplicate,
errors,
field,
global,

View File

@@ -0,0 +1,46 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
import { deepCopyObjectSimple } from '../../../utilities/deepCopyObject.js'
import { traverseFields } from './traverseFields.js'
type Args<T extends JsonObject> = {
collection: null | SanitizedCollectionConfig
context: RequestContext
doc?: T
id?: number | string
overrideAccess: boolean
req: PayloadRequest
}
/**
* This function is responsible for running beforeDuplicate hooks
* against a document including all locale data.
* It will run each field's beforeDuplicate hook
* and return the resulting docWithLocales.
*/
export const beforeDuplicate = async <T extends JsonObject>({
id,
collection,
context,
doc,
overrideAccess,
req,
}: Args<T>): Promise<T> => {
const newDoc = deepCopyObjectSimple(doc)
await traverseFields({
id,
collection,
context,
doc: newDoc,
fields: collection?.fields,
overrideAccess,
path: [],
req,
schemaPath: [],
siblingDoc: newDoc,
})
return newDoc
}

View File

@@ -0,0 +1,351 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { Field, FieldHookArgs, TabAsField } from '../../config/types.js'
import { fieldAffectsData, tabHasName } from '../../config/types.js'
import { getFieldPaths } from '../../getFieldPaths.js'
import { runBeforeDuplicateHooks } from './runHook.js'
import { traverseFields } from './traverseFields.js'
type Args<T> = {
collection: null | SanitizedCollectionConfig
context: RequestContext
doc: T
field: Field | TabAsField
id?: number | string
overrideAccess: boolean
parentPath: (number | string)[]
parentSchemaPath: string[]
req: PayloadRequest
siblingDoc: JsonObject
}
export const promise = async <T>({
id,
collection,
context,
doc,
field,
overrideAccess,
parentPath,
parentSchemaPath,
req,
siblingDoc,
}: Args<T>): Promise<void> => {
const { localization } = req.payload.config
const { path: fieldPath, schemaPath: fieldSchemaPath } = getFieldPaths({
field,
parentPath,
parentSchemaPath,
})
if (fieldAffectsData(field)) {
let fieldData = siblingDoc?.[field.name]
const fieldIsLocalized = field.localized && localization
// Run field beforeDuplicate hooks
if (Array.isArray(field.hooks?.beforeDuplicate)) {
if (fieldIsLocalized) {
const localeData = await localization.localeCodes.reduce(
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
const localizedValues = await localizedValuesPromise
const beforeDuplicateArgs: FieldHookArgs = {
collection,
context,
data: doc,
field,
global: undefined,
path: fieldPath,
previousSiblingDoc: siblingDoc,
previousValue: siblingDoc[field.name]?.[locale],
req,
schemaPath: parentSchemaPath,
siblingData: siblingDoc,
siblingDocWithLocales: siblingDoc,
value: siblingDoc[field.name]?.[locale],
}
const hookResult = await runBeforeDuplicateHooks(beforeDuplicateArgs)
if (typeof hookResult !== 'undefined') {
return {
...localizedValues,
[locale]: hookResult,
}
}
return localizedValuesPromise
},
Promise.resolve({}),
)
siblingDoc[field.name] = localeData
} else {
const beforeDuplicateArgs: FieldHookArgs = {
collection,
context,
data: doc,
field,
global: undefined,
path: fieldPath,
previousSiblingDoc: siblingDoc,
previousValue: siblingDoc[field.name],
req,
schemaPath: parentSchemaPath,
siblingData: siblingDoc,
siblingDocWithLocales: siblingDoc,
value: siblingDoc[field.name],
}
const hookResult = await runBeforeDuplicateHooks(beforeDuplicateArgs)
if (typeof hookResult !== 'undefined') {
siblingDoc[field.name] = hookResult
}
}
}
// First, for any localized fields, we will loop over locales
// and if locale data is present, traverse the sub fields.
// There are only a few different fields where this is possible.
if (fieldIsLocalized) {
if (typeof fieldData !== 'object' || fieldData === null) {
siblingDoc[field.name] = {}
fieldData = siblingDoc[field.name]
}
const promises = []
localization.localeCodes.forEach((locale) => {
if (fieldData[locale]) {
switch (field.type) {
case 'tab':
case 'group': {
promises.push(
traverseFields({
id,
collection,
context,
doc,
fields: field.fields,
overrideAccess,
path: fieldSchemaPath,
req,
schemaPath: fieldSchemaPath,
siblingDoc: fieldData[locale],
}),
)
break
}
case 'array': {
const rows = fieldData[locale]
if (Array.isArray(rows)) {
const promises = []
rows.forEach((row, i) => {
promises.push(
traverseFields({
id,
collection,
context,
doc,
fields: field.fields,
overrideAccess,
path: [...fieldPath, i],
req,
schemaPath: fieldSchemaPath,
siblingDoc: row,
}),
)
})
}
break
}
case 'blocks': {
const rows = fieldData[locale]
if (Array.isArray(rows)) {
const promises = []
rows.forEach((row, i) => {
const blockTypeToMatch = row.blockType
const block = field.blocks.find(
(blockType) => blockType.slug === blockTypeToMatch,
)
promises.push(
traverseFields({
id,
collection,
context,
doc,
fields: block.fields,
overrideAccess,
path: [...fieldPath, i],
req,
schemaPath: fieldSchemaPath,
siblingDoc: row,
}),
)
})
}
break
}
}
}
})
await Promise.all(promises)
} else {
// If the field is not localized, but it affects data,
// we need to further traverse its children
// so the child fields can run beforeDuplicate hooks
switch (field.type) {
case 'tab':
case 'group': {
if (field.type === 'tab' && !tabHasName(field)) {
await traverseFields({
id,
collection,
context,
doc,
fields: field.fields,
overrideAccess,
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
siblingDoc,
})
} else {
if (typeof siblingDoc[field.name] !== 'object') {
siblingDoc[field.name] = {}
}
const groupDoc = siblingDoc[field.name] as Record<string, unknown>
await traverseFields({
id,
collection,
context,
doc,
fields: field.fields,
overrideAccess,
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
siblingDoc: groupDoc as JsonObject,
})
}
break
}
case 'array': {
const rows = siblingDoc[field.name]
if (Array.isArray(rows)) {
const promises = []
rows.forEach((row, i) => {
promises.push(
traverseFields({
id,
collection,
context,
doc,
fields: field.fields,
overrideAccess,
path: [...fieldPath, i],
req,
schemaPath: fieldSchemaPath,
siblingDoc: row,
}),
)
})
await Promise.all(promises)
}
break
}
case 'blocks': {
const rows = siblingDoc[field.name]
if (Array.isArray(rows)) {
const promises = []
rows.forEach((row, i) => {
const blockTypeToMatch = row.blockType
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
if (block) {
;(row as JsonObject).blockType = blockTypeToMatch
promises.push(
traverseFields({
id,
collection,
context,
doc,
fields: block.fields,
overrideAccess,
path: [...fieldPath, i],
req,
schemaPath: fieldSchemaPath,
siblingDoc: row,
}),
)
}
})
await Promise.all(promises)
}
break
}
}
}
} else {
// Finally, we traverse fields which do not affect data here
switch (field.type) {
case 'row':
case 'collapsible': {
await traverseFields({
id,
collection,
context,
doc,
fields: field.fields,
overrideAccess,
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
siblingDoc,
})
break
}
case 'tabs': {
await traverseFields({
id,
collection,
context,
doc,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
overrideAccess,
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
siblingDoc,
})
break
}
default: {
break
}
}
}
}

View File

@@ -1,6 +1,6 @@
import type { FieldHookArgs } from '../../config/types.js'
export const beforeDuplicate = async (args: FieldHookArgs) =>
export const runBeforeDuplicateHooks = async (args: FieldHookArgs) =>
await args.field.hooks.beforeDuplicate.reduce(async (priorHook, currentHook) => {
await priorHook
return await currentHook(args)

View File

@@ -0,0 +1,50 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { Field, TabAsField } from '../../config/types.js'
import { promise } from './promise.js'
type Args<T> = {
collection: null | SanitizedCollectionConfig
context: RequestContext
doc: T
fields: (Field | TabAsField)[]
id?: number | string
overrideAccess: boolean
path: (number | string)[]
req: PayloadRequest
schemaPath: string[]
siblingDoc: JsonObject
}
export const traverseFields = async <T>({
id,
collection,
context,
doc,
fields,
overrideAccess,
path,
req,
schemaPath,
siblingDoc,
}: Args<T>): Promise<void> => {
const promises = []
fields.forEach((field) => {
promises.push(
promise({
id,
collection,
context,
doc,
field,
overrideAccess,
parentPath: path,
parentSchemaPath: schemaPath,
req,
siblingDoc,
}),
)
})
await Promise.all(promises)
}

View File

@@ -180,7 +180,6 @@ export type BeforeValidateNodeHookArgs<T extends SerializedLexicalNode> = {
}
export type BeforeChangeNodeHookArgs<T extends SerializedLexicalNode> = {
duplicate: boolean
/**
* Only available in `beforeChange` hooks.
*/

View File

@@ -414,7 +414,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
const {
collection,
context: _context,
duplicate,
errors,
field,
global,
@@ -494,7 +493,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
}
node = await hook({
context,
duplicate,
errors,
mergeLocaleActions,
node,
@@ -532,7 +530,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
data,
doc: originalData,
docWithLocales: originalDataWithLocales ?? {},
duplicate,
errors,
fields: subFields,
global,
@@ -635,7 +632,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
* - afterChange
* - beforeChange
* - beforeValidate
* - beforeDuplicate
*
* Other hooks are handled by the flattenedNodes. All nodes in the nodeIDMap are part of flattenedNodes.
*/

View File

@@ -131,6 +131,16 @@ export default buildConfigWithDefaults({
name: 'text',
type: 'text',
},
{
name: 'nestedArray',
type: 'array',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
slug: 'text',
},
@@ -148,6 +158,41 @@ export default buildConfigWithDefaults({
required: true,
type: 'blocks',
},
{
type: 'tabs',
tabs: [
{
name: 'myTab',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'group',
type: 'group',
localized: true,
fields: [
{
name: 'nestedArray2',
type: 'array',
fields: [
{
name: 'nestedText',
type: 'text',
},
],
},
{
name: 'nestedText',
type: 'text',
},
],
},
],
},
],
},
],
slug: withRequiredLocalizedFields,
},

View File

@@ -1120,6 +1120,13 @@ describe('Localization', () => {
})
it('should duplicate with localized blocks', async () => {
// This test covers a few things:
// 1. make sure we can duplicate localized blocks
// - in relational DBs, we need to create new block / array IDs
// - and this needs to be done recursively for all block / array fields
// 2. make sure localized arrays / blocks work inside of localized groups / tabs
// - this is covered with myTab.group.nestedArray2
const englishText = 'english'
const spanishText = 'spanish'
const doc = await payload.create({
@@ -1129,8 +1136,30 @@ describe('Localization', () => {
{
blockType: 'text',
text: englishText,
nestedArray: [
{
text: 'hello',
},
{
text: 'goodbye',
},
],
},
],
myTab: {
text: 'hello',
group: {
nestedText: 'hello',
nestedArray2: [
{
nestedText: 'hello',
},
{
nestedText: 'goodbye',
},
],
},
},
title: 'hello',
},
locale: defaultLocale,
@@ -1144,9 +1173,31 @@ describe('Localization', () => {
{
blockType: 'text',
text: spanishText,
nestedArray: [
{
text: 'hola',
},
{
text: 'adios',
},
],
},
],
title: 'hello',
myTab: {
text: 'hola',
group: {
nestedText: 'hola',
nestedArray2: [
{
nestedText: 'hola',
},
{
nestedText: 'adios',
},
],
},
},
},
locale: spanishLocale,
})
@@ -1168,6 +1219,14 @@ describe('Localization', () => {
expect(allLocales.layout.en[0].text).toStrictEqual(englishText)
expect(allLocales.layout.es[0].text).toStrictEqual(spanishText)
expect(allLocales.myTab.group.en.nestedText).toStrictEqual('hello')
expect(allLocales.myTab.group.en.nestedArray2[0].nestedText).toStrictEqual('hello')
expect(allLocales.myTab.group.en.nestedArray2[1].nestedText).toStrictEqual('goodbye')
expect(allLocales.myTab.group.es.nestedText).toStrictEqual('hola')
expect(allLocales.myTab.group.es.nestedArray2[0].nestedText).toStrictEqual('hola')
expect(allLocales.myTab.group.es.nestedArray2[1].nestedText).toStrictEqual('adios')
})
})