diff --git a/CHANGELOG.md b/CHANGELOG.md index 23813ed2..dd6e33ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package.json b/package.json index 47c622f1..3636f731 100644 --- a/package.json +++ b/package.json @@ -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": [], diff --git a/package.nls.json b/package.nls.json index de0ecc3a..7c22f73a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -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.", diff --git a/src/constants/settings.ts b/src/constants/settings.ts index ff9b7398..95379f7f 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -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'; diff --git a/src/dashboardWebView/components/Header/ClearFilters.tsx b/src/dashboardWebView/components/Header/ClearFilters.tsx index 872979bf..4837af1c 100644 --- a/src/dashboardWebView/components/Header/ClearFilters.tsx +++ b/src/dashboardWebView/components/Header/ClearFilters.tsx @@ -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 = ( 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 = ( 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; diff --git a/src/dashboardWebView/components/Header/Filters.tsx b/src/dashboardWebView/components/Header/Filters.tsx new file mode 100644 index 00000000..12f24cfb --- /dev/null +++ b/src/dashboardWebView/components/Header/Filters.tsx @@ -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 = (_: React.PropsWithChildren) => { + 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 ( + 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") && ( + + ) + } + + { + settings?.filters?.includes("tags") && ( + setCrntTag(value)} + /> + ) + } + + { + settings?.filters?.includes("categories") && ( + setCrntCategory(value)} + /> + ) + } + + {otherFilterValues} + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Header/Folders.tsx b/src/dashboardWebView/components/Header/FoldersFilter.tsx similarity index 89% rename from src/dashboardWebView/components/Header/Folders.tsx rename to src/dashboardWebView/components/Header/FoldersFilter.tsx index e5e0df83..56e482a1 100644 --- a/src/dashboardWebView/components/Header/Folders.tsx +++ b/src/dashboardWebView/components/Header/FoldersFilter.tsx @@ -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) => { +export const FoldersFilter: React.FunctionComponent< + IFoldersFilterProps +> = ({ }: React.PropsWithChildren) => { const DEFAULT_TYPE = l10n.t(LocalizationKey.dashboardHeaderFoldersDefault); const [crntFolder, setCrntFolder] = useRecoilState(FolderAtom); const settings = useRecoilValue(SettingsSelector); diff --git a/src/dashboardWebView/components/Header/Header.tsx b/src/dashboardWebView/components/Header/Header.tsx index 20b3090c..c90e9c3c 100644 --- a/src/dashboardWebView/components/Header/Header.tsx +++ b/src/dashboardWebView/components/Header/Header.tsx @@ -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 = ({ totalPages, settings }: React.PropsWithChildren) => { - 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 = ({ 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 (
@@ -214,21 +190,7 @@ export const Header: React.FunctionComponent = ({ > - - - setCrntTag(value)} - /> - - setCrntCategory(value)} - /> + diff --git a/src/dashboardWebView/components/Header/index.ts b/src/dashboardWebView/components/Header/index.ts index 5c186917..32b55e1c 100644 --- a/src/dashboardWebView/components/Header/index.ts +++ b/src/dashboardWebView/components/Header/index.ts @@ -1,5 +1,5 @@ export * from './Filter'; -export * from './Folders'; +export * from './FoldersFilter'; export * from './Grouping'; export * from './Header'; export * from './Searchbox'; diff --git a/src/dashboardWebView/hooks/usePages.tsx b/src/dashboardWebView/hooks/usePages.tsx index 6ab132ba..85d374a1 100644 --- a/src/dashboardWebView/hooks/usePages.tsx +++ b/src/dashboardWebView/hooks/usePages.tsx @@ -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([]); 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); diff --git a/src/dashboardWebView/models/Settings.ts b/src/dashboardWebView/models/Settings.ts index f5ff7b8d..4d8cbbc4 100644 --- a/src/dashboardWebView/models/Settings.ts +++ b/src/dashboardWebView/models/Settings.ts @@ -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; diff --git a/src/dashboardWebView/state/atom/FilterValuesAtom.ts b/src/dashboardWebView/state/atom/FilterValuesAtom.ts new file mode 100644 index 00000000..5a54f435 --- /dev/null +++ b/src/dashboardWebView/state/atom/FilterValuesAtom.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const FilterValuesAtom = atom<{ [filter: string]: string[] }>({ + key: 'FilterValuesAtom', + default: {} +}); diff --git a/src/dashboardWebView/state/atom/FiltersAtom.ts b/src/dashboardWebView/state/atom/FiltersAtom.ts new file mode 100644 index 00000000..4aa3d3aa --- /dev/null +++ b/src/dashboardWebView/state/atom/FiltersAtom.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const FiltersAtom = atom<{ [filter: string]: string }>({ + key: 'FiltersAtom', + default: {} +}); diff --git a/src/dashboardWebView/state/atom/index.ts b/src/dashboardWebView/state/atom/index.ts index 3fec87d0..caad6272 100644 --- a/src/dashboardWebView/state/atom/index.ts +++ b/src/dashboardWebView/state/atom/index.ts @@ -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'; diff --git a/src/helpers/DashboardSettings.ts b/src/helpers/DashboardSettings.ts index fc277e2c..ca4d1d4b 100644 --- a/src/helpers/DashboardSettings.ts +++ b/src/helpers/DashboardSettings.ts @@ -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(SETTING_CONTENT_DRAFT_FIELD), customSorting: Settings.get(SETTING_CONTENT_SORTING), contentFolders: Folders.get(), + filters: Settings.get(SETTING_CONTENT_FILTERS), crntFramework: Settings.get(SETTING_FRAMEWORK_ID), framework: !isInitialized && wsFolder ? await FrameworkDetector.get(wsFolder.fsPath) : null, scripts: Settings.get(SETTING_CUSTOM_SCRIPTS) || [],