mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-29 07:11:00 +00:00
97 lines
3.2 KiB
TypeScript
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…',
|
|
),
|
|
}}
|
|
/>
|
|
);
|
|
}
|