mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-08 01:42:58 +02:00
Support filtering, documentation updates
This commit is contained in:
+80
-33
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,5 +2,6 @@ export enum DashboardMessage {
|
||||
getData = 'getData',
|
||||
openFile = 'openFile',
|
||||
getTheme = 'getTheme',
|
||||
createContent = 'createContent'
|
||||
createContent = 'createContent',
|
||||
updateSetting = 'updateSetting',
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,4 +3,7 @@ import { ContentFolder } from './../../models/ContentFolder';
|
||||
export interface Settings {
|
||||
folders: ContentFolder[];
|
||||
initialized: boolean
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
openOnStart: boolean | null;
|
||||
}
|
||||
Reference in New Issue
Block a user