Merge branch '171-poc-metadata-value-extractors-idea' into nov-2024-refactoring-regroup-sorting-structures

# Conflicts:
#	manifest.json
#	src/test/unit/sorting-spec-processor.spec.ts
This commit is contained in:
SebastianMC 2025-01-03 19:43:40 +01:00
commit 6a47c51182
13 changed files with 938 additions and 785 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "obsidian-custom-sort", "name": "obsidian-custom-sort",
"version": "2.1.15", "version": "3.0.0",
"description": "Custom Sort plugin for Obsidian (https://obsidian.md)", "description": "Custom Sort plugin for Obsidian (https://obsidian.md)",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@ -24,12 +24,11 @@
"builtin-modules": "3.3.0", "builtin-modules": "3.3.0",
"esbuild": "0.17.3", "esbuild": "0.17.3",
"eslint": "^8.29.0", "eslint": "^8.29.0",
"jest": "^28.1.1", "jest": "^29.7.0",
"monkey-around": "^2.3.0", "monkey-around": "^3.0.0",
"obsidian": "^0.15.4", "obsidian": "^1.7.2",
"obsidian-1.4.11": "npm:obsidian@1.4.11", "ts-jest": "^29.2.5",
"ts-jest": "^28.0.5", "tslib": "2.8.1",
"tslib": "2.4.0", "typescript": "5.7.2"
"typescript": "4.7.4"
} }
} }

View File

@ -8,7 +8,6 @@ export enum CustomSortGroupType {
ExactSuffix, ExactSuffix,
ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title
HasMetadataField, // Notes (or folder's notes) containing a specific metadata field HasMetadataField, // Notes (or folder's notes) containing a specific metadata field
StarredOnly,
BookmarkedOnly, BookmarkedOnly,
HasIcon HasIcon
} }

View File

