2023-11-08 17:06:12 +05:30
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useCallback, useMemo, useState } from 'react';
|
|
|
|
|
|
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
|
|
2023-12-06 07:18:05 +05:30
|
|
|
import { Loader, Monitor, Moon, Sun } from 'lucide-react';
|
2023-11-08 17:06:12 +05:30
|
|
|
import { useTheme } from 'next-themes';
|
|
|
|
|
import { useHotkeys } from 'react-hotkeys-hook';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
DOCUMENTS_PAGE_SHORTCUT,
|
|
|
|
|
SETTINGS_PAGE_SHORTCUT,
|
2023-12-25 23:16:56 +00:00
|
|
|
TEMPLATES_PAGE_SHORTCUT,
|
2023-11-08 17:06:12 +05:30
|
|
|
} from '@documenso/lib/constants/keyboard-shortcuts';
|
2024-03-26 21:12:41 +08:00
|
|
|
import {
|
|
|
|
|
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
|
|
|
|
SKIP_QUERY_BATCH_META,
|
|
|
|
|
} from '@documenso/lib/constants/trpc';
|
2023-12-06 07:18:05 +05:30
|
|
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
2023-11-08 17:06:12 +05:30
|
|
|
import {
|
|
|
|
|
CommandDialog,
|
|
|
|
|
CommandEmpty,
|
|
|
|
|
CommandGroup,
|
|
|
|
|
CommandInput,
|
|
|
|
|
CommandItem,
|
|
|
|
|
CommandList,
|
|
|
|
|
CommandShortcut,
|
|
|
|
|
} from '@documenso/ui/primitives/command';
|
2023-12-26 05:19:27 +05:30
|
|
|
import { THEMES_TYPE } from '@documenso/ui/primitives/constants';
|
2023-11-08 17:06:12 +05:30
|
|
|
|
|
|
|
|
const DOCUMENTS_PAGES = [
|
|
|
|
|
{
|
|
|
|
|
label: 'All documents',
|
|
|
|
|
path: '/documents?status=ALL',
|
|
|
|
|
shortcut: DOCUMENTS_PAGE_SHORTCUT.replace('+', ''),
|
|
|
|
|
},
|
|
|
|
|
{ label: 'Draft documents', path: '/documents?status=DRAFT' },
|
2023-12-06 07:18:05 +05:30
|
|
|
{
|
|
|
|
|
label: 'Completed documents',
|
|
|
|
|
path: '/documents?status=COMPLETED',
|
|
|
|
|
},
|
2023-11-08 17:06:12 +05:30
|
|
|
{ label: 'Pending documents', path: '/documents?status=PENDING' },
|
|
|
|
|
{ label: 'Inbox documents', path: '/documents?status=INBOX' },
|
|
|
|
|
];
|
|
|
|
|
|
2023-12-25 23:16:56 +00:00
|
|
|
const TEMPLATES_PAGES = [
|
|
|
|
|
{
|
|
|
|
|
label: 'All templates',
|
|
|
|
|
path: '/templates',
|
|
|
|
|
shortcut: TEMPLATES_PAGE_SHORTCUT.replace('+', ''),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
2023-11-08 17:06:12 +05:30
|
|
|
const SETTINGS_PAGES = [
|
2023-12-06 07:18:05 +05:30
|
|
|
{
|
|
|
|
|
label: 'Settings',
|
|
|
|
|
path: '/settings',
|
|
|
|
|
shortcut: SETTINGS_PAGE_SHORTCUT.replace('+', ''),
|
|
|
|
|
},
|
2023-11-08 17:06:12 +05:30
|
|
|
{ label: 'Profile', path: '/settings/profile' },
|
|
|
|
|
{ label: 'Password', path: '/settings/password' },
|
|
|
|
|
];
|
|
|
|
|
|
2023-11-09 14:38:26 +11:00
|
|
|
export type CommandMenuProps = {
|
|
|
|
|
open?: boolean;
|
|
|
|
|
onOpenChange?: (_open: boolean) => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
2023-11-08 17:06:12 +05:30
|
|
|
const { setTheme } = useTheme();
|
2024-01-18 09:38:42 +02:00
|
|
|
|
2023-11-09 14:38:26 +11:00
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
const [isOpen, setIsOpen] = useState(() => open ?? false);
|
2023-11-08 17:06:12 +05:30
|
|
|
const [search, setSearch] = useState('');
|
|
|
|
|
const [pages, setPages] = useState<string[]>([]);
|
2023-11-09 14:38:26 +11:00
|
|
|
|
2023-12-06 07:18:05 +05:30
|
|
|
const { data: searchDocumentsData, isLoading: isSearchingDocuments } =
|
|
|
|
|
trpcReact.document.searchDocuments.useQuery(
|
|
|
|
|
{
|
|
|
|
|
query: search,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
keepPreviousData: true,
|
2024-03-26 21:12:41 +08:00
|
|
|
// Do not batch this due to relatively long request time compared to
|
|
|
|
|
// other queries which are generally batched with this.
|
|
|
|
|
...SKIP_QUERY_BATCH_META,
|
|
|
|
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
2023-12-06 07:18:05 +05:30
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const searchResults = useMemo(() => {
|
|
|
|
|
if (!searchDocumentsData) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return searchDocumentsData.map((document) => ({
|
|
|
|
|
label: document.title,
|
2024-04-15 10:29:56 +03:00
|
|
|
path: document.path,
|
|
|
|
|
value: document.value,
|
2023-12-06 07:18:05 +05:30
|
|
|
}));
|
2024-04-15 10:29:56 +03:00
|
|
|
}, [searchDocumentsData]);
|
2023-12-06 07:18:05 +05:30
|
|
|
|
2023-11-08 17:06:12 +05:30
|
|
|
const currentPage = pages[pages.length - 1];
|
|
|
|
|
|
2024-01-13 14:19:37 +05:30
|
|
|
const toggleOpen = () => {
|
2023-11-09 14:38:26 +11:00
|
|
|
setIsOpen((isOpen) => !isOpen);
|
|
|
|
|
onOpenChange?.(!isOpen);
|
|
|
|
|
|
|
|
|
|
if (isOpen) {
|
|
|
|
|
setPages([]);
|
|
|
|
|
setSearch('');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const setOpen = useCallback(
|
|
|
|
|
(open: boolean) => {
|
|
|
|
|
setIsOpen(open);
|
|
|
|
|
onOpenChange?.(open);
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
setPages([]);
|
|
|
|
|
setSearch('');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[onOpenChange],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const push = useCallback(
|
|
|
|
|
(path: string) => {
|
|
|
|
|
router.push(path);
|
|
|
|
|
setOpen(false);
|
|
|
|
|
},
|
|
|
|
|
[router, setOpen],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const addPage = (page: string) => {
|
|
|
|
|
setPages((pages) => [...pages, page]);
|
|
|
|
|
setSearch('');
|
2023-11-08 17:06:12 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const goToSettings = useCallback(() => push(SETTINGS_PAGES[0].path), [push]);
|
|
|
|
|
const goToDocuments = useCallback(() => push(DOCUMENTS_PAGES[0].path), [push]);
|
2023-12-25 23:16:56 +00:00
|
|
|
const goToTemplates = useCallback(() => push(TEMPLATES_PAGES[0].path), [push]);
|
2023-11-08 17:06:12 +05:30
|
|
|
|
2024-01-13 14:19:37 +05:30
|
|
|
useHotkeys(['ctrl+k', 'meta+k'], toggleOpen, { preventDefault: true });
|
2023-11-08 17:06:12 +05:30
|
|
|
useHotkeys(SETTINGS_PAGE_SHORTCUT, goToSettings);
|
|
|
|
|
useHotkeys(DOCUMENTS_PAGE_SHORTCUT, goToDocuments);
|
2023-12-25 23:16:56 +00:00
|
|
|
useHotkeys(TEMPLATES_PAGE_SHORTCUT, goToTemplates);
|
2023-11-08 17:06:12 +05:30
|
|
|
|
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
|
|
|
// Escape goes to previous page
|
|
|
|
|
// Backspace goes to previous page when search is empty
|
|
|
|
|
if (e.key === 'Escape' || (e.key === 'Backspace' && !search)) {
|
|
|
|
|
e.preventDefault();
|
2023-11-09 14:38:26 +11:00
|
|
|
|
2023-11-08 17:06:12 +05:30
|
|
|
if (currentPage === undefined) {
|
|
|
|
|
setOpen(false);
|
|
|
|
|
}
|
2023-11-09 14:38:26 +11:00
|
|
|
|
2023-11-08 17:06:12 +05:30
|
|
|
setPages((pages) => pages.slice(0, -1));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2023-12-06 07:18:05 +05:30
|
|
|
<CommandDialog
|
|
|
|
|
commandProps={{
|
|
|
|
|
onKeyDown: handleKeyDown,
|
|
|
|
|
}}
|
|
|
|
|
open={open}
|
|
|
|
|
onOpenChange={setOpen}
|
|
|
|
|
>
|
2023-11-08 17:06:12 +05:30
|
|
|
<CommandInput
|
|
|
|
|
value={search}
|
|
|
|
|
onValueChange={setSearch}
|
|
|
|
|
placeholder="Type a command or search..."
|
|
|
|
|
/>
|
2023-11-09 14:38:26 +11:00
|
|
|
|
2023-11-08 17:06:12 +05:30
|
|
|
<CommandList>
|
2023-12-06 07:18:05 +05:30
|
|
|
{isSearchingDocuments ? (
|
|
|
|
|
<CommandEmpty>
|
|
|
|
|
<div className="flex items-center justify-center">
|
|
|
|
|
<span className="animate-spin">
|
|
|
|
|
<Loader />
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</CommandEmpty>
|
|
|
|
|
) : (
|
|
|
|
|
<CommandEmpty>No results found.</CommandEmpty>
|
|
|
|
|
)}
|
2023-11-08 17:06:12 +05:30
|
|
|
{!currentPage && (
|
|
|
|
|
<>
|
2024-02-06 16:16:10 +11:00
|
|
|
<CommandGroup className="mx-2 p-0 pb-2" heading="Documents">
|
2023-11-08 17:06:12 +05:30
|
|
|
<Commands push={push} pages={DOCUMENTS_PAGES} />
|
|
|
|
|
</CommandGroup>
|
2024-02-06 16:16:10 +11:00
|
|
|
<CommandGroup className="mx-2 p-0 pb-2" heading="Templates">
|
2023-12-25 23:16:56 +00:00
|
|
|
<Commands push={push} pages={TEMPLATES_PAGES} />
|
|
|
|
|
</CommandGroup>
|
2024-02-06 16:16:10 +11:00
|
|
|
<CommandGroup className="mx-2 p-0 pb-2" heading="Settings">
|
2023-11-08 17:06:12 +05:30
|
|
|
<Commands push={push} pages={SETTINGS_PAGES} />
|
|
|
|
|
</CommandGroup>
|
2024-02-06 16:16:10 +11:00
|
|
|
<CommandGroup className="mx-2 p-0 pb-2" heading="Preferences">
|
|
|
|
|
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('theme')}>
|
|
|
|
|
Change theme
|
|
|
|
|
</CommandItem>
|
2023-11-08 17:06:12 +05:30
|
|
|
</CommandGroup>
|
2023-12-06 07:18:05 +05:30
|
|
|
{searchResults.length > 0 && (
|
2024-02-06 16:16:10 +11:00
|
|
|
<CommandGroup className="mx-2 p-0 pb-2" heading="Your documents">
|
2023-12-06 07:18:05 +05:30
|
|
|
<Commands push={push} pages={searchResults} />
|
|
|
|
|
</CommandGroup>
|
|
|
|
|
)}
|
2023-11-08 17:06:12 +05:30
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{currentPage === 'theme' && <ThemeCommands setTheme={setTheme} />}
|
|
|
|
|
</CommandList>
|
|
|
|
|
</CommandDialog>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const Commands = ({
|
|
|
|
|
push,
|
|
|
|
|
pages,
|
|
|
|
|
}: {
|
|
|
|
|
push: (_path: string) => void;
|
2023-12-06 07:18:05 +05:30
|
|
|
pages: { label: string; path: string; shortcut?: string; value?: string }[];
|
2023-11-08 17:06:12 +05:30
|
|
|
}) => {
|
2023-12-06 07:18:05 +05:30
|
|
|
return pages.map((page, idx) => (
|
|
|
|
|
<CommandItem
|
2024-02-06 16:16:10 +11:00
|
|
|
className="-mx-2 -my-1 rounded-lg"
|
2023-12-06 07:18:05 +05:30
|
|
|
key={page.path + idx}
|
|
|
|
|
value={page.value ?? page.label}
|
|
|
|
|
onSelect={() => push(page.path)}
|
|
|
|
|
>
|
2023-11-08 17:06:12 +05:30
|
|
|
{page.label}
|
|
|
|
|
{page.shortcut && <CommandShortcut>{page.shortcut}</CommandShortcut>}
|
|
|
|
|
</CommandItem>
|
|
|
|
|
));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) => {
|
|
|
|
|
const THEMES = useMemo(
|
|
|
|
|
() => [
|
2023-12-26 05:19:27 +05:30
|
|
|
{ label: 'Light Mode', theme: THEMES_TYPE.LIGHT, icon: Sun },
|
|
|
|
|
{ label: 'Dark Mode', theme: THEMES_TYPE.DARK, icon: Moon },
|
|
|
|
|
{ label: 'System Theme', theme: THEMES_TYPE.SYSTEM, icon: Monitor },
|
2023-11-08 17:06:12 +05:30
|
|
|
],
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return THEMES.map((theme) => (
|
2024-01-24 11:33:57 +05:30
|
|
|
<CommandItem
|
|
|
|
|
key={theme.theme}
|
|
|
|
|
onSelect={() => setTheme(theme.theme)}
|
2024-02-06 16:16:10 +11:00
|
|
|
className="-my-1 mx-2 rounded-lg first:mt-2 last:mb-2"
|
2024-01-24 11:33:57 +05:30
|
|
|
>
|
2023-11-08 17:06:12 +05:30
|
|
|
<theme.icon className="mr-2" />
|
|
|
|
|
{theme.label}
|
|
|
|
|
</CommandItem>
|
|
|
|
|
));
|
|
|
|
|
};
|