From 4e56dfd3a26afc878a80d79103e1b6dc51bb5f52 Mon Sep 17 00:00:00 2001 From: kitWarse <278602811+kitWarse@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:45:24 +0100 Subject: [PATCH] feat(dashboard): add channel keyword search for issue #497 - Add channelSearch state and ilteredChannels useMemo in NotificationPreferencesPage - Render a labelled search input above the delivery matrix that filters channels by keyword - Show a no-results empty state with a 'Clear search' button when nothing matches - Dynamically update matrix grid-template-columns to match visible channel count - Add accessible CSS styles for .channel-search, .channel-search__input, .channel-search__clear, .channel-search__clear-inline, and .notification-preferences__no-results in index.css Closes #497 --- dashboard/src/index.css | 90 +++++++++++++++++++ .../src/pages/NotificationPreferencesPage.tsx | 75 ++++++++++++++-- 2 files changed, 160 insertions(+), 5 deletions(-) diff --git a/dashboard/src/index.css b/dashboard/src/index.css index 2cfb1ef..e91633c 100644 --- a/dashboard/src/index.css +++ b/dashboard/src/index.css @@ -561,6 +561,96 @@ body { transform: translateX(18px); } +/* ── Issue #497: Channel search ────────────────────────────────────────────── */ +.channel-search { + display: flex; + align-items: center; + gap: 10px; + margin-top: 20px; + margin-bottom: 4px; +} + +.channel-search__label { + flex-shrink: 0; + font-size: 0.9rem; + color: #9aa0a6; + font-weight: 500; +} + +.channel-search__input { + flex: 1; + max-width: 280px; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 10px; + padding: 9px 14px; + background: #12151c; + color: inherit; + font-family: inherit; + font-size: 0.92rem; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.channel-search__input:focus-visible { + outline: none; + border-color: rgba(91, 125, 255, 0.6); + box-shadow: 0 0 0 3px rgba(91, 125, 255, 0.15); +} + +.channel-search__clear { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: #9aa0a6; + cursor: pointer; + font-size: 0.9rem; + line-height: 1; + padding: 6px 10px; + transition: background 0.15s ease, color 0.15s ease; +} + +.channel-search__clear:hover { + background: rgba(255, 255, 255, 0.06); + color: #e8eaed; +} + +.notification-preferences__no-results { + grid-column: 1 / -1; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 32px 20px; + border: 1px dashed rgba(255, 255, 255, 0.12); + border-radius: 14px; + text-align: center; + color: #9aa0a6; + margin-top: 4px; +} + +.notification-preferences__no-results p { + margin: 0; + font-size: 0.97rem; +} + +.notification-preferences__no-results strong { + color: #c2c8d3; +} + +.channel-search__clear-inline { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 8px; + color: #5b7dff; + cursor: pointer; + font-size: 0.88rem; + padding: 6px 14px; + transition: background 0.15s ease; +} + +.channel-search__clear-inline:hover { + background: rgba(91, 125, 255, 0.1); +} + .notification-preferences__control-panel { display: grid; gap: 18px; diff --git a/dashboard/src/pages/NotificationPreferencesPage.tsx b/dashboard/src/pages/NotificationPreferencesPage.tsx index b001146..a9c14dd 100644 --- a/dashboard/src/pages/NotificationPreferencesPage.tsx +++ b/dashboard/src/pages/NotificationPreferencesPage.tsx @@ -1,4 +1,4 @@ -import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; +import { Fragment, useCallback, useEffect, useMemo, useState, useRef } from 'react'; type ChannelKey = 'inApp' | 'email' | 'discord' | 'telegram'; type CategoryKey = 'security' | 'governance' | 'system' | 'custom'; @@ -46,6 +46,17 @@ export function NotificationPreferencesPage() { const [simulateFailure, setSimulateFailure] = useState(false); const [saveState, setSaveState] = useState<'idle' | 'saving' | 'saved'>('idle'); + // ── Issue #497: Channel search ──────────────────────────────────────────── + const [channelSearch, setChannelSearch] = useState(''); + const channelSearchRef = useRef(null); + + /** Channels filtered by the keyword search (case-insensitive). */ + const filteredChannels = useMemo(() => { + const q = channelSearch.trim().toLowerCase(); + if (!q) return channelDefinitions; + return channelDefinitions.filter((ch) => ch.label.toLowerCase().includes(q)); + }, [channelSearch]); + const errorCategories = useMemo( () => categoryDefinitions @@ -210,21 +221,75 @@ export function NotificationPreferencesPage() {

-
+ {/* Issue #497: Channel keyword search */} +
+ + setChannelSearch(e.target.value)} + aria-label="Search available channels" + aria-controls="notification-matrix" + /> + {channelSearch && ( + + )} +
+ +
Categories / Channels
- {channelDefinitions.map((channel) => ( + {filteredChannels.map((channel) => (
{channel.label}
))} - {categoryDefinitions.map((category) => ( + {filteredChannels.length === 0 && ( +
+

No channels match “{channelSearch}”.

+ +
+ )} + + {filteredChannels.length > 0 && categoryDefinitions.map((category) => (
{category.label}

{category.description}

- {channelDefinitions.map((channel) => { + {filteredChannels.map((channel) => { const fieldId = `pref-${category.key}-${channel.key}`; return (