fix: localized fields within block references were not handled properly if any parent is localized (#11207)

The `localized` properly was not stripped out of referenced block fields, if any parent was localized. For normal fields, this is done in sanitizeConfig. As the same referenced block config can be used in both a localized and non-localized config, we are not able to strip it out inside sanitizeConfig by modifying the block config.

Instead, this PR had to bring back tedious logic to handle it everywhere the `field.localized` property is accessed. For backwards-compatibility, we need to keep the existing sanitizeConfig logic. In 4.0, we should remove it to benefit from better test coverage of runtime field.localized handling - for now, this is done for our test suite using the `PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY` flag.
This commit is contained in:
Alessio Gravili
2025-02-17 12:50:32 -07:00
committed by GitHub
parent 749962a1db
commit e6fea1d132
105 changed files with 2036 additions and 1347 deletions

View File

@@ -23,6 +23,7 @@ export const defaultESLintIgnores = [
'next-env.d.ts',
'**/app',
'src/**/*.spec.ts',
'**/jest.setup.js',
]
/** @typedef {import('eslint').Linter.Config} Config */

View File

@@ -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 [

View File

@@ -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<any, PaginateOptions>(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<any, PaginateOptions>(paginate, { useEstimatedCount: true }).plugin(

View File

@@ -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)) {

View File

@@ -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)
})

View File

@@ -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<unknown> = {
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<string, FieldSchemaGenerator> = {
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<string, FieldSchemaGenerator> = {
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<string, FieldSchemaGenerator> = {
},
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<string, FieldSchemaGenerator> = {
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<string, FieldSchemaGenerator> = {
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<unknown> = {
type: {
type: String,
@@ -363,12 +409,21 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
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<string, FieldSchemaGenerator> = {
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<string, FieldSchemaGenerator> = {
}
}
},
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<string, FieldSchemaGenerator> = {
}
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<string, FieldSchemaGenerator> = {
}
} else {
localeSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: valueType,
ref: field.relationTo,
}
@@ -453,7 +509,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} 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<string, FieldSchemaGenerator> = {
}
} else {
schemaToReturn = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: valueType,
ref: field.relationTo,
}
@@ -490,25 +546,26 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
},
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<string, FieldSchemaGenerator> = {
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<string, FieldSchemaGenerator> = {
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<string, FieldSchemaGenerator> = {
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<string, FieldSchemaGenerator> = {
}
} else {
localeSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: valueType,
ref: field.relationTo,
}
@@ -663,7 +731,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} 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<string, FieldSchemaGenerator> = {
}
} else {
schemaToReturn = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: valueType,
ref: field.relationTo,
}

View File

@@ -13,12 +13,14 @@ const migrateModelWithBatching = async ({
config,
fields,
Model,
parentIsLocalized,
session,
}: {
batchSize: number
config: SanitizedConfig
fields: Field[]
Model: Model<any>
parentIsLocalized: boolean
session: ClientSession
}): Promise<void> => {
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,
})

View File

@@ -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<Record<string, unknown>[]> {
@@ -29,6 +31,7 @@ export async function buildAndOrConditions({
fields,
globalSlug,
locale,
parentIsLocalized,
payload,
where: condition,
})

View File

@@ -47,6 +47,7 @@ export const getBuildQueryPlugin = ({
fields,
globalSlug,
locale,
parentIsLocalized: false,
payload,
where,
})

View File

@@ -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<SearchParam> {
@@ -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,

View File

@@ -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

View File

@@ -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,
})

View File

@@ -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<Record<string, unknown>> {
@@ -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],
})

View File

@@ -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}`
}

View File

@@ -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

View File

@@ -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<string, true>
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>', 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<string, true>
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}<locale>.`
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,
})
}

View File

