feat!: auto-removes localized property from localized fields within other localized fields (#7933)

## Description

Payload localization works on a field-by-field basis. As you can nest
fields within other fields, you could potentially nest a localized field
within a localized field—but this would be redundant and unnecessary.
There would be no reason to define a localized field within a localized
parent field, given that the entire data structure from the parent field
onward would be localized.

Up until this point, Payload would _allow_ you to nest a localized field
within another localized field, and this might have worked in MongoDB
but it will throw errors in Postgres.

Now, Payload will automatically remove the `localized: true` property
from sub-fields within `sanitizeFields` if a parent field is localized.

This could potentially be a breaking change if you have a configuration
with MongoDB that nests localized fields within localized fields.

## Migrating

You probably only need to migrate if you are using MongoDB, as there,
you may not have noticed any problems. But in Postgres or SQLite, this
would have caused issues so it's unlikely that you've made it too far
without experiencing issues due to a nested localized fields config.

In the event you would like to keep existing data in this fashion, we
have added a `compatibility.allowLocalizedWithinLocalized` flag to the
Payload config, which you can set to `true`, and Payload will then
disable this new sanitization step.

Set this compatibility flag to `true` only if you have an existing
Payload MongoDB database from pre-3.0, and you have nested localized
fields that you would like to maintain without migrating.
This commit is contained in:
James Mikrut
2024-08-28 21:56:17 -04:00
committed by GitHub
parent 828f5d866d
commit 538b7ee616
21 changed files with 150 additions and 8 deletions

View File

@@ -71,6 +71,7 @@ The following options are available:
| **`db`** \* | The Database Adapter which will be used by Payload. [More details](../database/overview). | | **`db`** \* | The Database Adapter which will be used by Payload. [More details](../database/overview). |
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. | | **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). | | **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
| **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). |
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). | | **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). | | **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). |
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). | | **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
@@ -253,3 +254,13 @@ import type { Config, SanitizedConfig } from 'payload'
The Payload Config only lives on the server and is not allowed to contain any client-side code. That way, you can load up the Payload Config in any server environment or standalone script, without having to use Bundlers or Node.js loaders to handle importing client-only modules (e.g. scss files or React Components) without any errors. The Payload Config only lives on the server and is not allowed to contain any client-side code. That way, you can load up the Payload Config in any server environment or standalone script, without having to use Bundlers or Node.js loaders to handle importing client-only modules (e.g. scss files or React Components) without any errors.
Behind the curtains, the Next.js-based Admin Panel generates a ClientConfig, which strips away any server-only code and enriches the config with React Components. Behind the curtains, the Next.js-based Admin Panel generates a ClientConfig, which strips away any server-only code and enriches the config with React Components.
## Compatibility flags
The Payload Config can accept compatibility flags for running the newest versions but with older databases. You should only use these flags if you need to, and should confirm that you need to prior to enabling these flags.
`allowLocalizedWithinLocalized`
Payload localization works on a field-by-field basis. As you can nest fields within other fields, you could potentially nest a localized field within a localized field—but this would be redundant and unnecessary. There would be no reason to define a localized field within a localized parent field, given that the entire data structure from the parent field onward would be localized.
By default, Payload will remove the `localized: true` property from sub-fields if a parent field is localized. Set this compatibility flag to `true` only if you have an existing Payload MongoDB database from pre-3.0, and you have nested localized fields that you would like to maintain without migrating.

View File

@@ -271,6 +271,7 @@ export type RichTextAdapterProvider<
> = ({ > = ({
config, config,
isRoot, isRoot,
parentIsLocalized,
}: { }: {
config: SanitizedConfig config: SanitizedConfig
/** /**
@@ -279,6 +280,7 @@ export type RichTextAdapterProvider<
* @default false * @default false
*/ */
isRoot?: boolean isRoot?: boolean
parentIsLocalized: boolean
}) => }) =>
| Promise<RichTextAdapter<Value, AdapterProps, ExtraFieldProperties>> | Promise<RichTextAdapter<Value, AdapterProps, ExtraFieldProperties>>
| RichTextAdapter<Value, AdapterProps, ExtraFieldProperties> | RichTextAdapter<Value, AdapterProps, ExtraFieldProperties>

