From e6fea1d13285510d0e26058aa184986443649527 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Mon, 17 Feb 2025 12:50:32 -0700 Subject: [PATCH] fix: localized fields within block references were not handled properly if any parent is localized (#11207) The `localized` properly was not stripped out of referenced block fields, if any parent was localized. For normal fields, this is done in sanitizeConfig. As the same referenced block config can be used in both a localized and non-localized config, we are not able to strip it out inside sanitizeConfig by modifying the block config. Instead, this PR had to bring back tedious logic to handle it everywhere the `field.localized` property is accessed. For backwards-compatibility, we need to keep the existing sanitizeConfig logic. In 4.0, we should remove it to benefit from better test coverage of runtime field.localized handling - for now, this is done for our test suite using the `PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY` flag. --- eslint.config.js | 1 + next.config.mjs | 2 + packages/db-mongodb/src/init.ts | 38 +- .../src/models/buildCollectionSchema.ts | 20 +- .../db-mongodb/src/models/buildGlobalModel.ts | 10 +- packages/db-mongodb/src/models/buildSchema.ts | 434 +++-- .../migrateRelationshipsV2_V3.ts | 13 +- .../src/queries/buildAndOrConditions.ts | 3 + packages/db-mongodb/src/queries/buildQuery.ts | 1 + .../src/queries/buildSearchParams.ts | 5 + .../db-mongodb/src/queries/buildSortParam.ts | 3 + .../src/queries/getLocalizedSortProperty.ts | 11 +- .../db-mongodb/src/queries/parseParams.ts | 4 + .../src/queries/sanitizeQueryValue.ts | 9 +- .../src/utilities/buildJoinAggregation.ts | 11 +- .../utilities/buildProjectionFromSelect.ts | 31 +- .../src/utilities/sanitizeRelationshipIDs.ts | 15 +- packages/drizzle/src/find/traverseFields.ts | 28 +- .../src/queries/buildAndOrConditions.ts | 3 + packages/drizzle/src/queries/buildOrderBy.ts | 3 + packages/drizzle/src/queries/buildQuery.ts | 4 + .../src/queries/getTableColumnFromPath.ts | 34 +- packages/drizzle/src/queries/parseParams.ts | 4 + packages/drizzle/src/schema/build.ts | 3 + packages/drizzle/src/schema/buildRawSchema.ts | 4 + packages/drizzle/src/schema/traverseFields.ts | 52 +- packages/drizzle/src/transform/read/index.ts | 3 + .../src/transform/read/traverseFields.ts | 35 +- packages/drizzle/src/transform/write/array.ts | 7 +- .../drizzle/src/transform/write/blocks.ts | 6 +- packages/drizzle/src/transform/write/index.ts | 3 + .../src/transform/write/traverseFields.ts | 32 +- .../drizzle/src/utilities/hasLocalesTable.ts | 27 +- .../validateExistingBlockIsIdentical.ts | 44 +- .../src/schema/buildMutationInputType.ts | 60 +- .../graphql/src/schema/buildObjectType.ts | 92 +- .../graphql/src/schema/initCollections.ts | 2 + packages/graphql/src/schema/initGlobals.ts | 1 + .../graphql/src/schema/isFieldNullable.ts | 14 +- .../graphql/src/schema/withNullableType.ts | 18 +- .../DiffCollapser/index.tsx | 5 + .../RenderFieldsToDiff/buildVersionFields.tsx | 16 +- .../fields/Collapsible/index.tsx | 2 + .../RenderFieldsToDiff/fields/Group/index.tsx | 2 + .../fields/Iterable/index.tsx | 3 + .../fields/Relationship/index.tsx | 37 +- .../RenderFieldsToDiff/fields/Tabs/index.tsx | 10 +- .../utilities/countChangedFields.ts | 37 +- packages/next/src/views/Version/index.tsx | 1 + packages/payload/src/admin/RichText.ts | 4 +- packages/payload/src/admin/forms/Diff.ts | 1 + .../payload/src/collections/config/types.ts | 5 + packages/payload/src/config/sanitize.ts | 4 + packages/payload/src/config/types.ts | 2 + .../payload/src/database/getLocalizedPaths.ts | 24 +- .../src/database/queryValidation/types.ts | 4 + .../queryValidation/validateSearchParams.ts | 3 + packages/payload/src/exports/shared.ts | 1 + .../payload/src/fields/config/sanitize.ts | 11 +- .../src/fields/config/sanitizeJoinField.ts | 16 +- packages/payload/src/fields/config/types.ts | 25 + .../src/fields/hooks/afterChange/index.ts | 1 + .../src/fields/hooks/afterChange/promise.ts | 9 + .../hooks/afterChange/traverseFields.ts | 6 + .../src/fields/hooks/afterRead/index.ts | 1 + .../src/fields/hooks/afterRead/promise.ts | 24 +- .../relationshipPopulationPromise.ts | 6 +- .../fields/hooks/afterRead/traverseFields.ts | 6 + .../src/fields/hooks/beforeChange/index.ts | 1 + .../src/fields/hooks/beforeChange/promise.ts | 15 +- .../hooks/beforeChange/traverseFields.ts | 6 + .../src/fields/hooks/beforeDuplicate/index.ts | 1 + .../fields/hooks/beforeDuplicate/promise.ts | 16 +- .../hooks/beforeDuplicate/traverseFields.ts | 3 + .../src/fields/hooks/beforeValidate/index.ts | 1 + .../fields/hooks/beforeValidate/promise.ts | 9 + .../hooks/beforeValidate/traverseFields.ts | 6 + .../src/fields/setDefaultBeforeDuplicate.ts | 17 +- packages/payload/src/index.ts | 7 +- .../payload/src/utilities/traverseFields.ts | 32 +- .../blocks/server/graphQLPopulationPromise.ts | 3 + .../link/server/graphQLPopulationPromise.ts | 3 + .../src/features/typesServer.ts | 19 +- .../upload/server/graphQLPopulationPromise.ts | 4 + packages/richtext-lexical/src/index.ts | 24 +- .../populateLexicalPopulationPromises.ts | 3 + .../recursivelyPopulateFieldsForGraphQL.ts | 3 + .../src/data/richTextRelationshipPromise.ts | 1 + packages/richtext-slate/src/index.tsx | 4 + .../ui/src/utilities/copyDataFromLocale.ts | 48 +- test/_community/payload-types.ts | 26 +- test/dev.ts | 3 + test/eslint.config.js | 2 +- test/fields/baseConfig.ts | 20 + test/fields/collections/Blocks/index.ts | 15 + test/fields/collections/Blocks/shared.ts | 12 + test/fields/int.spec.ts | 30 +- test/fields/payload-types.ts | 1609 ++++++++--------- test/helpers/autoDedupeBlocksPlugin/index.ts | 1 + test/jest.setup.js | 2 + test/joins/int.spec.ts | 1 + test/localization/int.spec.ts | 16 +- test/next.config.mjs | 2 + test/runE2E.ts | 3 + tsconfig.base.json | 59 +- 105 files changed, 2036 insertions(+), 1347 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 935643867..b63fa17cc 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,6 +23,7 @@ export const defaultESLintIgnores = [ 'next-env.d.ts', '**/app', 'src/**/*.spec.ts', + '**/jest.setup.js', ] /** @typedef {import('eslint').Linter.Config} Config */ diff --git a/next.config.mjs b/next.config.mjs index 0452bd34f..c05dd022f 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -27,6 +27,8 @@ const config = withBundleAnalyzer( env: { PAYLOAD_CORE_DEV: 'true', ROOT_DIR: path.resolve(dirname), + // @todo remove in 4.0 - will behave like this by default in 4.0 + PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY: 'true', }, async redirects() { return [ diff --git a/packages/db-mongodb/src/init.ts b/packages/db-mongodb/src/init.ts index 554adb5c3..e24d4f5da 100644 --- a/packages/db-mongodb/src/init.ts +++ b/packages/db-mongodb/src/init.ts @@ -26,15 +26,19 @@ export const init: Init = function init(this: MongooseAdapter) { const versionCollectionFields = buildVersionCollectionFields(this.payload.config, collection) - const versionSchema = buildSchema(this.payload, versionCollectionFields, { - disableUnique: true, - draftsEnabled: true, - indexSortableFields: this.payload.config.indexSortableFields, - options: { - minimize: false, - timestamps: false, + const versionSchema = buildSchema({ + buildSchemaOptions: { + disableUnique: true, + draftsEnabled: true, + indexSortableFields: this.payload.config.indexSortableFields, + options: { + minimize: false, + timestamps: false, + }, + ...schemaOptions, }, - ...schemaOptions, + configFields: versionCollectionFields, + payload: this.payload, }) versionSchema.plugin(paginate, { useEstimatedCount: true }).plugin( @@ -77,14 +81,18 @@ export const init: Init = function init(this: MongooseAdapter) { const versionGlobalFields = buildVersionGlobalFields(this.payload.config, global) - const versionSchema = buildSchema(this.payload, versionGlobalFields, { - disableUnique: true, - draftsEnabled: true, - indexSortableFields: this.payload.config.indexSortableFields, - options: { - minimize: false, - timestamps: false, + const versionSchema = buildSchema({ + buildSchemaOptions: { + disableUnique: true, + draftsEnabled: true, + indexSortableFields: this.payload.config.indexSortableFields, + options: { + minimize: false, + timestamps: false, + }, }, + configFields: versionGlobalFields, + payload: this.payload, }) versionSchema.plugin(paginate, { useEstimatedCount: true }).plugin( diff --git a/packages/db-mongodb/src/models/buildCollectionSchema.ts b/packages/db-mongodb/src/models/buildCollectionSchema.ts index 3042efbe3..ca28ddf94 100644 --- a/packages/db-mongodb/src/models/buildCollectionSchema.ts +++ b/packages/db-mongodb/src/models/buildCollectionSchema.ts @@ -12,14 +12,20 @@ export const buildCollectionSchema = ( payload: Payload, schemaOptions = {}, ): Schema => { - const schema = buildSchema(payload, collection.fields, { - draftsEnabled: Boolean(typeof collection?.versions === 'object' && collection.versions.drafts), - indexSortableFields: payload.config.indexSortableFields, - options: { - minimize: false, - timestamps: collection.timestamps !== false, - ...schemaOptions, + const schema = buildSchema({ + buildSchemaOptions: { + draftsEnabled: Boolean( + typeof collection?.versions === 'object' && collection.versions.drafts, + ), + indexSortableFields: payload.config.indexSortableFields, + options: { + minimize: false, + timestamps: collection.timestamps !== false, + ...schemaOptions, + }, }, + configFields: collection.fields, + payload, }) if (Array.isArray(collection.upload.filenameCompoundIndex)) { diff --git a/packages/db-mongodb/src/models/buildGlobalModel.ts b/packages/db-mongodb/src/models/buildGlobalModel.ts index 4801943cb..20c3e22fd 100644 --- a/packages/db-mongodb/src/models/buildGlobalModel.ts +++ b/packages/db-mongodb/src/models/buildGlobalModel.ts @@ -19,10 +19,14 @@ export const buildGlobalModel = (payload: Payload): GlobalModel | null => { const Globals = mongoose.model('globals', globalsSchema, 'globals') as unknown as GlobalModel Object.values(payload.config.globals).forEach((globalConfig) => { - const globalSchema = buildSchema(payload, globalConfig.fields, { - options: { - minimize: false, + const globalSchema = buildSchema({ + buildSchemaOptions: { + options: { + minimize: false, + }, }, + configFields: globalConfig.fields, + payload, }) Globals.discriminator(globalConfig.slug, globalSchema) }) diff --git a/packages/db-mongodb/src/models/buildSchema.ts b/packages/db-mongodb/src/models/buildSchema.ts index 08059892b..af7f50158 100644 --- a/packages/db-mongodb/src/models/buildSchema.ts +++ b/packages/db-mongodb/src/models/buildSchema.ts @@ -31,9 +31,9 @@ import { } from 'payload' import { fieldAffectsData, - fieldIsLocalized, fieldIsPresentationalOnly, fieldIsVirtual, + fieldShouldBeLocalized, tabHasName, } from 'payload/shared' @@ -50,6 +50,7 @@ type FieldSchemaGenerator = ( schema: Schema, config: Payload, buildSchemaOptions: BuildSchemaOptions, + parentIsLocalized: boolean, ) => void /** @@ -61,7 +62,15 @@ const formatDefaultValue = (field: FieldAffectingData) => ? field.defaultValue : undefined -const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSchemaOptions) => { +const formatBaseSchema = ({ + buildSchemaOptions, + field, + parentIsLocalized, +}: { + buildSchemaOptions: BuildSchemaOptions + field: FieldAffectingData + parentIsLocalized: boolean +}) => { const { disableUnique, draftsEnabled, indexSortableFields } = buildSchemaOptions const schema: SchemaTypeOptions = { default: formatDefaultValue(field), @@ -72,7 +81,7 @@ const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSc if ( schema.unique && - (field.localized || + (fieldShouldBeLocalized({ field, parentIsLocalized }) || draftsEnabled || (fieldAffectsData(field) && field.type !== 'group' && @@ -93,8 +102,13 @@ const localizeSchema = ( entity: NonPresentationalField | Tab, schema, localization: false | SanitizedLocalizationConfig, + parentIsLocalized: boolean, ) => { - if (fieldIsLocalized(entity) && localization && Array.isArray(localization.locales)) { + if ( + fieldShouldBeLocalized({ field: entity, parentIsLocalized }) && + localization && + Array.isArray(localization.locales) + ) { return { type: localization.localeCodes.reduce( (localeSchema, locale) => ({ @@ -111,11 +125,13 @@ const localizeSchema = ( return schema } -export const buildSchema = ( - payload: Payload, - configFields: Field[], - buildSchemaOptions: BuildSchemaOptions = {}, -): Schema => { +export const buildSchema = (args: { + buildSchemaOptions: BuildSchemaOptions + configFields: Field[] + parentIsLocalized?: boolean + payload: Payload +}): Schema => { + const { buildSchemaOptions = {}, configFields, parentIsLocalized, payload } = args const { allowIDField, options } = buildSchemaOptions let fields = {} @@ -144,7 +160,7 @@ export const buildSchema = ( const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type] if (addFieldSchema) { - addFieldSchema(field, schema, payload, buildSchemaOptions) + addFieldSchema(field, schema, payload, buildSchemaOptions, parentIsLocalized) } } }) @@ -153,44 +169,49 @@ export const buildSchema = ( } const fieldToSchemaMap: Record = { - array: ( - field: ArrayField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ) => { + array: (field: ArrayField, schema, payload, buildSchemaOptions, parentIsLocalized) => { const baseSchema = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), type: [ - buildSchema(payload, field.fields, { - allowIDField: true, - disableUnique: buildSchemaOptions.disableUnique, - draftsEnabled: buildSchemaOptions.draftsEnabled, - options: { - _id: false, - id: false, - minimize: false, + buildSchema({ + buildSchemaOptions: { + allowIDField: true, + disableUnique: buildSchemaOptions.disableUnique, + draftsEnabled: buildSchemaOptions.draftsEnabled, + options: { + _id: false, + id: false, + minimize: false, + }, }, + configFields: field.fields, + parentIsLocalized: parentIsLocalized || field.localized, + payload, }), ], } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) }, - blocks: ( - field: BlocksField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { + blocks: (field: BlocksField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { const fieldSchema = { type: [new mongoose.Schema({}, { _id: false, discriminatorKey: 'blockType' })], } schema.add({ - [field.name]: localizeSchema(field, fieldSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + fieldSchema, + payload.config.localization, + parentIsLocalized, + ), }) ;(field.blockReferences ?? field.blocks).forEach((blockItem) => { const blockSchema = new mongoose.Schema({}, { _id: false, id: false }) @@ -200,11 +221,17 @@ const fieldToSchemaMap: Record = { block.fields.forEach((blockField) => { const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type] if (addFieldSchema) { - addFieldSchema(blockField, blockSchema, payload, buildSchemaOptions) + addFieldSchema( + blockField, + blockSchema, + payload, + buildSchemaOptions, + parentIsLocalized || field.localized, + ) } }) - if (field.localized && payload.config.localization) { + if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) { payload.config.localization.localeCodes.forEach((localeCode) => { // @ts-expect-error Possible incorrect typing in mongoose types, this works schema.path(`${field.name}.${localeCode}`).discriminator(block.slug, blockSchema) @@ -217,33 +244,46 @@ const fieldToSchemaMap: Record = { }, checkbox: ( field: CheckboxField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, + schema, + payload, + buildSchemaOptions, + parentIsLocalized, ): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean } + const baseSchema = { + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), + type: Boolean, + } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) }, - code: ( - field: CodeField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String } + code: (field: CodeField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { + const baseSchema = { + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), + type: String, + } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) }, collapsible: ( field: CollapsibleField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, + schema, + payload, + buildSchemaOptions, + parentIsLocalized, ): void => { field.fields.forEach((subField: Field) => { if (fieldIsVirtual(subField)) { @@ -253,41 +293,42 @@ const fieldToSchemaMap: Record = { const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type] if (addFieldSchema) { - addFieldSchema(subField, schema, payload, buildSchemaOptions) + addFieldSchema(subField, schema, payload, buildSchemaOptions, parentIsLocalized) } }) }, - date: ( - field: DateField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date } + date: (field: DateField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { + const baseSchema = { + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), + type: Date, + } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) }, - email: ( - field: EmailField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String } + email: (field: EmailField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { + const baseSchema = { + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), + type: String, + } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) }, - group: ( - field: GroupField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { - const formattedBaseSchema = formatBaseSchema(field, buildSchemaOptions) + group: (field: GroupField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { + const formattedBaseSchema = formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }) // carry indexSortableFields through to versions if drafts enabled const indexSortableFields = @@ -297,58 +338,63 @@ const fieldToSchemaMap: Record = { const baseSchema = { ...formattedBaseSchema, - type: buildSchema(payload, field.fields, { - disableUnique: buildSchemaOptions.disableUnique, - draftsEnabled: buildSchemaOptions.draftsEnabled, - indexSortableFields, - options: { - _id: false, - id: false, - minimize: false, + type: buildSchema({ + buildSchemaOptions: { + disableUnique: buildSchemaOptions.disableUnique, + draftsEnabled: buildSchemaOptions.draftsEnabled, + indexSortableFields, + options: { + _id: false, + id: false, + minimize: false, + }, }, + configFields: field.fields, + parentIsLocalized: parentIsLocalized || field.localized, + payload, }), } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) }, - json: ( - field: JSONField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { + json: (field: JSONField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { const baseSchema = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), type: mongoose.Schema.Types.Mixed, } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) }, - number: ( - field: NumberField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { + number: (field: NumberField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { const baseSchema = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), type: field.hasMany ? [Number] : Number, } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) }, - point: ( - field: PointField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { + point: (field: PointField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { const baseSchema: SchemaTypeOptions = { type: { type: String, @@ -363,12 +409,21 @@ const fieldToSchemaMap: Record = { required: false, }, } - if (buildSchemaOptions.disableUnique && field.unique && field.localized) { + if ( + buildSchemaOptions.disableUnique && + field.unique && + fieldShouldBeLocalized({ field, parentIsLocalized }) + ) { baseSchema.coordinates.sparse = true } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) if (field.index === true || field.index === undefined) { @@ -377,7 +432,7 @@ const fieldToSchemaMap: Record = { indexOptions.sparse = true indexOptions.unique = true } - if (field.localized && payload.config.localization) { + if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) { payload.config.localization.locales.forEach((locale) => { schema.index({ [`${field.name}.${locale.code}`]: '2dsphere' }, indexOptions) }) @@ -386,14 +441,9 @@ const fieldToSchemaMap: Record = { } } }, - radio: ( - field: RadioField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { + radio: (field: RadioField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { const baseSchema = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), type: String, enum: field.options.map((option) => { if (typeof option === 'object') { @@ -404,28 +454,34 @@ const fieldToSchemaMap: Record = { } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) }, relationship: ( field: RelationshipField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, + schema, + payload, + buildSchemaOptions, + parentIsLocalized, ) => { const hasManyRelations = Array.isArray(field.relationTo) let schemaToReturn: { [key: string]: any } = {} const valueType = getRelationshipValueType(field, payload) - if (field.localized && payload.config.localization) { + if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) { schemaToReturn = { type: payload.config.localization.localeCodes.reduce((locales, locale) => { let localeSchema: { [key: string]: any } = {} if (hasManyRelations) { localeSchema = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), _id: false, type: mongoose.Schema.Types.Mixed, relationTo: { type: String, enum: field.relationTo }, @@ -436,7 +492,7 @@ const fieldToSchemaMap: Record = { } } else { localeSchema = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), type: valueType, ref: field.relationTo, } @@ -453,7 +509,7 @@ const fieldToSchemaMap: Record = { } } else if (hasManyRelations) { schemaToReturn = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), _id: false, type: mongoose.Schema.Types.Mixed, relationTo: { type: String, enum: field.relationTo }, @@ -471,7 +527,7 @@ const fieldToSchemaMap: Record = { } } else { schemaToReturn = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), type: valueType, ref: field.relationTo, } @@ -490,25 +546,26 @@ const fieldToSchemaMap: Record = { }, richText: ( field: RichTextField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, + schema, + payload, + buildSchemaOptions, + parentIsLocalized, ): void => { const baseSchema = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), type: mongoose.Schema.Types.Mixed, } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) }, - row: ( - field: RowField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { + row: (field: RowField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { field.fields.forEach((subField: Field) => { if (fieldIsVirtual(subField)) { return @@ -517,18 +574,13 @@ const fieldToSchemaMap: Record = { const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type] if (addFieldSchema) { - addFieldSchema(subField, schema, payload, buildSchemaOptions) + addFieldSchema(subField, schema, payload, buildSchemaOptions, parentIsLocalized) } }) }, - select: ( - field: SelectField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { + select: (field: SelectField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { const baseSchema = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), type: String, enum: field.options.map((option) => { if (typeof option === 'object') { @@ -547,34 +599,40 @@ const fieldToSchemaMap: Record = { field, field.hasMany ? [baseSchema] : baseSchema, payload.config.localization, + parentIsLocalized, ), }) }, - tabs: ( - field: TabsField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { + tabs: (field: TabsField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { field.tabs.forEach((tab) => { if (tabHasName(tab)) { if (fieldIsVirtual(tab)) { return } const baseSchema = { - type: buildSchema(payload, tab.fields, { - disableUnique: buildSchemaOptions.disableUnique, - draftsEnabled: buildSchemaOptions.draftsEnabled, - options: { - _id: false, - id: false, - minimize: false, + type: buildSchema({ + buildSchemaOptions: { + disableUnique: buildSchemaOptions.disableUnique, + draftsEnabled: buildSchemaOptions.draftsEnabled, + options: { + _id: false, + id: false, + minimize: false, + }, }, + configFields: tab.fields, + parentIsLocalized: parentIsLocalized || tab.localized, + payload, }), } schema.add({ - [tab.name]: localizeSchema(tab, baseSchema, payload.config.localization), + [tab.name]: localizeSchema( + tab, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) } else { tab.fields.forEach((subField: Field) => { @@ -584,58 +642,68 @@ const fieldToSchemaMap: Record = { const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type] if (addFieldSchema) { - addFieldSchema(subField, schema, payload, buildSchemaOptions) + addFieldSchema( + subField, + schema, + payload, + buildSchemaOptions, + parentIsLocalized || tab.localized, + ) } }) } }) }, - text: ( - field: TextField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { + text: (field: TextField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { const baseSchema = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), type: field.hasMany ? [String] : String, } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) }, textarea: ( field: TextareaField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, + schema, + payload, + buildSchemaOptions, + parentIsLocalized, ): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String } + const baseSchema = { + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), + type: String, + } schema.add({ - [field.name]: localizeSchema(field, baseSchema, payload.config.localization), + [field.name]: localizeSchema( + field, + baseSchema, + payload.config.localization, + parentIsLocalized, + ), }) }, - upload: ( - field: UploadField, - schema: Schema, - payload: Payload, - buildSchemaOptions: BuildSchemaOptions, - ): void => { + upload: (field: UploadField, schema, payload, buildSchemaOptions, parentIsLocalized): void => { const hasManyRelations = Array.isArray(field.relationTo) let schemaToReturn: { [key: string]: any } = {} const valueType = getRelationshipValueType(field, payload) - if (field.localized && payload.config.localization) { + if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) { schemaToReturn = { type: payload.config.localization.localeCodes.reduce((locales, locale) => { let localeSchema: { [key: string]: any } = {} if (hasManyRelations) { localeSchema = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), _id: false, type: mongoose.Schema.Types.Mixed, relationTo: { type: String, enum: field.relationTo }, @@ -646,7 +714,7 @@ const fieldToSchemaMap: Record = { } } else { localeSchema = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), type: valueType, ref: field.relationTo, } @@ -663,7 +731,7 @@ const fieldToSchemaMap: Record = { } } else if (hasManyRelations) { schemaToReturn = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), _id: false, type: mongoose.Schema.Types.Mixed, relationTo: { type: String, enum: field.relationTo }, @@ -681,7 +749,7 @@ const fieldToSchemaMap: Record = { } } else { schemaToReturn = { - ...formatBaseSchema(field, buildSchemaOptions), + ...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }), type: valueType, ref: field.relationTo, } diff --git a/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts b/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts index 926eaf3d0..11d5f4b4f 100644 --- a/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts +++ b/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts @@ -13,12 +13,14 @@ const migrateModelWithBatching = async ({ config, fields, Model, + parentIsLocalized, session, }: { batchSize: number config: SanitizedConfig fields: Field[] Model: Model + parentIsLocalized: boolean session: ClientSession }): Promise => { let hasNext = true @@ -47,7 +49,7 @@ const migrateModelWithBatching = async ({ } for (const doc of docs) { - sanitizeRelationshipIDs({ config, data: doc, fields }) + sanitizeRelationshipIDs({ config, data: doc, fields, parentIsLocalized }) } await Model.collection.bulkWrite( @@ -124,6 +126,7 @@ export async function migrateRelationshipsV2_V3({ config, fields: collection.fields, Model: db.collections[collection.slug], + parentIsLocalized: false, session, }) @@ -138,6 +141,7 @@ export async function migrateRelationshipsV2_V3({ config, fields: buildVersionCollectionFields(config, collection), Model: db.versions[collection.slug], + parentIsLocalized: false, session, }) @@ -163,7 +167,11 @@ export async function migrateRelationshipsV2_V3({ // in case if the global doesn't exist in the database yet (not saved) if (doc) { - sanitizeRelationshipIDs({ config, data: doc, fields: global.fields }) + sanitizeRelationshipIDs({ + config, + data: doc, + fields: global.fields, + }) await GlobalsModel.collection.updateOne( { @@ -185,6 +193,7 @@ export async function migrateRelationshipsV2_V3({ config, fields: buildVersionGlobalFields(config, global), Model: db.versions[global.slug], + parentIsLocalized: false, session, }) diff --git a/packages/db-mongodb/src/queries/buildAndOrConditions.ts b/packages/db-mongodb/src/queries/buildAndOrConditions.ts index 9ac052967..8cc40e8a7 100644 --- a/packages/db-mongodb/src/queries/buildAndOrConditions.ts +++ b/packages/db-mongodb/src/queries/buildAndOrConditions.ts @@ -7,6 +7,7 @@ export async function buildAndOrConditions({ fields, globalSlug, locale, + parentIsLocalized, payload, where, }: { @@ -14,6 +15,7 @@ export async function buildAndOrConditions({ fields: FlattenedField[] globalSlug?: string locale?: string + parentIsLocalized: boolean payload: Payload where: Where[] }): Promise[]> { @@ -29,6 +31,7 @@ export async function buildAndOrConditions({ fields, globalSlug, locale, + parentIsLocalized, payload, where: condition, }) diff --git a/packages/db-mongodb/src/queries/buildQuery.ts b/packages/db-mongodb/src/queries/buildQuery.ts index 64cde6af7..d468bd5e6 100644 --- a/packages/db-mongodb/src/queries/buildQuery.ts +++ b/packages/db-mongodb/src/queries/buildQuery.ts @@ -47,6 +47,7 @@ export const getBuildQueryPlugin = ({ fields, globalSlug, locale, + parentIsLocalized: false, payload, where, }) diff --git a/packages/db-mongodb/src/queries/buildSearchParams.ts b/packages/db-mongodb/src/queries/buildSearchParams.ts index 617a1e1fd..736707e9e 100644 --- a/packages/db-mongodb/src/queries/buildSearchParams.ts +++ b/packages/db-mongodb/src/queries/buildSearchParams.ts @@ -30,6 +30,7 @@ export async function buildSearchParam({ incomingPath, locale, operator, + parentIsLocalized, payload, val, }: { @@ -39,6 +40,7 @@ export async function buildSearchParam({ incomingPath: string locale?: string operator: string + parentIsLocalized: boolean payload: Payload val: unknown }): Promise { @@ -69,6 +71,7 @@ export async function buildSearchParam({ name: 'id', type: idFieldType, } as FlattenedField, + parentIsLocalized, path: '_id', }) } else { @@ -78,6 +81,7 @@ export async function buildSearchParam({ globalSlug, incomingPath: sanitizedPath, locale, + parentIsLocalized, payload, }) } @@ -89,6 +93,7 @@ export async function buildSearchParam({ hasCustomID, locale, operator, + parentIsLocalized, path, payload, val, diff --git a/packages/db-mongodb/src/queries/buildSortParam.ts b/packages/db-mongodb/src/queries/buildSortParam.ts index 6f76cbffe..d58dcc6e0 100644 --- a/packages/db-mongodb/src/queries/buildSortParam.ts +++ b/packages/db-mongodb/src/queries/buildSortParam.ts @@ -7,6 +7,7 @@ type Args = { config: SanitizedConfig fields: FlattenedField[] locale: string + parentIsLocalized?: boolean sort: Sort timestamps: boolean } @@ -22,6 +23,7 @@ export const buildSortParam = ({ config, fields, locale, + parentIsLocalized, sort, timestamps, }: Args): PaginateOptions['sort'] => { @@ -55,6 +57,7 @@ export const buildSortParam = ({ config, fields, locale, + parentIsLocalized, segments: sortProperty.split('.'), }) acc[localizedProperty] = sortDirection diff --git a/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts b/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts index 1fcf5a341..48c23505c 100644 --- a/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts +++ b/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts @@ -1,11 +1,12 @@ import type { FlattenedField, SanitizedConfig } from 'payload' -import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared' +import { fieldAffectsData, fieldIsPresentationalOnly, fieldShouldBeLocalized } from 'payload/shared' type Args = { config: SanitizedConfig fields: FlattenedField[] locale: string + parentIsLocalized: boolean result?: string segments: string[] } @@ -14,6 +15,7 @@ export const getLocalizedSortProperty = ({ config, fields, locale, + parentIsLocalized, result: incomingResult, segments: incomingSegments, }: Args): string => { @@ -35,10 +37,11 @@ export const getLocalizedSortProperty = ({ if (matchedField && !fieldIsPresentationalOnly(matchedField)) { let nextFields: FlattenedField[] + let nextParentIsLocalized = parentIsLocalized const remainingSegments = [...segments] let localizedSegment = matchedField.name - if (matchedField.localized) { + if (fieldShouldBeLocalized({ field: matchedField, parentIsLocalized })) { // Check to see if next segment is a locale if (segments.length > 0) { const nextSegmentIsLocale = config.localization.localeCodes.includes(remainingSegments[0]) @@ -62,6 +65,9 @@ export const getLocalizedSortProperty = ({ matchedField.type === 'array' ) { nextFields = matchedField.flattenedFields + if (!nextParentIsLocalized) { + nextParentIsLocalized = matchedField.localized + } } if (matchedField.type === 'blocks') { @@ -92,6 +98,7 @@ export const getLocalizedSortProperty = ({ config, fields: nextFields, locale, + parentIsLocalized: nextParentIsLocalized, result, segments: remainingSegments, }) diff --git a/packages/db-mongodb/src/queries/parseParams.ts b/packages/db-mongodb/src/queries/parseParams.ts index b27a9b545..45b544ac1 100644 --- a/packages/db-mongodb/src/queries/parseParams.ts +++ b/packages/db-mongodb/src/queries/parseParams.ts @@ -12,6 +12,7 @@ export async function parseParams({ fields, globalSlug, locale, + parentIsLocalized, payload, where, }: { @@ -19,6 +20,7 @@ export async function parseParams({ fields: FlattenedField[] globalSlug?: string locale: string + parentIsLocalized: boolean payload: Payload where: Where }): Promise> { @@ -40,6 +42,7 @@ export async function parseParams({ fields, globalSlug, locale, + parentIsLocalized, payload, where: condition, }) @@ -63,6 +66,7 @@ export async function parseParams({ incomingPath: relationOrPath, locale, operator, + parentIsLocalized, payload, val: pathOperators[operator], }) diff --git a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts index 60292c7cf..3b7dfd085 100644 --- a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts +++ b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts @@ -8,12 +8,14 @@ import type { import { Types } from 'mongoose' import { createArrayFromCommaDelineated } from 'payload' +import { fieldShouldBeLocalized } from 'payload/shared' type SanitizeQueryValueArgs = { field: FlattenedField hasCustomID: boolean locale?: string operator: string + parentIsLocalized: boolean path: string payload: Payload val: any @@ -87,6 +89,7 @@ export const sanitizeQueryValue = ({ hasCustomID, locale, operator, + parentIsLocalized, path, payload, val, @@ -219,7 +222,11 @@ export const sanitizeQueryValue = ({ let localizedPath = path - if (field.localized && payload.config.localization && locale) { + if ( + fieldShouldBeLocalized({ field, parentIsLocalized }) && + payload.config.localization && + locale + ) { localizedPath = `${path}.${locale}` } diff --git a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts index 50895a039..0b08f7672 100644 --- a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts +++ b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts @@ -1,6 +1,8 @@ import type { PipelineStage } from 'mongoose' import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload' +import { fieldShouldBeLocalized } from 'payload/shared' + import type { MongooseAdapter } from '../index.js' import { buildSortParam } from '../queries/buildSortParam.js' @@ -148,7 +150,14 @@ export const buildJoinAggregation = async ({ }) } else { const localeSuffix = - join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : '' + fieldShouldBeLocalized({ + field: join.field, + parentIsLocalized: join.parentIsLocalized, + }) && + adapter.payload.config.localization && + locale + ? `.${locale}` + : '' const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${localeSuffix}` let foreignField: string diff --git a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts index c7445cfaf..5ef4f09b8 100644 --- a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts +++ b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts @@ -1,6 +1,11 @@ import type { FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload' -import { deepCopyObjectSimple, fieldAffectsData, getSelectMode } from 'payload/shared' +import { + deepCopyObjectSimple, + fieldAffectsData, + fieldShouldBeLocalized, + getSelectMode, +} from 'payload/shared' import type { MongooseAdapter } from '../index.js' @@ -8,18 +13,18 @@ const addFieldToProjection = ({ adapter, databaseSchemaPath, field, + parentIsLocalized, projection, - withinLocalizedField, }: { adapter: MongooseAdapter databaseSchemaPath: string field: FieldAffectingData + parentIsLocalized: boolean projection: Record - withinLocalizedField: boolean }) => { const { config } = adapter.payload - if (withinLocalizedField && config.localization) { + if (parentIsLocalized && config.localization) { for (const locale of config.localization.localeCodes) { const localeDatabaseSchemaPath = databaseSchemaPath.replace('', locale) projection[`${localeDatabaseSchemaPath}${field.name}`] = true @@ -33,20 +38,20 @@ const traverseFields = ({ adapter, databaseSchemaPath = '', fields, + parentIsLocalized = false, projection, select, selectAllOnCurrentLevel = false, selectMode, - withinLocalizedField = false, }: { adapter: MongooseAdapter databaseSchemaPath?: string fields: FlattenedField[] + parentIsLocalized?: boolean projection: Record select: SelectType selectAllOnCurrentLevel?: boolean selectMode: SelectMode - withinLocalizedField?: boolean }) => { for (const field of fields) { if (fieldAffectsData(field)) { @@ -56,8 +61,8 @@ const traverseFields = ({ adapter, databaseSchemaPath, field, + parentIsLocalized, projection, - withinLocalizedField, }) continue } @@ -73,8 +78,8 @@ const traverseFields = ({ adapter, databaseSchemaPath, field, + parentIsLocalized, projection, - withinLocalizedField, }) continue } @@ -86,14 +91,12 @@ const traverseFields = ({ } let fieldDatabaseSchemaPath = databaseSchemaPath - let fieldWithinLocalizedField = withinLocalizedField if (fieldAffectsData(field)) { fieldDatabaseSchemaPath = `${databaseSchemaPath}${field.name}.` - if (field.localized) { + if (fieldShouldBeLocalized({ field, parentIsLocalized })) { fieldDatabaseSchemaPath = `${fieldDatabaseSchemaPath}.` - fieldWithinLocalizedField = true } } @@ -111,10 +114,10 @@ const traverseFields = ({ adapter, databaseSchemaPath: fieldDatabaseSchemaPath, fields: field.flattenedFields, + parentIsLocalized: parentIsLocalized || field.localized, projection, select: fieldSelect, selectMode, - withinLocalizedField: fieldWithinLocalizedField, }) break @@ -133,11 +136,11 @@ const traverseFields = ({ adapter, databaseSchemaPath: fieldDatabaseSchemaPath, fields: block.flattenedFields, + parentIsLocalized: parentIsLocalized || field.localized, projection, select: {}, selectAllOnCurrentLevel: true, selectMode: 'include', - withinLocalizedField: fieldWithinLocalizedField, }) continue } @@ -161,10 +164,10 @@ const traverseFields = ({ adapter, databaseSchemaPath: fieldDatabaseSchemaPath, fields: block.flattenedFields, + parentIsLocalized: parentIsLocalized || field.localized, projection, select: blocksSelect[block.slug] as SelectType, selectMode: blockSelectMode, - withinLocalizedField: fieldWithinLocalizedField, }) } diff --git a/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts b/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts index 948b7b29b..cb699cc29 100644 --- a/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts +++ b/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts @@ -2,12 +2,13 @@ import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } import { Types } from 'mongoose' import { traverseFields } from 'payload' -import { fieldAffectsData } from 'payload/shared' +import { fieldAffectsData, fieldShouldBeLocalized } from 'payload/shared' type Args = { config: SanitizedConfig data: Record fields: Field[] + parentIsLocalized?: boolean } interface RelationObject { @@ -112,6 +113,7 @@ export const sanitizeRelationshipIDs = ({ config, data, fields, + parentIsLocalized, }: Args): Record => { const sanitize: TraverseFieldsCallback = ({ field, ref }) => { if (!ref || typeof ref !== 'object') { @@ -124,7 +126,7 @@ export const sanitizeRelationshipIDs = ({ } // handle localized relationships - if (config.localization && field.localized) { + if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) { const locales = config.localization.locales const fieldRef = ref[field.name] if (typeof fieldRef !== 'object') { @@ -150,7 +152,14 @@ export const sanitizeRelationshipIDs = ({ } } - traverseFields({ callback: sanitize, config, fields, fillEmpty: false, ref: data }) + traverseFields({ + callback: sanitize, + config, + fields, + fillEmpty: false, + parentIsLocalized, + ref: data, + }) return data } diff --git a/packages/drizzle/src/find/traverseFields.ts b/packages/drizzle/src/find/traverseFields.ts index 6a98b8c4e..9f2a1173d 100644 --- a/packages/drizzle/src/find/traverseFields.ts +++ b/packages/drizzle/src/find/traverseFields.ts @@ -2,7 +2,7 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql' import type { FlattenedField, JoinQuery, SelectMode, SelectType, Where } from 'payload' import { sql } from 'drizzle-orm' -import { fieldIsVirtual } from 'payload/shared' +import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared' import toSnakeCase from 'to-snake-case' import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../types.js' @@ -26,6 +26,7 @@ type TraverseFieldArgs = { joinQuery: JoinQuery joins?: BuildQueryJoinAliases locale?: string + parentIsLocalized?: boolean path: string select?: SelectType selectAllOnCurrentLevel?: boolean @@ -34,7 +35,6 @@ type TraverseFieldArgs = { topLevelArgs: Record topLevelTableName: string versions?: boolean - withinLocalizedField?: boolean withTabledFields: { numbers?: boolean rels?: boolean @@ -53,6 +53,7 @@ export const traverseFields = ({ joinQuery = {}, joins, locale, + parentIsLocalized = false, path, select, selectAllOnCurrentLevel = false, @@ -61,7 +62,6 @@ export const traverseFields = ({ topLevelArgs, topLevelTableName, versions, - withinLocalizedField = false, withTabledFields, }: TraverseFieldArgs) => { fields.forEach((field) => { @@ -69,6 +69,11 @@ export const traverseFields = ({ return } + const isFieldLocalized = fieldShouldBeLocalized({ + field, + parentIsLocalized, + }) + // handle simple relationship if ( depth > 0 && @@ -76,7 +81,7 @@ export const traverseFields = ({ !field.hasMany && typeof field.relationTo === 'string' ) { - if (field.localized) { + if (isFieldLocalized) { _locales.with[`${path}${field.name}`] = true } else { currentArgs.with[`${path}${field.name}`] = true @@ -152,13 +157,13 @@ export const traverseFields = ({ fields: field.flattenedFields, joinQuery, locale, + parentIsLocalized: parentIsLocalized || field.localized, path: '', select: typeof arraySelect === 'object' ? arraySelect : undefined, selectMode, tablePath: '', topLevelArgs, topLevelTableName, - withinLocalizedField: withinLocalizedField || field.localized, withTabledFields, }) @@ -263,13 +268,13 @@ export const traverseFields = ({ fields: block.flattenedFields, joinQuery, locale, + parentIsLocalized: parentIsLocalized || field.localized, path: '', select: typeof blockSelect === 'object' ? blockSelect : undefined, selectMode: blockSelectMode, tablePath: '', topLevelArgs, topLevelTableName, - withinLocalizedField: withinLocalizedField || field.localized, withTabledFields, }) @@ -305,6 +310,7 @@ export const traverseFields = ({ joinQuery, joins, locale, + parentIsLocalized: parentIsLocalized || field.localized, path: `${path}${field.name}_`, select: typeof fieldSelect === 'object' ? fieldSelect : undefined, selectAllOnCurrentLevel: @@ -316,7 +322,6 @@ export const traverseFields = ({ topLevelArgs, topLevelTableName, versions, - withinLocalizedField: withinLocalizedField || field.localized, withTabledFields, }) @@ -407,6 +412,9 @@ export const traverseFields = ({ fields, joins, locale, + // Parent is never localized, as we're passing the `fields` of a **different** collection here. This means that the + // parent localization "boundary" is crossed, and we're now in the context of the joined collection. + parentIsLocalized: false, selectLocale: true, sort, tableName: joinCollectionTableName, @@ -469,7 +477,7 @@ export const traverseFields = ({ break } - const args = field.localized ? _locales : currentArgs + const args = isFieldLocalized ? _locales : currentArgs if (!args.columns) { args.columns = {} } @@ -531,7 +539,7 @@ export const traverseFields = ({ if (select || selectAllOnCurrentLevel) { const fieldPath = `${path}${field.name}` - if ((field.localized || withinLocalizedField) && _locales) { + if ((isFieldLocalized || parentIsLocalized) && _locales) { _locales.columns[fieldPath] = true } else if (adapter.tables[currentTableName]?.[fieldPath]) { currentArgs.columns[fieldPath] = true @@ -553,7 +561,7 @@ export const traverseFields = ({ ) { const fieldPath = `${path}${field.name}` - if ((field.localized || withinLocalizedField) && _locales) { + if ((isFieldLocalized || parentIsLocalized) && _locales) { _locales.columns[fieldPath] = true } else if (adapter.tables[currentTableName]?.[fieldPath]) { currentArgs.columns[fieldPath] = true diff --git a/packages/drizzle/src/queries/buildAndOrConditions.ts b/packages/drizzle/src/queries/buildAndOrConditions.ts index c59687c5c..759eb843d 100644 --- a/packages/drizzle/src/queries/buildAndOrConditions.ts +++ b/packages/drizzle/src/queries/buildAndOrConditions.ts @@ -12,6 +12,7 @@ export function buildAndOrConditions({ fields, joins, locale, + parentIsLocalized, selectFields, selectLocale, tableName, @@ -24,6 +25,7 @@ export function buildAndOrConditions({ globalSlug?: string joins: BuildQueryJoinAliases locale?: string + parentIsLocalized: boolean selectFields: Record selectLocale?: boolean tableName: string @@ -42,6 +44,7 @@ export function buildAndOrConditions({ fields, joins, locale, + parentIsLocalized, selectFields, selectLocale, tableName, diff --git a/packages/drizzle/src/queries/buildOrderBy.ts b/packages/drizzle/src/queries/buildOrderBy.ts index 81c6e6ab6..da779bbb2 100644 --- a/packages/drizzle/src/queries/buildOrderBy.ts +++ b/packages/drizzle/src/queries/buildOrderBy.ts @@ -15,6 +15,7 @@ type Args = { fields: FlattenedField[] joins: BuildQueryJoinAliases locale?: string + parentIsLocalized: boolean selectFields: Record sort?: Sort tableName: string @@ -29,6 +30,7 @@ export const buildOrderBy = ({ fields, joins, locale, + parentIsLocalized, selectFields, sort, tableName, @@ -65,6 +67,7 @@ export const buildOrderBy = ({ fields, joins, locale, + parentIsLocalized, pathSegments: sortProperty.replace(/__/g, '.').split('.'), selectFields, tableName, diff --git a/packages/drizzle/src/queries/buildQuery.ts b/packages/drizzle/src/queries/buildQuery.ts index 95dc2a75a..a072a07e8 100644 --- a/packages/drizzle/src/queries/buildQuery.ts +++ b/packages/drizzle/src/queries/buildQuery.ts @@ -20,6 +20,7 @@ type BuildQueryArgs = { fields: FlattenedField[] joins?: BuildQueryJoinAliases locale?: string + parentIsLocalized?: boolean selectLocale?: boolean sort?: Sort tableName: string @@ -41,6 +42,7 @@ const buildQuery = function buildQuery({ fields, joins = [], locale, + parentIsLocalized, selectLocale, sort, tableName, @@ -56,6 +58,7 @@ const buildQuery = function buildQuery({ fields, joins, locale, + parentIsLocalized, selectFields, sort, tableName, @@ -70,6 +73,7 @@ const buildQuery = function buildQuery({ fields, joins, locale, + parentIsLocalized, selectFields, selectLocale, tableName, diff --git a/packages/drizzle/src/queries/getTableColumnFromPath.ts b/packages/drizzle/src/queries/getTableColumnFromPath.ts index 5717a6507..3e9dc92f7 100644 --- a/packages/drizzle/src/queries/getTableColumnFromPath.ts +++ b/packages/drizzle/src/queries/getTableColumnFromPath.ts @@ -5,7 +5,7 @@ import type { FlattenedBlock, FlattenedField, NumberField, TextField } from 'pay import { and, eq, like, sql } from 'drizzle-orm' import { type PgTableWithColumns } from 'drizzle-orm/pg-core' import { APIError } from 'payload' -import { tabHasName } from 'payload/shared' +import { fieldShouldBeLocalized, tabHasName } from 'payload/shared' import toSnakeCase from 'to-snake-case' import { validate as uuidValidate } from 'uuid' @@ -46,6 +46,7 @@ type Args = { fields: FlattenedField[] joins: BuildQueryJoinAliases locale?: string + parentIsLocalized: boolean pathSegments: string[] rootTableName?: string selectFields: Record @@ -75,6 +76,7 @@ export const getTableColumnFromPath = ({ fields, joins, locale: incomingLocale, + parentIsLocalized, pathSegments: incomingSegments, rootTableName: incomingRootTableName, selectFields, @@ -107,9 +109,11 @@ export const getTableColumnFromPath = ({ if (field) { const pathSegments = [...incomingSegments] + const isFieldLocalized = fieldShouldBeLocalized({ field, parentIsLocalized }) + // If next segment is a locale, // we need to take it out and use it as the locale from this point on - if ('localized' in field && field.localized && adapter.payload.config.localization) { + if (isFieldLocalized && adapter.payload.config.localization) { const matchedLocale = adapter.payload.config.localization.localeCodes.find( (locale) => locale === pathSegments[1], ) @@ -129,7 +133,7 @@ export const getTableColumnFromPath = ({ const arrayParentTable = aliasTable || adapter.tables[tableName] constraintPath = `${constraintPath}${field.name}.%.` - if (locale && field.localized && adapter.payload.config.localization) { + if (locale && isFieldLocalized && adapter.payload.config.localization) { const conditions = [eq(arrayParentTable.id, adapter.tables[newTableName]._parentID)] if (selectLocale) { @@ -159,6 +163,7 @@ export const getTableColumnFromPath = ({ fields: field.flattenedFields, joins, locale, + parentIsLocalized: parentIsLocalized || field.localized, pathSegments: pathSegments.slice(1), rootTableName, selectFields, @@ -224,6 +229,7 @@ export const getTableColumnFromPath = ({ fields: block.flattenedFields, joins, locale, + parentIsLocalized: parentIsLocalized || field.localized, pathSegments: pathSegments.slice(1), rootTableName, selectFields: blockSelectFields, @@ -240,7 +246,7 @@ export const getTableColumnFromPath = ({ blockTableColumn = result constraints = constraints.concat(blockConstraints) selectFields = { ...selectFields, ...blockSelectFields } - if (field.localized && adapter.payload.config.localization) { + if (isFieldLocalized && adapter.payload.config.localization) { const conditions = [ eq( (aliasTable || adapter.tables[tableName]).id, @@ -281,7 +287,7 @@ export const getTableColumnFromPath = ({ } case 'group': { - if (locale && field.localized && adapter.payload.config.localization) { + if (locale && isFieldLocalized && adapter.payload.config.localization) { newTableName = `${tableName}${adapter.localesSuffix}` let condition = eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID) @@ -306,6 +312,7 @@ export const getTableColumnFromPath = ({ fields: field.flattenedFields, joins, locale, + parentIsLocalized: parentIsLocalized || field.localized, pathSegments: pathSegments.slice(1), rootTableName, selectFields, @@ -331,7 +338,7 @@ export const getTableColumnFromPath = ({ like(adapter.tables[newTableName].path, `${constraintPath}${field.name}`), ] - if (locale && field.localized && adapter.payload.config.localization) { + if (locale && isFieldLocalized && adapter.payload.config.localization) { const conditions = [...joinConstraints] if (locale !== 'all') { @@ -375,12 +382,12 @@ export const getTableColumnFromPath = ({ tableName: relationTableName, }) - if (selectLocale && field.localized && adapter.payload.config.localization) { + if (selectLocale && isFieldLocalized && adapter.payload.config.localization) { selectFields._locale = aliasRelationshipTable.locale } // Join in the relationships table - if (locale && field.localized && adapter.payload.config.localization) { + if (locale && isFieldLocalized && adapter.payload.config.localization) { const conditions = [ eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent), like(aliasRelationshipTable.path, `${constraintPath}${field.name}`), @@ -546,9 +553,11 @@ export const getTableColumnFromPath = ({ aliasTable: newAliasTable, collectionPath: newCollectionPath, constraints, + // relationshipFields are fields from a different collection => no parentIsLocalized fields: relationshipFields, joins, locale, + parentIsLocalized: false, pathSegments: pathSegments.slice(1), rootTableName: newTableName, selectFields, @@ -567,7 +576,7 @@ export const getTableColumnFromPath = ({ ) const { newAliasTable } = getTableAlias({ adapter, tableName: newTableName }) - if (field.localized && adapter.payload.config.localization) { + if (isFieldLocalized && adapter.payload.config.localization) { const { newAliasTable: aliasLocaleTable } = getTableAlias({ adapter, tableName: `${rootTableName}${adapter.localesSuffix}`, @@ -614,6 +623,7 @@ export const getTableColumnFromPath = ({ fields: adapter.payload.collections[field.relationTo].config.flattenedFields, joins, locale, + parentIsLocalized: parentIsLocalized || field.localized, pathSegments: pathSegments.slice(1), selectFields, tableName: newTableName, @@ -629,7 +639,7 @@ export const getTableColumnFromPath = ({ `${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`, ) - if (locale && field.localized && adapter.payload.config.localization) { + if (locale && isFieldLocalized && adapter.payload.config.localization) { const conditions = [ eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent), eq(adapter.tables[newTableName]._locale, locale), @@ -674,6 +684,7 @@ export const getTableColumnFromPath = ({ fields: field.flattenedFields, joins, locale, + parentIsLocalized: parentIsLocalized || field.localized, pathSegments: pathSegments.slice(1), rootTableName, selectFields, @@ -693,6 +704,7 @@ export const getTableColumnFromPath = ({ fields: field.flattenedFields, joins, locale, + parentIsLocalized: parentIsLocalized || field.localized, pathSegments: pathSegments.slice(1), rootTableName, selectFields, @@ -711,7 +723,7 @@ export const getTableColumnFromPath = ({ let newTable = adapter.tables[newTableName] - if (field.localized && adapter.payload.config.localization) { + if (isFieldLocalized && adapter.payload.config.localization) { // If localized, we go to localized table and set aliasTable to undefined // so it is not picked up below to be used as targetTable const parentTable = aliasTable || adapter.tables[tableName] diff --git a/packages/drizzle/src/queries/parseParams.ts b/packages/drizzle/src/queries/parseParams.ts index 705ff2acd..a6182fb66 100644 --- a/packages/drizzle/src/queries/parseParams.ts +++ b/packages/drizzle/src/queries/parseParams.ts @@ -20,6 +20,7 @@ type Args = { fields: FlattenedField[] joins: BuildQueryJoinAliases locale: string + parentIsLocalized: boolean selectFields: Record selectLocale?: boolean tableName: string @@ -32,6 +33,7 @@ export function parseParams({ fields, joins, locale, + parentIsLocalized, selectFields, selectLocale, tableName, @@ -58,6 +60,7 @@ export function parseParams({ fields, joins, locale, + parentIsLocalized, selectFields, selectLocale, tableName, @@ -92,6 +95,7 @@ export function parseParams({ fields, joins, locale, + parentIsLocalized, pathSegments: relationOrPath.replace(/__/g, '.').split('.'), selectFields, selectLocale, diff --git a/packages/drizzle/src/schema/build.ts b/packages/drizzle/src/schema/build.ts index 048112245..045c7fd25 100644 --- a/packages/drizzle/src/schema/build.ts +++ b/packages/drizzle/src/schema/build.ts @@ -37,6 +37,7 @@ type Args = { disableRelsTableUnique?: boolean disableUnique: boolean fields: FlattenedField[] + parentIsLocalized: boolean rootRelationships?: Set rootRelationsToBuild?: RelationMap rootTableIDColType?: IDType @@ -71,6 +72,7 @@ export const buildTable = ({ disableRelsTableUnique = false, disableUnique = false, fields, + parentIsLocalized, rootRelationships, rootRelationsToBuild, rootTableIDColType, @@ -124,6 +126,7 @@ export const buildTable = ({ localesColumns, localesIndexes, newTableName: tableName, + parentIsLocalized, parentTableName: tableName, relationships, relationsToBuild, diff --git a/packages/drizzle/src/schema/buildRawSchema.ts b/packages/drizzle/src/schema/buildRawSchema.ts index 701d50521..101118f18 100644 --- a/packages/drizzle/src/schema/buildRawSchema.ts +++ b/packages/drizzle/src/schema/buildRawSchema.ts @@ -55,6 +55,7 @@ export const buildRawSchema = ({ disableNotNull: !!collection?.versions?.drafts, disableUnique: false, fields: collection.flattenedFields, + parentIsLocalized: false, setColumnID, tableName, timestamps: collection.timestamps, @@ -72,6 +73,7 @@ export const buildRawSchema = ({ disableNotNull: !!collection.versions?.drafts, disableUnique: true, fields: versionFields, + parentIsLocalized: false, setColumnID, tableName: versionsTableName, timestamps: true, @@ -91,6 +93,7 @@ export const buildRawSchema = ({ disableNotNull: !!global?.versions?.drafts, disableUnique: false, fields: global.flattenedFields, + parentIsLocalized: false, setColumnID, tableName, timestamps: false, @@ -112,6 +115,7 @@ export const buildRawSchema = ({ disableNotNull: !!global.versions?.drafts, disableUnique: true, fields: versionFields, + parentIsLocalized: false, setColumnID, tableName: versionsTableName, timestamps: true, diff --git a/packages/drizzle/src/schema/traverseFields.ts b/packages/drizzle/src/schema/traverseFields.ts index 67b8be712..d5243366a 100644 --- a/packages/drizzle/src/schema/traverseFields.ts +++ b/packages/drizzle/src/schema/traverseFields.ts @@ -1,7 +1,12 @@ import type { FlattenedField } from 'payload' import { InvalidConfiguration } from 'payload' -import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared' +import { + fieldAffectsData, + fieldIsVirtual, + fieldShouldBeLocalized, + optionIsObject, +} from 'payload/shared' import toSnakeCase from 'to-snake-case' import type { @@ -37,6 +42,7 @@ type Args = { localesColumns: Record localesIndexes: Record newTableName: string + parentIsLocalized: boolean parentTableName: string relationships: Set relationsToBuild: RelationMap @@ -76,6 +82,7 @@ export const traverseFields = ({ localesColumns, localesIndexes, newTableName, + parentIsLocalized, parentTableName, relationships, relationsToBuild, @@ -119,11 +126,13 @@ export const traverseFields = ({ )}` const fieldName = `${fieldPrefix?.replace('.', '_') || ''}${field.name}` + const isFieldLocalized = fieldShouldBeLocalized({ field, parentIsLocalized }) + // If field is localized, // add the column to the locale table instead of main table if ( adapter.payload.config.localization && - (field.localized || forceLocalized) && + (isFieldLocalized || forceLocalized) && field.type !== 'array' && field.type !== 'blocks' && (('hasMany' in field && field.hasMany !== true) || !('hasMany' in field)) @@ -152,7 +161,7 @@ export const traverseFields = ({ targetIndexes[indexName] = { name: indexName, - on: field.localized ? [fieldName, '_locale'] : fieldName, + on: isFieldLocalized ? [fieldName, '_locale'] : fieldName, unique, } } @@ -209,7 +218,7 @@ export const traverseFields = ({ } const isLocalized = - Boolean(field.localized && adapter.payload.config.localization) || + Boolean(isFieldLocalized && adapter.payload.config.localization) || withinLocalizedArrayOrBlock || forceLocalized @@ -243,6 +252,7 @@ export const traverseFields = ({ disableRelsTableUnique: true, disableUnique, fields: disableUnique ? idToUUID(field.flattenedFields) : field.flattenedFields, + parentIsLocalized: parentIsLocalized || field.localized, rootRelationships: relationships, rootRelationsToBuild, rootTableIDColType, @@ -299,7 +309,12 @@ export const traverseFields = ({ }, } - if (hasLocalesTable(field.fields)) { + if ( + hasLocalesTable({ + fields: field.fields, + parentIsLocalized: parentIsLocalized || field.localized, + }) + ) { arrayRelations._locales = { type: 'many', relationName: '_locales', @@ -403,7 +418,7 @@ export const traverseFields = ({ } const isLocalized = - Boolean(field.localized && adapter.payload.config.localization) || + Boolean(isFieldLocalized && adapter.payload.config.localization) || withinLocalizedArrayOrBlock || forceLocalized @@ -437,6 +452,7 @@ export const traverseFields = ({ disableRelsTableUnique: true, disableUnique, fields: disableUnique ? idToUUID(block.flattenedFields) : block.flattenedFields, + parentIsLocalized: parentIsLocalized || field.localized, rootRelationships: relationships, rootRelationsToBuild, rootTableIDColType, @@ -487,7 +503,12 @@ export const traverseFields = ({ }, } - if (hasLocalesTable(block.fields)) { + if ( + hasLocalesTable({ + fields: block.fields, + parentIsLocalized: parentIsLocalized || field.localized, + }) + ) { blockRelations._locales = { type: 'many', relationName: '_locales', @@ -529,6 +550,7 @@ export const traverseFields = ({ validateExistingBlockIsIdentical({ block, localized: field.localized, + parentIsLocalized: parentIsLocalized || field.localized, rootTableName, table: adapter.rawTables[blockTableName], tableLocales: adapter.rawTables[`${blockTableName}${adapter.localesSuffix}`], @@ -605,11 +627,12 @@ export const traverseFields = ({ disableUnique, fieldPrefix: `${fieldName}.`, fields: field.flattenedFields, - forceLocalized: field.localized, + forceLocalized: isFieldLocalized, indexes, localesColumns, localesIndexes, newTableName: `${parentTableName}_${columnName}`, + parentIsLocalized: parentIsLocalized || field.localized, parentTableName, relationships, relationsToBuild, @@ -619,7 +642,7 @@ export const traverseFields = ({ setColumnID, uniqueRelationships, versions, - withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized, + withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || isFieldLocalized, }) if (groupHasLocalizedField) { @@ -659,7 +682,7 @@ export const traverseFields = ({ case 'number': { if (field.hasMany) { const isLocalized = - Boolean(field.localized && adapter.payload.config.localization) || + Boolean(isFieldLocalized && adapter.payload.config.localization) || withinLocalizedArrayOrBlock || forceLocalized @@ -784,7 +807,7 @@ export const traverseFields = ({ } const isLocalized = - Boolean(field.localized && adapter.payload.config.localization) || + Boolean(isFieldLocalized && adapter.payload.config.localization) || withinLocalizedArrayOrBlock || forceLocalized @@ -817,6 +840,7 @@ export const traverseFields = ({ disableNotNull, disableUnique, fields: [], + parentIsLocalized: parentIsLocalized || field.localized, rootTableName, setColumnID, tableName: selectTableName, @@ -904,7 +928,7 @@ export const traverseFields = ({ // add relationship to table relationsToBuild.set(fieldName, { type: 'one', - localized: adapter.payload.config.localization && (field.localized || forceLocalized), + localized: adapter.payload.config.localization && (isFieldLocalized || forceLocalized), target: tableName, }) @@ -916,7 +940,7 @@ export const traverseFields = ({ } if ( - Boolean(field.localized && adapter.payload.config.localization) || + Boolean(isFieldLocalized && adapter.payload.config.localization) || withinLocalizedArrayOrBlock ) { hasLocalizedRelationshipField = true @@ -927,7 +951,7 @@ export const traverseFields = ({ case 'text': { if (field.hasMany) { const isLocalized = - Boolean(field.localized && adapter.payload.config.localization) || + Boolean(isFieldLocalized && adapter.payload.config.localization) || withinLocalizedArrayOrBlock || forceLocalized diff --git a/packages/drizzle/src/transform/read/index.ts b/packages/drizzle/src/transform/read/index.ts index 9c00af077..0d677c0db 100644 --- a/packages/drizzle/src/transform/read/index.ts +++ b/packages/drizzle/src/transform/read/index.ts @@ -14,6 +14,7 @@ type TransformArgs = { fields: FlattenedField[] joinQuery?: JoinQuery locale?: string + parentIsLocalized?: boolean } // This is the entry point to transform Drizzle output data @@ -24,6 +25,7 @@ export const transform = | TypeWithID>({ data, fields, joinQuery, + parentIsLocalized, }: TransformArgs): T => { let relationships: Record[]> = {} let texts: Record[]> = {} @@ -59,6 +61,7 @@ export const transform = | TypeWithID>({ fields, joinQuery, numbers, + parentIsLocalized, path: '', relationships, table: data, diff --git a/packages/drizzle/src/transform/read/traverseFields.ts b/packages/drizzle/src/transform/read/traverseFields.ts index d7078cc5f..c9cd95a48 100644 --- a/packages/drizzle/src/transform/read/traverseFields.ts +++ b/packages/drizzle/src/transform/read/traverseFields.ts @@ -1,6 +1,6 @@ import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload' -import { fieldIsVirtual } from 'payload/shared' +import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared' import type { DrizzleAdapter } from '../../types.js' import type { BlocksMap } from '../../utilities/createBlocksMap.js' @@ -46,6 +46,7 @@ type TraverseFieldsArgs = { * All hasMany number fields, as returned by Drizzle, keyed on an object by field path */ numbers: Record[]> + parentIsLocalized: boolean /** * The current field path (in dot notation), used to merge in relationships */ @@ -80,6 +81,7 @@ export const traverseFields = >({ fields, joinQuery, numbers, + parentIsLocalized, path, relationships, table, @@ -105,9 +107,11 @@ export const traverseFields = >({ deletions.push(() => delete table[fieldName]) } + const isLocalized = fieldShouldBeLocalized({ field, parentIsLocalized }) + if (field.type === 'array') { if (Array.isArray(fieldData)) { - if (field.localized) { + if (isLocalized) { result[field.name] = fieldData.reduce((arrayResult, row) => { if (typeof row._locale === 'string') { if (!arrayResult[row._locale]) { @@ -130,6 +134,7 @@ export const traverseFields = >({ fieldPrefix: '', fields: field.flattenedFields, numbers, + parentIsLocalized: parentIsLocalized || field.localized, path: `${sanitizedPath}${field.name}.${row._order - 1}`, relationships, table: row, @@ -175,6 +180,7 @@ export const traverseFields = >({ fieldPrefix: '', fields: field.flattenedFields, numbers, + parentIsLocalized: parentIsLocalized || field.localized, path: `${sanitizedPath}${field.name}.${i}`, relationships, table: row, @@ -197,7 +203,7 @@ export const traverseFields = >({ const blocksByPath = blocks[blockFieldPath] if (Array.isArray(blocksByPath)) { - if (field.localized) { + if (isLocalized) { result[field.name] = {} blocksByPath.forEach((row) => { @@ -232,6 +238,7 @@ export const traverseFields = >({ fieldPrefix: '', fields: block.flattenedFields, numbers, + parentIsLocalized: parentIsLocalized || field.localized, path: `${blockFieldPath}.${row._order - 1}`, relationships, table: row, @@ -303,6 +310,7 @@ export const traverseFields = >({ fieldPrefix: '', fields: block.flattenedFields, numbers, + parentIsLocalized: parentIsLocalized || field.localized, path: `${blockFieldPath}.${i}`, relationships, table: row, @@ -328,7 +336,7 @@ export const traverseFields = >({ if (field.type === 'relationship' || field.type === 'upload') { if (typeof field.relationTo === 'string' && !('hasMany' in field && field.hasMany)) { if ( - field.localized && + isLocalized && config.localization && config.localization.locales && Array.isArray(table?._locales) @@ -344,7 +352,7 @@ export const traverseFields = >({ if (!relationPathMatch) { if ('hasMany' in field && field.hasMany) { - if (field.localized && config.localization && config.localization.locales) { + if (isLocalized && config.localization && config.localization.locales) { result[field.name] = { [config.localization.defaultLocale]: [], } @@ -356,7 +364,7 @@ export const traverseFields = >({ return result } - if (field.localized) { + if (isLocalized) { result[field.name] = {} const relationsByLocale: Record[]> = {} @@ -402,7 +410,7 @@ export const traverseFields = >({ | { docs: unknown[]; hasNextPage: boolean } | Record if (Array.isArray(fieldData)) { - if (field.localized && adapter.payload.config.localization) { + if (isLocalized && adapter.payload.config.localization) { fieldResult = fieldData.reduce( (joinResult, row) => { if (typeof row.locale === 'string') { @@ -446,7 +454,7 @@ export const traverseFields = >({ return result } - if (field.localized) { + if (isLocalized) { result[field.name] = {} const textsByLocale: Record[]> = {} @@ -485,7 +493,7 @@ export const traverseFields = >({ return result } - if (field.localized) { + if (isLocalized) { result[field.name] = {} const numbersByLocale: Record[]> = {} @@ -520,7 +528,7 @@ export const traverseFields = >({ if (field.type === 'select' && field.hasMany) { if (Array.isArray(fieldData)) { - if (field.localized) { + if (isLocalized) { result[field.name] = fieldData.reduce((selectResult, row) => { if (typeof row.locale === 'string') { if (!selectResult[row.locale]) { @@ -542,7 +550,7 @@ export const traverseFields = >({ return result } - if (field.localized && Array.isArray(table._locales)) { + if (isLocalized && Array.isArray(table._locales)) { if (!table._locales.length && adapter.payload.config.localization) { adapter.payload.config.localization.localeCodes.forEach((_locale) => (table._locales as unknown[]).push({ _locale }), @@ -581,9 +589,9 @@ export const traverseFields = >({ const groupFieldPrefix = `${fieldPrefix || ''}${field.name}_` const groupData = {} const locale = table._locale as string - const refKey = field.localized && locale ? locale : field.name + const refKey = isLocalized && locale ? locale : field.name - if (field.localized && locale) { + if (isLocalized && locale) { delete table._locale } ref[refKey] = traverseFields>({ @@ -595,6 +603,7 @@ export const traverseFields = >({ fieldPrefix: groupFieldPrefix, fields: field.flattenedFields, numbers, + parentIsLocalized: parentIsLocalized || field.localized, path: `${sanitizedPath}${field.name}`, relationships, table, diff --git a/packages/drizzle/src/transform/write/array.ts b/packages/drizzle/src/transform/write/array.ts index e0071d4d1..b6d7fad48 100644 --- a/packages/drizzle/src/transform/write/array.ts +++ b/packages/drizzle/src/transform/write/array.ts @@ -1,5 +1,7 @@ import type { FlattenedArrayField } from 'payload' +import { fieldShouldBeLocalized } from 'payload/shared' + import type { DrizzleAdapter } from '../../types.js' import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js' @@ -18,6 +20,7 @@ type Args = { field: FlattenedArrayField locale?: string numbers: Record[] + parentIsLocalized: boolean path: string relationships: Record[] relationshipsToDelete: RelationshipToDelete[] @@ -42,6 +45,7 @@ export const transformArray = ({ field, locale, numbers, + parentIsLocalized, path, relationships, relationshipsToDelete, @@ -79,7 +83,7 @@ export const transformArray = ({ } } - if (field.localized) { + if (fieldShouldBeLocalized({ field, parentIsLocalized }) && locale) { newRow.row._locale = locale } @@ -100,6 +104,7 @@ export const transformArray = ({ insideArrayOrBlock: true, locales: newRow.locales, numbers, + parentIsLocalized: parentIsLocalized || field.localized, parentTableName: arrayTableName, path: `${path || ''}${field.name}.${i}.`, relationships, diff --git a/packages/drizzle/src/transform/write/blocks.ts b/packages/drizzle/src/transform/write/blocks.ts index 92e714d43..65589e6a3 100644 --- a/packages/drizzle/src/transform/write/blocks.ts +++ b/packages/drizzle/src/transform/write/blocks.ts @@ -1,5 +1,6 @@ import type { FlattenedBlock, FlattenedBlocksField } from 'payload' +import { fieldShouldBeLocalized } from 'payload/shared' import toSnakeCase from 'to-snake-case' import type { DrizzleAdapter } from '../../types.js' @@ -18,6 +19,7 @@ type Args = { field: FlattenedBlocksField locale?: string numbers: Record[] + parentIsLocalized: boolean path: string relationships: Record[] relationshipsToDelete: RelationshipToDelete[] @@ -40,6 +42,7 @@ export const transformBlocks = ({ field, locale, numbers, + parentIsLocalized, path, relationships, relationshipsToDelete, @@ -76,7 +79,7 @@ export const transformBlocks = ({ }, } - if (field.localized && locale) { + if (fieldShouldBeLocalized({ field, parentIsLocalized }) && locale) { newRow.row._locale = locale } if (withinArrayOrBlockLocale) { @@ -110,6 +113,7 @@ export const transformBlocks = ({ insideArrayOrBlock: true, locales: newRow.locales, numbers, + parentIsLocalized: parentIsLocalized || field.localized, parentTableName: blockTableName, path: `${path || ''}${field.name}.${i}.`, relationships, diff --git a/packages/drizzle/src/transform/write/index.ts b/packages/drizzle/src/transform/write/index.ts index faa03cf3b..2e45a3b12 100644 --- a/packages/drizzle/src/transform/write/index.ts +++ b/packages/drizzle/src/transform/write/index.ts @@ -9,6 +9,7 @@ type Args = { adapter: DrizzleAdapter data: Record fields: FlattenedField[] + parentIsLocalized?: boolean path?: string tableName: string } @@ -17,6 +18,7 @@ export const transformForWrite = ({ adapter, data, fields, + parentIsLocalized, path = '', tableName, }: Args): RowToInsert => { @@ -48,6 +50,7 @@ export const transformForWrite = ({ fields, locales: rowToInsert.locales, numbers: rowToInsert.numbers, + parentIsLocalized, parentTableName: tableName, path, relationships: rowToInsert.relationships, diff --git a/packages/drizzle/src/transform/write/traverseFields.ts b/packages/drizzle/src/transform/write/traverseFields.ts index ed011d82a..7fa8430cc 100644 --- a/packages/drizzle/src/transform/write/traverseFields.ts +++ b/packages/drizzle/src/transform/write/traverseFields.ts @@ -1,7 +1,7 @@ import type { FlattenedField } from 'payload' import { sql } from 'drizzle-orm' -import { fieldIsVirtual } from 'payload/shared' +import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared' import toSnakeCase from 'to-snake-case' import type { DrizzleAdapter } from '../../types.js' @@ -50,6 +50,7 @@ type Args = { [locale: string]: Record } numbers: Record[] + parentIsLocalized: boolean /** * This is the name of the parent table */ @@ -84,6 +85,7 @@ export const traverseFields = ({ insideArrayOrBlock = false, locales, numbers, + parentIsLocalized, parentTableName, path, relationships, @@ -110,6 +112,8 @@ export const traverseFields = ({ fieldName = `${fieldPrefix || ''}${field.name}` fieldData = data[field.name] + const isLocalized = fieldShouldBeLocalized({ field, parentIsLocalized }) + if (field.type === 'array') { const arrayTableName = adapter.tableNameMap.get(`${parentTableName}_${columnName}`) @@ -117,7 +121,7 @@ export const traverseFields = ({ arrays[arrayTableName] = [] } - if (field.localized) { + if (isLocalized) { if (typeof data[field.name] === 'object' && data[field.name] !== null) { Object.entries(data[field.name]).forEach(([localeKey, localeData]) => { if (Array.isArray(localeData)) { @@ -131,6 +135,7 @@ export const traverseFields = ({ field, locale: localeKey, numbers, + parentIsLocalized: parentIsLocalized || field.localized, path, relationships, relationshipsToDelete, @@ -153,6 +158,7 @@ export const traverseFields = ({ data: data[field.name], field, numbers, + parentIsLocalized: parentIsLocalized || field.localized, path, relationships, relationshipsToDelete, @@ -172,7 +178,7 @@ export const traverseFields = ({ blocksToDelete.add(toSnakeCase(typeof block === 'string' ? block : block.slug)) }) - if (field.localized) { + if (isLocalized) { if (typeof data[field.name] === 'object' && data[field.name] !== null) { Object.entries(data[field.name]).forEach(([localeKey, localeData]) => { if (Array.isArray(localeData)) { @@ -185,6 +191,7 @@ export const traverseFields = ({ field, locale: localeKey, numbers, + parentIsLocalized: parentIsLocalized || field.localized, path, relationships, relationshipsToDelete, @@ -204,6 +211,7 @@ export const traverseFields = ({ data: fieldData, field, numbers, + parentIsLocalized: parentIsLocalized || field.localized, path, relationships, relationshipsToDelete, @@ -218,7 +226,7 @@ export const traverseFields = ({ if (field.type === 'group' || field.type === 'tab') { if (typeof data[field.name] === 'object' && data[field.name] !== null) { - if (field.localized) { + if (isLocalized) { Object.entries(data[field.name]).forEach(([localeKey, localeData]) => { // preserve array ID if there is localeData._uuid = data.id || data._uuid @@ -238,6 +246,7 @@ export const traverseFields = ({ insideArrayOrBlock, locales, numbers, + parentIsLocalized: parentIsLocalized || field.localized, parentTableName, path: `${path || ''}${field.name}.`, relationships, @@ -267,6 +276,7 @@ export const traverseFields = ({ insideArrayOrBlock, locales, numbers, + parentIsLocalized: parentIsLocalized || field.localized, parentTableName, path: `${path || ''}${field.name}.`, relationships, @@ -286,7 +296,7 @@ export const traverseFields = ({ const relationshipPath = `${path || ''}${field.name}` if ( - field.localized && + isLocalized && (Array.isArray(field.relationTo) || ('hasMany' in field && field.hasMany)) ) { if (typeof fieldData === 'object') { @@ -329,14 +339,14 @@ export const traverseFields = ({ return } else { if ( - !field.localized && + !isLocalized && fieldData && typeof fieldData === 'object' && 'id' in fieldData && fieldData?.id ) { fieldData = fieldData.id - } else if (field.localized) { + } else if (isLocalized) { if (typeof fieldData === 'object') { Object.entries(fieldData).forEach(([localeKey, localeData]) => { if (typeof localeData === 'object') { @@ -355,7 +365,7 @@ export const traverseFields = ({ if (field.type === 'text' && field.hasMany) { const textPath = `${path || ''}${field.name}` - if (field.localized) { + if (isLocalized) { if (typeof fieldData === 'object') { Object.entries(fieldData).forEach(([localeKey, localeData]) => { if (Array.isArray(localeData)) { @@ -387,7 +397,7 @@ export const traverseFields = ({ if (field.type === 'number' && field.hasMany) { const numberPath = `${path || ''}${field.name}` - if (field.localized) { + if (isLocalized) { if (typeof fieldData === 'object') { Object.entries(fieldData).forEach(([localeKey, localeData]) => { if (Array.isArray(localeData)) { @@ -422,7 +432,7 @@ export const traverseFields = ({ selects[selectTableName] = [] } - if (field.localized) { + if (isLocalized) { if (typeof data[field.name] === 'object' && data[field.name] !== null) { Object.entries(data[field.name]).forEach(([localeKey, localeData]) => { if (Array.isArray(localeData)) { @@ -451,7 +461,7 @@ export const traverseFields = ({ const valuesToTransform: { localeKey?: string; ref: unknown; value: unknown }[] = [] - if (field.localized) { + if (isLocalized) { if (typeof fieldData === 'object' && fieldData !== null) { Object.entries(fieldData).forEach(([localeKey, localeData]) => { if (!locales[localeKey]) { diff --git a/packages/drizzle/src/utilities/hasLocalesTable.ts b/packages/drizzle/src/utilities/hasLocalesTable.ts index 11f6b6da5..80f39cdc4 100644 --- a/packages/drizzle/src/utilities/hasLocalesTable.ts +++ b/packages/drizzle/src/utilities/hasLocalesTable.ts @@ -1,21 +1,38 @@ import type { Field } from 'payload' -import { fieldAffectsData, fieldHasSubFields } from 'payload/shared' +import { fieldAffectsData, fieldHasSubFields, fieldShouldBeLocalized } from 'payload/shared' -export const hasLocalesTable = (fields: Field[]): boolean => { +export const hasLocalesTable = ({ + fields, + parentIsLocalized, +}: { + fields: Field[] + /** + * @todo make required in v4.0. Usually you'd wanna pass this in + */ + parentIsLocalized?: boolean +}): boolean => { return fields.some((field) => { // arrays always get a separate table if (field.type === 'array') { return false } - if (fieldAffectsData(field) && field.localized) { + if (fieldAffectsData(field) && fieldShouldBeLocalized({ field, parentIsLocalized })) { return true } if (fieldHasSubFields(field)) { - return hasLocalesTable(field.fields) + return hasLocalesTable({ + fields: field.fields, + parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized), + }) } if (field.type === 'tabs') { - return field.tabs.some((tab) => hasLocalesTable(tab.fields)) + return field.tabs.some((tab) => + hasLocalesTable({ + fields: tab.fields, + parentIsLocalized: parentIsLocalized || tab.localized, + }), + ) } return false }) diff --git a/packages/drizzle/src/utilities/validateExistingBlockIsIdentical.ts b/packages/drizzle/src/utilities/validateExistingBlockIsIdentical.ts index 32309020a..033ab38d6 100644 --- a/packages/drizzle/src/utilities/validateExistingBlockIsIdentical.ts +++ b/packages/drizzle/src/utilities/validateExistingBlockIsIdentical.ts @@ -1,22 +1,33 @@ import type { Block, Field } from 'payload' import { InvalidConfiguration } from 'payload' -import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/shared' +import { + fieldAffectsData, + fieldHasSubFields, + fieldShouldBeLocalized, + tabHasName, +} from 'payload/shared' import type { RawTable } from '../types.js' type Args = { block: Block localized: boolean + /** + * @todo make required in v4.0. Usually you'd wanna pass this in + */ + parentIsLocalized?: boolean rootTableName: string table: RawTable tableLocales?: RawTable } -const getFlattenedFieldNames = ( - fields: Field[], - prefix: string = '', -): { localized?: boolean; name: string }[] => { +const getFlattenedFieldNames = (args: { + fields: Field[] + parentIsLocalized: boolean + prefix?: string +}): { localized?: boolean; name: string }[] => { + const { fields, parentIsLocalized, prefix = '' } = args return fields.reduce((fieldsToUse, field) => { let fieldPrefix = prefix @@ -29,7 +40,14 @@ const getFlattenedFieldNames = ( if (fieldHasSubFields(field)) { fieldPrefix = 'name' in field ? `${prefix}${field.name}_` : prefix - return [...fieldsToUse, ...getFlattenedFieldNames(field.fields, fieldPrefix)] + return [ + ...fieldsToUse, + ...getFlattenedFieldNames({ + fields: field.fields, + parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized), + prefix: fieldPrefix, + }), + ] } if (field.type === 'tabs') { @@ -41,7 +59,11 @@ const getFlattenedFieldNames = ( ...tabFields, ...(tabHasName(tab) ? [{ ...tab, type: 'tab' }] - : getFlattenedFieldNames(tab.fields, fieldPrefix)), + : getFlattenedFieldNames({ + fields: tab.fields, + parentIsLocalized: parentIsLocalized || tab.localized, + prefix: fieldPrefix, + })), ] }, []), ] @@ -52,7 +74,7 @@ const getFlattenedFieldNames = ( ...fieldsToUse, { name: `${fieldPrefix}${field.name}`, - localized: field.localized, + localized: fieldShouldBeLocalized({ field, parentIsLocalized }), }, ] } @@ -64,11 +86,15 @@ const getFlattenedFieldNames = ( export const validateExistingBlockIsIdentical = ({ block, localized, + parentIsLocalized, rootTableName, table, tableLocales, }: Args): void => { - const fieldNames = getFlattenedFieldNames(block.fields) + const fieldNames = getFlattenedFieldNames({ + fields: block.fields, + parentIsLocalized: parentIsLocalized || localized, + }) const missingField = // ensure every field from the config is in the matching table diff --git a/packages/graphql/src/schema/buildMutationInputType.ts b/packages/graphql/src/schema/buildMutationInputType.ts index f4243485f..6e026e734 100644 --- a/packages/graphql/src/schema/buildMutationInputType.ts +++ b/packages/graphql/src/schema/buildMutationInputType.ts @@ -75,6 +75,7 @@ type BuildMutationInputTypeArgs = { forceNullable?: boolean graphqlResult: GraphQLInfo name: string + parentIsLocalized: boolean parentName: string } @@ -84,6 +85,7 @@ export function buildMutationInputType({ fields, forceNullable = false, graphqlResult, + parentIsLocalized, parentName, }: BuildMutationInputTypeArgs): GraphQLInputObjectType | null { const fieldToSchemaMap = { @@ -94,6 +96,7 @@ export function buildMutationInputType({ config, fields: field.fields, graphqlResult, + parentIsLocalized: parentIsLocalized || field.localized, parentName: fullName, }) @@ -101,7 +104,7 @@ export function buildMutationInputType({ return inputObjectTypeConfig } - type = new GraphQLList(withNullableType(field, type, forceNullable)) + type = new GraphQLList(withNullableType({ type, field, forceNullable, parentIsLocalized })) return { ...inputObjectTypeConfig, [field.name]: { type }, @@ -117,7 +120,9 @@ export function buildMutationInputType({ }), code: (inputObjectTypeConfig: InputObjectTypeConfig, field: CodeField) => ({ ...inputObjectTypeConfig, - [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + [field.name]: { + type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }), + }, }), collapsible: (inputObjectTypeConfig: InputObjectTypeConfig, field: CollapsibleField) => field.fields.reduce((acc, subField: CollapsibleField) => { @@ -129,11 +134,15 @@ export function buildMutationInputType({ }, inputObjectTypeConfig), date: (inputObjectTypeConfig: InputObjectTypeConfig, field: DateField) => ({ ...inputObjectTypeConfig, - [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + [field.name]: { + type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }), + }, }), email: (inputObjectTypeConfig: InputObjectTypeConfig, field: EmailField) => ({ ...inputObjectTypeConfig, - [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + [field.name]: { + type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }), + }, }), group: (inputObjectTypeConfig: InputObjectTypeConfig, field: GroupField) => { const requiresAtLeastOneField = groupOrTabHasRequiredSubfield(field) @@ -143,6 +152,7 @@ export function buildMutationInputType({ config, fields: field.fields, graphqlResult, + parentIsLocalized: parentIsLocalized || field.localized, parentName: fullName, }) @@ -160,28 +170,40 @@ export function buildMutationInputType({ }, json: (inputObjectTypeConfig: InputObjectTypeConfig, field: JSONField) => ({ ...inputObjectTypeConfig, - [field.name]: { type: withNullableType(field, GraphQLJSON, forceNullable) }, + [field.name]: { + type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }), + }, }), number: (inputObjectTypeConfig: InputObjectTypeConfig, field: NumberField) => { const type = field.name === 'id' ? GraphQLInt : GraphQLFloat return { ...inputObjectTypeConfig, [field.name]: { - type: withNullableType( + type: withNullableType({ + type: field.hasMany === true ? new GraphQLList(type) : type, field, - field.hasMany === true ? new GraphQLList(type) : type, forceNullable, - ), + parentIsLocalized, + }), }, } }, point: (inputObjectTypeConfig: InputObjectTypeConfig, field: PointField) => ({ ...inputObjectTypeConfig, - [field.name]: { type: withNullableType(field, new GraphQLList(GraphQLFloat), forceNullable) }, + [field.name]: { + type: withNullableType({ + type: new GraphQLList(GraphQLFloat), + field, + forceNullable, + parentIsLocalized, + }), + }, }), radio: (inputObjectTypeConfig: InputObjectTypeConfig, field: RadioField) => ({ ...inputObjectTypeConfig, - [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + [field.name]: { + type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }), + }, }), relationship: (inputObjectTypeConfig: InputObjectTypeConfig, field: RelationshipField) => { const { relationTo } = field @@ -230,7 +252,9 @@ export function buildMutationInputType({ }, richText: (inputObjectTypeConfig: InputObjectTypeConfig, field: RichTextField) => ({ ...inputObjectTypeConfig, - [field.name]: { type: withNullableType(field, GraphQLJSON, forceNullable) }, + [field.name]: { + type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }), + }, }), row: (inputObjectTypeConfig: InputObjectTypeConfig, field: RowField) => field.fields.reduce((acc, subField: Field) => { @@ -264,7 +288,7 @@ export function buildMutationInputType({ }) type = field.hasMany ? new GraphQLList(type) : type - type = withNullableType(field, type, forceNullable) + type = withNullableType({ type, field, forceNullable, parentIsLocalized }) return { ...inputObjectTypeConfig, @@ -281,6 +305,7 @@ export function buildMutationInputType({ config, fields: tab.fields, graphqlResult, + parentIsLocalized: parentIsLocalized || tab.localized, parentName: fullName, }) @@ -312,16 +337,19 @@ export function buildMutationInputType({ text: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextField) => ({ ...inputObjectTypeConfig, [field.name]: { - type: withNullableType( + type: withNullableType({ + type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString, field, - field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString, forceNullable, - ), + parentIsLocalized, + }), }, }), textarea: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextareaField) => ({ ...inputObjectTypeConfig, - [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + [field.name]: { + type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }), + }, }), upload: (inputObjectTypeConfig: InputObjectTypeConfig, field: UploadField) => { const { relationTo } = field diff --git a/packages/graphql/src/schema/buildObjectType.ts b/packages/graphql/src/schema/buildObjectType.ts index 2b88c1ce3..6beca8f91 100644 --- a/packages/graphql/src/schema/buildObjectType.ts +++ b/packages/graphql/src/schema/buildObjectType.ts @@ -62,6 +62,7 @@ type Args = { forceNullable?: boolean graphqlResult: GraphQLInfo name: string + parentIsLocalized?: boolean parentName: string } @@ -72,6 +73,7 @@ export function buildObjectType({ fields, forceNullable, graphqlResult, + parentIsLocalized, parentName, }: Args): GraphQLObjectType { const fieldToSchemaMap = { @@ -84,8 +86,9 @@ export function buildObjectType({ name: interfaceName, config, fields: field.fields, - forceNullable: isFieldNullable(field, forceNullable), + forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }), graphqlResult, + parentIsLocalized: field.localized || parentIsLocalized, parentName: interfaceName, }) @@ -104,7 +107,7 @@ export function buildObjectType({ return { ...objectTypeConfig, - [field.name]: { type: withNullableType(field, arrayType) }, + [field.name]: { type: withNullableType({ type: arrayType, field, parentIsLocalized }) }, } }, blocks: (objectTypeConfig: ObjectTypeConfig, field: BlocksField) => { @@ -132,6 +135,7 @@ export function buildObjectType({ ], forceNullable, graphqlResult, + parentIsLocalized, parentName: interfaceName, }) @@ -165,16 +169,20 @@ export function buildObjectType({ return { ...objectTypeConfig, - [field.name]: { type: withNullableType(field, type) }, + [field.name]: { type: withNullableType({ type, field, parentIsLocalized }) }, } }, checkbox: (objectTypeConfig: ObjectTypeConfig, field: CheckboxField) => ({ ...objectTypeConfig, - [field.name]: { type: withNullableType(field, GraphQLBoolean, forceNullable) }, + [field.name]: { + type: withNullableType({ type: GraphQLBoolean, field, forceNullable, parentIsLocalized }), + }, }), code: (objectTypeConfig: ObjectTypeConfig, field: CodeField) => ({ ...objectTypeConfig, - [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + [field.name]: { + type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }), + }, }), collapsible: (objectTypeConfig: ObjectTypeConfig, field: CollapsibleField) => field.fields.reduce((objectTypeConfigWithCollapsibleFields, subField) => { @@ -186,11 +194,20 @@ export function buildObjectType({ }, objectTypeConfig), date: (objectTypeConfig: ObjectTypeConfig, field: DateField) => ({ ...objectTypeConfig, - [field.name]: { type: withNullableType(field, DateTimeResolver, forceNullable) }, + [field.name]: { + type: withNullableType({ type: DateTimeResolver, field, forceNullable, parentIsLocalized }), + }, }), email: (objectTypeConfig: ObjectTypeConfig, field: EmailField) => ({ ...objectTypeConfig, - [field.name]: { type: withNullableType(field, EmailAddressResolver, forceNullable) }, + [field.name]: { + type: withNullableType({ + type: EmailAddressResolver, + field, + forceNullable, + parentIsLocalized, + }), + }, }), group: (objectTypeConfig: ObjectTypeConfig, field: GroupField) => { const interfaceName = @@ -201,8 +218,9 @@ export function buildObjectType({ name: interfaceName, config, fields: field.fields, - forceNullable: isFieldNullable(field, forceNullable), + forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }), graphqlResult, + parentIsLocalized: field.localized || parentIsLocalized, parentName: interfaceName, }) @@ -286,42 +304,47 @@ export function buildObjectType({ }, json: (objectTypeConfig: ObjectTypeConfig, field: JSONField) => ({ ...objectTypeConfig, - [field.name]: { type: withNullableType(field, GraphQLJSON, forceNullable) }, + [field.name]: { + type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }), + }, }), number: (objectTypeConfig: ObjectTypeConfig, field: NumberField) => { const type = field?.name === 'id' ? GraphQLInt : GraphQLFloat return { ...objectTypeConfig, [field.name]: { - type: withNullableType( + type: withNullableType({ + type: field?.hasMany === true ? new GraphQLList(type) : type, field, - field?.hasMany === true ? new GraphQLList(type) : type, forceNullable, - ), + parentIsLocalized, + }), }, } }, point: (objectTypeConfig: ObjectTypeConfig, field: PointField) => ({ ...objectTypeConfig, [field.name]: { - type: withNullableType( + type: withNullableType({ + type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)), field, - new GraphQLList(new GraphQLNonNull(GraphQLFloat)), forceNullable, - ), + parentIsLocalized, + }), }, }), radio: (objectTypeConfig: ObjectTypeConfig, field: RadioField) => ({ ...objectTypeConfig, [field.name]: { - type: withNullableType( - field, - new GraphQLEnumType({ + type: withNullableType({ + type: new GraphQLEnumType({ name: combineParentName(parentName, field.name), values: formatOptions(field), }), + field, forceNullable, - ), + parentIsLocalized, + }), }, }), relationship: (objectTypeConfig: ObjectTypeConfig, field: RelationshipField) => { @@ -420,11 +443,12 @@ export function buildObjectType({ } const relationship = { - type: withNullableType( + type: withNullableType({ + type: hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type, field, - hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type, forceNullable, - ), + parentIsLocalized, + }), args: relationshipArgs, extensions: { complexity: @@ -550,7 +574,7 @@ export function buildObjectType({ richText: (objectTypeConfig: ObjectTypeConfig, field: RichTextField) => ({ ...objectTypeConfig, [field.name]: { - type: withNullableType(field, GraphQLJSON, forceNullable), + type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }), args: { depth: { type: GraphQLInt, @@ -591,6 +615,7 @@ export function buildObjectType({ findMany: false, flattenLocales: false, overrideAccess: false, + parentIsLocalized, populationPromises, req: context.req, showHiddenFields: false, @@ -621,7 +646,7 @@ export function buildObjectType({ }) type = field.hasMany ? new GraphQLList(new GraphQLNonNull(type)) : type - type = withNullableType(field, type, forceNullable) + type = withNullableType({ type, field, forceNullable, parentIsLocalized }) return { ...objectTypeConfig, @@ -641,6 +666,7 @@ export function buildObjectType({ fields: tab.fields, forceNullable, graphqlResult, + parentIsLocalized: tab.localized || parentIsLocalized, parentName: interfaceName, }) @@ -681,16 +707,19 @@ export function buildObjectType({ text: (objectTypeConfig: ObjectTypeConfig, field: TextField) => ({ ...objectTypeConfig, [field.name]: { - type: withNullableType( + type: withNullableType({ + type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString, field, - field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString, forceNullable, - ), + parentIsLocalized, + }), }, }), textarea: (objectTypeConfig: ObjectTypeConfig, field: TextareaField) => ({ ...objectTypeConfig, - [field.name]: { type: withNullableType(field, GraphQLString, forceNullable) }, + [field.name]: { + type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }), + }, }), upload: (objectTypeConfig: ObjectTypeConfig, field: UploadField) => { const { relationTo } = field @@ -775,11 +804,12 @@ export function buildObjectType({ } const relationship = { - type: withNullableType( + type: withNullableType({ + type: hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type, field, - hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type, forceNullable, - ), + parentIsLocalized, + }), args: relationshipArgs, extensions: { complexity: diff --git a/packages/graphql/src/schema/initCollections.ts b/packages/graphql/src/schema/initCollections.ts index 3194b074e..e695ef3ec 100644 --- a/packages/graphql/src/schema/initCollections.ts +++ b/packages/graphql/src/schema/initCollections.ts @@ -150,6 +150,7 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ config, fields: mutationInputFields, graphqlResult, + parentIsLocalized: false, parentName: singularName, }) if (createMutationInputType) { @@ -164,6 +165,7 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ ), forceNullable: true, graphqlResult, + parentIsLocalized: false, parentName: `${singularName}Update`, }) if (updateMutationInputType) { diff --git a/packages/graphql/src/schema/initGlobals.ts b/packages/graphql/src/schema/initGlobals.ts index 37794582e..5ca20b065 100644 --- a/packages/graphql/src/schema/initGlobals.ts +++ b/packages/graphql/src/schema/initGlobals.ts @@ -45,6 +45,7 @@ export function initGlobals({ config, graphqlResult }: InitGlobalsGraphQLArgs): config, fields, graphqlResult, + parentIsLocalized: false, parentName: formattedName, }) graphqlResult.globals.graphQL[slug] = { diff --git a/packages/graphql/src/schema/isFieldNullable.ts b/packages/graphql/src/schema/isFieldNullable.ts index dd2ba85c6..88ad61568 100644 --- a/packages/graphql/src/schema/isFieldNullable.ts +++ b/packages/graphql/src/schema/isFieldNullable.ts @@ -2,15 +2,23 @@ import type { FieldAffectingData } from 'payload' import { fieldAffectsData } from 'payload/shared' -export const isFieldNullable = (field: FieldAffectingData, force: boolean): boolean => { +export const isFieldNullable = ({ + field, + forceNullable, + parentIsLocalized, +}: { + field: FieldAffectingData + forceNullable: boolean + parentIsLocalized: boolean +}): boolean => { const hasReadAccessControl = field.access && field.access.read const condition = field.admin && field.admin.condition return !( - force && + forceNullable && fieldAffectsData(field) && 'required' in field && field.required && - !field.localized && + (!field.localized || parentIsLocalized) && !condition && !hasReadAccessControl ) diff --git a/packages/graphql/src/schema/withNullableType.ts b/packages/graphql/src/schema/withNullableType.ts index 5cec0a233..b77f8f229 100644 --- a/packages/graphql/src/schema/withNullableType.ts +++ b/packages/graphql/src/schema/withNullableType.ts @@ -3,11 +3,17 @@ import type { FieldAffectingData } from 'payload' import { GraphQLNonNull } from 'graphql' -export const withNullableType = ( - field: FieldAffectingData, - type: GraphQLType, - forceNullable = false, -): GraphQLType => { +export const withNullableType = ({ + type, + field, + forceNullable, + parentIsLocalized, +}: { + field: FieldAffectingData + forceNullable?: boolean + parentIsLocalized: boolean + type: GraphQLType +}): GraphQLType => { const hasReadAccessControl = field.access && field.access.read const condition = field.admin && field.admin.condition const isTimestamp = field.name === 'createdAt' || field.name === 'updatedAt' @@ -16,7 +22,7 @@ export const withNullableType = ( !forceNullable && 'required' in field && field.required && - !field.localized && + (!field.localized || parentIsLocalized) && !condition && !hasReadAccessControl && !isTimestamp diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx index 81f58b659..1e642fb5c 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/DiffCollapser/index.tsx @@ -22,6 +22,7 @@ type Props = isIterable?: false label: React.ReactNode locales: string[] | undefined + parentIsLocalized: boolean version: unknown } | { @@ -34,6 +35,7 @@ type Props = isIterable: true label: React.ReactNode locales: string[] | undefined + parentIsLocalized: boolean version: unknown } @@ -46,6 +48,7 @@ export const DiffCollapser: React.FC = ({ isIterable = false, label, locales, + parentIsLocalized, version, }) => { const { t } = useTranslation() @@ -74,6 +77,7 @@ export const DiffCollapser: React.FC = ({ config, field, locales, + parentIsLocalized, versionRows, }) } else { @@ -82,6 +86,7 @@ export const DiffCollapser: React.FC = ({ config, fields, locales, + parentIsLocalized, version, }) } diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/buildVersionFields.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/buildVersionFields.tsx index b6b54686f..7accde08a 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/buildVersionFields.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/buildVersionFields.tsx @@ -17,7 +17,7 @@ import type { DiffMethod } from 'react-diff-viewer-continued' import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' import { dequal } from 'dequal/lite' -import { fieldIsID, getUniqueListBy, tabHasName } from 'payload/shared' +import { fieldIsID, fieldShouldBeLocalized, getUniqueListBy, tabHasName } from 'payload/shared' import { diffMethods } from './fields/diffMethods.js' import { diffComponents } from './fields/index.js' @@ -39,6 +39,7 @@ export type BuildVersionFieldsArgs = { i18n: I18nClient modifiedOnly: boolean parentIndexPath: string + parentIsLocalized: boolean parentPath: string parentSchemaPath: string req: PayloadRequest @@ -63,6 +64,7 @@ export const buildVersionFields = ({ i18n, modifiedOnly, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, req, @@ -103,7 +105,8 @@ export const buildVersionFields = ({ } const versionField: VersionField = {} - const isLocalized = 'localized' in field && field.localized + const isLocalized = fieldShouldBeLocalized({ field, parentIsLocalized }) + const fieldName: null | string = 'name' in field ? field.name : null const versionValue = fieldName ? versionSiblingData?.[fieldName] : versionSiblingData @@ -126,6 +129,7 @@ export const buildVersionFields = ({ indexPath, locale, modifiedOnly, + parentIsLocalized: true, parentPath, parentSchemaPath, path, @@ -150,6 +154,7 @@ export const buildVersionFields = ({ i18n, indexPath, modifiedOnly, + parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized), parentPath, parentSchemaPath, path, @@ -184,6 +189,7 @@ const buildVersionField = ({ indexPath, locale, modifiedOnly, + parentIsLocalized, parentPath, parentSchemaPath, path, @@ -198,6 +204,7 @@ const buildVersionField = ({ indexPath: string locale?: string modifiedOnly?: boolean + parentIsLocalized: boolean path: string schemaPath: string versionValue: unknown @@ -272,6 +279,7 @@ const buildVersionField = ({ i18n, modifiedOnly, parentIndexPath: isNamedTab ? '' : tabIndexPath, + parentIsLocalized: parentIsLocalized || tab.localized, parentPath: tabPath, parentSchemaPath: tabSchemaPath, req, @@ -300,6 +308,7 @@ const buildVersionField = ({ i18n, modifiedOnly, parentIndexPath: 'name' in field ? '' : indexPath, + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + i, parentSchemaPath: schemaPath, req, @@ -318,6 +327,7 @@ const buildVersionField = ({ i18n, modifiedOnly, parentIndexPath: 'name' in field ? '' : indexPath, + parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized), parentPath: path, parentSchemaPath: schemaPath, req, @@ -374,6 +384,7 @@ const buildVersionField = ({ i18n, modifiedOnly, parentIndexPath: 'name' in field ? '' : indexPath, + parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized), parentPath: path + '.' + i, parentSchemaPath: schemaPath + '.' + versionBlock.slug, req, @@ -392,6 +403,7 @@ const buildVersionField = ({ diffMethod, field: clientField, fieldPermissions: subFieldPermissions, + parentIsLocalized, versionValue, } diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Collapsible/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Collapsible/index.tsx index 30349405e..fe6c20412 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Collapsible/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Collapsible/index.tsx @@ -15,6 +15,7 @@ export const Collapsible: CollapsibleFieldDiffClientComponent = ({ baseVersionField, comparisonValue, field, + parentIsLocalized, versionValue, }) => { const { i18n } = useTranslation() @@ -35,6 +36,7 @@ export const Collapsible: CollapsibleFieldDiffClientComponent = ({ typeof field.label !== 'function' && {getTranslation(field.label, i18n)} } locales={selectedLocales} + parentIsLocalized={parentIsLocalized || field.localized} version={versionValue} > diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.tsx index 136406a06..5739fb64d 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Group/index.tsx @@ -19,6 +19,7 @@ export const Group: GroupFieldDiffClientComponent = ({ comparisonValue, field, locale, + parentIsLocalized, versionValue, }) => { const { i18n } = useTranslation() @@ -40,6 +41,7 @@ export const Group: GroupFieldDiffClientComponent = ({ ) } locales={selectedLocales} + parentIsLocalized={parentIsLocalized || field.localized} version={versionValue} > diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.tsx index 24e8cdd05..678479ca5 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Iterable/index.tsx @@ -22,6 +22,7 @@ export const Iterable: React.FC = ({ comparisonValue, field, locale, + parentIsLocalized, versionValue, }) => { const { i18n } = useTranslation() @@ -53,6 +54,7 @@ export const Iterable: React.FC = ({ ) } locales={selectedLocales} + parentIsLocalized={parentIsLocalized} version={versionValue} > {maxRows > 0 && ( @@ -80,6 +82,7 @@ export const Iterable: React.FC = ({ fields={fields} label={rowLabel} locales={selectedLocales} + parentIsLocalized={parentIsLocalized || field.localized} version={versionRow} > diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx index 75dd19582..fb71f64fd 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Relationship/index.tsx @@ -1,13 +1,14 @@ 'use client' import type { ClientCollectionConfig, + ClientConfig, ClientField, RelationshipFieldDiffClientComponent, } from 'payload' import { getTranslation } from '@payloadcms/translations' import { useConfig, useTranslation } from '@payloadcms/ui' -import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared' +import { fieldAffectsData, fieldIsPresentationalOnly, fieldShouldBeLocalized } from 'payload/shared' import React from 'react' import ReactDiffViewer from 'react-diff-viewer-continued' @@ -24,10 +25,12 @@ const generateLabelFromValue = ( field: ClientField, locale: string, value: { relationTo: string; value: RelationshipValue } | RelationshipValue, + config: ClientConfig, + parentIsLocalized: boolean, ): string => { if (Array.isArray(value)) { return value - .map((v) => generateLabelFromValue(collections, field, locale, v)) + .map((v) => generateLabelFromValue(collections, field, locale, v, config, parentIsLocalized)) .filter(Boolean) // Filters out any undefined or empty values .join(', ') } @@ -65,7 +68,7 @@ const generateLabelFromValue = ( let titleFieldIsLocalized = false if (useAsTitleField && fieldAffectsData(useAsTitleField)) { - titleFieldIsLocalized = useAsTitleField.localized + titleFieldIsLocalized = fieldShouldBeLocalized({ field: useAsTitleField, parentIsLocalized }) } if (typeof relatedDoc?.[useAsTitle] !== 'undefined') { @@ -102,9 +105,11 @@ export const Relationship: RelationshipFieldDiffClientComponent = ({ comparisonValue, field, locale, + parentIsLocalized, versionValue, }) => { const { i18n } = useTranslation() + const { config } = useConfig() const placeholder = `[${i18n.t('general:noValue')}]` @@ -119,11 +124,20 @@ export const Relationship: RelationshipFieldDiffClientComponent = ({ if ('hasMany' in field && field.hasMany && Array.isArray(versionValue)) { versionToRender = versionValue - .map((val) => generateLabelFromValue(collections, field, locale, val)) + .map((val) => + generateLabelFromValue(collections, field, locale, val, config, parentIsLocalized), + ) .join(', ') || placeholder } else { versionToRender = - generateLabelFromValue(collections, field, locale, versionValue) || placeholder + generateLabelFromValue( + collections, + field, + locale, + versionValue, + config, + parentIsLocalized, + ) || placeholder } } @@ -131,11 +145,20 @@ export const Relationship: RelationshipFieldDiffClientComponent = ({ if ('hasMany' in field && field.hasMany && Array.isArray(comparisonValue)) { comparisonToRender = comparisonValue - .map((val) => generateLabelFromValue(collections, field, locale, val)) + .map((val) => + generateLabelFromValue(collections, field, locale, val, config, parentIsLocalized), + ) .join(', ') || placeholder } else { comparisonToRender = - generateLabelFromValue(collections, field, locale, comparisonValue) || placeholder + generateLabelFromValue( + collections, + field, + locale, + comparisonValue, + config, + parentIsLocalized, + ) || placeholder } } diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.tsx index 00cc020b9..b8a437e4d 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/fields/Tabs/index.tsx @@ -79,7 +79,14 @@ type TabProps = { tab: VersionTab } & FieldDiffClientProps -const Tab: React.FC = ({ comparisonValue, fieldTab, locale, tab, versionValue }) => { +const Tab: React.FC = ({ + comparisonValue, + fieldTab, + locale, + parentIsLocalized, + tab, + versionValue, +}) => { const { i18n } = useTranslation() const { selectedLocales } = useSelectedLocales() @@ -102,6 +109,7 @@ const Tab: React.FC = ({ comparisonValue, fieldTab, locale, tab, versi ) } locales={selectedLocales} + parentIsLocalized={parentIsLocalized || fieldTab.localized} version={versionValue} > diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/utilities/countChangedFields.ts b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/countChangedFields.ts index 3470e2549..211837168 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/utilities/countChangedFields.ts +++ b/packages/next/src/views/Version/RenderFieldsToDiff/utilities/countChangedFields.ts @@ -1,5 +1,7 @@ import type { ArrayFieldClient, BlocksFieldClient, ClientConfig, ClientField } from 'payload' +import { fieldShouldBeLocalized } from 'payload/shared' + import { fieldHasChanges } from './fieldHasChanges.js' import { getFieldsForRowComparison } from './getFieldsForRowComparison.js' @@ -8,6 +10,7 @@ type Args = { config: ClientConfig fields: ClientField[] locales: string[] | undefined + parentIsLocalized: boolean version: unknown } @@ -15,7 +18,14 @@ type Args = { * Recursively counts the number of changed fields between comparison and * version data for a given set of fields. */ -export function countChangedFields({ comparison, config, fields, locales, version }: Args) { +export function countChangedFields({ + comparison, + config, + fields, + locales, + parentIsLocalized, + version, +}: Args) { let count = 0 fields.forEach((field) => { @@ -29,7 +39,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio // count the number of changed fields in each. case 'array': case 'blocks': { - if (locales && field.localized) { + if (locales && fieldShouldBeLocalized({ field, parentIsLocalized })) { locales.forEach((locale) => { const comparisonRows = comparison?.[field.name]?.[locale] ?? [] const versionRows = version?.[field.name]?.[locale] ?? [] @@ -38,13 +48,21 @@ export function countChangedFields({ comparison, config, fields, locales, versio config, field, locales, + parentIsLocalized: parentIsLocalized || field.localized, versionRows, }) }) } else { const comparisonRows = comparison?.[field.name] ?? [] const versionRows = version?.[field.name] ?? [] - count += countChangedFieldsInRows({ comparisonRows, config, field, locales, versionRows }) + count += countChangedFieldsInRows({ + comparisonRows, + config, + field, + locales, + parentIsLocalized: parentIsLocalized || field.localized, + versionRows, + }) } break } @@ -66,7 +84,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio case 'textarea': case 'upload': { // Fields that have a name and contain data. We can just check if the data has changed. - if (locales && field.localized) { + if (locales && fieldShouldBeLocalized({ field, parentIsLocalized })) { locales.forEach((locale) => { if ( fieldHasChanges(version?.[field.name]?.[locale], comparison?.[field.name]?.[locale]) @@ -87,6 +105,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio config, fields: field.fields, locales, + parentIsLocalized: parentIsLocalized || field.localized, version, }) @@ -95,13 +114,14 @@ export function countChangedFields({ comparison, config, fields, locales, versio // Fields that have nested fields and nest their fields' data. case 'group': { - if (locales && field.localized) { + if (locales && fieldShouldBeLocalized({ field, parentIsLocalized })) { locales.forEach((locale) => { count += countChangedFields({ comparison: comparison?.[field.name]?.[locale], config, fields: field.fields, locales, + parentIsLocalized: parentIsLocalized || field.localized, version: version?.[field.name]?.[locale], }) }) @@ -111,6 +131,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio config, fields: field.fields, locales, + parentIsLocalized: parentIsLocalized || field.localized, version: version?.[field.name], }) } @@ -129,6 +150,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio config, fields: tab.fields, locales, + parentIsLocalized: parentIsLocalized || tab.localized, version: version?.[tab.name]?.[locale], }) }) @@ -139,6 +161,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio config, fields: tab.fields, locales, + parentIsLocalized: parentIsLocalized || tab.localized, version: version?.[tab.name], }) } else { @@ -148,6 +171,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio config, fields: tab.fields, locales, + parentIsLocalized: parentIsLocalized || tab.localized, version, }) } @@ -176,6 +200,7 @@ type countChangedFieldsInRowsArgs = { config: ClientConfig field: ArrayFieldClient | BlocksFieldClient locales: string[] | undefined + parentIsLocalized: boolean versionRows: unknown[] } @@ -184,6 +209,7 @@ export function countChangedFieldsInRows({ config, field, locales, + parentIsLocalized, versionRows = [], }: countChangedFieldsInRowsArgs) { let count = 0 @@ -207,6 +233,7 @@ export function countChangedFieldsInRows({ config, fields: rowFields, locales, + parentIsLocalized: parentIsLocalized || field.localized, version: versionRow, }) diff --git a/packages/next/src/views/Version/index.tsx b/packages/next/src/views/Version/index.tsx index deda5cf7e..4cb614dbc 100644 --- a/packages/next/src/views/Version/index.tsx +++ b/packages/next/src/views/Version/index.tsx @@ -228,6 +228,7 @@ export async function VersionView(props: DocumentViewServerProps) { i18n, modifiedOnly, parentIndexPath: '', + parentIsLocalized: false, parentPath: '', parentSchemaPath: '', req, diff --git a/packages/payload/src/admin/RichText.ts b/packages/payload/src/admin/RichText.ts index 9d4bac05c..6812ba569 100644 --- a/packages/payload/src/admin/RichText.ts +++ b/packages/payload/src/admin/RichText.ts @@ -120,11 +120,12 @@ export type BaseRichTextHookArgs< indexPath: number[] /** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */ originalDoc?: TData + parentIsLocalized: boolean + /** * The path of the field, e.g. ["group", "myArray", 1, "textField"]. The path is the schemaPath but with indexes and would be used in the context of field data, not field schemas. */ path: (number | string)[] - /** The Express request object. It is mocked for Local API operations. */ req: PayloadRequest /** @@ -208,6 +209,7 @@ type RichTextAdapterBase< findMany: boolean flattenLocales: boolean overrideAccess?: boolean + parentIsLocalized: boolean populateArg?: PopulateType populationPromises: Promise[] req: PayloadRequest diff --git a/packages/payload/src/admin/forms/Diff.ts b/packages/payload/src/admin/forms/Diff.ts index b58b1a140..89253a58f 100644 --- a/packages/payload/src/admin/forms/Diff.ts +++ b/packages/payload/src/admin/forms/Diff.ts @@ -61,6 +61,7 @@ export type FieldDiffClientProps => { const sanitizedConfig = { ...configToSanitize } + if (configToSanitize?.compatibility?.allowLocalizedWithinLocalized) { + process.env.NEXT_PUBLIC_PAYLOAD_COMPATIBILITY_allowLocalizedWithinLocalized = 'true' + } + // default logging level will be 'error' if not provided sanitizedConfig.loggingLevels = { Forbidden: 'info', diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index 7b8bd0924..41aa07e0a 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -936,6 +936,8 @@ export type Config = { * 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. + * + * @todo Remove in v4 */ allowLocalizedWithinLocalized: true } diff --git a/packages/payload/src/database/getLocalizedPaths.ts b/packages/payload/src/database/getLocalizedPaths.ts index b9d6f7f5a..07a9e9ea6 100644 --- a/packages/payload/src/database/getLocalizedPaths.ts +++ b/packages/payload/src/database/getLocalizedPaths.ts @@ -1,8 +1,14 @@ -// @ts-strict-ignore -import type { Field, FlattenedBlock, FlattenedField } from '../fields/config/types.js' import type { Payload } from '../index.js' import type { PathToQuery } from './queryValidation/types.js' +// @ts-strict-ignore +import { + type Field, + fieldShouldBeLocalized, + type FlattenedBlock, + type FlattenedField, +} from '../fields/config/types.js' + export function getLocalizedPaths({ collectionSlug, fields, @@ -10,6 +16,7 @@ export function getLocalizedPaths({ incomingPath, locale, overrideAccess = false, + parentIsLocalized, payload, }: { collectionSlug?: string @@ -18,6 +25,10 @@ export function getLocalizedPaths({ incomingPath: string locale?: string overrideAccess?: boolean + /** + * @todo make required in v4.0. Usually, you'd wanna pass this through + */ + parentIsLocalized?: boolean payload: Payload }): PathToQuery[] { const pathSegments = incomingPath.split('.') @@ -31,6 +42,7 @@ export function getLocalizedPaths({ fields, globalSlug, invalid: false, + parentIsLocalized, path: '', }, ] @@ -45,6 +57,7 @@ export function getLocalizedPaths({ let currentPath = path ? `${path}.${segment}` : segment let fieldsToSearch: FlattenedField[] + let _parentIsLocalized = parentIsLocalized let matchedField: FlattenedField @@ -76,6 +89,7 @@ export function getLocalizedPaths({ } else { fieldsToSearch = lastIncompletePath.fields } + _parentIsLocalized = parentIsLocalized || lastIncompletePath.field?.localized matchedField = fieldsToSearch.find((field) => field.name === segment) } @@ -117,7 +131,10 @@ export function getLocalizedPaths({ // Skip the next iteration, because it's a locale i += 1 currentPath = `${currentPath}.${nextSegment}` - } else if (localizationConfig && 'localized' in matchedField && matchedField.localized) { + } else if ( + localizationConfig && + fieldShouldBeLocalized({ field: matchedField, parentIsLocalized: _parentIsLocalized }) + ) { currentPath = `${currentPath}.${locale}` } @@ -167,6 +184,7 @@ export function getLocalizedPaths({ globalSlug, incomingPath: nestedPathToQuery, locale, + parentIsLocalized: false, payload, }) diff --git a/packages/payload/src/database/queryValidation/types.ts b/packages/payload/src/database/queryValidation/types.ts index ef2c2d607..6d5c4dac9 100644 --- a/packages/payload/src/database/queryValidation/types.ts +++ b/packages/payload/src/database/queryValidation/types.ts @@ -17,5 +17,9 @@ export type PathToQuery = { fields?: FlattenedField[] globalSlug?: string invalid?: boolean + /** + * @todo make required in v4.0 + */ + parentIsLocalized: boolean path: string } diff --git a/packages/payload/src/database/queryValidation/validateSearchParams.ts b/packages/payload/src/database/queryValidation/validateSearchParams.ts index 6fda8b646..6374dee99 100644 --- a/packages/payload/src/database/queryValidation/validateSearchParams.ts +++ b/packages/payload/src/database/queryValidation/validateSearchParams.ts @@ -18,6 +18,7 @@ type Args = { globalConfig?: SanitizedGlobalConfig operator: string overrideAccess: boolean + parentIsLocalized?: boolean path: string policies: EntityPolicies req: PayloadRequest @@ -35,6 +36,7 @@ export async function validateSearchParam({ globalConfig, operator, overrideAccess, + parentIsLocalized, path: incomingPath, policies, req, @@ -68,6 +70,7 @@ export async function validateSearchParam({ incomingPath: sanitizedPath, locale: req.locale, overrideAccess, + parentIsLocalized, payload: req.payload, }) } diff --git a/packages/payload/src/exports/shared.ts b/packages/payload/src/exports/shared.ts index 7546180a9..afbc060ff 100644 --- a/packages/payload/src/exports/shared.ts +++ b/packages/payload/src/exports/shared.ts @@ -27,6 +27,7 @@ export { fieldIsPresentationalOnly, fieldIsSidebar, fieldIsVirtual, + fieldShouldBeLocalized, fieldSupportsMany, optionIsObject, optionIsValue, diff --git a/packages/payload/src/fields/config/sanitize.ts b/packages/payload/src/fields/config/sanitize.ts index f89c9ef40..f3f6430be 100644 --- a/packages/payload/src/fields/config/sanitize.ts +++ b/packages/payload/src/fields/config/sanitize.ts @@ -104,7 +104,7 @@ export const sanitizeFields = async ({ } if (field.type === 'join') { - sanitizeJoinField({ config, field, joinPath, joins }) + sanitizeJoinField({ config, field, joinPath, joins, parentIsLocalized }) } if (field.type === 'relationship' || field.type === 'upload') { @@ -160,7 +160,12 @@ export const sanitizeFields = async ({ if (typeof field.localized !== 'undefined') { let shouldDisableLocalized = !config.localization - if (!config.compatibility?.allowLocalizedWithinLocalized && parentIsLocalized) { + if ( + process.env.NEXT_PUBLIC_PAYLOAD_COMPATIBILITY_allowLocalizedWithinLocalized !== 'true' && + parentIsLocalized && + // @todo PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY=true will be the default in 4.0 + process.env.PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY !== 'true' + ) { shouldDisableLocalized = true } @@ -185,7 +190,7 @@ export const sanitizeFields = async ({ field.access = {} } - setDefaultBeforeDuplicate(field) + setDefaultBeforeDuplicate(field, parentIsLocalized) } if (!field.admin) { diff --git a/packages/payload/src/fields/config/sanitizeJoinField.ts b/packages/payload/src/fields/config/sanitizeJoinField.ts index 6c1bdee45..aa7929400 100644 --- a/packages/payload/src/fields/config/sanitizeJoinField.ts +++ b/packages/payload/src/fields/config/sanitizeJoinField.ts @@ -1,21 +1,29 @@ // @ts-strict-ignore import type { SanitizedJoin, SanitizedJoins } from '../../collections/config/types.js' import type { Config, SanitizedConfig } from '../../config/types.js' -import type { FlattenedJoinField, JoinField, RelationshipField, UploadField } from './types.js' import { APIError } from '../../errors/index.js' import { InvalidFieldJoin } from '../../errors/InvalidFieldJoin.js' import { traverseFields } from '../../utilities/traverseFields.js' +import { + fieldShouldBeLocalized, + type FlattenedJoinField, + type JoinField, + type RelationshipField, + type UploadField, +} from './types.js' export const sanitizeJoinField = ({ config, field, joinPath, joins, + parentIsLocalized, }: { config: Config field: FlattenedJoinField | JoinField joinPath?: string joins?: SanitizedJoins + parentIsLocalized: boolean }) => { // the `joins` arg is not passed for globals or when recursing on fields that do not allow a join field if (typeof joins === 'undefined') { @@ -27,6 +35,7 @@ export const sanitizeJoinField = ({ const join: SanitizedJoin = { field, joinPath: `${joinPath ? joinPath + '.' : ''}${field.name}`, + parentIsLocalized, targetField: undefined, } const joinCollection = config.collections.find( @@ -43,14 +52,14 @@ export const sanitizeJoinField = ({ let localized = false // Traverse fields and match based on the schema path traverseFields({ - callback: ({ field, next }) => { + callback: ({ field, next, parentIsLocalized }) => { if (!('name' in field) || !field.name) { return } const currentSegment = pathSegments[currentSegmentIndex] // match field on path segments if ('name' in field && field.name === currentSegment) { - if ('localized' in field && field.localized) { + if (fieldShouldBeLocalized({ field, parentIsLocalized })) { localized = true const fieldIndex = currentSegmentIndex @@ -93,6 +102,7 @@ export const sanitizeJoinField = ({ }, config: config as unknown as SanitizedConfig, fields: joinCollection.fields, + parentIsLocalized: false, }) if (!joinRelationship) { diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index 3c1452d71..1a6db09b3 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -1854,10 +1854,35 @@ export function tabHasName(tab: TField): tab is return 'name' in tab } +/** + * Check if a field has localized: true set. This does not check if a field *should* + * be localized. To check if a field should be localized, use `fieldShouldBeLocalized`. + * + * @deprecated this will be removed or modified in v4.0, as `fieldIsLocalized` can easily lead to bugs due to + * parent field localization not being taken into account. + */ export function fieldIsLocalized(field: Field | Tab): boolean { return 'localized' in field && field.localized } +/** + * Similar to `fieldIsLocalized`, but returns `false` if any parent field is localized. + */ +export function fieldShouldBeLocalized({ + field, + parentIsLocalized, +}: { + field: ClientField | ClientTab | Field | Tab + parentIsLocalized: boolean +}): boolean { + return ( + 'localized' in field && + field.localized && + (!parentIsLocalized || + process.env.NEXT_PUBLIC_PAYLOAD_COMPATIBILITY_allowLocalizedWithinLocalized === 'true') + ) +} + export function fieldIsVirtual(field: Field | Tab): boolean { return 'virtual' in field && field.virtual } diff --git a/packages/payload/src/fields/hooks/afterChange/index.ts b/packages/payload/src/fields/hooks/afterChange/index.ts index 4f72c9c9c..63bda81e3 100644 --- a/packages/payload/src/fields/hooks/afterChange/index.ts +++ b/packages/payload/src/fields/hooks/afterChange/index.ts @@ -46,6 +46,7 @@ export const afterChange = async ({ global, operation, parentIndexPath: '', + parentIsLocalized: false, parentPath: '', parentSchemaPath: '', previousDoc, diff --git a/packages/payload/src/fields/hooks/afterChange/promise.ts b/packages/payload/src/fields/hooks/afterChange/promise.ts index 8d694db6f..23b1959e6 100644 --- a/packages/payload/src/fields/hooks/afterChange/promise.ts +++ b/packages/payload/src/fields/hooks/afterChange/promise.ts @@ -25,6 +25,7 @@ type Args = { global: null | SanitizedGlobalConfig operation: 'create' | 'update' parentIndexPath: string + parentIsLocalized: boolean parentPath: string parentSchemaPath: string previousDoc: JsonObject @@ -49,6 +50,7 @@ export const promise = async ({ global, operation, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, previousDoc, @@ -123,6 +125,7 @@ export const promise = async ({ global, operation, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath, previousDoc, @@ -166,6 +169,7 @@ export const promise = async ({ global, operation, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath + '.' + block.slug, previousDoc, @@ -196,6 +200,7 @@ export const promise = async ({ global, operation, parentIndexPath: indexPath, + parentIsLocalized, parentPath, parentSchemaPath: schemaPath, previousDoc, @@ -219,6 +224,7 @@ export const promise = async ({ global, operation, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path, parentSchemaPath: schemaPath, previousDoc, @@ -255,6 +261,7 @@ export const promise = async ({ indexPath: indexPathSegments, operation, originalDoc: doc, + parentIsLocalized, path: pathSegments, previousDoc, previousSiblingDoc, @@ -296,6 +303,7 @@ export const promise = async ({ global, operation, parentIndexPath: isNamedTab ? '' : indexPath, + parentIsLocalized: parentIsLocalized || field.localized, parentPath: isNamedTab ? path : parentPath, parentSchemaPath: schemaPath, previousDoc, @@ -319,6 +327,7 @@ export const promise = async ({ global, operation, parentIndexPath: indexPath, + parentIsLocalized, parentPath: path, parentSchemaPath: schemaPath, previousDoc, diff --git a/packages/payload/src/fields/hooks/afterChange/traverseFields.ts b/packages/payload/src/fields/hooks/afterChange/traverseFields.ts index 223fce92e..822a1d345 100644 --- a/packages/payload/src/fields/hooks/afterChange/traverseFields.ts +++ b/packages/payload/src/fields/hooks/afterChange/traverseFields.ts @@ -20,6 +20,10 @@ type Args = { global: null | SanitizedGlobalConfig operation: 'create' | 'update' parentIndexPath: string + /** + * @todo make required in v4.0 + */ + parentIsLocalized?: boolean parentPath: string parentSchemaPath: string previousDoc: JsonObject @@ -40,6 +44,7 @@ export const traverseFields = async ({ global, operation, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, previousDoc, @@ -64,6 +69,7 @@ export const traverseFields = async ({ global, operation, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, previousDoc, diff --git a/packages/payload/src/fields/hooks/afterRead/index.ts b/packages/payload/src/fields/hooks/afterRead/index.ts index 84427ffd1..7c87add2c 100644 --- a/packages/payload/src/fields/hooks/afterRead/index.ts +++ b/packages/payload/src/fields/hooks/afterRead/index.ts @@ -85,6 +85,7 @@ export async function afterRead(args: Args): Promise locale, overrideAccess, parentIndexPath: '', + parentIsLocalized: false, parentPath: '', parentSchemaPath: '', populate, diff --git a/packages/payload/src/fields/hooks/afterRead/promise.ts b/packages/payload/src/fields/hooks/afterRead/promise.ts index 81ecd5a2e..df3da5dab 100644 --- a/packages/payload/src/fields/hooks/afterRead/promise.ts +++ b/packages/payload/src/fields/hooks/afterRead/promise.ts @@ -13,7 +13,7 @@ import type { import type { Block, Field, TabAsField } from '../../config/types.js' import { MissingEditorProp } from '../../../errors/index.js' -import { fieldAffectsData, tabHasName } from '../../config/types.js' +import { fieldAffectsData, fieldShouldBeLocalized, tabHasName } from '../../config/types.js' import { getDefaultValue } from '../../getDefaultValue.js' import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' import { relationshipPopulationPromise } from './relationshipPopulationPromise.js' @@ -43,6 +43,10 @@ type Args = { locale: null | string overrideAccess: boolean parentIndexPath: string + /** + * @todo make required in v4.0 + */ + parentIsLocalized?: boolean parentPath: string parentSchemaPath: string populate?: PopulateType @@ -83,6 +87,7 @@ export const promise = async ({ locale, overrideAccess, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, populate, @@ -139,7 +144,7 @@ export const promise = async ({ fieldAffectsData(field) && typeof siblingDoc[field.name] === 'object' && siblingDoc[field.name] !== null && - field.localized && + fieldShouldBeLocalized({ field, parentIsLocalized }) && locale !== 'all' && req.payload.config.localization @@ -236,7 +241,7 @@ export const promise = async ({ await priorHook const shouldRunHookOnAllLocales = - field.localized && + fieldShouldBeLocalized({ field, parentIsLocalized }) && (locale === 'all' || !flattenLocales) && typeof siblingDoc[field.name] === 'object' @@ -352,6 +357,7 @@ export const promise = async ({ field, locale, overrideAccess, + parentIsLocalized, populate, req, showHiddenFields, @@ -393,6 +399,7 @@ export const promise = async ({ locale, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath, populate, @@ -427,6 +434,7 @@ export const promise = async ({ locale, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath, populate, @@ -511,6 +519,7 @@ export const promise = async ({ locale, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath + '.' + block.slug, populate, @@ -555,6 +564,7 @@ export const promise = async ({ locale, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath + '.' + block.slug, populate, @@ -595,6 +605,7 @@ export const promise = async ({ locale, overrideAccess, parentIndexPath: indexPath, + parentIsLocalized, parentPath, parentSchemaPath: schemaPath, populate, @@ -637,6 +648,7 @@ export const promise = async ({ locale, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path, parentSchemaPath: schemaPath, populate, @@ -669,7 +681,7 @@ export const promise = async ({ await priorHook const shouldRunHookOnAllLocales = - field.localized && + fieldShouldBeLocalized({ field, parentIsLocalized }) && (locale === 'all' || !flattenLocales) && typeof siblingDoc[field.name] === 'object' @@ -694,6 +706,7 @@ export const promise = async ({ operation: 'read', originalDoc: doc, overrideAccess, + parentIsLocalized, path: pathSegments, populate, populationPromises, @@ -732,6 +745,7 @@ export const promise = async ({ operation: 'read', originalDoc: doc, overrideAccess, + parentIsLocalized, path: pathSegments, populate, populationPromises, @@ -790,6 +804,7 @@ export const promise = async ({ locale, overrideAccess, parentIndexPath: isNamedTab ? '' : indexPath, + parentIsLocalized: parentIsLocalized || field.localized, parentPath: isNamedTab ? path : parentPath, parentSchemaPath: schemaPath, populate, @@ -824,6 +839,7 @@ export const promise = async ({ locale, overrideAccess, parentIndexPath: indexPath, + parentIsLocalized, parentPath: path, parentSchemaPath: schemaPath, populate, diff --git a/packages/payload/src/fields/hooks/afterRead/relationshipPopulationPromise.ts b/packages/payload/src/fields/hooks/afterRead/relationshipPopulationPromise.ts index 0be10e840..5f45d85c2 100644 --- a/packages/payload/src/fields/hooks/afterRead/relationshipPopulationPromise.ts +++ b/packages/payload/src/fields/hooks/afterRead/relationshipPopulationPromise.ts @@ -3,7 +3,7 @@ import type { PayloadRequest, PopulateType } from '../../../types/index.js' import type { JoinField, RelationshipField, UploadField } from '../../config/types.js' import { createDataloaderCacheKey } from '../../../collections/dataloader.js' -import { fieldHasMaxDepth, fieldSupportsMany } from '../../config/types.js' +import { fieldHasMaxDepth, fieldShouldBeLocalized, fieldSupportsMany } from '../../config/types.js' type PopulateArgs = { currentDepth: number @@ -116,6 +116,7 @@ type PromiseArgs = { field: JoinField | RelationshipField | UploadField locale: null | string overrideAccess: boolean + parentIsLocalized: boolean populate?: PopulateType req: PayloadRequest showHiddenFields: boolean @@ -130,6 +131,7 @@ export const relationshipPopulationPromise = async ({ field, locale, overrideAccess, + parentIsLocalized, populate: populateArg, req, showHiddenFields, @@ -141,7 +143,7 @@ export const relationshipPopulationPromise = async ({ if (field.type === 'join' || (fieldSupportsMany(field) && field.hasMany)) { if ( - field.localized && + fieldShouldBeLocalized({ field, parentIsLocalized }) && locale === 'all' && typeof siblingDoc[field.name] === 'object' && siblingDoc[field.name] !== null diff --git a/packages/payload/src/fields/hooks/afterRead/traverseFields.ts b/packages/payload/src/fields/hooks/afterRead/traverseFields.ts index 076e93036..56ced3197 100644 --- a/packages/payload/src/fields/hooks/afterRead/traverseFields.ts +++ b/packages/payload/src/fields/hooks/afterRead/traverseFields.ts @@ -35,6 +35,10 @@ type Args = { locale: null | string overrideAccess: boolean parentIndexPath: string + /** + * @todo make required in v4.0 + */ + parentIsLocalized?: boolean parentPath: string parentSchemaPath: string populate?: PopulateType @@ -65,6 +69,7 @@ export const traverseFields = ({ locale, overrideAccess, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, populate, @@ -97,6 +102,7 @@ export const traverseFields = ({ locale, overrideAccess, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, populate, diff --git a/packages/payload/src/fields/hooks/beforeChange/index.ts b/packages/payload/src/fields/hooks/beforeChange/index.ts index bb53cf8fb..3738984f1 100644 --- a/packages/payload/src/fields/hooks/beforeChange/index.ts +++ b/packages/payload/src/fields/hooks/beforeChange/index.ts @@ -59,6 +59,7 @@ export const beforeChange = async ({ mergeLocaleActions, operation, parentIndexPath: '', + parentIsLocalized: false, parentPath: '', parentSchemaPath: '', req, diff --git a/packages/payload/src/fields/hooks/beforeChange/promise.ts b/packages/payload/src/fields/hooks/beforeChange/promise.ts index e715b16cb..cf284c231 100644 --- a/packages/payload/src/fields/hooks/beforeChange/promise.ts +++ b/packages/payload/src/fields/hooks/beforeChange/promise.ts @@ -11,7 +11,7 @@ import { MissingEditorProp } from '../../../errors/index.js' import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js' import { getLabelFromPath } from '../../../utilities/getLabelFromPath.js' import { getTranslatedLabel } from '../../../utilities/getTranslatedLabel.js' -import { fieldAffectsData, tabHasName } from '../../config/types.js' +import { fieldAffectsData, fieldShouldBeLocalized, tabHasName } from '../../config/types.js' import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' import { getExistingRowDoc } from './getExistingRowDoc.js' import { traverseFields } from './traverseFields.js' @@ -34,6 +34,7 @@ type Args = { mergeLocaleActions: (() => Promise)[] operation: Operation parentIndexPath: string + parentIsLocalized: boolean parentPath: string parentSchemaPath: string req: PayloadRequest @@ -67,6 +68,7 @@ export const promise = async ({ mergeLocaleActions, operation, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, req, @@ -98,7 +100,7 @@ export const promise = async ({ if (fieldAffectsData(field)) { // skip validation if the field is localized and the incoming data is null - if (field.localized && operationLocale !== defaultLocale) { + if (fieldShouldBeLocalized({ field, parentIsLocalized }) && operationLocale !== defaultLocale) { if (['array', 'blocks'].includes(field.type) && siblingData[field.name] === null) { skipValidationFromHere = true } @@ -189,7 +191,7 @@ export const promise = async ({ } // Push merge locale action if applicable - if (localization && field.localized) { + if (localization && fieldShouldBeLocalized({ field, parentIsLocalized })) { mergeLocaleActions.push(async () => { const localeData = await localization.localeCodes.reduce( async (localizedValuesPromise: Promise, locale) => { @@ -244,6 +246,7 @@ export const promise = async ({ mergeLocaleActions, operation, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath, req, @@ -301,6 +304,7 @@ export const promise = async ({ mergeLocaleActions, operation, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath + '.' + block.slug, req, @@ -335,6 +339,7 @@ export const promise = async ({ mergeLocaleActions, operation, parentIndexPath: indexPath, + parentIsLocalized, parentPath, parentSchemaPath: schemaPath, req, @@ -374,6 +379,7 @@ export const promise = async ({ mergeLocaleActions, operation, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path, parentSchemaPath: schemaPath, req, @@ -432,6 +438,7 @@ export const promise = async ({ mergeLocaleActions, operation, originalDoc: doc, + parentIsLocalized, path: pathSegments, previousSiblingDoc: siblingDoc, previousValue: siblingDoc[field.name], @@ -491,6 +498,7 @@ export const promise = async ({ mergeLocaleActions, operation, parentIndexPath: isNamedTab ? '' : indexPath, + parentIsLocalized: parentIsLocalized || field.localized, parentPath: isNamedTab ? path : parentPath, parentSchemaPath: schemaPath, req, @@ -518,6 +526,7 @@ export const promise = async ({ mergeLocaleActions, operation, parentIndexPath: indexPath, + parentIsLocalized, parentPath: path, parentSchemaPath: schemaPath, req, diff --git a/packages/payload/src/fields/hooks/beforeChange/traverseFields.ts b/packages/payload/src/fields/hooks/beforeChange/traverseFields.ts index c685b778d..abde171bd 100644 --- a/packages/payload/src/fields/hooks/beforeChange/traverseFields.ts +++ b/packages/payload/src/fields/hooks/beforeChange/traverseFields.ts @@ -31,6 +31,10 @@ type Args = { mergeLocaleActions: (() => Promise)[] operation: Operation parentIndexPath: string + /** + * @todo make required in v4.0 + */ + parentIsLocalized?: boolean parentPath: string parentSchemaPath: string req: PayloadRequest @@ -68,6 +72,7 @@ export const traverseFields = async ({ mergeLocaleActions, operation, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, req, @@ -95,6 +100,7 @@ export const traverseFields = async ({ mergeLocaleActions, operation, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, req, diff --git a/packages/payload/src/fields/hooks/beforeDuplicate/index.ts b/packages/payload/src/fields/hooks/beforeDuplicate/index.ts index 4a0910ae4..9a71214d5 100644 --- a/packages/payload/src/fields/hooks/beforeDuplicate/index.ts +++ b/packages/payload/src/fields/hooks/beforeDuplicate/index.ts @@ -36,6 +36,7 @@ export const beforeDuplicate = async ({ fields: collection?.fields, overrideAccess, parentIndexPath: '', + parentIsLocalized: false, parentPath: '', parentSchemaPath: '', req, diff --git a/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts b/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts index d95aa0b96..b289167bc 100644 --- a/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts +++ b/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts @@ -4,7 +4,7 @@ import type { RequestContext } from '../../../index.js' import type { JsonObject, PayloadRequest } from '../../../types/index.js' import type { Block, Field, FieldHookArgs, TabAsField } from '../../config/types.js' -import { fieldAffectsData } from '../../config/types.js' +import { fieldAffectsData, fieldShouldBeLocalized } from '../../config/types.js' import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' import { runBeforeDuplicateHooks } from './runHook.js' import { traverseFields } from './traverseFields.js' @@ -22,6 +22,7 @@ type Args = { id?: number | string overrideAccess: boolean parentIndexPath: string + parentIsLocalized: boolean parentPath: string parentSchemaPath: string req: PayloadRequest @@ -39,6 +40,7 @@ export const promise = async ({ fieldIndex, overrideAccess, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, req, @@ -61,7 +63,7 @@ export const promise = async ({ if (fieldAffectsData(field)) { let fieldData = siblingDoc?.[field.name] - const fieldIsLocalized = field.localized && localization + const fieldIsLocalized = localization && fieldShouldBeLocalized({ field, parentIsLocalized }) // Run field beforeDuplicate hooks if (Array.isArray(field.hooks?.beforeDuplicate)) { @@ -162,6 +164,7 @@ export const promise = async ({ fields: field.fields, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath, req, @@ -200,6 +203,7 @@ export const promise = async ({ fields: block.fields, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath + '.' + block.slug, req, @@ -223,6 +227,7 @@ export const promise = async ({ fields: field.fields, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path, parentSchemaPath: schemaPath, req, @@ -259,6 +264,7 @@ export const promise = async ({ fields: field.fields, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath, req, @@ -301,6 +307,7 @@ export const promise = async ({ fields: block.fields, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath + '.' + block.slug, req, @@ -332,6 +339,7 @@ export const promise = async ({ fields: field.fields, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path, parentSchemaPath: schemaPath, req, @@ -357,6 +365,7 @@ export const promise = async ({ fields: field.fields, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path, parentSchemaPath: schemaPath, req, @@ -381,6 +390,7 @@ export const promise = async ({ fields: field.fields, overrideAccess, parentIndexPath: indexPath, + parentIsLocalized, parentPath, parentSchemaPath: schemaPath, req, @@ -403,6 +413,7 @@ export const promise = async ({ fields: field.fields, overrideAccess, parentIndexPath: indexPath, + parentIsLocalized, parentPath, parentSchemaPath: schemaPath, req, @@ -422,6 +433,7 @@ export const promise = async ({ fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), overrideAccess, parentIndexPath: indexPath, + parentIsLocalized, parentPath: path, parentSchemaPath: schemaPath, req, diff --git a/packages/payload/src/fields/hooks/beforeDuplicate/traverseFields.ts b/packages/payload/src/fields/hooks/beforeDuplicate/traverseFields.ts index 78b3569ce..65e49a576 100644 --- a/packages/payload/src/fields/hooks/beforeDuplicate/traverseFields.ts +++ b/packages/payload/src/fields/hooks/beforeDuplicate/traverseFields.ts @@ -18,6 +18,7 @@ type Args = { id?: number | string overrideAccess: boolean parentIndexPath: string + parentIsLocalized: boolean parentPath: string parentSchemaPath: string req: PayloadRequest @@ -33,6 +34,7 @@ export const traverseFields = async ({ fields, overrideAccess, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, req, @@ -52,6 +54,7 @@ export const traverseFields = async ({ fieldIndex, overrideAccess, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, req, diff --git a/packages/payload/src/fields/hooks/beforeValidate/index.ts b/packages/payload/src/fields/hooks/beforeValidate/index.ts index 967193021..fbc92f5a4 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/index.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/index.ts @@ -49,6 +49,7 @@ export const beforeValidate = async ({ operation, overrideAccess, parentIndexPath: '', + parentIsLocalized: false, parentPath: '', parentSchemaPath: '', req, diff --git a/packages/payload/src/fields/hooks/beforeValidate/promise.ts b/packages/payload/src/fields/hooks/beforeValidate/promise.ts index 1072775bc..5917f6db8 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/promise.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/promise.ts @@ -33,6 +33,7 @@ type Args = { operation: 'create' | 'update' overrideAccess: boolean parentIndexPath: string + parentIsLocalized: boolean parentPath: string parentSchemaPath: string req: PayloadRequest @@ -64,6 +65,7 @@ export const promise = async ({ operation, overrideAccess, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, req, @@ -355,6 +357,7 @@ export const promise = async ({ operation, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath, req, @@ -401,6 +404,7 @@ export const promise = async ({ operation, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path + '.' + rowIndex, parentSchemaPath: schemaPath + '.' + block.slug, req, @@ -431,6 +435,7 @@ export const promise = async ({ operation, overrideAccess, parentIndexPath: indexPath, + parentIsLocalized, parentPath, parentSchemaPath: schemaPath, req, @@ -465,6 +470,7 @@ export const promise = async ({ operation, overrideAccess, parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, parentPath: path, parentSchemaPath: schemaPath, req, @@ -500,6 +506,7 @@ export const promise = async ({ operation, originalDoc: doc, overrideAccess, + parentIsLocalized, path: pathSegments, previousSiblingDoc: siblingDoc, previousValue: siblingData[field.name], @@ -551,6 +558,7 @@ export const promise = async ({ operation, overrideAccess, parentIndexPath: isNamedTab ? '' : indexPath, + parentIsLocalized: parentIsLocalized || field.localized, parentPath: isNamedTab ? path : parentPath, parentSchemaPath: schemaPath, req, @@ -574,6 +582,7 @@ export const promise = async ({ operation, overrideAccess, parentIndexPath: indexPath, + parentIsLocalized, parentPath: path, parentSchemaPath: schemaPath, req, diff --git a/packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts b/packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts index c8c594a31..523ea8b13 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts @@ -25,6 +25,10 @@ type Args = { operation: 'create' | 'update' overrideAccess: boolean parentIndexPath: string + /** + * @todo make required in v4.0 + */ + parentIsLocalized?: boolean parentPath: string parentSchemaPath: string req: PayloadRequest @@ -47,6 +51,7 @@ export const traverseFields = async ({ operation, overrideAccess, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, req, @@ -70,6 +75,7 @@ export const traverseFields = async ({ operation, overrideAccess, parentIndexPath, + parentIsLocalized, parentPath, parentSchemaPath, req, diff --git a/packages/payload/src/fields/setDefaultBeforeDuplicate.ts b/packages/payload/src/fields/setDefaultBeforeDuplicate.ts index 97f2314e5..034bc7de7 100644 --- a/packages/payload/src/fields/setDefaultBeforeDuplicate.ts +++ b/packages/payload/src/fields/setDefaultBeforeDuplicate.ts @@ -1,6 +1,6 @@ // @ts-strict-ignore // default beforeDuplicate hook for required and unique fields -import type { FieldAffectingData, FieldHook } from './config/types.js' +import { type FieldAffectingData, type FieldHook, fieldShouldBeLocalized } from './config/types.js' const unique: FieldHook = ({ value }) => (typeof value === 'string' ? `${value} - Copy` : undefined) const localizedUnique: FieldHook = ({ req, value }) => @@ -9,16 +9,25 @@ const uniqueRequired: FieldHook = ({ value }) => `${value} - Copy` const localizedUniqueRequired: FieldHook = ({ req, value }) => `${value} - ${req?.t('general:copy') ?? 'Copy'}` -export const setDefaultBeforeDuplicate = (field: FieldAffectingData) => { +export const setDefaultBeforeDuplicate = ( + field: FieldAffectingData, + parentIsLocalized: boolean, +) => { if ( (('required' in field && field.required) || field.unique) && (!field.hooks?.beforeDuplicate || (Array.isArray(field.hooks.beforeDuplicate) && field.hooks.beforeDuplicate.length === 0)) ) { if ((field.type === 'text' || field.type === 'textarea') && field.required && field.unique) { - field.hooks.beforeDuplicate = [field.localized ? localizedUniqueRequired : uniqueRequired] + field.hooks.beforeDuplicate = [ + fieldShouldBeLocalized({ field, parentIsLocalized }) + ? localizedUniqueRequired + : uniqueRequired, + ] } else if (field.unique) { - field.hooks.beforeDuplicate = [field.localized ? localizedUnique : unique] + field.hooks.beforeDuplicate = [ + fieldShouldBeLocalized({ field, parentIsLocalized }) ? localizedUnique : unique, + ] } } } diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 2249be3ba..0f61fdd54 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -616,7 +616,12 @@ export class BasePayload { } } - traverseFields({ callback: findCustomID, config: this.config, fields: collection.fields }) + traverseFields({ + callback: findCustomID, + config: this.config, + fields: collection.fields, + parentIsLocalized: false, + }) this.collections[collection.slug] = { config: collection, diff --git a/packages/payload/src/utilities/traverseFields.ts b/packages/payload/src/utilities/traverseFields.ts index 6ab548a82..63c01f7a5 100644 --- a/packages/payload/src/utilities/traverseFields.ts +++ b/packages/payload/src/utilities/traverseFields.ts @@ -2,7 +2,7 @@ import type { Config, SanitizedConfig } from '../config/types.js' import type { ArrayField, Block, BlocksField, Field, TabAsField } from '../fields/config/types.js' -import { fieldHasSubFields } from '../fields/config/types.js' +import { fieldHasSubFields, fieldShouldBeLocalized } from '../fields/config/types.js' const traverseArrayOrBlocksField = ({ callback, @@ -12,6 +12,7 @@ const traverseArrayOrBlocksField = ({ field, fillEmpty, leavesFirst, + parentIsLocalized, parentRef, }: { callback: TraverseFieldsCallback @@ -21,6 +22,7 @@ const traverseArrayOrBlocksField = ({ field: ArrayField | BlocksField fillEmpty: boolean leavesFirst: boolean + parentIsLocalized: boolean parentRef?: unknown }) => { if (fillEmpty) { @@ -32,6 +34,7 @@ const traverseArrayOrBlocksField = ({ fields: field.fields, isTopLevel: false, leavesFirst, + parentIsLocalized: parentIsLocalized || field.localized, parentRef, }) } @@ -48,6 +51,7 @@ const traverseArrayOrBlocksField = ({ fields: block.fields, isTopLevel: false, leavesFirst, + parentIsLocalized: parentIsLocalized || field.localized, parentRef, }) } @@ -80,6 +84,7 @@ const traverseArrayOrBlocksField = ({ fillEmpty, isTopLevel: false, leavesFirst, + parentIsLocalized: parentIsLocalized || field.localized, parentRef, ref, }) @@ -96,6 +101,7 @@ export type TraverseFieldsCallback = (args: { * Function that when called will skip the current field and continue to the next */ next?: () => void + parentIsLocalized: boolean /** * The parent reference object */ @@ -120,6 +126,7 @@ type TraverseFieldsArgs = { * The return value of the callback function will be ignored. */ leavesFirst?: boolean + parentIsLocalized?: boolean parentRef?: Record | unknown ref?: Record | unknown } @@ -141,6 +148,7 @@ export const traverseFields = ({ fillEmpty = true, isTopLevel = true, leavesFirst = false, + parentIsLocalized, parentRef = {}, ref = {}, }: TraverseFieldsArgs): void => { @@ -158,10 +166,10 @@ export const traverseFields = ({ return } - if (!leavesFirst && callback && callback({ field, next, parentRef, ref })) { + if (!leavesFirst && callback && callback({ field, next, parentIsLocalized, parentRef, ref })) { return true } else if (leavesFirst) { - callbackStack.push(() => callback({ field, next, parentRef, ref })) + callbackStack.push(() => callback({ field, next, parentIsLocalized, parentRef, ref })) } if (skip) { @@ -199,6 +207,7 @@ export const traverseFields = ({ callback({ field: { ...tab, type: 'tab' }, next, + parentIsLocalized, parentRef: currentParentRef, ref: tabRef, }) @@ -209,6 +218,7 @@ export const traverseFields = ({ callback({ field: { ...tab, type: 'tab' }, next, + parentIsLocalized, parentRef: currentParentRef, ref: tabRef, }), @@ -228,6 +238,7 @@ export const traverseFields = ({ fillEmpty, isTopLevel: false, leavesFirst, + parentIsLocalized: true, parentRef: currentParentRef, ref: tabRef[key], }) @@ -241,6 +252,7 @@ export const traverseFields = ({ callback({ field: { ...tab, type: 'tab' }, next, + parentIsLocalized, parentRef: currentParentRef, ref: tabRef, }) @@ -251,6 +263,7 @@ export const traverseFields = ({ callback({ field: { ...tab, type: 'tab' }, next, + parentIsLocalized, parentRef: currentParentRef, ref: tabRef, }), @@ -267,6 +280,7 @@ export const traverseFields = ({ fillEmpty, isTopLevel: false, leavesFirst, + parentIsLocalized: false, parentRef: currentParentRef, ref: tabRef, }) @@ -286,7 +300,7 @@ export const traverseFields = ({ if (!ref[field.name]) { if (fillEmpty) { if (field.type === 'group') { - if (field.localized) { + if (fieldShouldBeLocalized({ field, parentIsLocalized })) { ref[field.name] = { en: {}, } @@ -294,7 +308,7 @@ export const traverseFields = ({ ref[field.name] = {} } } else if (field.type === 'array' || field.type === 'blocks') { - if (field.localized) { + if (fieldShouldBeLocalized({ field, parentIsLocalized })) { ref[field.name] = { en: [], } @@ -311,7 +325,7 @@ export const traverseFields = ({ if ( field.type === 'group' && - field.localized && + fieldShouldBeLocalized({ field, parentIsLocalized }) && currentRef && typeof currentRef === 'object' ) { @@ -325,6 +339,7 @@ export const traverseFields = ({ fillEmpty, isTopLevel: false, leavesFirst, + parentIsLocalized: true, parentRef: currentParentRef, ref: currentRef[key], }) @@ -338,7 +353,7 @@ export const traverseFields = ({ currentRef && typeof currentRef === 'object' ) { - if (field.localized) { + if (fieldShouldBeLocalized({ field, parentIsLocalized })) { if (Array.isArray(currentRef)) { return } @@ -357,6 +372,7 @@ export const traverseFields = ({ field, fillEmpty, leavesFirst, + parentIsLocalized: true, parentRef: currentParentRef, }) } @@ -369,6 +385,7 @@ export const traverseFields = ({ field, fillEmpty, leavesFirst, + parentIsLocalized, parentRef: currentParentRef, }) } @@ -381,6 +398,7 @@ export const traverseFields = ({ fillEmpty, isTopLevel: false, leavesFirst, + parentIsLocalized, parentRef: currentParentRef, ref: currentRef, }) diff --git a/packages/richtext-lexical/src/features/blocks/server/graphQLPopulationPromise.ts b/packages/richtext-lexical/src/features/blocks/server/graphQLPopulationPromise.ts index 7874bd7ee..b8456e3b2 100644 --- a/packages/richtext-lexical/src/features/blocks/server/graphQLPopulationPromise.ts +++ b/packages/richtext-lexical/src/features/blocks/server/graphQLPopulationPromise.ts @@ -17,11 +17,13 @@ export const blockPopulationPromiseHOC = ( depth, draft, editorPopulationPromises, + field, fieldPromises, findMany, flattenLocales, node, overrideAccess, + parentIsLocalized, populationPromises, req, showHiddenFields, @@ -46,6 +48,7 @@ export const blockPopulationPromiseHOC = ( findMany, flattenLocales, overrideAccess, + parentIsLocalized: parentIsLocalized || field.localized || false, populationPromises, req, showHiddenFields, diff --git a/packages/richtext-lexical/src/features/link/server/graphQLPopulationPromise.ts b/packages/richtext-lexical/src/features/link/server/graphQLPopulationPromise.ts index 095ae14a5..1b1d66d52 100644 --- a/packages/richtext-lexical/src/features/link/server/graphQLPopulationPromise.ts +++ b/packages/richtext-lexical/src/features/link/server/graphQLPopulationPromise.ts @@ -13,11 +13,13 @@ export const linkPopulationPromiseHOC = ( depth, draft, editorPopulationPromises, + field, fieldPromises, findMany, flattenLocales, node, overrideAccess, + parentIsLocalized, populationPromises, req, showHiddenFields, @@ -42,6 +44,7 @@ export const linkPopulationPromiseHOC = ( findMany, flattenLocales, overrideAccess, + parentIsLocalized: parentIsLocalized || field.localized || false, populationPromises, req, showHiddenFields, diff --git a/packages/richtext-lexical/src/features/typesServer.ts b/packages/richtext-lexical/src/features/typesServer.ts index c2a599bb3..35ee04c64 100644 --- a/packages/richtext-lexical/src/features/typesServer.ts +++ b/packages/richtext-lexical/src/features/typesServer.ts @@ -30,23 +30,7 @@ import type { AdapterProps } from '../types.js' import type { HTMLConverter } from './converters/html/converter/types.js' import type { BaseClientFeatureProps } from './typesClient.js' -export type PopulationPromise = ({ - context, - currentDepth, - depth, - draft, - editorPopulationPromises, - field, - fieldPromises, - findMany, - flattenLocales, - node, - overrideAccess, - populationPromises, - req, - showHiddenFields, - siblingDoc, -}: { +export type PopulationPromise = (args: { context: RequestContext currentDepth: number depth: number @@ -64,6 +48,7 @@ export type PopulationPromise[] req: PayloadRequest showHiddenFields: boolean diff --git a/packages/richtext-lexical/src/features/upload/server/graphQLPopulationPromise.ts b/packages/richtext-lexical/src/features/upload/server/graphQLPopulationPromise.ts index e45ad73f4..cf1b7bdc5 100644 --- a/packages/richtext-lexical/src/features/upload/server/graphQLPopulationPromise.ts +++ b/packages/richtext-lexical/src/features/upload/server/graphQLPopulationPromise.ts @@ -14,11 +14,13 @@ export const uploadPopulationPromiseHOC = ( depth, draft, editorPopulationPromises, + field, fieldPromises, findMany, flattenLocales, node, overrideAccess, + parentIsLocalized, populationPromises, req, showHiddenFields, @@ -59,6 +61,8 @@ export const uploadPopulationPromiseHOC = ( currentDepth, data: node.fields || {}, depth, + parentIsLocalized: parentIsLocalized || field.localized || false, + draft, editorPopulationPromises, fieldPromises, diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index 53c8bca0a..61bff083d 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -157,6 +157,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte findMany, flattenLocales, overrideAccess, + parentIsLocalized, populationPromises, req, showHiddenFields, @@ -175,6 +176,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte findMany, flattenLocales, overrideAccess, + parentIsLocalized, populationPromises, req, showHiddenFields, @@ -189,10 +191,12 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte collection, context: _context, data, + field, global, indexPath, operation, originalDoc, + parentIsLocalized, path, previousDoc, previousValue, @@ -268,7 +272,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte originalNode: originalNodeIDMap[id], parentRichTextFieldPath: path, parentRichTextFieldSchemaPath: schemaPath, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + previousNode: previousNodeIDMap[id]!, req, }) @@ -282,10 +286,9 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte if (subFieldFn && subFieldDataFn) { const subFields = subFieldFn({ node, req }) const nodeSiblingData = subFieldDataFn({ node, req }) ?? {} - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const nodeSiblingDoc = subFieldDataFn({ node: originalNodeIDMap[id]!, req }) ?? {} const nodePreviousSiblingDoc = - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion subFieldDataFn({ node: previousNodeIDMap[id]!, req }) ?? {} if (subFields?.length) { @@ -299,6 +302,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte global, operation, parentIndexPath: indexPath.join('-'), + parentIsLocalized: parentIsLocalized || field.localized || false, parentPath: path.join('.'), parentSchemaPath: schemaPath.join('.'), previousDoc, @@ -325,6 +329,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte depth, draft, fallbackLocale, + field, fieldPromises, findMany, flattenLocales, @@ -333,6 +338,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte locale, originalDoc, overrideAccess, + parentIsLocalized, path, populate, populationPromises, @@ -419,6 +425,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte locale: locale!, overrideAccess: overrideAccess!, parentIndexPath: indexPath.join('-'), + parentIsLocalized: parentIsLocalized || field.localized || false, parentPath: path.join('.'), parentSchemaPath: schemaPath.join('.'), populate, @@ -450,6 +457,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte mergeLocaleActions, operation, originalDoc, + parentIsLocalized, path, previousValue, req, @@ -543,7 +551,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte originalNodeWithLocales: originalNodeWithLocalesIDMap[id], parentRichTextFieldPath: path, parentRichTextFieldSchemaPath: schemaPath, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + previousNode: previousNodeIDMap[id]!, req, skipValidation: skipValidation!, @@ -561,12 +569,10 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte const nodeSiblingData = subFieldDataFn({ node, req }) ?? {} const nodeSiblingDocWithLocales = subFieldDataFn({ - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion node: originalNodeWithLocalesIDMap[id]!, req, }) ?? {} const nodePreviousSiblingDoc = - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion subFieldDataFn({ node: previousNodeIDMap[id]!, req }) ?? {} if (subFields?.length) { @@ -584,6 +590,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte mergeLocaleActions: mergeLocaleActions!, operation: operation!, parentIndexPath: indexPath.join('-'), + parentIsLocalized: parentIsLocalized || field.localized || false, parentPath: path.join('.'), parentSchemaPath: schemaPath.join('.'), req, @@ -637,11 +644,13 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte collection, context, data, + field, global, indexPath, operation, originalDoc, overrideAccess, + parentIsLocalized, path, previousValue, req, @@ -762,7 +771,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte if (subFieldFn && subFieldDataFn) { const subFields = subFieldFn({ node, req }) const nodeSiblingData = subFieldDataFn({ node, req }) ?? {} - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const nodeSiblingDoc = subFieldDataFn({ node: originalNodeIDMap[id]!, req }) ?? {} if (subFields?.length) { @@ -778,6 +787,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte operation, overrideAccess: overrideAccess!, parentIndexPath: indexPath.join('-'), + parentIsLocalized: parentIsLocalized || field.localized || false, parentPath: path.join('.'), parentSchemaPath: schemaPath.join('.'), req, diff --git a/packages/richtext-lexical/src/populateGraphQL/populateLexicalPopulationPromises.ts b/packages/richtext-lexical/src/populateGraphQL/populateLexicalPopulationPromises.ts index 56f5378c5..73a6c30cd 100644 --- a/packages/richtext-lexical/src/populateGraphQL/populateLexicalPopulationPromises.ts +++ b/packages/richtext-lexical/src/populateGraphQL/populateLexicalPopulationPromises.ts @@ -8,6 +8,7 @@ import { recurseNodes } from '../utilities/forEachNodeRecursively.js' export type Args = { editorPopulationPromises: Map> + parentIsLocalized: boolean } & Parameters< NonNullable['graphQLPopulationPromises']> >[0] @@ -26,6 +27,7 @@ export const populateLexicalPopulationPromises = ({ findMany, flattenLocales, overrideAccess, + parentIsLocalized, populationPromises, req, showHiddenFields, @@ -54,6 +56,7 @@ export const populateLexicalPopulationPromises = ({ flattenLocales, node, overrideAccess: overrideAccess!, + parentIsLocalized, populationPromises, req, showHiddenFields, diff --git a/packages/richtext-lexical/src/populateGraphQL/recursivelyPopulateFieldsForGraphQL.ts b/packages/richtext-lexical/src/populateGraphQL/recursivelyPopulateFieldsForGraphQL.ts index d7c212937..22d3c6cfc 100644 --- a/packages/richtext-lexical/src/populateGraphQL/recursivelyPopulateFieldsForGraphQL.ts +++ b/packages/richtext-lexical/src/populateGraphQL/recursivelyPopulateFieldsForGraphQL.ts @@ -22,6 +22,7 @@ type NestedRichTextFieldsArgs = { findMany: boolean flattenLocales: boolean overrideAccess: boolean + parentIsLocalized: boolean populationPromises: Promise[] req: PayloadRequest showHiddenFields: boolean @@ -39,6 +40,7 @@ export const recursivelyPopulateFieldsForGraphQL = ({ findMany, flattenLocales, overrideAccess = false, + parentIsLocalized, populationPromises, req, showHiddenFields, @@ -60,6 +62,7 @@ export const recursivelyPopulateFieldsForGraphQL = ({ locale: req.locale!, overrideAccess, parentIndexPath: '', + parentIsLocalized, parentPath: '', parentSchemaPath: '', populationPromises, // This is not the same as populationPromises passed into this recurseNestedFields. These are just promises resolved at the very end. diff --git a/packages/richtext-slate/src/data/richTextRelationshipPromise.ts b/packages/richtext-slate/src/data/richTextRelationshipPromise.ts index 140fb5948..a99e6444c 100644 --- a/packages/richtext-slate/src/data/richTextRelationshipPromise.ts +++ b/packages/richtext-slate/src/data/richTextRelationshipPromise.ts @@ -156,6 +156,7 @@ export const richTextRelationshipPromise = ({ draft, field, overrideAccess, + parentIsLocalized, populateArg, populationPromises, req, diff --git a/packages/richtext-slate/src/index.tsx b/packages/richtext-slate/src/index.tsx index 7752f9b43..2aca3b914 100644 --- a/packages/richtext-slate/src/index.tsx +++ b/packages/richtext-slate/src/index.tsx @@ -119,6 +119,7 @@ export function slateEditor( findMany, flattenLocales, overrideAccess, + parentIsLocalized, populationPromises, req, showHiddenFields, @@ -140,6 +141,7 @@ export function slateEditor( findMany, flattenLocales, overrideAccess, + parentIsLocalized, populationPromises, req, showHiddenFields, @@ -159,6 +161,7 @@ export function slateEditor( findMany, flattenLocales, overrideAccess, + parentIsLocalized, populate, populationPromises, req, @@ -183,6 +186,7 @@ export function slateEditor( findMany, flattenLocales, overrideAccess, + parentIsLocalized, populateArg: populate, populationPromises, req, diff --git a/packages/ui/src/utilities/copyDataFromLocale.ts b/packages/ui/src/utilities/copyDataFromLocale.ts index 2984c34f2..2fbc404e3 100644 --- a/packages/ui/src/utilities/copyDataFromLocale.ts +++ b/packages/ui/src/utilities/copyDataFromLocale.ts @@ -7,7 +7,7 @@ import { formatErrors, type PayloadRequest, } from 'payload' -import { fieldAffectsData, tabHasName } from 'payload/shared' +import { fieldAffectsData, fieldShouldBeLocalized, tabHasName } from 'payload/shared' const ObjectId = (ObjectIdImport.default || ObjectIdImport) as unknown as typeof ObjectIdImport.default @@ -27,6 +27,7 @@ function iterateFields( fromLocaleData: Data, toLocaleData: Data, req: PayloadRequest, + parentIsLocalized: boolean, ): void { fields.map((field) => { if (fieldAffectsData(field)) { @@ -48,11 +49,17 @@ function iterateFields( toLocaleData[field.name].map((item: Data, index: number) => { if (fromLocaleData[field.name]?.[index]) { // Generate new IDs if the field is localized to prevent errors with relational DBs. - if (field.localized) { + if (fieldShouldBeLocalized({ field, parentIsLocalized })) { toLocaleData[field.name][index].id = new ObjectId().toHexString() } - iterateFields(field.fields, fromLocaleData[field.name][index], item, req) + iterateFields( + field.fields, + fromLocaleData[field.name][index], + item, + req, + parentIsLocalized || field.localized, + ) } }) } @@ -80,12 +87,18 @@ function iterateFields( ) as FlattenedBlock | undefined) // Generate new IDs if the field is localized to prevent errors with relational DBs. - if (field.localized) { + if (fieldShouldBeLocalized({ field, parentIsLocalized })) { toLocaleData[field.name][index].id = new ObjectId().toHexString() } if (block?.fields?.length) { - iterateFields(block?.fields, fromLocaleData[field.name][index], blockData, req) + iterateFields( + block?.fields, + fromLocaleData[field.name][index], + blockData, + req, + parentIsLocalized || field.localized, + ) } }) } @@ -118,7 +131,13 @@ function iterateFields( case 'group': { if (field.name in toLocaleData && fromLocaleData?.[field.name] !== undefined) { - iterateFields(field.fields, fromLocaleData[field.name], toLocaleData[field.name], req) + iterateFields( + field.fields, + fromLocaleData[field.name], + toLocaleData[field.name], + req, + parentIsLocalized || field.localized, + ) } break } @@ -127,17 +146,23 @@ function iterateFields( switch (field.type) { case 'collapsible': case 'row': - iterateFields(field.fields, fromLocaleData, toLocaleData, req) + iterateFields(field.fields, fromLocaleData, toLocaleData, req, parentIsLocalized) break case 'tabs': field.tabs.map((tab) => { if (tabHasName(tab)) { if (tab.name in toLocaleData && fromLocaleData?.[tab.name] !== undefined) { - iterateFields(tab.fields, fromLocaleData[tab.name], toLocaleData[tab.name], req) + iterateFields( + tab.fields, + fromLocaleData[tab.name], + toLocaleData[tab.name], + req, + parentIsLocalized, + ) } } else { - iterateFields(tab.fields, fromLocaleData, toLocaleData, req) + iterateFields(tab.fields, fromLocaleData, toLocaleData, req, parentIsLocalized) } }) break @@ -151,8 +176,9 @@ function mergeData( toLocaleData: Data, fields: Field[], req: PayloadRequest, + parentIsLocalized: boolean, ): Data { - iterateFields(fields, fromLocaleData, toLocaleData, req) + iterateFields(fields, fromLocaleData, toLocaleData, req, parentIsLocalized) return toLocaleData } @@ -272,6 +298,7 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => { toLocaleData.value, globals[globalSlug].config.fields, req, + false, ), locale: toLocale, overrideAccess: false, @@ -288,6 +315,7 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => { toLocaleData.value, collections[collectionSlug].config.fields, req, + false, ), locale: toLocale, overrideAccess: false, diff --git a/test/_community/payload-types.ts b/test/_community/payload-types.ts index 4d682afd5..eaa5dd9f2 100644 --- a/test/_community/payload-types.ts +++ b/test/_community/payload-types.ts @@ -83,7 +83,7 @@ export interface Config { 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; }; db: { - defaultIDType: number; + defaultIDType: string; }; globals: { menu: Menu; @@ -123,7 +123,7 @@ export interface UserAuthOperations { * via the `definition` "posts". */ export interface Post { - id: number; + id: string; title?: string | null; content?: { root: { @@ -148,7 +148,7 @@ export interface Post { * via the `definition` "media". */ export interface Media { - id: number; + id: string; updatedAt: string; createdAt: string; url?: string | null; @@ -192,7 +192,7 @@ export interface Media { * via the `definition` "users". */ export interface User { - id: number; + id: string; updatedAt: string; createdAt: string; email: string; @@ -209,24 +209,24 @@ export interface User { * via the `definition` "payload-locked-documents". */ export interface PayloadLockedDocument { - id: number; + id: string; document?: | ({ relationTo: 'posts'; - value: number | Post; + value: string | Post; } | null) | ({ relationTo: 'media'; - value: number | Media; + value: string | Media; } | null) | ({ relationTo: 'users'; - value: number | User; + value: string | User; } | null); globalSlug?: string | null; user: { relationTo: 'users'; - value: number | User; + value: string | User; }; updatedAt: string; createdAt: string; @@ -236,10 +236,10 @@ export interface PayloadLockedDocument { * via the `definition` "payload-preferences". */ export interface PayloadPreference { - id: number; + id: string; user: { relationTo: 'users'; - value: number | User; + value: string | User; }; key?: string | null; value?: @@ -259,7 +259,7 @@ export interface PayloadPreference { * via the `definition` "payload-migrations". */ export interface PayloadMigration { - id: number; + id: string; name?: string | null; batch?: number | null; updatedAt: string; @@ -378,7 +378,7 @@ export interface PayloadMigrationsSelect { * via the `definition` "menu". */ export interface Menu { - id: number; + id: string; globalText?: string | null; updatedAt?: string | null; createdAt?: string | null; diff --git a/test/dev.ts b/test/dev.ts index 578fd8e5e..368ed124b 100644 --- a/test/dev.ts +++ b/test/dev.ts @@ -16,6 +16,9 @@ import { runInit } from './runInit.js' import { child } from './safelyRunScript.js' import { createTestHooks } from './testHooks.js' +// @todo remove in 4.0 - will behave like this by default in 4.0 +process.env.PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY = 'true' + const prod = process.argv.includes('--prod') if (prod) { process.argv = process.argv.filter((arg) => arg !== '--prod') diff --git a/test/eslint.config.js b/test/eslint.config.js index 08a0b91f7..22b69b30a 100644 --- a/test/eslint.config.js +++ b/test/eslint.config.js @@ -8,7 +8,7 @@ import playwright from 'eslint-plugin-playwright' export const testEslintConfig = [ ...rootEslintConfig, { - ignores: [...defaultESLintIgnores, '**/payload-types.ts'], + ignores: [...defaultESLintIgnores, '**/payload-types.ts', 'jest.setup.js'], }, { languageOptions: { diff --git a/test/fields/baseConfig.ts b/test/fields/baseConfig.ts index 7d2db0db4..d185672e1 100644 --- a/test/fields/baseConfig.ts +++ b/test/fields/baseConfig.ts @@ -126,6 +126,26 @@ export const baseConfig: Partial = { }, ], }, + { + slug: 'localizedTextReference', + fields: [ + { + name: 'text', + type: 'text', + localized: true, + }, + ], + }, + { + slug: 'localizedTextReference2', + fields: [ + { + name: 'text', + type: 'text', + localized: true, + }, + ], + }, ], custom: { client: { diff --git a/test/fields/collections/Blocks/index.ts b/test/fields/collections/Blocks/index.ts index af8fdac10..db93506f2 100644 --- a/test/fields/collections/Blocks/index.ts +++ b/test/fields/collections/Blocks/index.ts @@ -409,6 +409,21 @@ const BlockFields: CollectionConfig = { blockReferences: ['ConfigBlockTest'], blocks: [], }, + { + name: 'localizedReferencesLocalizedBlock', + type: 'blocks', + blockReferences: ['localizedTextReference'], + blocks: [], + localized: true, + }, + { + name: 'localizedReferences', + type: 'blocks', + // Needs to be a separate block - otherwise this will break in postgres. This is unrelated to block references + // and an issue with all blocks. + blockReferences: ['localizedTextReference2'], + blocks: [], + }, ], } diff --git a/test/fields/collections/Blocks/shared.ts b/test/fields/collections/Blocks/shared.ts index da6431e5a..608729583 100644 --- a/test/fields/collections/Blocks/shared.ts +++ b/test/fields/collections/Blocks/shared.ts @@ -47,4 +47,16 @@ export const blocksDoc: Partial = { blockType: 'blockWithMinRows', }, ], + localizedReferencesLocalizedBlock: [ + { + blockType: 'localizedTextReference', + text: 'localized text', + }, + ], + localizedReferences: [ + { + blockType: 'localizedTextReference2', + text: 'localized text', + }, + ], } diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index b1bc3849f..c93712367 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -2,11 +2,11 @@ import type { MongooseAdapter } from '@payloadcms/db-mongodb' import type { IndexDirection, IndexOptions } from 'mongoose' import path from 'path' -import { type PaginatedDocs, type Payload, reload, ValidationError } from 'payload' +import { type PaginatedDocs, type Payload, reload } from 'payload' import { fileURLToPath } from 'url' import type { NextRESTClient } from '../helpers/NextRESTClient.js' -import type { GroupField, RichTextField } from './payload-types.js' +import type { BlockField, GroupField, RichTextField } from './payload-types.js' import { devUser } from '../credentials.js' import { initPayloadInt } from '../helpers/initPayloadInt.js' @@ -2562,6 +2562,32 @@ describe('Fields', () => { expect(result.blocksWithLocalizedArray[0].array[0].text).toEqual('localized') }) + + it('ensure localized field within block reference is saved correctly', async () => { + const blockFields = await payload.find({ + collection: 'block-fields', + locale: 'all', + }) + + const doc: BlockField = blockFields.docs[0] as BlockField + + expect(doc?.localizedReferences?.[0]?.blockType).toEqual('localizedTextReference2') + expect(doc?.localizedReferences?.[0]?.text).toEqual({ en: 'localized text' }) + }) + + it('ensure localized property is stripped from localized field within localized block reference', async () => { + const blockFields = await payload.find({ + collection: 'block-fields', + locale: 'all', + }) + + const doc: any = blockFields.docs[0] + + expect(doc?.localizedReferencesLocalizedBlock?.en?.[0]?.blockType).toEqual( + 'localizedTextReference', + ) + expect(doc?.localizedReferencesLocalizedBlock?.en?.[0]?.text).toEqual('localized text') + }) }) describe('collapsible', () => { diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 3ef225748..065c473c8 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -6,22 +6,6 @@ * and re-run `payload generate:types` to regenerate this file. */ -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "BlockColumns". - */ -export type BlockColumns = - | { - text?: string | null; - subArray?: - | { - requiredText: string; - id?: string | null; - }[] - | null; - id?: string | null; - }[] - | null; /** * Supported timezones in IANA format. * @@ -76,6 +60,22 @@ export type SupportedTimezones = | 'Pacific/Auckland' | 'Pacific/Fiji' | 'America/Monterrey'; +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "BlockColumns". + */ +export type BlockColumns = + | { + text?: string | null; + subArray?: + | { + requiredText: string; + id?: string | null; + }[] + | null; + id?: string | null; + }[] + | null; export interface Config { auth: { @@ -83,56 +83,8 @@ export interface Config { }; blocks: { ConfigBlockTest: ConfigBlockTest; - validationBlock: ValidationBlock; - filterOptionsBlock: FilterOptionsBlock; - asyncHooksBlock: AsyncHooksBlock; - richTextBlock: RichTextBlock; - textRequired: TextRequired; - uploadAndRichText: UploadAndRichText; - select: Select; - relationshipBlock: RelationshipBlock; - relationshipHasManyBlock: RelationshipHasManyBlock; - subBlockLexical: SubBlockLexical; - radioButtons: LexicalBlocksRadioButtonsBlock; - conditionalLayout: ConditionalLayout; - tabBlock: TabBlock; - code: Code; - myBlock: MyBlock; - myBlockWithLabel: MyBlockWithLabel; - myBlockWithBlock: MyBlockWithBlock; - BlockRSC: BlockRSC; - myBlockWithBlockAndLabel: MyBlockWithBlockAndLabel; - AvatarGroup: AvatarGroupBlock; - myInlineBlock: MyInlineBlock; - myInlineBlockWithLabel: MyInlineBlockWithLabel; - myInlineBlockWithBlock: MyInlineBlockWithBlock; - myInlineBlockWithBlockAndLabel: MyInlineBlockWithBlockAndLabel; - lexicalInBlock2: LexicalInBlock2; - block: Block; - content: ContentBlock; - number: NumberBlock; - subBlocks: SubBlocksBlock; - tabs: TabsBlock; - localizedContent: LocalizedContentBlock; - localizedNumber: LocalizedNumberBlock; - localizedSubBlocks: LocalizedSubBlocksBlock; - localizedTabs: LocalizedTabsBlock; - textInI18nBlock: TextInI18NBlock; - localizedArray: LocalizedArray; - 'block-a': BlockA; - 'block-b': BlockB; - 'group-block': GroupBlock; - blockWithMinRows: BlockWithMinRows; - 'block-1': Block1; - 'block-2': Block2; - relationships: Relationships; - text: Text; - blockWithConditionalField: BlockWithConditionalField; - dateBlock: DateBlock; - blockWithNumber: BlockWithNumber; - textBlock: TextBlock; - richTextBlockSlate: RichTextBlockSlate; - blockWithText: BlockWithText; + localizedTextReference: LocalizedTextReference; + localizedTextReference2: LocalizedTextReference2; }; collections: { 'lexical-fields': LexicalField; @@ -275,736 +227,23 @@ export interface ConfigBlockTest { } /** * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "validationBlock". + * via the `definition` "localizedTextReference". */ -export interface ValidationBlock { - text?: string | null; - group?: { - groupText?: string | null; - textDependsOnDocData?: string | null; - textDependsOnSiblingData?: string | null; - textDependsOnBlockData?: string | null; - }; - id?: string | null; - blockName?: string | null; - blockType: 'validationBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "filterOptionsBlock". - */ -export interface FilterOptionsBlock { - text?: string | null; - group?: { - groupText?: string | null; - dependsOnDocData?: (string | null) | TextField; - dependsOnSiblingData?: (string | null) | TextField; - dependsOnBlockData?: (string | null) | TextField; - }; - id?: string | null; - blockName?: string | null; - blockType: 'filterOptionsBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "text-fields". - */ -export interface TextField { - id: string; - text: string; - hiddenTextField?: string | null; - /** - * This field should be hidden - */ - adminHiddenTextField?: string | null; - /** - * This field should be disabled - */ - disabledTextField?: string | null; - localizedText?: string | null; - /** - * en description - */ - i18nText?: string | null; - defaultString?: string | null; - defaultEmptyString?: string | null; - defaultFunction?: string | null; - defaultAsync?: string | null; - overrideLength?: string | null; - fieldWithDefaultValue?: string | null; - dependentOnFieldWithDefaultValue?: string | null; - hasMany?: string[] | null; - readOnlyHasMany?: string[] | null; - validatesHasMany?: string[] | null; - localizedHasMany?: string[] | null; - withMinRows?: string[] | null; - withMaxRows?: string[] | null; - defaultValueFromReq?: string | null; - array?: - | { - texts?: string[] | null; - id?: string | null; - }[] - | null; - blocks?: BlockWithText[] | null; - updatedAt: string; - createdAt: string; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "blockWithText". - */ -export interface BlockWithText { - texts?: string[] | null; - id?: string | null; - blockName?: string | null; - blockType: 'blockWithText'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "asyncHooksBlock". - */ -export interface AsyncHooksBlock { - test1?: string | null; - test2?: string | null; - id?: string | null; - blockName?: string | null; - blockType: 'asyncHooksBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "richTextBlock". - */ -export interface RichTextBlock { - richTextField?: { - root: { - type: string; - children: { - type: string; - version: number; - [k: string]: unknown; - }[]; - direction: ('ltr' | 'rtl') | null; - format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; - indent: number; - version: number; - }; - [k: string]: unknown; - } | null; - id?: string | null; - blockName?: string | null; - blockType: 'richTextBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "textRequired". - */ -export interface TextRequired { - text: string; - id?: string | null; - blockName?: string | null; - blockType: 'textRequired'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "uploadAndRichText". - */ -export interface UploadAndRichText { - upload: string | Upload; - richText?: { - root: { - type: string; - children: { - type: string; - version: number; - [k: string]: unknown; - }[]; - direction: ('ltr' | 'rtl') | null; - format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; - indent: number; - version: number; - }; - [k: string]: unknown; - } | null; - id?: string | null; - blockName?: string | null; - blockType: 'uploadAndRichText'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "uploads". - */ -export interface Upload { - id: string; - text?: string | null; - media?: (string | null) | Upload; - updatedAt: string; - createdAt: string; - url?: string | null; - thumbnailURL?: string | null; - filename?: string | null; - mimeType?: string | null; - filesize?: number | null; - width?: number | null; - height?: number | null; - focalX?: number | null; - focalY?: number | null; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "select". - */ -export interface Select { - select?: ('option1' | 'option2' | 'option3' | 'option4' | 'option5') | null; - id?: string | null; - blockName?: string | null; - blockType: 'select'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "relationshipBlock". - */ -export interface RelationshipBlock { - rel: string | Upload; - id?: string | null; - blockName?: string | null; - blockType: 'relationshipBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "relationshipHasManyBlock". - */ -export interface RelationshipHasManyBlock { - rel: ( - | { - relationTo: 'text-fields'; - value: string | TextField; - } - | { - relationTo: 'uploads'; - value: string | Upload; - } - )[]; - id?: string | null; - blockName?: string | null; - blockType: 'relationshipHasManyBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "subBlockLexical". - */ -export interface SubBlockLexical { - subBlocksLexical?: - | ( - | { - richText: { - root: { - type: string; - children: { - type: string; - version: number; - [k: string]: unknown; - }[]; - direction: ('ltr' | 'rtl') | null; - format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; - indent: number; - version: number; - }; - [k: string]: unknown; - }; - id?: string | null; - blockName?: string | null; - blockType: 'contentBlock'; - } - | { - content: string; - id?: string | null; - blockName?: string | null; - blockType: 'textArea'; - } - | { - select?: ('option1' | 'option2' | 'option3' | 'option4' | 'option5') | null; - id?: string | null; - blockName?: string | null; - blockType: 'select'; - } - )[] - | null; - id?: string | null; - blockName?: string | null; - blockType: 'subBlockLexical'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "LexicalBlocksRadioButtonsBlock". - */ -export interface LexicalBlocksRadioButtonsBlock { - radioButtons?: ('option1' | 'option2' | 'option3') | null; - id?: string | null; - blockName?: string | null; - blockType: 'radioButtons'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "conditionalLayout". - */ -export interface ConditionalLayout { - layout: '1' | '2' | '3'; - columns?: BlockColumns; - columns2?: BlockColumns; - columns3?: BlockColumns; - id?: string | null; - blockName?: string | null; - blockType: 'conditionalLayout'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "tabBlock". - */ -export interface TabBlock { - tab1?: { - text1?: string | null; - }; - tab2?: { - text2?: string | null; - }; - id?: string | null; - blockName?: string | null; - blockType: 'tabBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "code". - */ -export interface Code { - code?: string | null; - id?: string | null; - blockName?: string | null; - blockType: 'code'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "myBlock". - */ -export interface MyBlock { - key?: ('value1' | 'value2' | 'value3') | null; - id?: string | null; - blockName?: string | null; - blockType: 'myBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "myBlockWithLabel". - */ -export interface MyBlockWithLabel { - key?: ('value1' | 'value2' | 'value3') | null; - id?: string | null; - blockName?: string | null; - blockType: 'myBlockWithLabel'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "myBlockWithBlock". - */ -export interface MyBlockWithBlock { - key?: ('value1' | 'value2' | 'value3') | null; - id?: string | null; - blockName?: string | null; - blockType: 'myBlockWithBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "BlockRSC". - */ -export interface BlockRSC { - key?: ('value1' | 'value2' | 'value3') | null; - id?: string | null; - blockName?: string | null; - blockType: 'BlockRSC'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "myBlockWithBlockAndLabel". - */ -export interface MyBlockWithBlockAndLabel { - key?: ('value1' | 'value2' | 'value3') | null; - id?: string | null; - blockName?: string | null; - blockType: 'myBlockWithBlockAndLabel'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "AvatarGroupBlock". - */ -export interface AvatarGroupBlock { - avatars?: - | { - image?: (string | null) | Upload; - id?: string | null; - }[] - | null; - id?: string | null; - blockName?: string | null; - blockType: 'AvatarGroup'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "myInlineBlock". - */ -export interface MyInlineBlock { - key?: ('value1' | 'value2' | 'value3') | null; - id?: string | null; - blockName?: string | null; - blockType: 'myInlineBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "myInlineBlockWithLabel". - */ -export interface MyInlineBlockWithLabel { - key?: ('value1' | 'value2' | 'value3') | null; - id?: string | null; - blockName?: string | null; - blockType: 'myInlineBlockWithLabel'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "myInlineBlockWithBlock". - */ -export interface MyInlineBlockWithBlock { - key?: ('value1' | 'value2' | 'value3') | null; - id?: string | null; - blockName?: string | null; - blockType: 'myInlineBlockWithBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "myInlineBlockWithBlockAndLabel". - */ -export interface MyInlineBlockWithBlockAndLabel { - key?: ('value1' | 'value2' | 'value3') | null; - id?: string | null; - blockName?: string | null; - blockType: 'myInlineBlockWithBlockAndLabel'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "lexicalInBlock2". - */ -export interface LexicalInBlock2 { - lexical?: { - root: { - type: string; - children: { - type: string; - version: number; - [k: string]: unknown; - }[]; - direction: ('ltr' | 'rtl') | null; - format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; - indent: number; - version: number; - }; - [k: string]: unknown; - } | null; - id?: string | null; - blockName?: string | null; - blockType: 'lexicalInBlock2'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "block". - */ -export interface Block { - hasManyBlocks?: ('a' | 'b' | 'c')[] | null; - id?: string | null; - blockName?: string | null; - blockType: 'block'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "ContentBlock". - */ -export interface ContentBlock { - text: string; - richText?: - | { - [k: string]: unknown; - }[] - | null; - id?: string | null; - blockName?: string | null; - blockType: 'content'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "NumberBlock". - */ -export interface NumberBlock { - number: number; - id?: string | null; - blockName?: string | null; - blockType: 'number'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "SubBlocksBlock". - */ -export interface SubBlocksBlock { - subBlocks?: - | ( - | { - text: string; - id?: string | null; - blockName?: string | null; - blockType: 'textRequired'; - } - | NumberBlock - )[] - | null; - id?: string | null; - blockName?: string | null; - blockType: 'subBlocks'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "TabsBlock". - */ -export interface TabsBlock { - textInCollapsible?: string | null; - textInRow?: string | null; - id?: string | null; - blockName?: string | null; - blockType: 'tabs'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "localizedContentBlock". - */ -export interface LocalizedContentBlock { - text: string; - richText?: - | { - [k: string]: unknown; - }[] - | null; - id?: string | null; - blockName?: string | null; - blockType: 'localizedContent'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "localizedNumberBlock". - */ -export interface LocalizedNumberBlock { - number: number; - id?: string | null; - blockName?: string | null; - blockType: 'localizedNumber'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "localizedSubBlocksBlock". - */ -export interface LocalizedSubBlocksBlock { - subBlocks?: - | ( - | { - text: string; - id?: string | null; - blockName?: string | null; - blockType: 'textRequired'; - } - | NumberBlock - )[] - | null; - id?: string | null; - blockName?: string | null; - blockType: 'localizedSubBlocks'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "localizedTabsBlock". - */ -export interface LocalizedTabsBlock { - textInCollapsible?: string | null; - textInRow?: string | null; - id?: string | null; - blockName?: string | null; - blockType: 'localizedTabs'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "textInI18nBlock". - */ -export interface TextInI18NBlock { +export interface LocalizedTextReference { text?: string | null; id?: string | null; blockName?: string | null; - blockType: 'textInI18nBlock'; + blockType: 'localizedTextReference'; } /** * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "localizedArray". + * via the `definition` "localizedTextReference2". */ -export interface LocalizedArray { - array?: - | { - text?: string | null; - id?: string | null; - }[] - | null; - id?: string | null; - blockName?: string | null; - blockType: 'localizedArray'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "block-a". - */ -export interface BlockA { - items?: - | { - title: string; - id?: string | null; - }[] - | null; - id?: string | null; - blockName?: string | null; - blockType: 'block-a'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "block-b". - */ -export interface BlockB { - items?: - | { - title2: string; - id?: string | null; - }[] - | null; - id?: string | null; - blockName?: string | null; - blockType: 'block-b'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "group-block". - */ -export interface GroupBlock { - group?: { - text?: string | null; - }; - id?: string | null; - blockName?: string | null; - blockType: 'group-block'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "blockWithMinRows". - */ -export interface BlockWithMinRows { - blockTitle?: string | null; - id?: string | null; - blockName?: string | null; - blockType: 'blockWithMinRows'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "block-1". - */ -export interface Block1 { - block1Title?: string | null; - id?: string | null; - blockName?: string | null; - blockType: 'block-1'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "block-2". - */ -export interface Block2 { - block2Title?: string | null; - id?: string | null; - blockName?: string | null; - blockType: 'block-2'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "relationships". - */ -export interface Relationships { - relationship?: (string | null) | TextField; - id?: string | null; - blockName?: string | null; - blockType: 'relationships'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "text". - */ -export interface Text { +export interface LocalizedTextReference2 { text?: string | null; id?: string | null; blockName?: string | null; - blockType: 'text'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "blockWithConditionalField". - */ -export interface BlockWithConditionalField { - text?: string | null; - textWithCondition?: string | null; - id?: string | null; - blockName?: string | null; - blockType: 'blockWithConditionalField'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "dateBlock". - */ -export interface DateBlock { - dayAndTime?: string | null; - dayAndTime_tz?: SupportedTimezones; - id?: string | null; - blockName?: string | null; - blockType: 'dateBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "blockWithNumber". - */ -export interface BlockWithNumber { - numbers?: number[] | null; - id?: string | null; - blockName?: string | null; - blockType: 'blockWithNumber'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "textBlock". - */ -export interface TextBlock { - text?: string | null; - id?: string | null; - blockName?: string | null; - blockType: 'textBlock'; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "richTextBlockSlate". - */ -export interface RichTextBlockSlate { - text?: - | { - [k: string]: unknown; - }[] - | null; - id?: string | null; - blockName?: string | null; - blockType: 'richTextBlockSlate'; + blockType: 'localizedTextReference2'; } /** * This interface was referenced by `Config`'s JSON-Schema @@ -1281,7 +520,28 @@ export interface LexicalInBlock { }; [k: string]: unknown; } | null; - blocks?: LexicalInBlock2[] | null; + blocks?: + | { + lexical?: { + root: { + type: string; + children: { + type: string; + version: number; + [k: string]: unknown; + }[]; + direction: ('ltr' | 'rtl') | null; + format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; + id?: string | null; + blockName?: string | null; + blockType: 'lexicalInBlock2'; + }[] + | null; updatedAt: string; createdAt: string; } @@ -1323,7 +583,14 @@ export interface SelectVersionsField { id?: string | null; }[] | null; - blocks?: Block[] | null; + blocks?: + | { + hasManyBlocks?: ('a' | 'b' | 'c')[] | null; + id?: string | null; + blockName?: string | null; + blockType: 'block'; + }[] + | null; updatedAt: string; createdAt: string; _status?: ('draft' | 'published') | null; @@ -1442,19 +709,295 @@ export interface BlockField { )[]; disableSort: (LocalizedContentBlock | LocalizedNumberBlock | LocalizedSubBlocksBlock | LocalizedTabsBlock)[]; localizedBlocks: (LocalizedContentBlock | LocalizedNumberBlock | LocalizedSubBlocksBlock | LocalizedTabsBlock)[]; - i18nBlocks?: TextInI18NBlock[] | null; - blocksWithLocalizedArray?: LocalizedArray[] | null; - blocksWithSimilarConfigs?: (BlockA | BlockB | GroupBlock)[] | null; + i18nBlocks?: + | { + text?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'textInI18nBlock'; + }[] + | null; + blocksWithLocalizedArray?: + | { + array?: + | { + text?: string | null; + id?: string | null; + }[] + | null; + id?: string | null; + blockName?: string | null; + blockType: 'localizedArray'; + }[] + | null; + blocksWithSimilarConfigs?: + | ( + | { + items?: + | { + title: string; + id?: string | null; + }[] + | null; + id?: string | null; + blockName?: string | null; + blockType: 'block-a'; + } + | { + items?: + | { + title2: string; + id?: string | null; + }[] + | null; + id?: string | null; + blockName?: string | null; + blockType: 'block-b'; + } + | { + group?: { + text?: string | null; + }; + id?: string | null; + blockName?: string | null; + blockType: 'group-block'; + } + )[] + | null; /** * The purpose of this field is to test validateExistingBlockIsIdentical works with similar blocks with group fields */ - blocksWithSimilarGroup?: (GroupBlock | BlockB)[] | null; - blocksWithMinRows?: BlockWithMinRows[] | null; - customBlocks?: (Block1 | Block2)[] | null; - relationshipBlocks?: Relationships[] | null; - blockWithLabels?: Text[] | null; + blocksWithSimilarGroup?: + | ( + | { + group?: { + text?: string | null; + }; + id?: string | null; + blockName?: string | null; + blockType: 'group-block'; + } + | { + items?: + | { + title2: string; + id?: string | null; + }[] + | null; + id?: string | null; + blockName?: string | null; + blockType: 'block-b'; + } + )[] + | null; + blocksWithMinRows?: + | { + blockTitle?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'blockWithMinRows'; + }[] + | null; + customBlocks?: + | ( + | { + block1Title?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'block-1'; + } + | { + block2Title?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'block-2'; + } + )[] + | null; + relationshipBlocks?: + | { + relationship?: (string | null) | TextField; + id?: string | null; + blockName?: string | null; + blockType: 'relationships'; + }[] + | null; + blockWithLabels?: + | { + text?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'text'; + }[] + | null; deduplicatedBlocks?: ConfigBlockTest[] | null; deduplicatedBlocks2?: ConfigBlockTest[] | null; + localizedReferencesLocalizedBlock?: LocalizedTextReference[] | null; + localizedReferences?: LocalizedTextReference2[] | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "ContentBlock". + */ +export interface ContentBlock { + text: string; + richText?: + | { + [k: string]: unknown; + }[] + | null; + id?: string | null; + blockName?: string | null; + blockType: 'content'; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "NumberBlock". + */ +export interface NumberBlock { + number: number; + id?: string | null; + blockName?: string | null; + blockType: 'number'; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "SubBlocksBlock". + */ +export interface SubBlocksBlock { + subBlocks?: + | ( + | { + text: string; + id?: string | null; + blockName?: string | null; + blockType: 'textRequired'; + } + | NumberBlock + )[] + | null; + id?: string | null; + blockName?: string | null; + blockType: 'subBlocks'; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "TabsBlock". + */ +export interface TabsBlock { + textInCollapsible?: string | null; + textInRow?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'tabs'; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "localizedContentBlock". + */ +export interface LocalizedContentBlock { + text: string; + richText?: + | { + [k: string]: unknown; + }[] + | null; + id?: string | null; + blockName?: string | null; + blockType: 'localizedContent'; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "localizedNumberBlock". + */ +export interface LocalizedNumberBlock { + number: number; + id?: string | null; + blockName?: string | null; + blockType: 'localizedNumber'; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "localizedSubBlocksBlock". + */ +export interface LocalizedSubBlocksBlock { + subBlocks?: + | ( + | { + text: string; + id?: string | null; + blockName?: string | null; + blockType: 'textRequired'; + } + | NumberBlock + )[] + | null; + id?: string | null; + blockName?: string | null; + blockType: 'localizedSubBlocks'; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "localizedTabsBlock". + */ +export interface LocalizedTabsBlock { + textInCollapsible?: string | null; + textInRow?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'localizedTabs'; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "text-fields". + */ +export interface TextField { + id: string; + text: string; + hiddenTextField?: string | null; + /** + * This field should be hidden + */ + adminHiddenTextField?: string | null; + /** + * This field should be disabled + */ + disabledTextField?: string | null; + localizedText?: string | null; + /** + * en description + */ + i18nText?: string | null; + defaultString?: string | null; + defaultEmptyString?: string | null; + defaultFunction?: string | null; + defaultAsync?: string | null; + overrideLength?: string | null; + fieldWithDefaultValue?: string | null; + dependentOnFieldWithDefaultValue?: string | null; + hasMany?: string[] | null; + readOnlyHasMany?: string[] | null; + validatesHasMany?: string[] | null; + localizedHasMany?: string[] | null; + withMinRows?: string[] | null; + withMaxRows?: string[] | null; + defaultValueFromReq?: string | null; + array?: + | { + texts?: string[] | null; + id?: string | null; + }[] + | null; + blocks?: + | { + texts?: string[] | null; + id?: string | null; + blockName?: string | null; + blockType: 'blockWithText'; + }[] + | null; updatedAt: string; createdAt: string; } @@ -1570,7 +1113,15 @@ export interface ConditionalLogic { id?: string | null; }[] | null; - blocksWithConditionalField?: BlockWithConditionalField[] | null; + blocksWithConditionalField?: + | { + text?: string | null; + textWithCondition?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'blockWithConditionalField'; + }[] + | null; updatedAt: string; createdAt: string; } @@ -1620,7 +1171,15 @@ export interface DateField { */ dayAndTimeWithTimezone: string; dayAndTimeWithTimezone_tz: SupportedTimezones; - timezoneBlocks?: DateBlock[] | null; + timezoneBlocks?: + | { + dayAndTime?: string | null; + dayAndTime_tz?: SupportedTimezones; + id?: string | null; + blockName?: string | null; + blockType: 'dateBlock'; + }[] + | null; timezoneArray?: | { dayAndTime?: string | null; @@ -1911,7 +1470,14 @@ export interface NumberField { id?: string | null; }[] | null; - blocks?: BlockWithNumber[] | null; + blocks?: + | { + numbers?: number[] | null; + id?: string | null; + blockName?: string | null; + blockType: 'blockWithNumber'; + }[] + | null; updatedAt: string; createdAt: string; } @@ -2095,7 +1661,26 @@ export interface RichTextField { [k: string]: unknown; }[] | null; - blocks?: (TextBlock | RichTextBlockSlate)[] | null; + blocks?: + | ( + | { + text?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'textBlock'; + } + | { + text?: + | { + [k: string]: unknown; + }[] + | null; + id?: string | null; + blockName?: string | null; + blockType: 'richTextBlockSlate'; + } + )[] + | null; updatedAt: string; createdAt: string; } @@ -2229,6 +1814,26 @@ export interface TabWithName { }[] | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "uploads". + */ +export interface Upload { + id: string; + text?: string | null; + media?: (string | null) | Upload; + updatedAt: string; + createdAt: string; + url?: string | null; + thumbnailURL?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "uploads2". @@ -2638,7 +2243,17 @@ export interface UsersSelect { */ export interface LexicalInBlockSelect { content?: T; - blocks?: T | {}; + blocks?: + | T + | { + lexicalInBlock2?: + | T + | { + lexical?: T; + id?: T; + blockName?: T; + }; + }; updatedAt?: T; createdAt?: T; } @@ -2664,7 +2279,17 @@ export interface SelectVersionsFieldsSelect { hasManyArr?: T; id?: T; }; - blocks?: T | {}; + blocks?: + | T + | { + block?: + | T + | { + hasManyBlocks?: T; + id?: T; + blockName?: T; + }; + }; updatedAt?: T; createdAt?: T; _status?: T; @@ -2774,9 +2399,30 @@ export interface ArrayFieldsSelect { * via the `definition` "block-fields_select". */ export interface BlockFieldsSelect { - blocks?: T | {}; - duplicate?: T | {}; - collapsedByDefaultBlocks?: T | {}; + blocks?: + | T + | { + content?: T | ContentBlockSelect; + number?: T | NumberBlockSelect; + subBlocks?: T | SubBlocksBlockSelect; + tabs?: T | TabsBlockSelect; + }; + duplicate?: + | T + | { + content?: T | ContentBlockSelect; + number?: T | NumberBlockSelect; + subBlocks?: T | SubBlocksBlockSelect; + tabs?: T | TabsBlockSelect; + }; + collapsedByDefaultBlocks?: + | T + | { + localizedContent?: T | LocalizedContentBlockSelect; + localizedNumber?: T | LocalizedNumberBlockSelect; + localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect; + localizedTabs?: T | LocalizedTabsBlockSelect; + }; disableSort?: | T | { @@ -2785,20 +2431,214 @@ export interface BlockFieldsSelect { localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect; localizedTabs?: T | LocalizedTabsBlockSelect; }; - localizedBlocks?: T | {}; - i18nBlocks?: T | {}; - blocksWithLocalizedArray?: T | {}; - blocksWithSimilarConfigs?: T | {}; - blocksWithSimilarGroup?: T | {}; - blocksWithMinRows?: T | {}; - customBlocks?: T | {}; - relationshipBlocks?: T | {}; - blockWithLabels?: T | {}; + localizedBlocks?: + | T + | { + localizedContent?: T | LocalizedContentBlockSelect; + localizedNumber?: T | LocalizedNumberBlockSelect; + localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect; + localizedTabs?: T | LocalizedTabsBlockSelect; + }; + i18nBlocks?: + | T + | { + textInI18nBlock?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + }; + blocksWithLocalizedArray?: + | T + | { + localizedArray?: + | T + | { + array?: + | T + | { + text?: T; + id?: T; + }; + id?: T; + blockName?: T; + }; + }; + blocksWithSimilarConfigs?: + | T + | { + 'block-a'?: + | T + | { + items?: + | T + | { + title?: T; + id?: T; + }; + id?: T; + blockName?: T; + }; + 'block-b'?: + | T + | { + items?: + | T + | { + title2?: T; + id?: T; + }; + id?: T; + blockName?: T; + }; + 'group-block'?: + | T + | { + group?: + | T + | { + text?: T; + }; + id?: T; + blockName?: T; + }; + }; + blocksWithSimilarGroup?: + | T + | { + 'group-block'?: + | T + | { + group?: + | T + | { + text?: T; + }; + id?: T; + blockName?: T; + }; + 'block-b'?: + | T + | { + items?: + | T + | { + title2?: T; + id?: T; + }; + id?: T; + blockName?: T; + }; + }; + blocksWithMinRows?: + | T + | { + blockWithMinRows?: + | T + | { + blockTitle?: T; + id?: T; + blockName?: T; + }; + }; + customBlocks?: + | T + | { + 'block-1'?: + | T + | { + block1Title?: T; + id?: T; + blockName?: T; + }; + 'block-2'?: + | T + | { + block2Title?: T; + id?: T; + blockName?: T; + }; + }; + relationshipBlocks?: + | T + | { + relationships?: + | T + | { + relationship?: T; + id?: T; + blockName?: T; + }; + }; + blockWithLabels?: + | T + | { + text?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + }; deduplicatedBlocks?: T | {}; deduplicatedBlocks2?: T | {}; + localizedReferencesLocalizedBlock?: T | {}; + localizedReferences?: T | {}; updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "ContentBlock_select". + */ +export interface ContentBlockSelect { + text?: T; + richText?: T; + id?: T; + blockName?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "NumberBlock_select". + */ +export interface NumberBlockSelect { + number?: T; + id?: T; + blockName?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "SubBlocksBlock_select". + */ +export interface SubBlocksBlockSelect { + subBlocks?: + | T + | { + textRequired?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + number?: T | NumberBlockSelect; + }; + id?: T; + blockName?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "TabsBlock_select". + */ +export interface TabsBlockSelect { + textInCollapsible?: T; + textInRow?: T; + id?: T; + blockName?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "localizedContentBlock_select". @@ -2838,15 +2678,6 @@ export interface LocalizedSubBlocksBlockSelect { id?: T; blockName?: T; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "NumberBlock_select". - */ -export interface NumberBlockSelect { - number?: T; - id?: T; - blockName?: T; -} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "localizedTabsBlock_select". @@ -2959,7 +2790,18 @@ export interface ConditionalLogicSelect { textWithCondition?: T; id?: T; }; - blocksWithConditionalField?: T | {}; + blocksWithConditionalField?: + | T + | { + blockWithConditionalField?: + | T + | { + text?: T; + textWithCondition?: T; + id?: T; + blockName?: T; + }; + }; updatedAt?: T; createdAt?: T; } @@ -3005,7 +2847,18 @@ export interface DateFieldsSelect { defaultWithTimezone_tz?: T; dayAndTimeWithTimezone?: T; dayAndTimeWithTimezone_tz?: T; - timezoneBlocks?: T | {}; + timezoneBlocks?: + | T + | { + dateBlock?: + | T + | { + dayAndTime?: T; + dayAndTime_tz?: T; + id?: T; + blockName?: T; + }; + }; timezoneArray?: | T | { @@ -3275,7 +3128,17 @@ export interface NumberFieldsSelect { numbers?: T; id?: T; }; - blocks?: T | {}; + blocks?: + | T + | { + blockWithNumber?: + | T + | { + numbers?: T; + id?: T; + blockName?: T; + }; + }; updatedAt?: T; createdAt?: T; } @@ -3346,7 +3209,24 @@ export interface RichTextFieldsSelect { richText?: T; richTextCustomFields?: T; richTextReadOnly?: T; - blocks?: T | {}; + blocks?: + | T + | { + textBlock?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + richTextBlockSlate?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + }; updatedAt?: T; createdAt?: T; } @@ -3477,46 +3357,6 @@ export interface TabsFieldsSelect { updatedAt?: T; createdAt?: T; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "ContentBlock_select". - */ -export interface ContentBlockSelect { - text?: T; - richText?: T; - id?: T; - blockName?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "SubBlocksBlock_select". - */ -export interface SubBlocksBlockSelect { - subBlocks?: - | T - | { - textRequired?: - | T - | { - text?: T; - id?: T; - blockName?: T; - }; - number?: T | NumberBlockSelect; - }; - id?: T; - blockName?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "TabsBlock_select". - */ -export interface TabsBlockSelect { - textInCollapsible?: T; - textInRow?: T; - id?: T; - blockName?: T; -} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "TabWithName_select". @@ -3568,7 +3408,17 @@ export interface TextFieldsSelect { texts?: T; id?: T; }; - blocks?: T | {}; + blocks?: + | T + | { + blockWithText?: + | T + | { + texts?: T; + id?: T; + blockName?: T; + }; + }; updatedAt?: T; createdAt?: T; } @@ -3773,6 +3623,31 @@ export interface TabsWithRichTextSelect { createdAt?: T; globalType?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "LexicalBlocksRadioButtonsBlock". + */ +export interface LexicalBlocksRadioButtonsBlock { + radioButtons?: ('option1' | 'option2' | 'option3') | null; + id?: string | null; + blockName?: string | null; + blockType: 'radioButtons'; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "AvatarGroupBlock". + */ +export interface AvatarGroupBlock { + avatars?: + | { + image?: (string | null) | Upload; + id?: string | null; + }[] + | null; + id?: string | null; + blockName?: string | null; + blockType: 'AvatarGroup'; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "auth". diff --git a/test/helpers/autoDedupeBlocksPlugin/index.ts b/test/helpers/autoDedupeBlocksPlugin/index.ts index 1072ca435..b8ca31027 100644 --- a/test/helpers/autoDedupeBlocksPlugin/index.ts +++ b/test/helpers/autoDedupeBlocksPlugin/index.ts @@ -16,6 +16,7 @@ export const autoDedupeBlocksPlugin = traverseFields({ config, leavesFirst: true, + parentIsLocalized: false, isTopLevel: true, fields: [ ...(config.collections?.length diff --git a/test/jest.setup.js b/test/jest.setup.js index c4d855b97..b92a4fc53 100644 --- a/test/jest.setup.js +++ b/test/jest.setup.js @@ -16,6 +16,8 @@ process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER = 's3' process.env.NODE_OPTIONS = '--no-deprecation' process.env.PAYLOAD_CI_DEPENDENCY_CHECKER = 'true' +// @todo remove in 4.0 - will behave like this by default in 4.0 +process.env.PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY = 'true' // Mock createTestAccount to prevent calling external services jest.spyOn(nodemailer, 'createTestAccount').mockImplementation(() => { diff --git a/test/joins/int.spec.ts b/test/joins/int.spec.ts index b2ca5a7c3..9256e201b 100644 --- a/test/joins/int.spec.ts +++ b/test/joins/int.spec.ts @@ -213,6 +213,7 @@ describe('Joins Field', () => { }) expect(categoryWithPosts.arrayPosts.docs).toBeDefined() + expect(categoryWithPosts.arrayPosts.docs).toHaveLength(10) }) it('should populate joins with localized array relationships', async () => { diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index 628ac69ca..b134bf586 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -276,7 +276,7 @@ describe('Localization', () => { expect(localized.title.es).toEqual(spanishTitle) }) - it('REST all locales with all', async () => { + it('rest all locales with all', async () => { const response = await restClient.GET(`/${collection}/${localizedPost.id}`, { query: { locale: 'all', @@ -290,7 +290,7 @@ describe('Localization', () => { expect(localized.title.es).toEqual(spanishTitle) }) - it('REST all locales with asterisk', async () => { + it('rest all locales with asterisk', async () => { const response = await restClient.GET(`/${collection}/${localizedPost.id}`, { query: { locale: '*', @@ -1869,14 +1869,16 @@ describe('Localization', () => { }) }) + // Nested localized fields do no longer have their localized property stripped in + // this monorepo, as this is handled at runtime. describe('nested localized field sanitization', () => { - it('should sanitize nested localized fields', () => { + it('ensure nested localized fields keep localized property in monorepo', () => { 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() + expect(collection.fields[0].tabs[0].fields[0].localized).toBeDefined() + expect(collection.fields[1].fields[0].localized).toBeDefined() + expect(collection.fields[2].blocks[0].fields[0].localized).toBeDefined() + expect(collection.fields[3].fields[0].localized).toBeDefined() }) }) diff --git a/test/next.config.mjs b/test/next.config.mjs index 021d8343f..65697b4df 100644 --- a/test/next.config.mjs +++ b/test/next.config.mjs @@ -27,6 +27,8 @@ export default withBundleAnalyzer( env: { PAYLOAD_CORE_DEV: 'true', ROOT_DIR: path.resolve(dirname), + // @todo remove in 4.0 - will behave like this by default in 4.0 + PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY: 'true', }, async redirects() { return [ diff --git a/test/runE2E.ts b/test/runE2E.ts index f6b41bf82..f176bdbec 100644 --- a/test/runE2E.ts +++ b/test/runE2E.ts @@ -9,6 +9,9 @@ import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url) const dirname = path.dirname(__filename) +// @todo remove in 4.0 - will behave like this by default in 4.0 +process.env.PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY = 'true' + shelljs.env.DISABLE_LOGGING = 'true' const prod = process.argv.includes('--prod') diff --git a/tsconfig.base.json b/tsconfig.base.json index ffd7ec771..115d214e7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -16,13 +16,21 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "jsx": "preserve", - "lib": ["DOM", "DOM.Iterable", "ES2022"], + "lib": [ + "DOM", + "DOM.Iterable", + "ES2022" + ], "outDir": "${configDir}/dist", "resolveJsonModule": true, "skipLibCheck": true, "emitDeclarationOnly": true, "sourceMap": true, - "types": ["jest", "node", "@types/jest"], + "types": [ + "jest", + "node", + "@types/jest" + ], "incremental": true, "isolatedModules": true, "plugins": [ @@ -43,19 +51,33 @@ "@payloadcms/richtext-lexical/client": [ "./packages/richtext-lexical/src/exports/client/index.ts" ], - "@payloadcms/richtext-lexical/rsc": ["./packages/richtext-lexical/src/exports/server/rsc.ts"], - "@payloadcms/richtext-slate/rsc": ["./packages/richtext-slate/src/exports/server/rsc.ts"], + "@payloadcms/richtext-lexical/rsc": [ + "./packages/richtext-lexical/src/exports/server/rsc.ts" + ], + "@payloadcms/richtext-slate/rsc": [ + "./packages/richtext-slate/src/exports/server/rsc.ts" + ], "@payloadcms/richtext-slate/client": [ "./packages/richtext-slate/src/exports/client/index.ts" ], - "@payloadcms/plugin-seo/client": ["./packages/plugin-seo/src/exports/client.ts"], - "@payloadcms/plugin-sentry/client": ["./packages/plugin-sentry/src/exports/client.ts"], - "@payloadcms/plugin-stripe/client": ["./packages/plugin-stripe/src/exports/client.ts"], - "@payloadcms/plugin-search/client": ["./packages/plugin-search/src/exports/client.ts"], + "@payloadcms/plugin-seo/client": [ + "./packages/plugin-seo/src/exports/client.ts" + ], + "@payloadcms/plugin-sentry/client": [ + "./packages/plugin-sentry/src/exports/client.ts" + ], + "@payloadcms/plugin-stripe/client": [ + "./packages/plugin-stripe/src/exports/client.ts" + ], + "@payloadcms/plugin-search/client": [ + "./packages/plugin-search/src/exports/client.ts" + ], "@payloadcms/plugin-form-builder/client": [ "./packages/plugin-form-builder/src/exports/client.ts" ], - "@payloadcms/plugin-multi-tenant/rsc": ["./packages/plugin-multi-tenant/src/exports/rsc.ts"], + "@payloadcms/plugin-multi-tenant/rsc": [ + "./packages/plugin-multi-tenant/src/exports/rsc.ts" + ], "@payloadcms/plugin-multi-tenant/utilities": [ "./packages/plugin-multi-tenant/src/exports/utilities.ts" ], @@ -65,10 +87,21 @@ "@payloadcms/plugin-multi-tenant/client": [ "./packages/plugin-multi-tenant/src/exports/client.ts" ], - "@payloadcms/plugin-multi-tenant": ["./packages/plugin-multi-tenant/src/index.ts"], - "@payloadcms/next": ["./packages/next/src/exports/*"] + "@payloadcms/plugin-multi-tenant": [ + "./packages/plugin-multi-tenant/src/index.ts" + ], + "@payloadcms/next": [ + "./packages/next/src/exports/*" + ] } }, - "include": ["${configDir}/src"], - "exclude": ["${configDir}/dist", "${configDir}/build", "${configDir}/temp", "**/*.spec.ts"] + "include": [ + "${configDir}/src" + ], + "exclude": [ + "${configDir}/dist", + "${configDir}/build", + "${configDir}/temp", + "**/*.spec.ts" + ] }