mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
#675 - Pin content
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -204,6 +204,7 @@ export class Dashboard {
|
||||
command: DashboardCommand;
|
||||
requestId?: string;
|
||||
payload?: unknown;
|
||||
error?: unknown;
|
||||
}) {
|
||||
if (Dashboard.isDisposed) {
|
||||
return;
|
||||
|
||||
@@ -4,5 +4,6 @@ export const LocalStore = {
|
||||
databaseFolder: 'database',
|
||||
templatesFolder: 'templates',
|
||||
mediaDatabaseFile: 'mediaDb.json',
|
||||
taxonomyDatabaseFile: 'taxonomyDb.json'
|
||||
taxonomyDatabaseFile: 'taxonomyDb.json',
|
||||
pinnedItemsDatabaseFile: 'pinnedItemsDb.json'
|
||||
};
|
||||
|
||||
@@ -26,6 +26,9 @@ export enum DashboardMessage {
|
||||
searchPages = 'searchPages',
|
||||
openFile = 'openFile',
|
||||
deleteFile = 'deleteFile',
|
||||
getPinnedItems = 'getPinnedItems',
|
||||
pinItem = 'pinItem',
|
||||
unpinItem = 'unpinItem',
|
||||
|
||||
// Media Dashboard
|
||||
getMedia = 'getMedia',
|
||||
|
||||
@@ -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<IContentActionsProps> = ({
|
||||
title,
|
||||
path,
|
||||
relPath,
|
||||
scripts,
|
||||
onOpen,
|
||||
listView
|
||||
}: React.PropsWithChildren<IContentActionsProps>) => {
|
||||
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<IContentActionsProps> = ({
|
||||
}
|
||||
}, [settings?.websiteUrl, path]);
|
||||
|
||||
const pinItem = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
messageHandler.request<string[]>(DashboardMessage.pinItem, path).then((result) => {
|
||||
setPinnedItems(result || []);
|
||||
})
|
||||
}, [path]);
|
||||
|
||||
const unpinItem = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
messageHandler.request<string[]>(DashboardMessage.unpinItem, path).then((result) => {
|
||||
setPinnedItems(result || []);
|
||||
})
|
||||
}, [path]);
|
||||
|
||||
const runCustomScript = React.useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>, script: CustomScript) => {
|
||||
e.stopPropagation();
|
||||
@@ -76,6 +95,10 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
[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<IContentActionsProps> = ({
|
||||
widthClass="w-44"
|
||||
marginTopClass={listView ? '' : ''}
|
||||
>
|
||||
<MenuItem
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<PinIcon className={`mr-2 h-5 w-5 flex-shrink-0 ${isPinned ? "" : "-rotate-90"}`} aria-hidden={true} />{' '}
|
||||
<span>{isPinned ? l10n.t(LocalizationKey.commonUnpin) : l10n.t(LocalizationKey.commonPin)}</span>
|
||||
</div>
|
||||
}
|
||||
onClick={(_, e) => isPinned ? unpinItem(e) : pinItem(e)}
|
||||
/>
|
||||
<MenuItem
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -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<IItemProps> = ({
|
||||
fmFilePath,
|
||||
fmDateFormat,
|
||||
date,
|
||||
title,
|
||||
description,
|
||||
type,
|
||||
...pageData
|
||||
}: React.PropsWithChildren<IItemProps>) => {
|
||||
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<IItemProps> = ({
|
||||
dateHtml ? (
|
||||
<div className='mr-4' dangerouslySetInnerHTML={{ __html: dateHtml }} />
|
||||
) : (
|
||||
cardFields?.date && <DateField className={`mr-4`} value={date} format={fmDateFormat} />
|
||||
cardFields?.date && <DateField className={`mr-4`} value={pageData.date} format={pageData.fmDateFormat} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<ContentActions
|
||||
title={title}
|
||||
path={fmFilePath}
|
||||
title={pageData.title}
|
||||
path={pageData.fmFilePath}
|
||||
relPath={pageData.fmRelFileWsPath}
|
||||
scripts={settings?.scripts}
|
||||
onOpen={openFile}
|
||||
/>
|
||||
@@ -245,14 +202,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
|
||||
<ContentActions
|
||||
title={escapedTitle || ""}
|
||||
path={fmFilePath}
|
||||
path={pageData.fmFilePath}
|
||||
relPath={pageData.fmRelFileWsPath}
|
||||
scripts={settings?.scripts}
|
||||
onOpen={openFile}
|
||||
listView
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<DateField value={date} />
|
||||
<DateField value={pageData.date} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
{draftField && draftField.name && <Status draft={pageData[draftField.name]} />}
|
||||
|
||||
@@ -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<IOverviewProps> = ({
|
||||
pages,
|
||||
settings
|
||||
}: React.PropsWithChildren<IOverviewProps>) => {
|
||||
const [isReady, setIsReady] = React.useState<boolean>(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<IOverviewProps> = ({
|
||||
}
|
||||
|
||||
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<IOverviewProps> = ({
|
||||
[grouping]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
messageHandler.request<string[]>(DashboardMessage.getPinnedItems).then((items) => {
|
||||
setIsReady(true);
|
||||
setPinnedItems(items || []);
|
||||
}).catch(() => {
|
||||
setIsReady(true);
|
||||
setPinnedItems([]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pages || !pages.length) {
|
||||
return (
|
||||
<div className={`flex items-center justify-center h-full`}>
|
||||
@@ -108,10 +139,34 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<List>
|
||||
{pagedPages.map((page, idx) => (
|
||||
<Item key={`${page.slug}-${idx}`} {...page} />
|
||||
))}
|
||||
</List>
|
||||
<div className='divide-y divide-[var(--frontmatter-border)]'>
|
||||
{
|
||||
pinnedPages.length > 0 && (
|
||||
<div className='mb-8'>
|
||||
<h1 className='text-xl flex space-x-2 items-center mb-4'>
|
||||
<PinIcon className={`-rotate-45`} />
|
||||
<span>{l10n.t(LocalizationKey.dashboardContentsOverviewPinned)}</span>
|
||||
</h1>
|
||||
<List>
|
||||
{pinnedPages.map((page, idx) => (
|
||||
view === DashboardViewType.List ? (
|
||||
<Item key={`${page.slug}-${idx}`} {...page} />
|
||||
) : (
|
||||
<PinnedItem key={`${page.slug}-${idx}`} {...page} />
|
||||
)
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className={pinnedItems.length > 0 ? "pt-8" : ""}>
|
||||
<List>
|
||||
{pagedPages.map((page, idx) => (
|
||||
<Item key={`${page.slug}-${idx}`} {...page} />
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
57
src/dashboardWebView/components/Contents/PinnedItem.tsx
Normal file
57
src/dashboardWebView/components/Contents/PinnedItem.tsx
Normal file
@@ -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<IPinnedItemProps> = ({
|
||||
...pageData
|
||||
}: React.PropsWithChildren<IPinnedItemProps>) => {
|
||||
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 (
|
||||
<li className='group flex w-full border border-[var(--frontmatter-border)] rounded bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-sideBarTitle-foreground)]'>
|
||||
<button onClick={openFile} className='relative h-full w-1/3'>
|
||||
{
|
||||
pageData["fmPreviewImage"] ? (
|
||||
<img
|
||||
src={`${pageData["fmPreviewImage"]}`}
|
||||
alt={pageData.title || ""}
|
||||
className="absolute inset-0 h-full w-full object-left-top object-cover group-hover:brightness-75"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`h-full flex items-center justify-center bg-[var(--vscode-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)] border-r border-[var(--frontmatter-border)]`}
|
||||
>
|
||||
<MarkdownIcon className={`h-8 text-[var(--vscode-sideBarTitle-foreground)] opacity-80`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
|
||||
<button onClick={openFile} className='relative w-2/3 p-4 text-left flex items-start'>
|
||||
<p className='font-bold'>{escapedTitle}</p>
|
||||
|
||||
<ContentActions
|
||||
title={pageData.title}
|
||||
path={pageData.fmFilePath}
|
||||
relPath={pageData.fmRelFileWsPath}
|
||||
scripts={settings?.scripts}
|
||||
onOpen={openFile}
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
13
src/dashboardWebView/components/Icons/PinIcon.tsx
Normal file
13
src/dashboardWebView/components/Icons/PinIcon.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IPinIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const PinIcon: React.FunctionComponent<IPinIconProps> = ({
|
||||
className
|
||||
}: React.PropsWithChildren<IPinIconProps>) => {
|
||||
return (
|
||||
<svg className={className || ""} stroke="currentColor" fill="currentColor" stroke-width=".5" viewBox="0 0 16 16" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M14 5v7h-.278c-.406 0-.778-.086-1.117-.258A2.528 2.528 0 0 1 11.73 11H8.87a3.463 3.463 0 0 1-.546.828 3.685 3.685 0 0 1-.735.633c-.27.177-.565.31-.882.398a3.875 3.875 0 0 1-.985.141h-.5V9H2l-1-.5L2 8h3.222V4h.5c.339 0 .664.047.977.14.312.094.607.227.883.4A3.404 3.404 0 0 1 8.87 6h2.859a2.56 2.56 0 0 1 .875-.734c.338-.172.71-.26 1.117-.266H14zm-.778 1.086a1.222 1.222 0 0 0-.32.156 1.491 1.491 0 0 0-.43.461L12.285 7H8.183l-.117-.336a2.457 2.457 0 0 0-.711-1.047C7.027 5.331 6.427 5.09 6 5v7c.427-.088 1.027-.33 1.355-.617.328-.287.565-.636.71-1.047L8.184 10h4.102l.18.297c.057.094.122.177.195.25.073.073.153.143.242.21.088.069.195.12.32.157V6.086z"></path></svg>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './useCard';
|
||||
export * from './useExtensibility';
|
||||
export * from './useMedia';
|
||||
export * from './useMessages';
|
||||
|
||||
55
src/dashboardWebView/hooks/useCard.tsx
Normal file
55
src/dashboardWebView/hooks/useCard.tsx
Normal file
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export interface Page {
|
||||
// Front matter fields
|
||||
fmFolder: string;
|
||||
fmFilePath: string;
|
||||
fmRelFileWsPath: string;
|
||||
fmRelFilePath: string;
|
||||
fmFileName: string;
|
||||
fmModified: number;
|
||||
|
||||
6
src/dashboardWebView/state/atom/PinnedItems.ts
Normal file
6
src/dashboardWebView/state/atom/PinnedItems.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const PinnedItemsAtom = atom<string[]>({
|
||||
key: 'PinnedItemsAtom',
|
||||
default: []
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,12 @@ export abstract class BaseListener {
|
||||
payload
|
||||
});
|
||||
}
|
||||
|
||||
public static sendError(command: DashboardCommand, requestId: string, error: any) {
|
||||
Dashboard.postWebviewMessage({
|
||||
command,
|
||||
requestId,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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),
|
||||
|
||||
111
src/services/PinnedItems.ts
Normal file
111
src/services/PinnedItems.ts
Normal file
@@ -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, '/'));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './Credentials';
|
||||
export * from './ModeSwitch';
|
||||
export * from './PagesParser';
|
||||
export * from './PinnedItems';
|
||||
export * from './SponsorAI';
|
||||
export * from './Terminal';
|
||||
|
||||
Reference in New Issue
Block a user