#65 - Dashboard implementation

This commit is contained in:
Elio Struyf
2021-08-24 21:01:58 +02:00
parent ce8a343ffd
commit 722c0d6888
44 changed files with 745 additions and 58 deletions
+13 -3
View File
@@ -48,6 +48,16 @@
"lit-element": "^2.5.1"
}
},
"@headlessui/react": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.0.tgz",
"integrity": "sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw=="
},
"@heroicons/react": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-1.0.4.tgz",
"integrity": "sha512-3kOrTmo8+Z8o6AL0rzN82MOf8J5CuxhRLFhpI8mrn+3OqekA6d5eb1GYO3EYYo1Vn6mYQSMNTzCWbEwUInb0cQ=="
},
"@iarna/toml": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.3.tgz",
@@ -1487,9 +1497,9 @@
"dev": true
},
"date-fns": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.0.1.tgz",
"integrity": "sha512-C14oTzTZy8DH1Eq8N78owrCWvf3+cnJw88BTK/N3DYWVxDJuJzPaNdplzYxDYuuXXGvqBcO4Vy5SOrwAooXSWw==",
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.23.0.tgz",
"integrity": "sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==",
"dev": true
},
"debug": {
+18 -1
View File
@@ -59,6 +59,7 @@
"onCommand:frontMatter.init",
"onCommand:frontMatter.collapseSections",
"onCommand:frontMatter.preview",
"onCommand:frontMatter.dashboard",
"onView:frontMatter.explorer"
],
"main": "./dist/extension",
@@ -101,6 +102,11 @@
"default": [],
"markdownDescription": "This array of folders defines where the extension can easily create new content by running the create article command."
},
"frontMatter.content.publicFolder": {
"type": "string",
"default": "",
"markdownDescription": "Specify the folder name where all your assets are located. For instance in Hugo this is the `static` folder."
},
"frontMatter.custom.scripts": {
"type": "array",
"default": [],
@@ -299,6 +305,11 @@
"command": "frontMatter.preview",
"title": "Preview article",
"category": "Front matter"
},
{
"command": "frontMatter.dashboard",
"title": "Open pages dashboard",
"category": "Front matter"
}
],
"menus": {
@@ -335,6 +346,10 @@
{
"command": "frontMatter.collapseSections",
"when": "false"
},
{
"command": "frontMatter.dashboard",
"when": "frontMatterCanOpenDashboard"
}
],
"view/title": [
@@ -375,7 +390,7 @@
"@vscode/codicons": "0.0.20",
"autoprefixer": "^10.3.2",
"css-loader": "5.2.7",
"date-fns": "2.0.1",
"date-fns": "2.23.0",
"downshift": "6.0.6",
"glob": "7.1.6",
"gray-matter": "4.0.2",
@@ -396,6 +411,8 @@
"webpack-cli": "3.3.12"
},
"dependencies": {
"@headlessui/react": "1.4.0",
"@heroicons/react": "1.0.4",
"lodash.uniqby": "4.7.0"
}
}
+154
View File
@@ -0,0 +1,154 @@
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 { SettingsHelper } from '../helpers';
import { PreviewSettings } from '../models';
import { format } from 'date-fns';
import { CONTEXT } from '../constants/context';
import { Folders } from './Folders';
import { getNonce } from '../helpers/getNonce';
import { DashboardCommand } from '../pagesView/DashboardCommand';
import { DashboardMessage } from '../pagesView/DashboardMessage';
import { Page } from '../pagesView/models/Page';
import { openFileInEditor } from '../helpers/openFileInEditor';
export class Dashboard {
private static webview: WebviewPanel | null = null;
/** 
* Init the dashboard
*/
public static async init() {
const folders = Folders.get();
await commands.executeCommand('setContext', CONTEXT.canOpenDashboard, folders && folders.length > 0);
}
/**
* Open the markdown preview in the editor
*/
public static async open(extensionPath: string) {
// Create the preview webview
Dashboard.webview = window.createWebviewPanel(
'frontMatterDashboard',
'FrontMatter Dashboard',
ViewColumn.One,
{
enableScripts: true
}
);
Dashboard.webview.iconPath = {
dark: Uri.file(join(extensionPath, 'assets/frontmatter-dark.svg')),
light: Uri.file(join(extensionPath, 'assets/frontmatter.svg'))
};
Dashboard.webview.webview.html = Dashboard.getWebviewContent(Dashboard.webview.webview, Uri.parse(extensionPath));
Dashboard.webview.onDidChangeViewState(() => {
if (this.webview?.visible) {
console.log(`Dashboard opened`);
}
});
Dashboard.webview.webview.onDidReceiveMessage(async (msg) => {
switch(msg.command) {
case DashboardMessage.getData:
Dashboard.getPages();
break;
case DashboardMessage.openFile:
openFileInEditor(msg.data);
break;
}
});
}
private static async getPages() {
const config = SettingsHelper.getConfig();
const wsFolders = workspace.workspaceFolders;
const crntWsFolder = wsFolders && wsFolders.length > 0 ? wsFolders[0] : null;
const descriptionField = config.get(SETTING_SEO_DESCRIPTION_FIELD) as string || "description";
const dateField = config.get(SETTING_DATE_FIELD) as string || "date";
const staticFolder = config.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
const folderInfo = await Folders.getInfo();
const pages: Page[] = [];
if (folderInfo) {
for (const folder of folderInfo) {
for (const file of folder.lastModified) {
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() || "";
}
pages.push(page);
}
}
}
}
Dashboard.postWebviewMessage({
command: DashboardCommand.data,
data: pages
});
}
/**
* Post data to the dashboard
* @param msg
*/
private static postWebviewMessage(msg: { command: DashboardCommand, data?: any }) {
Dashboard.webview?.webview.postMessage(msg);
}
/**
* Retrieve the webview HTML contents
* @param webView
*/
private static getWebviewContent(webView: Webview, extensionPath: Uri): string {
const scriptUri = webView.asWebviewUri(Uri.joinPath(extensionPath, 'dist', 'pages.js'));
const nonce = getNonce();
return `
<!DOCTYPE html>
<html lang="en" style="width:100%;height:100%;margin:0;padding:0;">
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https://images.unsplash.com/ ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Front Matter</title>
</head>
<body style="width:100%;height:100%;margin:0;padding:0;background:rgba(250, 250, 250, 1);">
<div id="app" style="width:100%;height:100%;margin:0;padding:0;"></div>
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759" alt="Daily usage" />
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>
`;
}
}
+7 -3
View File
@@ -122,7 +122,7 @@ export class Folders {
/**
* Get the registered folders information
*/
public static async getInfo(): Promise<FolderInfo[] | null> {
public static async getInfo(limit?: number): Promise<FolderInfo[] | null> {
const folders = Folders.get();
if (folders && folders.length > 0) {
let folderInfo: FolderInfo[] = [];
@@ -149,7 +149,11 @@ export class Folders {
}
}
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime).slice(0, 10);
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime);
if (limit) {
fileStats = fileStats.slice(0, limit);
}
folderInfo.push({
title: folder.title,
@@ -172,7 +176,7 @@ export class Folders {
* Get the folder settings
* @returns
*/
private static get() {
public static get() {
const config = SettingsHelper.getConfig();
const folders: ContentFolder[] = config.get(SETTINGS_CONTENT_FOLDERS) as ContentFolder[];
return folders;
+1
View File
@@ -23,4 +23,5 @@ export const COMMAND_NAME = {
createTemplate: getCommandName("createTemplate"),
collapseSections: getCommandName("collapseSections"),
preview: getCommandName("preview"),
dashboard: getCommandName("dashboard"),
};
+1 -2
View File
@@ -1,7 +1,6 @@
export const CONTEXT = {
canInit: "frontMatterCanInit",
canOpenPreview: "frontMatterCanOpenPreview",
canOpenDashboard: "frontMatterCanOpenDashboard",
registeredFolders: 'frontMatter.registeredFolders'
};
+1
View File
@@ -34,4 +34,5 @@ export const SETTING_CUSTOM_SCRIPTS = "custom.scripts";
export const SETTING_AUTO_UPDATE_DATE = "content.autoUpdateDate";
export const SETTINGS_CONTENT_FOLDERS = "content.folders";
export const SETTINGS_CONTENT_STATIC_FOLDERS = "content.publicFolder";
export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
+6
View File
@@ -1,3 +1,4 @@
import { Dashboard } from './commands/Dashboard';
import * as vscode from 'vscode';
import { Article, Settings, StatusListener } from './commands';
import { Folders } from './commands/Folders';
@@ -107,6 +108,7 @@ export async function activate({ subscriptions, extensionUri, extensionPath }: v
vscode.workspace.onDidChangeConfiguration(() => {
Template.init();
Preview.init();
Dashboard.init();
Folders.updateVsCodeCtx();
const exView = ExplorerView.getInstance();
@@ -144,6 +146,10 @@ export async function activate({ subscriptions, extensionUri, extensionPath }: v
Preview.init();
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) ));
// Subscribe all commands
subscriptions.push(
insertTags,
@@ -1,4 +1,6 @@
import { CommandToCode } from "../CommandToCode";
import { DashboardMessage } from './../pagesView/DashboardMessage';
import { CommandToCode } from "../viewpanel/CommandToCode";
interface ClientVsCode<T> {
getState: () => T;
@@ -16,7 +18,7 @@ export class MessageHelper {
return MessageHelper.vscode;
}
public static sendMessage = (command: CommandToCode, data?: any) => {
public static sendMessage = (command: CommandToCode | DashboardMessage, data?: any) => {
if (data) {
MessageHelper.vscode.postMessage({ command, data });
} else {
+10
View File
@@ -0,0 +1,10 @@
export const getNonce = () => {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
+13
View File
@@ -0,0 +1,13 @@
import { Uri, workspace, window } from "vscode";
import { Notifications } from "./Notifications";
export const openFileInEditor = async (filePath: string) => {
if (filePath) {
try {
const doc = await workspace.openTextDocument(Uri.file(filePath));
await window.showTextDocument(doc, 1, false);
} catch (e) {
Notifications.error(`Couldn't open the file.`);
}
}
};
+4
View File
@@ -0,0 +1,4 @@
export enum DashboardCommand {
loading = "loading",
data = "data"
}
+4
View File
@@ -0,0 +1,4 @@
export enum DashboardMessage {
getData = 'getData',
openFile = 'openFile',
}
+54
View File
@@ -0,0 +1,54 @@
import * as React from 'react';
import { Spinner } from './Spinner';
import useMessages from '../hooks/useMessages';
import { Overview } from './Overview';
import { Header } from './Header';
import { Tab } from '../constants/Tab';
import { SortOption } from '../constants/SortOption';
export interface IDashboardProps {}
export const Dashboard: React.FunctionComponent<IDashboardProps> = (props: React.PropsWithChildren<IDashboardProps>) => {
const { loading, pages } = useMessages();
const [ tab, setTab ] = React.useState(Tab.All);
const [ sorting, setSorting ] = React.useState(SortOption.LastModified);
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;
}
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);
}
// Show draft/published
// Filter by draft
// Filter by folder (if multiple)
// TODO: Sort by last modified
return (
<main className="h-full w-full">
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<Header currentTab={tab}
currentSorting={sorting}
switchTab={(tabId: Tab) => setTab(tabId)}
switchSorting={(sortId: SortOption) => setSorting(sortId)}
/>
<Overview pages={pagesSorted} />
</div>
{ loading ? <Spinner /> : null }
</main>
);
};
+16
View File
@@ -0,0 +1,16 @@
import { format, parseJSON } from 'date-fns';
import * as React from 'react';
export interface IDateFieldProps {
value: Date | string;
}
export const DateField: React.FunctionComponent<IDateFieldProps> = ({value}: React.PropsWithChildren<IDateFieldProps>) => {
const parsedValue = typeof value === 'string' ? parseJSON(value) : value;
const dateString = format(parsedValue, 'yyyy-MM-dd');
return (
<span className={`text-vulcan-100 text-xs`}>{dateString}</span>
);
};
+97
View File
@@ -0,0 +1,97 @@
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';
export interface IHeaderProps {
currentTab: Tab;
currentSorting: SortOption;
switchTab: (tabId: Tab) => void;
switchSorting: (sortId: SortOption) => void;
}
function classNames(...classes: any[]) {
return classes.filter(Boolean).join(' ')
}
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<IHeaderProps> = ({currentTab, currentSorting, switchSorting, switchTab}: React.PropsWithChildren<IHeaderProps>) => {
return (
<div className="px-4 flex items-center border-b border-gray-200 mb-8 sticky top-0 z-50 bg-gray-50 shadow-sm">
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<button
key={tab.name}
className={classNames(
tab.id === currentTab
? 'border-teal-900 text-teal-900'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
'whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm'
)}
aria-current={tab.id === currentTab ? 'page' : undefined}
onClick={() => switchTab(tab.id)}
>
{tab.name}
</button>
))}
</nav>
<div className="flex items-center ml-6">
<Menu as="div" className="relative z-10 inline-block text-left">
<div>
<Menu.Button className="group inline-flex justify-center text-sm font-medium text-gray-500 hover:text-gray-700">
Sort
<ChevronDownIcon
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 z-10 mt-2 w-40 rounded-md shadow-2xl bg-white ring-1 ring-vulcan-400 ring-opacity-5 focus:outline-none text-sm">
<div className="py-1">
{sortOptions.map((option) => (
<Menu.Item key={option.id}>
<button
onClick={() => switchSorting(option.id)}
className={classNames(
option.id === currentSorting ? 'text-vulcan-500' : 'text-gray-500',
'block px-4 py-2 text-sm font-medium w-full text-left hover:text-gray-700'
)}
>
{option.name}
</button>
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
);
};
+44
View File
@@ -0,0 +1,44 @@
import * as React from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { DashboardMessage } from '../DashboardMessage';
import { Page } from '../models/Page';
import { DateField } from './DateField';
import { Status } from './Status';
export interface IItemProps extends Page {}
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, draft, description, preview }: React.PropsWithChildren<IItemProps>) => {
const openFile = () => {
MessageHelper.sendMessage(DashboardMessage.openFile, fmFilePath);
};
return (
<li className="relative">
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full rounded-lg bg-gray-50 text-vulcan-500 text-left overflow-hidden shadow-md hover:shadow-xl`}
onClick={openFile}>
<div className="relative h-36 w-full overflow-hidden">
{
preview ? (
<img src={`${preview}`} alt={title} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
) : (
<img src={`https://images.unsplash.com/photo-1598620617148-c9e8ddee6711?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80`} alt={title} className="absolute inset-0 h-full w-full object-cover group-hover:opacity-95" loading="lazy" />
)
}
</div>
<div className="p-4">
<div className={`flex justify-between items-center`}>
<Status draft={!!draft} />
<DateField value={date} />
</div>
<h2 className="mt-2 mb-2 font-bold">{title}</h2>
<p className="text-xs text-vulcan-200">{description}</p>
</div>
</button>
</li>
);
};
+11
View File
@@ -0,0 +1,11 @@
import * as React from 'react';
export interface IListProps {}
export const List: React.FunctionComponent<IListProps> = ({children}: React.PropsWithChildren<IListProps>) => {
return (
<ul role="list" className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8">
{children}
</ul>
);
};
+19
View File
@@ -0,0 +1,19 @@
import * as React from 'react';
import { Page } from '../models/Page';
import { Item } from './Item';
import { List } from './List';
export interface IOverviewProps {
pages: Page[];
}
export const Overview: React.FunctionComponent<IOverviewProps> = ({pages}: React.PropsWithChildren<IOverviewProps>) => {
return (
<List>
{pages.map(page => (
<Item key={page.slug} {...page} />
))}
</List>
);
};
+11
View File
@@ -0,0 +1,11 @@
import * as React from 'react';
export interface ISpinnerProps {}
export const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
return (
<div className={`fixed top-0 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-40`}>
<div className="loader ease-linear rounded-full border-8 border-t-8 border-gray-50 h-32 w-32" />
</div>
);
};
+11
View File
@@ -0,0 +1,11 @@
import * as React from 'react';
export interface IStatusProps {
draft: boolean;
}
export const Status: React.FunctionComponent<IStatusProps> = ({draft}: React.PropsWithChildren<IStatusProps>) => {
return (
<span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs ${draft ? "bg-red-500 text-whisper-200" : "bg-teal-500 text-whisper-500"}`}>{draft ? "Draft" : "Published"}</span>
);
};
+5
View File
@@ -0,0 +1,5 @@
export enum SortOption {
LastModified = 1,
FileNameAsc,
FileNameDesc
}
+5
View File
@@ -0,0 +1,5 @@
export enum Tab {
All = 'all',
Published = 'published',
Draft = 'draft',
};
+36
View File
@@ -0,0 +1,36 @@
import { useState, useEffect } from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { DashboardCommand } from '../DashboardCommand';
import { DashboardMessage } from '../DashboardMessage';
import { Page } from '../models/Page';
const vscode = MessageHelper.getVsCodeAPI();
export default function useMessages(options?: any) {
const [loading, setLoading] = useState<boolean>(false);
const [pages, setPages] = useState<Page[]>([]);
window.addEventListener('message', event => {
const message = event.data;
switch (message.command) {
case DashboardCommand.loading:
setLoading(message.data);
break;
case DashboardCommand.data:
setPages(message.data);
setLoading(false);
break;
}
});
useEffect(() => {
setLoading(true);
vscode.postMessage({ command: DashboardMessage.getData });
}, ['']);
return {
loading,
pages
};
}
+14
View File
@@ -0,0 +1,14 @@
import * as React from "react";
import { render } from "react-dom";
import { Dashboard } from "./components/Dashboard";
import './styles.css';
declare const acquireVsCodeApi: <T = unknown>() => {
getState: () => T;
setState: (data: T) => void;
postMessage: (msg: unknown) => void;
};
const elm = document.querySelector("#app");
render(<Dashboard />, elm);
+17
View File
@@ -0,0 +1,17 @@
import { Uri } from "vscode";
export interface Page {
fmGroup: string;
fmFilePath: string;
fmFileName: string;
fmModified: number;
title: string;
slug: string;
date: string | Date;
draft: string;
description: string;
preview?: string;
[prop: string]: any;
}
+19
View File
@@ -0,0 +1,19 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
.loader {
border-top-color: var(--vscode-activityBar-activeBorder);;
animation: spinner 1.5s linear infinite;
}
@-webkit-keyframes spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
+1 -1
View File
@@ -1,7 +1,7 @@
import * as React from 'react';
import { FolderInfo, PanelSettings } from '../../models';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../helper/MessageHelper';
import { MessageHelper } from '../../helpers/MessageHelper';
import { Collapsible } from './Collapsible';
import { GlobalSettings } from './GlobalSettings';
import { OtherActions } from './OtherActions';
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from 'react';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../helper/MessageHelper';
import { MessageHelper } from '../../helpers/MessageHelper';
import { ActionButton } from './ActionButton';
export interface ICustomScriptProps {
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from 'react';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../helper/MessageHelper';
import { MessageHelper } from '../../helpers/MessageHelper';
import { ActionButton } from './ActionButton';
export interface IDateActionProps {}
+1 -1
View File
@@ -1,7 +1,7 @@
import * as React from 'react';
import { FileInfo } from '../../models';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../helper/MessageHelper';
import { MessageHelper } from '../../helpers/MessageHelper';
import { FileIcon } from './Icons/FileIcon';
import { MarkdownIcon } from './Icons/MarkdownIcon';
import { VsLabel } from './VscodeComponents';
+1 -1
View File
@@ -1,7 +1,7 @@
import * as React from 'react';
import { PanelSettings } from '../../models';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../helper/MessageHelper';
import { MessageHelper } from '../../helpers/MessageHelper';
import { useDebounce } from '../hooks/useDebounce';
import { Collapsible } from './Collapsible';
import { VsCheckbox, VsLabel } from './VscodeComponents';
+1 -1
View File
@@ -1,7 +1,7 @@
import * as React from 'react';
import { PanelSettings } from '../../models';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../helper/MessageHelper';
import { MessageHelper } from '../../helpers/MessageHelper';
import { TagType } from '../TagType';
import { Collapsible } from './Collapsible';
import { Toggle } from './Fields/Toggle';
+1 -1
View File
@@ -1,7 +1,7 @@
import * as React from 'react';
import { PanelSettings } from '../../models';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../helper/MessageHelper';
import { MessageHelper } from '../../helpers/MessageHelper';
import { Collapsible } from './Collapsible';
import { BugIcon } from './Icons/BugIcon';
import { CenterIcon } from './Icons/CenterIcon';
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../helper/MessageHelper';
import { ActionButton } from './ActionButton';
export interface IPreviewProps {
+1 -1
View File
@@ -1,8 +1,8 @@
import * as React from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../helper/MessageHelper';
import { ActionButton } from './ActionButton';
export interface IPublishActionProps {
+1 -1
View File
@@ -1,8 +1,8 @@
import * as React from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { SlugHelper } from '../../helpers/SlugHelper';
import { Slug } from '../../models/PanelSettings';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../helper/MessageHelper';
import { ActionButton } from './ActionButton';
export interface ISlugActionProps {
+1 -1
View File
@@ -3,10 +3,10 @@ import { Tags } from './Tags';
import { usePrevious } from '../hooks/usePrevious';
import { CommandToCode } from '../CommandToCode';
import { TagType } from '../TagType';
import { MessageHelper } from '../helper/MessageHelper';
import Downshift from 'downshift';
import { AddIcon } from './Icons/AddIcon';
import { VsLabel } from './VscodeComponents';
import { MessageHelper } from '../../helpers/MessageHelper';
export interface ITagPickerProps {
type: string;
+1 -1
View File
@@ -1,8 +1,8 @@
import { useState, useEffect } from 'react';
import { MessageHelper } from '../../helpers/MessageHelper';
import { FolderInfo, PanelSettings } from '../../models/PanelSettings';
import { Command } from '../Command';
import { CommandToCode } from '../CommandToCode';
import { MessageHelper } from '../helper/MessageHelper';
import { TagType } from '../TagType';
const vscode = MessageHelper.getVsCodeAPI();
-2
View File
@@ -2,8 +2,6 @@ import * as React from "react";
import { render } from "react-dom";
import { ViewPanel } from "./ViewPanel";
import './styles.css';
// require('@vscode/codicons/dist/codicon.css');
import '@bendera/vscode-webview-elements/dist/vscode-table';
import '@bendera/vscode-webview-elements/dist/vscode-table-header';
-3
View File
@@ -1,3 +0,0 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
+6 -29
View File
@@ -18,7 +18,10 @@ import { Notifications } from '../helpers/Notifications';
import { COMMAND_NAME } from '../constants/Extension';
import { Folders } from '../commands/Folders';
import { Preview } from '../commands/Preview';
import { getNonce } from '../helpers/getNonce';
import { openFileInEditor } from '../helpers/openFileInEditor';
const FILE_LIMIT = 10;
export class ExplorerView implements WebviewViewProvider, Disposable {
public static readonly viewType = "frontMatter.explorer";
@@ -170,7 +173,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
this.updatePreviewUrl(msg.data || "");
break;
case CommandToCode.openInEditor:
this.openFileInEditor(msg.data);
openFileInEditor(msg.data);
break;
case CommandToCode.updateMetadata:
this.updateMetadata(msg.data);
@@ -253,20 +256,6 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
ArticleHelper.update(editor, article);
}
/**
* Open the file via its path
*/
private async openFileInEditor(filePath: string) {
if (filePath) {
try {
const doc = await workspace.openTextDocument(Uri.file(filePath));
await window.showTextDocument(doc, 1, false);
} catch (e) {
Notifications.error(`Couldn't open the file.`);
}
}
}
/**
* Run a custom script
* @param msg
@@ -348,7 +337,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
public async getFoldersAndFiles() {
this.postWebviewMessage({
command: Command.folderInfo,
data: await Folders.getInfo() || null
data: await Folders.getInfo(FILE_LIMIT) || null
});
}
@@ -530,18 +519,6 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
this.panel!.webview.postMessage(msg);
}
/**
* Generate a unique nonce
*/
private getNonce() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
/**
* Retrieve the webview HTML contents
* @param webView
@@ -552,7 +529,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
const stylesUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'assets/media', 'styles.css'));
const scriptUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'dist', 'viewpanel.js'));
const nonce = this.getNonce();
const nonce = getNonce();
return `
<!DOCTYPE html>
+104 -1
View File
@@ -1,9 +1,112 @@
const colors = require('tailwindcss/colors');
module.exports = {
mode: 'jit',
purge: ['./src/**/*.{js,jsx,ts,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
extend: {
colors: {
white: colors.white,
gray: colors.trueGray,
"red": {
"50": "#ff7c7b",
"100": "#ff7271",
"200": "#ff6867",
"300": "#ff5e5d",
"400": "#ff5453",
"500": "#fe4a49",
"600": "#f4403f",
"700": "#ea3635",
"800": "#e02c2b",
"900": "#d62221"
},
"blue": {
"50": "#90dfff",
"100": "#86d5ff",
"200": "#7ccbff",
"300": "#72c1ff",
"400": "#68b7fc",
"500": "#5eadf2",
"600": "#54a3e8",
"700": "#4a99de",
"800": "#408fd4",
"900": "#3685ca"
},
"teal": {
"50": "#47f4fd",
"100": "#3deaf3",
"200": "#33e0e9",
"300": "#29d6df",
"400": "#1fccd5",
"500": "#15c2cb",
"600": "#0bb8c1",
"700": "#01aeb7",
"800": "#00a4ad",
"900": "#009aa3"
},
"aqua": {
"50": "#76ffff",
"100": "#6cfffa",
"200": "#62fff0",
"300": "#58ffe6",
"400": "#4effdc",
"500": "#44ffd2",
"600": "#3af5c8",
"700": "#30ebbe",
"800": "#26e1b4",
"900": "#1cd7aa"
},
"yellow": {
"50": "#ffff90",
"100": "#ffff86",
"200": "#ffff7c",
"300": "#fff872",
"400": "#ffee68",
"500": "#ffe45e",
"600": "#f5da54",
"700": "#ebd04a",
"800": "#e1c640",
"900": "#d7bc36"
},
"whisper": {
"50": "#ffffff",
"100": "#ffffff",
"200": "#ffffff",
"300": "#ffffff",
"400": "#fdf9ff",
"500": "#f3eff5",
"600": "#e9e5eb",
"700": "#dfdbe1",
"800": "#d5d1d7",
"900": "#cbc7cd"
},
"vulcan": {
"50": "#404551",
"100": "#363b47",
"200": "#2c313d",
"300": "#222733",
"400": "#181d29",
"500": "#0e131f",
"600": "#040915",
"700": "#00000b",
"800": "#000001",
"900": "#000000"
},
"rose": {
"50": "#ff73da",
"100": "#ff69d0",
"200": "#ff5fc6",
"300": "#ff55bc",
"400": "#fb4bb2",
"500": "#f141a8",
"600": "#e7379e",
"700": "#dd2d94",
"800": "#d3238a",
"900": "#c91980"
}
}
},
},
variants: {
extend: {},
+28
View File
@@ -48,6 +48,34 @@ module.exports = [
resolve: {
extensions: ['.ts', '.js', '.tsx', '.jsx']
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: [{
loader: 'ts-loader'
}]
}
]
},
performance: {
maxEntrypointSize: 400000,
maxAssetSize: 400000
}
},
{
name: 'pagesView',
target: 'web',
entry: './src/pagesView/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'pages.js'
},
devtool: 'source-map',
resolve: {
extensions: ['.ts', '.js', '.tsx', '.jsx']
},
module: {
rules: [
{