From f50a74661926bbd6580098e4dc8c0b3a2676df05 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 21 Apr 2026 17:11:03 +0200 Subject: [PATCH] feat(dashboard): enrich activity feed DTO with commentId + annotationId ActivityFeedItemDTO gains nullable commentId and annotationId fields. DashboardService.getActivity forwards commentId from the projection and batch-resolves annotationId via the new CommentService.findAnnotationIdsByIds lookup. Both remain null for non-comment kinds, so the bulk lookup is skipped entirely when the feed has no comment rows. Refs #300. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../dashboard/ActivityFeedItemDTO.java | 14 +++- .../dashboard/DashboardService.java | 17 ++++- .../dashboard/DashboardServiceTest.java | 70 ++++++++++++++++++- 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dashboard/ActivityFeedItemDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/dashboard/ActivityFeedItemDTO.java index dbf6a0cd..b7767d15 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/dashboard/ActivityFeedItemDTO.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/dashboard/ActivityFeedItemDTO.java @@ -17,5 +17,17 @@ public record ActivityFeedItemDTO( @Schema(requiredMode = Schema.RequiredMode.REQUIRED) boolean youMentioned, @Schema(requiredMode = Schema.RequiredMode.REQUIRED) boolean youParticipated, @Schema(requiredMode = Schema.RequiredMode.REQUIRED) int count, - @Nullable OffsetDateTime happenedAtUntil + @Nullable OffsetDateTime happenedAtUntil, + @Nullable + @Schema( + requiredMode = Schema.RequiredMode.NOT_REQUIRED, + description = "Deep-link target comment; populated only for COMMENT_ADDED and MENTION_CREATED kinds." + ) + UUID commentId, + @Nullable + @Schema( + requiredMode = Schema.RequiredMode.NOT_REQUIRED, + description = "Annotation associated with the comment; populated only for COMMENT_ADDED and MENTION_CREATED kinds." + ) + UUID annotationId ) {} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardService.java b/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardService.java index 9e2eea75..47f5fa13 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardService.java @@ -10,6 +10,7 @@ import org.raddatz.familienarchiv.model.AppUser; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.Person; import org.raddatz.familienarchiv.model.TranscriptionBlock; +import org.raddatz.familienarchiv.service.CommentService; import org.raddatz.familienarchiv.service.DocumentService; import org.raddatz.familienarchiv.service.TranscriptionService; import org.raddatz.familienarchiv.service.UserService; @@ -32,6 +33,7 @@ public class DashboardService { private final DocumentService documentService; private final TranscriptionService transcriptionService; private final UserService userService; + private final CommentService commentService; public DashboardResumeDTO getResume(UUID userId) { Optional docIdOpt = auditLogQueryService.findMostRecentDocumentForUser(userId); @@ -125,6 +127,15 @@ public class DashboardService { log.warn("Activity: failed to bulk-load document titles", e); } + List commentIds = rows.stream() + .map(ActivityFeedRow::getCommentId) + .filter(Objects::nonNull) + .distinct() + .toList(); + Map annotationByComment = commentIds.isEmpty() + ? Map.of() + : commentService.findAnnotationIdsByIds(commentIds); + return rows.stream().map(row -> { ActivityActorDTO actor = row.getActorId() != null ? new ActivityActorDTO(row.getActorInitials(), row.getActorColor(), row.getActorName()) @@ -133,6 +144,8 @@ public class DashboardService { OffsetDateTime happenedAtUntil = row.getHappenedAtUntil() != null ? row.getHappenedAtUntil().atOffset(ZoneOffset.UTC) : null; + UUID commentId = row.getCommentId(); + UUID annotationId = commentId != null ? annotationByComment.get(commentId) : null; return new ActivityFeedItemDTO( org.raddatz.familienarchiv.audit.AuditKind.valueOf(row.getKind()), actor, @@ -142,7 +155,9 @@ public class DashboardService { row.isYouMentioned(), row.isYouParticipated(), row.getCount(), - happenedAtUntil + happenedAtUntil, + commentId, + annotationId ); }).toList(); } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/DashboardServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/DashboardServiceTest.java index 700b6fca..d453f2e7 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/DashboardServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/DashboardServiceTest.java @@ -10,6 +10,7 @@ import org.raddatz.familienarchiv.audit.AuditLogQueryService; import org.raddatz.familienarchiv.model.AppUser; import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.TranscriptionBlock; +import org.raddatz.familienarchiv.service.CommentService; import org.raddatz.familienarchiv.service.DocumentService; import org.raddatz.familienarchiv.service.TranscriptionService; import org.raddatz.familienarchiv.service.UserService; @@ -17,6 +18,7 @@ import org.raddatz.familienarchiv.service.UserService; import java.time.Instant; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -33,6 +35,7 @@ class DashboardServiceTest { @Mock DocumentService documentService; @Mock TranscriptionService transcriptionService; @Mock UserService userService; + @Mock CommentService commentService; @InjectMocks DashboardService dashboardService; @@ -94,7 +97,72 @@ class DashboardServiceTest { verify(documentService, never()).getDocumentById(docId); } + // ─── getActivity comment/annotation enrichment ──────────────────────────── + + @Test + void getActivity_populatesCommentId_forCommentEvents() { + UUID userId = UUID.randomUUID(); + UUID docId = UUID.randomUUID(); + UUID commentId = UUID.randomUUID(); + + ActivityFeedRow row = mockFeedRow(docId, "COMMENT_ADDED", commentId); + when(auditLogQueryService.findActivityFeed(userId, 5)).thenReturn(List.of(row)); + when(documentService.getDocumentsByIds(List.of(docId))).thenReturn(List.of( + Document.builder().id(docId).title("B").originalFilename("b.pdf").receivers(new HashSet<>()).build() + )); + when(commentService.findAnnotationIdsByIds(List.of(commentId))).thenReturn(Map.of()); + + List items = dashboardService.getActivity(userId, 5); + + assertThat(items).hasSize(1); + assertThat(items.get(0).commentId()).isEqualTo(commentId); + } + + @Test + void getActivity_populatesAnnotationId_viaCommentService() { + UUID userId = UUID.randomUUID(); + UUID docId = UUID.randomUUID(); + UUID commentId = UUID.randomUUID(); + UUID annotationId = UUID.randomUUID(); + + ActivityFeedRow row = mockFeedRow(docId, "COMMENT_ADDED", commentId); + when(auditLogQueryService.findActivityFeed(userId, 5)).thenReturn(List.of(row)); + when(documentService.getDocumentsByIds(List.of(docId))).thenReturn(List.of( + Document.builder().id(docId).title("B").originalFilename("b.pdf").receivers(new HashSet<>()).build() + )); + when(commentService.findAnnotationIdsByIds(List.of(commentId))) + .thenReturn(Map.of(commentId, annotationId)); + + List items = dashboardService.getActivity(userId, 5); + + assertThat(items).hasSize(1); + assertThat(items.get(0).annotationId()).isEqualTo(annotationId); + } + + @Test + void getActivity_leavesBothNull_forNonCommentKinds() { + UUID userId = UUID.randomUUID(); + UUID docId = UUID.randomUUID(); + + ActivityFeedRow row = mockFeedRow(docId, "TEXT_SAVED", null); + when(auditLogQueryService.findActivityFeed(userId, 5)).thenReturn(List.of(row)); + when(documentService.getDocumentsByIds(List.of(docId))).thenReturn(List.of( + Document.builder().id(docId).title("B").originalFilename("b.pdf").receivers(new HashSet<>()).build() + )); + + List items = dashboardService.getActivity(userId, 5); + + assertThat(items).hasSize(1); + assertThat(items.get(0).commentId()).isNull(); + assertThat(items.get(0).annotationId()).isNull(); + verify(commentService, never()).findAnnotationIdsByIds(anyList()); + } + private ActivityFeedRow mockFeedRow(UUID docId, String kind) { + return mockFeedRow(docId, kind, null); + } + + private ActivityFeedRow mockFeedRow(UUID docId, String kind, UUID commentId) { return new ActivityFeedRow() { public String getKind() { return kind; } public UUID getActorId() { return null; } @@ -107,7 +175,7 @@ class DashboardServiceTest { public boolean isYouParticipated() { return false; } public int getCount() { return 1; } public Instant getHappenedAtUntil() { return null; } - public UUID getCommentId() { return null; } + public UUID getCommentId() { return commentId; } }; } }