diff --git a/src/custom-sort/custom-sort-getComparator.spec.ts b/src/custom-sort/custom-sort-getComparator.spec.ts new file mode 100644 index 0000000..35f039d --- /dev/null +++ b/src/custom-sort/custom-sort-getComparator.spec.ts @@ -0,0 +1,294 @@ +import { + FolderItemForSorting, + getComparator, + getSorterFnFor, + getMdata, + OS_byCreatedTime, + OS_byModifiedTime, + OS_byModifiedTimeReverse, SortingLevelId +} from './custom-sort'; +import * as CustomSortModule from './custom-sort'; +import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from './custom-sort-types'; + +const MOCK_TIMESTAMP: number = 1656417542418 + +const FlatLevelSortSpec: CustomSortSpec = { + groups: [{ // Not relevant in unit test + exactText: "Nothing", + filesOnly: true, + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.ExactName + },{ // prepared for unit test + exactPrefix: "Fi", + order: CustomSortOrder.byMetadataFieldAlphabeticalReverse, + type: CustomSortGroupType.ExactPrefix + },{ // Not relevant in unit test + type: CustomSortGroupType.Outsiders, + order: CustomSortOrder.byCreatedTime + }], + outsidersGroupIdx: 2, + defaultOrder: CustomSortOrder.byCreatedTime, + targetFoldersPaths: ['parent folder'] +} + +const MultiLevelSortSpecGroupLevel: CustomSortSpec = { + groups: [{ // Not relevant in unit test + exactText: "Nothing", + filesOnly: true, + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.ExactName + },{ // prepared for unit test + exactPrefix: "Fi", + order: CustomSortOrder.byMetadataFieldAlphabeticalReverse, + secondaryOrder: CustomSortOrder.byMetadataFieldTrueAlphabetical, + type: CustomSortGroupType.ExactPrefix + },{ // Not relevant in unit test + type: CustomSortGroupType.Outsiders, + order: CustomSortOrder.byCreatedTime + }], + outsidersGroupIdx: 2, + defaultOrder: CustomSortOrder.byCreatedTime, + targetFoldersPaths: ['parent folder'] +} + +const MultiLevelSortSpecTargetFolderLevel: CustomSortSpec = { + groups: [{ // Not relevant in unit test + exactText: "Nothing", + filesOnly: true, + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.ExactName + },{ // prepared for unit test + exactPrefix: "Fi", + order: CustomSortOrder.byMetadataFieldAlphabeticalReverse, + type: CustomSortGroupType.ExactPrefix + },{ // Not relevant in unit test + type: CustomSortGroupType.Outsiders, + order: CustomSortOrder.byCreatedTime + }], + outsidersGroupIdx: 2, + defaultOrder: CustomSortOrder.byCreatedTime, + defaultSecondaryOrder: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, + targetFoldersPaths: ['parent folder'] +} + +const MultiLevelSortSpecAndTargetFolderLevel: CustomSortSpec = { + groups: [{ // Not relevant in unit test + exactText: "Nothing", + filesOnly: true, + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.ExactName + },{ // prepared for unit test + exactPrefix: "Fi", + order: CustomSortOrder.byMetadataFieldAlphabetical, + secondaryOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse, + type: CustomSortGroupType.ExactPrefix + },{ // Not relevant in unit test + type: CustomSortGroupType.Outsiders, + order: CustomSortOrder.byCreatedTime + }], + outsidersGroupIdx: 2, + defaultOrder: CustomSortOrder.byMetadataFieldTrueAlphabetical, + defaultSecondaryOrder: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, + targetFoldersPaths: ['parent folder'] +} + +const A_GOES_FIRST: number = -1 +const B_GOES_FIRST: number = 1 +const AB_EQUAL: number = 0 + +const BaseItemForSorting1: FolderItemForSorting = { + groupIdx: 1, + isFolder: false, + sortString: "References.md", + ctime: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'parent folder/References.md', + metadataFieldValue: 'direct metadata on file, under default name', + metadataFieldValueSecondary: 'only used if secondary sort by metadata is used', + metadataFieldValueForDerived: 'only used if derived primary sort by metadata is used', + metadataFieldValueForDerivedSecondary: 'only used if derived secondary sort by metadata is used' +} + +function getBaseItemForSorting(overrides?: Partial): FolderItemForSorting { + return Object.assign({}, BaseItemForSorting1, overrides) +} + +describe('getComparator', () => { + const sp = jest.spyOn(CustomSortModule, 'getSorterFnFor') + const collatorCmp = jest.spyOn(CustomSortModule, 'CollatorCompare') + beforeEach(() => { + sp.mockClear() + }) + describe('should correctly handle flat sorting spec', () => { + const comparator = getComparator(FlatLevelSortSpec, OS_byModifiedTime) + it( 'in simple case - group-level comparison succeeds', () => { + const a = getBaseItemForSorting({ + metadataFieldValue: 'value X' + }) + const b= getBaseItemForSorting({ + metadataFieldValue: 'value Y' + }) + const result = comparator(a,b) + expect(result).toBe(B_GOES_FIRST) + expect(sp).toBeCalledTimes(1) + expect(sp).toBeCalledWith(CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary) + }) + it( 'in simple case - group-level comparison fails, use folder-level', () => { + const a = getBaseItemForSorting() + const b= getBaseItemForSorting({ + ctime: a.ctime - 100 + }) + const result = Math.sign(comparator(a,b)) + expect(result).toBe(B_GOES_FIRST) + expect(sp).toBeCalledTimes(2) + expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary) + expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTime, SortingLevelId.forDerivedPrimary) + }) + it( 'in simple case - group-level comparison fails, folder-level fails, ui-selected in effect', () => { + const a = getBaseItemForSorting() + const b= getBaseItemForSorting({ + mtime: a.mtime + 100 // Make be fresher than a + }) + const result = Math.sign(comparator(a,b)) + expect(result).toBe(B_GOES_FIRST) + expect(sp).toBeCalledTimes(3) + expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary) + expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTime, SortingLevelId.forDerivedPrimary) + expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.standardObsidian, OS_byModifiedTime, SortingLevelId.forUISelected) + }) + it( 'in simple case - group-level comparison fails, folder-level fails, ui-selected fails, the last resort default comes into play - case A', () => { + const a = getBaseItemForSorting({ + sortString: 'Second' + }) + const b= getBaseItemForSorting({ + sortString: 'First' + }) + const result = comparator(a,b) + expect(result).toBe(B_GOES_FIRST) + expect(sp).toBeCalledTimes(4) + expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary) + expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTime, SortingLevelId.forDerivedPrimary) + expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.standardObsidian, OS_byModifiedTime, SortingLevelId.forUISelected) + expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.default, undefined, SortingLevelId.forLastResort) + }) + }) + describe('should correctly handle secondary sorting spec', () => { + beforeEach(() => { + sp.mockClear() + }) + describe('at group level', () => { + const comparator = getComparator(MultiLevelSortSpecGroupLevel, OS_byModifiedTimeReverse) + it('in simple case - secondary sort comparison succeeds', () => { + const a = getBaseItemForSorting({ + metadataFieldValueSecondary: 'This goes 1' + }) + const b = getBaseItemForSorting({ + metadataFieldValueSecondary: 'This goes 2' + }) + const result = comparator(a, b) + expect(result).toBe(A_GOES_FIRST) + expect(sp).toBeCalledTimes(2) + expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary) + expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byModifiedTimeReverse, SortingLevelId.forSecondary) + }) + it( 'in complex case - secondary sort comparison fails, last resort comes into play', () => { + const a = getBaseItemForSorting({ + sortString: 'Second' + }) + const b= getBaseItemForSorting({ + sortString: 'First' + }) + const result = comparator(a,b) + expect(result).toBe(B_GOES_FIRST) + expect(sp).toBeCalledTimes(5) + expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary) + expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byModifiedTimeReverse, SortingLevelId.forSecondary) + expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary ) + expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.standardObsidian, OS_byModifiedTimeReverse, SortingLevelId.forUISelected) + expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.default, undefined, SortingLevelId.forLastResort) + }) + }) + describe('at target folder level (aka derived)', () => { + const comparator = getComparator(MultiLevelSortSpecTargetFolderLevel, OS_byModifiedTimeReverse) + it('in simple case - derived secondary sort comparison succeeds', () => { + const a = getBaseItemForSorting({ + metadataFieldValueForDerivedSecondary: 'This goes 2 first (reverse is in effect)' + }) + const b = getBaseItemForSorting({ + metadataFieldValueForDerivedSecondary: 'This goes 1 second (reverse is in effect)' + }) + const result = comparator(a, b) + expect(result).toBe(A_GOES_FIRST) + expect(sp).toBeCalledTimes(3) + expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary) + expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary) + expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forDerivedSecondary) + }) + it( 'in complex case - secondary sort comparison fails, last resort comes into play', () => { + const a = getBaseItemForSorting({ + sortString: 'Second' + }) + const b= getBaseItemForSorting({ + sortString: 'First' + }) + const result = comparator(a,b) + expect(result).toBe(B_GOES_FIRST) + expect(sp).toBeCalledTimes(5) + expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary) + expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary) + expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forDerivedSecondary) + expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.standardObsidian, OS_byModifiedTimeReverse, SortingLevelId.forUISelected) + expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.default, undefined, SortingLevelId.forLastResort) + }) + }) + describe('at group and at target folder level (aka derived)', () => { + const comparator = getComparator(MultiLevelSortSpecAndTargetFolderLevel, OS_byCreatedTime) + const mdataGetter = jest.spyOn(CustomSortModule, 'getMdata') + beforeEach(() => { + mdataGetter.mockClear() + }) + it('most complex case - last resort comest into play, all sort levels present, all involve metadata', () => { + const a = getBaseItemForSorting({ + path: 'test 1', // Not used in comparisons, used only to identify source of compared metadata + metadataFieldValue: 'm', + metadataFieldValueSecondary: 'ms', + metadataFieldValueForDerived: 'dm', + metadataFieldValueForDerivedSecondary: 'dms' + }) + const b= getBaseItemForSorting({ + path: 'test 2', // Not used in comparisons, used only to identify source of compared metadata + metadataFieldValue: 'm', + metadataFieldValueSecondary: 'ms', + metadataFieldValueForDerived: 'dm', + metadataFieldValueForDerivedSecondary: 'dms' + }) + const result = Math.sign(comparator(a,b)) + expect(result).toBe(AB_EQUAL) + expect(sp).toBeCalledTimes(6) + expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabetical, OS_byCreatedTime, SortingLevelId.forPrimary) + expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byCreatedTime, SortingLevelId.forSecondary) + expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byCreatedTime, SortingLevelId.forDerivedPrimary) + expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byCreatedTime, SortingLevelId.forDerivedSecondary) + expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.standardObsidian, OS_byCreatedTime, SortingLevelId.forUISelected) + expect(sp).toHaveBeenNthCalledWith(6, CustomSortOrder.default, undefined, SortingLevelId.forLastResort) + expect(mdataGetter).toHaveBeenCalledTimes(8) + expect(mdataGetter).toHaveBeenNthCalledWith(1, expect.objectContaining({path: 'test 1'}), SortingLevelId.forPrimary) + expect(mdataGetter).toHaveNthReturnedWith(1, 'm') + expect(mdataGetter).toHaveBeenNthCalledWith(2, expect.objectContaining({path: 'test 2'}), SortingLevelId.forPrimary) + expect(mdataGetter).toHaveNthReturnedWith(2, 'm') + expect(mdataGetter).toHaveBeenNthCalledWith(3, expect.objectContaining({path: 'test 1'}), SortingLevelId.forSecondary) + expect(mdataGetter).toHaveNthReturnedWith(3, 'ms') + expect(mdataGetter).toHaveBeenNthCalledWith(4, expect.objectContaining({path: 'test 2'}), SortingLevelId.forSecondary) + expect(mdataGetter).toHaveNthReturnedWith(4, 'ms') + expect(mdataGetter).toHaveBeenNthCalledWith(5, expect.objectContaining({path: 'test 1'}), SortingLevelId.forDerivedPrimary) + expect(mdataGetter).toHaveNthReturnedWith(5, 'dm') + expect(mdataGetter).toHaveBeenNthCalledWith(6, expect.objectContaining({path: 'test 2'}), SortingLevelId.forDerivedPrimary) + expect(mdataGetter).toHaveNthReturnedWith(6, 'dm') + expect(mdataGetter).toHaveBeenNthCalledWith(7, expect.objectContaining({path: 'test 1'}), SortingLevelId.forDerivedSecondary) + expect(mdataGetter).toHaveNthReturnedWith(7, 'dms') + expect(mdataGetter).toHaveBeenNthCalledWith(8, expect.objectContaining({path: 'test 2'}), SortingLevelId.forDerivedSecondary) + expect(mdataGetter).toHaveNthReturnedWith(8, 'dms') + }) + }) + }) +}) diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index fb7a254..996076a 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -57,6 +57,7 @@ export interface CustomSortGroup { overrideTitle?: boolean // instead of title, use a derived text for sorting (e.g. regexp matching group). order?: CustomSortOrder byMetadataField?: string // for 'by-metadata:' sorting if the order is by metadata alphabetical or reverse + byMetadataFieldSecondary?: string // for 'by-metadata:' sorting if the order is by metadata alphabetical or reverse secondaryOrder?: CustomSortOrder filesOnly?: boolean matchFilenameWithExt?: boolean @@ -71,7 +72,9 @@ export interface CustomSortSpec { // plays only informative role about the original parsed 'target-folder:' values targetFoldersPaths: Array // For root use '/' defaultOrder?: CustomSortOrder - byMetadataField?: string // for 'by-metadata:' if the defaultOrder is by metadata alphabetical or reverse + defaultSecondaryOrder?: CustomSortOrder + byMetadataField?: string // for 'by-metadata:' if the defaultOrder is by metadata + byMetadataFieldSecondary?: string groups: Array groupsShadow?: Array // A shallow copy of groups, used at applying sorting for items in a folder. // Stores folder-specific values (e.g. macros expanded with folder-specific values) diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index 25b5196..bc07fed 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -6,11 +6,11 @@ import { determineSortingGroup, EQUAL_OR_UNCOMPARABLE, FolderItemForSorting, - matchGroupRegex, - sorterByMetadataField, - SorterFn, getSorterFnFor, - ProcessingContext + matchGroupRegex, + ProcessingContext, + sorterByMetadataField, + SorterFn } from './custom-sort'; import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types'; import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; @@ -1881,7 +1881,7 @@ describe('determineSortingGroup', () => { ctime: MOCK_TIMESTAMP + 222, mtime: MOCK_TIMESTAMP + 333, path: 'Some parent folder/References.md', - metadataFieldValue: 'direct metadata on file, not obvious' + metadataFieldValueForDerived: 'direct metadata on file, not obvious' } as FolderItemForSorting); }) it('should correctly read direct metadata from File item (order by metadata set on group, no metadata name specified on group)', () => { @@ -1966,6 +1966,406 @@ describe('determineSortingGroup', () => { }) }) + describe('when sort by metadata is involved (specified in secondary sort, for group of for target folder)', () => { + it('should correctly read direct metadata from File item (order by metadata set on group) alph', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.ExactPrefix, + byMetadataFieldSecondary: 'metadata-field-for-sorting', + exactPrefix: 'Ref', + order: CustomSortOrder.alphabetical, + secondaryOrder: CustomSortOrder.byMetadataFieldAlphabetical + }] + } + const ctx: Partial = { + _mCache: { + getCache: function (path: string): CachedMetadata | undefined { + return { + 'Some parent folder/References.md': { + frontmatter: { + "metadata-field-for-sorting": "direct metadata on file", + position: MockedLoc + } + } + }[path] + } + } as MetadataCache + } + + // when + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: false, + sortString: "References.md", + ctime: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md', + metadataFieldValueSecondary: 'direct metadata on file' + } as FolderItemForSorting); + }) + it('should correctly read direct metadata from File item (order by metadata set on group) alph rev', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.ExactPrefix, + byMetadataFieldSecondary: 'metadata-field-for-sorting', + exactPrefix: 'Ref', + order: CustomSortOrder.alphabeticalReverse, + secondaryOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse + }] + } + const ctx: Partial = { + _mCache: { + getCache: function (path: string): CachedMetadata | undefined { + return { + 'Some parent folder/References.md': { + frontmatter: { + "metadata-field-for-sorting": "direct metadata on file", + position: MockedLoc + } + } + }[path] + } + } as MetadataCache + } + + // when + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: false, + sortString: "References.md", + ctime: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md', + metadataFieldValueSecondary: 'direct metadata on file' + } as FolderItemForSorting); + }) + it('should correctly read direct metadata from File item (order by metadata set on group) true alph', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.ExactPrefix, + byMetadataField: 'non-existing-mdata', + byMetadataFieldSecondary: 'metadata-field-for-sorting', + exactPrefix: 'Ref', + order: CustomSortOrder.byMetadataFieldTrueAlphabetical, + secondaryOrder: CustomSortOrder.byMetadataFieldTrueAlphabetical + }] + } + const ctx: Partial = { + _mCache: { + getCache: function (path: string): CachedMetadata | undefined { + return { + 'Some parent folder/References.md': { + frontmatter: { + "metadata-field-for-sorting": "direct metadata on file", + position: MockedLoc + } + } + }[path] + } + } as MetadataCache + } + + // when + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: false, + sortString: "References.md", + ctime: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md', + metadataFieldValueSecondary: 'direct metadata on file' + } as FolderItemForSorting); + }) + it('should correctly read direct metadata from File item (order by metadata set on group) true alph rev (dbl mdata)', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.ExactPrefix, + byMetadataField: 'metadata-field-for-sorting', + byMetadataFieldSecondary: 'metadata-field-for-sorting secondary', + exactPrefix: 'Ref', + order: CustomSortOrder.byMetadataFieldTrueAlphabetical, + secondaryOrder: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse + }] + } + const ctx: Partial = { + _mCache: { + getCache: function (path: string): CachedMetadata | undefined { + return { + 'Some parent folder/References.md': { + frontmatter: { + "metadata-field-for-sorting": "direct metadata on file", + "metadata-field-for-sorting secondary": "direct another metadata on file", + position: MockedLoc + } + } + }[path] + } + } as MetadataCache + } + + // when + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: false, + sortString: "References.md", + ctime: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md', + metadataFieldValue: 'direct metadata on file', + metadataFieldValueSecondary: 'direct another metadata on file' + } as FolderItemForSorting); + }) + it('should correctly read direct metadata from folder note item (order by metadata set on group)', () => { + // given + const folder: TFolder = mockTFolder('References'); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.ExactPrefix, + exactPrefix: 'Ref', + byMetadataFieldSecondary: 'metadata-field-for-sorting', + order: CustomSortOrder.standardObsidian, + secondaryOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse + }] + } + const ctx: Partial = { + _mCache: { + getCache: function (path: string): CachedMetadata | undefined { + return { + 'References/References.md': { + frontmatter: { + 'metadata-field-for-sorting': "metadata on folder note", + position: MockedLoc + } + } + }[path] + } + } as MetadataCache + } + + // when + const result = determineSortingGroup(folder, sortSpec, ctx as ProcessingContext) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: true, + sortString: "References", + ctime: DEFAULT_FOLDER_CTIME, + mtime: DEFAULT_FOLDER_MTIME, + path: 'References', + metadataFieldValueSecondary: 'metadata on folder note', + folder: folder + } as FolderItemForSorting); + }) + it('should correctly read direct metadata from File item (order by metadata set on target folder)', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.ExactPrefix, + exactPrefix: 'Ref', + order: CustomSortOrder.trueAlphabetical, + secondaryOrder: CustomSortOrder.byMetadataFieldAlphabetical + }], + defaultOrder: CustomSortOrder.byCreatedTime, + defaultSecondaryOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse, + byMetadataFieldSecondary: 'metadata-field-for-sorting-specified-on-target-folder' + } + const ctx: Partial = { + _mCache: { + getCache: function (path: string): CachedMetadata | undefined { + return { + 'Some parent folder/References.md': { + frontmatter: { + "metadata-field-for-sorting-specified-on-target-folder": "direct metadata on file, not obvious", + position: MockedLoc + } + } + }[path] + } + } as MetadataCache + } + + // when + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: false, + sortString: "References.md", + ctime: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md', + metadataFieldValueForDerivedSecondary: 'direct metadata on file, not obvious' + } as FolderItemForSorting); + }) + it('should correctly read direct metadata from File item (order by metadata set on group, no metadata name specified on group)', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.HasMetadataField, + order: CustomSortOrder.standardObsidian, + secondaryOrder: CustomSortOrder.byMetadataFieldAlphabetical, + withMetadataFieldName: 'field-used-with-with-metadata-syntax' + }] + } + const ctx: Partial = { + _mCache: { + getCache: function (path: string): CachedMetadata | undefined { + return { + 'Some parent folder/References.md': { + frontmatter: { + 'field-used-with-with-metadata-syntax': "direct metadata on file, tricky", + position: MockedLoc + } + } + }[path] + } + } as MetadataCache + } + + // when + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: false, + sortString: "References.md", + ctime: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md', + metadataFieldValueSecondary: 'direct metadata on file, tricky', + } as FolderItemForSorting); + }) + it('should correctly read direct metadata from File item (order by metadata set on group, no metadata name specified anywhere)', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.ExactPrefix, + exactPrefix: 'Ref', + order: CustomSortOrder.byCreatedTimeReverse, + secondaryOrder: CustomSortOrder.byMetadataFieldAlphabetical + }] + } + const ctx: Partial = { + _mCache: { + getCache: function (path: string): CachedMetadata | undefined { + return { + 'Some parent folder/References.md': { + frontmatter: { + 'sort-index-value': "direct metadata on file, under default name", + position: MockedLoc + } + } + }[path] + } + } as MetadataCache + } + + // when + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: false, + sortString: "References.md", + ctime: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md', + metadataFieldValueSecondary: 'direct metadata on file, under default name', + } as FolderItemForSorting); + }) + }) + + describe('when sort by metadata is involved, at every level', () => { + it('should correctly read direct metadata from File item (order by metadata set at each level)', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.ExactPrefix, + exactPrefix: 'Ref', + order: CustomSortOrder.byMetadataFieldAlphabetical, + byMetadataField: 'mdata-for-primary', + secondaryOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse, + byMetadataFieldSecondary: 'mdata-for-secondary' + }], + defaultOrder: CustomSortOrder.byMetadataFieldTrueAlphabetical, + byMetadataField: 'mdata-for-default-primary', + defaultSecondaryOrder: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, + byMetadataFieldSecondary: 'mdata-for-default-secondary' + } + const ctx: Partial = { + _mCache: { + getCache: function (path: string): CachedMetadata | undefined { + return { + 'Some parent folder/References.md': { + frontmatter: { + 'mdata-for-primary': "filemdata 1", + 'mdata-for-secondary': "filemdata 2", + 'mdata-for-default-primary': "filemdata 3", + 'mdata-for-default-secondary': "filemdata 4", + position: MockedLoc + } + } + }[path] + } + } as MetadataCache + } + + // when + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: false, + sortString: "References.md", + ctime: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md', + metadataFieldValue: 'filemdata 1', + metadataFieldValueSecondary: 'filemdata 2', + metadataFieldValueForDerived: 'filemdata 3', + metadataFieldValueForDerivedSecondary: 'filemdata 4', + } as FolderItemForSorting); + }) + }) + it('should correctly apply priority group', () => { // given const file: TFile = mockTFile('Abcdef!', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index 476fdf7..0158503 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -40,13 +40,13 @@ export interface ProcessingContext { iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance } -let CollatorCompare = new Intl.Collator(undefined, { +export const CollatorCompare = new Intl.Collator(undefined, { usage: "sort", sensitivity: "base", numeric: true, }).compare; -let CollatorTrueAlphabeticalCompare = new Intl.Collator(undefined, { +export const CollatorTrueAlphabeticalCompare = new Intl.Collator(undefined, { usage: "sort", sensitivity: "base", numeric: false, @@ -56,13 +56,25 @@ export interface FolderItemForSorting { path: string groupIdx?: number // the index itself represents order for groups sortString: string // fragment (or full name) to be used for sorting - metadataFieldValue?: string // relevant to metadata-based sorting only + metadataFieldValue?: string // relevant to metadata-based group sorting only + metadataFieldValueSecondary?: string // relevant to secondary metadata-based sorting only + metadataFieldValueForDerived?: string // relevant to metadata-based sorting-spec level sorting only + metadataFieldValueForDerivedSecondary?: string // relevant to metadata-based sorting-spec level secondary sorting only ctime: number // for a file ctime is obvious, for a folder = ctime of the oldest child file mtime: number // for a file mtime is obvious, for a folder = date of most recently modified child file isFolder: boolean folder?: TFolder } +export enum SortingLevelId { + forPrimary, + forSecondary, + forDerivedPrimary, + forDerivedSecondary, + forUISelected, + forLastResort +} + export type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number export type PlainSorterFn = (a: TAbstractFile, b: TAbstractFile) => number export type PlainFileOnlySorterFn = (a: TFile, b: TFile) => number @@ -75,19 +87,30 @@ const StraightOrder: boolean = false export const EQUAL_OR_UNCOMPARABLE: number = 0 -export const sorterByMetadataField:(reverseOrder?: boolean, trueAlphabetical?: boolean) => SorterFn = (reverseOrder: boolean, trueAlphabetical?: boolean) => { +export const getMdata = (it: FolderItemForSorting, mdataId?: SortingLevelId) => { + switch (mdataId) { + case SortingLevelId.forSecondary: return it.metadataFieldValueSecondary + case SortingLevelId.forDerivedPrimary: return it.metadataFieldValueForDerived + case SortingLevelId.forDerivedSecondary: return it.metadataFieldValueForDerivedSecondary + case SortingLevelId.forPrimary: + default: return it.metadataFieldValue + } +} + +export const sorterByMetadataField = (reverseOrder?: boolean, trueAlphabetical?: boolean, sortLevelId?: SortingLevelId): SorterFn => { const collatorCompareFn: CollatorCompareFn = trueAlphabetical ? CollatorTrueAlphabeticalCompare : CollatorCompare return (a: FolderItemForSorting, b: FolderItemForSorting) => { + let [amdata, bmdata] = [getMdata(a, sortLevelId), getMdata(b, sortLevelId)] if (reverseOrder) { - [a, b] = [b, a] + [amdata, bmdata] = [bmdata, amdata] } - if (a.metadataFieldValue && b.metadataFieldValue) { - const sortResult: number = collatorCompareFn(a.metadataFieldValue, b.metadataFieldValue) + if (amdata && bmdata) { + const sortResult: number = collatorCompareFn(amdata, bmdata) return sortResult } // Item with metadata goes before the w/o metadata - if (a.metadataFieldValue) return -1 - if (b.metadataFieldValue) return 1 + if (amdata) return -1 + if (bmdata) return 1 return EQUAL_OR_UNCOMPARABLE } @@ -106,21 +129,43 @@ let Sorters: { [key in CustomSortOrder]: SorterFn } = { [CustomSortOrder.byCreatedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctime - b.ctime, [CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? CollatorCompare(a.sortString, b.sortString) : (b.ctime - a.ctime), [CustomSortOrder.byCreatedTimeReverseAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctime - a.ctime, - [CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder), - [CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical), - [CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder), - [CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical), + [CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forPrimary), + [CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forPrimary), + [CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forPrimary), + [CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forPrimary), // This is a fallback entry which should not be used - the getSorterFor() function below should protect against it [CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString), }; +// Some sorters are different when used in primary vs. secondary sorting order +let SortersForSecondary: { [key in CustomSortOrder]?: SorterFn } = { + [CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forSecondary), + [CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forSecondary), + [CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forSecondary), + [CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forSecondary) +}; + +let SortersForDerivedPrimary: { [key in CustomSortOrder]?: SorterFn } = { + [CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forDerivedPrimary), + [CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forDerivedPrimary), + [CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forDerivedPrimary), + [CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forDerivedPrimary) +}; + +let SortersForDerivedSecondary: { [key in CustomSortOrder]?: SorterFn } = { + [CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forDerivedSecondary), + [CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forDerivedSecondary), + [CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forDerivedSecondary), + [CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forDerivedSecondary) +}; + // OS - Obsidian Sort const OS_alphabetical = 'alphabetical' const OS_alphabeticalReverse = 'alphabeticalReverse' -const OS_byModifiedTime = 'byModifiedTime' -const OS_byModifiedTimeReverse = 'byModifiedTimeReverse' -const OS_byCreatedTime = 'byCreatedTime' +export const OS_byModifiedTime = 'byModifiedTime' +export const OS_byModifiedTimeReverse = 'byModifiedTimeReverse' +export const OS_byCreatedTime = 'byCreatedTime' const OS_byCreatedTimeReverse = 'byCreatedTimeReverse' export const ObsidianStandardDefaultSortingName = OS_alphabetical @@ -169,29 +214,38 @@ export const StandardPlainObsidianComparator = (order: string): PlainSorterFn => } } -export const getSorterFnFor = (sorting: CustomSortOrder, currentUIselectedSorting?: string): SorterFn => { +export const getSorterFnFor = (sorting: CustomSortOrder, currentUIselectedSorting?: string, sortLevelId?: SortingLevelId): SorterFn => { if (sorting === CustomSortOrder.standardObsidian) { sorting = StandardObsidianToCustomSort[currentUIselectedSorting ?? 'alphabetical'] ?? CustomSortOrder.alphabetical return StandardObsidianComparator(sorting) } else { - return Sorters[sorting] + // Some sorters have to know at which sorting level they are used + switch(sortLevelId) { + case SortingLevelId.forSecondary: return SortersForSecondary[sorting] ?? Sorters[sorting] + case SortingLevelId.forDerivedPrimary: return SortersForDerivedPrimary[sorting] ?? Sorters[sorting] + case SortingLevelId.forDerivedSecondary: return SortersForDerivedSecondary[sorting] ?? Sorters[sorting] + case SortingLevelId.forPrimary: + default: return Sorters[sorting] + } } } -function getComparator(sortSpec: CustomSortSpec, currentUIselectedSorting?: string): SorterFn { +export const getComparator = (sortSpec: CustomSortSpec, currentUIselectedSorting?: string): SorterFn => { const compareTwoItems = (itA: FolderItemForSorting, itB: FolderItemForSorting) => { if (itA.groupIdx != undefined && itB.groupIdx != undefined) { if (itA.groupIdx === itB.groupIdx) { const group: CustomSortGroup | undefined = sortSpec.groups[itA.groupIdx] - const primary: number = group?.order ? getSorterFnFor(group.order, currentUIselectedSorting)(itA, itB) : EQUAL_OR_UNCOMPARABLE + const primary: number = group?.order ? getSorterFnFor(group.order, currentUIselectedSorting, SortingLevelId.forPrimary)(itA, itB) : EQUAL_OR_UNCOMPARABLE if (primary !== EQUAL_OR_UNCOMPARABLE) return primary - const secondary: number = group?.secondaryOrder ? getSorterFnFor(group.secondaryOrder, currentUIselectedSorting)(itA, itB) : EQUAL_OR_UNCOMPARABLE + const secondary: number = group?.secondaryOrder ? getSorterFnFor(group.secondaryOrder, currentUIselectedSorting, SortingLevelId.forSecondary)(itA, itB) : EQUAL_OR_UNCOMPARABLE if (secondary !== EQUAL_OR_UNCOMPARABLE) return secondary - const folderLevel: number = sortSpec.defaultOrder ? getSorterFnFor(sortSpec.defaultOrder, currentUIselectedSorting)(itA, itB) : EQUAL_OR_UNCOMPARABLE + const folderLevel: number = sortSpec.defaultOrder ? getSorterFnFor(sortSpec.defaultOrder, currentUIselectedSorting, SortingLevelId.forDerivedPrimary)(itA, itB) : EQUAL_OR_UNCOMPARABLE if (folderLevel !== EQUAL_OR_UNCOMPARABLE) return folderLevel - const uiSelected: number = currentUIselectedSorting ? getSorterFnFor(CustomSortOrder.standardObsidian, currentUIselectedSorting)(itA, itB) : EQUAL_OR_UNCOMPARABLE + const folderLevelSecondary: number = sortSpec.defaultSecondaryOrder ? getSorterFnFor(sortSpec.defaultSecondaryOrder, currentUIselectedSorting, SortingLevelId.forDerivedSecondary)(itA, itB) : EQUAL_OR_UNCOMPARABLE + if (folderLevelSecondary !== EQUAL_OR_UNCOMPARABLE) return folderLevelSecondary + const uiSelected: number = currentUIselectedSorting ? getSorterFnFor(CustomSortOrder.standardObsidian, currentUIselectedSorting, SortingLevelId.forUISelected)(itA, itB) : EQUAL_OR_UNCOMPARABLE if (uiSelected !== EQUAL_OR_UNCOMPARABLE) return uiSelected - const lastResort: number = getSorterFnFor(CustomSortOrder.default)(itA, itB) + const lastResort: number = getSorterFnFor(CustomSortOrder.default, undefined, SortingLevelId.forLastResort)(itA, itB) return lastResort } else { return itA.groupIdx - itB.groupIdx; @@ -243,7 +297,7 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus let groupIdx: number let determined: boolean = false let derivedText: string | null | undefined - let metadataValueToSortBy: string | undefined + const aFolder: boolean = isFolder(entry) const aFile: boolean = !aFolder const entryAsTFile: TFile = entry as TFile @@ -399,39 +453,26 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus } } - // The not obvious logic of determining the value of metadata field to use its value for sorting - // - the sorting spec processor automatically populates the order field of CustomSortingGroup for each group - // - yet defensive code should assume some default - // - if the order in group is by metadata (and only in that case): - // - if byMetadata field name is defined for the group -> use it. Done even if value empty or not present. - // - else, if byMetadata field name is defined for the Sorting spec (folder level, for all groups) -> use it. Done even if value empty or not present. - // - else, if withMetadata field name is defined for the group -> use it. Done even if value empty or not present. - // - otherwise, fallback to the default metadata field name (hardcoded in the plugin as 'sort-index-value') - - // TODO: in manual of plugin, in details, explain these nuances. Let readme.md contain only the basic simple example and reference to manual.md section + let metadataValueToSortBy: string | undefined + let metadataValueSecondaryToSortBy: string | undefined + let metadataValueDerivedPrimaryToSortBy: string | undefined + let metadataValueDerivedSecondaryToSortBy: string | undefined if (determined && determinedGroupIdx !== undefined) { // <-- defensive code, maybe too defensive const group: CustomSortGroup = spec.groups[determinedGroupIdx]; - if (isByMetadata(group?.order)) { - let metadataFieldName: string | undefined = group.byMetadataField - if (!metadataFieldName) { - if (isByMetadata(spec.defaultOrder)) { - metadataFieldName = spec.byMetadataField - } - } - if (!metadataFieldName) { - metadataFieldName = group.withMetadataFieldName - } - if (!metadataFieldName) { - metadataFieldName = DEFAULT_METADATA_FIELD_FOR_SORTING - } - if (metadataFieldName) { - if (ctx?._mCache) { - // For folders - scan metadata of 'folder note' - const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md` - const frontMatterCache: FrontMatterCache | undefined = ctx._mCache.getCache(notePathToScan)?.frontmatter - metadataValueToSortBy = frontMatterCache?.[metadataFieldName] - } + const isPrimaryOrderByMetadata: boolean = isByMetadata(group?.order) + const isSecondaryOrderByMetadata: boolean = isByMetadata(group?.secondaryOrder) + const isDerivedPrimaryByMetadata: boolean = isByMetadata(spec.defaultOrder) + const isDerivedSecondaryByMetadata: boolean = isByMetadata(spec.defaultSecondaryOrder) + if (isPrimaryOrderByMetadata || isSecondaryOrderByMetadata || isDerivedPrimaryByMetadata || isDerivedSecondaryByMetadata) { + if (ctx?._mCache) { + // For folders - scan metadata of 'folder note' + const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md` + const frontMatterCache: FrontMatterCache | undefined = ctx._mCache.getCache(notePathToScan)?.frontmatter + if (isPrimaryOrderByMetadata) metadataValueToSortBy = frontMatterCache?.[group?.byMetadataField || group?.withMetadataFieldName || DEFAULT_METADATA_FIELD_FOR_SORTING] + if (isSecondaryOrderByMetadata) metadataValueSecondaryToSortBy = frontMatterCache?.[group?.byMetadataFieldSecondary || group?.withMetadataFieldName || DEFAULT_METADATA_FIELD_FOR_SORTING] + if (isDerivedPrimaryByMetadata) metadataValueDerivedPrimaryToSortBy = frontMatterCache?.[spec.byMetadataField || DEFAULT_METADATA_FIELD_FOR_SORTING] + if (isDerivedSecondaryByMetadata) metadataValueDerivedSecondaryToSortBy = frontMatterCache?.[spec.byMetadataFieldSecondary || DEFAULT_METADATA_FIELD_FOR_SORTING] } } } @@ -441,6 +482,9 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus groupIdx: determinedGroupIdx, sortString: derivedText ?? entry.name, metadataFieldValue: metadataValueToSortBy, + metadataFieldValueSecondary: metadataValueSecondaryToSortBy, + metadataFieldValueForDerived: metadataValueDerivedPrimaryToSortBy, + metadataFieldValueForDerivedSecondary: metadataValueDerivedSecondaryToSortBy, isFolder: aFolder, folder: aFolder ? (entry as TFolder) : undefined, path: entry.path,