View File

@@ -39,6 +39,7 @@ export const sanitizeCollection = async (
collectionConfig: sanitized, collectionConfig: sanitized,
config, config,
fields: sanitized.fields, fields: sanitized.fields,
parentIsLocalized: false,
richTextSanitizationPromises, richTextSanitizationPromises,
validRelationships, validRelationships,
}) })

View File

@@ -188,6 +188,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
config.editor = await incomingConfig.editor({ config.editor = await incomingConfig.editor({
config: config as SanitizedConfig, config: config as SanitizedConfig,
isRoot: true, isRoot: true,
parentIsLocalized: false,
}) })
if (config.editor.i18n && Object.keys(config.editor.i18n).length >= 0) { if (config.editor.i18n && Object.keys(config.editor.i18n).length >= 0) {
config.i18n.translations = deepMergeSimple(config.i18n.translations, config.editor.i18n) config.i18n.translations = deepMergeSimple(config.i18n.translations, config.editor.i18n)

View File

@@ -811,6 +811,19 @@ export type Config = {
* @see https://payloadcms.com/docs/configuration/collections#collection-configs * @see https://payloadcms.com/docs/configuration/collections#collection-configs
*/ */
collections?: CollectionConfig[] collections?: CollectionConfig[]
/**
* Compatibility flags for prior Payload versions
*/
compatibility?: {
/**
* By default, Payload will remove the `localized: true` property
* from fields if a parent field is localized. Set this property
* to `true` only if you have an existing Payload database from pre-3.0
* that you would like to maintain without migrating. This is only
* relevant for MongoDB databases.
*/
allowLocalizedWithinLocalized: true
}
/** /**
* Prefix a string to all cookies that Payload sets. * Prefix a string to all cookies that Payload sets.
* *

View File

@@ -23,18 +23,19 @@ type Args = {
config: Config config: Config
existingFieldNames?: Set<string> existingFieldNames?: Set<string>
fields: Field[] fields: Field[]
parentIsLocalized: boolean
/** /**
* If true, a richText field will require an editor property to be set, as the sanitizeFields function will not add it from the payload config if not present. * If true, a richText field will require an editor property to be set, as the sanitizeFields function will not add it from the payload config if not present.
* *
* @default false * @default false
*/ */
requireFieldLevelRichTextEditor?: boolean requireFieldLevelRichTextEditor?: boolean
/** /**
* If this property is set, RichText fields won't be sanitized immediately. Instead, they will be added to this array as promises * If this property is set, RichText fields won't be sanitized immediately. Instead, they will be added to this array as promises
* so that you can sanitize them together, after the config has been sanitized. * so that you can sanitize them together, after the config has been sanitized.
*/ */
richTextSanitizationPromises?: Array<(config: SanitizedConfig) => Promise<void>> richTextSanitizationPromises?: Array<(config: SanitizedConfig) => Promise<void>>
/** /**
* If not null, will validate that upload and relationship fields do not relate to a collection that is not in this array. * If not null, will validate that upload and relationship fields do not relate to a collection that is not in this array.
* This validation will be skipped if validRelationships is null. * This validation will be skipped if validRelationships is null.
@@ -47,6 +48,7 @@ export const sanitizeFields = async ({
config, config,
existingFieldNames = new Set(), existingFieldNames = new Set(),
fields, fields,
parentIsLocalized,
requireFieldLevelRichTextEditor = false, requireFieldLevelRichTextEditor = false,
richTextSanitizationPromises, richTextSanitizationPromises,
validRelationships, validRelationships,
@@ -137,7 +139,17 @@ export const sanitizeFields = async ({
existingFieldNames.add(field.name) existingFieldNames.add(field.name)
} }
if (field.localized && !config.localization) delete field.localized if (typeof field.localized !== 'undefined') {
let shouldDisableLocalized = !config.localization
if (!config.compatibility?.allowLocalizedWithinLocalized && parentIsLocalized) {
shouldDisableLocalized = true
}
if (shouldDisableLocalized) {
delete field.localized
}
}
if (typeof field.validate === 'undefined') { if (typeof field.validate === 'undefined') {
const defaultValidate = validations[field.type] const defaultValidate = validations[field.type]
@@ -174,6 +186,7 @@ export const sanitizeFields = async ({
field.editor = await field.editor({ field.editor = await field.editor({
config: _config, config: _config,
isRoot: requireFieldLevelRichTextEditor, isRoot: requireFieldLevelRichTextEditor,
parentIsLocalized: parentIsLocalized || field.localized,
}) })
} }
@@ -201,6 +214,7 @@ export const sanitizeFields = async ({
config, config,
existingFieldNames: new Set(), existingFieldNames: new Set(),
fields: block.fields, fields: block.fields,
parentIsLocalized: parentIsLocalized || field.localized,
requireFieldLevelRichTextEditor, requireFieldLevelRichTextEditor,
richTextSanitizationPromises, richTextSanitizationPromises,
validRelationships, validRelationships,
@@ -213,6 +227,7 @@ export const sanitizeFields = async ({
config, config,
existingFieldNames: fieldAffectsData(field) ? new Set() : existingFieldNames, existingFieldNames: fieldAffectsData(field) ? new Set() : existingFieldNames,
fields: field.fields, fields: field.fields,
parentIsLocalized: parentIsLocalized || field.localized,
requireFieldLevelRichTextEditor, requireFieldLevelRichTextEditor,
richTextSanitizationPromises, richTextSanitizationPromises,
validRelationships, validRelationships,
@@ -230,6 +245,7 @@ export const sanitizeFields = async ({
config, config,
existingFieldNames: tabHasName(tab) ? new Set() : existingFieldNames, existingFieldNames: tabHasName(tab) ? new Set() : existingFieldNames,
fields: tab.fields, fields: tab.fields,
parentIsLocalized: parentIsLocalized || (tabHasName(tab) && tab.localized),
requireFieldLevelRichTextEditor, requireFieldLevelRichTextEditor,
richTextSanitizationPromises, richTextSanitizationPromises,
validRelationships, validRelationships,

View File

@@ -46,6 +46,7 @@ export const sanitizeGlobals = async (
global.fields = await sanitizeFields({ global.fields = await sanitizeFields({
config, config,
fields: global.fields, fields: global.fields,
parentIsLocalized: false,
richTextSanitizationPromises, richTextSanitizationPromises,
validRelationships, validRelationships,
}) })

View File

@@ -22,7 +22,7 @@ export const BlocksFeature = createServerFeature<
BlocksFeatureProps, BlocksFeatureProps,
BlocksFeatureClientProps BlocksFeatureClientProps
>({ >({
feature: async ({ config: _config, isRoot, props }) => { feature: async ({ config: _config, isRoot, parentIsLocalized, props }) => {
// Build clientProps // Build clientProps
const clientProps: BlocksFeatureClientProps = { const clientProps: BlocksFeatureClientProps = {
clientBlockSlugs: [], clientBlockSlugs: [],
@@ -44,6 +44,7 @@ export const BlocksFeature = createServerFeature<
blocks: props.inlineBlocks ?? [], blocks: props.inlineBlocks ?? [],
}, },
], ],
parentIsLocalized,
requireFieldLevelRichTextEditor: isRoot, requireFieldLevelRichTextEditor: isRoot,
validRelationships, validRelationships,
}) })

View File

@@ -49,12 +49,13 @@ export type SerializedTableRowNode = Spread<
_SerializedTableRowNode _SerializedTableRowNode
> >
export const EXPERIMENTAL_TableFeature = createServerFeature({ export const EXPERIMENTAL_TableFeature = createServerFeature({
feature: async ({ config, isRoot }) => { feature: async ({ config, isRoot, parentIsLocalized }) => {
const validRelationships = config.collections.map((c) => c.slug) || [] const validRelationships = config.collections.map((c) => c.slug) || []
const sanitizedFields = await sanitizeFields({ const sanitizedFields = await sanitizeFields({
config: config as unknown as Config, config: config as unknown as Config,
fields, fields,
parentIsLocalized,
requireFieldLevelRichTextEditor: isRoot, requireFieldLevelRichTextEditor: isRoot,
validRelationships, validRelationships,
}) })

View File

@@ -64,7 +64,7 @@ export const LinkFeature = createServerFeature<
LinkFeatureServerProps, LinkFeatureServerProps,
ClientProps ClientProps
>({ >({
feature: async ({ config: _config, isRoot, props }) => { feature: async ({ config: _config, isRoot, parentIsLocalized, props }) => {
if (!props) { if (!props) {
props = {} props = {}
} }
@@ -81,6 +81,7 @@ export const LinkFeature = createServerFeature<
const sanitizedFields = await sanitizeFields({ const sanitizedFields = await sanitizeFields({
config: _config as unknown as Config, config: _config as unknown as Config,
fields: _transformedFields, fields: _transformedFields,
parentIsLocalized,
requireFieldLevelRichTextEditor: isRoot, requireFieldLevelRichTextEditor: isRoot,
validRelationships, validRelationships,
}) })

View File

@@ -110,6 +110,7 @@ export type FeatureProviderServer<
/** unSanitizedEditorConfig.features, but mapped */ /** unSanitizedEditorConfig.features, but mapped */
featureProviderMap: ServerFeatureProviderMap featureProviderMap: ServerFeatureProviderMap
isRoot?: boolean isRoot?: boolean
parentIsLocalized: boolean
// other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here // other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here
resolvedFeatures: ResolvedServerFeatureMap resolvedFeatures: ResolvedServerFeatureMap
// unSanitized EditorConfig, // unSanitized EditorConfig,

View File

@@ -47,7 +47,7 @@ export const UploadFeature = createServerFeature<
UploadFeatureProps, UploadFeatureProps,
UploadFeaturePropsClient UploadFeaturePropsClient
>({ >({
feature: async ({ config: _config, isRoot, props }) => { feature: async ({ config: _config, isRoot, parentIsLocalized, props }) => {
if (!props) { if (!props) {
props = { collections: {} } props = { collections: {} }
} }
@@ -70,6 +70,7 @@ export const UploadFeature = createServerFeature<
props.collections[collection].fields = await sanitizeFields({ props.collections[collection].fields = await sanitizeFields({
config: _config as unknown as Config, config: _config as unknown as Config,
fields: props.collections[collection].fields, fields: props.collections[collection].fields,
parentIsLocalized,
requireFieldLevelRichTextEditor: isRoot, requireFieldLevelRichTextEditor: isRoot,
validRelationships, validRelationships,
}) })

View File

@@ -41,7 +41,7 @@ import { richTextValidateHOC } from './validate/index.js'
let defaultSanitizedServerEditorConfig: SanitizedServerEditorConfig = null let defaultSanitizedServerEditorConfig: SanitizedServerEditorConfig = null
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapterProvider { export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapterProvider {
return async ({ config, isRoot }) => { return async ({ config, isRoot, parentIsLocalized }) => {
if ( if (
process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'production' &&
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true' process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true'
@@ -77,6 +77,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
defaultSanitizedServerEditorConfig = await sanitizeServerEditorConfig( defaultSanitizedServerEditorConfig = await sanitizeServerEditorConfig(
defaultEditorConfig, defaultEditorConfig,
config, config,
parentIsLocalized,
) )
features = deepCopyObject(defaultEditorFeatures) features = deepCopyObject(defaultEditorFeatures)
} }
@@ -108,6 +109,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
resolvedFeatureMap = await loadFeatures({ resolvedFeatureMap = await loadFeatures({
config, config,
isRoot, isRoot,
parentIsLocalized,
unSanitizedEditorConfig: { unSanitizedEditorConfig: {
features, features,
lexical, lexical,

View File

@@ -108,10 +108,12 @@ export function sortFeaturesForOptimalLoading(
export async function loadFeatures({ export async function loadFeatures({
config, config,
isRoot, isRoot,
parentIsLocalized,
unSanitizedEditorConfig, unSanitizedEditorConfig,
}: { }: {
config: SanitizedConfig config: SanitizedConfig
isRoot?: boolean isRoot?: boolean
parentIsLocalized: boolean
unSanitizedEditorConfig: ServerEditorConfig unSanitizedEditorConfig: ServerEditorConfig
}): Promise<ResolvedServerFeatureMap> { }): Promise<ResolvedServerFeatureMap> {
// First remove all duplicate features. The LAST feature with a given key wins. // First remove all duplicate features. The LAST feature with a given key wins.
@@ -179,6 +181,7 @@ export async function loadFeatures({
config, config,
featureProviderMap, featureProviderMap,
isRoot, isRoot,
parentIsLocalized,
resolvedFeatures, resolvedFeatures,
unSanitizedEditorConfig, unSanitizedEditorConfig,
}) })

View File

@@ -125,9 +125,11 @@ export const sanitizeServerFeatures = (
export async function sanitizeServerEditorConfig( export async function sanitizeServerEditorConfig(
editorConfig: ServerEditorConfig, editorConfig: ServerEditorConfig,
config: SanitizedConfig, config: SanitizedConfig,
parentIsLocalized?: boolean,
): Promise<SanitizedServerEditorConfig> { ): Promise<SanitizedServerEditorConfig> {
const resolvedFeatureMap = await loadFeatures({ const resolvedFeatureMap = await loadFeatures({
config, config,
parentIsLocalized,
unSanitizedEditorConfig: editorConfig, unSanitizedEditorConfig: editorConfig,
}) })

View File

@@ -67,9 +67,11 @@ export type LexicalRichTextAdapterProvider =
({ ({
config, config,
isRoot, isRoot,
parentIsLocalized,
}: { }: {
config: SanitizedConfig config: SanitizedConfig
isRoot?: boolean isRoot?: boolean
parentIsLocalized: boolean
}) => Promise<LexicalRichTextAdapter> }) => Promise<LexicalRichTextAdapter>
export type LexicalRichTextFieldProps = { export type LexicalRichTextFieldProps = {

View File

@@ -16,6 +16,7 @@ export type CreateServerFeatureArgs<UnSanitizedProps, SanitizedProps, ClientProp
/** unSanitizedEditorConfig.features, but mapped */ /** unSanitizedEditorConfig.features, but mapped */
featureProviderMap: ServerFeatureProviderMap featureProviderMap: ServerFeatureProviderMap
isRoot?: boolean isRoot?: boolean
parentIsLocalized: boolean
props: UnSanitizedProps props: UnSanitizedProps
// other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here // other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here
resolvedFeatures: ResolvedServerFeatureMap resolvedFeatures: ResolvedServerFeatureMap
@@ -57,6 +58,7 @@ export const createServerFeature: <
config, config,
featureProviderMap, featureProviderMap,
isRoot, isRoot,
parentIsLocalized,
resolvedFeatures, resolvedFeatures,
unSanitizedEditorConfig, unSanitizedEditorConfig,
}) => { }) => {
@@ -64,6 +66,7 @@ export const createServerFeature: <
config, config,
featureProviderMap, featureProviderMap,
isRoot, isRoot,
parentIsLocalized,
props, props,
resolvedFeatures, resolvedFeatures,
unSanitizedEditorConfig, unSanitizedEditorConfig,

View File

@@ -29,6 +29,7 @@ export function slateEditor(
args.admin.link.fields = await sanitizeFields({ args.admin.link.fields = await sanitizeFields({
config: config as unknown as Config, config: config as unknown as Config,
fields: transformExtraFields(args.admin?.link?.fields, config), fields: transformExtraFields(args.admin?.link?.fields, config),
parentIsLocalized: false,
validRelationships, validRelationships,
}) })
@@ -38,6 +39,7 @@ export function slateEditor(
args.admin.upload.collections[collection].fields = await sanitizeFields({ args.admin.upload.collections[collection].fields = await sanitizeFields({
config: config as unknown as Config, config: config as unknown as Config,
fields: args.admin?.upload?.collections[collection]?.fields, fields: args.admin?.upload?.collections[collection]?.fields,
parentIsLocalized: false,
validRelationships, validRelationships,
}) })
} }

View File

@@ -0,0 +1,65 @@
import type { CollectionConfig } from 'payload'
export const LocalizedWithinLocalized: CollectionConfig = {
slug: 'localized-within-localized',
fields: [
{
type: 'tabs',
tabs: [
{
name: 'myTab',
label: 'My Tab',
localized: true,
fields: [
{
name: 'shouldNotBeLocalized',
type: 'text',
localized: true,
},
],
},
],
},
{
name: 'myArray',
type: 'array',
localized: true,
fields: [
{
name: 'shouldNotBeLocalized',
type: 'text',
localized: true,
},
],
},
{
name: 'myBlocks',
type: 'blocks',
localized: true,
blocks: [
{
slug: 'myBlock',
fields: [
{
name: 'shouldNotBeLocalized',
type: 'text',
localized: true,
},
],
},
],
},
{
name: 'myGroup',
type: 'group',
localized: true,
fields: [
{
name: 'shouldNotBeLocalized',
type: 'text',
localized: true,
},
],
},
],
}

View File

@@ -9,6 +9,7 @@ import { devUser } from '../credentials.js'
import { ArrayCollection } from './collections/Array/index.js' import { ArrayCollection } from './collections/Array/index.js'
import { BlocksCollection } from './collections/Blocks/index.js' import { BlocksCollection } from './collections/Blocks/index.js'
import { Group } from './collections/Group/index.js' import { Group } from './collections/Group/index.js'
import { LocalizedWithinLocalized } from './collections/LocalizedWithinLocalized/index.js'
import { NestedArray } from './collections/NestedArray/index.js' import { NestedArray } from './collections/NestedArray/index.js'
import { NestedFields } from './collections/NestedFields/index.js' import { NestedFields } from './collections/NestedFields/index.js'
import { NestedToArrayAndBlock } from './collections/NestedToArrayAndBlock/index.js' import { NestedToArrayAndBlock } from './collections/NestedToArrayAndBlock/index.js'
@@ -288,6 +289,7 @@ export default buildConfigWithDefaults({
}, },
], ],
}, },
LocalizedWithinLocalized,
], ],
globals: [ globals: [
{ {

View File

@@ -6,7 +6,6 @@ import { fileURLToPath } from 'url'
import type { NextRESTClient } from '../helpers/NextRESTClient.js' import type { NextRESTClient } from '../helpers/NextRESTClient.js'
import type { LocalizedPost, WithLocalizedRelationship } from './payload-types.js' import type { LocalizedPost, WithLocalizedRelationship } from './payload-types.js'
import { englishLocale } from '../globals/config.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'
@@ -15,6 +14,7 @@ import { nestedToArrayAndBlockCollectionSlug } from './collections/NestedToArray
import { tabSlug } from './collections/Tab/index.js' import { tabSlug } from './collections/Tab/index.js'
import { import {
defaultLocale, defaultLocale,
defaultLocale as englishLocale,
englishTitle, englishTitle,
hungarianLocale, hungarianLocale,
localizedPostsSlug, localizedPostsSlug,
@@ -1396,6 +1396,17 @@ describe('Localization', () => {
}) })
}) })
describe('nested localized field sanitization', () => {
it('should sanitize nested localized fields', () => {
const collection = payload.collections['localized-within-localized'].config
expect(collection.fields[0].tabs[0].fields[0].localized).toBeUndefined()
expect(collection.fields[1].fields[0].localized).toBeUndefined()
expect(collection.fields[2].blocks[0].fields[0].localized).toBeUndefined()
expect(collection.fields[3].fields[0].localized).toBeUndefined()
})
})
describe('nested blocks', () => { describe('nested blocks', () => {
let id let id
it('should allow creating nested blocks per locale', async () => { it('should allow creating nested blocks per locale', async () => {