#675 - Pin content

This commit is contained in:
Elio Struyf
2023-09-23 14:11:00 +02:00
parent 2a763ab384
commit 2c8316288d
21 changed files with 475 additions and 69 deletions
@@ -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>
);
};