Support filtering, documentation updates

This commit is contained in:
Elio Struyf
2021-08-26 11:40:18 +02:00
parent 6cce35de6c
commit 975bb10001
26 changed files with 511 additions and 220 deletions
+3 -1
View File
@@ -1,9 +1,11 @@
# Change Log
## [2.6.0]
## [3.0.0]
- [#61](https://github.com/estruyf/vscode-front-matter/issues/61): List of recently modified files
- [#64](https://github.com/estruyf/vscode-front-matter/issues/64): Publish toggle for easier publishing an article
- [#65](https://github.com/estruyf/vscode-front-matter/issues/65): Aggregate articles in draft
- [#66](https://github.com/estruyf/vscode-front-matter/issues/66): New dashboard webview on which you can manage all your content
## [2.5.1] - 2020-08-23
+45 -16
View File
@@ -30,9 +30,13 @@ The extension will automatically verify if your title and description are SEO co
> If you see something missing in your article creation flow, please feel free to reach out.
**Version 3**
In version v3 we introduced the dashboard webview. Which allows you to manage all your markdown pages in one place. This makes it easy to search, filter, sort, and more.
**Version 2**
In version v2.0.0 we released the newly redesigned sidebar panel with improved SEO support. This extension makes it the only extension to manage your Markdown pages for your static sites in Visual Studio Code.
In version v2 we released the re-designed sidebar panel with improved SEO support. This extension makes it the only extension to manage your Markdown pages for your static sites in Visual Studio Code.
<p align="center" style="margin-top: 2rem;">
<a href="https://www.producthunt.com/posts/front-matter?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-front-matter" target="_blank">
@@ -46,6 +50,7 @@ In version v2.0.0 we released the newly redesigned sidebar panel with improved S
<summary>Table of Contents</summary>
<ol>
<li><a href="#markdown-features">Markdown features</a></li>
<li><a href="#dashboard">Dashboard</a></li>
<li><a href="#the-panel">The panel</a></li>
<li><a href="#site-preview">Site preview</a></li>
<li><a href="#custom-actions">Custom actions/scripts</a></li>
@@ -75,6 +80,22 @@ The Front Matter extension tries to make it easy to manage your Markdown pages/c
> **Info**: If you do not want this feature, you can disable it in the extension settings -> `Highlight Front Matter` or by setting the `frontMatter.content.fmHighlight` setting to `false`.
## Dashboard
Managing your Markdown pages has never been easier in VS Code. With the Front Matter dashboard, you will be able to view all your pages and search through them, filter, sort, and much more.
<p align="center">
<img src="./assets/v3.0.0/dashboard.png" alt="Dashboard" style="display: inline-block" />
</p>
In order to start using the dashboard, you will have to let the extension know in which folder(s) it can find your pages. A content folder can be registered or unregistered, by right-clicking on the folder in your VSCode explorer panel and clicking on the `Register folder` or `Unregister folder` menu item.
<p align="center">
<img src="./assets/v2.1.0/register-folder.png" alt="Register/unregister a folder" style="display: inline-block" />
</p>
> **Info**: If you want, you can click on the `Open on startup?` checkbox. This setting will allow the dashboard to automatically open when you launch the project in VS Code. It will only apply to the current project, not for all of them.
## The panel
The Front Matter panel allows you to perform most of the extension actions by just a click on the button and it shows the SEO statuses of your title, description, and more.
@@ -86,7 +107,7 @@ To leverage most of the capabilities of the extension. SEO information and every
When you open the panel and the current file is not a Markdown file, it will contain the following sections:
<p align="center">
<img src="./assets/v2.5.0/baseview.png" alt="Base view" style="display: inline-block" />
<img src="./assets/v3.0.0/baseview.png" alt="Base view" style="display: inline-block" />
</p>
> **Info**: both **Global Settings** and **Other Actions** sections are shown for the base view as when a Markdown file is openend.
@@ -113,14 +134,22 @@ When you open the Front Matter panel on a Markdown file, you get to see the foll
> **Info**: To gain the `open preview` button to show up, you will need to first set the `local preview URL`. You can do this within the `Global Settings` section or by updating the `frontMatter.preview.host` setting.
**Metadata: Keywords, Tags, Categories**
**Metadata**
<p align="center">
<img src="./assets/v2.0.0/metadata.png" alt="Article metadata" style="display: inline-block" />
<img src="./assets/v3.0.0/metadata.png" alt="Article metadata" style="display: inline-block" />
</p>
> **Info**: By default, the tags/categories picker allows you to insert existing and none tags/categories. When you enter a none existing tag/category, the panel shows an add `+` icon in front of that button. This functionality allows you to store this tag/category in your settings. If you want to disable this feature, you can do that by setting the `frontMatter.panel.freeform` setting to `false`.
**Recently Modified**
<p align="center">
<img src="./assets/v3.0.0/recent-files.png" alt="Recently modified files" style="display: inline-block" />
</p>
Navigate quickly to a recently modified file. In the section, the latest 10 modified files get shown. In order to use this functionality, a registered content folder needs to be present. Check [Front Matter: New article from template](#front-matter-new-article-from-template) for more information about how you can register your content folders.
**Other actions**
<p align="center">
@@ -191,17 +220,17 @@ When adding files in the folder, you'll be able to run the `Front Matter: New ar
## Available commands
**Front Matter: Initialize project**
### Front Matter: Initialize project
This command will initialize the project with a template folder and an article template. It makes it easier to get you started with the extension and creating your content.
**Front Matter: Create a template from current file**
### Front Matter: Create a template from current file
This command allows you to create a new template from the current open Markdown file. It will ask you for the name of the template and if you want to keep the current file its content in the template.
> **Info**: The create as template action is also available from the `other actions` section in the Front Matter panel.
**Front Matter: New article from template**
### Front Matter: New article from template
With this command, you can easily create content in your project within the registered folders and provided templates.
@@ -215,7 +244,7 @@ Once you registered a folder and a template has been defined ([how to create a t
> **Info**: The benefit of this command is that you do not need to search the folder in which you want to create a new article/page/... The extension will do it automatically for you.
**Front Matter: Create <tag | category>**
### Front Matter: Create <tag | category>
Creates a new <tag | category> and allows you to include it into your post automatically
@@ -223,35 +252,35 @@ Creates a new <tag | category> and allows you to include it into your post autom
<img src="./assets/create-tag-category.gif" alt="Create tag or category" style="display: inline-block" />
</p>
**Front Matter: Insert <tags | categories>**
### Front Matter: Insert <tags | categories>
Inserts a selected <tags | categories> into the front matter of your article/post/... - When using this command, the Front Matter panel opens and focuses on the specified type.
> **Info**: This experience changed in version `1.11.0`.
**Front Matter: Export all tags & categories to your settings**
### Front Matter: Export all tags & categories to your settings
Export all the already used tags & categories in your articles/posts/... to your user settings.
**Front Matter: Remap or remove tag/category in all articles**
### Front Matter: Remap or remove tag/category in all articles
This command helps you quickly update/remap or delete a tag or category in your markdown files. The extension will ask you to select the taxonomy type (*tag* or *category*), the old taxonomy value, and the new one (leave the input field *blank* to remove the tag/category).
> **Info**: Once the remapping/deleting process completes. Your VSCode settings update with all new taxonomy tags/categories.
**Front Matter: Set current date**
### Front Matter: Set current date
Update the `date` property of the current article/post/... to the current date & time.
**Optional**: if you want, you can specify the date property format by adding your settings' preference. Settings key: `frontMatter.taxonomy.dateFormat`. Check [date-fns formatting](https://date-fns.org/v2.0.1/docs/format) for more information on which patterns you can use.
> **Optional**: if you want, you can specify the date property format by adding your settings' preference. Settings key: `frontMatter.taxonomy.dateFormat`. Check [date-fns formatting](https://date-fns.org/v2.0.1/docs/format) for more information on which patterns you can use.
**Front Matter: Set lastmod date**
### Front Matter: Set lastmod date
Update the `lastmod` (last modified) property of the current article/post/... to the current date & time. By setting the `frontMatter.content.autoUpdateDate` setting, it can be done automatically when performing changes to your markdown files.
> **note**: Uses the same date format settings key as current date: `frontMatter.taxonomy.dateFormat`.
**Front Matter: Generate slug based on article title**
### Front Matter: Generate slug based on article title
This command generates a clean slug for your article. It removes known stop words, punctuations, and special characters.
@@ -266,7 +295,7 @@ You can also specify a prefix and suffix, which can be added to the slug if you
> **Info**: At the moment, the extension only supports English stopwords.
**Front Matter: Preview article**
### Front Matter: Preview article
Opens the site preview for the current article. More information about it can be found in the [site preview](#site-preview) section.
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+6 -1
View File
@@ -1,7 +1,7 @@
{
"name": "vscode-front-matter",
"displayName": "Front Matter",
"description": "Simplifies working with front matter of your articles. Useful extension when you are using a static site generator like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
"description": "An essential Visual Studio Code extension when you want to manage the markdown pages of your static site like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-128x128.png",
"version": "2.5.1",
"preview": false,
@@ -112,6 +112,11 @@
"default": [],
"markdownDescription": "Specify the path to a Node.js script to execute. The current file path will be provided as an argument."
},
"frontMatter.dashboard.openOnStart": {
"type": "boolean",
"default": null,
"description": "Specify if you want to open the dashboard when you start VS Code."
},
"frontMatter.panel.freeform": {
"type": "boolean",
"default": true,
+80 -33
View File
@@ -1,11 +1,9 @@
import { SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_DATE_FIELD, SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, SETTING_SEO_DESCRIPTION_FIELD } from './../constants/settings';
import { SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART } from './../constants/settings';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { join } from "path";
import { ColorThemeKind, commands, env, ThemeColor, Uri, ViewColumn, Webview, WebviewOptions, WebviewPanel, WebviewPanelOptions, window, workspace } from "vscode";
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace } from "vscode";
import { SettingsHelper } from '../helpers';
import { PreviewSettings } from '../models';
import { format } from 'date-fns';
import { CONTEXT } from '../constants/context';
import { TaxonomyType } from '../models';
import { Folders } from './Folders';
import { getNonce } from '../helpers/getNonce';
import { DashboardCommand } from '../pagesView/DashboardCommand';
@@ -14,6 +12,8 @@ import { Page } from '../pagesView/models/Page';
import { openFileInEditor } from '../helpers/openFileInEditor';
import { COMMAND_NAME } from '../constants/Extension';
import { Template } from './Template';
import { Notifications } from '../helpers/Notifications';
import { Settings } from '../pagesView/models/Settings';
export class Dashboard {
@@ -23,15 +23,35 @@ export class Dashboard {
/** 
* Init the dashboard
*/
public static async init() {
const folders = Folders.get();
await commands.executeCommand('setContext', CONTEXT.canOpenDashboard, folders && folders.length > 0);
public static async init(extensionPath: string) {
const config = SettingsHelper.getConfig();
const openOnStartup = config.get(SETTINGS_DASHBOARD_OPENONSTART);
if (openOnStartup) {
Dashboard.open(extensionPath);
}
}
/**
* Open or reveal the dashboard
*/
public static async open(extensionPath: string) {
if (Dashboard.isOpen) {
Dashboard.reveal();
} else {
Dashboard.create(extensionPath);
}
}
/**
* Check if the dashboard is still open
*/
public static get isOpen(): boolean {
return !Dashboard.isDisposed;
}
/**
* Reveal the dashboard if it is open
*/
public static reveal() {
if (Dashboard.webview) {
Dashboard.webview.reveal();
@@ -39,9 +59,9 @@ export class Dashboard {
}
/**
* Open the markdown preview in the editor
* Create the dashboard webview
*/
public static async open(extensionPath: string) {
public static async create(extensionPath: string) {
// Create the preview webview
Dashboard.webview = window.createWebviewPanel(
@@ -84,20 +104,40 @@ export class Dashboard {
case DashboardMessage.createContent:
await commands.executeCommand(COMMAND_NAME.createContent);
break;
case DashboardMessage.updateSetting:
Dashboard.updateSetting(msg.data);
break;
}
});
}
/**
* Retrieve the settings for the dashboard
*/
private static async getSettings() {
Dashboard.postWebviewMessage({
command: DashboardCommand.settings,
data: {
folders: Folders.get(),
initialized: await Template.isInitialized()
}
initialized: await Template.isInitialized(),
tags: SettingsHelper.getTaxonomy(TaxonomyType.Tag),
categories: SettingsHelper.getTaxonomy(TaxonomyType.Category),
openOnStart: SettingsHelper.getConfig().get(SETTINGS_DASHBOARD_OPENONSTART)
} as Settings
});
}
/**
* Update a setting from the dashboard
*/
private static async updateSetting(data: { name: string, value: any }) {
await SettingsHelper.updateSetting(data.name, data.value);
Dashboard.getSettings();
}
/**
* Retrieve all the markdown pages
*/
private static async getPages() {
const config = SettingsHelper.getConfig();
const wsFolders = workspace.workspaceFolders;
@@ -114,29 +154,36 @@ export class Dashboard {
for (const folder of folderInfo) {
for (const file of folder.lastModified) {
if (file.fileName.endsWith(`.md`) || file.fileName.endsWith(`.mdx`)) {
const article = ArticleHelper.getFrontMatterByPath(file.filePath);
try {
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 = {
...article.data,
// FrontMatter properties
fmGroup: folder.title,
fmModified: file.mtime,
fmFilePath: file.filePath,
fmFileName: file.fileName,
// Make sure these are always set
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);
} catch (error) {
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
}
}
}
+58 -29
View File
@@ -111,14 +111,38 @@ export class Folders {
*/
public static getFolderPath(folder: Uri) {
let folderPath = "";
const wsFolder = Folders.getWorkspaceFolder();
if (folder && folder.fsPath) {
folderPath = folder.fsPath;
} else if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) {
folderPath = workspace.workspaceFolders[0].uri.fsPath;
} else if (wsFolder) {
folderPath = wsFolder.fsPath;
}
return folderPath;
}
/**
* Retrieve the workspace folder
*/
public static getWorkspaceFolder(): Uri | undefined {
const folders = workspace.workspaceFolders;
if (folders && folders.length > 0) {
return folders[0].uri;
}
return undefined;
}
/**
* Get the name of the project
*/
public static getProjectFolderName(): string {
const wsFolder = Folders.getWorkspaceFolder();
if (wsFolder) {
// const projectFolder = wsFolder?.fsPath.split('\\').join('/').split('/').pop();
return basename(wsFolder.fsPath);
}
return "";
}
/**
* Get the registered folders information
*/
@@ -129,37 +153,42 @@ export class Folders {
for (const folder of folders) {
try {
const folderPath = Uri.file(folder.fsPath);
const files = await workspace.fs.readDirectory(folderPath);
if (files) {
let fileStats: FileInfo[] = [];
const projectName = Folders.getProjectFolderName();
let projectStart = folder.fsPath.split(projectName).pop();
if (projectStart) {
projectStart = projectStart.startsWith('/') ? projectStart.substr(1) : projectStart;
const mdFiles = await workspace.findFiles(join(projectStart, '**/*.md'));
const mdxFiles = await workspace.findFiles(join(projectStart, '**/*.mdx'));
let files = [...mdFiles, ...mdxFiles];
if (files) {
let fileStats: FileInfo[] = [];
for (const file of files) {
try {
const fileName = file[0];
const filePath = Uri.file(join(folderPath.fsPath, fileName));
const stats = await workspace.fs.stat(filePath);
fileStats.push({
filePath: filePath.fsPath,
fileName,
...stats
});
} catch (error) {
// Skip the file
for (const file of files) {
try {
const fileName = basename(file.fsPath);
const stats = await workspace.fs.stat(file);
fileStats.push({
filePath: file.fsPath,
fileName,
...stats
});
} catch (error) {
// Skip the file
}
}
}
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime);
if (limit) {
fileStats = fileStats.slice(0, limit);
}
fileStats = fileStats.sort((a, b) => b.mtime - a.mtime);
if (limit) {
fileStats = fileStats.slice(0, limit);
}
folderInfo.push({
title: folder.title,
files: files.length,
lastModified: fileStats
});
folderInfo.push({
title: folder.title,
files: files.length,
lastModified: fileStats
});
}
}
} catch (e) {
// Skip the current folder
+3 -1
View File
@@ -35,4 +35,6 @@ 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";
export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
+2 -5
View File
@@ -146,12 +146,9 @@ export async function activate({ subscriptions, extensionUri, extensionPath }: v
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.preview, () => Preview.open(extensionPath) ));
// Pages dashboard
Dashboard.init(extensionPath);
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboard, () => {
if (Dashboard.isOpen) {
Dashboard.reveal();
} else {
Dashboard.open(extensionPath);
}
Dashboard.open(extensionPath);
}));
// Subscribe all commands
+5
View File
@@ -8,6 +8,11 @@ export class SettingsHelper {
return vscode.workspace.getConfiguration(CONFIG_KEY);
}
public static async updateSetting(name: string, value: any) {
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
await config.update(name, value);
}
/**
* Return the taxonomy settings
*
+2 -1
View File
@@ -2,5 +2,6 @@ export enum DashboardMessage {
getData = 'getData',
openFile = 'openFile',
getTheme = 'getTheme',
createContent = 'createContent'
createContent = 'createContent',
updateSetting = 'updateSetting',
}
+19
View File
@@ -0,0 +1,19 @@
import * as React from 'react';
export interface IButtonProps {
disabled?: boolean;
onClick: () => void;
}
export const Button: React.FunctionComponent<IButtonProps> = ({onClick, disabled, children}: React.PropsWithChildren<IButtonProps>) => {
return (
<button
type="button"
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500"
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
+8 -50
View File
@@ -5,68 +5,22 @@ 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';
import usePages from '../hooks/usePages';
export interface IDashboardProps {}
// TODO: Filter by tag / category
const fuseOptions: Fuse.IFuseOptions<Page> = {
keys: [
"title",
"slug",
"description",
"fmFileName"
]
};
export const Dashboard: React.FunctionComponent<IDashboardProps> = ({}: React.PropsWithChildren<IDashboardProps>) => {
const { loading, pages, settings } = useMessages();
const [ tab, setTab ] = React.useState(Tab.All);
const [ sorting, setSorting ] = React.useState(SortOption.LastModified);
const [ group, setGroup ] = React.useState<string | null>(null);
const [ search, setSearch ] = React.useState<string | null>(null);
const [ pageItems, setPageItems ] = React.useState<Page[]>([]);
const [ tag, setTag ] = React.useState<string | null>(null);
const [ category, setCategory ] = React.useState<string | null>(null);
const { pageItems } = usePages(pages, tab, sorting, group, search, tag, category);
useDarkMode();
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);
}
// 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;
}
// 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 (
@@ -77,9 +31,13 @@ export const Dashboard: React.FunctionComponent<IDashboardProps> = ({}: React.Pr
groups={pageGroups}
crntGroup={group}
totalPages={pageItems.length}
crntTag={tag}
crntCategory={category}
switchTab={(tabId: Tab) => setTab(tabId)}
switchSorting={(sortId: SortOption) => setSorting(sortId)}
switchGroup={(groupId: string | null) => setGroup(groupId)}
switchTag={(tagId: string | null) => setTag(tagId)}
switchCategory={(categoryId: string | null) => setCategory(categoryId)}
onSearch={(value: string | null) => setSearch(value)}
settings={settings}
/>
+15 -3
View File
@@ -6,11 +6,23 @@ export interface IDateFieldProps {
}
export const DateField: React.FunctionComponent<IDateFieldProps> = ({value}: React.PropsWithChildren<IDateFieldProps>) => {
const [ dateValue, setDateValue ] = React.useState<string>("");
const parsedValue = typeof value === 'string' ? parseJSON(value) : value;
const dateString = format(parsedValue, 'yyyy-MM-dd');
React.useEffect(() => {
try {
const parsedValue = typeof value === 'string' ? parseJSON(value) : value;
const dateString = format(parsedValue, 'yyyy-MM-dd');
setDateValue(dateString);
} catch (e) {
// Date is invalid
}
}, [value]);
if (!dateValue) {
return null;
}
return (
<span className={`text-vulcan-100 dark:text-whisper-900 text-xs`}>{dateString}</span>
<span className={`text-vulcan-100 dark:text-whisper-900 text-xs`}>{dateValue}</span>
);
};
+48
View File
@@ -0,0 +1,48 @@
import { Menu } from '@headlessui/react';
import * as React from 'react';
import { MenuButton } from './MenuButton';
import { MenuItem } from './MenuItem';
import { MenuItems } from './MenuItems';
export interface IFilterProps {
label: string;
items: string[];
activeItem: string | null;
onClick: (item: string | null) => void;
}
const DEFAULT_VALUE = "No filter";
export const Filter: React.FunctionComponent<IFilterProps> = ({label, activeItem, items, onClick}: React.PropsWithChildren<IFilterProps>) => {
console.log(items);
if (!items || items.length === 0) {
return null;
}
return (
<div className="flex items-center ml-6">
<Menu as="div" className="relative z-10 inline-block text-left">
<MenuButton label={label} title={activeItem || DEFAULT_VALUE} />
<MenuItems>
<MenuItem
title={DEFAULT_VALUE}
value={null}
isCurrent={!!activeItem}
onClick={() => onClick(null)} />
{items.map((option) => (
<MenuItem
key={option}
title={option}
value={option}
isCurrent={option === activeItem}
onClick={() => onClick(option)} />
))}
</MenuItems>
</Menu>
</div>
);
};
+18 -37
View File
@@ -2,7 +2,9 @@ import { Menu, Transition } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/solid';
import * as React from 'react';
import { Fragment } from 'react';
import { MenuButton } from './MenuButton';
import { MenuItem } from './MenuItem';
import { MenuItems } from './MenuItems';
export interface IGroupingProps {
groups: string[];
@@ -20,45 +22,24 @@ export const Grouping: React.FunctionComponent<IGroupingProps> = ({groups, crntG
return (
<div className="flex items-center ml-6">
<Menu as="div" className="relative z-10 inline-block text-left">
<div>
<span className={`text-gray-500 dark:text-whisper-700 mr-2 font-medium`}>Showing:</span>
<Menu.Button className="group inline-flex justify-center text-sm font-medium text-vulcan-500 hover:text-vulcan-600 dark:text-whisper-500 dark:hover:text-whisper-600">
{crntGroup || DEFAULT_TYPE}
<ChevronDownIcon
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500 dark:text-whisper-600 dark:group-hover:text-whisper-700"
aria-hidden="true"
/>
</Menu.Button>
</div>
<MenuButton label={`Showing`} title={crntGroup || DEFAULT_TYPE} />
<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 dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm">
<div className="py-1">
<MenuItem
title={DEFAULT_TYPE}
value={null}
isCurrent={!crntGroup}
onClick={switchGroup} />
<MenuItems>
<MenuItem
title={DEFAULT_TYPE}
value={null}
isCurrent={!crntGroup}
onClick={switchGroup} />
{groups.map((option) => (
<MenuItem
key={option}
title={option}
value={option}
isCurrent={option === crntGroup}
onClick={switchGroup} />
))}
</div>
</Menu.Items>
</Transition>
{groups.map((option) => (
<MenuItem
key={option}
title={option}
value={option}
isCurrent={option === crntGroup}
onClick={switchGroup} />
))}
</MenuItems>
</Menu>
</div>
);
+22 -10
View File
@@ -8,6 +8,9 @@ import { MessageHelper } from '../../helpers/MessageHelper';
import { DashboardMessage } from '../DashboardMessage';
import { Searchbox } from './Searchbox';
import { Settings } from '../models/Settings';
import { Startup } from './Startup';
import { Button } from './Button';
import { Filter } from './Filter';
export interface IHeaderProps {
settings: Settings;
@@ -28,9 +31,17 @@ export interface IHeaderProps {
// Searching
onSearch: (value: string | null) => void;
// Tags
crntTag: string | null;
switchTag: (tag: string | null) => void;
// Categories
crntCategory: string | null;
switchCategory: (category: string | null) => void;
}
export const Header: React.FunctionComponent<IHeaderProps> = ({currentTab, currentSorting, switchSorting, switchTab, totalPages, crntGroup, groups, switchGroup, onSearch, settings}: React.PropsWithChildren<IHeaderProps>) => {
export const Header: React.FunctionComponent<IHeaderProps> = ({currentTab, currentSorting, switchSorting, switchTab, totalPages, crntGroup, groups, switchGroup, onSearch, settings, switchTag, crntTag, switchCategory, crntCategory}: React.PropsWithChildren<IHeaderProps>) => {
const createContent = () => {
MessageHelper.sendMessage(DashboardMessage.createContent);
@@ -40,15 +51,12 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({currentTab, curre
<div className={`mb-6 sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
<div className={`px-4 mb-2 flex items-center justify-between`}>
<Searchbox onSearch={onSearch} />
<button
type="button"
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500"
onClick={createContent}
disabled={!settings.initialized}
>
Create content
</button>
<div className={`flex items-center space-x-4`}>
<Startup settings={settings} />
<Button onClick={createContent} disabled={!settings.initialized}>Create content</Button>
</div>
</div>
<div className="px-4 flex items-center border-b border-gray-200 dark:border-whisper-600">
@@ -56,6 +64,10 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({currentTab, curre
<Grouping crntGroup={crntGroup} groups={groups} switchGroup={switchGroup} />
<Filter label={`Tag filter`} activeItem={crntTag} items={settings.tags} onClick={switchTag} />
<Filter label={`Category filter`} activeItem={crntCategory} items={settings.categories} onClick={switchCategory} />
<Sorting currentSorting={currentSorting} switchSorting={switchSorting} />
</div>
</div>
+23
View File
@@ -0,0 +1,23 @@
import { Menu } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/solid';
import * as React from 'react';
export interface IMenuButtonProps {
label: string;
title: string;
}
export const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({label, title}: React.PropsWithChildren<IMenuButtonProps>) => {
return (
<div>
<span className={`text-gray-500 dark:text-whisper-700 mr-2 font-medium`}>{label}:</span>
<Menu.Button className="group inline-flex justify-center text-sm font-medium text-vulcan-500 hover:text-vulcan-600 dark:text-whisper-500 dark:hover:text-whisper-600">
{title}
<ChevronDownIcon
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500 dark:text-whisper-600 dark:group-hover:text-whisper-700"
aria-hidden="true"
/>
</Menu.Button>
</div>
);
};
+1 -1
View File
@@ -13,7 +13,7 @@ export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value,
<Menu.Item>
<button
onClick={() => onClick(value)}
className={`${!isCurrent ? `text-vulcan-500 dark:text-whisper-500` : `text-gray-500 dark:text-whisper-900`} block px-4 py-2 text-sm font-medium w-full text-left hover:text-gray-700 dark:hover:text-whisper-600`}
className={`${!isCurrent ? `text-vulcan-500 dark:text-whisper-500` : `text-gray-500 dark:text-whisper-900`} block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100`}
>
{title}
</button>
+25
View File
@@ -0,0 +1,25 @@
import { Menu, Transition } from '@headlessui/react';
import * as React from 'react';
import { Fragment } from 'react';
export interface IMenuItemsProps {}
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({children}: React.PropsWithChildren<IMenuItemsProps>) => {
return (
<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 dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto">
<div className="py-1">
{children}
</div>
</Menu.Items>
</Transition>
);
};
+13 -32
View File
@@ -4,6 +4,8 @@ import { SortOption } from '../constants/SortOption';
import { ChevronDownIcon } from '@heroicons/react/solid';
import { Fragment } from 'react';
import { MenuItem } from './MenuItem';
import { MenuItems } from './MenuItems';
import { MenuButton } from './MenuButton';
export interface ISortingProps {
currentSorting: SortOption;
@@ -24,39 +26,18 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({currentSorting,
return (
<div className="flex items-center ml-6">
<Menu as="div" className="relative z-10 inline-block text-left">
<div>
<span className={`text-gray-500 dark:text-whisper-700 mr-2 font-medium`}>Sort by:</span>
<Menu.Button className="group inline-flex justify-center text-sm font-medium text-vulcan-500 hover:text-vulcan-600 dark:text-whisper-500 dark:hover:text-whisper-600">
{crntSort?.name}
<ChevronDownIcon
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500 dark:text-whisper-600 dark:group-hover:text-whisper-700"
aria-hidden="true"
/>
</Menu.Button>
</div>
<MenuButton label={`Sort by`} title={crntSort?.name || ""} />
<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 dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm">
<div className="py-1">
{sortOptions.map((option) => (
<MenuItem
key={option.id}
title={option.name}
value={option.id}
isCurrent={option.id === currentSorting}
onClick={switchSorting} />
))}
</div>
</Menu.Items>
</Transition>
<MenuItems>
{sortOptions.map((option) => (
<MenuItem
key={option.id}
title={option.name}
value={option.id}
isCurrent={option.id === currentSorting}
onClick={switchSorting} />
))}
</MenuItems>
</Menu>
</div>
);
+44
View File
@@ -0,0 +1,44 @@
import * as React from 'react';
import { SETTINGS_DASHBOARD_OPENONSTART } from '../../constants';
import { MessageHelper } from '../../helpers/MessageHelper';
import { DashboardMessage } from '../DashboardMessage';
import { Settings } from '../models/Settings';
export interface IStartupProps {
settings: Settings;
}
export const Startup: React.FunctionComponent<IStartupProps> = ({settings}: React.PropsWithChildren<IStartupProps>) => {
const [isChecked, setIsChecked] = React.useState(false);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsChecked(e.target.checked);
MessageHelper.sendMessage(DashboardMessage.updateSetting, { name: SETTINGS_DASHBOARD_OPENONSTART, value: e.target.checked });
};
React.useEffect(() => {
console.log(`openOnStart`, settings.openOnStart);
setIsChecked(!!settings.openOnStart);
}, [settings?.openOnStart]);
return (
<div className={`relative flex items-start`}>
<div className="flex items-center h-5">
<input
id="startup"
aria-describedby="startup-description"
name="startup"
type="checkbox"
checked={isChecked}
onChange={onChange}
className="focus:outline-none focus:ring-teal-500 h-4 w-4 text-teal-600 border-gray-300 dark:border-vulcan-50 rounded"
/>
</div>
<div className="ml-2 text-sm">
<label id="startup-description" htmlFor="startup" className="font-medium text-vulcan-50 dark:text-whisper-900">
Open on startup?
</label>
</div>
</div>
);
};
+68
View File
@@ -0,0 +1,68 @@
import { useState, useEffect } from 'react';
import { SortOption } from '../constants/SortOption';
import { Tab } from '../constants/Tab';
import { Page } from '../models/Page';
import Fuse from 'fuse.js';
const fuseOptions: Fuse.IFuseOptions<Page> = {
keys: [
"title",
"slug",
"description",
"fmFileName"
]
};
export default function usePages(pages: Page[], tab: Tab, sorting: SortOption, group: string | null, search: string | null, tag: string | null, category: string | null) {
const [ pageItems, setPageItems ] = useState<Page[]>([]);
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);
}
// 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;
}
// 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);
}
// Filter by tag
if (tag) {
pagesSorted = pagesSorted.filter(page => page.tags && page.tags.includes(tag));
}
// Filter by category
if (category) {
pagesSorted = pagesSorted.filter(page => page.categories && page.categories.includes(category));
}
setPageItems(pagesSorted);
}, [ pages, tab, sorting, group, search, tag, category ]);
return {
pageItems
};
}
+3
View File
@@ -3,4 +3,7 @@ import { ContentFolder } from './../../models/ContentFolder';
export interface Settings {
folders: ContentFolder[];
initialized: boolean
tags: string[];
categories: string[];
openOnStart: boolean | null;
}