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 9356438674..b63fa17cc4 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 0452bd34fa..c05dd022f1 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 554adb5c3a..e24d4f5da9 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 3042efbe33..ca28ddf941 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 4801943cb1..20c3e22fd8 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 08059892b0..af7f501585 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 926eaf3d08..11d5f4b4fc 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 9ac0529679..8cc40e8a7f 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 64cde6af7a..d468bd5e69 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 617a1e1fde..736707e9e4 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 6f76cbffef..d58dcc6e0a 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 1fcf5a341d..48c23505cf 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 b27a9b5458..45b544ac1d 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 60292c7cf7..3b7dfd085b 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 50895a0392..0b08f7672e 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 c7445cfaff..5ef4f09b89 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 948b7b29b9..cb699cc294 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 6a98b8c4ef..9f2a1173d8 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 c59687c5c1..759eb843d5 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 81c6e6ab64..da779bbb20 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 95dc2a75ab..a072a07e8d 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 5717a6507e..3e9dc92f74 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 705ff2acde..a6182fb668 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 048112245f..045c7fd251 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 701d505211..101118f182 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 67b8be7129..d5243366a5 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 9c00af0774..0d677c0db2 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 d7078cc5f7..c9cd95a48b 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 e0071d4d13..b6d7fad482 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 92e714d43f..65589e6a3d 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 faa03cf3b0..2e45a3b12d 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 ed011d82a5..7fa8430cc6 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 11f6b6da5c..80f39cdc40 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 32309020a1..033ab38d6f 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 f4243485f0..6e026e7342 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 2b88c1ce3d..6beca8f91d 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 3194b074e6..e695ef3ec2 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 37794582e3..5ca20b065d 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 dd2ba85c69..88ad61568d 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 5cec0a2335..b77f8f229c 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 81f58b6592..1e642fb5c3 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 b6b54686f5..7accde08a0 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 30349405e0..fe6c204125 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 136406a068..5739fb64d9 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 24e8cdd054..678479ca52 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 75dd19582b..fb71f64fd7 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 00cc020b97..b8a437e4d1 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 3470e25496..211837168b 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 deda5cf7e7..4cb614dbc9 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 9d4bac05ce..6812ba569b 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 b58b1a140c..89253a58fd 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 7b8bd0924f..41aa07e0a2 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 b9d6f7f5a5..07a9e9ea62 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 ef2c2d6074..6d5c4dac94 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 6fda8b6462..6374dee999 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 7546180a94..afbc060ff8 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 f89c9ef409..f3f6430bee 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 6c1bdee457..aa79294005 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 3c1452d711..1a6db09b3c 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 4f72c9c9c8..63bda81e3f 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 8d694db6f0..23b1959e65 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 223fce92e0..822a1d3451 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 84427ffd1b..7c87add2cf 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 81ecd5a2e2..df3da5dabc 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 0be10e8409..5f45d85c26 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 076e930363..56ced31970 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 bb53cf8fb5..3738984f14 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 e715b16cbf..cf284c2319 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 c685b778d1..abde171bd6 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 4a0910ae42..9a71214d52 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 d95aa0b962..b289167bc4 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 78b3569ce5..65e49a5768 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 967193021a..fbc92f5a4e 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 1072775bcb..5917f6db85 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 c8c594a31f..523ea8b133 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 97f2314e50..034bc7de7e 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 2249be3ba2..0f61fdd545 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 6ab548a82a..63c01f7a55 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 7874bd7eec..b8456e3b25 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 095ae14a59..1b1d66d523 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 c2a599bb38..35ee04c646 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 e45ad73f45..cf1b7bdc58 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 53c8bca0ae..61bff083d8 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 56f5378c52..73a6c30cdf 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 d7c212937e..22d3c6cfc3 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 140fb59485..a99e6444c9 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 7752f9b43a..2aca3b914c 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 2984c34f29..2fbc404e31 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 4d682afd58..eaa5dd9f23 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 578fd8e5e6..368ed124be 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 08a0b91f74..22b69b30a5 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 7d2db0db4d..d185672e16 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 af8fdac104..db93506f28 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 da6431e5a5..6087295831 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 b1bc3849f4..c937123679 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 3ef2257488..065c473c8d 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 1072ca4358..b8ca31027b 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 c4d855b97b..b92a4fc53b 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 b2ca5a7c35..9256e201bf 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 628ac69ca2..b134bf586f 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 021d8343fc..65697b4dfa 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 f6b41bf82e..f176bdbecb 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 ffd7ec771c..115d214e77 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" + ] }