Content tagging

This commit is contained in:
Elio Struyf
2024-01-15 14:41:10 +01:00
parent e161fe23d5
commit 624afe9029
5 changed files with 183 additions and 78 deletions
@@ -1,7 +1,6 @@
import { Dialog, Transition } from '@headlessui/react';
import * as React from 'react';
import { Fragment } from 'react';
import useThemeColors from '../../hooks/useThemeColors';
export interface IInfoDialogProps {
icon?: JSX.Element;
@@ -17,7 +16,6 @@ export const InfoDialog: React.FunctionComponent<IInfoDialogProps> = ({
description,
children
}: React.PropsWithChildren<IInfoDialogProps>) => {
const { getColors } = useThemeColors();
return (
<Transition.Root show={true} as={Fragment}>
@@ -32,11 +30,7 @@ export const InfoDialog: React.FunctionComponent<IInfoDialogProps> = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className={`fixed inset-0 transition-opacity ${getColors(
`bg-vulcan-500 bg-opacity-75`,
`bg-[var(--vscode-editor-background)] opacity-75`
)
}`} />
<Dialog.Overlay className={`fixed inset-0 transition-opacity bg-[var(--vscode-editor-background)] opacity-75`} />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
@@ -53,31 +47,22 @@ export const InfoDialog: React.FunctionComponent<IInfoDialogProps> = ({
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className={`inline-block align-bottom rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6 border-2 ${getColors(
`bg-white dark:bg-vulcan-500 border-whisper-900`,
`bg-[var(--vscode-editor-background)] border-[var(--frontmatter-border)]`
)
}`}>
<div className={`inline-block align-bottom rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6 border-2 bg-[var(--vscode-editor-background)] border-[var(--frontmatter-border)]`}>
<div className="sm:flex sm:items-start">
{icon && (
<div className={`mt-3 sm:mr-4 mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full sm:mx-0 sm:h-10 sm:w-10 ${getColors(
`bg-gray-50 dark:bg-vulcan-400`,
`bg-[var(--vscode-sidebar-background)]'`
)
}`}>
<div className={`mt-3 sm:mr-4 mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full sm:mx-0 sm:h-10 sm:w-10 bg-[var(--vscode-sidebar-background)]`}>
{icon}
</div>
)}
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<Dialog.Title
as="h3"
className={`text-lg leading-6 font-medium ${getColors(`text-vulcan-300 dark:text-whisper-900`, `text-[var(--vscode-editor-foreground)]`)
}`}
className={`text-lg leading-6 font-medium text-[var(--vscode-editor-foreground)]`}
>
{title}
</Dialog.Title>
<div className="mt-2">
<p className={`text-sm ${getColors(`text-vulcan-500 dark:text-whisper-500`, `text-[var(--vscode-editor-foreground)]`)}`}>{description}</p>
<p className={`text-sm text-[var(--vscode-editor-foreground)]`}>{description}</p>
</div>
</div>
</div>
@@ -18,12 +18,14 @@ export interface ITaxonomyActionsProps {
field: string | null;
value: string;
unmapped?: boolean;
onContentTagging: (value: string) => void;
}
export const TaxonomyActions: React.FunctionComponent<ITaxonomyActionsProps> = ({
field,
value,
unmapped
unmapped,
onContentTagging
}: React.PropsWithChildren<ITaxonomyActionsProps>) => {
const onEdit = useCallback(() => {
Messenger.send(DashboardMessage.editTaxonomy, {
@@ -60,59 +62,65 @@ export const TaxonomyActions: React.FunctionComponent<ITaxonomyActionsProps> = (
});
}, [field, value]);
const onTagging = useCallback(() => {
onContentTagging(value);
}, [value]);
return (
<div className={`space-x-2`}>
{unmapped && (
<>
<div className={`space-x-2`}>
{unmapped && (
<LinkButton
title={l10n.t(LocalizationKey.dashboardTaxonomyViewButtonAddTitle, value)}
onClick={onAdd}>
<PlusIcon className={`w-4 h-4`} aria-hidden={true} />
<span className="sr-only">
{l10n.t(LocalizationKey.dashboardTaxonomyViewButtonAddTitle, value)}
</span>
</LinkButton>
)}
<LinkButton
title={l10n.t(LocalizationKey.dashboardTaxonomyViewButtonAddTitle, value)}
onClick={onAdd}>
<PlusIcon className={`w-4 h-4`} aria-hidden={true} />
title={`Tag content`}
onClick={onTagging}>
<SquaresPlusIcon className={`w-4 h-4`} aria-hidden={true} />
<span className="sr-only">{l10n.t(LocalizationKey.commonEdit)}</span>
</LinkButton>
<LinkButton
title={l10n.t(LocalizationKey.dashboardTaxonomyViewButtonEditTitle, value)}
onClick={onEdit}>
<PencilIcon className={`w-4 h-4`} aria-hidden={true} />
<span className="sr-only">{l10n.t(LocalizationKey.commonEdit)}</span>
</LinkButton>
<LinkButton
title={l10n.t(LocalizationKey.dashboardTaxonomyViewButtonMergeTitle, value)}
onClick={onMerge}>
<MergeIcon className={`w-4 h-4`} aria-hidden={true} />
<span className="sr-only">
{l10n.t(LocalizationKey.dashboardTaxonomyViewButtonAddTitle, value)}
{l10n.t(LocalizationKey.dashboardTaxonomyViewButtonMergeTitle, value)}
</span>
</LinkButton>
)}
<LinkButton
title={`Tag content`}
onClick={onEdit}>
<SquaresPlusIcon className={`w-4 h-4`} aria-hidden={true} />
<span className="sr-only">{l10n.t(LocalizationKey.commonEdit)}</span>
</LinkButton>
<LinkButton
title={l10n.t(LocalizationKey.dashboardTaxonomyViewButtonMoveTitle)}
onClick={onMove}>
<ArrowUpCircleIcon className={`w-4 h-4`} aria-hidden={true} />
<span className="sr-only">
{l10n.t(LocalizationKey.dashboardTaxonomyViewButtonMoveTitle)}
</span>
</LinkButton>
<LinkButton
title={l10n.t(LocalizationKey.dashboardTaxonomyViewButtonEditTitle, value)}
onClick={onEdit}>
<PencilIcon className={`w-4 h-4`} aria-hidden={true} />
<span className="sr-only">{l10n.t(LocalizationKey.commonEdit)}</span>
</LinkButton>
<LinkButton
title={l10n.t(LocalizationKey.dashboardTaxonomyViewButtonMergeTitle, value)}
onClick={onMerge}>
<MergeIcon className={`w-4 h-4`} aria-hidden={true} />
<span className="sr-only">
{l10n.t(LocalizationKey.dashboardTaxonomyViewButtonMergeTitle, value)}
</span>
</LinkButton>
<LinkButton
title={l10n.t(LocalizationKey.dashboardTaxonomyViewButtonMoveTitle)}
onClick={onMove}>
<ArrowUpCircleIcon className={`w-4 h-4`} aria-hidden={true} />
<span className="sr-only">
{l10n.t(LocalizationKey.dashboardTaxonomyViewButtonMoveTitle)}
</span>
</LinkButton>
<LinkButton
title={l10n.t(LocalizationKey.dashboardTaxonomyViewButtonDeleteTitle, value)}
onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden={true} />
<span className="sr-only">
{l10n.t(LocalizationKey.dashboardTaxonomyViewButtonDeleteTitle, value)}
</span>
</LinkButton>
</div>
<LinkButton
title={l10n.t(LocalizationKey.dashboardTaxonomyViewButtonDeleteTitle, value)}
onClick={onDelete}>
<TrashIcon className={`w-4 h-4`} aria-hidden={true} />
<span className="sr-only">
{l10n.t(LocalizationKey.dashboardTaxonomyViewButtonDeleteTitle, value)}
</span>
</LinkButton>
</div>
</>
);
};
@@ -21,12 +21,14 @@ export interface ITaxonomyManagerProps {
data: TaxonomyData | undefined;
taxonomy: string | null;
pages: Page[];
onContentTagging: (value: string) => void;
}
export const TaxonomyManager: React.FunctionComponent<ITaxonomyManagerProps> = ({
data,
taxonomy,
pages
pages,
onContentTagging
}: React.PropsWithChildren<ITaxonomyManagerProps>) => {
const settings = useRecoilValue(SettingsSelector);
const { getColors } = useThemeColors();
@@ -216,7 +218,7 @@ export const TaxonomyManager: React.FunctionComponent<ITaxonomyManagerProps> = (
<TaxonomyLookup taxonomy={taxonomy} value={item} pages={pages} />
</td>
<td className={`px-6 py-4 whitespace-nowrap text-right text-sm font-medium`}>
<TaxonomyActions field={taxonomy} value={item} />
<TaxonomyActions field={taxonomy} value={item} onContentTagging={onContentTagging} />
</td>
</tr>
))
@@ -247,7 +249,7 @@ export const TaxonomyManager: React.FunctionComponent<ITaxonomyManagerProps> = (
<TaxonomyLookup taxonomy={taxonomy} value={item} pages={pages} />
</td>
<td className={`px-6 py-4 whitespace-nowrap text-right text-sm font-medium`}>
<TaxonomyActions field={taxonomy} value={item} unmapped />
<TaxonomyActions field={taxonomy} value={item} onContentTagging={onContentTagging} unmapped />
</td>
</tr>
))}
@@ -0,0 +1,88 @@
import * as React from 'react';
import { Page } from '../../models';
import { useRecoilValue } from 'recoil';
import { SettingsSelector } from '../../state';
import { getTaxonomyField } from '../../../helpers/getTaxonomyField';
import { Sorting } from '../../../helpers/Sorting';
import { ArrowLeftIcon } from '@heroicons/react/24/outline';
export interface ITaxonomyTaggingProps {
taxonomy: string | null;
value: string;
pages: Page[];
onDismiss: () => void;
}
export const TaxonomyTagging: React.FunctionComponent<ITaxonomyTaggingProps> = ({
taxonomy,
value,
pages,
onDismiss
}: React.PropsWithChildren<ITaxonomyTaggingProps>) => {
const settings = useRecoilValue(SettingsSelector);
const untaggedPages = React.useMemo(() => {
let untagged: Page[] = [];
if (!pages || !settings?.contentTypes || !taxonomy) {
return untagged;
}
for (const page of pages) {
if (taxonomy === 'tags') {
if (!page.fmTags || page.fmTags.indexOf(value) === -1) {
untagged.push(page);
}
continue;
} else if (taxonomy === 'categories') {
if (!page.fmCategories || page.fmCategories.indexOf(value) === -1) {
untagged.push(page);
}
continue;
} else {
const contentType = settings.contentTypes.find((ct) => ct.name === page.fmContentType);
if (!contentType) {
continue;
}
let fieldName = getTaxonomyField(taxonomy, contentType);
if (fieldName && (!page[fieldName] || page[fieldName].indexOf(value) === -1)) {
untagged.push(page);
}
}
}
untagged = untagged.sort(Sorting.number('fmPublished')).reverse();
return untagged;
}, [pages, taxonomy, value]);
return (
<div className={`py-6 px-4 flex flex-col h-full overflow-hidden`}>
<div className={`flex w-full justify-between flex-shrink-0`}>
<h2 className='text-lg first-letter:uppercase flex items-center'>
<button onClick={onDismiss} title='Back' className='mr-2'>
<span className='sr-only'>Back</span>
<ArrowLeftIcon className='w-5 h-5 text-[var(--frontmatter-text)]' />
</button>
{taxonomy}: {value}
</h2>
</div>
<div className='mt-6 -mr-4 pr-4 flex flex-col flex-grow overflow-auto'>
<ul>
{untaggedPages.map((page) => (
<li key={page.fmFilePath}>
<input type='checkbox' />
<span>{page.title}</span>
</li>
))}
</ul>
</div>
</div>
);
};
@@ -14,6 +14,7 @@ import { SponsorMsg } from '../Layout/SponsorMsg';
import { TaxonomyManager } from './TaxonomyManager';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { TaxonomyTagging } from './TaxonomyTagging';
export interface ITaxonomyViewProps {
pages: Page[];
@@ -25,6 +26,7 @@ export const TaxonomyView: React.FunctionComponent<ITaxonomyViewProps> = ({
const settings = useRecoilValue(SettingsSelector);
const [taxonomySettings, setTaxonomySettings] = useState<TaxonomyData>();
const [selectedTaxonomy, setSelectedTaxonomy] = useState<string | null>(`tags`);
const [contentTagging, setContentTagging] = useState<string | null>(null);
const onImport = () => {
Messenger.send(DashboardMessage.importTaxonomy);
@@ -62,7 +64,10 @@ export const TaxonomyView: React.FunctionComponent<ITaxonomyViewProps> = ({
>
<NavigationItem
isSelected={selectedTaxonomy === 'tags'}
onClick={() => setSelectedTaxonomy(`tags`)}
onClick={() => {
setSelectedTaxonomy(`tags`);
setContentTagging(null);
}}
>
<ChevronRightIcon className="-ml-1 w-5 mr-2" />
<span>{l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyViewNavigationItemTags)}</span>
@@ -70,7 +75,10 @@ export const TaxonomyView: React.FunctionComponent<ITaxonomyViewProps> = ({
<NavigationItem
isSelected={selectedTaxonomy === 'categories'}
onClick={() => setSelectedTaxonomy(`categories`)}
onClick={() => {
setSelectedTaxonomy(`categories`);
setContentTagging(null);
}}
>
<ChevronRightIcon className="-ml-1 w-5 mr-2" />
<span>{l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyViewNavigationItemCategories)}</span>
@@ -81,7 +89,10 @@ export const TaxonomyView: React.FunctionComponent<ITaxonomyViewProps> = ({
<NavigationItem
key={`${taxonomy.id}-${index}`}
isSelected={selectedTaxonomy === taxonomy.id}
onClick={() => setSelectedTaxonomy(taxonomy.id)}
onClick={() => {
setSelectedTaxonomy(taxonomy.id);
setContentTagging(null);
}}
>
<ChevronRightIcon className="-ml-1 w-5 mr-2" />
<span className={`first-letter:uppercase`}>{taxonomy.id}</span>
@@ -90,10 +101,21 @@ export const TaxonomyView: React.FunctionComponent<ITaxonomyViewProps> = ({
</NavigationBar>
<div className={`w-10/12 h-full overflow-hidden`}>
<TaxonomyManager
data={taxonomySettings}
taxonomy={selectedTaxonomy}
pages={pages} />
{
contentTagging ? (
<TaxonomyTagging
value={contentTagging}
taxonomy={selectedTaxonomy}
pages={pages}
onDismiss={() => setContentTagging(null)} />
) : (
<TaxonomyManager
data={taxonomySettings}
taxonomy={selectedTaxonomy}
pages={pages}
onContentTagging={(value: string) => setContentTagging(value)} />
)
}
</div>
</div>