fix(#221): resolve inherited color on child tags in document responses
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m51s
CI / Backend Unit Tests (push) Failing after 2m46s

Colors are stored only on root-level tags. DocumentService now calls
TagService.resolveEffectiveColors() before returning search results and
single-document responses, so child tags carry their parent's color when
serialised to JSON. Parent tags are batch-loaded in a single query.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-16 19:28:21 +02:00
parent 171f06da22
commit 06fd5ae2da
3 changed files with 115 additions and 5 deletions

View File

@@ -319,12 +319,12 @@ public class DocumentService {
if (sort == DocumentSort.RECEIVER) {
List<Document> results = documentRepository.findAll(spec);
List<Document> sorted = sortByFirstReceiver(results, dir);
return DocumentSearchResult.withMatchData(sorted, enrichWithMatchData(sorted, text));
return DocumentSearchResult.withMatchData(resolveDocumentTagColors(sorted), enrichWithMatchData(sorted, text));
}
if (sort == DocumentSort.SENDER) {
List<Document> results = documentRepository.findAll(spec);
List<Document> sorted = sortBySender(results, dir);
return DocumentSearchResult.withMatchData(sorted, enrichWithMatchData(sorted, text));
return DocumentSearchResult.withMatchData(resolveDocumentTagColors(sorted), enrichWithMatchData(sorted, text));
}
// RELEVANCE: default when text present and no explicit sort given
@@ -337,12 +337,12 @@ public class DocumentService {
.sorted(Comparator.comparingInt(
doc -> rankMap.getOrDefault(doc.getId(), Integer.MAX_VALUE)))
.toList();
return DocumentSearchResult.withMatchData(sorted, enrichWithMatchData(sorted, text));
return DocumentSearchResult.withMatchData(resolveDocumentTagColors(sorted), enrichWithMatchData(sorted, text));
}
Sort springSort = resolveSort(sort, dir);
List<Document> results = documentRepository.findAll(spec, springSort);
return DocumentSearchResult.withMatchData(results, enrichWithMatchData(results, text));
return DocumentSearchResult.withMatchData(resolveDocumentTagColors(results), enrichWithMatchData(results, text));
}
private Sort resolveSort(DocumentSort sort, String dir) {
@@ -433,8 +433,10 @@ public class DocumentService {
}
public Document getDocumentById(UUID id) {
return documentRepository.findById(id)
Document doc = documentRepository.findById(id)
.orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id));
tagService.resolveEffectiveColors(doc.getTags());
return doc;
}
public List<Document> getDocumentsWithoutVersions() {
@@ -513,6 +515,12 @@ public class DocumentService {
// ─── private helpers ──────────────────────────────────────────────────────
private List<Document> resolveDocumentTagColors(List<Document> docs) {
List<Tag> allTags = docs.stream().flatMap(d -> d.getTags().stream()).toList();
tagService.resolveEffectiveColors(allTags);
return docs;
}
private static String stripExtension(String filename) {
if (filename == null) return null;
int dot = filename.lastIndexOf('.');

View File

@@ -1,12 +1,14 @@
package org.raddatz.familienarchiv.service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.raddatz.familienarchiv.dto.TagTreeNodeDTO;
import org.raddatz.familienarchiv.dto.TagUpdateDTO;
@@ -73,6 +75,36 @@ public class TagService {
tagRepository.delete(getById(id));
}
/**
* Sets the effective (inherited) color on child tags that have no color of their own.
* Colors are stored only on root-level tags; children inherit the parent's color.
* Parent tags are batch-loaded in a single query. Safe to call on detached entities.
*/
public void resolveEffectiveColors(Collection<Tag> tags) {
if (tags == null || tags.isEmpty()) return;
Set<UUID> parentIdsNeeded = tags.stream()
.filter(t -> t.getColor() == null && t.getParentId() != null)
.map(Tag::getParentId)
.collect(Collectors.toSet());
if (parentIdsNeeded.isEmpty()) return;
Map<UUID, String> parentColors = tagRepository.findAllById(parentIdsNeeded)
.stream()
.filter(p -> p.getColor() != null)
.collect(Collectors.toMap(Tag::getId, Tag::getColor));
tags.forEach(tag -> {
if (tag.getColor() == null && tag.getParentId() != null) {
String resolved = parentColors.get(tag.getParentId());
if (resolved != null) {
tag.setColor(resolved);
}
}
});
}
/**
* 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.