diff --git a/package-lock.json b/package-lock.json
index f90b9e7f..77c28a44 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2359,6 +2359,12 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
+ "fuse.js": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.4.6.tgz",
+ "integrity": "sha512-/gYxR/0VpXmWSfZOIPS3rWwU8SHgsRTwWuXhyb2O6s7aRuVtHtxCkR33bNYu3wyLyNx/Wpv0vU7FZy8Vj53VNw==",
+ "dev": true
+ },
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
diff --git a/package.json b/package.json
index b1064f50..181b97b8 100644
--- a/package.json
+++ b/package.json
@@ -346,10 +346,6 @@
{
"command": "frontMatter.collapseSections",
"when": "false"
- },
- {
- "command": "frontMatter.dashboard",
- "when": "frontMatterCanOpenDashboard"
}
],
"view/title": [
@@ -392,6 +388,7 @@
"css-loader": "5.2.7",
"date-fns": "2.23.0",
"downshift": "6.0.6",
+ "fuse.js": "6.4.6",
"glob": "7.1.6",
"gray-matter": "4.0.2",
"html-loader": "1.3.2",
diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts
index 2707ae89..e5cb1c32 100644
--- a/src/commands/Dashboard.ts
+++ b/src/commands/Dashboard.ts
@@ -1,7 +1,7 @@
import { SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_DATE_FIELD, SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, SETTING_SEO_DESCRIPTION_FIELD } from './../constants/settings';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { join } from "path";
-import { commands, env, Uri, ViewColumn, Webview, WebviewOptions, WebviewPanel, WebviewPanelOptions, window, workspace } from "vscode";
+import { ColorThemeKind, commands, env, ThemeColor, Uri, ViewColumn, Webview, WebviewOptions, WebviewPanel, WebviewPanelOptions, window, workspace } from "vscode";
import { SettingsHelper } from '../helpers';
import { PreviewSettings } from '../models';
import { format } from 'date-fns';
@@ -12,10 +12,13 @@ import { DashboardCommand } from '../pagesView/DashboardCommand';
import { DashboardMessage } from '../pagesView/DashboardMessage';
import { Page } from '../pagesView/models/Page';
import { openFileInEditor } from '../helpers/openFileInEditor';
+import { COMMAND_NAME } from '../constants/Extension';
+import { Template } from './Template';
export class Dashboard {
private static webview: WebviewPanel | null = null;
+ private static isDisposed: boolean = true;
/**
* Init the dashboard
@@ -24,6 +27,16 @@ export class Dashboard {
const folders = Folders.get();
await commands.executeCommand('setContext', CONTEXT.canOpenDashboard, folders && folders.length > 0);
}
+
+ public static get isOpen(): boolean {
+ return !Dashboard.isDisposed;
+ }
+
+ public static reveal() {
+ if (Dashboard.webview) {
+ Dashboard.webview.reveal();
+ }
+ }
/**
* Open the markdown preview in the editor
@@ -37,9 +50,11 @@ export class Dashboard {
ViewColumn.One,
{
enableScripts: true
- }
+ }
);
+ Dashboard.isDisposed = false;
+
Dashboard.webview.iconPath = {
dark: Uri.file(join(extensionPath, 'assets/frontmatter-dark.svg')),
light: Uri.file(join(extensionPath, 'assets/frontmatter.svg'))
@@ -53,18 +68,35 @@ export class Dashboard {
}
});
+ Dashboard.webview.onDidDispose(() => {
+ Dashboard.isDisposed = true;
+ });
+
Dashboard.webview.webview.onDidReceiveMessage(async (msg) => {
switch(msg.command) {
case DashboardMessage.getData:
+ Dashboard.getSettings();
Dashboard.getPages();
break;
case DashboardMessage.openFile:
openFileInEditor(msg.data);
break;
+ case DashboardMessage.createContent:
+ await commands.executeCommand(COMMAND_NAME.createContent);
+ break;
}
});
}
+ private static async getSettings() {
+ Dashboard.postWebviewMessage({
+ command: DashboardCommand.settings,
+ data: {
+ folders: Folders.get(),
+ initialized: await Template.isInitialized()
+ }
+ });
+ }
private static async getPages() {
const config = SettingsHelper.getConfig();
@@ -81,36 +113,38 @@ export class Dashboard {
if (folderInfo) {
for (const folder of folderInfo) {
for (const file of folder.lastModified) {
- const article = ArticleHelper.getFrontMatterByPath(file.filePath);
+ if (file.fileName.endsWith(`.md`) || file.fileName.endsWith(`.mdx`)) {
+ const article = ArticleHelper.getFrontMatterByPath(file.filePath);
- if (article?.data.title) {
- const page: Page = {
- fmGroup: folder.title,
- fmModified: file.mtime,
- fmFilePath: file.filePath,
- fmFileName: file.fileName,
- title: article?.data.title,
- slug: article?.data.slug,
- date: article?.data[dateField] || "",
- draft: article?.data.draft,
- description: article?.data[descriptionField] || "",
- };
-
- if (article?.data.preview && crntWsFolder) {
- const previewPath = join(crntWsFolder.uri.fsPath, staticFolder || "", article?.data.preview);
- const previewUri = Uri.file(previewPath);
- const preview = Dashboard.webview?.webview.asWebviewUri(previewUri);
- page.preview = preview?.toString() || "";
+ if (article?.data.title) {
+ const page: Page = {
+ fmGroup: folder.title,
+ fmModified: file.mtime,
+ fmFilePath: file.filePath,
+ fmFileName: file.fileName,
+ title: article?.data.title,
+ slug: article?.data.slug,
+ date: article?.data[dateField] || "",
+ draft: article?.data.draft,
+ description: article?.data[descriptionField] || "",
+ };
+
+ if (article?.data.preview && crntWsFolder) {
+ const previewPath = join(crntWsFolder.uri.fsPath, staticFolder || "", article?.data.preview);
+ const previewUri = Uri.file(previewPath);
+ const preview = Dashboard.webview?.webview.asWebviewUri(previewUri);
+ page.preview = preview?.toString() || "";
+ }
+
+ pages.push(page);
}
-
- pages.push(page);
}
}
}
}
Dashboard.postWebviewMessage({
- command: DashboardCommand.data,
+ command: DashboardCommand.pages,
data: pages
});
}
@@ -136,12 +170,12 @@ export class Dashboard {
-
+
- Front Matter
+ Front Matter Dashboard
-
+
diff --git a/src/extension.ts b/src/extension.ts
index 7a041934..acceed69 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -108,7 +108,6 @@ export async function activate({ subscriptions, extensionUri, extensionPath }: v
vscode.workspace.onDidChangeConfiguration(() => {
Template.init();
Preview.init();
- Dashboard.init();
Folders.updateVsCodeCtx();
const exView = ExplorerView.getInstance();
@@ -147,8 +146,13 @@ export async function activate({ subscriptions, extensionUri, extensionPath }: v
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.preview, () => Preview.open(extensionPath) ));
// Pages dashboard
- Dashboard.init();
- subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboard, () => Dashboard.open(extensionPath) ));
+ subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboard, () => {
+ if (Dashboard.isOpen) {
+ Dashboard.reveal();
+ } else {
+ Dashboard.open(extensionPath);
+ }
+ }));
// Subscribe all commands
subscriptions.push(
diff --git a/src/hooks/useDarkMode.tsx b/src/hooks/useDarkMode.tsx
new file mode 100644
index 00000000..5055a4f8
--- /dev/null
+++ b/src/hooks/useDarkMode.tsx
@@ -0,0 +1,28 @@
+import { useState, useEffect } from 'react';
+
+export default function useDarkMode() {
+
+ const setTheme = (elm: HTMLElement) => {
+ if (elm) {
+ const darkMode = elm.classList.contains('vscode-dark');
+ document.documentElement.classList.remove(`${darkMode ? "light" : "dark"}`);
+ document.documentElement.classList.add(`${darkMode ? "dark" : "light"}`);
+ }
+ };
+
+ useEffect(() => {
+ const mutationObserver = new MutationObserver((mutationsList, observer) => {
+ const last = mutationsList.filter(item => item.type === "attributes" || item.attributeName === 'class').pop();
+ setTheme(last?.target as HTMLElement);
+ });
+
+ setTheme(document.body);
+
+ mutationObserver.observe(document.body, { childList: false, attributes: true })
+
+ return () => {
+ mutationObserver.disconnect();
+ };
+ }, ['']);
+
+}
\ No newline at end of file
diff --git a/src/viewpanel/hooks/useDebounce.tsx b/src/hooks/useDebounce.tsx
similarity index 84%
rename from src/viewpanel/hooks/useDebounce.tsx
rename to src/hooks/useDebounce.tsx
index cca5bd92..398ec0cd 100644
--- a/src/viewpanel/hooks/useDebounce.tsx
+++ b/src/hooks/useDebounce.tsx
@@ -1,8 +1,8 @@
import { useEffect, useState } from "react";
-export function useDebounce(value: string, delay: number) {
+export function useDebounce(value: T, delay: number) {
// State and setters for debounced value
- const [debouncedValue, setDebouncedValue] = useState(value);
+ const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
diff --git a/src/pagesView/DashboardCommand.ts b/src/pagesView/DashboardCommand.ts
index b6510f8a..987fc7d8 100644
--- a/src/pagesView/DashboardCommand.ts
+++ b/src/pagesView/DashboardCommand.ts
@@ -1,4 +1,5 @@
export enum DashboardCommand {
loading = "loading",
- data = "data"
+ pages = "pages",
+ settings = "settings"
}
\ No newline at end of file
diff --git a/src/pagesView/DashboardMessage.ts b/src/pagesView/DashboardMessage.ts
index 2dfc0193..21c6d266 100644
--- a/src/pagesView/DashboardMessage.ts
+++ b/src/pagesView/DashboardMessage.ts
@@ -1,4 +1,6 @@
export enum DashboardMessage {
getData = 'getData',
openFile = 'openFile',
+ getTheme = 'getTheme',
+ createContent = 'createContent'
}
\ No newline at end of file
diff --git a/src/pagesView/components/Dashboard.tsx b/src/pagesView/components/Dashboard.tsx
index bfcfd667..1168bad1 100644
--- a/src/pagesView/components/Dashboard.tsx
+++ b/src/pagesView/components/Dashboard.tsx
@@ -5,50 +5,87 @@ import { Overview } from './Overview';
import { Header } from './Header';
import { Tab } from '../constants/Tab';
import { SortOption } from '../constants/SortOption';
+import Fuse from 'fuse.js';
+import { Page } from '../models/Page';
+import useDarkMode from '../../hooks/useDarkMode';
export interface IDashboardProps {}
-export const Dashboard: React.FunctionComponent = (props: React.PropsWithChildren) => {
- const { loading, pages } = useMessages();
+// TODO: Filter by tag / category
+
+const fuseOptions: Fuse.IFuseOptions = {
+ keys: [
+ "title",
+ "slug",
+ "description",
+ "fmFileName"
+ ]
+};
+
+export const Dashboard: React.FunctionComponent = ({}: React.PropsWithChildren) => {
+ const { loading, pages, settings } = useMessages();
const [ tab, setTab ] = React.useState(Tab.All);
const [ sorting, setSorting ] = React.useState(SortOption.LastModified);
+ const [ group, setGroup ] = React.useState(null);
+ const [ search, setSearch ] = React.useState(null);
+ const [ pageItems, setPageItems ] = React.useState([]);
+ useDarkMode();
- let pagesToShow = pages;
- if (tab === Tab.Published) {
- pagesToShow = pages.filter(page => !page.draft);
- } else if (tab === Tab.Draft) {
- pagesToShow = pages.filter(page => !!page.draft);
- } else {
- pagesToShow = pages;
- }
+ React.useEffect(() => {
+ // Check if search needs to be performed
+ let searchedPages = pages;
+ if (search) {
+ const fuse = new Fuse(pages, fuseOptions);
+ const results = fuse.search(search);
+ searchedPages = results.map(page => page.item);
+ }
- let pagesSorted = pagesToShow;
- if (sorting === SortOption.FileNameAsc) {
- pagesSorted = pagesToShow.sort((a, b) => a.fmFileName.toLowerCase().localeCompare(b.fmFileName.toLowerCase()));
- } else if (sorting === SortOption.FileNameDesc) {
- pagesSorted = pagesToShow.sort((a, b) => b.fmFileName.toLowerCase().localeCompare(a.fmFileName.toLowerCase()));
- } else {
- pagesSorted = pagesToShow.sort((a, b) => b.fmModified - a.fmModified);
- }
+ // Filter the pages
+ let pagesToShow = searchedPages;
+ if (tab === Tab.Published) {
+ pagesToShow = searchedPages.filter(page => !page.draft);
+ } else if (tab === Tab.Draft) {
+ pagesToShow = searchedPages.filter(page => !!page.draft);
+ } else {
+ pagesToShow = searchedPages;
+ }
- // Show draft/published
- // Filter by draft
- // Filter by folder (if multiple)
- // TODO: Sort by last modified
+ // Sort the pages
+ let pagesSorted = pagesToShow;
+ if (sorting === SortOption.FileNameAsc) {
+ pagesSorted = pagesToShow.sort((a, b) => a.fmFileName.toLowerCase().localeCompare(b.fmFileName.toLowerCase()));
+ } else if (sorting === SortOption.FileNameDesc) {
+ pagesSorted = pagesToShow.sort((a, b) => b.fmFileName.toLowerCase().localeCompare(a.fmFileName.toLowerCase()));
+ } else {
+ pagesSorted = pagesToShow.sort((a, b) => b.fmModified - a.fmModified);
+ }
+
+ if (group) {
+ pagesSorted = pagesSorted.filter(page => page.fmGroup === group);
+ }
+
+ setPageItems(pagesSorted);
+ }, [ pages, tab, sorting, group, search ]);
+
+ const pageGroups = [...new Set(pages.map(page => page.fmGroup))];
return (
-
+
setTab(tabId)}
switchSorting={(sortId: SortOption) => setSorting(sortId)}
+ switchGroup={(groupId: string | null) => setGroup(groupId)}
+ onSearch={(value: string | null) => setSearch(value)}
+ settings={settings}
/>
-
-
-
- { loading ? : null }
+ { loading ? : }
+
);
};
\ No newline at end of file
diff --git a/src/pagesView/components/DateField.tsx b/src/pagesView/components/DateField.tsx
index 431fc788..59628ca6 100644
--- a/src/pagesView/components/DateField.tsx
+++ b/src/pagesView/components/DateField.tsx
@@ -11,6 +11,6 @@ export const DateField: React.FunctionComponent = ({value}: Rea
const dateString = format(parsedValue, 'yyyy-MM-dd');
return (
- {dateString}
+ {dateString}
);
};
\ No newline at end of file
diff --git a/src/pagesView/components/Grouping.tsx b/src/pagesView/components/Grouping.tsx
new file mode 100644
index 00000000..bd41d601
--- /dev/null
+++ b/src/pagesView/components/Grouping.tsx
@@ -0,0 +1,65 @@
+import { Menu, Transition } from '@headlessui/react';
+import { ChevronDownIcon } from '@heroicons/react/solid';
+import * as React from 'react';
+import { Fragment } from 'react';
+import { MenuItem } from './MenuItem';
+
+export interface IGroupingProps {
+ groups: string[];
+ crntGroup: string | null;
+ switchGroup: (group: string | null) => void;
+}
+
+const DEFAULT_TYPE = "All types";
+
+export const Grouping: React.FunctionComponent = ({groups, crntGroup, switchGroup}: React.PropsWithChildren) => {
+ if (groups.length <= 1) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/pagesView/components/Header.tsx b/src/pagesView/components/Header.tsx
index e9f8831d..a23fe40c 100644
--- a/src/pagesView/components/Header.tsx
+++ b/src/pagesView/components/Header.tsx
@@ -1,96 +1,62 @@
-import { Menu, Transition } from '@headlessui/react';
import * as React from 'react';
import { Tab } from '../constants/Tab';
-import { ChevronDownIcon } from '@heroicons/react/solid';
-import { Fragment } from 'react';
import { SortOption } from '../constants/SortOption';
+import { Navigation } from './Navigation';
+import { Sorting } from './Sorting';
+import { Grouping } from './Grouping';
+import { MessageHelper } from '../../helpers/MessageHelper';
+import { DashboardMessage } from '../DashboardMessage';
+import { Searchbox } from './Searchbox';
+import { Settings } from '../models/Settings';
export interface IHeaderProps {
+ settings: Settings;
+
+ // Navigation
currentTab: Tab;
- currentSorting: SortOption;
-
+ totalPages: number;
switchTab: (tabId: Tab) => void;
+
+ // Sorting
+ currentSorting: SortOption;
switchSorting: (sortId: SortOption) => void;
+
+ // Grouping
+ groups: string[];
+ crntGroup: string | null;
+ switchGroup: (group: string | null) => void;
+
+ // Searching
+ onSearch: (value: string | null) => void;
}
-function classNames(...classes: any[]) {
- return classes.filter(Boolean).join(' ')
-}
+export const Header: React.FunctionComponent = ({currentTab, currentSorting, switchSorting, switchTab, totalPages, crntGroup, groups, switchGroup, onSearch, settings}: React.PropsWithChildren) => {
-export const tabs = [
- { name: 'All articles', id: Tab.All},
- { name: 'Published', id: Tab.Published },
- { name: 'In draft', id: Tab.Draft }
-];
-
-export const sortOptions = [
- { name: "Last modified", id: SortOption.LastModified },
- { name: "By filename (asc)", id: SortOption.FileNameAsc },
- { name: "By filename (desc)", id: SortOption.FileNameDesc },
-];
-
-export const Header: React.FunctionComponent = ({currentTab, currentSorting, switchSorting, switchTab}: React.PropsWithChildren) => {
+ const createContent = () => {
+ MessageHelper.sendMessage(DashboardMessage.createContent);
+ };
return (
-
-
+
+
+
+
+
+
-
-
);
diff --git a/src/pagesView/components/Item.tsx b/src/pagesView/components/Item.tsx
index 337214a5..3aa2cd7c 100644
--- a/src/pagesView/components/Item.tsx
+++ b/src/pagesView/components/Item.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
+import { MarkdownIcon } from '../../viewpanel/components/Icons/MarkdownIcon';
import { DashboardMessage } from '../DashboardMessage';
import { Page } from '../models/Page';
import { DateField } from './DateField';
@@ -15,19 +16,21 @@ export const Item: React.FunctionComponent
= ({ fmFilePath, date, ti
return (
-