diff --git a/package-lock.json b/package-lock.json index 8d817168..8133c61e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 793478bc..43a987cf 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/dashboardWebView/components/App.tsx b/src/dashboardWebView/components/App.tsx index e22ebc95..36085faf 100644 --- a/src/dashboardWebView/components/App.tsx +++ b/src/dashboardWebView/components/App.tsx @@ -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 = ({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 ; } @@ -39,45 +67,24 @@ export const App: React.FunctionComponent = ({showWelcome}: React.Pro return ; } - if (view === NavigationType.Snippets) { - return ( -
- -
- ); - } - - if (view === NavigationType.Media) { - return ( -
- -
- ); - } - - if (view === NavigationType.Data) { - return ( - -
- -
-
- ); - } - - if (view === NavigationType.Taxonomy) { - return ( - -
- -
-
- ); - } - return (
- + + } /> + } /> + } /> + } /> + + { + allowDataView && } /> + } + + { + allowTaxonomyView && } /> + } + + } /> +
); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/Header/ClearFilters.tsx b/src/dashboardWebView/components/Header/ClearFilters.tsx index c688388b..9bb699f8 100644 --- a/src/dashboardWebView/components/Header/ClearFilters.tsx +++ b/src/dashboardWebView/components/Header/ClearFilters.tsx @@ -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 = (props: resetCategory(); }; - React.useEffect(() => { + useEffect(() => { if (folder !== DEFAULT_FOLDER_STATE || tag !== DEFAULT_TAG_STATE || category !== DEFAULT_CATEGORY_STATE) { setShow(true); } else { diff --git a/src/dashboardWebView/components/Header/Header.tsx b/src/dashboardWebView/components/Header/Header.tsx index e139c360..b44af033 100644 --- a/src/dashboardWebView/components/Header/Header.tsx +++ b/src/dashboardWebView/components/Header/Header.tsx @@ -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 = ({header, totalPages, folders, settings }: React.PropsWithChildren) => { 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 = ({header, totalPage }; const updateView = (view: NavigationType) => { - setView(view); + navigate(routePaths[view]); resetSorting(); } @@ -68,6 +72,27 @@ export const Header: React.FunctionComponent = ({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 (
@@ -76,7 +101,7 @@ export const Header: React.FunctionComponent = ({header, totalPage
{ - view === NavigationType.Contents && ( + location.pathname === routePaths.contents && ( <>
@@ -141,7 +166,7 @@ export const Header: React.FunctionComponent = ({header, totalPage } { - view === NavigationType.Media && ( + location.pathname === routePaths.media && ( <> diff --git a/src/dashboardWebView/components/Header/Tab.tsx b/src/dashboardWebView/components/Header/Tab.tsx index 1bfdcfcf..c438b3f8 100644 --- a/src/dashboardWebView/components/Header/Tab.tsx +++ b/src/dashboardWebView/components/Header/Tab.tsx @@ -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 = ({navigationType, onNavigate, children}: React.PropsWithChildren) => { - const view = useRecoilValue(DashboardViewAtom); + const location = useLocation(); return ( + ); + } return ( - - {total} + + {total || `-`} ); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/TaxonomyView/TaxonomyManager.tsx b/src/dashboardWebView/components/TaxonomyView/TaxonomyManager.tsx index 40ef1b05..ac383064 100644 --- a/src/dashboardWebView/components/TaxonomyView/TaxonomyManager.tsx +++ b/src/dashboardWebView/components/TaxonomyView/TaxonomyManager.tsx @@ -63,21 +63,29 @@ export const TaxonomyManager: React.FunctionComponent = ( } 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 = ( Name - Times used + Count Action diff --git a/src/dashboardWebView/components/UnknownView/UnknownView.tsx b/src/dashboardWebView/components/UnknownView/UnknownView.tsx new file mode 100644 index 00000000..e508adb6 --- /dev/null +++ b/src/dashboardWebView/components/UnknownView/UnknownView.tsx @@ -0,0 +1,18 @@ +import { StopIcon } from '@heroicons/react/outline'; +import * as React from 'react'; + +export interface IUnknownViewProps {} + +export const UnknownView: React.FunctionComponent = (props: React.PropsWithChildren) => { + return ( +
+
+ +

View does not exist

+

+ You seem to have ended up on a view that doesn't exist. Please re-open the dashboard. +

+
+
+ ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/UnknownView/index.ts b/src/dashboardWebView/components/UnknownView/index.ts new file mode 100644 index 00000000..74596b70 --- /dev/null +++ b/src/dashboardWebView/components/UnknownView/index.ts @@ -0,0 +1 @@ +export * from './UnknownView'; diff --git a/src/dashboardWebView/hooks/usePages.tsx b/src/dashboardWebView/hooks/usePages.tsx index fbfd96f2..d8fec691 100644 --- a/src/dashboardWebView/hooks/usePages.tsx +++ b/src/dashboardWebView/hooks/usePages.tsx @@ -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); diff --git a/src/dashboardWebView/index.tsx b/src/dashboardWebView/index.tsx index 6424e325..cd648392 100644 --- a/src/dashboardWebView/index.tsx +++ b/src/dashboardWebView/index.tsx @@ -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: () => { 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(( - + routePaths[key]) as string[]} + initialIndex={1}> + + ), elm); } diff --git a/src/listeners/dashboard/PagesListener.ts b/src/listeners/dashboard/PagesListener.ts index c401e9c2..7e642746 100644 --- a/src/listeners/dashboard/PagesListener.ts +++ b/src/listeners/dashboard/PagesListener.ts @@ -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) {