fix(#145): dashboard notification widget shows all recent notifications
- Add type-only filter to notification repo/service (previously only worked with type+read=false together) - Dashboard widget now fetches all recent notifications (mentions + replies, both read and unread) instead of unread mentions only - Update component heading and show type label per row Root cause: Berit's mentions were read=true, so the unread-only filter returned 0 results. The recent docs widget had no REVIEWED documents because 'marking ready' sets metadata_complete, not status=REVIEWED. Recent docs now shows all uploads without a status filter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,9 @@ public interface NotificationRepository extends JpaRepository<Notification, UUID
|
|||||||
|
|
||||||
Page<Notification> findByRecipientIdOrderByCreatedAtDesc(UUID recipientId, Pageable pageable);
|
Page<Notification> findByRecipientIdOrderByCreatedAtDesc(UUID recipientId, Pageable pageable);
|
||||||
|
|
||||||
|
Page<Notification> findByRecipientIdAndTypeOrderByCreatedAtDesc(
|
||||||
|
UUID recipientId, NotificationType type, Pageable pageable);
|
||||||
|
|
||||||
Page<Notification> findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(
|
Page<Notification> findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(
|
||||||
UUID recipientId, NotificationType type, Pageable pageable);
|
UUID recipientId, NotificationType type, Pageable pageable);
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,10 @@ public class NotificationService {
|
|||||||
return notificationRepository.findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(userId, type, pageable)
|
return notificationRepository.findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(userId, type, pageable)
|
||||||
.map(this::toDTO);
|
.map(this::toDTO);
|
||||||
}
|
}
|
||||||
|
if (type != null) {
|
||||||
|
return notificationRepository.findByRecipientIdAndTypeOrderByCreatedAtDesc(userId, type, pageable)
|
||||||
|
.map(this::toDTO);
|
||||||
|
}
|
||||||
return notificationRepository.findByRecipientIdOrderByCreatedAtDesc(userId, pageable)
|
return notificationRepository.findByRecipientIdOrderByCreatedAtDesc(userId, pageable)
|
||||||
.map(this::toDTO);
|
.map(this::toDTO);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,24 @@ class NotificationRepositoryTest {
|
|||||||
assertThat(result.getTotalElements()).isEqualTo(5);
|
assertThat(result.getTotalElements()).isEqualTo(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── findByRecipientIdAndType (without read filter) ──────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findByType_returnsBothReadAndUnreadMentions() {
|
||||||
|
notificationRepository.save(mention(userA, false)); // unread
|
||||||
|
notificationRepository.save(mention(userA, true)); // read — should also be included
|
||||||
|
notificationRepository.save(reply(userA, false)); // REPLY — excluded
|
||||||
|
notificationRepository.save(mention(userB, false)); // different user — excluded
|
||||||
|
|
||||||
|
Page<Notification> result = notificationRepository
|
||||||
|
.findByRecipientIdAndTypeOrderByCreatedAtDesc(
|
||||||
|
userA.getId(), NotificationType.MENTION, Pageable.ofSize(10));
|
||||||
|
|
||||||
|
assertThat(result.getContent()).hasSize(2);
|
||||||
|
assertThat(result.getContent()).allMatch(n -> n.getType() == NotificationType.MENTION);
|
||||||
|
assertThat(result.getContent()).allMatch(n -> n.getRecipient().getId().equals(userA.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
// ─── helpers ─────────────────────────────────────────────────────────────
|
// ─── helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private Notification mention(AppUser recipient, boolean read) {
|
private Notification mention(AppUser recipient, boolean read) {
|
||||||
|
|||||||
@@ -386,6 +386,21 @@ class NotificationServiceTest {
|
|||||||
verify(notificationRepository, never()).findByRecipientIdOrderByCreatedAtDesc(any(), any());
|
verify(notificationRepository, never()).findByRecipientIdOrderByCreatedAtDesc(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getNotifications_withTypeOnly_usesTypeFilteredRepoMethod() {
|
||||||
|
when(notificationRepository.findByRecipientIdAndTypeOrderByCreatedAtDesc(
|
||||||
|
eq(userA.getId()), eq(NotificationType.MENTION), any()))
|
||||||
|
.thenReturn(Page.empty());
|
||||||
|
|
||||||
|
notificationService.getNotifications(userA.getId(), NotificationType.MENTION, null, Pageable.ofSize(5));
|
||||||
|
|
||||||
|
verify(notificationRepository).findByRecipientIdAndTypeOrderByCreatedAtDesc(
|
||||||
|
eq(userA.getId()), eq(NotificationType.MENTION), any());
|
||||||
|
verify(notificationRepository, never()).findByRecipientIdOrderByCreatedAtDesc(any(), any());
|
||||||
|
verify(notificationRepository, never())
|
||||||
|
.findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
// ─── private helpers ──────────────────────────────────────────────────────
|
// ─── private helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
private DocumentComment commentWithAuthor(UUID id, UUID parentId, UUID authorId, String authorName) {
|
private DocumentComment commentWithAuthor(UUID id, UUID parentId, UUID authorId, String authorName) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ let { mentions }: Props = $props();
|
|||||||
{#if mentions.length > 0}
|
{#if mentions.length > 0}
|
||||||
<div data-testid="dashboard-mentions" class="rounded-sm border border-line bg-surface p-6">
|
<div data-testid="dashboard-mentions" class="rounded-sm border border-line bg-surface p-6">
|
||||||
<h2 class="mb-4 font-sans text-xs font-bold tracking-widest text-gray-400 uppercase">
|
<h2 class="mb-4 font-sans text-xs font-bold tracking-widest text-gray-400 uppercase">
|
||||||
Erwähnungen
|
Benachrichtigungen
|
||||||
</h2>
|
</h2>
|
||||||
{#each mentions as mention (mention.id)}
|
{#each mentions as mention (mention.id)}
|
||||||
<div class="flex items-center gap-3 border-b border-line py-2 last:border-0">
|
<div class="flex items-center gap-3 border-b border-line py-2 last:border-0">
|
||||||
@@ -28,6 +28,9 @@ let { mentions }: Props = $props();
|
|||||||
class="font-serif text-sm text-ink hover:text-ink-2"
|
class="font-serif text-sm text-ink hover:text-ink-2"
|
||||||
>
|
>
|
||||||
{mention.actorName ?? ''}
|
{mention.actorName ?? ''}
|
||||||
|
{#if mention.type === 'MENTION'}<span class="ml-1 font-sans text-xs text-gray-400"
|
||||||
|
>erwähnt Sie</span
|
||||||
|
>{:else}<span class="ml-1 font-sans text-xs text-gray-400">hat geantwortet</span>{/if}
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="font-serif text-sm text-ink">{mention.actorName ?? ''}</span>
|
<span class="font-serif text-sm text-ink">{mention.actorName ?? ''}</span>
|
||||||
|
|||||||
@@ -58,13 +58,9 @@ export async function load({ url, fetch }) {
|
|||||||
|
|
||||||
if (isDashboard) {
|
if (isDashboard) {
|
||||||
const [mentionsResult, incompleteResult, recentResult] = await Promise.allSettled([
|
const [mentionsResult, incompleteResult, recentResult] = await Promise.allSettled([
|
||||||
api.GET('/api/notifications', {
|
api.GET('/api/notifications', { params: { query: { size: 5 } } }),
|
||||||
params: { query: { type: 'MENTION', read: false, size: 5 } }
|
|
||||||
}),
|
|
||||||
api.GET('/api/documents/incomplete', { params: { query: { size: 5 } } }),
|
api.GET('/api/documents/incomplete', { params: { query: { size: 5 } } }),
|
||||||
api.GET('/api/documents/search', {
|
api.GET('/api/documents/search', { params: { query: {} } })
|
||||||
params: { query: { status: 'REVIEWED' } }
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (mentionsResult.status === 'fulfilled' && mentionsResult.value.response.ok) {
|
if (mentionsResult.status === 'fulfilled' && mentionsResult.value.response.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user