#749 - Filter setting

This commit is contained in:
Elio Struyf
2024-02-02 18:01:36 +02:00
parent cc2c878c5c
commit 4a4db839ab
15 changed files with 209 additions and 55 deletions
+1
View File
@@ -5,6 +5,7 @@
### ✨ New features
- [#731](https://github.com/estruyf/vscode-front-matter/issues/731): Added the ability to map/unmap taxonomy to multiple pages at once
- [#749](https://github.com/estruyf/vscode-front-matter/issues/749): Ability to set your own filters on the content dashboard with the `frontMatter.content.filters` setting
### 🎨 Enhancements
+23
View File
@@ -492,6 +492,29 @@
"markdownDescription": "%setting.frontMatter.content.wysiwyg.markdownDescription%",
"scope": "Content"
},
"frontMatter.content.filters": {
"type": "array",
"default": [
"pageFolders", "tags", "categories"
],
"markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%",
"items": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"title": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
]
},
"frontMatter.custom.scripts": {
"type": "array",
"default": [],
+1
View File
@@ -92,6 +92,7 @@
"setting.frontMatter.content.sorting.items.properties.type.description": "Type of the field value",
"setting.frontMatter.content.supportedFileTypes.markdownDescription": "Specify the file types that you want to use in Front Matter. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.supportedfiletypes)",
"setting.frontMatter.content.wysiwyg.markdownDescription": "Specifies if you want to enable/disable the What You See, Is What You Get (WYSIWYG) markdown controls. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.wysiwyg)",
"setting.frontMatter.content.filters.markdownDescription": "Specify the filters you want to use for your content dashboard. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.filters)",
"setting.frontMatter.custom.scripts.markdownDescription": "Specify the path to a Node.js script to execute. The current file path will be provided as an argument. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.custom.scripts)",
"setting.frontMatter.custom.scripts.items.properties.id.description": "ID of the script.",
"setting.frontMatter.custom.scripts.items.properties.title.description": "Title you want to give to your script. Will be shown as the title of the button.",
+1
View File
@@ -60,6 +60,7 @@ export const SETTING_CONTENT_STATIC_FOLDER = 'content.publicFolder';
export const SETTING_CONTENT_FRONTMATTER_HIGHLIGHT = 'content.fmHighlight';
export const SETTING_CONTENT_DRAFT_FIELD = 'content.draftField';
export const SETTING_CONTENT_SORTING = 'content.sorting';
export const SETTING_CONTENT_FILTERS = 'content.filters';
export const SETTING_CONTENT_WYSIWYG = 'content.wysiwyg';
export const SETTING_CONTENT_PLACEHOLDERS = 'content.placeholders';
export const SETTING_CONTENT_SNIPPETS = 'content.snippets';
@@ -11,10 +11,11 @@ import {
TagAtom,
CategoryAtom,
DEFAULT_TAG_STATE,
DEFAULT_CATEGORY_STATE
DEFAULT_CATEGORY_STATE,
FiltersAtom
} from '../../state';
import { DefaultValue } from 'recoil';
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
@@ -33,11 +34,13 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (
const folder = useRecoilValue(FolderSelector);
const tag = useRecoilValue(TagSelector);
const category = useRecoilValue(CategorySelector);
const filters = useRecoilValue(FiltersAtom);
const resetSorting = useResetRecoilState(SortingAtom);
const resetFolder = useResetRecoilState(FolderAtom);
const resetTag = useResetRecoilState(TagAtom);
const resetCategory = useResetRecoilState(CategoryAtom);
const resetFilters = useResetRecoilState(FiltersAtom);
const reset = () => {
setShow(false);
@@ -45,19 +48,26 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (
resetFolder();
resetTag();
resetCategory();
resetFilters();
};
const hasCustomFilters = useMemo(() => {
const names = Object.keys(filters);
return names.some((name) => filters[name]);
}, [filters]);
useEffect(() => {
if (
folder !== DEFAULT_FOLDER_STATE ||
tag !== DEFAULT_TAG_STATE ||
category !== DEFAULT_CATEGORY_STATE
category !== DEFAULT_CATEGORY_STATE ||
hasCustomFilters
) {
setShow(true);
} else {
setShow(false);
}
}, [folder, tag, category]);
}, [folder, tag, category, hasCustomFilters]);
if (!show) return null;
@@ -0,0 +1,108 @@
import * as React from 'react';
import { FoldersFilter } from './FoldersFilter';
import { Filter } from './Filter';
import { useRecoilState, useRecoilValue } from 'recoil';
import { CategoryAtom, SettingsSelector, TagAtom, FiltersAtom, FilterValuesAtom } from '../../state';
import { useEffect, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { firstToUpper } from '../../../helpers/StringHelpers';
export interface IFiltersProps { }
export const Filters: React.FunctionComponent<IFiltersProps> = (_: React.PropsWithChildren<IFiltersProps>) => {
const [crntFilters, setCrntFilters] = useRecoilState(FiltersAtom);
const [crntTag, setCrntTag] = useRecoilState(TagAtom);
const [crntCategory, setCrntCategory] = useRecoilState(CategoryAtom);
const filterValues = useRecoilValue(FilterValuesAtom);
const settings = useRecoilValue(SettingsSelector);
const location = useLocation();
const otherFilters = useMemo(() => settings?.filters?.filter((filter) => filter !== "pageFolders" && filter !== "tags" && filter !== "categories"), [settings?.filters]);
const otherFilterValues = useMemo(() => {
return otherFilters?.map((filter) => {
const filterName = typeof filter === "string" ? filter : filter.name;
const filterTitle = typeof filter === "string" ? firstToUpper(filter) : filter.title;
const values = filterValues?.[filterName];
if (!values || values.length === 0) {
return null;
}
return (
<Filter
key={filterName}
label={filterTitle}
activeItem={crntFilters[filterName]}
items={values}
onClick={(value) => setCrntFilters((prev) => {
let clone = Object.assign({}, prev);
if (!clone[filterName] && value) {
clone[filterName] = value;
} else {
clone[filterName] = value || "";
}
return clone;
})}
/>
)
})
}, [otherFilters, crntFilters, filterValues, setCrntFilters]);
useEffect(() => {
if (location.search) {
const searchParams = new URLSearchParams(location.search);
const taxonomy = searchParams.get('taxonomy');
const value = searchParams.get('value');
if (taxonomy && value) {
if (taxonomy === 'tags') {
setCrntTag(value);
} else if (taxonomy === 'categories') {
setCrntCategory(value);
}
}
return;
}
setCrntFilters({});
setCrntTag('');
setCrntCategory('');
}, [location.search]);
return (
<>
{
settings?.filters?.includes("pageFolders") && (
<FoldersFilter />
)
}
{
settings?.filters?.includes("tags") && (
<Filter
label={`Tag`}
activeItem={crntTag}
items={settings?.tags || []}
onClick={(value) => setCrntTag(value)}
/>
)
}
{
settings?.filters?.includes("categories") && (
<Filter
label={`Category`}
activeItem={crntCategory}
items={settings?.categories || []}
onClick={(value) => setCrntCategory(value)}
/>
)
}
{otherFilterValues}
</>
);
};
@@ -6,11 +6,11 @@ import { MenuButton, MenuItem, MenuItems } from '../Menu';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
export interface IFoldersProps { }
export interface IFoldersFilterProps { }
export const Folders: React.FunctionComponent<
IFoldersProps
> = ({ }: React.PropsWithChildren<IFoldersProps>) => {
export const FoldersFilter: React.FunctionComponent<
IFoldersFilterProps
> = ({ }: React.PropsWithChildren<IFoldersFilterProps>) => {
const DEFAULT_TYPE = l10n.t(LocalizationKey.dashboardHeaderFoldersDefault);
const [crntFolder, setCrntFolder] = useRecoilState(FolderAtom);
const settings = useRecoilValue(SettingsSelector);
@@ -1,14 +1,12 @@
import * as React from 'react';
import { Sorting } from './Sorting';
import { Searchbox } from './Searchbox';
import { Filter } from './Filter';
import { Folders } from './Folders';
import { Settings, NavigationType } from '../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { Grouping } from '.';
import { ViewSwitch } from './ViewSwitch';
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil';
import { CategoryAtom, GroupingSelector, SortingAtom, TagAtom } from '../../state';
import { useRecoilValue, useResetRecoilState } from 'recoil';
import { GroupingSelector, SortingAtom } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { ClearFilters } from './ClearFilters';
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
@@ -33,6 +31,7 @@ import { LocalizationKey } from '../../../localization';
import { SettingsLink } from '../SettingsView/SettingsLink';
import { Link } from '../Common/Link';
import { SPONSOR_LINK } from '../../../constants';
import { Filters } from './Filters';
export interface IHeaderProps {
header?: React.ReactNode;
@@ -50,8 +49,6 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
totalPages,
settings
}: React.PropsWithChildren<IHeaderProps>) => {
const [crntTag, setCrntTag] = useRecoilState(TagAtom);
const [crntCategory, setCrntCategory] = useRecoilState(CategoryAtom);
const grouping = useRecoilValue(GroupingSelector);
const resetSorting = useResetRecoilState(SortingAtom);
const location = useLocation();
@@ -123,27 +120,6 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
return [];
}, [settings?.dashboardState?.contents?.templatesEnabled]);
useEffect(() => {
if (location.search) {
const searchParams = new URLSearchParams(location.search);
const taxonomy = searchParams.get('taxonomy');
const value = searchParams.get('value');
if (taxonomy && value) {
if (taxonomy === 'tags') {
setCrntTag(value);
} else if (taxonomy === 'categories') {
setCrntCategory(value);
}
}
return;
}
setCrntTag('');
setCrntCategory('');
}, [location.search]);
return (
<div className={`w-full sticky top-0 z-20 bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)]`}>
<div className={`mb-0 border-b flex justify-between bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)] border-[var(--frontmatter-border)]`}>
@@ -214,21 +190,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
>
<ClearFilters />
<Folders />
<Filter
label={`Tag`}
activeItem={crntTag}
items={settings?.tags || []}
onClick={(value) => setCrntTag(value)}
/>
<Filter
label={`Category`}
activeItem={crntCategory}
items={settings?.categories || []}
onClick={(value) => setCrntCategory(value)}
/>
<Filters />
<Grouping />
@@ -1,5 +1,5 @@
export * from './Filter';
export * from './Folders';
export * from './FoldersFilter';
export * from './Grouping';
export * from './Header';
export * from './Searchbox';
+34 -3
View File
@@ -5,6 +5,8 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import {
AllPagesAtom,
CategorySelector,
FilterValuesAtom,
FiltersAtom,
FolderSelector,
SearchSelector,
SettingsSelector,
@@ -26,12 +28,14 @@ export default function usePages(pages: Page[]) {
const [sortedPages, setSortedPages] = useState<Page[]>([]);
const [sorting, setSorting] = useRecoilState(SortingAtom);
const [tabInfo, setTabInfo] = useRecoilState(TabInfoAtom);
const [, setFilterValues] = useRecoilState(FilterValuesAtom);
const settings = useRecoilValue(SettingsSelector);
const tab = useRecoilValue(TabSelector);
const folder = useRecoilValue(FolderSelector);
const search = useRecoilValue(SearchSelector);
const tag = useRecoilValue(TagSelector);
const category = useRecoilValue(CategorySelector);
const filters = useRecoilValue(FiltersAtom);
/**
* Process all the pages by applying the sorting, filtering and searching.
@@ -86,9 +90,19 @@ export default function usePages(pages: Page[]) {
);
}
const filterNames = Object.keys(filters);
if (filterNames.length > 0) {
for (const filter of filterNames) {
const filterValue = filters[filter];
if (filterValue) {
pagesSorted = pagesSorted.filter((page) => page[filter] === filterValue);
}
}
}
setSortedPages(pagesSorted);
},
[settings, tab, folder, search, tag, category, sorting, tabInfo]
[settings, tab, folder, search, tag, category, sorting, tabInfo, filters]
);
/**
@@ -159,10 +173,27 @@ export default function usePages(pages: Page[]) {
// Set the tab information
setTabInfo(draftTypes);
if (Object.keys(filters).length === 0) {
const availableFilters = (settings?.filters || []).filter((f) => f !== 'pageFolders' && f !== 'tags' && f !== 'categories');
if (availableFilters.length > 0) {
const allFilters: { [filter: string]: string[]; } = {};
for (const filter of availableFilters) {
if (filter) {
const filterName = typeof filter === 'string' ? filter : filter.name;
const values = crntPages.map((page) => page[filterName]).filter((value) => value);
allFilters[filterName] = [...new Set(values)];
}
}
setFilterValues(allFilters);
} else {
setFilterValues({});
}
}
// Set the pages
setPageItems(crntPages);
},
[tab, tabInfo, settings]
[tab, tabInfo, settings, filters]
);
/**
@@ -204,7 +235,7 @@ export default function usePages(pages: Page[]) {
} else {
startPageProcessing();
}
}, [settings?.draftField, pages, sorting, search, tag, category, folder]);
}, [settings?.draftField, pages, sorting, search, tag, category, filters, folder]);
useEffect(() => {
processByTab(sortedPages);
+1
View File
@@ -37,6 +37,7 @@ export interface Settings {
framework: Framework | null | undefined;
draftField: DraftField | null | undefined;
customSorting: SortingSetting[] | undefined;
filters: (string | { title: string; name: string })[] | undefined;
dashboardState: DashboardState;
scripts: CustomScript[];
dataFiles: DataFile[] | undefined;
@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const FilterValuesAtom = atom<{ [filter: string]: string[] }>({
key: 'FilterValuesAtom',
default: {}
});
@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const FiltersAtom = atom<{ [filter: string]: string }>({
key: 'FiltersAtom',
default: {}
});
+3
View File
@@ -3,6 +3,8 @@ export * from './AllPagesAtom';
export * from './AllStaticFoldersAtom';
export * from './CategoryAtom';
export * from './DashboardViewAtom';
export * from './FilterValuesAtom';
export * from './FiltersAtom';
export * from './FolderAtom';
export * from './GroupingAtom';
export * from './LightboxAtom';
@@ -11,6 +13,7 @@ export * from './MediaFoldersAtom';
export * from './MediaTotalAtom';
export * from './ModeAtom';
export * from './PageAtom';
export * from './PinnedItems';
export * from './SearchAtom';
export * from './SearchReadyAtom';
export * from './SelectedMediaFolderAtom';
+2 -1
View File
@@ -7,6 +7,7 @@ import {
CONTEXT,
ExtensionState,
SETTING_CONTENT_DRAFT_FIELD,
SETTING_CONTENT_FILTERS,
SETTING_CONTENT_SORTING,
SETTING_CONTENT_SORTING_DEFAULT,
SETTING_DASHBOARD_OPENONSTART,
@@ -16,7 +17,6 @@ import {
SETTING_FRAMEWORK_ID,
SETTING_MEDIA_SORTING_DEFAULT,
SETTING_CUSTOM_SCRIPTS,
SETTING_TAXONOMY_CONTENT_TYPES,
SETTING_CONTENT_SNIPPETS,
SETTING_DATE_FORMAT,
SETTING_DASHBOARD_CONTENT_TAGS,
@@ -106,6 +106,7 @@ export class DashboardSettings {
draftField: Settings.get<DraftField>(SETTING_CONTENT_DRAFT_FIELD),
customSorting: Settings.get<SortingSetting[]>(SETTING_CONTENT_SORTING),
contentFolders: Folders.get(),
filters: Settings.get<string[]>(SETTING_CONTENT_FILTERS),
crntFramework: Settings.get<string>(SETTING_FRAMEWORK_ID),
framework: !isInitialized && wsFolder ? await FrameworkDetector.get(wsFolder.fsPath) : null,
scripts: Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || [],