diff --git a/CHANGELOG.md b/CHANGELOG.md index c071c3c5..1eee7f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - [#553](https://github.com/estruyf/vscode-front-matter/issues/553): New `frontMatter.config.dynamicFilePath` setting which allows you to dynamically update the settings from a custom JS file - [#563](https://github.com/estruyf/vscode-front-matter/issues/563): New `fieldCollection` to inherit/reuse fields in multiple content-types - [#653](https://github.com/estruyf/vscode-front-matter/issues/653): Retrieve the Astro Content Collections to allow content type generation +- [#675](https://github.com/estruyf/vscode-front-matter/issues/675): Pinning content to the top of the content dashboard ### 🎨 Enhancements diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index e6c60826..9551f07b 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -24,6 +24,8 @@ "common.error.message": "Sorry, something went wrong.", "common.openOnWebsite": "Open on website", "common.settings": "Settings", + "common.pin": "Pin", + "common.unpin": "Unpin", "settings.contentTypes": "Content types", "settings.contentFolders": "Content folders", @@ -68,6 +70,7 @@ "dashboard.contents.overview.noMarkdown": "No Markdown to show", "dashboard.contents.overview.noFolders": "Make sure you registered a content folder in your project to let Front Matter find the contents.", + "dashboard.contents.overview.pinned": "Pinned", "dashboard.contents.status.draft": "Draft", "dashboard.contents.status.published": "Published", diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index 60de2f8c..342b747f 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -204,6 +204,7 @@ export class Dashboard { command: DashboardCommand; requestId?: string; payload?: unknown; + error?: unknown; }) { if (Dashboard.isDisposed) { return; diff --git a/src/constants/LocalStore.ts b/src/constants/LocalStore.ts index e3ecc86c..04c9443f 100644 --- a/src/constants/LocalStore.ts +++ b/src/constants/LocalStore.ts @@ -4,5 +4,6 @@ export const LocalStore = { databaseFolder: 'database', templatesFolder: 'templates', mediaDatabaseFile: 'mediaDb.json', - taxonomyDatabaseFile: 'taxonomyDb.json' + taxonomyDatabaseFile: 'taxonomyDb.json', + pinnedItemsDatabaseFile: 'pinnedItemsDb.json' }; diff --git a/src/dashboardWebView/DashboardMessage.ts b/src/dashboardWebView/DashboardMessage.ts index 8610fc01..f10a32fe 100644 --- a/src/dashboardWebView/DashboardMessage.ts +++ b/src/dashboardWebView/DashboardMessage.ts @@ -26,6 +26,9 @@ export enum DashboardMessage { searchPages = 'searchPages', openFile = 'openFile', deleteFile = 'deleteFile', + getPinnedItems = 'getPinnedItems', + pinItem = 'pinItem', + unpinItem = 'unpinItem', // Media Dashboard getMedia = 'getMedia', diff --git a/src/dashboardWebView/components/Contents/ContentActions.tsx b/src/dashboardWebView/components/Contents/ContentActions.tsx index 0cc59cd0..6e3e81d4 100644 --- a/src/dashboardWebView/components/Contents/ContentActions.tsx +++ b/src/dashboardWebView/components/Contents/ContentActions.tsx @@ -1,4 +1,4 @@ -import { Messenger } from '@estruyf/vscode/dist/client'; +import { Messenger, messageHandler } from '@estruyf/vscode/dist/client'; import { Menu } from '@headlessui/react'; import { EyeIcon, GlobeIcon, TerminalIcon, TrashIcon } from '@heroicons/react/outline'; import * as React from 'react'; @@ -11,13 +11,16 @@ import { useState } from 'react'; import useThemeColors from '../../hooks/useThemeColors'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../../localization'; -import { useRecoilValue } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { SettingsSelector } from '../../state'; import { GeneralCommands } from '../../../constants'; +import { PinIcon } from '../Icons/PinIcon'; +import { PinnedItemsAtom } from '../../state/atom/PinnedItems'; export interface IContentActionsProps { title: string; path: string; + relPath: string; scripts: CustomScript[] | undefined; listView?: boolean; onOpen: () => void; @@ -26,10 +29,12 @@ export interface IContentActionsProps { export const ContentActions: React.FunctionComponent = ({ title, path, + relPath, scripts, onOpen, listView }: React.PropsWithChildren) => { + const [pinnedItems, setPinnedItems] = useRecoilState(PinnedItemsAtom); const [showDeletionAlert, setShowDeletionAlert] = React.useState(false); const { getColors } = useThemeColors(); const settings = useRecoilValue(SettingsSelector); @@ -68,6 +73,20 @@ export const ContentActions: React.FunctionComponent = ({ } }, [settings?.websiteUrl, path]); + const pinItem = React.useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + messageHandler.request(DashboardMessage.pinItem, path).then((result) => { + setPinnedItems(result || []); + }) + }, [path]); + + const unpinItem = React.useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + messageHandler.request(DashboardMessage.unpinItem, path).then((result) => { + setPinnedItems(result || []); + }) + }, [path]); + const runCustomScript = React.useCallback( (e: React.MouseEvent, script: CustomScript) => { e.stopPropagation(); @@ -76,6 +95,10 @@ export const ContentActions: React.FunctionComponent = ({ [path] ); + const isPinned = React.useMemo(() => { + return pinnedItems.includes(relPath); + }, [pinnedItems, relPath]); + const customScriptActions = React.useMemo(() => { return (scripts || []) .filter( @@ -148,6 +171,15 @@ export const ContentActions: React.FunctionComponent = ({ widthClass="w-44" marginTopClass={listView ? '' : ''} > + + {' '} + {isPinned ? l10n.t(LocalizationKey.commonUnpin) : l10n.t(LocalizationKey.commonPin)} + + } + onClick={(_, e) => isPinned ? unpinItem(e) : pinItem(e)} + /> diff --git a/src/dashboardWebView/components/Contents/Item.tsx b/src/dashboardWebView/components/Contents/Item.tsx index de63ae77..e7870289 100644 --- a/src/dashboardWebView/components/Contents/Item.tsx +++ b/src/dashboardWebView/components/Contents/Item.tsx @@ -15,76 +15,32 @@ import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../../localization'; import { useNavigate } from 'react-router-dom'; import { routePaths } from '../..'; +import useCard from '../../hooks/useCard'; export interface IItemProps extends Page { } const PREVIEW_IMAGE_FIELD = 'fmPreviewImage'; export const Item: React.FunctionComponent = ({ - fmFilePath, - fmDateFormat, - date, - title, - description, - type, ...pageData }: React.PropsWithChildren) => { const view = useRecoilValue(ViewSelector); const settings = useRecoilValue(SettingsSelector); const draftField = useMemo(() => settings?.draftField, [settings]); const cardFields = useMemo(() => settings?.dashboardState?.contents?.cardFields, [settings?.dashboardState?.contents?.cardFields]); + const { escapedTitle, escapedDescription } = useCard(pageData, settings?.dashboardState?.contents?.cardFields); const navigate = useNavigate(); const { titleHtml, descriptionHtml, dateHtml, statusHtml, tagsHtml, imageHtml, footerHtml } = useExtensibility({ - fmFilePath, - date, - title, - description, - type, + fmFilePath: pageData.fmFileData, + date: pageData.date, + title: pageData.title, + description: pageData.description, + type: pageData.type, pageData }); - const escapedTitle = useMemo(() => { - let value = title; - - if (cardFields?.title) { - if (cardFields.title === "description") { - value = description; - } else if (cardFields?.title !== "title") { - value = pageData[cardFields?.title] || title; - } - } else if (cardFields?.title === null) { - return null; - } - - if (value && typeof value !== 'string') { - return l10n.t(LocalizationKey.dashboardContentsItemInvalidTitle); - } - - return value; - }, [title, description, cardFields?.title, pageData]); - - const escapedDescription = useMemo(() => { - let value = description; - - if (cardFields?.description) { - if (cardFields.description === "title") { - value = title; - } else if (cardFields?.description !== "description") { - value = pageData[cardFields?.description] || description; - } - } else if (cardFields?.description === null) { - return null; - } - - if (value && typeof value !== 'string') { - return l10n.t(LocalizationKey.dashboardContentsItemInvalidDescription); - } - - return value; - }, [description, title, cardFields?.description, pageData]); - const openFile = () => { - Messenger.send(DashboardMessage.openFile, fmFilePath); + Messenger.send(DashboardMessage.openFile, pageData.fmFilePath); }; const tags: string[] | undefined = useMemo(() => { @@ -161,14 +117,15 @@ export const Item: React.FunctionComponent = ({ dateHtml ? (
) : ( - cardFields?.date && + cardFields?.date && ) }
@@ -245,14 +202,15 @@ export const Item: React.FunctionComponent = ({
- +
{draftField && draftField.name && } diff --git a/src/dashboardWebView/components/Contents/Overview.tsx b/src/dashboardWebView/components/Contents/Overview.tsx index 330f1bb4..c5e7b9bd 100644 --- a/src/dashboardWebView/components/Contents/Overview.tsx +++ b/src/dashboardWebView/components/Contents/Overview.tsx @@ -2,19 +2,25 @@ import { Disclosure } from '@headlessui/react'; import { ChevronRightIcon } from '@heroicons/react/solid'; import * as React from 'react'; import { useCallback, useMemo } from 'react'; -import { useRecoilValue } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { groupBy } from '../../../helpers/GroupBy'; import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon'; import { GroupOption } from '../../constants/GroupOption'; import { Page } from '../../models/Page'; import { Settings } from '../../models/Settings'; -import { GroupingSelector, PageAtom } from '../../state'; +import { GroupingSelector, PageAtom, ViewSelector } from '../../state'; import { Item } from './Item'; import { List } from './List'; import usePagination from '../../hooks/usePagination'; import useThemeColors from '../../hooks/useThemeColors'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../../localization'; +import { PinnedItemsAtom } from '../../state/atom/PinnedItems'; +import { messageHandler } from '@estruyf/vscode/dist/client'; +import { DashboardMessage } from '../../DashboardMessage'; +import { PinIcon } from '../Icons/PinIcon'; +import { PinnedItem } from './PinnedItem'; +import { DashboardViewType } from '../../models'; export interface IOverviewProps { pages: Page[]; @@ -25,10 +31,13 @@ export const Overview: React.FunctionComponent = ({ pages, settings }: React.PropsWithChildren) => { + const [isReady, setIsReady] = React.useState(false); + const [pinnedItems, setPinnedItems] = useRecoilState(PinnedItemsAtom); const grouping = useRecoilValue(GroupingSelector); const page = useRecoilValue(PageAtom); const { pageSetNr } = usePagination(settings?.dashboardState.contents.pagination); const { getColors } = useThemeColors(); + const view = useRecoilValue(ViewSelector); const pagedPages = useMemo(() => { if (pageSetNr) { @@ -36,7 +45,15 @@ export const Overview: React.FunctionComponent = ({ } return pages; - }, [pages, page, pageSetNr]); + }, [pages, page, pageSetNr, pinnedItems, grouping]); + + const pinnedPages = useMemo(() => { + if (grouping === GroupOption.none) { + return pages.filter((page) => pinnedItems.includes(page.fmRelFileWsPath)); + } + + return []; + }, [pages, pinnedItems, grouping]); const groupName = useCallback( (groupId, groupedPages) => { @@ -49,6 +66,20 @@ export const Overview: React.FunctionComponent = ({ [grouping] ); + React.useEffect(() => { + messageHandler.request(DashboardMessage.getPinnedItems).then((items) => { + setIsReady(true); + setPinnedItems(items || []); + }).catch(() => { + setIsReady(true); + setPinnedItems([]); + }); + }, []); + + if (!isReady) { + return null; + } + if (!pages || !pages.length) { return (
@@ -108,10 +139,34 @@ export const Overview: React.FunctionComponent = ({ } return ( - - {pagedPages.map((page, idx) => ( - - ))} - +
+ { + pinnedPages.length > 0 && ( +
+

+ + {l10n.t(LocalizationKey.dashboardContentsOverviewPinned)} +

+ + {pinnedPages.map((page, idx) => ( + view === DashboardViewType.List ? ( + + ) : ( + + ) + ))} + +
+ ) + } + +
0 ? "pt-8" : ""}> + + {pagedPages.map((page, idx) => ( + + ))} + +
+
); }; diff --git a/src/dashboardWebView/components/Contents/PinnedItem.tsx b/src/dashboardWebView/components/Contents/PinnedItem.tsx new file mode 100644 index 00000000..9998c17c --- /dev/null +++ b/src/dashboardWebView/components/Contents/PinnedItem.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { Page } from '../../models'; +import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon'; +import { ContentActions } from './ContentActions'; +import { DashboardMessage } from '../../DashboardMessage'; +import { messageHandler } from '@estruyf/vscode/dist/client'; +import useCard from '../../hooks/useCard'; +import { SettingsSelector } from '../../state'; +import { useRecoilValue } from 'recoil'; + +export interface IPinnedItemProps extends Page { } + +export const PinnedItem: React.FunctionComponent = ({ + ...pageData +}: React.PropsWithChildren) => { + const settings = useRecoilValue(SettingsSelector); + const { escapedTitle } = useCard(pageData, settings?.dashboardState?.contents?.cardFields); + + const openFile = React.useCallback(() => { + messageHandler.send(DashboardMessage.openFile, pageData.fmFilePath); + }, [pageData.fmFilePath]); + + return ( +
  • + + + +
  • + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Icons/PinIcon.tsx b/src/dashboardWebView/components/Icons/PinIcon.tsx new file mode 100644 index 00000000..438bbeb4 --- /dev/null +++ b/src/dashboardWebView/components/Icons/PinIcon.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; + +export interface IPinIconProps { + className?: string; +} + +export const PinIcon: React.FunctionComponent = ({ + className +}: React.PropsWithChildren) => { + return ( + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/hooks/index.ts b/src/dashboardWebView/hooks/index.ts index 9682e0fc..df5f5509 100644 --- a/src/dashboardWebView/hooks/index.ts +++ b/src/dashboardWebView/hooks/index.ts @@ -1,3 +1,4 @@ +export * from './useCard'; export * from './useExtensibility'; export * from './useMedia'; export * from './useMessages'; diff --git a/src/dashboardWebView/hooks/useCard.tsx b/src/dashboardWebView/hooks/useCard.tsx new file mode 100644 index 00000000..40aa979b --- /dev/null +++ b/src/dashboardWebView/hooks/useCard.tsx @@ -0,0 +1,55 @@ +import { useMemo } from 'react'; +import { CardFields, Page } from '../models'; +import * as l10n from '@vscode/l10n'; +import { LocalizationKey } from '../../localization'; + +export default function useCard( + pageData: Page, + cardFields: CardFields | undefined, +) { + + const escapedTitle = useMemo(() => { + let value = pageData.title; + + if (cardFields?.title) { + if (cardFields.title === "description") { + value = pageData.description; + } else if (cardFields?.title !== "title") { + value = pageData[cardFields?.title] || pageData.title; + } + } else if (cardFields?.title === null) { + return null; + } + + if (value && typeof value !== 'string') { + return l10n.t(LocalizationKey.dashboardContentsItemInvalidTitle); + } + + return value; + }, [pageData.title, pageData.description, cardFields?.title, pageData]); + + const escapedDescription = useMemo(() => { + let value = pageData.description; + + if (cardFields?.description) { + if (cardFields.description === "title") { + value = pageData.title; + } else if (cardFields?.description !== "description") { + value = pageData[cardFields?.description] || pageData.description; + } + } else if (cardFields?.description === null) { + return null; + } + + if (value && typeof value !== 'string') { + return l10n.t(LocalizationKey.dashboardContentsItemInvalidDescription); + } + + return value; + }, [pageData.description, pageData.title, cardFields?.description, pageData]); + + return { + escapedTitle, + escapedDescription + }; +} \ No newline at end of file diff --git a/src/dashboardWebView/models/Page.ts b/src/dashboardWebView/models/Page.ts index 2f3aefd4..2d20f3e0 100644 --- a/src/dashboardWebView/models/Page.ts +++ b/src/dashboardWebView/models/Page.ts @@ -6,6 +6,7 @@ export interface Page { // Front matter fields fmFolder: string; fmFilePath: string; + fmRelFileWsPath: string; fmRelFilePath: string; fmFileName: string; fmModified: number; diff --git a/src/dashboardWebView/state/atom/PinnedItems.ts b/src/dashboardWebView/state/atom/PinnedItems.ts new file mode 100644 index 00000000..00d4cca3 --- /dev/null +++ b/src/dashboardWebView/state/atom/PinnedItems.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const PinnedItemsAtom = atom({ + key: 'PinnedItemsAtom', + default: [] +}); diff --git a/src/helpers/FilesHelper.ts b/src/helpers/FilesHelper.ts index ca20652c..840a32de 100644 --- a/src/helpers/FilesHelper.ts +++ b/src/helpers/FilesHelper.ts @@ -1,6 +1,6 @@ import { Notifications } from './Notifications'; import { Uri } from 'vscode'; -import { Folders } from '../commands/Folders'; +import { Folders, WORKSPACE_PLACEHOLDER } from '../commands/Folders'; import { isValidFile } from './isValidFile'; import { parseWinPath } from './parseWinPath'; import { join } from 'path'; @@ -41,4 +41,15 @@ export class FilesHelper { let absPath = join(parseWinPath(wsFolder?.fsPath || ''), filePath); return parseWinPath(absPath); } + + /** + * Absolute path to rel path + * @param filePath + * @returns + */ + public static absToRelPath(filePath: string): string { + const wsFolder = Folders.getWorkspaceFolder(); + const relPath = filePath.replace(wsFolder?.fsPath || '', WORKSPACE_PLACEHOLDER); + return parseWinPath(relPath); + } } diff --git a/src/listeners/dashboard/BaseListener.ts b/src/listeners/dashboard/BaseListener.ts index 11166dc1..6e3fd26f 100644 --- a/src/listeners/dashboard/BaseListener.ts +++ b/src/listeners/dashboard/BaseListener.ts @@ -28,4 +28,12 @@ export abstract class BaseListener { payload }); } + + public static sendError(command: DashboardCommand, requestId: string, error: any) { + Dashboard.postWebviewMessage({ + command, + requestId, + error + }); + } } diff --git a/src/listeners/dashboard/DashboardListener.ts b/src/listeners/dashboard/DashboardListener.ts index 0a7c72d9..0ba85e54 100644 --- a/src/listeners/dashboard/DashboardListener.ts +++ b/src/listeners/dashboard/DashboardListener.ts @@ -4,6 +4,7 @@ import { DashboardCommand } from '../../dashboardWebView/DashboardCommand'; import { DashboardMessage } from '../../dashboardWebView/DashboardMessage'; import { Extension, Notifications } from '../../helpers'; import { PostMessageData } from '../../models'; +import { PinnedItems } from '../../services'; import { BaseListener } from './BaseListener'; export class DashboardListener extends BaseListener { @@ -29,6 +30,79 @@ export class DashboardListener extends BaseListener { case DashboardMessage.showWarning: Notifications.warning(msg.payload); break; + case DashboardMessage.pinItem: + DashboardListener.pinItem(msg); + break; + case DashboardMessage.unpinItem: + DashboardListener.unpinItem(msg); + break; + case DashboardMessage.getPinnedItems: + DashboardListener.getPinnedItems(msg); + break; } } + + /** + * Get the pinned items + * @param msg + */ + private static async getPinnedItems({ command, requestId }: PostMessageData) { + if (!requestId || !command) { + return; + } + + const allPinned = (await PinnedItems.get()) || []; + this.sendRequest(command as any, requestId, allPinned); + } + + /** + * Pin an item to the dashboard + * @param payload + */ + private static async pinItem({ command, requestId, payload }: PostMessageData) { + if (!requestId || !command || !payload) { + return; + } + + const path = payload; + if (!path) { + this.sendError(command as any, requestId, 'No path provided.'); + return; + } + + const allPinned = await PinnedItems.pin(path); + + if (!allPinned) { + this.sendError(command as any, requestId, 'Could not pin item.'); + return; + } + + this.sendRequest(command as any, requestId, allPinned); + } + + /** + * Unpin an item from the dashboard + * @param param0 + * @returns + */ + private static async unpinItem({ command, requestId, payload }: PostMessageData) { + if (!requestId || !command || !payload) { + return; + } + + const path = payload; + if (!path) { + this.sendError(command as any, requestId, 'No path provided.'); + return; + } + + const updatedPinned = await PinnedItems.remove(path); + + if (!updatedPinned) { + this.sendError(command as any, requestId, 'Could not unpin item.'); + return; + } + + this.sendRequest(command as any, requestId, updatedPinned); + } } diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index 5f789344..f888ddc0 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -99,6 +99,14 @@ export enum LocalizationKey { * Settings */ commonSettings = 'common.settings', + /** + * Pin + */ + commonPin = 'common.pin', + /** + * Unpin + */ + commonUnpin = 'common.unpin', /** * Content types */ @@ -227,6 +235,10 @@ export enum LocalizationKey { * Make sure you registered a content folder in your project to let Front Matter find the contents. */ dashboardContentsOverviewNoFolders = 'dashboard.contents.overview.noFolders', + /** + * Pinned + */ + dashboardContentsOverviewPinned = 'dashboard.contents.overview.pinned', /** * Draft */ diff --git a/src/services/PagesParser.ts b/src/services/PagesParser.ts index 88caa5e3..329554f3 100644 --- a/src/services/PagesParser.ts +++ b/src/services/PagesParser.ts @@ -18,6 +18,7 @@ import { ContentType, DateHelper, Extension, + FilesHelper, isValidFile, Logger, Notifications, @@ -207,6 +208,7 @@ export class PagesParser { // FrontMatter properties fmFolder: folderTitle, fmFilePath: filePath, + fmRelFileWsPath: FilesHelper.absToRelPath(filePath), fmRelFilePath: parseWinPath(filePath).replace(wsFolder?.fsPath || '', ''), fmFileName: fileName, fmDraft: ContentType.getDraftStatus(article?.data), diff --git a/src/services/PinnedItems.ts b/src/services/PinnedItems.ts new file mode 100644 index 00000000..88018b1f --- /dev/null +++ b/src/services/PinnedItems.ts @@ -0,0 +1,111 @@ +import { Config, JsonDB } from 'node-json-db'; +import { Folders } from '../commands'; +import { join } from 'path'; +import { LocalStore } from '../constants'; +import { FilesHelper, parseWinPath } from '../helpers'; + +const PINNED_DB = '/pinned'; + +export class PinnedItems { + /** + * Retrieve all the pinned items + * @returns + */ + public static async get() { + const db = PinnedItems.getPinnedDb(); + if (!db) { + return undefined; + } + + let allPinned: string[] = []; + if (await db.exists(PINNED_DB)) { + allPinned = (await db.getObject(PINNED_DB)) as string[]; + } + + return allPinned; + } + + /** + * Pin an item + * @param path + */ + public static async pin(path: string) { + if (!path) { + return; + } + + const relPath = FilesHelper.absToRelPath(path); + if (!relPath) { + return; + } + + const db = PinnedItems.getPinnedDb(); + if (!db) { + return; + } + + let allPinned: string[] = []; + if (await db.exists(PINNED_DB)) { + allPinned = (await db.getObject(PINNED_DB)) as string[]; + } + + allPinned.push(relPath); + allPinned = [...new Set(allPinned)]; + + await db.push(PINNED_DB, allPinned, true); + + return allPinned; + } + + /** + * Remove a pinned item + * @param path + * @returns + */ + public static async remove(path: string) { + if (!path) { + return; + } + + const relPath = FilesHelper.absToRelPath(path); + if (!relPath) { + return; + } + + const db = PinnedItems.getPinnedDb(); + if (!db) { + return; + } + + let allPinned: string[] = []; + if (await db.exists(PINNED_DB)) { + allPinned = (await db.getObject(PINNED_DB)) as string[]; + } + + allPinned = allPinned.filter((p) => p !== relPath); + + await db.push(PINNED_DB, allPinned, true); + + return allPinned; + } + + /** + * Retrieve the pinned database + * @returns + */ + private static getPinnedDb() { + const wsFolder = Folders.getWorkspaceFolder(); + if (!wsFolder) { + return; + } + + const dbFolder = join( + parseWinPath(wsFolder?.fsPath || ''), + LocalStore.rootFolder, + LocalStore.databaseFolder + ); + const dbPath = join(dbFolder, LocalStore.pinnedItemsDatabaseFile); + + return new JsonDB(new Config(dbPath, true, false, '/')); + } +} diff --git a/src/services/index.ts b/src/services/index.ts index b5a3a446..2e13bb82 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,5 +1,6 @@ export * from './Credentials'; export * from './ModeSwitch'; export * from './PagesParser'; +export * from './PinnedItems'; export * from './SponsorAI'; export * from './Terminal';