@ -16,7 +16,6 @@ export interface HasSortingTypes {
export interface HasGroupingTypes { export interface HasGroupingTypes {
byBookmarks: number byBookmarks: number
byStarred: number
byIcon: number byIcon: number
total: number total: number
} }
@ -31,10 +30,6 @@ export const checkByBookmark = (has: HasSortingOrGrouping, order?: CustomSortOrd
(order === CustomSortOrder.byBookmarkOrder || order === CustomSortOrder.byBookmarkOrderReverse) && has.sorting.byBookmarks++; (order === CustomSortOrder.byBookmarkOrder || order === CustomSortOrder.byBookmarkOrderReverse) && has.sorting.byBookmarks++;
} }
export const checkByStarred = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
groupType === CustomSortGroupType.StarredOnly && has.grouping.byStarred++;
}
export const checkByIcon = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => { export const checkByIcon = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
groupType === CustomSortGroupType.HasIcon && has.grouping.byIcon++; groupType === CustomSortGroupType.HasIcon && has.grouping.byIcon++;
} }
@ -45,7 +40,6 @@ export const checkStandardObsidian = (has: HasSortingOrGrouping, order?: CustomS
export const doCheck = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType) => { export const doCheck = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType) => {
checkByBookmark(has, order, groupType) checkByBookmark(has, order, groupType)
checkByStarred(has, order, groupType)
checkByIcon(has, order, groupType) checkByIcon(has, order, groupType)
checkStandardObsidian(has, order, groupType) checkStandardObsidian(has, order, groupType)
@ -56,7 +50,7 @@ export const doCheck = (has: HasSortingOrGrouping, order?: CustomSortOrder, grou
export const collectSortingAndGroupingTypes = (sortSpec?: CustomSortSpec|null): HasSortingOrGrouping => { export const collectSortingAndGroupingTypes = (sortSpec?: CustomSortSpec|null): HasSortingOrGrouping => {
const has: HasSortingOrGrouping = { const has: HasSortingOrGrouping = {
grouping: { grouping: {
byIcon: 0, byStarred: 0, byBookmarks: 0, total: 0 byIcon: 0, byBookmarks: 0, total: 0
}, },
sorting: { sorting: {
byBookmarks: 0, standardObsidian: 0, total: 0 byBookmarks: 0, standardObsidian: 0, total: 0

View File

@ -1,17 +1,11 @@
import { import {
FrontMatterCache, FrontMatterCache,
MetadataCache, MetadataCache,
Plugin,
requireApiVersion,
TAbstractFile, TAbstractFile,
TFile, TFile,
TFolder, TFolder,
Vault Vault
} from 'obsidian'; } from 'obsidian';
import {
determineStarredStatusOf,
Starred_PluginInstance
} from '../utils/StarredPluginSignature';
import { import {
determineIconOf, determineIconOf,
ObsidianIconFolder_PluginInstance ObsidianIconFolder_PluginInstance
@ -42,7 +36,6 @@ export interface ProcessingContext {
// For internal transient use // For internal transient use
plugin?: CustomSortPluginAPI // to hand over the access to App instance to the sorting engine plugin?: CustomSortPluginAPI // to hand over the access to App instance to the sorting engine
_mCache?: MetadataCache _mCache?: MetadataCache
starredPluginInstance?: Starred_PluginInstance
bookmarksPluginInstance?: BookmarksPluginInterface, bookmarksPluginInstance?: BookmarksPluginInterface,
iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance
} }
@ -498,14 +491,6 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
} }
} }
break break
case CustomSortGroupType.StarredOnly:
if (ctx?.starredPluginInstance) {
const starred: boolean = determineStarredStatusOf(entry, aFile, ctx.starredPluginInstance)
if (starred) {
determined = true
}
}
break
case CustomSortGroupType.BookmarkedOnly: case CustomSortGroupType.BookmarkedOnly:
if (ctx?.bookmarksPluginInstance) { if (ctx?.bookmarksPluginInstance) {
const bookmarkOrder: number | undefined = ctx?.bookmarksPluginInstance.determineBookmarkOrder(entry.path) const bookmarkOrder: number | undefined = ctx?.bookmarksPluginInstance.determineBookmarkOrder(entry.path)
@ -751,28 +736,8 @@ export const determineBookmarksOrderIfNeeded = (folderItems: Array<FolderItemFor
}) })
} }
// This function is a replacement for the Obsidian File Explorer function sort(...) up to Obsidian 1.6.0 export const getSortedFolderItems = function (sortedFolder: TFolder, sortingSpec: CustomSortSpec, ctx: ProcessingContext) {
// when a major refactoring of sorting mechanics happened const sortOrder = this.sortOrder // this is bound to FileExplorer Obsidian component
export const folderSort_vUpTo_1_6_0 = function (sortingSpec: CustomSortSpec, ctx: ProcessingContext) {
const fileExplorerView = this.fileExplorer ?? this.view // this.view replaces the former since 1.5.4 insider build
const folderUnderSort = this.file as TFolder
const sortOrder = this.sortOrder
const allFileItemsCollection = fileExplorerView.fileItems
const items = folderSortCore(folderUnderSort, sortOrder, sortingSpec, allFileItemsCollection, ctx)
if (requireApiVersion && requireApiVersion("0.15.0")) {
this.vChildren.setChildren(items);
} else {
this.children = items;
}
}
// This function is a replacement for the Obsidian File Explorer function getSortedFolderItems(...)
// which first appeared in Obsidian 1.6.0 and simplified a bit the plugin integration point
export const getSortedFolderItems_vFrom_1_6_0 = function (sortedFolder: TFolder, sortingSpec: CustomSortSpec, ctx: ProcessingContext) {
const sortOrder = this.sortOrder
const allFileItemsCollection = this.fileItems const allFileItemsCollection = this.fileItems
return folderSortCore(sortedFolder, sortOrder, sortingSpec, allFileItemsCollection, ctx) return folderSortCore(sortedFolder, sortOrder, sortingSpec, allFileItemsCollection, ctx)
} }

View File

@ -271,8 +271,6 @@ const MetadataFieldIndicatorLexeme: string = 'with-metadata:'
const BookmarkedItemIndicatorLexeme: string = 'bookmarked:' const BookmarkedItemIndicatorLexeme: string = 'bookmarked:'
const StarredItemsIndicatorLexeme: string = 'starred:'
const IconIndicatorLexeme: string = 'with-icon:' const IconIndicatorLexeme: string = 'with-icon:'
const CommentPrefix: string = '//' const CommentPrefix: string = '//'
@ -1720,13 +1718,6 @@ export class SortingSpecProcessor {
foldersOnly: spec.foldersOnly, foldersOnly: spec.foldersOnly,
matchFilenameWithExt: spec.matchFilenameWithExt matchFilenameWithExt: spec.matchFilenameWithExt
} }
} else if (theOnly.startsWith(StarredItemsIndicatorLexeme)) {
return {
type: CustomSortGroupType.StarredOnly,
filesOnly: spec.filesOnly,
foldersOnly: spec.foldersOnly,
matchFilenameWithExt: spec.matchFilenameWithExt
}
} else { } else {
// For non-three dots single text line assume exact match group // For non-three dots single text line assume exact match group
return { return {

View File

@ -1,19 +1,12 @@
import { import {
apiVersion,
App,
FileExplorerView, FileExplorerView,
Menu, Menu,
MenuItem, MenuItem,
MetadataCache, MetadataCache,
normalizePath,
Notice, Notice,
Platform, Platform,
Plugin, Plugin,
PluginSettingTab,
requireApiVersion,
sanitizeHTMLToDom,
setIcon, setIcon,
Setting,
TAbstractFile, TAbstractFile,
TFile, TFile,
TFolder, TFolder,
@ -21,8 +14,7 @@ import {
} from 'obsidian'; } from 'obsidian';
import {around} from 'monkey-around'; import {around} from 'monkey-around';
import { import {
folderSort_vUpTo_1_6_0, getSortedFolderItems,
getSortedFolderItems_vFrom_1_6_0,
ObsidianStandardDefaultSortingName, ObsidianStandardDefaultSortingName,
ProcessingContext, ProcessingContext,
sortFolderItemsForBookmarking sortFolderItemsForBookmarking
@ -44,7 +36,6 @@ import {
ICON_SORT_SUSPENDED_GENERAL_ERROR, ICON_SORT_SUSPENDED_GENERAL_ERROR,
ICON_SORT_SUSPENDED_SYNTAX_ERROR ICON_SORT_SUSPENDED_SYNTAX_ERROR
} from "./custom-sort/icons"; } from "./custom-sort/icons";
import {getStarredPlugin} from "./utils/StarredPluginSignature";
import { import {
BookmarksPluginInterface, BookmarksPluginInterface,
getBookmarksPlugin, getBookmarksPlugin,
@ -124,7 +115,7 @@ export default class CustomSortPlugin
if (failed) return if (failed) return
if (file instanceof TFile) { if (file instanceof TFile) {
const aFile: TFile = file as TFile const aFile: TFile = file as TFile
const parent: TFolder = aFile.parent const parent: TFolder = aFile.parent!
// Read sorting spec from three sources of equal priority: // Read sorting spec from three sources of equal priority:
// - files with designated predefined name // - files with designated predefined name
// - files with the same name as parent folders (aka folder notes), e.g.: References/References.md // - files with the same name as parent folders (aka folder notes), e.g.: References/References.md
@ -179,16 +170,9 @@ export default class CustomSortPlugin
checkFileExplorerIsAvailableAndPatchable(logWarning: boolean = true): FileExplorerView | undefined { checkFileExplorerIsAvailableAndPatchable(logWarning: boolean = true): FileExplorerView | undefined {
let fileExplorerView: FileExplorerView | undefined = this.getFileExplorer() let fileExplorerView: FileExplorerView | undefined = this.getFileExplorer()
if (fileExplorerView && typeof fileExplorerView.requestSort === 'function') { if (fileExplorerView && typeof fileExplorerView.requestSort === 'function') {
// The plugin integration points changed with Obsidian 1.6.0 hence the patchability-check should also be Obsidian version aware
if (requireApiVersion && requireApiVersion("1.6.0")) {
if (typeof fileExplorerView.getSortedFolderItems === 'function') { if (typeof fileExplorerView.getSortedFolderItems === 'function') {
return fileExplorerView return fileExplorerView
} }
} else { // Obsidian versions prior to 1.6.0
if (typeof fileExplorerView.createFolderDom === 'function') {
return fileExplorerView
}
}
} }
// Various scenarios when File Explorer was turned off (e.g. by some other plugin) // Various scenarios when File Explorer was turned off (e.g. by some other plugin)
if (logWarning) { if (logWarning) {
@ -396,7 +380,7 @@ export default class CustomSortPlugin
item.onClick(() => { item.onClick(() => {
const bookmarksPlugin = getBookmarksPlugin(plugin.app, plugin.settings.bookmarksGroupToConsumeAsOrderingReference) const bookmarksPlugin = getBookmarksPlugin(plugin.app, plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) { if (bookmarksPlugin) {
const orderedChildren: Array<TAbstractFile> = plugin.orderedFolderItemsForBookmarking(file.parent, bookmarksPlugin) const orderedChildren: Array<TAbstractFile> = plugin.orderedFolderItemsForBookmarking(file.parent!, bookmarksPlugin)
bookmarksPlugin.bookmarkSiblings(orderedChildren) bookmarksPlugin.bookmarkSiblings(orderedChildren)
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true) bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
} }
@ -409,7 +393,7 @@ export default class CustomSortPlugin
item.onClick(() => { item.onClick(() => {
const bookmarksPlugin = getBookmarksPlugin(plugin.app, plugin.settings.bookmarksGroupToConsumeAsOrderingReference) const bookmarksPlugin = getBookmarksPlugin(plugin.app, plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) { if (bookmarksPlugin) {
const orderedChildren: Array<TAbstractFile> = file.parent.children.map((entry: TFile | TFolder) => entry) const orderedChildren: Array<TAbstractFile> = file.parent!.children.map((entry: TFile | TFolder) => entry)
bookmarksPlugin.unbookmarkSiblings(orderedChildren) bookmarksPlugin.unbookmarkSiblings(orderedChildren)
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true) bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
} }
@ -486,7 +470,6 @@ export default class CustomSortPlugin
}) })
) )
if (requireApiVersion('1.4.11')) {
this.registerEvent( this.registerEvent(
// "files-menu" event was exposed in 1.4.11 // "files-menu" event was exposed in 1.4.11
// @ts-ignore // @ts-ignore
@ -523,7 +506,6 @@ export default class CustomSortPlugin
} }
}) })
) )
}
this.registerEvent( this.registerEvent(
this.app.vault.on("rename", (file: TAbstractFile, oldPath: string) => { this.app.vault.on("rename", (file: TAbstractFile, oldPath: string) => {
@ -583,7 +565,6 @@ export default class CustomSortPlugin
createProcessingContextForSorting(has: HasSortingOrGrouping): ProcessingContext { createProcessingContextForSorting(has: HasSortingOrGrouping): ProcessingContext {
const ctx: ProcessingContext = { const ctx: ProcessingContext = {
_mCache: this.app.metadataCache, _mCache: this.app.metadataCache,
starredPluginInstance: has.grouping.byStarred ? getStarredPlugin(this.app) : undefined,
bookmarksPluginInstance: has.grouping.byBookmarks || has.sorting.byBookmarks ? getBookmarksPlugin(this.app, this.settings.bookmarksGroupToConsumeAsOrderingReference, false, true) : undefined, bookmarksPluginInstance: has.grouping.byBookmarks || has.sorting.byBookmarks ? getBookmarksPlugin(this.app, this.settings.bookmarksGroupToConsumeAsOrderingReference, false, true) : undefined,
iconFolderPluginInstance: has.grouping.byIcon ? getIconFolderPlugin(this.app) : undefined, iconFolderPluginInstance: has.grouping.byIcon ? getIconFolderPlugin(this.app) : undefined,
plugin: this plugin: this
@ -635,8 +616,6 @@ export default class CustomSortPlugin
// That's why not showing and not logging error message here // That's why not showing and not logging error message here
patchableFileExplorer = patchableFileExplorer ?? this.checkFileExplorerIsAvailableAndPatchable(false) patchableFileExplorer = patchableFileExplorer ?? this.checkFileExplorerIsAvailableAndPatchable(false)
if (patchableFileExplorer) { if (patchableFileExplorer) {
if (requireApiVersion && requireApiVersion("1.6.0")) {
// Starting from Obsidian 1.6.0 the sorting mechanics has been significantly refactored internally in Obsidian
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(patchableFileExplorer.constructor.prototype, { const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(patchableFileExplorer.constructor.prototype, {
getSortedFolderItems(old: any) { getSortedFolderItems(old: any) {
return function (...args: any[]) { return function (...args: any[]) {
@ -651,7 +630,7 @@ export default class CustomSortPlugin
const sortingData = plugin.determineAndPrepareSortingDataForFolder(folder) const sortingData = plugin.determineAndPrepareSortingDataForFolder(folder)
if (sortingData.sortSpec) { if (sortingData.sortSpec) {
return getSortedFolderItems_vFrom_1_6_0.call(this, folder, sortingData.sortSpec, plugin.createProcessingContextForSorting(sortingData.sortingAndGroupingStats)) return getSortedFolderItems.call(this, folder, sortingData.sortSpec, plugin.createProcessingContextForSorting(sortingData.sortingAndGroupingStats))
} else { } else {
return old.call(this, ...args); return old.call(this, ...args);
} }
@ -660,35 +639,6 @@ export default class CustomSortPlugin
}) })
this.register(requestStandardObsidianSortAfter(uninstallerOfFolderSortFunctionWrapper)) this.register(requestStandardObsidianSortAfter(uninstallerOfFolderSortFunctionWrapper))
return true return true
} else {
// Up to Obsidian 1.6.0
// @ts-ignore
let tmpFolder = new TFolder(Vault, "");
let Folder = patchableFileExplorer.createFolderDom(tmpFolder).constructor;
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, {
sort(old: any) {
return function (...args: any[]) {
// quick check for plugin status
if (plugin.settings.suspended) {
return old.call(this, ...args);
}
plugin.resetIconInaccurateStateToEnabled()
const folder: TFolder = this.file
const sortingData = plugin.determineAndPrepareSortingDataForFolder(folder)
if (sortingData.sortSpec) {
return folderSort_vUpTo_1_6_0.call(this, sortingData.sortSpec, plugin.createProcessingContextForSorting(sortingData.sortingAndGroupingStats));
} else {
return old.call(this, ...args);
}
};
}
})
this.register(requestStandardObsidianSortAfter(uninstallerOfFolderSortFunctionWrapper))
return true
}
} else { } else {
return false return false
} }
@ -732,7 +682,7 @@ export default class CustomSortPlugin
const data: any = await this.loadData() || {} const data: any = await this.loadData() || {}
const isFreshInstall: boolean = Object.keys(data).length === 0 const isFreshInstall: boolean = Object.keys(data).length === 0
this.settings = Object.assign({}, DEFAULT_SETTINGS, data); this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
if (requireApiVersion('1.2.0') && isFreshInstall) { if (isFreshInstall) {
this.settings = Object.assign(this.settings, DEFAULT_SETTING_FOR_1_2_0_UP) this.settings = Object.assign(this.settings, DEFAULT_SETTING_FOR_1_2_0_UP)
} }
} }

View File

@ -18,7 +18,6 @@ const getHas = (gTotal?: NM, gBkmrk?: NM, gStar?: NM, gIcon?: NM, sTot?: NM, sBk
grouping: { grouping: {
total: gTotal ||0, total: gTotal ||0,
byBookmarks: gBkmrk ||0, byBookmarks: gBkmrk ||0,
byStarred: gStar ||0,
byIcon: gIcon ||0 byIcon: gIcon ||0
}, },
sorting: { sorting: {

View File

@ -1,7 +1,7 @@
import { import {
CachedMetadata, CachedMetadata,
MetadataCache, MetadataCache,
Pos, Pos, TAbstractFile,
TFile, TFile,
TFolder, TFolder,
Vault Vault
@ -36,9 +36,8 @@ import {
CompoundDotRomanNumberNormalizerFn CompoundDotRomanNumberNormalizerFn
} from "../../custom-sort/sorting-spec-processor"; } from "../../custom-sort/sorting-spec-processor";
import { import {
findStarredFile_pathParam, BookmarksPluginInterface
Starred_PluginInstance } from "../../utils/BookmarksCorePluginSignature";
} from "../../utils/StarredPluginSignature";
import { import {
ObsidianIconFolder_PluginInstance, ObsidianIconFolder_PluginInstance,
ObsidianIconFolderPlugin_Data ObsidianIconFolderPlugin_Data
@ -937,25 +936,25 @@ describe('determineSortingGroup', () => {
} as FolderItemForSorting); } as FolderItemForSorting);
}) })
}) })
describe('CustomSortGroupType.StarredOnly', () => { describe('CustomSortGroupType.BookmarkedOnly', () => {
it('should not match not starred file', () => { it('should not match not bookmarked file', () => {
// given // given
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
const sortSpec: CustomSortSpec = { const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'], targetFoldersPaths: ['/'],
groups: [{ groups: [{
type: CustomSortGroupType.StarredOnly type: CustomSortGroupType.BookmarkedOnly
}] }]
} }
const starredPluginInstance: Partial<Starred_PluginInstance> = { const bookmarksPluginInstance: Partial<BookmarksPluginInterface> = {
findStarredFile: jest.fn( function(filePath: findStarredFile_pathParam): TFile | null { determineBookmarkOrder: jest.fn( function(path: string): number | undefined {
return null return undefined
}) })
} }
// when // when
const result = determineSortingGroup(file, sortSpec, { const result = determineSortingGroup(file, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance bookmarksPluginInstance: bookmarksPluginInstance as BookmarksPluginInterface
} as ProcessingContext) } as ProcessingContext)
// then // then
@ -968,30 +967,32 @@ describe('determineSortingGroup', () => {
mtime: MOCK_TIMESTAMP + 333, mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md' path: 'Some parent folder/References.md'
}); });
expect(starredPluginInstance.findStarredFile).toHaveBeenCalledTimes(1) expect(bookmarksPluginInstance.determineBookmarkOrder).toHaveBeenCalledTimes(1)
}) })
it('should match starred file', () => { it('should match bookmarked file', () => {
// given // given
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
const sortSpec: CustomSortSpec = { const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'], targetFoldersPaths: ['/'],
groups: [{ groups: [{
type: CustomSortGroupType.StarredOnly type: CustomSortGroupType.BookmarkedOnly
}] }]
} }
const starredPluginInstance: Partial<Starred_PluginInstance> = { const BOOKMARK_ORDER = 123
findStarredFile: jest.fn( function(filePath: findStarredFile_pathParam): TFile | null { const bookmarksPluginInstance: Partial<BookmarksPluginInterface> = {
return filePath.path === 'Some parent folder/References.md' ? file : null determineBookmarkOrder: jest.fn( function(path: string): number | undefined {
return path === 'Some parent folder/References.md' ? BOOKMARK_ORDER : undefined
}) })
} }
// when // when
const result = determineSortingGroup(file, sortSpec, { const result = determineSortingGroup(file, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance bookmarksPluginInstance: bookmarksPluginInstance as BookmarksPluginInterface
} as ProcessingContext) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
bookmarkedIdx: BOOKMARK_ORDER,
groupIdx: 0, groupIdx: 0,
isFolder: false, isFolder: false,
sortString: "References", sortString: "References",
@ -1000,131 +1001,7 @@ describe('determineSortingGroup', () => {
mtime: MOCK_TIMESTAMP + 333, mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md' path: 'Some parent folder/References.md'
}); });
expect(starredPluginInstance.findStarredFile).toHaveBeenCalledTimes(1) expect(bookmarksPluginInstance.determineBookmarkOrder).toHaveBeenCalledTimes(1)
})
it('should not match empty folder', () => {
// given
const folder: TFolder = mockTFolder('TestEmptyFolder');
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.StarredOnly
}]
}
const starredPluginInstance: Partial<Starred_PluginInstance> = {
findStarredFile: jest.fn( function(filePath: findStarredFile_pathParam): TFile | null {
return filePath.path === 'Some parent folder/References.md' ? {} as TFile : null
})
}
// when
const result = determineSortingGroup(folder, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance
} as ProcessingContext)
// then
expect(result).toEqual({
groupIdx: 1, // The lastIdx+1, group not determined
isFolder: true,
sortString: "TestEmptyFolder",
sortStringWithExt: "TestEmptyFolder",
ctime: 0,
mtime: 0,
path: 'TestEmptyFolder',
folder: {
children: [],
isRoot: expect.any(Function),
name: "TestEmptyFolder",
parent: {},
path: "TestEmptyFolder",
vault: {}
}
});
expect(starredPluginInstance.findStarredFile).not.toHaveBeenCalled()
})
it('should not match folder w/o starred items', () => {
// given
const folder: TFolder = mockTFolderWithChildren('TestEmptyFolder');
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.StarredOnly
}]
}
const starredPluginInstance: Partial<Starred_PluginInstance> = {
findStarredFile: jest.fn( function(filePath: findStarredFile_pathParam): TFile | null {
return filePath.path === 'Some parent folder/References.md' ? {} as TFile : null
})
}
// when
const result = determineSortingGroup(folder, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance
} as ProcessingContext)
// then
expect(result).toEqual({
groupIdx: 1, // The lastIdx+1, group not determined
isFolder: true,
sortString: "TestEmptyFolder",
sortStringWithExt: "TestEmptyFolder",
ctime: 0,
mtime: 0,
path: 'TestEmptyFolder',
folder: {
children: expect.any(Array),
isRoot: expect.any(Function),
name: "TestEmptyFolder",
parent: {},
path: "TestEmptyFolder",
vault: {}
}
});
expect(starredPluginInstance.findStarredFile).toHaveBeenCalledTimes(folder.children.filter(f => (f as any).isRoot === undefined).length)
})
it('should match folder with one starred item', () => {
// given
const folder: TFolder = mockTFolderWithChildren('TestEmptyFolder');
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.StarredOnly
}]
}
const starredPluginInstance: Partial<Starred_PluginInstance> = {
findStarredFile: jest.fn(function (filePath: findStarredFile_pathParam): TFile | null {
return filePath.path === 'Some parent folder/Child file 2 created as newest, not modified at all.md' ? {} as TFile : null
})
}
// when
const result = determineSortingGroup(folder, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance
} as ProcessingContext)
// then
expect(result).toEqual({
groupIdx: 0,
isFolder: true,
sortString: "TestEmptyFolder",
sortStringWithExt: "TestEmptyFolder",
ctime: 0,
mtime: 0,
path: 'TestEmptyFolder',
folder: {
children: expect.any(Array),
isRoot: expect.any(Function),
name: "TestEmptyFolder",
parent: {},
path: "TestEmptyFolder",
vault: {}
}
});
// assume optimized checking of starred items -> first match ends the check
expect(starredPluginInstance.findStarredFile).toHaveBeenCalledTimes(2)
}) })
}) })
describe('CustomSortGroupType.HasIcon', () => { describe('CustomSortGroupType.HasIcon', () => {

View File

@ -42,9 +42,9 @@ with-metadata: Pages
> a-z by-metadata: > a-z by-metadata:
/: with-icon: /: with-icon:
with-icon: RiClock24 with-icon: RiClock24
starred: bookmarked:
/:files starred: /:files bookmarked:
/folders starred: /folders bookmarked:
:::: folder of bookmarks :::: folder of bookmarks
< by-bookmarks-order < by-bookmarks-order
@ -107,9 +107,9 @@ target-folder: tricky folder 2
> a-z by-metadata: > a-z by-metadata:
/:files with-icon: /:files with-icon:
/folders:files with-icon: RiClock24 /folders:files with-icon: RiClock24
/folders:files starred: /folders:files bookmarked:
/:files starred: /:files bookmarked:
/folders starred: /folders bookmarked:
target-folder: folder of bookmarks target-folder: folder of bookmarks
order-asc: by-bookmarks-order order-asc: by-bookmarks-order
@ -204,12 +204,12 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = {
type: CustomSortGroupType.HasIcon, type: CustomSortGroupType.HasIcon,
iconName: 'RiClock24' iconName: 'RiClock24'
}, { }, {
type: CustomSortGroupType.StarredOnly type: CustomSortGroupType.BookmarkedOnly
}, { }, {
type: CustomSortGroupType.StarredOnly, type: CustomSortGroupType.BookmarkedOnly,
filesOnly: true filesOnly: true
}, { }, {
type: CustomSortGroupType.StarredOnly, type: CustomSortGroupType.BookmarkedOnly,
foldersOnly: true foldersOnly: true
}, { }, {
type: CustomSortGroupType.Outsiders type: CustomSortGroupType.Outsiders

View File

@ -50,7 +50,6 @@ declare module 'obsidian' {
} }
export interface FileExplorerView extends View { export interface FileExplorerView extends View {
createFolderDom(folder: TFolder): FileExplorerFolder;
getSortedFolderItems(sortedFolder: TFolder): any[]; getSortedFolderItems(sortedFolder: TFolder): any[];
requestSort(): void; requestSort(): void;

View File

@ -1,43 +0,0 @@
import {App, InstalledPlugin, PluginInstance, TAbstractFile, TFile, TFolder} from "obsidian";
export const StarredPlugin_findStarredFile_methodName = 'findStarredFile'
export interface findStarredFile_pathParam {
path: string
}
export interface Starred_PluginInstance extends PluginInstance {
[StarredPlugin_findStarredFile_methodName]: (filePath: findStarredFile_pathParam) => TFile | null
}
export const StarredCorePluginId: string = 'starred'
export const getStarredPlugin = (app: App): Starred_PluginInstance | undefined => {
const starredPlugin: InstalledPlugin | undefined = app?.internalPlugins?.getPluginById(StarredCorePluginId)
if (starredPlugin && starredPlugin.enabled && starredPlugin.instance) {
const starredPluginInstance: Starred_PluginInstance = starredPlugin.instance as Starred_PluginInstance
// defensive programming, in case Obsidian changes its internal APIs
if (typeof starredPluginInstance?.[StarredPlugin_findStarredFile_methodName] === 'function') {
return starredPluginInstance
}
}
}
const isFolder = (entry: TAbstractFile) => {
// The plain obvious 'entry instanceof TFolder' doesn't work inside Jest unit tests, hence a workaround below
return !!((entry as any).isRoot);
}
export const determineStarredStatusOfFolder = (folder: TFolder, starredPluginInstance: Starred_PluginInstance): boolean => {
return folder.children.some((folderItem) => {
return !isFolder(folderItem) && starredPluginInstance[StarredPlugin_findStarredFile_methodName]({path: folderItem.path})
})
}
export const determineStarredStatusOf = (entry: TFile | TFolder, aFile: boolean, starredPluginInstance: Starred_PluginInstance) => {
if (aFile) {
return !!starredPluginInstance[StarredPlugin_findStarredFile_methodName]({path: entry.path})
} else { // aFolder
return determineStarredStatusOfFolder(entry as TFolder, starredPluginInstance)
}
}

View File

@ -48,5 +48,6 @@
"2.1.12": "0.16.2", "2.1.12": "0.16.2",
"2.1.13": "0.16.2", "2.1.13": "0.16.2",
"2.1.14": "0.16.2", "2.1.14": "0.16.2",
"2.1.15": "0.16.2" "2.1.15": "0.16.2",
"3.0.0": "1.7.2"
} }

1250
yarn.lock

File diff suppressed because it is too large Load Diff