[Notifications] Activity Center UI
>>> [!note] Migrated issue <!-- Drupal.org comment --> <!-- Migrated from issue #3580215. --> Reported by: [justafish](https://www.drupal.org/user/161058) Related to !840 >>> <p>Build the Activity Center dropdown UI: a bell icon with badge count in the Topbar, a dropdown panel listing notifications with type-specific styling, read/unread indicators, and "Mark all as read" functionality. The entire feature is gated behind <code>drupalSettings.canvas.devMode</code>.</p> <h2>Acceptance criteria</h2> <h3>Bell icon and badge</h3> <ul> <li>A bell icon appears in the Topbar right column, before <code>UnpublishedChanges</code>.</li> <li>The bell icon is only visible when <code>drupalSettings.canvas.devMode</code> is true.</li> <li>A badge displays the count of all unread <code>info</code>, <code>error</code>, and <code>warning</code> notifications.</li> <li>The badge is hidden when the count is 0.</li> <li>Clicking the bell toggles the Activity Center dropdown.</li> <li>When the Activity Center is opened and then closed, all unread <code>success</code> and <code>info</code> notifications are automatically marked as read upon close.</li> </ul> <h3>Activity Center dropdown</h3> <ul> <li>Dropdown panel appears below the bell icon.</li> <li>Header displays "Activity Center" and a "Mark all as read" button.</li> <li>"Mark all as read" calls <code>markNotificationsRead</code> with all unread notification IDs, immediately updating the badge count to 0.</li> <li>Dropdown closes when clicking outside it.</li> <li>An empty state is displayed when no notifications exist.</li> </ul> <h3>Notification cards</h3> <ul> <li>Each card displays: left colour border, type icon, title, message, timestamp, and optional action links.</li> <li>Background colours by type: Warning: <code>#fefbe9</code> (amber/2); Error: <code>#feebec</code> (red/3); Info/Success/Progress: white.</li> <li>Left border (4px) by type: Warning: <code>#E2A336</code> (amber/8); Error: <code>#E5484D</code> (red/9); Info/Progress: <code>#0090FF</code> (blue/9); Success: <code>#30A46C</code> (green/9).</li> <li>Each type has a unique icon matching its border colour.</li> <li>The processing icon spins continuously via CSS animation.</li> <li>Action links appear as blue text, separated by <code>|</code>.</li> <li>Clicking an action executes the action only &mdash; it does not auto-dismiss or mark the notification as read.</li> <li>Timestamp displays in the bottom-right corner of the card.</li> </ul> <h3>Read/unread indicator</h3> <ul> <li><code>info</code>, <code>warning</code>, and <code>error</code> notifications display a read/unread indicator.</li> <li><code>success</code> and <code>progress</code> notifications do NOT display the indicator.</li> <li>Unread: filled blue dot. Read: outlined grey circle.</li> <li>Clicking the indicator toggles read/unread state.</li> </ul> <h3>Sorting</h3> <ul> <li>Notifications are sorted: processing first, then unread errors, then unread warnings, then everything else chronologically.</li> <li>When an error or warning is marked as read, it drops into chronological order.</li> </ul> <h3>Scrollable notification list</h3> <ul> <li>The notification list is scrollable within the dropdown panel.</li> <li>The dropdown has a max height so it does not overflow the viewport (e.g. <code>min(400px, 60vh)</code>).</li> <li>All notifications returned by the server (up to 25 plus any processing notifications) are rendered in the scrollable list &mdash; no client-side pagination.</li> </ul> <h2>Implementation plan</h2> <h3>File structure</h3> <p>New files under <code>ui/src/features/notifications/</code>:</p> <ul> <li><code>ActivityCenter.tsx</code> &mdash; dropdown panel</li> <li><code>ActivityCenter.module.css</code> &mdash; scoped styles</li> <li><code>NotificationCard.tsx</code> &mdash; individual notification card</li> <li><code>NotificationCard.module.css</code> &mdash; card styles</li> <li><code>NotificationBell.tsx</code> &mdash; bell icon + badge for Topbar</li> <li><code>NotificationBell.module.css</code> &mdash; bell/badge styles</li> </ul> <h3>Topbar integration</h3> <p>In <code>ui/src/components/topbar/Topbar.tsx</code>, add <code>NotificationBell</code> to the right column <code>&amp;lt;Flex&amp;gt;</code>, before <code>&amp;lt;PreviewControls /&amp;gt;</code>, conditionally rendered:</p> <pre> {getCanvasSettings()?.devMode &amp;&amp; &lt;NotificationBell /&gt;} </pre><h3>NotificationBell component</h3> <p>Use Radix <code>Popover</code> for the dropdown (consistent with existing Radix usage). On open, auto-mark <code>success</code> and <code>info</code> notifications as read. Badge count is computed from all unread <code>info</code>, <code>warning</code>, and <code>error</code> notifications. The open/closed state is stored in Redux via <code>setActivityCenterOpen</code>.</p> <h3>CSS design tokens</h3> <p>Use CSS custom properties matching the spec:</p> <pre> --notification-bg-warning: #fefbe9; --notification-bg-error: #feebec; --notification-border-warning: #E2A336; --notification-border-error: #E5484D; --notification-border-info: #0090FF; --notification-border-success: #30A46C; </pre><h2>Tests required</h2> <h3>Component tests (Vitest + React Testing Library)</h3> <p><code>NotificationBell.test.tsx</code>:</p> <ul> <li>Renders bell icon</li> <li>Shows badge with correct count (all unread info/warning/error)</li> <li>Hides badge when count is 0</li> <li>Toggles Activity Center on click</li> <li>Closing panel auto-marks unread success notifications as read</li> <li>Closing panel auto-marks unread info notifications as read</li> <li>Opening panel does NOT auto-mark success/info as read</li> <li>Opening panel does NOT auto-mark warning/error as read</li> </ul> <p><code>ActivityCenter.test.tsx</code>:</p> <ul> <li>Renders "Activity Center" header</li> <li>Renders notification cards</li> <li>Shows empty state when no notifications</li> <li>"Mark all as read" marks all unread notifications</li> <li>Renders all notifications in a scrollable container</li> </ul> <p><code>NotificationCard.test.tsx</code>:</p> <ul> <li>Renders correct background for warning</li> <li>Renders correct background for error</li> <li>Renders correct left border colour</li> <li>Shows read/unread dot for info/warning/error</li> <li>Hides read/unread dot for success/processing</li> <li>Clicking dot toggles read state</li> <li>Renders action links separated by pipe</li> <li>Renders timestamp</li> <li>Processing icon has spin animation</li> </ul> <h3>Playwright E2E tests</h3> <p>File: <code>tests/src/Playwright/notifications.spec.ts</code></p> <p>Notifications are seeded via <code>drush php-eval</code> calls to <code>NotificationService::create()</code>. Tests should use <code>page.waitForResponse</code> or <code>expect.poll</code> to handle the async polling cycle rather than arbitrary timeouts. Tests that validate sorting should seed notifications with controlled timestamps to ensure deterministic order.</p> <p>Test setup installs <code>canvas</code>, <code>canvas_dev_mode</code>, and <code>canvas_e2e_support</code> modules before the suite runs.</p> <p>Test cases:</p> <ul> <li>Bell icon visible with devMode enabled</li> <li>Badge count reflects all unread notifications</li> <li>Badge hidden when no unread notifications exist</li> <li>Activity Center opens on bell click</li> <li>Activity Center closes on outside click</li> <li>Notification cards render with correct styling per type</li> <li>Processing notification shows spinner animation</li> <li>Sorting: processing first, then unread errors, then rest</li> <li>Read/unread dot toggle decrements badge count</li> <li>Mark all as read clears badge and updates all dots</li> <li>Auto-read on panel open: info and success marked as read, error remains unread</li> <li>Key-based replacement: seeding a notification with an existing key removes the previous one</li> <li>Scrollable list renders all seeded notifications</li> <li>Empty state displayed when no notifications exist</li> <li>Feature flag gating: bell icon absent when <code>canvas_dev_mode</code> is uninstalled</li> </ul> <h2>Manual testing steps</h2> <ol> <li>Enable <code>canvas_dev_mode</code>. Create several notifications via Drush (<code>info</code>, <code>warning</code>, <code>error</code>, <code>success</code>, <code>processing</code>). Open Canvas UI. Verify the bell icon appears and the badge count is correct.</li> <li>Click the bell to open the Activity Center. Verify: notifications are sorted correctly (processing at top, then unread errors, unread warnings, then everything else); each card has the correct background colour and left border; the processing card icon is spinning; success and info notifications are automatically marked as read; warning and error notifications remain unread; badge count decrements accordingly.</li> <li>Click the read/unread dot on a notification. Verify the dot changes appearance, the badge decrements, and if the notification was an unread error/warning it moves to the chronological section.</li> <li>Click "Mark all as read". Verify all dots show read state and the badge disappears.</li> <li>Create a warning notification with key <code>sync</code>, then create a processing notification with the same key. Verify only the processing notification is visible in the Activity Center.</li> <li>Create more than 10 notifications. Verify all appear in the dropdown and the list is scrollable.</li> <li>Disable <code>canvas_dev_mode</code> and reload. Verify the bell icon is no longer present.</li> </ol>
issue