From e877847b7ecec842a7c98e93e471570b8cc93b35 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 7 May 2026 17:55:02 +0200 Subject: [PATCH] feat(dashboard): add commentPreview to ActivityFeedItemDTO; wire via findDataByIds() ActivityFeedItemDTO gains a nullable commentPreview field (plain-text, 120 chars max). DashboardService.getActivity() now calls findDataByIds() once instead of findAnnotationIdsByIds(), halving DB round-trips for the Chronik page load. Empty-string previews are normalised to null so the frontend can use ?? cleanly. Co-Authored-By: Claude Sonnet 4.6 --- .../dashboard/ActivityFeedItemDTO.java | 8 ++- .../dashboard/DashboardService.java | 13 ++-- .../dashboard/DashboardServiceTest.java | 65 +++++++++++++++++-- 3 files changed, 77 insertions(+), 9 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 b7767d15..bcd24bdb 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/dashboard/ActivityFeedItemDTO.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/dashboard/ActivityFeedItemDTO.java @@ -29,5 +29,11 @@ public record ActivityFeedItemDTO( requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Annotation associated with the comment; populated only for COMMENT_ADDED and MENTION_CREATED kinds." ) - UUID annotationId + UUID annotationId, + @Nullable + @Schema( + requiredMode = Schema.RequiredMode.NOT_REQUIRED, + description = "Plain-text preview of the comment body (HTML stripped server-side, truncated to 120 chars); null for non-comment feed items or deleted comments." + ) + String commentPreview ) {} 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 3b1300fd..5b5e03a1 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardService.java @@ -12,6 +12,7 @@ import org.raddatz.familienarchiv.document.Document; import org.raddatz.familienarchiv.person.Person; import org.raddatz.familienarchiv.document.transcription.TranscriptionBlock; import org.raddatz.familienarchiv.document.comment.CommentService; +import org.raddatz.familienarchiv.document.comment.CommentService.CommentData; import org.raddatz.familienarchiv.document.DocumentService; import org.raddatz.familienarchiv.document.transcription.TranscriptionService; import org.raddatz.familienarchiv.user.UserService; @@ -133,9 +134,9 @@ public class DashboardService { .filter(Objects::nonNull) .distinct() .toList(); - Map annotationByComment = commentIds.isEmpty() + Map commentDataByComment = commentIds.isEmpty() ? Map.of() - : commentService.findAnnotationIdsByIds(commentIds); + : commentService.findDataByIds(commentIds); return rows.stream().map(row -> { ActivityActorDTO actor = row.getActorId() != null @@ -146,7 +147,10 @@ public class DashboardService { ? row.getHappenedAtUntil().atOffset(ZoneOffset.UTC) : null; UUID commentId = row.getCommentId(); - UUID annotationId = commentId != null ? annotationByComment.get(commentId) : null; + CommentData commentData = commentId != null ? commentDataByComment.get(commentId) : null; + UUID annotationId = commentData != null ? commentData.annotationId() : null; + String commentPreview = commentData != null && !commentData.preview().isEmpty() + ? commentData.preview() : null; return new ActivityFeedItemDTO( org.raddatz.familienarchiv.audit.AuditKind.valueOf(row.getKind()), actor, @@ -158,7 +162,8 @@ public class DashboardService { row.getCount(), happenedAtUntil, commentId, - annotationId + annotationId, + commentPreview ); }).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 472bbaf3..ab790806 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/DashboardServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/DashboardServiceTest.java @@ -13,6 +13,7 @@ import org.raddatz.familienarchiv.user.AppUser; import org.raddatz.familienarchiv.document.Document; import org.raddatz.familienarchiv.document.transcription.TranscriptionBlock; import org.raddatz.familienarchiv.document.comment.CommentService; +import org.raddatz.familienarchiv.document.comment.CommentService.CommentData; import org.raddatz.familienarchiv.document.DocumentService; import org.raddatz.familienarchiv.document.transcription.TranscriptionService; import org.raddatz.familienarchiv.user.UserService; @@ -142,7 +143,8 @@ class DashboardServiceTest { 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()); + when(commentService.findDataByIds(List.of(commentId))) + .thenReturn(Map.of(commentId, new CommentData(null, "preview text"))); List items = dashboardService.getActivity(userId, 5, AuditKind.ROLLUP_ELIGIBLE); @@ -162,8 +164,8 @@ class DashboardServiceTest { 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)); + when(commentService.findDataByIds(List.of(commentId))) + .thenReturn(Map.of(commentId, new CommentData(annotationId, "preview text"))); List items = dashboardService.getActivity(userId, 5, AuditKind.ROLLUP_ELIGIBLE); @@ -187,7 +189,62 @@ class DashboardServiceTest { assertThat(items).hasSize(1); assertThat(items.get(0).commentId()).isNull(); assertThat(items.get(0).annotationId()).isNull(); - verify(commentService, never()).findAnnotationIdsByIds(anyList()); + verify(commentService, never()).findDataByIds(anyList()); + } + + // ─── getActivity commentPreview ─────────────────────────────────────────── + + @Test + void getActivity_populates_commentPreview_for_COMMENT_ADDED_rows() { + UUID userId = UUID.randomUUID(); + UUID docId = UUID.randomUUID(); + UUID commentId = UUID.randomUUID(); + + ActivityFeedRow row = mockFeedRow(docId, "COMMENT_ADDED", commentId); + when(auditLogQueryService.findActivityFeed(userId, 5, AuditKind.ROLLUP_ELIGIBLE)).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.findDataByIds(List.of(commentId))) + .thenReturn(Map.of(commentId, new CommentData(null, "Hello family!"))); + + List items = dashboardService.getActivity(userId, 5, AuditKind.ROLLUP_ELIGIBLE); + + assertThat(items.get(0).commentPreview()).isEqualTo("Hello family!"); + } + + @Test + void getActivity_leaves_commentPreview_null_for_TEXT_SAVED_rows() { + UUID userId = UUID.randomUUID(); + UUID docId = UUID.randomUUID(); + + ActivityFeedRow row = mockFeedRow(docId, "TEXT_SAVED", null); + when(auditLogQueryService.findActivityFeed(userId, 5, AuditKind.ROLLUP_ELIGIBLE)).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, AuditKind.ROLLUP_ELIGIBLE); + + assertThat(items.get(0).commentPreview()).isNull(); + } + + @Test + void getActivity_leaves_commentPreview_null_when_comment_is_deleted() { + UUID userId = UUID.randomUUID(); + UUID docId = UUID.randomUUID(); + UUID deletedCommentId = UUID.randomUUID(); + + ActivityFeedRow row = mockFeedRow(docId, "COMMENT_ADDED", deletedCommentId); + when(auditLogQueryService.findActivityFeed(userId, 5, AuditKind.ROLLUP_ELIGIBLE)).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.findDataByIds(List.of(deletedCommentId))).thenReturn(Map.of()); + + List items = dashboardService.getActivity(userId, 5, AuditKind.ROLLUP_ELIGIBLE); + + assertThat(items.get(0).commentPreview()).isNull(); } // ─── getPulse — always uses full ROLLUP_ELIGIBLE set ─────────────────────