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
+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;
}