@@ -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<string, unknown>
fields: Field[]
parentIsLocalized?: boolean
}
interface RelationObject {
@@ -112,6 +113,7 @@ export const sanitizeRelationshipIDs = ({
config,
data,
fields,
parentIsLocalized,
}: Args): Record<string, unknown> => {
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
}

View File

@@ -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<string, unknown>
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

View File

@@ -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<string, GenericColumn>
selectLocale?: boolean
tableName: string
@@ -42,6 +44,7 @@ export function buildAndOrConditions({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
selectLocale,
tableName,

View File

@@ -15,6 +15,7 @@ type Args = {
fields: FlattenedField[]
joins: BuildQueryJoinAliases
locale?: string
parentIsLocalized: boolean
selectFields: Record<string, GenericColumn>
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,

View File

@@ -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,

View File

@@ -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<string, GenericColumn>
@@ -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]

View File

@@ -20,6 +20,7 @@ type Args = {
fields: FlattenedField[]
joins: BuildQueryJoinAliases
locale: string
parentIsLocalized: boolean
selectFields: Record<string, GenericColumn>
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,

View File

@@ -37,6 +37,7 @@ type Args = {
disableRelsTableUnique?: boolean
disableUnique: boolean
fields: FlattenedField[]
parentIsLocalized: boolean
rootRelationships?: Set<string>
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,

View File

@@ -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,

View File

@@ -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<string, RawColumn>
localesIndexes: Record<string, RawIndex>
newTableName: string
parentIsLocalized: boolean
parentTableName: string
relationships: Set<string>
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

View File

@@ -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 = <T extends Record<string, unknown> | TypeWithID>({
data,
fields,
joinQuery,
parentIsLocalized,
}: TransformArgs): T => {
let relationships: Record<string, Record<string, unknown>[]> = {}
let texts: Record<string, Record<string, unknown>[]> = {}
@@ -59,6 +61,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
fields,
joinQuery,
numbers,
parentIsLocalized,
path: '',
relationships,
table: data,

View File

@@ -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<string, Record<string, unknown>[]>
parentIsLocalized: boolean
/**
* The current field path (in dot notation), used to merge in relationships
*/
@@ -80,6 +81,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
fields,
joinQuery,
numbers,
parentIsLocalized,
path,
relationships,
table,
@@ -105,9 +107,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
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 = <T extends Record<string, unknown>>({
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 = <T extends Record<string, unknown>>({
fieldPrefix: '',
fields: field.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${sanitizedPath}${field.name}.${i}`,
relationships,
table: row,
@@ -197,7 +203,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
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 = <T extends Record<string, unknown>>({
fieldPrefix: '',
fields: block.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${blockFieldPath}.${row._order - 1}`,
relationships,
table: row,
@@ -303,6 +310,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
fieldPrefix: '',
fields: block.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${blockFieldPath}.${i}`,
relationships,
table: row,
@@ -328,7 +336,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
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 = <T extends Record<string, unknown>>({
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 = <T extends Record<string, unknown>>({
return result
}
if (field.localized) {
if (isLocalized) {
result[field.name] = {}
const relationsByLocale: Record<string, Record<string, unknown>[]> = {}
@@ -402,7 +410,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
| { docs: unknown[]; hasNextPage: boolean }
| Record<string, { docs: unknown[]; hasNextPage: boolean }>
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 = <T extends Record<string, unknown>>({
return result
}
if (field.localized) {
if (isLocalized) {
result[field.name] = {}
const textsByLocale: Record<string, Record<string, unknown>[]> = {}
@@ -485,7 +493,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
return result
}
if (field.localized) {
if (isLocalized) {
result[field.name] = {}
const numbersByLocale: Record<string, Record<string, unknown>[]> = {}
@@ -520,7 +528,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
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 = <T extends Record<string, unknown>>({
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 = <T extends Record<string, unknown>>({
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<Record<string, unknown>>({
@@ -595,6 +603,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
fieldPrefix: groupFieldPrefix,
fields: field.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${sanitizedPath}${field.name}`,
relationships,
table,

View File

@@ -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<string, unknown>[]
parentIsLocalized: boolean
path: string
relationships: Record<string, unknown>[]
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,

View File

@@ -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<string, unknown>[]
parentIsLocalized: boolean
path: string
relationships: Record<string, unknown>[]
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,

View File

@@ -9,6 +9,7 @@ type Args = {
adapter: DrizzleAdapter
data: Record<string, unknown>
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,

View File

@@ -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<string, unknown>
}
numbers: Record<string, unknown>[]
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]) {

View File

@@ -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
})

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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) {

View File

@@ -45,6 +45,7 @@ export function initGlobals({ config, graphqlResult }: InitGlobalsGraphQLArgs):
config,
fields,
graphqlResult,
parentIsLocalized: false,
parentName: formattedName,
})
graphqlResult.globals.graphQL[slug] = {

View File

@@ -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
)

View File

@@ -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

View File

@@ -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<Props> = ({
isIterable = false,
label,
locales,
parentIsLocalized,
version,
}) => {
const { t } = useTranslation()
@@ -74,6 +77,7 @@ export const DiffCollapser: React.FC<Props> = ({
config,
field,
locales,
parentIsLocalized,
versionRows,
})
} else {
@@ -82,6 +86,7 @@ export const DiffCollapser: React.FC<Props> = ({
config,
fields,
locales,
parentIsLocalized,
version,
})
}

View File

@@ -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,
}

View File

@@ -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' && <span>{getTranslation(field.label, i18n)}</span>
}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized || field.localized}
version={versionValue}
>
<RenderVersionFieldsToDiff versionFields={baseVersionField.fields} />

View File

@@ -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}
>
<RenderVersionFieldsToDiff versionFields={baseVersionField.fields} />

View File

@@ -22,6 +22,7 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
comparisonValue,
field,
locale,
parentIsLocalized,
versionValue,
}) => {
const { i18n } = useTranslation()
@@ -53,6 +54,7 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
)
}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized}
version={versionValue}
>
{maxRows > 0 && (
@@ -80,6 +82,7 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
fields={fields}
label={rowLabel}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized || field.localized}
version={versionRow}
>
<RenderVersionFieldsToDiff versionFields={versionFields} />

View File

@@ -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
}
}

