Cmd Palette Improvements (#50)

- Fuzzy matching
- Show hotkeys
- Add actions
This commit is contained in:
Gregory Schier
2024-06-10 21:37:41 -07:00
committed by GitHub
parent a2dbd7f849
commit 6215914212
11 changed files with 308 additions and 123 deletions

42
package-lock.json generated
View File

@@ -32,6 +32,7 @@
"codemirror": "^6.0.1",
"codemirror-json-schema": "^0.6.1",
"date-fns": "^3.3.1",
"fast-fuzzy": "^1.12.0",
"focus-trap-react": "^10.1.1",
"format-graphql": "^1.4.0",
"framer-motion": "^9.0.4",
@@ -5750,6 +5751,14 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-fuzzy": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/fast-fuzzy/-/fast-fuzzy-1.12.0.tgz",
"integrity": "sha512-sXxGgHS+ubYpsdLnvOvJ9w5GYYZrtL9mkosG3nfuD446ahvoWEsSKBP7ieGmWIKVLnaxRDgUJkZMdxRgA2Ni+Q==",
"dependencies": {
"graphemesplit": "^2.4.1"
}
},
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -6410,6 +6419,15 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
"node_modules/graphemesplit": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/graphemesplit/-/graphemesplit-2.4.4.tgz",
"integrity": "sha512-lKrpp1mk1NH26USxC/Asw4OHbhSQf5XfrWZ+CDv/dFVvd1j17kFgMotdJvOesmHkbFX9P9sBfpH8VogxOWLg8w==",
"dependencies": {
"js-base64": "^3.6.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/graphql": {
"version": "16.8.1",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
@@ -7288,6 +7306,11 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-base64": {
"version": "3.7.7",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz",
"integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="
},
"node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
@@ -8737,6 +8760,11 @@
"semver": "bin/semver"
}
},
"node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
},
"node_modules/papaparse": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
@@ -11106,6 +11134,11 @@
"node": ">=0.10.0"
}
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -11362,6 +11395,15 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unique-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz",

View File

@@ -52,6 +52,7 @@
"codemirror": "^6.0.1",
"codemirror-json-schema": "^0.6.1",
"date-fns": "^3.3.1",
"fast-fuzzy": "^1.12.0",
"focus-trap-react": "^10.1.1",
"format-graphql": "^1.4.0",
"framer-motion": "^9.0.4",

View File

