Merge pull request #16 from SebastianMC/13-feature-support-modified-date-sort-for-folders

13 feature support modified date sort for folders
This commit is contained in:
SebastianMC 2022-09-26 21:12:52 +02:00 committed by GitHub
commit 2ac5a12a2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 386 additions and 37 deletions

View File

@ -355,12 +355,13 @@ sorting-spec: |
the result is: the result is:
![Book - Roman compond suffixes](./docs/svg/roman-suffix.svg) ![Book - Roman compound suffixes](./docs/svg/roman-suffix.svg)
### Example 12: Apply same sorting to all folders in the vault ### Example 12: Apply same sorting to all folders in the vault
Apply the alphabetical sorting to all folders in the Vault. The alphabetical sorting treats the folders and files equally Apply the same advanced modified date sorting to all folders in the Vault. The advanced modified sorting treats the folders
(which is different from the standard Obsidian sort, which groups folders in the top of File Explorer) and files equally (which is different from the standard Obsidian sort, which groups folders in the top of File Explorer)
The modified date for a folder is derived from its newest direct child file (if any), otherwise a folder is considered old
This involves the wildcard suffix syntax `*` which means _apply the sorting rule to the specified folder This involves the wildcard suffix syntax `*` which means _apply the sorting rule to the specified folder
and all of its subfolders, including descendants. In other words, this is imposing a deep inheritance and all of its subfolders, including descendants. In other words, this is imposing a deep inheritance
@ -371,7 +372,7 @@ Applying the wildcard suffix to root folder path `/*` actually means _apply the
--- ---
sorting-spec: | sorting-spec: |
target-folder: /* target-folder: /*
< a-z > advanced modified
--- ---
``` ```

View File

@ -1,3 +1,45 @@
Yet to be filled with content ;-) > Document is partial, creation in progress
> Please refer to [README.md](../../README.md) for usage examples
> Check [manual.md](), maybe that file has already some content?
Check [manual.md](), maybe that file has already some content? ### Supported sorting methods
#### At folder level only
- `sorting: standard` - gives back the control on order of items in hands of standard Obsidian mechanisms (UI driven).
Typical (and intended) use: exclude a folder (or folders subtree) from a custom sorting resulting from wilcard-based target folder rule
#### At folder and group level
- `< a-z` - alphabetical
- `> a-z` - alphabetical reverse, aka alphabetical descending, 'z' goes before 'a'
- `< modified` - by modified time, the long untouched item goes first (modified time of folder is assumed the beginning of the world, so folders go first and alphabetical)
- `> modified` - by modified time reverse, the most recently modified item goes first (modified time of folder is assumed the beginning of the world, so folders land in the bottom and alphabetical)
- `< created` - by created time, the oldest item goes first (modified time of folder is assumed the beginning of the world, so folders go first and alphabetical)
- `> created` - by created time reverse, the newest item goes first (modified time of folder is assumed the beginning of the world, so folders land in the bottom and alphabetical)
- `< advanced modified` - by modified time, the long untouched item goes first. For folders, their modification date is derived from the most recently modified direct child file.
For extremely large vaults use with caution, as the sorting needs to scan all files inside a folder to determine the folder's modified date
- `> advanced modified` - by modified time reverse, the most recently modified item goes first. For folders, their modification date is derived from the most recently modified direct child file.
For extremely large vaults use with caution, as the sorting needs to scan all files inside a folder to determine the folder's modified date
- `< advanced created` - by created time, the oldest item goes first. For folders, their creation date is derived from the oldest direct child file.
For extremely large vaults use with caution, as the sorting needs to scan all files inside a folder to determine the folder's created date
- `> advanced created` - by created time reverse, the newest item goes first. For folders, their creation date is derived from the newest direct child file.
For extremely large vaults use with caution, as the sorting needs to scan all files inside a folder to determine the folder's created date
#### At group level only (aka secondary sorting rule)
> Only applicable in edge cases based on numerical symbols, when the regex-based match is equal for more than one item
and need to apply a secondary order on same matches.
- `< a-z, created`
- `> a-z, created`
- `< a-z, created desc`
- `> a-z, created desc`
- `< a-z, modified`
- `> a-z, modified`
- `< a-z, modified desc`
- `> a-z, modified desc`
- `< a-z, advanced modified`
- `> a-z, advanced modified`
- `< a-z, advanced modified desc`
- `> a-z, advanced modified desc`

View File

@ -10,10 +10,14 @@ export enum CustomSortGroupType {
export enum CustomSortOrder { export enum CustomSortOrder {
alphabetical = 1, // = 1 to allow: if (customSortOrder) { ... alphabetical = 1, // = 1 to allow: if (customSortOrder) { ...
alphabeticalReverse, alphabeticalReverse,
byModifiedTime, byModifiedTime, // New to old
byModifiedTimeReverse, byModifiedTimeAdvanced,
byCreatedTime, byModifiedTimeReverse, // Old to new
byModifiedTimeReverseAdvanced,
byCreatedTime, // New to old
byCreatedTimeAdvanced,
byCreatedTimeReverse, byCreatedTimeReverse,
byCreatedTimeReverseAdvanced,
standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
default = alphabetical default = alphabetical
} }

View File

@ -1,6 +1,11 @@
import {TFile, TFolder, Vault} from 'obsidian'; import {TFile, TFolder, Vault} from 'obsidian';
import {determineSortingGroup} from './custom-sort'; import {
import {CustomSortGroupType, CustomSortSpec} from './custom-sort-types'; DEFAULT_FOLDER_CTIME,
determineFolderDatesIfNeeded,
determineSortingGroup,
FolderItemForSorting
} from './custom-sort';
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from './custom-sort-types';
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor";
const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, mtime?: number): TFile => { const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, mtime?: number): TFile => {
@ -19,7 +24,31 @@ const mockTFile = (basename: string, ext: string, size?: number, ctime?: number,
} }
} }
const mockTFolder = (name: string, children?: Array<TFolder|TFile>, parent?: TFolder): TFolder => {
return {
isRoot(): boolean { return name === '/' },
vault: {} as Vault, // To satisfy TS typechecking
path: `/${name}`,
name: name,
parent: parent ?? ({} as TFolder), // To satisfy TS typechecking
children: children ?? []
}
}
const MOCK_TIMESTAMP: number = 1656417542418 const MOCK_TIMESTAMP: number = 1656417542418
const TIMESTAMP_OLDEST: number = MOCK_TIMESTAMP
const TIMESTAMP_NEWEST: number = MOCK_TIMESTAMP + 1000
const TIMESTAMP_INBETWEEN: number = MOCK_TIMESTAMP + 500
const mockTFolderWithChildren = (name: string): TFolder => {
const child1: TFolder = mockTFolder('Section A')
const child2: TFolder = mockTFolder('Section B')
const child3: TFile = mockTFile('Child file 1 created as oldest, modified recently', 'md', 100, TIMESTAMP_OLDEST, TIMESTAMP_NEWEST)
const child4: TFile = mockTFile('Child file 2 created as newest, not modified at all', 'md', 100, TIMESTAMP_NEWEST, TIMESTAMP_NEWEST)
const child5: TFile = mockTFile('Child file 3 created inbetween, modified inbetween', 'md', 100, TIMESTAMP_INBETWEEN, TIMESTAMP_INBETWEEN)
return mockTFolder('Mock parent folder', [child1, child2, child3, child4, child5])
}
describe('determineSortingGroup', () => { describe('determineSortingGroup', () => {
describe('CustomSortGroupType.ExactHeadAndTail', () => { describe('CustomSortGroupType.ExactHeadAndTail', () => {
@ -43,7 +72,8 @@ describe('determineSortingGroup', () => {
groupIdx: 0, groupIdx: 0,
isFolder: false, isFolder: false,
sortString: "References.md", sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222, ctimeNewest: MOCK_TIMESTAMP + 222,
ctimeOldest: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333, mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md' path: 'Some parent folder/References.md'
}); });
@ -68,7 +98,8 @@ describe('determineSortingGroup', () => {
groupIdx: 1, // This indicates the last+1 idx groupIdx: 1, // This indicates the last+1 idx
isFolder: false, isFolder: false,
sortString: "References.md", sortString: "References.md",
ctime: MOCK_TIMESTAMP + 555, ctimeNewest: MOCK_TIMESTAMP + 555,
ctimeOldest: MOCK_TIMESTAMP + 555,
mtime: MOCK_TIMESTAMP + 666, mtime: MOCK_TIMESTAMP + 666,
path: 'Some parent folder/References.md' path: 'Some parent folder/References.md'
}); });
@ -96,7 +127,8 @@ describe('determineSortingGroup', () => {
groupIdx: 1, // This indicates the last+1 idx groupIdx: 1, // This indicates the last+1 idx
isFolder: false, isFolder: false,
sortString: "Part123:-icle.md", sortString: "Part123:-icle.md",
ctime: MOCK_TIMESTAMP + 555, ctimeNewest: MOCK_TIMESTAMP + 555,
ctimeOldest: MOCK_TIMESTAMP + 555,
mtime: MOCK_TIMESTAMP + 666, mtime: MOCK_TIMESTAMP + 666,
path: 'Some parent folder/Part123:-icle.md' path: 'Some parent folder/Part123:-icle.md'
}); });
@ -125,7 +157,8 @@ describe('determineSortingGroup', () => {
isFolder: false, isFolder: false,
sortString: "00000123////Part123:-icle.md", sortString: "00000123////Part123:-icle.md",
matchGroup: '00000123//', matchGroup: '00000123//',
ctime: MOCK_TIMESTAMP + 555, ctimeNewest: MOCK_TIMESTAMP + 555,
ctimeOldest: MOCK_TIMESTAMP + 555,
mtime: MOCK_TIMESTAMP + 666, mtime: MOCK_TIMESTAMP + 666,
path: 'Some parent folder/Part123:-icle.md' path: 'Some parent folder/Part123:-icle.md'
}); });
@ -153,7 +186,8 @@ describe('determineSortingGroup', () => {
groupIdx: 1, // This indicates the last+1 idx groupIdx: 1, // This indicates the last+1 idx
isFolder: false, isFolder: false,
sortString: "Part:123-icle.md", sortString: "Part:123-icle.md",
ctime: MOCK_TIMESTAMP + 555, ctimeNewest: MOCK_TIMESTAMP + 555,
ctimeOldest: MOCK_TIMESTAMP + 555,
mtime: MOCK_TIMESTAMP + 666, mtime: MOCK_TIMESTAMP + 666,
path: 'Some parent folder/Part:123-icle.md' path: 'Some parent folder/Part:123-icle.md'
}); });
@ -182,7 +216,8 @@ describe('determineSortingGroup', () => {
isFolder: false, isFolder: false,
sortString: "00000123////Part:123-icle.md", sortString: "00000123////Part:123-icle.md",
matchGroup: '00000123//', matchGroup: '00000123//',
ctime: MOCK_TIMESTAMP + 555, ctimeNewest: MOCK_TIMESTAMP + 555,
ctimeOldest: MOCK_TIMESTAMP + 555,
mtime: MOCK_TIMESTAMP + 666, mtime: MOCK_TIMESTAMP + 666,
path: 'Some parent folder/Part:123-icle.md' path: 'Some parent folder/Part:123-icle.md'
}); });
@ -208,7 +243,8 @@ describe('determineSortingGroup', () => {
groupIdx: 0, groupIdx: 0,
isFolder: false, isFolder: false,
sortString: "References.md", sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222, ctimeNewest: MOCK_TIMESTAMP + 222,
ctimeOldest: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333, mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md' path: 'Some parent folder/References.md'
}); });
@ -236,7 +272,8 @@ describe('determineSortingGroup', () => {
isFolder: false, isFolder: false,
sortString: '00000001|00000030|00000006|00001900////Reference i.xxx.vi.mcm.md', sortString: '00000001|00000030|00000006|00001900////Reference i.xxx.vi.mcm.md',
matchGroup: "00000001|00000030|00000006|00001900//", matchGroup: "00000001|00000030|00000006|00001900//",
ctime: MOCK_TIMESTAMP + 222, ctimeNewest: MOCK_TIMESTAMP + 222,
ctimeOldest: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333, mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/Reference i.xxx.vi.mcm.md' path: 'Some parent folder/Reference i.xxx.vi.mcm.md'
}); });
@ -260,10 +297,83 @@ describe('determineSortingGroup', () => {
groupIdx: 1, // This indicates the last+1 idx groupIdx: 1, // This indicates the last+1 idx
isFolder: false, isFolder: false,
sortString: "References.md", sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222, ctimeNewest: MOCK_TIMESTAMP + 222,
ctimeOldest: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333, mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md' path: 'Some parent folder/References.md'
}); });
}) })
}) })
}) })
describe('determineFolderDatesIfNeeded', () => {
it('should not be triggered if not needed - sorting method does not require it', () => {
// given
const folder: TFolder = mockTFolderWithChildren('Test folder 1')
const OUTSIDERS_GROUP_IDX = 0
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.Outsiders,
order: CustomSortOrder.alphabetical
}],
outsidersGroupIdx: OUTSIDERS_GROUP_IDX
}
const cardinality = {[OUTSIDERS_GROUP_IDX]: 10} // Group 0 contains 10 items
// when
const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec)
determineFolderDatesIfNeeded([result], sortSpec, cardinality)
// then
expect(result.ctimeOldest).toEqual(DEFAULT_FOLDER_CTIME)
expect(result.ctimeNewest).toEqual(DEFAULT_FOLDER_CTIME)
expect(result.mtime).toEqual(DEFAULT_FOLDER_CTIME)
})
it('should not be triggered if not needed - the folder is an only item', () => {
// given
const folder: TFolder = mockTFolderWithChildren('Test folder 1')
const OUTSIDERS_GROUP_IDX = 0
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.Outsiders,
order: CustomSortOrder.byModifiedTimeAdvanced
}],
outsidersGroupIdx: OUTSIDERS_GROUP_IDX
}
const cardinality = {[OUTSIDERS_GROUP_IDX]: 1} // Group 0 contains the folder alone
// when
const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec)
determineFolderDatesIfNeeded([result], sortSpec, cardinality)
// then
expect(result.ctimeOldest).toEqual(DEFAULT_FOLDER_CTIME)
expect(result.ctimeNewest).toEqual(DEFAULT_FOLDER_CTIME)
expect(result.mtime).toEqual(DEFAULT_FOLDER_CTIME)
})
it('should correctly determine dates, if triggered', () => {
// given
const folder: TFolder = mockTFolderWithChildren('Test folder 1')
const OUTSIDERS_GROUP_IDX = 0
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.Outsiders,
order: CustomSortOrder.byCreatedTimeReverseAdvanced
}],
outsidersGroupIdx: OUTSIDERS_GROUP_IDX
}
const cardinality = {[OUTSIDERS_GROUP_IDX]: 10} // Group 0 contains 10 items
// when
const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec)
determineFolderDatesIfNeeded([result], sortSpec, cardinality)
// then
expect(result.ctimeOldest).toEqual(TIMESTAMP_OLDEST)
expect(result.ctimeNewest).toEqual(TIMESTAMP_NEWEST)
expect(result.mtime).toEqual(TIMESTAMP_NEWEST)
})
})

View File

@ -1,4 +1,4 @@
import {requireApiVersion, TFile, TFolder} from 'obsidian'; import {requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian';
import {CustomSortGroup, CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types"; import {CustomSortGroup, CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types";
import {isDefined} from "../utils/utils"; import {isDefined} from "../utils/utils";
@ -8,14 +8,16 @@ let Collator = new Intl.Collator(undefined, {
numeric: true, numeric: true,
}).compare; }).compare;
interface FolderItemForSorting { export interface FolderItemForSorting {
path: string path: string
groupIdx?: number // the index itself represents order for groups groupIdx?: number // the index itself represents order for groups
sortString: string // fragment (or full name) to be used for sorting sortString: string // fragment (or full name) to be used for sorting
matchGroup?: string // advanced - used for secondary sorting rule, to recognize 'same regex match' matchGroup?: string // advanced - used for secondary sorting rule, to recognize 'same regex match'
ctime: number ctimeOldest: number // for a file, both ctime values are the same. For folder they can be different:
ctimeNewest: number // ctimeOldest = ctime of oldest child file, ctimeNewest = ctime of newest child file
mtime: number mtime: number
isFolder: boolean isFolder: boolean
folder?: TFolder
} }
type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
@ -23,10 +25,14 @@ type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
let Sorters: { [key in CustomSortOrder]: SorterFn } = { let Sorters: { [key in CustomSortOrder]: SorterFn } = {
[CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString), [CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString),
[CustomSortOrder.alphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(b.sortString, a.sortString), [CustomSortOrder.alphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(b.sortString, a.sortString),
[CustomSortOrder.byModifiedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.mtime - b.mtime, [CustomSortOrder.byModifiedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (a.mtime - b.mtime),
[CustomSortOrder.byModifiedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.mtime - a.mtime, [CustomSortOrder.byModifiedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.mtime - b.mtime,
[CustomSortOrder.byCreatedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctime - b.ctime, [CustomSortOrder.byModifiedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (b.mtime - a.mtime),
[CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctime - a.ctime, [CustomSortOrder.byModifiedTimeReverseAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.mtime - a.mtime,
[CustomSortOrder.byCreatedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (a.ctimeNewest - b.ctimeNewest),
[CustomSortOrder.byCreatedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctimeNewest - b.ctimeNewest,
[CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (b.ctimeOldest - a.ctimeOldest),
[CustomSortOrder.byCreatedTimeReverseAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctimeOldest - a.ctimeOldest,
// This is a fallback entry which should not be used - the plugin code should refrain from custom sorting at all // This is a fallback entry which should not be used - the plugin code should refrain from custom sorting at all
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString), [CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString),
@ -52,11 +58,14 @@ function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, s
} }
} }
const isFolder = (entry: TFile | TFolder) => { const isFolder = (entry: TAbstractFile) => {
// The plain obvious 'entry instanceof TFolder' doesn't work inside Jest unit tests, hence a workaround below // The plain obvious 'entry instanceof TFolder' doesn't work inside Jest unit tests, hence a workaround below
return !!((entry as any).isRoot); return !!((entry as any).isRoot);
} }
export const DEFAULT_FOLDER_MTIME: number = 0
export const DEFAULT_FOLDER_CTIME: number = 0
export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec): FolderItemForSorting { export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec): FolderItemForSorting {
let groupIdx: number let groupIdx: number
let determined: boolean = false let determined: boolean = false
@ -165,14 +174,71 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
sortString: matchedGroup ? (matchedGroup + '//' + entry.name) : entry.name, sortString: matchedGroup ? (matchedGroup + '//' + entry.name) : entry.name,
matchGroup: matchedGroup ?? undefined, matchGroup: matchedGroup ?? undefined,
isFolder: aFolder, isFolder: aFolder,
folder: aFolder ? (entry as TFolder) : undefined,
path: entry.path, path: entry.path,
ctime: aFile ? entryAsTFile.stat.ctime : 0, ctimeNewest: aFile ? entryAsTFile.stat.ctime : DEFAULT_FOLDER_CTIME,
mtime: aFile ? entryAsTFile.stat.mtime : 0 ctimeOldest: aFile ? entryAsTFile.stat.ctime : DEFAULT_FOLDER_CTIME,
mtime: aFile ? entryAsTFile.stat.mtime : DEFAULT_FOLDER_MTIME
} }
} }
const SortOrderRequiringFolderDate = new Set<CustomSortOrder>([
CustomSortOrder.byModifiedTimeAdvanced,
CustomSortOrder.byModifiedTimeReverseAdvanced,
CustomSortOrder.byCreatedTimeAdvanced,
CustomSortOrder.byCreatedTimeReverseAdvanced
])
export const sortOrderNeedsFolderDates = (order: CustomSortOrder | undefined, secondary?: CustomSortOrder): boolean => {
// The CustomSortOrder.standardObsidian used as default because it doesn't require date on folders
return SortOrderRequiringFolderDate.has(order ?? CustomSortOrder.standardObsidian)
|| SortOrderRequiringFolderDate.has(secondary ?? CustomSortOrder.standardObsidian)
}
// Syntax sugar for readability
export type ModifiedTime = number
export type CreatedTimeNewest = number
export type CreatedTimeOldest = number
export const determineDatesForFolder = (folder: TFolder, now: number): [ModifiedTime, CreatedTimeNewest, CreatedTimeOldest] => {
let mtimeOfFolder: ModifiedTime = DEFAULT_FOLDER_MTIME
let ctimeNewestOfFolder: CreatedTimeNewest = DEFAULT_FOLDER_CTIME
let ctimeOldestOfFolder: CreatedTimeOldest = now
folder.children.forEach((item) => {
if (!isFolder(item)) {
const file: TFile = item as TFile
if (file.stat.mtime > mtimeOfFolder) {
mtimeOfFolder = file.stat.mtime
}
if (file.stat.ctime > ctimeNewestOfFolder) {
ctimeNewestOfFolder = file.stat.ctime
}
if (file.stat.ctime < ctimeOldestOfFolder) {
ctimeOldestOfFolder = file.stat.ctime
}
}
})
return [mtimeOfFolder, ctimeNewestOfFolder, ctimeOldestOfFolder]
}
export const determineFolderDatesIfNeeded = (folderItems: Array<FolderItemForSorting>, sortingSpec: CustomSortSpec, sortingGroupsCardinality: {[key: number]: number} = {}) => {
const Now: number = Date.now()
folderItems.forEach((item) => {
const groupIdx: number | undefined = item.groupIdx
if (groupIdx !== undefined && sortingGroupsCardinality[groupIdx] > 1) {
const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order
if (sortOrderNeedsFolderDates(groupOrder)) {
if (item.folder) {
[item.mtime, item.ctimeNewest, item.ctimeOldest] = determineDatesForFolder(item.folder, Now)
}
}
}
})
}
export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) { export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) {
let fileExplorer = this.fileExplorer let fileExplorer = this.fileExplorer
const sortingGroupsCardinality: {[key: number]: number} = {}
const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ? const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ?
this.file.children.filter((entry: TFile | TFolder) => { this.file.children.filter((entry: TFile | TFolder) => {
@ -180,9 +246,17 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
}) })
: :
this.file.children) this.file.children)
.map((entry: TFile | TFolder) => .map((entry: TFile | TFolder) => {
determineSortingGroup(entry, sortingSpec) const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec)
) const groupIdx: number | undefined = itemForSorting.groupIdx
if (groupIdx !== undefined) {
sortingGroupsCardinality[groupIdx] = 1 + (sortingGroupsCardinality[groupIdx] ?? 0)
}
return itemForSorting
})
// Finally, for advanced sorting by modified date, for some of the folders the modified date has to be determined
determineFolderDatesIfNeeded(folderItems, sortingSpec, sortingGroupsCardinality)
folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) { folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) {
return compareTwoItems(itA, itB, sortingSpec); return compareTwoItems(itA, itB, sortingSpec);

View File

@ -31,7 +31,7 @@ target-folder: tricky folder
target-folder: / target-folder: /
/: Con... /: Con...
/ /
> modified > advanced modified
/: /:
< modified < modified
/: Ref... /: Ref...
@ -78,7 +78,7 @@ target-folder: tricky folder
target-folder: / target-folder: /
/:files Con... /:files Con...
/folders /folders
> modified > advanced modified
/:files /:files
< modified < modified
/:files Ref... /:files Ref...
@ -163,7 +163,7 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = {
type: CustomSortGroupType.ExactPrefix type: CustomSortGroupType.ExactPrefix
}, { }, {
foldersOnly: true, foldersOnly: true,
order: CustomSortOrder.byModifiedTimeReverse, order: CustomSortOrder.byModifiedTimeReverseAdvanced,
type: CustomSortGroupType.Outsiders type: CustomSortGroupType.Outsiders
}, { }, {
filesOnly: true, filesOnly: true,
@ -865,6 +865,102 @@ describe('SortingSpecProcessor path wildcard priorities', () => {
}) })
}) })
const txtInputAdvancedFolderDateSortingMethods: string = `
target-folder: A
< advanced modified
target-folder: B
> advanced modified
target-folder: C
< advanced created
target-folder: D
> advanced created
target-folder: AA
/folders
< advanced modified
/:files
> advanced modified
/folders Archive...
< advanced created
/:files Archive...
> advanced created
`
const expectedSortSpecForAdvancedFolderDateSortingMethods: { [key: string]: CustomSortSpec } = {
'A': {
defaultOrder: CustomSortOrder.byModifiedTimeAdvanced,
groups: [{
order: CustomSortOrder.byModifiedTimeAdvanced,
type: CustomSortGroupType.Outsiders
}],
outsidersGroupIdx: 0,
targetFoldersPaths: ['A']
},
'B': {
defaultOrder: CustomSortOrder.byModifiedTimeReverseAdvanced,
groups: [{
order: CustomSortOrder.byModifiedTimeReverseAdvanced,
type: CustomSortGroupType.Outsiders
}],
outsidersGroupIdx: 0,
targetFoldersPaths: ['B']
},
'C': {
defaultOrder: CustomSortOrder.byCreatedTimeAdvanced,
groups: [{
order: CustomSortOrder.byCreatedTimeAdvanced,
type: CustomSortGroupType.Outsiders
}],
outsidersGroupIdx: 0,
targetFoldersPaths: ['C']
},
'D': {
defaultOrder: CustomSortOrder.byCreatedTimeReverseAdvanced,
groups: [{
order: CustomSortOrder.byCreatedTimeReverseAdvanced,
type: CustomSortGroupType.Outsiders
}],
outsidersGroupIdx: 0,
targetFoldersPaths: ['D']
},
'AA': {
groups: [{
foldersOnly: true,
order: CustomSortOrder.byModifiedTimeAdvanced,
type: CustomSortGroupType.Outsiders
}, {
filesOnly: true,
order: CustomSortOrder.byModifiedTimeReverseAdvanced,
type: CustomSortGroupType.Outsiders
}, {
exactPrefix: 'Archive',
foldersOnly: true,
order: CustomSortOrder.byCreatedTimeAdvanced,
type: 3
}, {
exactPrefix: 'Archive',
filesOnly: true,
order: CustomSortOrder.byCreatedTimeReverseAdvanced,
type: 3
}],
outsidersFilesGroupIdx: 1,
outsidersFoldersGroupIdx: 0,
targetFoldersPaths: ['AA']
}
}
describe('SortingSpecProcessor advanced folder-date based sorting methods', () => {
let processor: SortingSpecProcessor;
beforeEach(() => {
processor = new SortingSpecProcessor();
});
it('should not raise error for multiple spec for the same path and choose correct spec, case A', () => {
const inputTxtArr: Array<string> = txtInputAdvancedFolderDateSortingMethods.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForAdvancedFolderDateSortingMethods)
expect(result?.sortSpecByWildcard).toBeUndefined()
})
})
const errorsLogger = jest.fn(); const errorsLogger = jest.fn();
const ERR_PREFIX = 'Sorting specification problem:' const ERR_PREFIX = 'Sorting specification problem:'

View File

@ -86,6 +86,8 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
'a-z': {asc: CustomSortOrder.alphabetical, desc: CustomSortOrder.alphabeticalReverse}, 'a-z': {asc: CustomSortOrder.alphabetical, desc: CustomSortOrder.alphabeticalReverse},
'created': {asc: CustomSortOrder.byCreatedTime, desc: CustomSortOrder.byCreatedTimeReverse}, 'created': {asc: CustomSortOrder.byCreatedTime, desc: CustomSortOrder.byCreatedTimeReverse},
'modified': {asc: CustomSortOrder.byModifiedTime, desc: CustomSortOrder.byModifiedTimeReverse}, 'modified': {asc: CustomSortOrder.byModifiedTime, desc: CustomSortOrder.byModifiedTimeReverse},
'advanced modified': {asc: CustomSortOrder.byModifiedTimeAdvanced, desc: CustomSortOrder.byModifiedTimeReverseAdvanced},
'advanced created': {asc: CustomSortOrder.byCreatedTimeAdvanced, desc: CustomSortOrder.byCreatedTimeReverseAdvanced},
// Advanced, for edge cases of secondary sorting, when if regexp match is the same, override the alphabetical sorting by full name // Advanced, for edge cases of secondary sorting, when if regexp match is the same, override the alphabetical sorting by full name
'a-z, created': { 'a-z, created': {
@ -107,6 +109,26 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
asc: CustomSortOrder.alphabetical, asc: CustomSortOrder.alphabetical,
desc: CustomSortOrder.alphabeticalReverse, desc: CustomSortOrder.alphabeticalReverse,
secondary: CustomSortOrder.byModifiedTimeReverse secondary: CustomSortOrder.byModifiedTimeReverse
},
'a-z, advanced created': {
asc: CustomSortOrder.alphabetical,
desc: CustomSortOrder.alphabeticalReverse,
secondary: CustomSortOrder.byCreatedTimeAdvanced
},
'a-z, advanced created desc': {
asc: CustomSortOrder.alphabetical,
desc: CustomSortOrder.alphabeticalReverse,
secondary: CustomSortOrder.byCreatedTimeReverseAdvanced
},
'a-z, advanced modified': {
asc: CustomSortOrder.alphabetical,
desc: CustomSortOrder.alphabeticalReverse,
secondary: CustomSortOrder.byModifiedTimeAdvanced
},
'a-z, advanced modified desc': {
asc: CustomSortOrder.alphabetical,
desc: CustomSortOrder.alphabeticalReverse,
secondary: CustomSortOrder.byModifiedTimeReverseAdvanced
} }
} }
@ -399,7 +421,7 @@ export class SortingSpecProcessor {
lineIdx++ lineIdx++
this.currentEntryLine = entryLine this.currentEntryLine = entryLine
this.currentEntryLineIdx = lineIdx this.currentEntryLineIdx = lineIdx
this.currentSortingSpecContainerFilePath = `${folderPath}/${sortingSpecFileName}` this.currentSortingSpecContainerFilePath = `${folderPath === '/' ? '' : folderPath}/${sortingSpecFileName}`
this.problemAlreadyReportedForCurrentLine = false this.problemAlreadyReportedForCurrentLine = false
const trimmedEntryLine: string = entryLine.trim() const trimmedEntryLine: string = entryLine.trim()