Files
vscode-front-matter/src/dashboardWebView/components/TaxonomyView/TaxonomyManager.tsx
T
2024-01-15 22:23:10 +01:00

248 lines
9.1 KiB
TypeScript

import { Messenger } from '@estruyf/vscode/dist/client';
import { ExclamationTriangleIcon, PlusIcon, TagIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { TaxonomyData } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { Page } from '../../models';
import { SettingsSelector } from '../../state';
import { getTaxonomyField } from '../../../helpers/getTaxonomyField';
import { TaxonomyActions } from './TaxonomyActions';
import { TaxonomyLookup } from './TaxonomyLookup';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { FilterInput } from './FilterInput';
import { useDebounce } from '../../../hooks/useDebounce';
import { usePrevious } from '../../../panelWebView/hooks/usePrevious';
export interface ITaxonomyManagerProps {
data: TaxonomyData | undefined;
taxonomy: string | null;
pages: Page[];
onContentTagging: (value: string) => void;
}
export const TaxonomyManager: React.FunctionComponent<ITaxonomyManagerProps> = ({
data,
taxonomy,
pages,
onContentTagging
}: React.PropsWithChildren<ITaxonomyManagerProps>) => {
const settings = useRecoilValue(SettingsSelector);
const [filterValue, setFilterValue] = React.useState('');
const debounceFilterValue = useDebounce<string>(filterValue, 500);
const prevTaxonomy = usePrevious<string | null>(taxonomy);
const onCreate = () => {
Messenger.send(DashboardMessage.createTaxonomy, {
type: taxonomy
});
};
const items = useMemo(() => {
if (data && taxonomy) {
let crntItems: string[] = [];
if (taxonomy === 'tags' || taxonomy === 'categories') {
crntItems = data[taxonomy];
} else {
crntItems = data.customTaxonomy.find((c) => c.id === taxonomy)?.options || [];
}
// Only allow string values
crntItems = crntItems.filter((i) => typeof i === 'string');
// Alphabetically sort the items
crntItems = Object.assign([], crntItems).sort((a: string, b: string) => {
a = a || '';
b = b || '';
if (a.toLowerCase() < b.toLowerCase()) {
return -1;
}
if (a.toLowerCase() > b.toLowerCase()) {
return 1;
}
return 0;
});
if (debounceFilterValue) {
crntItems = crntItems.filter((i) => i.toLowerCase().includes(debounceFilterValue.toLowerCase()));
}
return crntItems.filter(i => i);
}
return [];
}, [data, taxonomy, debounceFilterValue]);
const unmappedItems = useMemo(() => {
let unmapped: string[] = [];
if (!pages || !settings?.contentTypes || !taxonomy) {
return unmapped;
}
for (const page of pages) {
let values: string[] = [];
if (taxonomy === 'tags') {
values = page.fmTags || [];
} else if (taxonomy === 'categories') {
values = page.fmCategories || [];
} else {
const contentType = settings.contentTypes.find((ct) => ct.name === page.fmContentType);
if (!contentType) {
return false;
}
let fieldName = getTaxonomyField(taxonomy, contentType);
if (fieldName && page[fieldName]) {
values = page[fieldName];
}
}
if (typeof values === 'string') {
values = [values];
}
for (const value of values) {
if (!items.includes(value)) {
unmapped.push(value);
}
}
}
const unmappedItems = [...new Set(unmapped)].filter(i => i);
if (debounceFilterValue) {
return unmappedItems.filter((i) => i.toLowerCase().includes(debounceFilterValue.toLowerCase()));
}
return unmappedItems;
}, [items, taxonomy, pages, settings?.contentTypes, debounceFilterValue]);
React.useEffect(() => {
if (prevTaxonomy !== taxonomy) {
setFilterValue('');
}
}, [prevTaxonomy, taxonomy])
if (!taxonomy) {
return null;
}
return (
<div className={`py-6 px-4 flex flex-col h-full overflow-hidden`}>
<div className={`flex w-full justify-between flex-shrink-0`}>
<div>
<h2 className={`text-lg first-letter:uppercase text-[var(--frontmatter-text)]`}>
{taxonomy}
</h2>
<p className={`mt-2 text-sm first-letter:uppercase text-[var(--frontmatter-secondary-text)]`}>
{l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerDescription, taxonomy)}
</p>
</div>
<div className='flex gap-4 justify-center items-center'>
<FilterInput
placeholder={l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerFilterInputPlaceholder, taxonomy)}
value={filterValue}
onChange={(value) => setFilterValue(value)}
onReset={() => setFilterValue('')} />
<div>
<button
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium focus:outline-none rounded text-[var(--vscode-button-foreground)] bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:opacity-50`}
title={l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerButtonCreate, taxonomy)}
onClick={onCreate}
>
<PlusIcon className={`mr-2 h-6 w-6`} />
<span className={`text-sm`}>{l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerButtonCreate, taxonomy)}</span>
</button>
</div>
</div>
</div>
<div className="mt-6 pb-6 -mr-4 pr-4 flex flex-col flex-grow overflow-auto">
<table className="min-w-full divide-y divide-[var(--frontmatter-border)]">
<thead>
<tr>
<th
scope="col"
className={`px-6 py-3 text-left text-xs font-medium uppercase text-[var(--frontmatter-secondary-text)]'`}
>
{l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerTableHeadingName)}
</th>
<th
scope="col"
className={`px-6 py-3 text-left text-xs font-medium uppercase text-[var(--frontmatter-secondary-text)]`}
>
{l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerTableHeadingCount)}
</th>
<th
scope="col"
className={`px-6 py-3 text-right text-xs font-medium uppercase text-[var(--frontmatter-secondary-text)]`}
>
{l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerTableHeadingAction)}
</th>
</tr>
</thead>
<tbody className={`divide-y divide-[var(--frontmatter-border)]`}>
{items && items.length > 0
? items.map((item, index) => (
<tr key={index}>
<td className={`px-6 py-4 whitespace-nowrap text-sm font-medium text-[var(--frontmatter-text)]`}>
<TagIcon className="inline-block h-4 w-4 mr-2" />
<span>{item}</span>
</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm font-medium text-[var(--frontmatter-text)]`}>
<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} onContentTagging={onContentTagging} />
</td>
</tr>
))
: !unmappedItems ||
(unmappedItems.length === 0 && (
<tr>
<td
className={`px-6 py-4 whitespace-nowrap text-sm font-medium text-[var(--frontmatter-text)]`}
colSpan={4}
>
{l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerTableRowEmpty, taxonomy)}
</td>
</tr>
))}
{unmappedItems &&
unmappedItems.length > 0 &&
unmappedItems.map((item, index) => (
<tr key={index}>
<td
className={`px-6 py-4 whitespace-nowrap text-sm font-medium text-[var(--frontmatter-text)]`}
title={l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerTableUnmappedTitle)}
>
<ExclamationTriangleIcon className="inline-block h-4 w-4 mr-2" />
<span>{item}</span>
</td>
<td className={`px-6 py-4 whitespace-nowrap text-sm font-medium text-[var(--frontmatter-text)]`}>
<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} onContentTagging={onContentTagging} unmapped />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};