mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-05-16 22:35:42 +02:00
#675 - Pin content
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user