Files
emaui/apps/backoffice/src/app/layouts/CommandPalette.tsx
2026-08-02 22:44:08 +03:00

97 lines
3.2 KiB
TypeScript

import { useMemo, useState } from 'react';
import { Spotlight, type SpotlightActionData } from '@mantine/spotlight';
import { useDebouncedValue } from '@mantine/hooks';
import { IconFileText, IconSearch } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { flattenNav, type NavSection } from '@ema-platform/ui';
import { useGetAllApplicationsQuery } from '@ema-platform/api';
/** Long enough that typing a company name does not fire a request per keystroke. */
const SEARCH_DEBOUNCE_MS = 250;
/** Below this, a server search matches too much to be useful. */
const MIN_SEARCH_LENGTH = 2;
interface CommandPaletteProps {
/** Already permission-filtered, so the palette cannot reach a hidden route. */
sections: NavSection[];
}
/**
* ⌘K search over every destination and recent application.
*
* With twenty-plus destinations plus five licence types, hunting through
* nested menus is the slow path. This makes nesting cheap: anything reachable
* by clicking is reachable by typing, including applications by number,
* company or TIN.
*/
export function CommandPalette({ sections }: CommandPaletteProps) {
const { t } = useTranslation();
const navigate = useNavigate();
const [query, setQuery] = useState('');
const [debounced] = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
const term = debounced.trim();
// Only hits the API once the palette is open and the query is meaningful.
const { data: applications } = useGetAllApplicationsQuery(
{ search: term, take: 8 },
{ skip: term.length < MIN_SEARCH_LENGTH },
);
const destinationActions = useMemo<SpotlightActionData[]>(
() =>
flattenNav(sections)
.filter((item) => item.to && !item.soon)
.map((item) => ({
id: item.to as string,
label: t(item.label),
description: item.to,
leftSection: <item.icon size={18} stroke={1.6} />,
onClick: () => navigate(item.to as string),
})),
[sections, navigate, t],
);
const applicationActions = useMemo<SpotlightActionData[]>(
() =>
(applications?.items ?? []).map((app) => ({
id: `application-${app.id}`,
label: app.companyName ?? app.applicationNumber,
description: [app.applicationNumber, app.tinNumber]
.filter(Boolean)
.join(' · '),
leftSection: <IconFileText size={18} stroke={1.6} />,
onClick: () => navigate(`/licence-review/${app.id}`),
})),
[applications, navigate],
);
return (
<Spotlight
query={query}
onQueryChange={setQuery}
actions={[
{
group: t('nav.destinations', 'Go to'),
actions: destinationActions,
},
{
group: t('nav.applications', 'Applications'),
actions: applicationActions,
},
]}
shortcut={['mod + K']}
nothingFound={t('nav.noResults', 'Nothing found')}
highlightQuery
searchProps={{
leftSection: <IconSearch size={18} stroke={1.6} />,
placeholder: t(
'nav.commandPlaceholder',
'Search screens, applications, companies, TIN…',
),
}}
/>
);
}