fix(#221): resolve inherited color on child tags in document responses
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:
@@ -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('.');
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -233,6 +234,75 @@ class TagServiceTest {
|
||||
assertThat(tree.get(0).children().get(0).id()).isEqualTo(childId);
|
||||
}
|
||||
|
||||
// ─── resolveEffectiveColors ───────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void resolveEffectiveColors_doesNothing_whenCollectionIsEmpty() {
|
||||
tagService.resolveEffectiveColors(List.of());
|
||||
verifyNoInteractions(tagRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveEffectiveColors_doesNothing_whenAllTagsHaveOwnColor() {
|
||||
Tag tag = Tag.builder().id(UUID.randomUUID()).name("Root").color("sage").build();
|
||||
|
||||
tagService.resolveEffectiveColors(List.of(tag));
|
||||
|
||||
verify(tagRepository, never()).findAllById(any());
|
||||
assertThat(tag.getColor()).isEqualTo("sage");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveEffectiveColors_doesNothing_whenTagHasNoParentAndNoColor() {
|
||||
Tag tag = Tag.builder().id(UUID.randomUUID()).name("Root").build();
|
||||
|
||||
tagService.resolveEffectiveColors(List.of(tag));
|
||||
|
||||
verify(tagRepository, never()).findAllById(any());
|
||||
assertThat(tag.getColor()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveEffectiveColors_setsParentColor_onChildTagWithNoColor() {
|
||||
UUID parentId = UUID.randomUUID();
|
||||
Tag parent = Tag.builder().id(parentId).name("Parent").color("sage").build();
|
||||
Tag child = Tag.builder().id(UUID.randomUUID()).name("Child").parentId(parentId).build();
|
||||
when(tagRepository.findAllById(Set.of(parentId))).thenReturn(List.of(parent));
|
||||
|
||||
tagService.resolveEffectiveColors(List.of(child));
|
||||
|
||||
assertThat(child.getColor()).isEqualTo("sage");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveEffectiveColors_leavesChildUnchanged_whenParentHasNoColor() {
|
||||
UUID parentId = UUID.randomUUID();
|
||||
Tag parent = Tag.builder().id(parentId).name("Parent").build();
|
||||
Tag child = Tag.builder().id(UUID.randomUUID()).name("Child").parentId(parentId).build();
|
||||
when(tagRepository.findAllById(Set.of(parentId))).thenReturn(List.of(parent));
|
||||
|
||||
tagService.resolveEffectiveColors(List.of(child));
|
||||
|
||||
assertThat(child.getColor()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveEffectiveColors_batchLoadsParents_inOneQuery() {
|
||||
UUID parentId1 = UUID.randomUUID();
|
||||
UUID parentId2 = UUID.randomUUID();
|
||||
Tag parent1 = Tag.builder().id(parentId1).name("P1").color("sage").build();
|
||||
Tag parent2 = Tag.builder().id(parentId2).name("P2").color("sienna").build();
|
||||
Tag child1 = Tag.builder().id(UUID.randomUUID()).name("C1").parentId(parentId1).build();
|
||||
Tag child2 = Tag.builder().id(UUID.randomUUID()).name("C2").parentId(parentId2).build();
|
||||
when(tagRepository.findAllById(Set.of(parentId1, parentId2))).thenReturn(List.of(parent1, parent2));
|
||||
|
||||
tagService.resolveEffectiveColors(List.of(child1, child2));
|
||||
|
||||
verify(tagRepository, times(1)).findAllById(any());
|
||||
assertThat(child1.getColor()).isEqualTo("sage");
|
||||
assertThat(child2.getColor()).isEqualTo("sienna");
|
||||
}
|
||||
|
||||
// ─── delete ───────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user