@@ -1,21 +1,40 @@
import { invoke } from '@tauri-apps/api/core';
import classNames from 'classnames';
import { search } from 'fast-fuzzy';
import type { KeyboardEvent, ReactNode } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDebouncedState } from '../hooks/useDebouncedState';
import { useEnvironments } from '../hooks/useEnvironments';
import type { HotkeyAction } from '../hooks/useHotKey';
import { useHotKey } from '../hooks/useHotKey';
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useRequests } from '../hooks/useRequests';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { CookieDialog } from './CookieDialog';
import { Button } from './core/Button';
import { Heading } from './core/Heading';
import { HotKey } from './core/HotKey';
import { HttpMethodTag } from './core/HttpMethodTag';
import { Icon } from './core/Icon';
import { PlainInput } from './core/PlainInput';
import { HStack } from './core/Stacks';
import { useDialog } from './DialogContext';
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
interface CommandPaletteGroup {
key: string;
@@ -26,22 +45,120 @@ interface CommandPaletteGroup {
type CommandPaletteItem = {
key: string;
onSelect: () => void;
action?: HotkeyAction;
} & ({ searchText: string; label: ReactNode } | { label: string });
const MAX_PER_GROUP = 4;
const MAX_PER_GROUP = 8;
export function CommandPalette({ onClose }: { onClose: () => void }) {
const [command, setCommand] = useDebouncedState<string>('', 150);
const [selectedItemKey, setSelectedItemKey] = useState<string | null>(null);
const routes = useAppRoutes();
const activeEnvironmentId = useActiveEnvironmentId();
const activeRequestId = useActiveRequestId();
const activeWorkspaceId = useActiveWorkspaceId();
const active = useActiveWorkspaceId();
const workspaces = useWorkspaces();
const environments = useEnvironments();
const recentEnvironments = useRecentEnvironments();
const recentWorkspaces = useRecentWorkspaces();
const requests = useRequests();
const recentRequests = useRecentRequests();
const [command, setCommand] = useState<string>('');
const openWorkspace = useOpenWorkspace();
const createWorkspace = useCreateWorkspace();
const createHttpRequest = useCreateHttpRequest();
const { activeCookieJar } = useActiveCookieJar();
const createGrpcRequest = useCreateGrpcRequest();
const createEnvironment = useCreateEnvironment();
const dialog = useDialog();
const workspaceId = useActiveWorkspaceId();
const activeEnvironment = useActiveEnvironment();
const [, setSidebarHidden] = useSidebarHidden();
const workspaceCommands = useMemo<CommandPaletteItem[]>(() => {
const commands: CommandPaletteItem[] = [
{
key: 'settings.open',
label: 'Open Settings',
action: 'settings.show',
onSelect: async () => {
if (workspaceId == null) return;
await invoke('cmd_new_nested_window', {
url: routes.paths.workspaceSettings({ workspaceId }),
label: 'settings',
title: 'Yaak Settings',
});
},
},
{
key: 'app.create',
label: 'Create Workspace',
onSelect: createWorkspace.mutate,
},
{
key: 'http_request.create',
label: 'Create HTTP Request',
onSelect: () => createHttpRequest.mutate({}),
},
{
key: 'cookies.show',
label: 'Show Cookies',
onSelect: async () => {
dialog.show({
id: 'cookies',
title: 'Manage Cookies',
size: 'full',
render: () => <CookieDialog cookieJarId={activeCookieJar?.id ?? null} />,
});
},
},
{
key: 'grpc_request.create',
label: 'Create GRPC Request',
onSelect: () => createGrpcRequest.mutate({}),
},
{
key: 'environment.edit',
label: 'Edit Environment',
action: 'environmentEditor.toggle',
onSelect: () => {
dialog.toggle({
id: 'environment-editor',
noPadding: true,
size: 'lg',
className: 'h-[80vh]',
render: () => <EnvironmentEditDialog initialEnvironment={activeEnvironment} />,
});
},
},
{
key: 'environment.create',
label: 'Create Environment',
onSelect: createEnvironment.mutate,
},
{
key: 'sidebar.toggle',
label: 'Toggle Sidebar',
action: 'sidebar.focus',
onSelect: () => setSidebarHidden((h) => !h),
},
];
return commands.sort((a, b) =>
('searchText' in a ? a.searchText : a.label).localeCompare(
'searchText' in b ? b.searchText : b.label,
),
);
}, [
activeCookieJar,
activeEnvironment,
createEnvironment.mutate,
createGrpcRequest,
createHttpRequest,
createWorkspace.mutate,
dialog,
routes.paths,
setSidebarHidden,
workspaceId,
]);
const sortedRequests = useMemo(() => {
return [...requests].sort((a, b) => {
@@ -60,6 +177,23 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
});
}, [recentRequests, requests]);
const sortedEnvironments = useMemo(() => {
return [...environments].sort((a, b) => {
const aRecentIndex = recentEnvironments.indexOf(a.id);
const bRecentIndex = recentEnvironments.indexOf(b.id);
if (aRecentIndex >= 0 && bRecentIndex >= 0) {
return aRecentIndex - bRecentIndex;
} else if (aRecentIndex >= 0 && bRecentIndex === -1) {
return -1;
} else if (aRecentIndex === -1 && bRecentIndex >= 0) {
return 1;
} else {
return a.createdAt.localeCompare(b.createdAt);
}
});
}, [environments, recentEnvironments]);
const sortedWorkspaces = useMemo(() => {
return [...workspaces].sort((a, b) => {
const aRecentIndex = recentWorkspaces.indexOf(a.id);
@@ -78,6 +212,12 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
}, [recentWorkspaces, workspaces]);
const groups = useMemo<CommandPaletteGroup[]>(() => {
const actionsGroup: CommandPaletteGroup = {
key: 'actions',
label: 'Actions',
items: workspaceCommands,
};
const requestGroup: CommandPaletteGroup = {
key: 'requests',
label: 'Requests',
@@ -91,10 +231,10 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
requestGroup.items.push({
key: `switch-request-${r.id}`,
searchText: `${r.method} ${r.name}`,
searchText: fallbackRequestName(r),
label: (
<HStack space={2}>
<HttpMethodTag className="text-fg-subtler" shortNames request={r} />
<HttpMethodTag className="text-fg-subtler" request={r} />
<div className="truncate">{fallbackRequestName(r)}</div>
</HStack>
),
@@ -108,6 +248,23 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
});
}
const environmentGroup: CommandPaletteGroup = {
key: 'environments',
label: 'Environments',
items: [],
};
for (const e of sortedEnvironments) {
if (e.id === activeEnvironment?.id) {
continue;
}
environmentGroup.items.push({
key: `switch-environment-${e.id}`,
label: e.name,
onSelect: () => routes.setEnvironment(e),
});
}
const workspaceGroup: CommandPaletteGroup = {
key: 'workspaces',
label: 'Workspaces',
@@ -115,7 +272,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
};
for (const w of sortedWorkspaces) {
if (w.id === activeWorkspaceId) {
if (w.id === active) {
continue;
}
workspaceGroup.items.push({
@@ -125,30 +282,44 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
});
}
return [requestGroup, workspaceGroup];
return [actionsGroup, requestGroup, environmentGroup, workspaceGroup];
}, [
activeEnvironmentId,
activeRequestId,
activeWorkspaceId,
openWorkspace,
routes,
workspaceCommands,
sortedRequests,
activeRequestId,
routes,
activeEnvironmentId,
sortedEnvironments,
activeEnvironment?.id,
sortedWorkspaces,
active,
openWorkspace,
]);
const filteredGroups = useMemo(
() =>
groups
.map((g) => {
g.items = g.items.filter((v) => {
const s = 'searchText' in v ? v.searchText : v.label;
return s.toLowerCase().includes(command.toLowerCase());
});
return g;
const allItems = useMemo(() => groups.flatMap((g) => g.items), [groups]);
useEffect(() => {
setSelectedItemKey(null);
}, [command]);
const { filteredGroups, filteredAllItems } = useMemo(() => {
const result = command
? search(command, allItems, {
threshold: 0.4,
keySelector: (v) => ('searchText' in v ? v.searchText : v.label),
})
.filter((g) => g.items.length > 0),
[command, groups],
);
: allItems;
const filteredGroups = groups
.map((g) => {
g.items = result.filter((i) => g.items.includes(i)).slice(0, MAX_PER_GROUP);
return g;
})
.filter((g) => g.items.length > 0);
const filteredAllItems = filteredGroups.flatMap((g) => g.items);
return { filteredAllItems, filteredGroups };
}, [allItems, command, groups]);
const handleSelectAndClose = useCallback(
(cb: () => void) => {
@@ -158,38 +329,37 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
[onClose],
);
const { allItems, selectedItem } = useMemo(() => {
const allItems = filteredGroups.flatMap((g) => g.items);
let selectedItem = allItems.find((i) => i.key === selectedItemKey) ?? null;
const selectedItem = useMemo(() => {
let selectedItem = filteredAllItems.find((i) => i.key === selectedItemKey) ?? null;
if (selectedItem == null) {
selectedItem = allItems[0] ?? null;
selectedItem = filteredAllItems[0] ?? null;
}
return { selectedItem, allItems };
}, [filteredGroups, selectedItemKey]);
return selectedItem;
}, [filteredAllItems, selectedItemKey]);
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
const index = allItems.findIndex((v) => v.key === selectedItem?.key);
const index = filteredAllItems.findIndex((v) => v.key === selectedItem?.key);
if (e.key === 'ArrowDown' || (e.ctrlKey && e.key === 'n')) {
const next = allItems[index + 1];
const next = filteredAllItems[index + 1] ?? filteredAllItems[0];
setSelectedItemKey(next?.key ?? null);
} else if (e.key === 'ArrowUp' || (e.ctrlKey && e.key === 'k')) {
const prev = allItems[index - 1];
const prev = filteredAllItems[index - 1] ?? filteredAllItems[filteredAllItems.length - 1];
setSelectedItemKey(prev?.key ?? null);
} else if (e.key === 'Enter') {
const selected = allItems[index];
const selected = filteredAllItems[index];
setSelectedItemKey(selected?.key ?? null);
if (selected) {
handleSelectAndClose(selected.onSelect);
}
}
},
[allItems, handleSelectAndClose, selectedItem?.key],
[filteredAllItems, handleSelectAndClose, selectedItem?.key],
);
return (
<div className="h-full max-h-[20rem] w-[400px] grid grid-rows-[auto_minmax(0,1fr)]">
<div className="h-full w-[400px] grid grid-rows-[auto_minmax(0,1fr)] overflow-hidden">
<div className="px-2 py-2 w-full">
<PlainInput
hideLabel
@@ -209,15 +379,18 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
</div>
<div className="h-full px-1.5 overflow-y-auto pb-1">
{filteredGroups.map((g) => (
<div key={g.key} className="mb-1.5">
<div key={g.key} className="mb-1.5 w-full">
<Heading size={2} className="!text-xs uppercase px-1.5 h-sm flex items-center">
{g.label}
</Heading>
{g.items.slice(0, MAX_PER_GROUP).map((v) => (
{g.items.map((v) => (
<CommandPaletteItem
active={v.key === selectedItem?.key}
key={v.key}
onClick={() => handleSelectAndClose(v.onSelect)}
rightSlot={
v.action && <CommandPaletteAction action={v.action} onAction={v.onSelect} />
}
>
{v.label}
</CommandPaletteItem>
@@ -233,15 +406,20 @@ function CommandPaletteItem({
children,
active,
onClick,
rightSlot,
}: {
children: ReactNode;
active: boolean;
onClick: () => void;
rightSlot?: ReactNode;
}) {
return (
<button
<Button
onClick={onClick}
tabIndex={active ? undefined : -1}
rightSlot={rightSlot}
color="custom"
justify="start"
className={classNames(
'w-full h-sm flex items-center rounded px-1.5',
'hover:text-fg',
@@ -250,6 +428,17 @@ function CommandPaletteItem({
)}
>
<span className="truncate">{children}</span>
</button>
</Button>
);
}
function CommandPaletteAction({
action,
onAction,
}: {
action: HotkeyAction;
onAction: () => void;
}) {
useHotKey(action, onAction);
return <HotKey className="ml-auto" action={action} />;
}

View File

@@ -6,7 +6,6 @@ import { useCommandPalette } from '../hooks/useCommandPalette';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { environmentsQueryKey } from '../hooks/useEnvironments';
import { foldersQueryKey } from '../hooks/useFolders';
import { useGlobalCommands } from '../hooks/useGlobalCommands';
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
@@ -42,7 +41,6 @@ export function GlobalHooks() {
// Other useful things
useSyncThemeToDocument();
useGlobalCommands();
useCommandPalette();
useNotificationToast();

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames';
import { memo, useMemo } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useCommand } from '../hooks/useCommands';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
import { usePrompt } from '../hooks/usePrompt';
@@ -28,7 +28,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const activeWorkspaceId = activeWorkspace?.id ?? null;
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
const createWorkspace = useCommand('workspace.create');
const createWorkspace = useCreateWorkspace();
const dialog = useDialog();
const prompt = usePrompt();
const settings = useSettings();
@@ -101,7 +101,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
key: 'create-workspace',
label: 'New Workspace',
leftSlot: <Icon icon="plus" />,
onSelect: () => createWorkspace.mutate({}),
onSelect: createWorkspace.mutate,
},
];
}, [

View File

@@ -54,7 +54,7 @@ export function Dialog({
<Overlay open={open} onClose={onClose} portalName="dialog">
<div
className={classNames(
'x-theme-dialog absolute inset-0 flex flex-col items-center pointer-events-none my-5',
'x-theme-dialog absolute inset-0 flex flex-col items-center pointer-events-none',
vAlign === 'top' && 'justify-start',
vAlign === 'center' && 'justify-center',
)}
@@ -74,12 +74,12 @@ export function Dialog({
'relative bg-background pointer-events-auto',
'rounded-lg',
'border border-background-highlight-secondary shadow-lg shadow-[rgba(0,0,0,0.1)]',
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-4rem)]',
size === 'sm' && 'w-[28rem] max-h-[80vh]',
size === 'md' && 'w-[45rem] max-h-[80vh]',
size === 'lg' && 'w-[65rem] max-h-[80vh]',
size === 'full' && 'w-[100vw] h-[100vh]',
size === 'dynamic' && 'min-w-[20rem] max-w-[80vw] max-h-[80vh]',
size === 'dynamic' && 'min-w-[20rem] max-w-[100vw] w-full mt-8',
)}
>
{title ? (

View File

@@ -9,6 +9,7 @@ export function useCommandPalette() {
id: 'command_palette',
size: 'dynamic',
hideX: true,
className: '!max-h-[min(30rem,calc(100vh-4rem))]',
vAlign: 'top',
noPadding: true,
noScroll: true,

View File

@@ -1,41 +0,0 @@
import type { UseMutationOptions } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import { useEffect } from 'react';
import { createGlobalState } from 'react-use';
import type { TrackAction, TrackResource } from '../lib/analytics';
import type { Workspace } from '../lib/models';
interface CommandInstance<T, V> extends UseMutationOptions<V, unknown, T> {
track?: [TrackResource, TrackAction];
name: string;
}
export type Commands = {
'workspace.create': CommandInstance<Partial<Pick<Workspace, 'name'>>, Workspace>;
};
const useCommandState = createGlobalState<Commands>();
export function useRegisterCommand<K extends keyof Commands>(action: K, command: Commands[K]) {
const [, setState] = useCommandState();
useEffect(() => {
setState((commands) => {
return { ...commands, [action]: command };
});
// Remove action when it goes out of scope
return () => {
setState((commands) => {
return { ...commands, [action]: undefined };
});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [action]);
}
export function useCommand<K extends keyof Commands>(action: K) {
const [commands] = useCommandState();
const cmd = commands[action];
return useMutation({ ...cmd });
}

View File

@@ -14,7 +14,7 @@ export function useCreateHttpRequest() {
const routes = useAppRoutes();
return useMutation<HttpRequest, unknown, Partial<HttpRequest>>({
mutationFn: (patch) => {
mutationFn: (patch = {}) => {
if (workspaceId === null) {
throw new Error("Cannot create request when there's no active workspace");
}
@@ -28,7 +28,6 @@ export function useCreateHttpRequest() {
}
}
patch.folderId = patch.folderId || activeRequest?.folderId;
console.log('PATCH', patch);
return invoke('cmd_create_http_request', { request: { workspaceId, ...patch } });
},
onSettled: () => trackEvent('http_request', 'create'),

View File

@@ -0,0 +1,27 @@
import { useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import type { Workspace } from '../lib/models';
import { useAppRoutes } from './useAppRoutes';
import { usePrompt } from './usePrompt';
export function useCreateWorkspace() {
const routes = useAppRoutes();
const prompt = usePrompt();
return useMutation<Workspace, void, void>({
mutationFn: async () => {
const name = await prompt({
id: 'new-workspace',
name: 'name',
label: 'Name',
defaultValue: 'My Workspace',
title: 'New Workspace',
confirmLabel: 'Create',
placeholder: 'My Workspace',
});
return invoke('cmd_create_workspace', { name });
},
onSuccess: async (workspace) => {
routes.navigate('workspace', { workspaceId: workspace.id });
},
});
}

View File

@@ -1,31 +0,0 @@
import { invoke } from '@tauri-apps/api/core';
import { useAppRoutes } from './useAppRoutes';
import { useRegisterCommand } from './useCommands';
import { usePrompt } from './usePrompt';
export function useGlobalCommands() {
const prompt = usePrompt();
const routes = useAppRoutes();
useRegisterCommand('workspace.create', {
name: 'New Workspace',
track: ['workspace', 'create'],
onSuccess: async (workspace) => {
routes.navigate('workspace', { workspaceId: workspace.id });
},
mutationFn: async ({ name: patchName }) => {
const name =
patchName ??
(await prompt({
id: 'new-workspace',
name: 'name',
label: 'Name',
defaultValue: 'My Workspace',
title: 'New Workspace',
confirmLabel: 'Create',
placeholder: 'My Workspace',
}));
return invoke('cmd_create_workspace', { name });
},
});
}