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:
@@ -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
|
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.
|
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.
|
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:
|
Here is an example of a number field with a hook that increments the number to avoid unique constraint errors when duplicating a document:
|
||||||
|
|||||||
@@ -166,7 +166,8 @@ export const traverseFields = ({
|
|||||||
if (field.hasMany) {
|
if (field.hasMany) {
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||||
withinLocalizedArrayOrBlock
|
withinLocalizedArrayOrBlock ||
|
||||||
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
hasLocalizedManyTextField = true
|
hasLocalizedManyTextField = true
|
||||||
@@ -199,7 +200,8 @@ export const traverseFields = ({
|
|||||||
if (field.hasMany) {
|
if (field.hasMany) {
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||||
withinLocalizedArrayOrBlock
|
withinLocalizedArrayOrBlock ||
|
||||||
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
hasLocalizedManyNumberField = true
|
hasLocalizedManyNumberField = true
|
||||||
@@ -279,7 +281,8 @@ export const traverseFields = ({
|
|||||||
|
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||||
withinLocalizedArrayOrBlock
|
withinLocalizedArrayOrBlock ||
|
||||||
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
baseColumns.locale = text('locale', { enum: locales }).notNull()
|
baseColumns.locale = text('locale', { enum: locales }).notNull()
|
||||||
@@ -365,7 +368,8 @@ export const traverseFields = ({
|
|||||||
|
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||||
withinLocalizedArrayOrBlock
|
withinLocalizedArrayOrBlock ||
|
||||||
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
baseColumns._locale = text('_locale', { enum: locales }).notNull()
|
baseColumns._locale = text('_locale', { enum: locales }).notNull()
|
||||||
@@ -503,7 +507,8 @@ export const traverseFields = ({
|
|||||||
|
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||||
withinLocalizedArrayOrBlock
|
withinLocalizedArrayOrBlock ||
|
||||||
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
baseColumns._locale = text('_locale', { enum: locales }).notNull()
|
baseColumns._locale = text('_locale', { enum: locales }).notNull()
|
||||||
|
|||||||
@@ -172,7 +172,8 @@ export const traverseFields = ({
|
|||||||
if (field.hasMany) {
|
if (field.hasMany) {
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||||
withinLocalizedArrayOrBlock
|
withinLocalizedArrayOrBlock ||
|
||||||
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
hasLocalizedManyTextField = true
|
hasLocalizedManyTextField = true
|
||||||
@@ -205,7 +206,8 @@ export const traverseFields = ({
|
|||||||
if (field.hasMany) {
|
if (field.hasMany) {
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||||
withinLocalizedArrayOrBlock
|
withinLocalizedArrayOrBlock ||
|
||||||
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
hasLocalizedManyNumberField = true
|
hasLocalizedManyNumberField = true
|
||||||
@@ -300,7 +302,8 @@ export const traverseFields = ({
|
|||||||
|
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||||
withinLocalizedArrayOrBlock
|
withinLocalizedArrayOrBlock ||
|
||||||
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
|
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
|
||||||
@@ -382,7 +385,8 @@ export const traverseFields = ({
|
|||||||
|
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||||
withinLocalizedArrayOrBlock
|
withinLocalizedArrayOrBlock ||
|
||||||
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||||
@@ -516,7 +520,8 @@ export const traverseFields = ({
|
|||||||
|
|
||||||
const isLocalized =
|
const isLocalized =
|
||||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||||
withinLocalizedArrayOrBlock
|
withinLocalizedArrayOrBlock ||
|
||||||
|
forceLocalized
|
||||||
|
|
||||||
if (isLocalized) {
|
if (isLocalized) {
|
||||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||||
|
|||||||
@@ -489,6 +489,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
valuesToTransform.push({
|
valuesToTransform.push({
|
||||||
ref: localizedFieldData,
|
ref: localizedFieldData,
|
||||||
table: {
|
table: {
|
||||||
|
...table,
|
||||||
...localeRow,
|
...localeRow,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -526,7 +527,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
relationships,
|
relationships,
|
||||||
table,
|
table,
|
||||||
texts,
|
texts,
|
||||||
withinArrayOrBlockLocale,
|
withinArrayOrBlockLocale: locale || withinArrayOrBlockLocale,
|
||||||
})
|
})
|
||||||
|
|
||||||
if ('_order' in ref) {
|
if ('_order' in ref) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { APIError, Forbidden, NotFound } from '../../errors/index.js'
|
|||||||
import { afterChange } from '../../fields/hooks/afterChange/index.js'
|
import { afterChange } from '../../fields/hooks/afterChange/index.js'
|
||||||
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
||||||
import { beforeChange } from '../../fields/hooks/beforeChange/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 { beforeValidate } from '../../fields/hooks/beforeValidate/index.js'
|
||||||
import { commitTransaction } from '../../utilities/commitTransaction.js'
|
import { commitTransaction } from '../../utilities/commitTransaction.js'
|
||||||
import { initTransaction } from '../../utilities/initTransaction.js'
|
import { initTransaction } from '../../utilities/initTransaction.js'
|
||||||
@@ -93,7 +94,7 @@ export const duplicateOperation = async <TSlug extends CollectionSlug>(
|
|||||||
where: combineQueries({ id: { equals: id } }, accessResults),
|
where: combineQueries({ id: { equals: id } }, accessResults),
|
||||||
}
|
}
|
||||||
|
|
||||||
const docWithLocales = await getLatestCollectionVersion({
|
let docWithLocales = await getLatestCollectionVersion({
|
||||||
id,
|
id,
|
||||||
config: collectionConfig,
|
config: collectionConfig,
|
||||||
payload,
|
payload,
|
||||||
@@ -112,6 +113,15 @@ export const duplicateOperation = async <TSlug extends CollectionSlug>(
|
|||||||
delete docWithLocales.createdAt
|
delete docWithLocales.createdAt
|
||||||
delete docWithLocales.id
|
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
|
// for version enabled collections, override the current status with draft, unless draft is explicitly set to false
|
||||||
if (shouldSaveDraft) {
|
if (shouldSaveDraft) {
|
||||||
docWithLocales._status = 'draft'
|
docWithLocales._status = 'draft'
|
||||||
@@ -205,7 +215,6 @@ export const duplicateOperation = async <TSlug extends CollectionSlug>(
|
|||||||
data,
|
data,
|
||||||
doc: originalDoc,
|
doc: originalDoc,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate: true,
|
|
||||||
global: null,
|
global: null,
|
||||||
operation,
|
operation,
|
||||||
req,
|
req,
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,6 +25,11 @@ export const baseIDField: TextField = {
|
|||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
beforeDuplicate: [
|
||||||
|
() => {
|
||||||
|
return new ObjectId().toHexString()
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
label: 'ID',
|
label: 'ID',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from '../../errors/index.js'
|
} from '../../errors/index.js'
|
||||||
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
|
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
|
||||||
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
|
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
|
||||||
import { baseBeforeDuplicateArrays } from '../baseFields/baseBeforeDuplicateArrays.js'
|
|
||||||
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
|
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
|
||||||
import { baseIDField } from '../baseFields/baseIDField.js'
|
import { baseIDField } from '../baseFields/baseIDField.js'
|
||||||
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
|
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
|
||||||
@@ -131,15 +130,6 @@ export const sanitizeFields = async ({
|
|||||||
|
|
||||||
if (field.type === 'array' && field.fields) {
|
if (field.type === 'array' && field.fields) {
|
||||||
field.fields.push(baseIDField)
|
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) {
|
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.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) {
|
for (const block of field.blocks) {
|
||||||
if (block._sanitized === true) {
|
if (block._sanitized === true) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ type Args<T extends JsonObject> = {
|
|||||||
data: T
|
data: T
|
||||||
doc: T
|
doc: T
|
||||||
docWithLocales: JsonObject
|
docWithLocales: JsonObject
|
||||||
duplicate?: boolean
|
|
||||||
global: null | SanitizedGlobalConfig
|
global: null | SanitizedGlobalConfig
|
||||||
id?: number | string
|
id?: number | string
|
||||||
operation: Operation
|
operation: Operation
|
||||||
@@ -26,7 +25,6 @@ type Args<T extends JsonObject> = {
|
|||||||
* - Execute field hooks
|
* - Execute field hooks
|
||||||
* - Validate data
|
* - Validate data
|
||||||
* - Transform data for storage
|
* - 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.
|
* - 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>({
|
export const beforeChange = async <T extends JsonObject>({
|
||||||
@@ -36,7 +34,6 @@ export const beforeChange = async <T extends JsonObject>({
|
|||||||
data: incomingData,
|
data: incomingData,
|
||||||
doc,
|
doc,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate = false,
|
|
||||||
global,
|
global,
|
||||||
operation,
|
operation,
|
||||||
req,
|
req,
|
||||||
@@ -53,7 +50,6 @@ export const beforeChange = async <T extends JsonObject>({
|
|||||||
data,
|
data,
|
||||||
doc,
|
doc,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
fields: collection?.fields || global?.fields,
|
fields: collection?.fields || global?.fields,
|
||||||
global,
|
global,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { MissingEditorProp } from '../../../errors/index.js'
|
|||||||
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
|
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
|
||||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||||
import { beforeDuplicate } from './beforeDuplicate.js'
|
|
||||||
import { getExistingRowDoc } from './getExistingRowDoc.js'
|
import { getExistingRowDoc } from './getExistingRowDoc.js'
|
||||||
import { traverseFields } from './traverseFields.js'
|
import { traverseFields } from './traverseFields.js'
|
||||||
|
|
||||||
@@ -18,7 +17,6 @@ type Args = {
|
|||||||
data: JsonObject
|
data: JsonObject
|
||||||
doc: JsonObject
|
doc: JsonObject
|
||||||
docWithLocales: JsonObject
|
docWithLocales: JsonObject
|
||||||
duplicate: boolean
|
|
||||||
errors: { field: string; message: string }[]
|
errors: { field: string; message: string }[]
|
||||||
field: Field | TabAsField
|
field: Field | TabAsField
|
||||||
global: null | SanitizedGlobalConfig
|
global: null | SanitizedGlobalConfig
|
||||||
@@ -55,7 +53,6 @@ export const promise = async ({
|
|||||||
data,
|
data,
|
||||||
doc,
|
doc,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
field,
|
field,
|
||||||
global,
|
global,
|
||||||
@@ -176,16 +173,11 @@ export const promise = async ({
|
|||||||
const localeData = await localization.localeCodes.reduce(
|
const localeData = await localization.localeCodes.reduce(
|
||||||
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
|
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
|
||||||
const localizedValues = await localizedValuesPromise
|
const localizedValues = await localizedValuesPromise
|
||||||
let fieldValue =
|
const fieldValue =
|
||||||
locale === req.locale
|
locale === req.locale
|
||||||
? siblingData[field.name]
|
? siblingData[field.name]
|
||||||
: siblingDocWithLocales?.[field.name]?.[locale]
|
: siblingDocWithLocales?.[field.name]?.[locale]
|
||||||
|
|
||||||
if (duplicate && field.hooks?.beforeDuplicate?.length) {
|
|
||||||
beforeDuplicateArgs.value = fieldValue
|
|
||||||
fieldValue = await beforeDuplicate(beforeDuplicateArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// const result = await localizedValues
|
// const result = await localizedValues
|
||||||
// update locale value if it's not undefined
|
// update locale value if it's not undefined
|
||||||
if (typeof fieldValue !== 'undefined') {
|
if (typeof fieldValue !== 'undefined') {
|
||||||
@@ -205,10 +197,6 @@ export const promise = async ({
|
|||||||
siblingData[field.name] = localeData
|
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,
|
data,
|
||||||
doc,
|
doc,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
global,
|
global,
|
||||||
@@ -282,7 +269,6 @@ export const promise = async ({
|
|||||||
data,
|
data,
|
||||||
doc,
|
doc,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
global,
|
global,
|
||||||
@@ -332,7 +318,6 @@ export const promise = async ({
|
|||||||
data,
|
data,
|
||||||
doc,
|
doc,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
fields: block.fields,
|
fields: block.fields,
|
||||||
global,
|
global,
|
||||||
@@ -365,7 +350,6 @@ export const promise = async ({
|
|||||||
data,
|
data,
|
||||||
doc,
|
doc,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
global,
|
global,
|
||||||
@@ -411,7 +395,6 @@ export const promise = async ({
|
|||||||
data,
|
data,
|
||||||
doc,
|
doc,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
global,
|
global,
|
||||||
@@ -437,7 +420,6 @@ export const promise = async ({
|
|||||||
data,
|
data,
|
||||||
doc,
|
doc,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||||
global,
|
global,
|
||||||
@@ -474,7 +456,6 @@ export const promise = async ({
|
|||||||
context,
|
context,
|
||||||
data,
|
data,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
field,
|
field,
|
||||||
global,
|
global,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ type Args = {
|
|||||||
* The original data with locales (not modified by any hooks)
|
* The original data with locales (not modified by any hooks)
|
||||||
*/
|
*/
|
||||||
docWithLocales: JsonObject
|
docWithLocales: JsonObject
|
||||||
duplicate: boolean
|
|
||||||
errors: { field: string; message: string }[]
|
errors: { field: string; message: string }[]
|
||||||
fields: (Field | TabAsField)[]
|
fields: (Field | TabAsField)[]
|
||||||
global: null | SanitizedGlobalConfig
|
global: null | SanitizedGlobalConfig
|
||||||
@@ -54,7 +53,6 @@ export const traverseFields = async ({
|
|||||||
data,
|
data,
|
||||||
doc,
|
doc,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
fields,
|
fields,
|
||||||
global,
|
global,
|
||||||
@@ -79,7 +77,6 @@ export const traverseFields = async ({
|
|||||||
data,
|
data,
|
||||||
doc,
|
doc,
|
||||||
docWithLocales,
|
docWithLocales,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
field,
|
field,
|
||||||
global,
|
global,
|
||||||
|
|||||||
46
packages/payload/src/fields/hooks/beforeDuplicate/index.ts
Normal file
46
packages/payload/src/fields/hooks/beforeDuplicate/index.ts
Normal 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
|
||||||
|
}
|
||||||
351
packages/payload/src/fields/hooks/beforeDuplicate/promise.ts
Normal file
351
packages/payload/src/fields/hooks/beforeDuplicate/promise.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { FieldHookArgs } from '../../config/types.js'
|
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 args.field.hooks.beforeDuplicate.reduce(async (priorHook, currentHook) => {
|
||||||
await priorHook
|
await priorHook
|
||||||
return await currentHook(args)
|
return await currentHook(args)
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -180,7 +180,6 @@ export type BeforeValidateNodeHookArgs<T extends SerializedLexicalNode> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type BeforeChangeNodeHookArgs<T extends SerializedLexicalNode> = {
|
export type BeforeChangeNodeHookArgs<T extends SerializedLexicalNode> = {
|
||||||
duplicate: boolean
|
|
||||||
/**
|
/**
|
||||||
* Only available in `beforeChange` hooks.
|
* Only available in `beforeChange` hooks.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -414,7 +414,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
const {
|
const {
|
||||||
collection,
|
collection,
|
||||||
context: _context,
|
context: _context,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
field,
|
field,
|
||||||
global,
|
global,
|
||||||
@@ -494,7 +493,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
}
|
}
|
||||||
node = await hook({
|
node = await hook({
|
||||||
context,
|
context,
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
mergeLocaleActions,
|
mergeLocaleActions,
|
||||||
node,
|
node,
|
||||||
@@ -532,7 +530,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
data,
|
data,
|
||||||
doc: originalData,
|
doc: originalData,
|
||||||
docWithLocales: originalDataWithLocales ?? {},
|
docWithLocales: originalDataWithLocales ?? {},
|
||||||
duplicate,
|
|
||||||
errors,
|
errors,
|
||||||
fields: subFields,
|
fields: subFields,
|
||||||
global,
|
global,
|
||||||
@@ -635,7 +632,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
* - afterChange
|
* - afterChange
|
||||||
* - beforeChange
|
* - beforeChange
|
||||||
* - beforeValidate
|
* - beforeValidate
|
||||||
* - beforeDuplicate
|
|
||||||
*
|
*
|
||||||
* Other hooks are handled by the flattenedNodes. All nodes in the nodeIDMap are part of flattenedNodes.
|
* Other hooks are handled by the flattenedNodes. All nodes in the nodeIDMap are part of flattenedNodes.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -131,6 +131,16 @@ export default buildConfigWithDefaults({
|
|||||||
name: 'text',
|
name: 'text',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'nestedArray',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
slug: 'text',
|
slug: 'text',
|
||||||
},
|
},
|
||||||
@@ -148,6 +158,41 @@ export default buildConfigWithDefaults({
|
|||||||
required: true,
|
required: true,
|
||||||
type: 'blocks',
|
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,
|
slug: withRequiredLocalizedFields,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1120,6 +1120,13 @@ describe('Localization', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should duplicate with localized blocks', async () => {
|
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 englishText = 'english'
|
||||||
const spanishText = 'spanish'
|
const spanishText = 'spanish'
|
||||||
const doc = await payload.create({
|
const doc = await payload.create({
|
||||||
@@ -1129,8 +1136,30 @@ describe('Localization', () => {
|
|||||||
{
|
{
|
||||||
blockType: 'text',
|
blockType: 'text',
|
||||||
text: englishText,
|
text: englishText,
|
||||||
|
nestedArray: [
|
||||||
|
{
|
||||||
|
text: 'hello',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'goodbye',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
myTab: {
|
||||||
|
text: 'hello',
|
||||||
|
group: {
|
||||||
|
nestedText: 'hello',
|
||||||
|
nestedArray2: [
|
||||||
|
{
|
||||||
|
nestedText: 'hello',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nestedText: 'goodbye',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
title: 'hello',
|
title: 'hello',
|
||||||
},
|
},
|
||||||
locale: defaultLocale,
|
locale: defaultLocale,
|
||||||
@@ -1144,9 +1173,31 @@ describe('Localization', () => {
|
|||||||
{
|
{
|
||||||
blockType: 'text',
|
blockType: 'text',
|
||||||
text: spanishText,
|
text: spanishText,
|
||||||
|
nestedArray: [
|
||||||
|
{
|
||||||
|
text: 'hola',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'adios',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
title: 'hello',
|
title: 'hello',
|
||||||
|
myTab: {
|
||||||
|
text: 'hola',
|
||||||
|
group: {
|
||||||
|
nestedText: 'hola',
|
||||||
|
nestedArray2: [
|
||||||
|
{
|
||||||
|
nestedText: 'hola',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nestedText: 'adios',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
locale: spanishLocale,
|
locale: spanishLocale,
|
||||||
})
|
})
|
||||||
@@ -1168,6 +1219,14 @@ describe('Localization', () => {
|
|||||||
|
|
||||||
expect(allLocales.layout.en[0].text).toStrictEqual(englishText)
|
expect(allLocales.layout.en[0].text).toStrictEqual(englishText)
|
||||||
expect(allLocales.layout.es[0].text).toStrictEqual(spanishText)
|
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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user