Fix status filter: strict tree filtering and URL param handling

- render_tree_node now applies entry_matches_filter directly; parent
  entries no longer bleed through when only a descendant matches
- Filter dropdowns use set_filter() to persist state in URL query params
- apply_search() reads filter state from URL on init and popstate
- build_url() encodes filter state; uses ?status=all for "all statuses"
  to distinguish from absent param (which defaults to open)
- apply_search() treats absent or empty status param as 'open'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 16:14:15 +00:00
parent 40722310ae
commit 8596f539c7

View File

@@ -306,8 +306,8 @@ function make_entry_row(entry, children_map) {
return row; return row;
} }
function render_tree_node(entry, children_map, is_child = false) { function render_tree_node(entry, children_map) {
if (!is_child && !node_or_descendants_match(entry, children_map)) { return null; } if (!entry_matches_filter(entry)) { return null; }
const children = children_map.get(entry.id) ?? []; const children = children_map.get(entry.id) ?? [];
const node = document.createElement('div'); const node = document.createElement('div');
@@ -318,7 +318,8 @@ function render_tree_node(entry, children_map, is_child = false) {
const children_el = document.createElement('div'); const children_el = document.createElement('div');
children_el.className = 'task-children'; children_el.className = 'task-children';
for (const child of children) { for (const child of children) {
children_el.appendChild(render_tree_node(child, children_map, true)); const child_node = render_tree_node(child, children_map);
if (child_node) { children_el.appendChild(child_node); }
} }
node.appendChild(children_el); node.appendChild(children_el);
} }
@@ -409,7 +410,7 @@ function render_entries(container) {
if (val === state.filter_status) { opt.selected = true; } if (val === state.filter_status) { opt.selected = true; }
status_sel.appendChild(opt); status_sel.appendChild(opt);
} }
status_sel.addEventListener('change', () => { state.filter_status = status_sel.value; render(); }); status_sel.addEventListener('change', () => { set_filter('filter_status', status_sel.value); });
filter_bar.appendChild(status_sel); filter_bar.appendChild(status_sel);
const priority_sel = document.createElement('select'); const priority_sel = document.createElement('select');
@@ -419,7 +420,7 @@ function render_entries(container) {
if (val === state.filter_priority) { opt.selected = true; } if (val === state.filter_priority) { opt.selected = true; }
priority_sel.appendChild(opt); priority_sel.appendChild(opt);
} }
priority_sel.addEventListener('change', () => { state.filter_priority = priority_sel.value; render(); }); priority_sel.addEventListener('change', () => { set_filter('filter_priority', priority_sel.value); });
filter_bar.appendChild(priority_sel); filter_bar.appendChild(priority_sel);
const tag_sel = document.createElement('select'); const tag_sel = document.createElement('select');
@@ -443,7 +444,7 @@ function render_entries(container) {
search_input.type = 'text'; search_input.type = 'text';
search_input.placeholder = 'Search…'; search_input.placeholder = 'Search…';
search_input.value = state.search; search_input.value = state.search;
search_input.addEventListener('input', () => { state.search = search_input.value; render(); }); search_input.addEventListener('input', () => { set_filter('search', search_input.value); });
filter_bar.appendChild(search_input); filter_bar.appendChild(search_input);
container.appendChild(filter_bar); container.appendChild(filter_bar);
@@ -464,7 +465,7 @@ function render_entries(container) {
list.appendChild(make_entry_row(entry, children_map)); list.appendChild(make_entry_row(entry, children_map));
} }
} else { } else {
const visible = roots.filter(e => node_or_descendants_match(e, children_map)); const visible = roots.filter(e => entry_matches_filter(e));
if (!visible.length) { if (!visible.length) {
const empty = document.createElement('div'); const empty = document.createElement('div');
empty.className = 'empty-state'; empty.className = 'empty-state';
@@ -646,14 +647,38 @@ function navigate_new(type, parent_id) {
navigate(hash); navigate(hash);
} }
function build_url(hash) {
const params = new URLSearchParams();
if (state.filter_status !== 'open') { params.set('status', state.filter_status || 'all'); }
if (state.filter_priority) { params.set('priority', state.filter_priority); }
if (state.search) { params.set('search', state.search); }
const qs = params.toString();
return (qs ? '?' + qs : '') + '#' + hash;
}
function apply_search() {
const params = new URLSearchParams(location.search);
const s = params.get('status');
state.filter_status = (s === null || s === '') ? 'open' : (s === 'all' ? '' : s);
state.filter_priority = params.get('priority') ?? '';
state.search = params.get('search') ?? '';
}
function navigate(hash) { function navigate(hash) {
history.pushState(null, '', '#' + hash); history.pushState(null, '', build_url(hash));
apply_hash(hash); apply_hash(hash);
render(); render();
} }
function set_filter(key, val) {
state[key] = val;
history.replaceState(null, '', build_url(location.hash.slice(1)));
render();
}
window.addEventListener('popstate', () => { window.addEventListener('popstate', () => {
apply_hash(location.hash.slice(1)); apply_hash(location.hash.slice(1));
apply_search();
render(); render();
}); });
@@ -1188,6 +1213,7 @@ async function init() {
state.entry_types = types_res.entry_types; state.entry_types = types_res.entry_types;
state.entries = entries_res.entries; state.entries = entries_res.entries;
apply_search();
apply_hash(location.hash.slice(1)); apply_hash(location.hash.slice(1));
render(); render();
} }