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" + ] }