import { useCallback, useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Search } from 'lucide-react'; import { useAuthStore } from '@/store/authStore'; import { NAV_GROUPS, filterNavGroupsByRole, canAccessRoute } from '@/config/adminAccess'; import { Dialog, DialogContent, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { cn } from '@/lib/utils'; type PaletteItem = { id: string; label: string; group: string; path: string; }; interface CommandPaletteProps { open: boolean; onOpenChange: (open: boolean) => void; } /** Looks like EventHub entity ids (base64url-ish). */ const ENTITY_ID_RE = /^[A-Za-z0-9_-]{16,40}$/; export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) { const { t } = useTranslation(); const navigate = useNavigate(); const role = useAuthStore((s) => s.user?.role); const [query, setQuery] = useState(''); const [activeIndex, setActiveIndex] = useState(0); const items = useMemo(() => { const groups = filterNavGroupsByRole(NAV_GROUPS, role); const routes = groups.flatMap((g) => g.items.map((item) => ({ id: item.key, label: t(item.label), group: t(g.label), path: item.key, })) ); routes.push( { id: '/profile', label: t('nav.profile'), group: t('navGroup.account'), path: '/profile' }, { id: '/dashboard', label: t('nav.dashboard'), group: t('navGroup.overview'), path: '/dashboard' } ); const seen = new Set(); return routes.filter((r) => { if (seen.has(r.path)) return false; seen.add(r.path); return true; }); }, [role, t]); const filtered = useMemo(() => { const q = query.trim(); const qLower = q.toLowerCase(); const navHits = !qLower ? items : items.filter( (item) => item.label.toLowerCase().includes(qLower) || item.group.toLowerCase().includes(qLower) || item.path.toLowerCase().includes(qLower) ); if (!ENTITY_ID_RE.test(q)) return navHits; const jumps: PaletteItem[] = []; const pushIf = (path: string, labelKey: string, id: string) => { if (canAccessRoute(path.split('?')[0], role)) { jumps.push({ id, label: `${t(labelKey)} ยท ${q}`, group: t('layout.paletteJumpGroup'), path, }); } }; pushIf(`/inbox/reports?selected=${encodeURIComponent(q)}`, 'layout.paletteOpenReportInbox', `jump-report-inbox-${q}`); pushIf(`/reports/${encodeURIComponent(q)}`, 'layout.paletteOpenReport', `jump-report-${q}`); pushIf(`/inbox/tickets?selected=${encodeURIComponent(q)}`, 'layout.paletteOpenTicketInbox', `jump-ticket-inbox-${q}`); pushIf(`/tickets/${encodeURIComponent(q)}`, 'layout.paletteOpenTicket', `jump-ticket-${q}`); pushIf(`/events/${encodeURIComponent(q)}`, 'layout.paletteOpenEvent', `jump-event-${q}`); pushIf(`/users/${encodeURIComponent(q)}`, 'layout.paletteOpenUser', `jump-user-${q}`); return [...jumps, ...navHits]; }, [items, query, role, t]); useEffect(() => { setActiveIndex(0); }, [query, open]); useEffect(() => { if (!open) setQuery(''); }, [open]); const go = useCallback( (path: string) => { onOpenChange(false); navigate(path); }, [navigate, onOpenChange] ); const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIndex((i) => Math.min(i + 1, Math.max(filtered.length - 1, 0))); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIndex((i) => Math.max(i - 1, 0)); } else if (e.key === 'Enter' && filtered[activeIndex]) { e.preventDefault(); go(filtered[activeIndex].path); } }; return ( {t('layout.paletteTitle')}
setQuery(e.target.value)} onKeyDown={onKeyDown} placeholder={t('layout.palettePlaceholder')} className="h-12 border-0 shadow-none focus-visible:ring-0" />
    {filtered.length === 0 ? (
  • {t('common.nothingFound')}
  • ) : ( filtered.map((item, index) => (
  • )) )}
{t('layout.paletteHints')}
); }