#89 - Support for multi-level sorting
- full runtime handling (application) of multi-level sorting - full unit tests coverage of new functions - metadata-based sorting extended to be applicable at each of sorting level, possibly with different metadata + full unit tests coverage - having the run-time part ready, the missing part is the extending the sorting-spec-processor.ts
This commit is contained in:
parent
24af493734
commit
c5cd18f498
|
@ -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>): 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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<string> // 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<CustomSortGroup>
|
||||
groupsShadow?: Array<CustomSortGroup> // 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)
|
||||
|
|
|
@ -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<ProcessingContext> = {
|
||||
_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<ProcessingContext> = {
|
||||
_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<ProcessingContext> = {
|
||||
_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<ProcessingContext> = {
|
||||
_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<ProcessingContext> = {
|
||||
_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<ProcessingContext> = {
|
||||
_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<ProcessingContext> = {
|
||||
_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<ProcessingContext> = {
|
||||
_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<ProcessingContext> = {
|
||||
_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);
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue