From daf657721ca5ace89ee683c2e60325ccfb59251e Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Wed, 4 Oct 2023 13:19:19 +0200 Subject: [PATCH] #74 - Integration with Bookmarks core plugin and support for indirect drag & drop arrangement - advanced performance optimizations: bookmarks cache finetuning, switching to Obsidian sort code if the implicit bookmark-integration sorting spec is in effect for a folder and there are no bookmarked items in the folder - unit tests for some of newly introduced functions --- src/custom-sort/custom-sort-utils.spec.ts | 187 ++++++++++++++++++++++ src/custom-sort/custom-sort-utils.ts | 80 +++++++++ src/main.ts | 60 ++++--- src/utils/BookmarksCorePluginSignature.ts | 44 ++++- src/utils/utils.spec.ts | 5 +- 5 files changed, 347 insertions(+), 29 deletions(-) create mode 100644 src/custom-sort/custom-sort-utils.spec.ts create mode 100644 src/custom-sort/custom-sort-utils.ts diff --git a/src/custom-sort/custom-sort-utils.spec.ts b/src/custom-sort/custom-sort-utils.spec.ts new file mode 100644 index 0000000..4c55453 --- /dev/null +++ b/src/custom-sort/custom-sort-utils.spec.ts @@ -0,0 +1,187 @@ +import { + CustomSortGroupType, + CustomSortOrder, + CustomSortSpec +} from "./custom-sort-types"; +import { + collectSortingAndGroupingTypes, + hasOnlyByBookmarkOrStandardObsidian, + HasSortingOrGrouping, + ImplicitSortspecForBookmarksIntegration +} from "./custom-sort-utils"; +import {SortingSpecProcessor, SortSpecsCollection} from "./sorting-spec-processor"; + +type NM = number + +const getHas = (gTotal?: NM, gBkmrk?: NM, gStar?: NM, gIcon?: NM, sTot?: NM, sBkmrk?: NM, sStd?: NM) => { + const has: HasSortingOrGrouping = { + grouping: { + total: gTotal ||0, + byBookmarks: gBkmrk ||0, + byStarred: gStar ||0, + byIcon: gIcon ||0 + }, + sorting: { + total: sTot ||0, + byBookmarks: sBkmrk ||0, + standardObsidian: sStd ||0 + } + } + return has +} + +describe('hasOnlyByBookmarkOrStandardObsidian and collectSortingAndGroupingTypes', () => { + it('should handle empty spec correctly', () => { + const spec: Partial|undefined|null = undefined + const expectedHas: HasSortingOrGrouping = getHas() + const has = collectSortingAndGroupingTypes(spec) + const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has) + expect(has).toEqual(expectedHas) + expect(hasOnly).toBeTruthy() + }) + it('should handle empty spec correctly (null variant)', () => { + const spec: Partial|undefined|null = null + const expectedHas: HasSortingOrGrouping = getHas() + const has = collectSortingAndGroupingTypes(spec) + const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has) + expect(hasOnly).toBeTruthy() + expect(has).toEqual(expectedHas) + }) + it('should handle spec with empty orders correctly', () => { + const spec: Partial|undefined = { + groups: [ + {type: CustomSortGroupType.Outsiders, filesOnly: true}, + {type: CustomSortGroupType.Outsiders} + ] + } + const expectedHas: HasSortingOrGrouping = getHas() + const has = collectSortingAndGroupingTypes(spec as CustomSortSpec) + const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has) + expect(hasOnly).toBeTruthy() + expect(has).toEqual(expectedHas) + }) + it('should detect not matching default order', () => { + const spec: Partial|undefined = { + defaultOrder: CustomSortOrder.default, + groups: [ + { + type: CustomSortGroupType.ExactName, + }, + { + type: CustomSortGroupType.Outsiders, + } + ] + } + const expectedHas: HasSortingOrGrouping = getHas(1, 0, 0, 0, 1, 0, 0) + const has = collectSortingAndGroupingTypes(spec as CustomSortSpec) + const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has) + expect(hasOnly).toBeFalsy() + expect(has).toEqual(expectedHas) + }) + it('should detect not matching default secondary order', () => { + const spec: Partial|undefined = { + defaultOrder: CustomSortOrder.byBookmarkOrder, + defaultSecondaryOrder: CustomSortOrder.default, + groups: [ + { + type: CustomSortGroupType.BookmarkedOnly, + }, + { + type: CustomSortGroupType.Outsiders, + } + ] + } + const expectedHas: HasSortingOrGrouping = getHas(1, 1, 0, 0, 2, 1, 0) + const has = collectSortingAndGroupingTypes(spec as CustomSortSpec) + const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has) + expect(hasOnly).toBeFalsy() + expect(has).toEqual(expectedHas) + }) + it('should detect not matching order in group', () => { + const spec: Partial|undefined = { + defaultOrder: CustomSortOrder.byBookmarkOrder, + defaultSecondaryOrder: CustomSortOrder.standardObsidian, + groups: [ + { + type: CustomSortGroupType.ExactName, + order: CustomSortOrder.byCreatedTimeReverse + }, + { + type: CustomSortGroupType.Outsiders, + } + ] + } + const expectedHas: HasSortingOrGrouping = getHas(1, 0, 0, 0, 3, 1, 1) + const has = collectSortingAndGroupingTypes(spec as CustomSortSpec) + const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has) + expect(hasOnly).toBeFalsy() + expect(has).toEqual(expectedHas) + }) + it('should detect not matching secondary order in group', () => { + const spec: Partial|undefined = { + defaultOrder: CustomSortOrder.byBookmarkOrder, + defaultSecondaryOrder: CustomSortOrder.standardObsidian, + groups: [ + { + type: CustomSortGroupType.ExactName, + order: CustomSortOrder.byBookmarkOrderReverse, + secondaryOrder: CustomSortOrder.standardObsidian + }, + { + type: CustomSortGroupType.Outsiders, + order: CustomSortOrder.byBookmarkOrder, + secondaryOrder: CustomSortOrder.alphabetical + } + ] + } + const expectedHas: HasSortingOrGrouping = getHas(1, 0, 0, 0, 6, 3, 2) + const has = collectSortingAndGroupingTypes(spec as CustomSortSpec) + const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has) + expect(hasOnly).toBeFalsy() + expect(has).toEqual(expectedHas) + }) + it('should detect matching orders at all levels', () => { + const spec: Partial|undefined = { + defaultOrder: CustomSortOrder.byBookmarkOrder, + defaultSecondaryOrder: CustomSortOrder.standardObsidian, + groups: [ + { + type: CustomSortGroupType.BookmarkedOnly, + order: CustomSortOrder.byBookmarkOrderReverse, + secondaryOrder: CustomSortOrder.standardObsidian + }, + { + type: CustomSortGroupType.Outsiders, + order: CustomSortOrder.byBookmarkOrder, + secondaryOrder: CustomSortOrder.byBookmarkOrderReverse + } + ] + } + const expectedHas: HasSortingOrGrouping = getHas(1, 1, 0, 0, 6, 4, 2) + const has = collectSortingAndGroupingTypes(spec as CustomSortSpec) + const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has) + expect(hasOnly).toBeTruthy() + expect(has).toEqual(expectedHas) + }) +}) + +describe('ImplicitSortspecForBookmarksIntegration', () => { + it('should correctly be recognized as only bookmark and obsidian standard', () => { + const processor: SortingSpecProcessor = new SortingSpecProcessor(); + const inputTxtArr: Array = ImplicitSortspecForBookmarksIntegration.replace(/\t/gi, '').split('\n') + const spec: SortSpecsCollection|null|undefined = processor.parseSortSpecFromText( + inputTxtArr, + 'mock-folder', + 'custom-name-note.md', + null, + true + ) + const expectedHas: HasSortingOrGrouping = getHas(1, 1, 0, 0, 2, 1, 1) + const has = collectSortingAndGroupingTypes(spec?.sortSpecByPath!['/']) + const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has) + expect(hasOnly).toBeTruthy() + expect(has).toEqual(expectedHas) + }) +}) + +// TODO - czy tamto sprawdzanie dla itemów w rootowym filderze hasBookmarkInFolder dobrze zadziala diff --git a/src/custom-sort/custom-sort-utils.ts b/src/custom-sort/custom-sort-utils.ts new file mode 100644 index 0000000..e3f0ed0 --- /dev/null +++ b/src/custom-sort/custom-sort-utils.ts @@ -0,0 +1,80 @@ +import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types"; + +// Put here to allow unit tests coverage of this specific implicit sorting spec +export const ImplicitSortspecForBookmarksIntegration: string = ` +target-folder: /* +bookmarked: + < by-bookmarks-order +sorting: standard +` + +export interface HasSortingTypes { + byBookmarks: number + standardObsidian: number + total: number +} + +export interface HasGroupingTypes { + byBookmarks: number + byStarred: number + byIcon: number + total: number +} + +export interface HasSortingOrGrouping { + sorting: HasSortingTypes + grouping: HasGroupingTypes +} + +export const checkByBookmark = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => { + groupType === CustomSortGroupType.BookmarkedOnly && has.grouping.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 ) => { + groupType === CustomSortGroupType.HasIcon && has.grouping.byIcon++; +} + +export const checkStandardObsidian = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => { + order === CustomSortOrder.standardObsidian && has.sorting.standardObsidian++; +} + +export const doCheck = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType) => { + checkByBookmark(has, order, groupType) + checkByStarred(has, order, groupType) + checkByIcon(has, order, groupType) + checkStandardObsidian(has, order, groupType) + + order !== undefined && has.sorting.total++ + groupType !== undefined && groupType !== CustomSortGroupType.Outsiders && has.grouping.total++; +} + +export const collectSortingAndGroupingTypes = (sortSpec?: CustomSortSpec|null): HasSortingOrGrouping => { + const has: HasSortingOrGrouping = { + grouping: { + byIcon: 0, byStarred: 0, byBookmarks: 0, total: 0 + }, + sorting: { + byBookmarks: 0, standardObsidian: 0, total: 0 + } + } + if (!sortSpec) return has + doCheck(has, sortSpec.defaultOrder) + doCheck(has, sortSpec.defaultSecondaryOrder) + if (sortSpec.groups) { + for (let group of sortSpec.groups) { + doCheck(has, group.order, group.type) + doCheck(has, group.secondaryOrder) + } + } + return has +} + +export const hasOnlyByBookmarkOrStandardObsidian = (has: HasSortingOrGrouping): boolean => { + return has.sorting.total === has.sorting.standardObsidian + has.sorting.byBookmarks && + has.grouping.total === has.grouping.byBookmarks +} diff --git a/src/main.ts b/src/main.ts index 188bf69..d237a73 100644 --- a/src/main.ts +++ b/src/main.ts @@ -30,7 +30,9 @@ import { SortingSpecProcessor, SortSpecsCollection } from './custom-sort/sorting-spec-processor'; -import {CustomSortSpec} from './custom-sort/custom-sort-types'; +import { + CustomSortSpec +} from './custom-sort/custom-sort-types'; import { addIcons, @@ -43,10 +45,17 @@ import { } from "./custom-sort/icons"; import {getStarredPlugin} from "./utils/StarredPluginSignature"; import { + BookmarksPluginInterface, getBookmarksPlugin } from "./utils/BookmarksCorePluginSignature"; import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature"; import {lastPathComponent} from "./utils/utils"; +import { + collectSortingAndGroupingTypes, + hasOnlyByBookmarkOrStandardObsidian, + HasSortingOrGrouping, + ImplicitSortspecForBookmarksIntegration +} from "./custom-sort/custom-sort-utils"; interface CustomSortPluginSettings { additionalSortspecFile: string @@ -70,7 +79,8 @@ const DEFAULT_SETTINGS: CustomSortPluginSettings = { bookmarksGroupToConsumeAsOrderingReference: 'sortspec' } -const DEFAULT_SETTING_FOR_FRESH_INSTALL_1_2_0: Partial = { +// On API 1.2.x+ enable the bookmarks integration by default +const DEFAULT_SETTING_FOR_1_2_0_UP: Partial = { automaticBookmarksIntegration: true, bookmarksContextMenus: true } @@ -80,13 +90,6 @@ const SORTINGSPEC_YAML_KEY: string = 'sorting-spec' const ERROR_NOTICE_TIMEOUT: number = 10000 -const ImplicitSortspecForBookmarksIntegration: string = ` -target-folder: /* -bookmarked: - < by-bookmarks-order -sorting: standard -` - // the monkey-around package doesn't export the below type type MonkeyAroundUninstaller = () => void @@ -360,7 +363,7 @@ export default class CustomSortPlugin extends Plugin { item.onClick(() => { const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference) if (bookmarksPlugin) { - const orderedChildren: Array = plugin.orderedFolderItemsForBookmarking(file.parent) + const orderedChildren: Array = plugin.orderedFolderItemsForBookmarking(file.parent, bookmarksPlugin) bookmarksPlugin.bookmarkSiblings(orderedChildren) bookmarksPlugin.saveDataAndUpdateBookmarkViews(true) } @@ -429,12 +432,12 @@ export default class CustomSortPlugin extends Plugin { return sortSpec } - createProcessingContextForSorting(): ProcessingContext { + createProcessingContextForSorting(has: HasSortingOrGrouping): ProcessingContext { const ctx: ProcessingContext = { _mCache: app.metadataCache, - starredPluginInstance: getStarredPlugin(), - bookmarksPluginInstance: getBookmarksPlugin(this.settings.bookmarksGroupToConsumeAsOrderingReference), - iconFolderPluginInstance: getIconFolderPlugin(), + starredPluginInstance: has.grouping.byStarred ? getStarredPlugin() : undefined, + bookmarksPluginInstance: has.grouping.byBookmarks || has.sorting.byBookmarks ? getBookmarksPlugin(this.settings.bookmarksGroupToConsumeAsOrderingReference, false, true) : undefined, + iconFolderPluginInstance: has.grouping.byIcon ? getIconFolderPlugin() : undefined, plugin: this } return ctx @@ -466,8 +469,18 @@ export default class CustomSortPlugin extends Plugin { const folder: TFolder = this.file let sortSpec: CustomSortSpec | null | undefined = plugin.determineSortSpecForFolder(folder.path, folder.name) + // Performance optimization + // Primary intention: when the implicit bookmarks integration is enabled, remain on std Obsidian, if no need to involve bookmarks + let has: HasSortingOrGrouping = collectSortingAndGroupingTypes(sortSpec) + if (hasOnlyByBookmarkOrStandardObsidian(has)) { + const bookmarksPlugin: BookmarksPluginInterface|undefined = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference, false, true) + if ( !bookmarksPlugin?.bookmarksIncludeItemsInFolder(folder.path)) { + sortSpec = null + } + } + if (sortSpec) { - return folderSort.call(this, sortSpec, plugin.createProcessingContextForSorting()); + return folderSort.call(this, sortSpec, plugin.createProcessingContextForSorting(has)); } else { return old.call(this, ...args); } @@ -481,14 +494,21 @@ export default class CustomSortPlugin extends Plugin { } } - orderedFolderItemsForBookmarking(folder: TFolder): Array { + orderedFolderItemsForBookmarking(folder: TFolder, bookmarksPlugin: BookmarksPluginInterface): Array { let sortSpec: CustomSortSpec | null | undefined = undefined if (!this.settings.suspended) { sortSpec = this.determineSortSpecForFolder(folder.path, folder.name) } let uiSortOrder: string = this.getFileExplorer()?.sortOrder || ObsidianStandardDefaultSortingName - return sortFolderItemsForBookmarking(folder.children, sortSpec, this.createProcessingContextForSorting(), uiSortOrder) + const has: HasSortingOrGrouping = collectSortingAndGroupingTypes(sortSpec) + + return sortFolderItemsForBookmarking( + folder.children, + sortSpec, + this.createProcessingContextForSorting(has), + uiSortOrder + ) } // Credits go to https://github.com/nothingislost/obsidian-bartender @@ -511,10 +531,8 @@ export default class CustomSortPlugin extends Plugin { const data: any = await this.loadData() || {} const isFreshInstall: boolean = Object.keys(data).length === 0 this.settings = Object.assign({}, DEFAULT_SETTINGS, data); - if (isFreshInstall) { - if (requireApiVersion('1.2.0')) { - this.settings = Object.assign(this.settings, DEFAULT_SETTING_FOR_FRESH_INSTALL_1_2_0) - } + if (requireApiVersion('1.2.0')) { + this.settings = Object.assign(this.settings, DEFAULT_SETTING_FOR_1_2_0_UP) } } diff --git a/src/utils/BookmarksCorePluginSignature.ts b/src/utils/BookmarksCorePluginSignature.ts index 1c76740..9bc0ccd 100644 --- a/src/utils/BookmarksCorePluginSignature.ts +++ b/src/utils/BookmarksCorePluginSignature.ts @@ -77,6 +77,9 @@ export interface BookmarksPluginInterface { updateSortingBookmarksAfterItemRenamed(renamedItem: TAbstractFile, oldPath: string): void updateSortingBookmarksAfterItemDeleted(deletedItem: TAbstractFile): void isBookmarkedForSorting(item: TAbstractFile): boolean + + // To support performance optimization + bookmarksIncludeItemsInFolder(folderPath: string): boolean } class BookmarksPluginWrapper implements BookmarksPluginInterface { @@ -91,15 +94,21 @@ class BookmarksPluginWrapper implements BookmarksPluginInterface { // undefined ==> item not found in bookmarks // > 0 ==> item found in bookmarks at returned position // Intentionally not returning 0 to allow simple syntax of processing the result - determineBookmarkOrder = (path: string): number | undefined => { + // + // Parameterless invocation enforces cache population, if empty + determineBookmarkOrder = (path?: string): number | undefined => { if (!bookmarksCache) { - bookmarksCache = getOrderedBookmarks(this.plugin!, this.groupNameForSorting) + [bookmarksCache, bookmarksFoldersCoverage] = getOrderedBookmarks(this.plugin!, this.groupNameForSorting) bookmarksCacheTimestamp = Date.now() } - const bookmarkedItemPosition: number | undefined = bookmarksCache?.[path]?.order + if (path && path.length > 0) { + const bookmarkedItemPosition: number | undefined = bookmarksCache?.[path]?.order - return (bookmarkedItemPosition !== undefined && bookmarkedItemPosition >= 0) ? (bookmarkedItemPosition + 1) : undefined + return (bookmarkedItemPosition !== undefined && bookmarkedItemPosition >= 0) ? (bookmarkedItemPosition + 1) : undefined + } else { + return undefined + } } bookmarkFolderItem = (item: TAbstractFile) => { @@ -166,11 +175,16 @@ class BookmarksPluginWrapper implements BookmarksPluginInterface { } return false } + + bookmarksIncludeItemsInFolder = (folderPath: string): boolean => { + console.error(`C: for ${folderPath} is ${bookmarksFoldersCoverage?.[folderPath]}`) + return !! bookmarksFoldersCoverage?.[folderPath] + } } export const BookmarksCorePluginId: string = 'bookmarks' -export const getBookmarksPlugin = (bookmarksGroupName?: string, forceFlushCache?: boolean): BookmarksPluginInterface | undefined => { +export const getBookmarksPlugin = (bookmarksGroupName?: string, forceFlushCache?: boolean, ensureCachePopulated?: boolean): BookmarksPluginInterface | undefined => { invalidateExpiredBookmarksCache(forceFlushCache) const installedBookmarksPlugin: InstalledPlugin | undefined = app?.internalPlugins?.getPluginById(BookmarksCorePluginId) if (installedBookmarksPlugin && installedBookmarksPlugin.enabled && installedBookmarksPlugin.instance) { @@ -179,6 +193,9 @@ export const getBookmarksPlugin = (bookmarksGroupName?: string, forceFlushCache? if (typeof bookmarksPluginInstance?.[BookmarksPlugin_getBookmarks_methodName] === 'function') { bookmarksPlugin.plugin = bookmarksPluginInstance bookmarksPlugin.groupNameForSorting = bookmarksGroupName + if (ensureCachePopulated && !bookmarksCache) { + bookmarksPlugin.determineBookmarkOrder() + } return bookmarksPlugin } } @@ -187,6 +204,9 @@ export const getBookmarksPlugin = (bookmarksGroupName?: string, forceFlushCache? // cache can outlive the wrapper instances let bookmarksCache: OrderedBookmarks | undefined = undefined let bookmarksCacheTimestamp: number | undefined = undefined +type FolderPath = string +type FoldersCoverage = { [key: FolderPath]: boolean } +let bookmarksFoldersCoverage: FoldersCoverage | undefined = undefined const bookmarksPlugin: BookmarksPluginWrapper = new BookmarksPluginWrapper() @@ -203,6 +223,7 @@ const invalidateExpiredBookmarksCache = (force?: boolean): void => { if (flush) { bookmarksCache = undefined bookmarksCacheTimestamp = undefined + bookmarksFoldersCoverage = undefined } } } @@ -226,8 +247,12 @@ const bookmarkLocationAndPathOverlap = (bookmarkParentGroupPath: string, fileOrF return fileOrFolderPath?.startsWith(bookmarkParentGroupPath) ? bookmarkParentGroupPath.length : 0 } -const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupName?: string): OrderedBookmarks | undefined => { +const ROOT_FOLDER_PATH = '/' + +const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupName?: string): [OrderedBookmarks, FoldersCoverage] | [undefined, undefined] => { + console.log(`getOrderedBookmarks()`) let bookmarksItems: Array | undefined = plugin?.[BookmarksPlugin_items_collectionName] + let bookmarksCoveredFolders: FoldersCoverage = {} if (bookmarksItems) { if (bookmarksGroupName) { // scanning only top level items because by desing the bookmarks group for sorting is a top level item @@ -249,6 +274,10 @@ const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupNam const path = isGroup ? pathOfGroup : (item as BookmarkWithPath).path const alreadyConsumed = orderedBookmarks[path] + const parentFolderPathOfBookmarkedItem = isGroup ? parentGroupsPath : extractParentFolderPath(path) + console.log(`Add ${path}`) + bookmarksCoveredFolders[parentFolderPathOfBookmarkedItem.length > 0 ? parentFolderPathOfBookmarkedItem : ROOT_FOLDER_PATH] = true + console.log(bookmarksCoveredFolders) // for groups (they represent folders from sorting perspective) bookmark them unconditionally // the idea of better match is not applicable if (alreadyConsumed && isGroup && alreadyConsumed.group) { @@ -277,9 +306,10 @@ const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupNam } traverseBookmarksCollection(bookmarksItems, consumeItem) - return orderedBookmarks + return [orderedBookmarks, bookmarksCoveredFolders] } } + return [undefined, undefined] } const createBookmarkFileEntry = (path: string): BookmarkedFile => { diff --git a/src/utils/utils.spec.ts b/src/utils/utils.spec.ts index 5189d85..21c71e4 100644 --- a/src/utils/utils.spec.ts +++ b/src/utils/utils.spec.ts @@ -1,4 +1,7 @@ -import {lastPathComponent, extractParentFolderPath} from "./utils"; +import { + lastPathComponent, + extractParentFolderPath +} from "./utils"; describe('lastPathComponent and extractParentFolderPath', () => { it.each([