#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:
SebastianMC 2024-01-25 18:10:36 +01:00
parent c12ecb5c8c
commit 958a9b017c
5 changed files with 109 additions and 13 deletions

View File

@ -35,7 +35,11 @@ export enum CustomSortOrder {
standardObsidian, // whatever user selected in the UI
byBookmarkOrder,
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 {

View File

@ -22,6 +22,9 @@ import {
sorterByMetadataField,
SorterFn
} from './custom-sort';
import {
_unitTests
} from './custom-sort'
import {
CustomSortGroupType,
CustomSortOrder,
@ -2959,3 +2962,38 @@ describe('sorterByFolderCDate', () => {
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)
})
})

View File

@ -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 } = {
[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.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortString, b.sortString),
[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.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder),
[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
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
// A fallback entry which should not be used - the getSorterFor() function below should protect against it
[CustomSortOrder.standardObsidian]: (a: FIFS, b: FIFS) => CollatorCompare(a.sortString, b.sortString),
};
// 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
}
};
export const _unitTests = {
fileGoesFirstWhenSameBasenameAsFolder: fileGoesFirstWhenSameBasenameAsFolder,
folderGoesFirstWhenSameBasenameAsFolder: folderGoesFirstWhenSameBasenameAsFolder
}

View File

@ -14,16 +14,8 @@ import {
RomanNumberNormalizerFn,
SortingSpecProcessor
} from "./sorting-spec-processor"
import {
CustomSortGroupType,
CustomSortOrder,
CustomSortSpec,
IdentityNormalizerFn
} from "./custom-sort-types";
import {
FolderMatchingRegexp,
FolderMatchingTreeNode
} from "./folder-matching-rules";
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, IdentityNormalizerFn} from "./custom-sort-types";
import {FolderMatchingRegexp, FolderMatchingTreeNode} from "./folder-matching-rules";
const txtInputExampleA: string = `
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 = `
target-folder: True Alpha
< true a-z

View File

@ -123,6 +123,8 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
'standard': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
'ui selected': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
'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:'