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:
commit
2ac5a12a2c
|
@ -355,12 +355,13 @@ sorting-spec: |
|
||||||
|
|
||||||
the result is:
|
the result is:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 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
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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:'
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue