89 lines
3.2 KiB
TypeScript
89 lines
3.2 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { useFetcher, useNavigate } from "react-router";
|
|
import { UI_SURFACE_IDS } from "~/application/ui/uiRegistry";
|
|
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
|
|
import { getCommandRegistryActions, type CommandRegistryLayout, type ScopedCommandAction } from "~/services/command-registry/commandRegistry";
|
|
import type { AppLoaderData } from "~/utils/data/useAppData";
|
|
import { useAppOrAdminData } from "~/utils/data/useAppOrAdminData";
|
|
import { useRootData } from "~/utils/data/useRootData";
|
|
|
|
export default function CommandPaletteDialog({
|
|
actions,
|
|
layout,
|
|
open,
|
|
setOpen,
|
|
}: {
|
|
actions?: ScopedCommandAction[];
|
|
layout?: CommandRegistryLayout;
|
|
open: boolean;
|
|
setOpen: (open: boolean) => void;
|
|
}) {
|
|
const { t, i18n } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const fetcher = useFetcher();
|
|
const appData = useAppOrAdminData() as AppLoaderData | undefined;
|
|
const rootData = useRootData();
|
|
|
|
const resolvedActions =
|
|
actions ??
|
|
(layout
|
|
? getCommandRegistryActions({
|
|
layout,
|
|
t,
|
|
i18n,
|
|
navigate,
|
|
submitLogout: () => {
|
|
const form = new FormData();
|
|
if (rootData?.csrf) {
|
|
form.set("csrf", rootData.csrf);
|
|
}
|
|
fetcher.submit(form, { method: "post", action: "/logout" });
|
|
},
|
|
appData,
|
|
rootData,
|
|
})
|
|
: []);
|
|
|
|
return (
|
|
<div>
|
|
<CommandDialog
|
|
open={open}
|
|
onOpenChange={setOpen}
|
|
dataUiId={UI_SURFACE_IDS.commandPalettePortal}
|
|
onCloseAutoFocus={(event) => {
|
|
// The palette opens via controlled state, not a Radix trigger, so Radix
|
|
// drops focus to <body> on close. Return focus to the search trigger.
|
|
const trigger = typeof document !== "undefined" ? document.querySelector<HTMLElement>('[data-ui-id="ui.action.shell.search"]') : null;
|
|
if (trigger) {
|
|
event.preventDefault();
|
|
trigger.focus();
|
|
}
|
|
}}
|
|
>
|
|
<CommandInput data-ui-id={UI_SURFACE_IDS.commandPaletteSearch} />
|
|
<CommandList data-ui-id={UI_SURFACE_IDS.commandPaletteResults}>
|
|
<CommandEmpty>{t("app.commands.empty.title")}</CommandEmpty>
|
|
<CommandGroup>
|
|
{resolvedActions.map((action) => (
|
|
<CommandItem
|
|
key={action.id}
|
|
data-ui-id={UI_SURFACE_IDS.commandPaletteRow}
|
|
value={[action.name, action.subtitle, action.keywords, action.id].filter(Boolean).join(" ")}
|
|
onSelect={() => {
|
|
action.perform?.(action);
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
<div className="flex w-full items-center justify-between gap-2">
|
|
<div className="text-text-neutral-primary/80 text-sm font-medium">{action.name}</div>
|
|
{action.subtitle ? <div className="text-text-neutral-tertiary truncate text-xs">{action.subtitle}</div> : null}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</CommandDialog>
|
|
</div>
|
|
);
|
|
}
|