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

@@ -34,4 +34,21 @@ public interface TagRepository extends JpaRepository<Tag, UUID> {
SELECT parent_id FROM ancestors
""", nativeQuery = true)
List<UUID> findAncestorIds(@Param("tagId") UUID tagId);
/**
* Returns the IDs of the tag with the given name AND all of its descendants
* via a recursive CTE. Used to expand a selected tag to inclusive hierarchy results.
* Includes a depth guard of 50 levels to prevent runaway queries.
*/
@Query(value = """
WITH RECURSIVE descendants AS (
SELECT id, 0 AS depth FROM tag WHERE LOWER(name) = LOWER(:name)
UNION ALL
SELECT t.id, d.depth + 1 FROM tag t
JOIN descendants d ON t.parent_id = d.id
WHERE d.depth < 50
)
SELECT id FROM descendants
""", nativeQuery = true)
List<UUID> findDescendantIdsByName(@Param("name") String name);
}

View File

@@ -2,6 +2,7 @@ package org.raddatz.familienarchiv.service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -16,6 +17,7 @@ import org.raddatz.familienarchiv.repository.TagRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ResponseStatusException;
import lombok.RequiredArgsConstructor;
@@ -71,6 +73,18 @@ public class TagService {
tagRepository.delete(getById(id));
}
/**
* For each tag name, returns the set of that tag's ID plus all descendant IDs.
* Used by DocumentService to expand selected filter tags before applying AND/OR logic.
*/
public List<Set<UUID>> expandTagNamesToDescendantIdSets(List<String> tagNames) {
if (tagNames == null || tagNames.isEmpty()) return List.of();
return tagNames.stream()
.filter(StringUtils::hasText)
.map(name -> (Set<UUID>) new HashSet<>(tagRepository.findDescendantIdsByName(name.trim())))
.toList();
}
/**
* Returns all tags assembled into a tree. Document counts are not included here
* (they are populated by the controller layer if needed, or set to 0).