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