feat(#221): change TagInput binding to Tag[], add color dots and hierarchy grouping

Backend:
- TagRepository: add findDescendantIdsByName() recursive CTE query
- TagService: add expandTagNamesToDescendantIdSets() for document search

Frontend:
- TagInput: accept Tag[] (id, name, color, parentId) instead of string[]
- Chips show color dot via var(--c-tag-{color}) when tag has color
- Suggestions grouped hierarchically: children indented under their parents
- Update DescriptionSection, edit/new pages, SearchFilterBar, +page.svelte

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-16 16:11:38 +02:00
parent e4f21bd896
commit e8e54cc282
10 changed files with 158 additions and 39 deletions

View File

@@ -20,7 +20,9 @@ let from = $state(untrack(() => data.filters?.from || ''));
let to = $state(untrack(() => data.filters?.to || ''));
let senderId = $state(untrack(() => data.filters?.senderId || ''));
let receiverId = $state(untrack(() => data.filters?.receiverId || ''));
let tagNames = $state<string[]>(untrack(() => data.filters?.tags || []));
let tagNames = $state<{ name: string; id?: string; color?: string; parentId?: string }[]>(
untrack(() => (data.filters?.tags || []).map((name: string) => ({ name })))
);
let sort = $state(untrack(() => data.filters?.sort || 'DATE'));
let dir = $state(untrack(() => data.filters?.dir || 'desc'));
let tagQ = $state(untrack(() => data.filters?.tagQ || ''));
@@ -43,7 +45,7 @@ function triggerSearch() {
if (to) params.set('to', to);
if (senderId) params.set('senderId', senderId);
if (receiverId) params.set('receiverId', receiverId);
if (tagNames) tagNames.forEach((tag) => params.append('tag', tag));
if (tagNames) tagNames.forEach((tag) => params.append('tag', tag.name));
if (sort) params.set('sort', sort);
if (dir) params.set('dir', dir);
if (tagQ) params.set('tagQ', tagQ);
@@ -56,9 +58,9 @@ function handleTextSearch() {
}
// Trigger search when tags change
let prevTagStr = untrack(() => tagNames.join(','));
let prevTagStr = untrack(() => tagNames.map((t) => t.name).join(','));
$effect(() => {
const cur = tagNames.join(',');
const cur = tagNames.map((t) => t.name).join(',');
if (cur !== prevTagStr) {
prevTagStr = cur;
triggerSearch();
@@ -73,7 +75,7 @@ $effect(() => {
to = data.filters?.to || '';
senderId = data.filters?.senderId || '';
receiverId = data.filters?.receiverId || '';
tagNames = data.filters?.tags || [];
tagNames = (data.filters?.tags || []).map((name: string) => ({ name }));
sort = data.filters?.sort || 'DATE';
dir = data.filters?.dir || 'desc';
tagQ = data.filters?.tagQ || '';