View File

@@ -79,7 +79,14 @@ type TabProps = {
tab: VersionTab
} & FieldDiffClientProps<TabsFieldClient>
const Tab: React.FC<TabProps> = ({ comparisonValue, fieldTab, locale, tab, versionValue }) => {
const Tab: React.FC<TabProps> = ({
comparisonValue,
fieldTab,
locale,
parentIsLocalized,
tab,
versionValue,
}) => {
const { i18n } = useTranslation()
const { selectedLocales } = useSelectedLocales()
@@ -102,6 +109,7 @@ const Tab: React.FC<TabProps> = ({ comparisonValue, fieldTab, locale, tab, versi
)
}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized || fieldTab.localized}
version={versionValue}
>
<RenderVersionFieldsToDiff versionFields={tab.fields} />

View File

@@ -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,
})

View File

@@ -228,6 +228,7 @@ export async function VersionView(props: DocumentViewServerProps) {
i18n,
modifiedOnly,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
parentSchemaPath: '',
req,

View File

@@ -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<void>[]
req: PayloadRequest

View File

@@ -61,6 +61,7 @@ export type FieldDiffClientProps<TClientField extends ClientFieldWithOptionalTyp
* If this field is localized, this will be the locale of the field
*/
locale?: string
parentIsLocalized: boolean
/**
* Field value from the current version
*/

View File

@@ -516,6 +516,11 @@ export type SanitizedJoin = {
* The path of the join field in dot notation
*/
joinPath: string
/**
* `parentIsLocalized` is true if any parent field of the
* field configuration defining the join is localized
*/
parentIsLocalized: boolean
targetField: RelationshipField | UploadField
}

View File

@@ -36,6 +36,10 @@ import { defaults } from './defaults.js'
const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig> => {
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',

View File

@@ -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
}

View File

@@ -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,
})

