UX-25: Notification type differentiation, grouping, and batch actions#646
Conversation
…ypes Extend NotificationType enum with BoardChange and System variants. Add POST /api/notifications/mark-all-read endpoint with optional boardId filter. Wire through service, repository, and NoOp layers. Closes part of #625
…rk-all-read - Type-specific left-border colors: amber (proposal), blue (mention), green (board change), purple (assignment), gray (system) - Type badge with accessible label so color is not sole differentiator - Time-based section headers (Today, Yesterday, This week, Older) - Smart grouping: consecutive same-type notifications collapse into expandable summary cards - "Mark all read" batch action button in header - New composable useNotificationGrouping with pure functions for type normalization, border/badge classes, time grouping, and smart notification grouping - Frontend API and store wired for mark-all-read endpoint Closes #625
- 23 vitest tests for useNotificationGrouping composable covering type normalization, labels, border/badge classes, time grouping, and smart notification collapsing - 10 view tests for NotificationInboxView including Mark all read button visibility, click behavior, time headers, and collapsed group rendering - 2 store tests for markAllRead action and error handling - 4 backend tests for MarkAllAsReadAsync covering success, empty list, forbidden board access, and validation error cases
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Self-Review FindingsAccessibility
Performance
API Backward Compatibility
TypeScript Type Safety
Tailwind Classes
Potential Improvements (not blocking)
No blocking issues found. |
There was a problem hiding this comment.
Code Review
This pull request introduces a 'Mark all as read' feature for notifications, including backend API support, repository methods, and a revamped frontend inbox with time-based grouping and smart collapsing of consecutive notifications. The review identified a critical logic bug in the frontend store where marking all as read ignores the board filter, potentially marking unrelated notifications as read. Additionally, there are concerns regarding performance and memory pressure in the backend implementation, as it currently fetches and iterates over all unread notifications in memory rather than using a bulk update. Improving the user experience by displaying board names instead of raw GUIDs was also suggested.
| notifications.value = notifications.value.map((item) => ({ | ||
| ...item, | ||
| isRead: true, | ||
| readAt: item.readAt ?? new Date().toISOString(), | ||
| })) |
There was a problem hiding this comment.
There is a logic bug here: the markAllRead action updates the local state for all notifications in the store, even if a boardId filter was provided to the API call. This will cause the UI to incorrectly show all notifications as read when the user only intended to mark those for a specific board. The mapping logic should be filtered by boardId.
| notifications.value = notifications.value.map((item) => ({ | |
| ...item, | |
| isRead: true, | |
| readAt: item.readAt ?? new Date().toISOString(), | |
| })) | |
| notifications.value = notifications.value.map((item) => | |
| (!boardId || item.boardId === boardId) && !item.isRead | |
| ? { ...item, isRead: true, readAt: new Date().toISOString() } | |
| : item | |
| ) |
| var unreadNotifications = await _unitOfWork.Notifications.GetUnreadByUserIdAsync( | ||
| userId, boardId, cancellationToken); | ||
|
|
||
| var count = 0; | ||
| foreach (var notification in unreadNotifications) | ||
| { | ||
| notification.MarkAsRead(); | ||
| count++; | ||
| } |
There was a problem hiding this comment.
The current implementation of MarkAllAsReadAsync fetches all unread notifications into memory and iterates over them to update each one individually. For users with a large volume of unread notifications, this can lead to significant memory pressure and performance issues. Consider implementing a bulk update operation in the repository (e.g., using EF Core's ExecuteUpdateAsync) to perform this change directly in the database.
| query = query.Where(n => n.BoardId == boardId.Value); | ||
| } | ||
|
|
||
| return await query.ToListAsync(cancellationToken); |
There was a problem hiding this comment.
Fetching all unread notifications without a limit can be dangerous for performance if a user has accumulated a very large number of notifications. Since this method is primarily used for batch updates, consider if a bulk update approach would be more appropriate, or at least apply a reasonable maximum limit to the query.
| </p> | ||
| <p v-if="activeBoardId" class="td-notifications__board-context"> | ||
| <p v-if="activeBoardId" class="mt-2 text-sm font-semibold text-[color:var(--td-color-primary)]"> | ||
| Showing notifications linked to board {{ activeBoardId }}. |
There was a problem hiding this comment.
Cover both no-scope and board-scoped mark-all-read calls to bring src/api/** branch coverage above the 49% threshold.
Adversarial Code Review -- PR #646Reviewed the full diff (16 files, ~950 additions) across backend and frontend. Below are findings rated by confidence and severity. Critical (90-100)1. Store
|
| # | Severity | Description | Blocking? |
|---|---|---|---|
| 1 | Critical | Store markAllRead ignores boardId filter in local state |
Yes |
| 2 | Critical | CI red -- src/api/** branch coverage below threshold |
Yes |
| 3 | Important | Authorization silently skipped when service is null | No (matches existing pattern) |
| 4 | Important | groupsForHeader O(n*h) in render loop |
No |
| 5 | Important | No user preference controls for new notification types | No (note it) |
| 6 | Important | Timezone-sensitive time grouping | No (document it) |
Verdict: Do not merge. Issues #1 (boardId-aware optimistic update) and #2 (coverage threshold) must be fixed first. The remaining items can be addressed as follow-ups but should at minimum have tracking comments or TODOs.
When a boardId is provided, only mark matching notifications as read locally instead of blindly marking all. Add test for the board-scoped behavior.
Summary
Implements #625 — notification list UX improvements with type differentiation, smart grouping, time-based sections, and batch actions.
Backend
NotificationTypeenum withBoardChangeandSystemvariantsPOST /api/notifications/mark-all-readendpoint with optionalboardIdfilterINotificationService,NotificationService,INotificationRepository,NotificationRepository, andNoOpNotificationServiceFrontend
useNotificationGroupingcomposable with pure functions for type normalization, border/badge classes, time grouping, and smart notification collapsingTest plan
useNotificationGroupingcomposable (type normalization, labels, border/badge classes, time grouping, smart collapsing)NotificationInboxView(Mark all read visibility, click behavior, time headers, collapsed groups, existing navigation tests preserved)markAllReadaction (success and error paths)MarkAllAsReadAsync(success, empty list, forbidden board access, validation error)npm run build)dotnet build -c Release)Closes #625