diff --git a/README.md b/README.md index 1f2bd6a..1fbdcdd 100644 --- a/README.md +++ b/README.md @@ -57,15 +57,19 @@ columns: ... ``` -Filters can contain the same rules as columns, except for the `rootNotebookPath` property. This defines the notebook from which notes are displayed on the board. By default, it is the parent notebook of the config note, but you can set it to anything. It's a `/` separated path so with a notebook structure like +Filters can contain the same rules as columns, and more. Here's the list of supported rules apart from [columns](#columns) rules: -``` -Parent/ -├─ Nested Parent/ -│ ├─ Kanban board/ -``` +* `rootNotebookPath: ...` This defines the notebook from which notes are displayed on the board. By default, it is the parent notebook of the config note, but you can set it to anything. It's a `/` separated path so with a notebook structure like + + ``` + Parent/ + ├─ Nested Parent/ + │ ├─ Kanban board/ + ``` + + To give the path to `Kanban board` you should write `"Parent/Nested Parent/Kanban board"` -To give the path to `Kanban board` you should write `"Parent/Nested Parent/Kanban board"` +* `-tag: ...` Exclude note if the note has the given tag. You can also use `-tags` to define a list tags. To edit the filters via the config dialog, click the gear icon next to the board name. diff --git a/src/board.ts b/src/board.ts index 8d17498..fd4a021 100644 --- a/src/board.ts +++ b/src/board.ts @@ -1,6 +1,6 @@ import { getNotebookPath, getNoteById } from "./noteData"; -import rules from "./rules"; +import { rules, filtersRules } from "./rules"; import { parseConfigNote } from "./parser"; import { Action } from "./actions"; import { @@ -135,8 +135,8 @@ export default class Board { for (const key in configObj.filters) { let val = configObj.filters[key]; if (typeof val === "boolean") val = `${val}`; - if (val && key in rules) { - const rule = await rules[key](val, rootNotebookPath, configObj); + if (val && key in filtersRules) { + const rule = await filtersRules[key](val, rootNotebookPath, configObj); this.baseFilters.push(rule); if (key === "tag") this.hiddenTags.push(val as string); else if (key === "tags") diff --git a/src/configui/useConfig.tsx b/src/configui/useConfig.tsx index f562559..0ebf394 100644 --- a/src/configui/useConfig.tsx +++ b/src/configui/useConfig.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import * as yaml from "js-yaml"; import type { Config, RuleValue } from "../types"; +import { toggleSingularPlural } from "../utils"; export default function (editedPath: string, inputConfig: Config) { const [editedKey, colIdxStr = null] = editedPath.split(".", 2) as [ @@ -28,20 +29,22 @@ export default function (editedPath: string, inputConfig: Config) { }); useEffect(() => { - if ("tag" in editedObj) { + if ("tag" in editedObj || "-tag" in editedObj) { + const thisProp = "tag" in editedObj ? "tag" : ("-tag" in editedObj ? "-tag" : ""); + const pluralProp = toggleSingularPlural(thisProp); setEditedObj((obj) => { const filteredEntries = Object.entries(obj).filter( - ([k]) => k !== "tag" + ([k]) => k !== thisProp ); const newObj = Object.fromEntries(filteredEntries) as typeof obj; - (newObj as any).tags = [ - editedObj.tag as string, - ...((editedObj?.tags as string[]) || []), + (newObj as any)[pluralProp] = [ + editedObj[thisProp] as string, + ...((editedObj?.[pluralProp] as string[]) || []), ]; return newObj; }); } - }, ["tag" in editedObj]); + }, ["tag" in editedObj, "-tag" in editedObj]); const isBacklog = "backlog" in editedObj && editedObj.backlog; useEffect(() => { @@ -57,9 +60,14 @@ export default function (editedPath: string, inputConfig: Config) { }, [isBacklog, Object.keys(editedObj).length]); const outObjEntries = Object.entries(editedObj) - .map(([prop, val]) => - prop === "tags" && val.length === 1 ? ["tag", val[0]] : [prop, val] - ) + .map(([prop, val]) => { + if ((prop === "tags" || prop === "-tags") && val.length === 1) { + const thisProp = prop === "tags" ? "tags" : (prop === "-tags" ? "-tags" : ""); + const singularProp = toggleSingularPlural(thisProp); + return [singularProp, val[0]]; + } + return [prop, val] + }) .filter(([_, val]) => val !== "" && val !== null && val !== []); const outObj = Object.fromEntries(outObjEntries); const outConf = diff --git a/src/parser.ts b/src/parser.ts index a12ceb4..2ebefd9 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,5 +1,5 @@ import * as yaml from "js-yaml"; -import rules from "./rules"; +import { rules, filtersRules } from "./rules"; import { Config, Message } from "./types"; const configRegex = /([\s\S]*?)```kanban([\s\S]*?)```([\s\S]*)/; @@ -56,7 +56,7 @@ export const validateConfig = (config: Config | {} | null): Message | null => { if (typeof config.filters !== "object" || Array.isArray(config.filters)) return configErr("Filters has to contain a dictionary of rules"); for (const key in config.filters) { - if (!(key in rules) && key !== "rootNotebookPath") + if (!(key in filtersRules) && key !== "rootNotebookPath") return configErr(`Invalid rule type "${key}" in filters`); } } diff --git a/src/rules.ts b/src/rules.ts index 48ac1c5..3810d38 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -16,7 +16,7 @@ type RuleFactory = ( /** * This map contains all supported rules. */ -const rules: Record = { +export const rules: Record = { async tag(arg: string | string[]) { const tagName = Array.isArray(arg) ? arg[0] : arg; const tagID = (await getTagId(tagName)) || (await createTag(tagName)); @@ -139,9 +139,37 @@ const rules: Record = { }, }; +const _filtersRules: Record = { + "-tag": async (arg: string | string[]) => { + const tagName = Array.isArray(arg) ? arg[0] : arg; + + const ruleObj = await rules.tag(arg, "", {} as Config); + ruleObj.name = "-tag"; + ruleObj.filterNote = (note: NoteData) => !note.tags.includes(tagName); + return ruleObj; + }, + + "-tags": async (tagNames: string | string[], rootNbPath: string, config: Config) => { + if (!Array.isArray(tagNames)) tagNames = [tagNames]; + const tagRules = await Promise.all( + tagNames.map((t) => filtersRules["-tag"](t, rootNbPath, config)) + ); + return { + name: "-tags", + filterNote: (note: NoteData) => + tagRules.every(({ filterNote }) => filterNote(note)), + set: (noteId: string) => tagRules.flatMap(({ set }) => set(noteId)), + unset: (noteId: string) => tagRules.flatMap(({ unset }) => unset(noteId)), + editorType: "text", + }; + }, +} +export const filtersRules: Record = Object.assign({}, rules, _filtersRules); + const editorTypes = { filters: { tags: "tags", + "-tags": "tags", rootNotebookPath: "notebook", completed: "checkbox", }, @@ -158,5 +186,3 @@ export const getRuleEditorTypes = (targetPath: string) => { if (targetPath.startsWith("column")) return editorTypes.columns; throw new Error(`Unkown target path ${targetPath}`); }; - -export default rules; diff --git a/src/utils.ts b/src/utils.ts index 0aa5368..0dcea68 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,2 +1,5 @@ export const capitalize = (s: string) => - s.replace(/^\w/, (c) => c.toUpperCase()); + s.replace(/^-?\w/, (c) => c.toUpperCase()); + +export const toggleSingularPlural = (s: string) => + s.endsWith("s") ? s.slice(0, -1) : s + "s";