View File

@@ -17,5 +17,9 @@ export type PathToQuery = {
fields?: FlattenedField[]
globalSlug?: string
invalid?: boolean
/**
* @todo make required in v4.0
*/
parentIsLocalized: boolean
path: string
}

View File

@@ -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,
})
}

View File

@@ -27,6 +27,7 @@ export {
fieldIsPresentationalOnly,
fieldIsSidebar,
fieldIsVirtual,
fieldShouldBeLocalized,
fieldSupportsMany,
optionIsObject,
optionIsValue,

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -1854,10 +1854,35 @@ export function tabHasName<TField extends ClientTab | Tab>(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
}

View File

@@ -46,6 +46,7 @@ export const afterChange = async <T extends JsonObject>({
global,
operation,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
parentSchemaPath: '',
previousDoc,

View File

@@ -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,

View File

@@ -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,

View File

@@ -85,6 +85,7 @@ export async function afterRead<T extends JsonObject>(args: Args<T>): Promise<T>
locale,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
parentSchemaPath: '',
populate,

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -59,6 +59,7 @@ export const beforeChange = async <T extends JsonObject>({
mergeLocaleActions,
operation,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
parentSchemaPath: '',
req,

View File

@@ -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<void>)[]
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<JsonObject>, 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,

View File

@@ -31,6 +31,10 @@ type Args = {
mergeLocaleActions: (() => Promise<void>)[]
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,

View File

@@ -36,6 +36,7 @@ export const beforeDuplicate = async <T extends JsonObject>({
fields: collection?.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
parentSchemaPath: '',
req,

View File

@@ -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<T> = {
id?: number | string
overrideAccess: boolean
parentIndexPath: string
parentIsLocalized: boolean
parentPath: string
parentSchemaPath: string
req: PayloadRequest
@@ -39,6 +40,7 @@ export const promise = async <T>({
fieldIndex,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,
@@ -61,7 +63,7 @@ export const promise = async <T>({
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 <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath,
req,
@@ -200,6 +203,7 @@ export const promise = async <T>({
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 <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path,
parentSchemaPath: schemaPath,
req,
@@ -259,6 +264,7 @@ export const promise = async <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath,
req,
@@ -301,6 +307,7 @@ export const promise = async <T>({
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 <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path,
parentSchemaPath: schemaPath,
req,
@@ -357,6 +365,7 @@ export const promise = async <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path,
parentSchemaPath: schemaPath,
req,
@@ -381,6 +390,7 @@ export const promise = async <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath,
parentSchemaPath: schemaPath,
req,
@@ -403,6 +413,7 @@ export const promise = async <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath,
parentSchemaPath: schemaPath,
req,
@@ -422,6 +433,7 @@ export const promise = async <T>({
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath: path,
parentSchemaPath: schemaPath,
req,

View File

@@ -18,6 +18,7 @@ type Args<T> = {
id?: number | string
overrideAccess: boolean
parentIndexPath: string
parentIsLocalized: boolean
parentPath: string
parentSchemaPath: string
req: PayloadRequest
@@ -33,6 +34,7 @@ export const traverseFields = async <T>({
fields,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,
@@ -52,6 +54,7 @@ export const traverseFields = async <T>({
fieldIndex,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,

View File

@@ -49,6 +49,7 @@ export const beforeValidate = async <T extends JsonObject>({
operation,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
parentSchemaPath: '',
req,

View File

@@ -33,6 +33,7 @@ type Args<T> = {
operation: 'create' | 'update'
overrideAccess: boolean
parentIndexPath: string
parentIsLocalized: boolean
parentPath: string
parentSchemaPath: string
req: PayloadRequest
@@ -64,6 +65,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,
@@ -355,6 +357,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath,
req,
@@ -401,6 +404,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath + '.' + block.slug,
req,
@@ -431,6 +435,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath,
parentSchemaPath: schemaPath,
req,
@@ -465,6 +470,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path,
parentSchemaPath: schemaPath,
req,
@@ -500,6 +506,7 @@ export const promise = async <T>({
operation,
originalDoc: doc,
overrideAccess,
parentIsLocalized,
path: pathSegments,
previousSiblingDoc: siblingDoc,
previousValue: siblingData[field.name],
@@ -551,6 +558,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath: isNamedTab ? '' : indexPath,
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: isNamedTab ? path : parentPath,
parentSchemaPath: schemaPath,
req,
@@ -574,6 +582,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath: path,
parentSchemaPath: schemaPath,
req,

View File

@@ -25,6 +25,10 @@ type Args<T> = {
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 <T>({
operation,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,
@@ -70,6 +75,7 @@ export const traverseFields = async <T>({
operation,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,

View File

@@ -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,
]
}
}
}

View File

@@ -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,

View File

@@ -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<string, unknown> | unknown
ref?: Record<string, unknown> | 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,
})

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
context,
currentDepth,
depth,
draft,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
node,
overrideAccess,
populationPromises,
req,
showHiddenFields,
siblingDoc,
}: {
export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = (args: {
context: RequestContext
currentDepth: number
depth: number
@@ -64,6 +48,7 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
flattenLocales: boolean
node: T
overrideAccess: boolean
parentIsLocalized: boolean
populationPromises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean

View File

@@ -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,

View File

@@ -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,

View File

@@ -8,6 +8,7 @@ import { recurseNodes } from '../utilities/forEachNodeRecursively.js'
export type Args = {
editorPopulationPromises: Map<string, Array<PopulationPromise>>
parentIsLocalized: boolean
} & Parameters<
NonNullable<RichTextAdapter<SerializedEditorState, AdapterProps>['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,

View File

@@ -22,6 +22,7 @@ type NestedRichTextFieldsArgs = {
findMany: boolean
flattenLocales: boolean
overrideAccess: boolean
parentIsLocalized: boolean
populationPromises: Promise<void>[]
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.

View File

@@ -156,6 +156,7 @@ export const richTextRelationshipPromise = ({
draft,
field,
overrideAccess,
parentIsLocalized,
populateArg,
populationPromises,
req,

View File

@@ -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,

View File

@@ -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,

View File

@@ -83,7 +83,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
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<T extends boolean = true> {
* via the `definition` "menu".
*/
export interface Menu {
id: number;
id: string;
globalText?: string | null;
updatedAt?: string | null;
createdAt?: string | null;

View File

@@ -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')

View File

@@ -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: {

View File

@@ -126,6 +126,26 @@ export const baseConfig: Partial<Config> = {
},
],
},
{
slug: 'localizedTextReference',
fields: [
{
name: 'text',
type: 'text',
localized: true,
},
],
},
{
slug: 'localizedTextReference2',
fields: [
{
name: 'text',
type: 'text',
localized: true,
},
],
},
],
custom: {
client: {

View File

@@ -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: [],
},
],
}

View File

@@ -47,4 +47,16 @@ export const blocksDoc: Partial<BlockField> = {
blockType: 'blockWithMinRows',
},
],
localizedReferencesLocalizedBlock: [
{
blockType: 'localizedTextReference',
text: 'localized text',
},
],
localizedReferences: [
{
blockType: 'localizedTextReference2',
text: 'localized text',
},
],
}

View File

@@ -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', () => {

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ export const autoDedupeBlocksPlugin =
traverseFields({
config,
leavesFirst: true,
parentIsLocalized: false,
isTopLevel: true,
fields: [
...(config.collections?.length

View File

@@ -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(() => {

Some files were not shown because too many files have changed in this diff Show More