feat: allow hidden tags, either permanent or when there are no unread messages

This commit is contained in:
Mathy Vanvoorden
2026-07-29 17:18:42 +02:00
parent ca0ba818b7
commit 0c1e238223
10 changed files with 204 additions and 6 deletions
+45
View File
@@ -3,6 +3,8 @@ import {
MAX_KEYWORD_ID_LENGTH,
buildKeywordTree,
composeKeywordId,
countKeywordNodes,
filterKeywordTree,
getParentKeywordId,
hasChildKeywords,
isKeywordDescendant,
@@ -136,3 +138,46 @@ describe("buildKeywordTree", () => {
expect(flat.every((n) => n.depth === 0 && n.children.length === 0)).toBe(true);
});
});
describe("filterKeywordTree", () => {
const tree = buildKeywordTree(KEYWORDS);
it("drops the nodes the predicate rejects", () => {
const kept = filterKeywordTree(tree, (node) => node.id !== "work/personal");
const [work] = kept;
expect(work.children.map((c) => c.id)).toEqual(["work/clients"]);
});
it("keeps a rejected node when a descendant survives, so nothing is stranded", () => {
const kept = filterKeywordTree(tree, (node) => node.id === "work/clients/acme");
const [work] = kept;
expect(work.id).toBe("work");
expect(work.children.map((c) => c.id)).toEqual(["work/clients"]);
expect(work.children[0].children.map((c) => c.id)).toEqual(["work/clients/acme"]);
});
it("keeps the depth of a surviving node so its indentation does not shift", () => {
const kept = filterKeywordTree(tree, (node) => node.id === "work/clients/acme");
expect(kept[0].children[0].children[0].depth).toBe(2);
});
it("returns nothing when the predicate rejects everything", () => {
expect(filterKeywordTree(tree, () => false)).toEqual([]);
});
it("leaves the original tree untouched", () => {
filterKeywordTree(tree, (node) => node.id === "work");
expect(countKeywordNodes(tree)).toBe(4);
});
});
describe("countKeywordNodes", () => {
it("counts every level, not just the roots", () => {
expect(countKeywordNodes(buildKeywordTree(KEYWORDS))).toBe(4);
expect(countKeywordNodes([])).toBe(0);
});
});
+26
View File
@@ -111,3 +111,29 @@ export function buildKeywordTree(keywords: KeywordDefinition[]): KeywordNode[] {
return roots;
}
/**
* Prunes a tag tree down to the nodes worth showing.
*
* A node survives when the predicate accepts it or when any of its descendants
* survives, so hiding a parent never strands the children below it. Depths are
* left untouched: a kept node keeps the indentation of its original level even
* when the level above it is only there to carry it.
*/
export function filterKeywordTree(
nodes: KeywordNode[],
isVisible: (node: KeywordNode) => boolean,
): KeywordNode[] {
const kept: KeywordNode[] = [];
for (const node of nodes) {
const children = filterKeywordTree(node.children, isVisible);
if (children.length > 0 || isVisible(node)) {
kept.push({ ...node, children });
}
}
return kept;
}
/** Total number of nodes in a tag tree, at every level. */
export function countKeywordNodes(nodes: KeywordNode[]): number {
return nodes.reduce((total, node) => total + 1 + countKeywordNodes(node.children), 0);
}