#291 - Taxonomy dashboard improvements + command

This commit is contained in:
Elio Struyf
2022-06-10 15:50:49 +02:00
parent 1a6acce77f
commit cf376cdda7
13 changed files with 252 additions and 116 deletions
+28
View File
@@ -2693,6 +2693,15 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"history": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
"dev": true,
"requires": {
"@babel/runtime": "^7.7.6"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -4802,6 +4811,25 @@
"quill": "^1.3.7"
}
},
"react-router": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
"dev": true,
"requires": {
"history": "^5.2.0"
}
},
"react-router-dom": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"dev": true,
"requires": {
"history": "^5.2.0",
"react-router": "6.3.0"
}
},
"react-sortable-hoc": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz",
+10
View File
@@ -1524,6 +1524,15 @@
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.dashboard.taxonomy",
"title": "Open taxonomy dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.markup.orderedlist",
"title": "Ordered list",
@@ -1973,6 +1982,7 @@
"react-dom": "17.0.1",
"react-dropzone": "^11.3.4",
"react-quill": "^2.0.0-beta.4",
"react-router-dom": "^6.3.0",
"react-sortable-hoc": "^2.0.0",
"react-toastify": "^8.1.0",
"recoil": "^0.4.1",
+46 -39
View File
@@ -7,13 +7,15 @@ import { useRecoilValue } from 'recoil';
import { DashboardViewSelector, ModeAtom } from '../state';
import { Contents } from './Contents/Contents';
import { Media } from './Media/Media';
import { NavigationType } from '../models';
import { DataView } from './DataView';
import { Snippets } from './SnippetsView/Snippets';
import { FeatureFlag } from '../../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../../constants';
import { Messenger } from '@estruyf/vscode/dist/client';
import { TaxonomyView } from './TaxonomyView';
import { Route, Routes, useNavigate } from 'react-router-dom';
import { routePaths } from '..';
import { useEffect, useMemo } from 'react';
import { UnknownView } from './UnknownView';
export interface IAppProps {
showWelcome: boolean;
@@ -23,10 +25,36 @@ export const App: React.FunctionComponent<IAppProps> = ({showWelcome}: React.Pro
const { loading, pages, settings } = useMessages();
const view = useRecoilValue(DashboardViewSelector);
const mode = useRecoilValue(ModeAtom);
const navigate = useNavigate();
useDarkMode();
const viewState: any = Messenger.getState() || {};
const isAllowed = (features: string[], flag: string) => {
if (!features ||( features.length > 0 && !features.includes(flag))) {
return false;
}
return true;
}
const allowDataView = useMemo(() => {
return isAllowed(mode?.features || [], FEATURE_FLAG.dashboard.data.view)
}, [mode?.features]);
const allowTaxonomyView = useMemo(() => {
return isAllowed(mode?.features || [], FEATURE_FLAG.dashboard.taxonomy.view)
}, [mode?.features]);
useEffect(() => {
if (view && routePaths[view]) {
navigate(routePaths[view]);
return;
}
navigate(routePaths[view]);
}, [view]);
if (!settings) {
return <Spinner />;
}
@@ -39,45 +67,24 @@ export const App: React.FunctionComponent<IAppProps> = ({showWelcome}: React.Pro
return <WelcomeScreen settings={settings} />;
}
if (view === NavigationType.Snippets) {
return (
<main className={`h-full w-full`}>
<Snippets />
</main>
);
}
if (view === NavigationType.Media) {
return (
<main className={`h-full w-full`}>
<Media />
</main>
);
}
if (view === NavigationType.Data) {
return (
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.data.view}>
<main className={`h-full w-full`}>
<DataView />
</main>
</FeatureFlag>
);
}
if (view === NavigationType.Taxonomy) {
return (
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.dashboard.taxonomy.view}>
<main className={`h-full w-full`}>
<TaxonomyView pages={pages} />
</main>
</FeatureFlag>
);
}
return (
<main className={`h-full w-full`}>
<Contents pages={pages} loading={loading} />
<Routes>
<Route path={routePaths.welcome} element={<WelcomeScreen settings={settings} />} />
<Route path={routePaths.contents} element={<Contents pages={pages} loading={loading} />} />
<Route path={routePaths.media} element={<Media />} />
<Route path={routePaths.snippets} element={<Snippets />} />
{
allowDataView && <Route path={routePaths.data} element={<DataView />} />
}
{
allowTaxonomyView && <Route path={routePaths.taxonomy} element={<TaxonomyView pages={pages} />} />
}
<Route path={`*`} element={<UnknownView />} />
</Routes>
</main>
);
};
@@ -4,6 +4,8 @@ import { useRecoilValue, useResetRecoilState } from 'recoil';
import { FolderSelector, TagSelector, CategorySelector, SortingAtom, FolderAtom, DEFAULT_FOLDER_STATE, TagAtom, CategoryAtom, DEFAULT_TAG_STATE, DEFAULT_CATEGORY_STATE } from '../../state';
import { DefaultValue } from 'recoil';
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';
export const guardRecoilDefaultValue = (
candidate: any
@@ -34,7 +36,7 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (props:
resetCategory();
};
React.useEffect(() => {
useEffect(() => {
if (folder !== DEFAULT_FOLDER_STATE || tag !== DEFAULT_TAG_STATE || category !== DEFAULT_CATEGORY_STATE) {
setShow(true);
} else {
@@ -10,7 +10,7 @@ import { Navigation } from '../Navigation';
import { Grouping } from '.';
import { ViewSwitch } from './ViewSwitch';
import { useRecoilState, useResetRecoilState } from 'recoil';
import { CategoryAtom, DashboardViewAtom, SortingAtom, TagAtom } from '../../state';
import { CategoryAtom, SortingAtom, TagAtom } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { ClearFilters } from './ClearFilters';
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
@@ -19,6 +19,9 @@ import { MediaHeaderBottom } from '../Media/MediaHeaderBottom';
import { Tabs } from './Tabs';
import { CustomScript } from '../../../models';
import { LightningBoltIcon, PlusIcon } from '@heroicons/react/outline';
import { useLocation, useNavigate } from 'react-router-dom';
import { routePaths } from '../..';
import { useEffect } from 'react';
export interface IHeaderProps {
header?: React.ReactNode;
@@ -34,8 +37,9 @@ export interface IHeaderProps {
export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPages, folders, settings }: React.PropsWithChildren<IHeaderProps>) => {
const [ crntTag, setCrntTag ] = useRecoilState(TagAtom);
const [ crntCategory, setCrntCategory ] = useRecoilState(CategoryAtom);
const [ view, setView ] = useRecoilState(DashboardViewAtom);
const resetSorting = useResetRecoilState(SortingAtom)
const resetSorting = useResetRecoilState(SortingAtom);
const location = useLocation();
const navigate = useNavigate();
const createContent = () => {
Messenger.send(DashboardMessage.createContent);
@@ -50,7 +54,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
};
const updateView = (view: NavigationType) => {
setView(view);
navigate(routePaths[view]);
resetSorting();
}
@@ -68,6 +72,27 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
onClick: () => runBulkScript(s)
}));
useEffect(() => {
if (location.search) {
const searchParams = new URLSearchParams(location.search);
const taxonomy = searchParams.get("taxonomy");
const value = searchParams.get("value");
if (taxonomy && value) {
if (taxonomy === "tags") {
setCrntTag(value);
} else if (taxonomy === "categories") {
setCrntCategory(value);
}
}
return;
}
setCrntTag("");
setCrntCategory("");
}, [location.search]);
return (
<div className={`w-full sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
@@ -76,7 +101,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
</div>
{
view === NavigationType.Contents && (
location.pathname === routePaths.contents && (
<>
<div className={`px-4 mt-3 mb-2 flex items-center justify-between`}>
<Searchbox />
@@ -141,7 +166,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({header, totalPage
}
{
view === NavigationType.Media && (
location.pathname === routePaths.media && (
<>
<MediaHeaderTop />
@@ -1,7 +1,6 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { useLocation } from 'react-router-dom';
import { NavigationType } from '../../models';
import { DashboardViewAtom } from '../../state';
export interface ITabProps {
navigationType: NavigationType;
@@ -9,11 +8,11 @@ export interface ITabProps {
}
export const Tab: React.FunctionComponent<ITabProps> = ({navigationType, onNavigate, children}: React.PropsWithChildren<ITabProps>) => {
const view = useRecoilValue(DashboardViewAtom);
const location = useLocation();
return (
<button
className={`h-full flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === navigationType ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`}
className={`h-full flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${location.pathname === `/${navigationType}` ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`}
type="button"
role="tab"
aria-controls="profile"
@@ -1,9 +1,11 @@
import * as React from 'react';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { Page } from '../../models';
import { SettingsSelector } from '../../state';
import { useRecoilValue } from 'recoil';
import { getTaxonomyField } from '../../../helpers/getTaxonomyField';
import { useNavigate } from 'react-router-dom';
import { routePaths } from '../..';
export interface ITaxonomyLookupProps {
taxonomy: string | null;
@@ -13,13 +15,20 @@ export interface ITaxonomyLookupProps {
export const TaxonomyLookup: React.FunctionComponent<ITaxonomyLookupProps> = ({ taxonomy, value, pages }: React.PropsWithChildren<ITaxonomyLookupProps>) => {
const settings = useRecoilValue(SettingsSelector);
const navigate = useNavigate();
const total = useMemo(() => {
const total: number | undefined = useMemo(() => {
if (!taxonomy || !value || !pages || !settings?.contentTypes) {
return <>-</>;
return undefined;
}
return pages.filter(page => {
if (taxonomy === "tags") {
return (page.fmTags || []).includes(value);
} else if (taxonomy === "categories") {
return (page.fmCategories || []).includes(value);
}
const contentType = settings.contentTypes.find(ct => ct.name === page.fmContentType);
if (!contentType) {
@@ -31,10 +40,28 @@ export const TaxonomyLookup: React.FunctionComponent<ITaxonomyLookupProps> = ({
return fieldName && page[fieldName] ? page[fieldName].includes(value) : false;
}).length;
}, [taxonomy, value, pages, settings?.contentTypes]);
const onNavigate = useCallback(() => {
if (total) {
navigate(`${routePaths.contents}?taxonomy=${taxonomy}&value=${value}`);
}
}, [total, navigate]);
if (taxonomy === "tags" || taxonomy === "categories") {
return (
<button
className={total ? `text-teal-900 hover:text-teal-600` : ``}
title={total ? `Show contents with ${value} in ${taxonomy}` : ``}
onClick={onNavigate}>
{total || `-`}
</button>
);
}
return (
<span className={``}>
{total}
<span>
{total || `-`}
</span>
);
};
@@ -63,21 +63,29 @@ export const TaxonomyManager: React.FunctionComponent<ITaxonomyManagerProps> = (
}
for (const page of pages) {
const contentType = settings.contentTypes.find(ct => ct.name === page.fmContentType);
let values: string[] = [];
if (!contentType) {
return false;
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];
}
}
let fieldName = getTaxonomyField(taxonomy, contentType);
if (fieldName && page[fieldName]) {
const values = page[fieldName];
for (const value of values) {
if (!items.includes(value)) {
unmapped.push(value);
}
for (const value of values) {
if (!items.includes(value)) {
unmapped.push(value);
}
}
}
@@ -108,7 +116,7 @@ export const TaxonomyManager: React.FunctionComponent<ITaxonomyManagerProps> = (
<thead>
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Name</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Times used</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Count</th>
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Action</th>
</tr>
</thead>
@@ -0,0 +1,18 @@
import { StopIcon } from '@heroicons/react/outline';
import * as React from 'react';
export interface IUnknownViewProps {}
export const UnknownView: React.FunctionComponent<IUnknownViewProps> = (props: React.PropsWithChildren<IUnknownViewProps>) => {
return (
<div className={`w-full h-full flex items-center justify-center`}>
<div className='flex flex-col items-center text-gray-500 dark:text-whisper-900'>
<StopIcon className='w-32 h-32' />
<p className='text-3xl mt-2'>View does not exist</p>
<p className='text-xl mt-4'>
You seem to have ended up on a view that doesn't exist. Please re-open the dashboard.
</p>
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from './UnknownView';
+42 -41
View File
@@ -2,7 +2,6 @@ import { useState, useEffect, useCallback } from 'react';
import { SortOption } from '../constants/SortOption';
import { Tab } from '../constants/Tab';
import { Page } from '../models/Page';
import Fuse from 'fuse.js';
import { useRecoilState, useRecoilValue } from 'recoil';
import { CategorySelector, FolderSelector, SearchSelector, SettingsSelector, SortingAtom, TabInfoAtom, TabSelector, TagSelector } from '../state';
import { SortOrder, SortType } from '../../models';
@@ -46,45 +45,6 @@ export default function usePages(pages: Page[]) {
});
}
const draftTypes = Object.assign({}, tabInfo);
draftTypes[Tab.All] = pagesToShow.length;
// Filter by draft status
if (draftField && draftField.type === 'choice') {
const draftChoices = settings?.draftField?.choices;
for (const choice of (draftChoices || [])) {
if (choice) {
draftTypes[choice] = pagesToShow.filter(page => page.fmDraft === choice).length;
}
}
if (tab !== Tab.All) {
pagesToShow = pagesToShow.filter(page => page.fmDraft === tab);
} else {
pagesToShow = searchedPages;
}
} else {
// Draft field is a boolean field
const draftFieldName = draftField?.name || "draft";
const drafts = pagesToShow.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true");
const published = pagesToShow.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined");
draftTypes[Tab.Draft] = draftField?.invert ? published.length : drafts.length;
draftTypes[Tab.Published] = draftField?.invert ? drafts.length : published.length;
if (tab === Tab.Published) {
pagesToShow = draftField?.invert ? drafts : published;
} else if (tab === Tab.Draft) {
pagesToShow = draftField?.invert ? published : drafts;
} else {
pagesToShow = searchedPages;
}
}
// Set the tab information
setTabInfo(draftTypes);
// Sort the pages
let pagesSorted: Page[] = Object.assign([], pagesToShow);
if (!search) {
@@ -131,6 +91,47 @@ export default function usePages(pages: Page[]) {
pagesSorted = pagesSorted.filter(page => page.fmCategories && page.fmCategories.includes(category));
}
// Process the tab data
const draftTypes = Object.assign({}, tabInfo);
draftTypes[Tab.All] = pagesSorted.length;
// Filter by draft status
if (draftField && draftField.type === 'choice') {
const draftChoices = settings?.draftField?.choices;
for (const choice of (draftChoices || [])) {
if (choice) {
draftTypes[choice] = pagesSorted.filter(page => page.fmDraft === choice).length;
}
}
if (tab !== Tab.All) {
pagesSorted = pagesSorted.filter(page => page.fmDraft === tab);
} else {
pagesSorted = pagesSorted;
}
} else {
// Draft field is a boolean field
const draftFieldName = draftField?.name || "draft";
const drafts = pagesSorted.filter(page => page[draftFieldName] == true || page[draftFieldName] === "true");
const published = pagesSorted.filter(page => page[draftFieldName] == false || page[draftFieldName] === "false" || typeof page[draftFieldName] === "undefined");
draftTypes[Tab.Draft] = draftField?.invert ? published.length : drafts.length;
draftTypes[Tab.Published] = draftField?.invert ? drafts.length : published.length;
if (tab === Tab.Published) {
pagesSorted = draftField?.invert ? drafts : published;
} else if (tab === Tab.Draft) {
pagesSorted = draftField?.invert ? published : drafts;
} else {
pagesSorted = pagesSorted;
}
}
// Set the tab information
setTabInfo(draftTypes);
// Set the pages
setPageItems(pagesSorted);
}, [ settings, tab, folder, search, tag, category, sorting, tabInfo ]);
@@ -165,7 +166,7 @@ export default function usePages(pages: Page[]) {
} else {
processPages(searchedPages);
}
}, [ settings?.draftField, pages, sorting, search, tab ]);
}, [ settings?.draftField, pages, sorting, search, tab, tag, category, folder ]);
useEffect(() => {
Messenger.listen(searchListener);
+15 -1
View File
@@ -5,6 +5,7 @@ import { App } from "./components/App";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import { SENTRY_LINK } from "../constants";
import { MemoryRouter } from "react-router-dom";
import './styles.css';
import { Preview } from "./components/Preview";
@@ -14,6 +15,15 @@ declare const acquireVsCodeApi: <T = unknown>() => {
postMessage: (msg: unknown) => void;
};
export const routePaths: { [name: string]: string } = {
welcome: "/welcome",
contents: "/contents",
media: "/media",
snippets: "/snippets",
data: "/data",
taxonomy: "/taxonomy",
};
const elm = document.querySelector("#app");
if (elm) {
const welcome = elm?.getAttribute("data-showWelcome");
@@ -39,7 +49,11 @@ if (elm) {
} else {
render((
<RecoilRoot>
<App showWelcome={!!welcome} />
<MemoryRouter
initialEntries={Object.keys(routePaths).map((key: string) => routePaths[key]) as string[]}
initialIndex={1}>
<App showWelcome={!!welcome} />
</MemoryRouter>
</RecoilRoot>
), elm);
}
+2 -6
View File
@@ -307,14 +307,10 @@ export class PagesListener extends BaseListener {
}
let tagParents = this.findFieldByType(contentType.fields, "tags");
if (tagParents.length !== 0) {
page.fmTags = this.getFieldValue(article.data, tagParents);
}
page.fmTags = this.getFieldValue(article.data, tagParents.length !== 0 ? tagParents : ["tags"]);
let categoryParents = this.findFieldByType(contentType.fields, "categories");
if (categoryParents.length !== 0) {
page.fmCategories = this.getFieldValue(article.data, categoryParents);
}
page.fmCategories = this.getFieldValue(article.data, categoryParents.length !== 0 ? categoryParents : ["categories"]);
// Check if parent fields were retrieved, if not there was no image present
if (previewFieldParents.length > 0) {