#127 - folder and file with the same (base) name advanced sorting support:
- the last-resort default fallback sorting method (which was alphabetical) is extended to give preference to files. In other words, if all the specified sorting levels don't sort two items, the file goes first (if the other item is a folder) - added explicit syntax to specify `files-first` or `folders-first`
This commit is contained in:
parent
c12ecb5c8c
commit
958a9b017c
|
@ -35,7 +35,11 @@ export enum CustomSortOrder {
|
||||||
standardObsidian, // whatever user selected in the UI
|
standardObsidian, // whatever user selected in the UI
|
||||||
byBookmarkOrder,
|
byBookmarkOrder,
|
||||||
byBookmarkOrderReverse,
|
byBookmarkOrderReverse,
|
||||||
default = alphabetical
|
fileFirst,
|
||||||
|
folderFirst,
|
||||||
|
alphabeticalWithFilesPreferred, // When the (base)names are equal, the file has precedence over a folder
|
||||||
|
alphabeticalWithFoldersPreferred, // When the (base)names are equal, the file has precedence over a folder
|
||||||
|
default = alphabeticalWithFilesPreferred
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecognizedOrderValue {
|
export interface RecognizedOrderValue {
|
||||||
|
|
|
@ -22,6 +22,9 @@ import {
|
||||||
sorterByMetadataField,
|
sorterByMetadataField,
|
||||||
SorterFn
|
SorterFn
|
||||||
} from './custom-sort';
|
} from './custom-sort';
|
||||||
|
import {
|
||||||
|
_unitTests
|
||||||
|
} from './custom-sort'
|
||||||
import {
|
import {
|
||||||
CustomSortGroupType,
|
CustomSortGroupType,
|
||||||
CustomSortOrder,
|
CustomSortOrder,
|
||||||
|
@ -2959,3 +2962,38 @@ describe('sorterByFolderCDate', () => {
|
||||||
expect(normalizedResultR2).toBe(orderReverseRevParams)
|
expect(normalizedResultR2).toBe(orderReverseRevParams)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('fileGoesFirstWhenSameBasenameAsFolder', () => {
|
||||||
|
const file = 'file'
|
||||||
|
const folder = 'folder'
|
||||||
|
it.each([
|
||||||
|
// main scenario - file goes first unconditionally before folder with the same name
|
||||||
|
[0, file, folder, -1, 1],
|
||||||
|
[0, folder, file, 1, -1],
|
||||||
|
// Not possible - two folders with the same name - the test only documents the behavior for clarity
|
||||||
|
[0, folder, folder, 0, 0],
|
||||||
|
// Realistic yet useless - two files with the same basename,
|
||||||
|
[0, file, file, 0, 0],
|
||||||
|
// Obvious cases - text compare returned !== 0, simply pass through
|
||||||
|
[1, file, file, 1, 1],
|
||||||
|
[1, file, folder, 1, 1],
|
||||||
|
[1, folder, file, 1, 1],
|
||||||
|
[1, folder, folder, 1, 1],
|
||||||
|
[-1, file, file, -1, -1],
|
||||||
|
[-1, file, folder, -1, -1],
|
||||||
|
[-1, folder, file, -1, -1],
|
||||||
|
[-1, folder, folder, -1, -1],
|
||||||
|
])('text compare %s of %s %s gives %s (files first) and %s (folders first)',
|
||||||
|
(textCompare: number, aIsFolder: string, bIsFolder: string, filePreferredOder: number, folderPreferredOrder: number) => {
|
||||||
|
// given
|
||||||
|
const a: Partial<FolderItemForSorting> = { isFolder: aIsFolder === folder }
|
||||||
|
const b: Partial<FolderItemForSorting> = { isFolder: bIsFolder === folder }
|
||||||
|
|
||||||
|
const resultFilePreferred: number = _unitTests.fileGoesFirstWhenSameBasenameAsFolder(textCompare, a as FolderItemForSorting, b as FolderItemForSorting)
|
||||||
|
const resultFolderPreferred: number = _unitTests.folderGoesFirstWhenSameBasenameAsFolder(textCompare, a as FolderItemForSorting, b as FolderItemForSorting)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(resultFilePreferred).toBe(filePreferredOder)
|
||||||
|
expect(resultFolderPreferred).toBe(folderPreferredOrder)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -169,8 +169,18 @@ export const sorterByFolderMDate:(reverseOrder?: boolean) => SorterFn = (reverse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FIFS = FolderItemForSorting
|
||||||
|
|
||||||
|
const fileGoesFirstWhenSameBasenameAsFolder = (stringCompareResult: number, a: FIFS, b: FIFS) =>
|
||||||
|
(!!stringCompareResult) ? stringCompareResult : (a.isFolder === b.isFolder ? EQUAL_OR_UNCOMPARABLE : (a.isFolder ? 1 : -1) );
|
||||||
|
|
||||||
|
const folderGoesFirstWhenSameBasenameAsFolder = (stringCompareResult: number, a: FIFS, b: FIFS) =>
|
||||||
|
(!!stringCompareResult) ? stringCompareResult : (a.isFolder === b.isFolder ? EQUAL_OR_UNCOMPARABLE : (a.isFolder ? -1 : 1) );
|
||||||
|
|
||||||
const Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
const Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
||||||
[CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
|
[CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
|
||||||
|
[CustomSortOrder.alphabeticalWithFilesPreferred]: (a: FIFS, b: FIFS) => fileGoesFirstWhenSameBasenameAsFolder(CollatorCompare(a.sortString, b.sortString),a,b),
|
||||||
|
[CustomSortOrder.alphabeticalWithFoldersPreferred]: (a: FIFS, b: FIFS) => fileGoesFirstWhenSameBasenameAsFolder(CollatorCompare(a.sortString, b.sortString),a,b),
|
||||||
[CustomSortOrder.alphabeticalWithFileExt]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortStringWithExt, b.sortStringWithExt),
|
[CustomSortOrder.alphabeticalWithFileExt]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortStringWithExt, b.sortStringWithExt),
|
||||||
[CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortString, b.sortString),
|
[CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortString, b.sortString),
|
||||||
[CustomSortOrder.trueAlphabeticalWithFileExt]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortStringWithExt, b.sortStringWithExt),
|
[CustomSortOrder.trueAlphabeticalWithFileExt]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortStringWithExt, b.sortStringWithExt),
|
||||||
|
@ -192,9 +202,11 @@ const Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
||||||
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forPrimary),
|
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forPrimary),
|
||||||
[CustomSortOrder.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder),
|
[CustomSortOrder.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder),
|
||||||
[CustomSortOrder.byBookmarkOrderReverse]: sorterByBookmarkOrder(ReverseOrder),
|
[CustomSortOrder.byBookmarkOrderReverse]: sorterByBookmarkOrder(ReverseOrder),
|
||||||
|
[CustomSortOrder.fileFirst]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? EQUAL_OR_UNCOMPARABLE : (a.isFolder ? 1 : -1),
|
||||||
|
[CustomSortOrder.folderFirst]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? EQUAL_OR_UNCOMPARABLE : (a.isFolder ? -1 : 0),
|
||||||
|
|
||||||
// This is a fallback entry which should not be used - the getSorterFor() function below should protect against it
|
// 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),
|
[CustomSortOrder.standardObsidian]: (a: FIFS, b: FIFS) => CollatorCompare(a.sortString, b.sortString),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Some sorters are different when used in primary vs. secondary sorting order
|
// Some sorters are different when used in primary vs. secondary sorting order
|
||||||
|
@ -729,3 +741,8 @@ export const sortFolderItemsForBookmarking = function (folder: TFolder, items: A
|
||||||
return folderItems
|
return folderItems
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const _unitTests = {
|
||||||
|
fileGoesFirstWhenSameBasenameAsFolder: fileGoesFirstWhenSameBasenameAsFolder,
|
||||||
|
folderGoesFirstWhenSameBasenameAsFolder: folderGoesFirstWhenSameBasenameAsFolder
|
||||||
|
}
|
||||||
|
|
|
@ -14,16 +14,8 @@ import {
|
||||||
RomanNumberNormalizerFn,
|
RomanNumberNormalizerFn,
|
||||||
SortingSpecProcessor
|
SortingSpecProcessor
|
||||||
} from "./sorting-spec-processor"
|
} from "./sorting-spec-processor"
|
||||||
import {
|
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, IdentityNormalizerFn} from "./custom-sort-types";
|
||||||
CustomSortGroupType,
|
import {FolderMatchingRegexp, FolderMatchingTreeNode} from "./folder-matching-rules";
|
||||||
CustomSortOrder,
|
|
||||||
CustomSortSpec,
|
|
||||||
IdentityNormalizerFn
|
|
||||||
} from "./custom-sort-types";
|
|
||||||
import {
|
|
||||||
FolderMatchingRegexp,
|
|
||||||
FolderMatchingTreeNode
|
|
||||||
} from "./folder-matching-rules";
|
|
||||||
|
|
||||||
const txtInputExampleA: string = `
|
const txtInputExampleA: string = `
|
||||||
order-asc: a-z
|
order-asc: a-z
|
||||||
|
@ -527,6 +519,49 @@ describe('SortingSpecProcessor', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const txtInputFilesOrFoldersPreferred: string = `
|
||||||
|
target-folder: AAA
|
||||||
|
< a-z, files-first
|
||||||
|
subitems1...
|
||||||
|
> folders-first, true a-z.
|
||||||
|
subitems2...
|
||||||
|
< created, folders-first
|
||||||
|
`
|
||||||
|
|
||||||
|
const expectedSortSpecForFilesOrFoldersPreferred: { [key: string]: CustomSortSpec } = {
|
||||||
|
"AAA": {
|
||||||
|
defaultOrder: CustomSortOrder.alphabetical,
|
||||||
|
defaultSecondaryOrder: CustomSortOrder.fileFirst,
|
||||||
|
groups: [{
|
||||||
|
exactPrefix: 'subitems1',
|
||||||
|
order: CustomSortOrder.folderFirst,
|
||||||
|
secondaryOrder: CustomSortOrder.trueAlphabeticalWithFileExt,
|
||||||
|
type: CustomSortGroupType.ExactPrefix
|
||||||
|
},{
|
||||||
|
exactPrefix: 'subitems2',
|
||||||
|
order: CustomSortOrder.byCreatedTime,
|
||||||
|
secondaryOrder: CustomSortOrder.folderFirst,
|
||||||
|
type: CustomSortGroupType.ExactPrefix
|
||||||
|
}, {
|
||||||
|
type: CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
outsidersGroupIdx: 2,
|
||||||
|
targetFoldersPaths: ['AAA']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SortingSpecProcessor', () => {
|
||||||
|
let processor: SortingSpecProcessor;
|
||||||
|
beforeEach(() => {
|
||||||
|
processor = new SortingSpecProcessor();
|
||||||
|
});
|
||||||
|
it('should recognize the files / folders preferred as a primary and secondary orders', () => {
|
||||||
|
const inputTxtArr: Array<string> = txtInputFilesOrFoldersPreferred.split('\n')
|
||||||
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForFilesOrFoldersPreferred)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const txtInputTrueAlphabeticalSortAttr: string = `
|
const txtInputTrueAlphabeticalSortAttr: string = `
|
||||||
target-folder: True Alpha
|
target-folder: True Alpha
|
||||||
< true a-z
|
< true a-z
|
||||||
|
|
|
@ -123,6 +123,8 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
|
||||||
'standard': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
'standard': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
||||||
'ui selected': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
'ui selected': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
||||||
'by-bookmarks-order': {asc: CustomSortOrder.byBookmarkOrder, desc: CustomSortOrder.byBookmarkOrderReverse},
|
'by-bookmarks-order': {asc: CustomSortOrder.byBookmarkOrder, desc: CustomSortOrder.byBookmarkOrderReverse},
|
||||||
|
'files-first': {asc: CustomSortOrder.fileFirst, desc: CustomSortOrder.fileFirst},
|
||||||
|
'folders-first': {asc: CustomSortOrder.folderFirst, desc: CustomSortOrder.folderFirst}
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrderByMetadataLexeme: string = 'by-metadata:'
|
const OrderByMetadataLexeme: string = 'by-metadata:'
|
||||||
|
|
Loading…
Reference in New Issue