diff --git a/src/collections/operations/find.ts b/src/collections/operations/find.ts index 10b475119..a8a9a3beb 100644 --- a/src/collections/operations/find.ts +++ b/src/collections/operations/find.ts @@ -70,7 +70,7 @@ async function find(incomingArgs: Arguments): Promis // Access // ///////////////////////////////////// - const queryToBuild: { where?: Where} = { + const queryToBuild: { where?: Where } = { where: { and: [], }, @@ -128,7 +128,13 @@ async function find(incomingArgs: Arguments): Promis // Find // ///////////////////////////////////// - const [sortProperty, sortOrder] = buildSortParam(args.sort, collectionConfig.timestamps); + const [sortProperty, sortOrder] = buildSortParam({ + sort: args.sort, + config: payload.config, + fields: collectionConfig.fields, + timestamps: collectionConfig.timestamps, + locale, + }); const optionsToExecute = { page: page || 1, diff --git a/src/collections/operations/findVersions.ts b/src/collections/operations/findVersions.ts index bbf8fc7fb..6b6ed8ac4 100644 --- a/src/collections/operations/findVersions.ts +++ b/src/collections/operations/findVersions.ts @@ -9,6 +9,7 @@ import { buildSortParam } from '../../mongoose/buildSortParam'; import { PaginatedDocs } from '../../mongoose/types'; import { TypeWithVersion } from '../../versions/types'; import { afterRead } from '../../fields/hooks/afterRead'; +import { buildVersionCollectionFields } from '../../versions/buildCollectionFields'; export type Arguments = { collection: Collection @@ -46,7 +47,7 @@ async function findVersions = any>(args: Arguments) // Access // ///////////////////////////////////// - const queryToBuild: { where?: Where} = {}; + const queryToBuild: { where?: Where } = {}; let useEstimatedCount = false; if (where) { @@ -89,7 +90,13 @@ async function findVersions = any>(args: Arguments) // Find // ///////////////////////////////////// - const [sortProperty, sortOrder] = buildSortParam(args.sort || '-updatedAt', true); + const [sortProperty, sortOrder] = buildSortParam({ + sort: args.sort || '-updatedAt', + fields: buildVersionCollectionFields(collectionConfig), + timestamps: true, + config: payload.config, + locale, + }); const optionsToExecute = { page: page || 1, diff --git a/src/globals/operations/findVersions.ts b/src/globals/operations/findVersions.ts index d76ea340d..3feb2bce2 100644 --- a/src/globals/operations/findVersions.ts +++ b/src/globals/operations/findVersions.ts @@ -9,6 +9,7 @@ import { buildSortParam } from '../../mongoose/buildSortParam'; import { TypeWithVersion } from '../../versions/types'; import { SanitizedGlobalConfig } from '../config/types'; import { afterRead } from '../../fields/hooks/afterRead'; +import { buildVersionGlobalFields } from '../../versions/buildGlobalFields'; export type Arguments = { globalConfig: SanitizedGlobalConfig @@ -44,7 +45,7 @@ async function findVersions = any>(args: Arguments) // Access // ///////////////////////////////////// - const queryToBuild: { where?: Where} = {}; + const queryToBuild: { where?: Where } = {}; let useEstimatedCount = false; if (where) { @@ -87,7 +88,13 @@ async function findVersions = any>(args: Arguments) // Find // ///////////////////////////////////// - const [sortProperty, sortOrder] = buildSortParam(args.sort || '-updatedAt', true); + const [sortProperty, sortOrder] = buildSortParam({ + sort: args.sort || '-updatedAt', + fields: buildVersionGlobalFields(globalConfig), + timestamps: true, + config: payload.config, + locale, + }); const optionsToExecute = { page: page || 1, diff --git a/src/mongoose/buildSortParam.ts b/src/mongoose/buildSortParam.ts index a8864dcce..2b840ebca 100644 --- a/src/mongoose/buildSortParam.ts +++ b/src/mongoose/buildSortParam.ts @@ -1,4 +1,16 @@ -export const buildSortParam = (sort: string, timestamps: boolean) => { +import { Config } from '../config/types'; +import { getLocalizedSortProperty } from './getLocalizedSortProperty'; +import { Field } from '../fields/config/types'; + +type Args = { + sort: string + config: Config + fields: Field[] + timestamps: boolean + locale: string +} + +export const buildSortParam = ({ sort, config, fields, timestamps, locale }: Args): [string, string] => { let sortProperty: string; let sortOrder = 'desc'; @@ -15,7 +27,16 @@ export const buildSortParam = (sort: string, timestamps: boolean) => { sortOrder = 'asc'; } - if (sortProperty === 'id') sortProperty = '_id'; + if (sortProperty === 'id') { + sortProperty = '_id'; + } else { + sortProperty = getLocalizedSortProperty({ + segments: sortProperty.split('.'), + config, + fields, + locale, + }); + } return [sortProperty, sortOrder]; }; diff --git a/src/mongoose/getLocalizedSortProperty.spec.ts b/src/mongoose/getLocalizedSortProperty.spec.ts new file mode 100644 index 000000000..3599ec581 --- /dev/null +++ b/src/mongoose/getLocalizedSortProperty.spec.ts @@ -0,0 +1,182 @@ +import { Config } from '../config/types'; +import { getLocalizedSortProperty } from './getLocalizedSortProperty'; + +const config = { + localization: { + locales: ['en', 'es'], + }, +} as Config; + +describe('get localized sort property', () => { + it('passes through a non-localized sort property', () => { + const result = getLocalizedSortProperty({ + segments: ['title'], + config, + fields: [ + { + name: 'title', + type: 'text', + }, + ], + locale: 'en', + }); + + expect(result).toStrictEqual('title'); + }); + + it('properly localizes an un-localized sort property', () => { + const result = getLocalizedSortProperty({ + segments: ['title'], + config, + fields: [ + { + name: 'title', + type: 'text', + localized: true, + }, + ], + locale: 'en', + }); + + expect(result).toStrictEqual('title.en'); + }); + + it('keeps specifically asked-for localized sort properties', () => { + const result = getLocalizedSortProperty({ + segments: ['title', 'es'], + config, + fields: [ + { + name: 'title', + type: 'text', + localized: true, + }, + ], + locale: 'en', + }); + + expect(result).toStrictEqual('title.es'); + }); + + it('properly localizes nested sort properties', () => { + const result = getLocalizedSortProperty({ + segments: ['group', 'title'], + config, + fields: [ + { + name: 'group', + type: 'group', + fields: [ + { + name: 'title', + type: 'text', + localized: true, + }, + ], + }, + ], + locale: 'en', + }); + + expect(result).toStrictEqual('group.title.en'); + }); + + it('keeps requested locale with nested sort properties', () => { + const result = getLocalizedSortProperty({ + segments: ['group', 'title', 'es'], + config, + fields: [ + { + name: 'group', + type: 'group', + fields: [ + { + name: 'title', + type: 'text', + localized: true, + }, + ], + }, + ], + locale: 'en', + }); + + expect(result).toStrictEqual('group.title.es'); + }); + + it('properly localizes field within row', () => { + const result = getLocalizedSortProperty({ + segments: ['title'], + config, + fields: [ + { + type: 'row', + fields: [ + { + name: 'title', + type: 'text', + localized: true, + }, + ], + }, + ], + locale: 'en', + }); + + expect(result).toStrictEqual('title.en'); + }); + + it('properly localizes field within named tab', () => { + const result = getLocalizedSortProperty({ + segments: ['tab', 'title'], + config, + fields: [ + { + type: 'tabs', + tabs: [ + { + name: 'tab', + fields: [ + { + name: 'title', + type: 'text', + localized: true, + }, + ], + }, + ], + }, + ], + locale: 'en', + }); + + expect(result).toStrictEqual('tab.title.en'); + }); + + it('properly localizes field within unnamed tab', () => { + const result = getLocalizedSortProperty({ + segments: ['title'], + config, + fields: [ + { + type: 'tabs', + tabs: [ + { + label: 'Tab', + fields: [ + { + name: 'title', + type: 'text', + localized: true, + }, + ], + }, + ], + }, + ], + locale: 'en', + }); + + expect(result).toStrictEqual('title.en'); + }); +}); diff --git a/src/mongoose/getLocalizedSortProperty.ts b/src/mongoose/getLocalizedSortProperty.ts new file mode 100644 index 000000000..8f24ee4fd --- /dev/null +++ b/src/mongoose/getLocalizedSortProperty.ts @@ -0,0 +1,89 @@ +import { Config } from '../config/types'; +import { Field, fieldAffectsData, fieldIsPresentationalOnly } from '../fields/config/types'; +import flattenTopLevelFields from '../utilities/flattenTopLevelFields'; + +type Args = { + segments: string[] + config: Config + fields: Field[] + locale: string + result?: string +} + +export const getLocalizedSortProperty = ({ + segments: incomingSegments, + config, + fields: incomingFields, + locale, + result: incomingResult, +}: Args): string => { + // If localization is not enabled, accept exactly + // what is sent in + if (!config.localization) { + return incomingSegments.join('.'); + } + + // Flatten incoming fields (row, etc) + const fields = flattenTopLevelFields(incomingFields); + + const segments = [...incomingSegments]; + + // Retrieve first segment, and remove from segments + const firstSegment = segments.shift(); + + // Attempt to find a matched field + const matchedField = fields.find((field) => fieldAffectsData(field) && field.name === firstSegment); + + if (matchedField && !fieldIsPresentationalOnly(matchedField)) { + let nextFields: Field[]; + const remainingSegments = [...segments]; + let localizedSegment = matchedField.name; + + if (matchedField.localized) { + // Check to see if next segment is a locale + if (segments.length > 0) { + const nextSegmentIsLocale = config.localization.locales.includes(remainingSegments[0]); + + // If next segment is locale, remove it from remaining segments + // and use it to localize the current segment + if (nextSegmentIsLocale) { + const nextSegment = remainingSegments.shift(); + localizedSegment = `${matchedField.name}.${nextSegment}`; + } + } else { + // If no more segments, but field is localized, use default locale + localizedSegment = `${matchedField.name}.${locale}`; + } + } + + // If there are subfields, pass them through + if (matchedField.type === 'tab' || matchedField.type === 'group' || matchedField.type === 'array') { + nextFields = matchedField.fields; + } + + if (matchedField.type === 'blocks') { + nextFields = matchedField.blocks.reduce((flattenedBlockFields, block) => { + return [ + ...flattenedBlockFields, + ...block.fields.filter((blockField) => (fieldAffectsData(blockField) && (blockField.name !== 'blockType' && blockField.name !== 'blockName')) || !fieldAffectsData(blockField)), + ]; + }, []); + } + + const result = incomingResult ? `${incomingResult}.${localizedSegment}` : localizedSegment; + + if (nextFields) { + return getLocalizedSortProperty({ + segments: remainingSegments, + config, + fields: nextFields, + locale, + result, + }); + } + + return result; + } + + return incomingSegments.join('.'); +}; diff --git a/src/utilities/flattenTopLevelFields.ts b/src/utilities/flattenTopLevelFields.ts index 1d0e3724b..f8ce20c81 100644 --- a/src/utilities/flattenTopLevelFields.ts +++ b/src/utilities/flattenTopLevelFields.ts @@ -30,7 +30,7 @@ const flattenFields = (fields: Field[], keepPresentationalFields?: boolean): (Fi ...field.tabs.reduce((tabFields, tab) => { return [ ...tabFields, - ...(tabHasName(tab) ? [tab] : flattenFields(tab.fields, keepPresentationalFields)), + ...(tabHasName(tab) ? [{ ...tab, type: 'tab' }] : flattenFields(tab.fields, keepPresentationalFields)), ]; }, []), ]; diff --git a/test/localization/config.ts b/test/localization/config.ts index e132a8204..770002967 100644 --- a/test/localization/config.ts +++ b/test/localization/config.ts @@ -56,6 +56,7 @@ export default buildConfig({ name: 'title', type: 'text', localized: true, + index: true, }, { name: 'description',