From 29e6e1c05267094c2a42ef77ef88aa202ee2dcdf Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sun, 22 Mar 2026 20:26:05 -0400 Subject: [PATCH] Add PermissionError handling to vault indexer and implement recursive descendant expansion in sidebar tree filter with auto-expand for matching directories --- backend/indexer.py | 3 +++ frontend/app.js | 52 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/backend/indexer.py b/backend/indexer.py index 07a9c91..90b6d8e 100644 --- a/backend/indexer.py +++ b/backend/indexer.py @@ -218,6 +218,9 @@ def _scan_vault(vault_name: str, vault_path: str) -> Dict[str, Any]: for tag in tags: tag_counts[tag] = tag_counts.get(tag, 0) + 1 + except PermissionError as e: + logger.warning(f"Permission denied, skipping {fpath}: {e}") + continue except Exception as e: logger.error(f"Error indexing {fpath}: {e}") continue diff --git a/frontend/app.js b/frontend/app.js index d8adb4a..2fe70bf 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -758,23 +758,31 @@ function filterSidebarTree(query) { const tree = document.getElementById("vault-tree"); const items = tree.querySelectorAll(".tree-item"); + const containers = tree.querySelectorAll(".tree-children"); if (!query) { items.forEach((item) => item.classList.remove("filtered-out")); - tree.querySelectorAll(".tree-children").forEach((c) => c.classList.remove("filtered-out")); + containers.forEach((c) => { + c.classList.remove("filtered-out"); + // Keep current collapsed state when clearing filter + }); return; } // First pass: mark all as filtered out items.forEach((item) => item.classList.add("filtered-out")); - tree.querySelectorAll(".tree-children").forEach((c) => c.classList.add("filtered-out")); + containers.forEach((c) => c.classList.add("filtered-out")); - // Second pass: show matching items and their ancestors + // Second pass: find matching items and mark them + ancestors + descendants + const matchingItems = new Set(); + items.forEach((item) => { const text = sidebarFilterCaseSensitive ? item.textContent : item.textContent.toLowerCase(); const searchQuery = sidebarFilterCaseSensitive ? query : query.toLowerCase(); if (text.includes(searchQuery)) { + matchingItems.add(item); item.classList.remove("filtered-out"); + // Show all ancestor containers let parent = item.parentElement; while (parent && parent !== tree) { @@ -784,8 +792,46 @@ } parent = parent.parentElement; } + + // If this is a directory (has a children container after it), show all descendants + const nextEl = item.nextElementSibling; + if (nextEl && nextEl.classList.contains("tree-children")) { + nextEl.classList.remove("filtered-out"); + nextEl.classList.remove("collapsed"); + // Recursively show all children in this container + showAllDescendants(nextEl); + } } }); + + // Third pass: show items that are descendants of matching directories + // and ensure their containers are visible + matchingItems.forEach((item) => { + const nextEl = item.nextElementSibling; + if (nextEl && nextEl.classList.contains("tree-children")) { + const children = nextEl.querySelectorAll(".tree-item"); + children.forEach((child) => child.classList.remove("filtered-out")); + } + }); + } + + function showAllDescendants(container) { + const items = container.querySelectorAll(".tree-item"); + items.forEach((item) => { + item.classList.remove("filtered-out"); + // If this item has children, also show them + const nextEl = item.nextElementSibling; + if (nextEl && nextEl.classList.contains("tree-children")) { + nextEl.classList.remove("filtered-out"); + nextEl.classList.remove("collapsed"); + } + }); + // Also ensure all nested containers are visible + const nestedContainers = container.querySelectorAll(".tree-children"); + nestedContainers.forEach((c) => { + c.classList.remove("filtered-out"); + c.classList.remove("collapsed"); + }); } function filterTagCloud(query) {