mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-19 23:41:18 +02:00
Good hotkey support
This commit is contained in:
@@ -33,7 +33,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
|||||||
});
|
});
|
||||||
}, [dialog, activeEnvironment]);
|
}, [dialog, activeEnvironment]);
|
||||||
|
|
||||||
useHotkey('environmentEditor.show', showEnvironmentDialog);
|
useHotkey('environmentEditor.toggle', showEnvironmentDialog);
|
||||||
|
|
||||||
const items: DropdownItem[] = useMemo(
|
const items: DropdownItem[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -58,6 +58,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
|||||||
{
|
{
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: 'Manage Environments',
|
label: 'Manage Environments',
|
||||||
|
hotkeyAction: 'environmentEditor.toggle',
|
||||||
leftSlot: <Icon icon="gear" />,
|
leftSlot: <Icon icon="gear" />,
|
||||||
onSelect: showEnvironmentDialog,
|
onSelect: showEnvironmentDialog,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
|||||||
[environments, selectedEnvironmentId],
|
[environments, selectedEnvironmentId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleCreateEnvironment = async () => {
|
||||||
|
const e = await createEnvironment.mutateAsync();
|
||||||
|
setSelectedEnvironmentId(e.id);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -76,7 +81,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
|||||||
className="w-full text-center"
|
className="w-full text-center"
|
||||||
color="gray"
|
color="gray"
|
||||||
justify="center"
|
justify="center"
|
||||||
onClick={() => createEnvironment.mutate()}
|
onClick={handleCreateEnvironment}
|
||||||
>
|
>
|
||||||
New Environment
|
New Environment
|
||||||
</Button>
|
</Button>
|
||||||
@@ -191,15 +196,24 @@ const EnvironmentEditor = function ({
|
|||||||
<h1 className="text-xl">{environment?.name ?? 'Base Environment'}</h1>
|
<h1 className="text-xl">{environment?.name ?? 'Base Environment'}</h1>
|
||||||
{items != null && (
|
{items != null && (
|
||||||
<Dropdown items={items}>
|
<Dropdown items={items}>
|
||||||
<IconButton icon="gear" title="Environment Actions" size="sm" className="!h-auto w-8" />
|
<IconButton
|
||||||
|
icon="dotsV"
|
||||||
|
title="Environment Actions"
|
||||||
|
size="sm"
|
||||||
|
className="!h-auto w-8"
|
||||||
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
|
{environment == null && (
|
||||||
|
<span className="text-sm italic text-gray-500">
|
||||||
|
Base variables available at all times
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
<PairEditor
|
<PairEditor
|
||||||
nameAutocomplete={nameAutocomplete}
|
nameAutocomplete={nameAutocomplete}
|
||||||
nameAutocompleteVariables={false}
|
nameAutocompleteVariables={false}
|
||||||
namePlaceholder="VAR_NAME"
|
namePlaceholder="VAR_NAME"
|
||||||
valuePlaceholder="variable value"
|
|
||||||
nameValidate={validateName}
|
nameValidate={validateName}
|
||||||
valueAutocompleteVariables={false}
|
valueAutocompleteVariables={false}
|
||||||
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
|
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { useRef } from 'react';
|
|
||||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
|
||||||
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
|
||||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
|
||||||
import type { DropdownProps, DropdownRef } from './core/Dropdown';
|
|
||||||
import { Dropdown } from './core/Dropdown';
|
|
||||||
import { HotKey } from './core/HotKey';
|
|
||||||
import { Icon } from './core/Icon';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
requestId: string | null;
|
|
||||||
children: DropdownProps['children'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RequestActionsDropdown({ requestId, children }: Props) {
|
|
||||||
const deleteRequest = useDeleteRequest(requestId);
|
|
||||||
const duplicateRequest = useDuplicateRequest({ id: requestId, navigateAfter: true });
|
|
||||||
const dropdownRef = useRef<DropdownRef>(null);
|
|
||||||
|
|
||||||
useListenToTauriEvent('toggle_settings', () => {
|
|
||||||
dropdownRef.current?.toggle();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Put this somewhere better
|
|
||||||
useListenToTauriEvent('duplicate_request', () => {
|
|
||||||
duplicateRequest.mutate();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (requestId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
ref={dropdownRef}
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
key: 'duplicate',
|
|
||||||
label: 'Duplicate',
|
|
||||||
onSelect: duplicateRequest.mutate,
|
|
||||||
leftSlot: <Icon icon="copy" />,
|
|
||||||
rightSlot: <HotKey modifier="Meta" keyName="D" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'delete',
|
|
||||||
label: 'Delete',
|
|
||||||
onSelect: deleteRequest.mutate,
|
|
||||||
variant: 'danger',
|
|
||||||
leftSlot: <Icon icon="trash" />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
69
src-web/components/SettingsDropdown.tsx
Normal file
69
src-web/components/SettingsDropdown.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { useAppVersion } from '../hooks/useAppVersion';
|
||||||
|
import { useExportData } from '../hooks/useExportData';
|
||||||
|
import { useImportData } from '../hooks/useImportData';
|
||||||
|
import { useTheme } from '../hooks/useTheme';
|
||||||
|
import { useUpdateMode } from '../hooks/useUpdateMode';
|
||||||
|
import type { DropdownProps, DropdownRef } from './core/Dropdown';
|
||||||
|
import { Dropdown } from './core/Dropdown';
|
||||||
|
import { Icon } from './core/Icon';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
requestId: string | null;
|
||||||
|
children: DropdownProps['children'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsDropdown({ requestId, children }: Props) {
|
||||||
|
const importData = useImportData();
|
||||||
|
const exportData = useExportData();
|
||||||
|
const { appearance, toggleAppearance } = useTheme();
|
||||||
|
const appVersion = useAppVersion();
|
||||||
|
const [updateMode, setUpdateMode] = useUpdateMode();
|
||||||
|
const dropdownRef = useRef<DropdownRef>(null);
|
||||||
|
|
||||||
|
if (requestId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
ref={dropdownRef}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'import-data',
|
||||||
|
label: 'Import',
|
||||||
|
leftSlot: <Icon icon="download" />,
|
||||||
|
onSelect: () => importData.mutate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'export-data',
|
||||||
|
label: 'Export',
|
||||||
|
leftSlot: <Icon icon="upload" />,
|
||||||
|
onSelect: () => exportData.mutate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'appearance',
|
||||||
|
label: 'Toggle Theme',
|
||||||
|
onSelect: toggleAppearance,
|
||||||
|
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
|
||||||
|
},
|
||||||
|
{ type: 'separator', label: `v${appVersion.data}` },
|
||||||
|
{
|
||||||
|
key: 'update-mode',
|
||||||
|
label: updateMode === 'stable' ? 'Enable Beta' : 'Disable Beta',
|
||||||
|
onSelect: () => setUpdateMode(updateMode === 'stable' ? 'beta' : 'stable'),
|
||||||
|
leftSlot: <Icon icon="camera" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'update-check',
|
||||||
|
label: 'Check for Updates',
|
||||||
|
onSelect: () => invoke('check_for_updates'),
|
||||||
|
leftSlot: <Icon icon="update" />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ import { useCreateFolder } from '../hooks/useCreateFolder';
|
|||||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||||
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
|
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
|
||||||
import { useDeleteFolder } from '../hooks/useDeleteFolder';
|
import { useDeleteFolder } from '../hooks/useDeleteFolder';
|
||||||
|
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||||
|
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||||
import { useFolders } from '../hooks/useFolders';
|
import { useFolders } from '../hooks/useFolders';
|
||||||
import { useHotkey } from '../hooks/useHotkey';
|
import { useHotkey } from '../hooks/useHotkey';
|
||||||
import { useKeyValue } from '../hooks/useKeyValue';
|
import { useKeyValue } from '../hooks/useKeyValue';
|
||||||
@@ -28,9 +30,8 @@ import { fallbackRequestName } from '../lib/fallbackRequestName';
|
|||||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||||
import type { Folder, HttpRequest, Workspace } from '../lib/models';
|
import type { Folder, HttpRequest, Workspace } from '../lib/models';
|
||||||
import { isResponseLoading } from '../lib/models';
|
import { isResponseLoading } from '../lib/models';
|
||||||
import { Dropdown } from './core/Dropdown';
|
import { ContextMenu } from './core/Dropdown';
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { IconButton } from './core/IconButton';
|
|
||||||
import { InlineCode } from './core/InlineCode';
|
import { InlineCode } from './core/InlineCode';
|
||||||
import { VStack } from './core/Stacks';
|
import { VStack } from './core/Stacks';
|
||||||
import { StatusTag } from './core/StatusTag';
|
import { StatusTag } from './core/StatusTag';
|
||||||
@@ -82,10 +83,18 @@ export function Sidebar({ className }: Props) {
|
|||||||
const { tree, treeParentMap, selectableRequests } = useMemo<{
|
const { tree, treeParentMap, selectableRequests } = useMemo<{
|
||||||
tree: TreeNode | null;
|
tree: TreeNode | null;
|
||||||
treeParentMap: Record<string, TreeNode>;
|
treeParentMap: Record<string, TreeNode>;
|
||||||
selectableRequests: { id: string; index: number; tree: TreeNode }[];
|
selectableRequests: {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
tree: TreeNode;
|
||||||
|
}[];
|
||||||
}>(() => {
|
}>(() => {
|
||||||
const treeParentMap: Record<string, TreeNode> = {};
|
const treeParentMap: Record<string, TreeNode> = {};
|
||||||
const selectableRequests: { id: string; index: number; tree: TreeNode }[] = [];
|
const selectableRequests: {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
tree: TreeNode;
|
||||||
|
}[] = [];
|
||||||
if (activeWorkspace == null) {
|
if (activeWorkspace == null) {
|
||||||
return { tree: null, treeParentMap, selectableRequests };
|
return { tree: null, treeParentMap, selectableRequests };
|
||||||
}
|
}
|
||||||
@@ -116,7 +125,15 @@ export function Sidebar({ className }: Props) {
|
|||||||
}, [activeWorkspace, requests, folders]);
|
}, [activeWorkspace, requests, folders]);
|
||||||
|
|
||||||
const focusActiveRequest = useCallback(
|
const focusActiveRequest = useCallback(
|
||||||
(args: { forced?: { id: string; tree: TreeNode }; noFocusSidebar?: boolean } = {}) => {
|
(
|
||||||
|
args: {
|
||||||
|
forced?: {
|
||||||
|
id: string;
|
||||||
|
tree: TreeNode;
|
||||||
|
};
|
||||||
|
noFocusSidebar?: boolean;
|
||||||
|
} = {},
|
||||||
|
) => {
|
||||||
const { forced, noFocusSidebar } = args;
|
const { forced, noFocusSidebar } = args;
|
||||||
const tree = forced?.tree ?? treeParentMap[activeRequestId ?? 'n/a'] ?? null;
|
const tree = forced?.tree ?? treeParentMap[activeRequestId ?? 'n/a'] ?? null;
|
||||||
const children = tree?.children ?? [];
|
const children = tree?.children ?? [];
|
||||||
@@ -502,6 +519,8 @@ const SidebarItem = forwardRef(function SidebarItem(
|
|||||||
const createRequest = useCreateRequest();
|
const createRequest = useCreateRequest();
|
||||||
const createFolder = useCreateFolder();
|
const createFolder = useCreateFolder();
|
||||||
const deleteFolder = useDeleteFolder(itemId);
|
const deleteFolder = useDeleteFolder(itemId);
|
||||||
|
const deleteRequest = useDeleteRequest(itemId);
|
||||||
|
const duplicateRequest = useDuplicateRequest({ id: itemId, navigateAfter: true });
|
||||||
const sendManyRequests = useSendManyRequests();
|
const sendManyRequests = useSendManyRequests();
|
||||||
const latestResponse = useLatestResponse(itemId);
|
const latestResponse = useLatestResponse(itemId);
|
||||||
const updateRequest = useUpdateRequest(itemId);
|
const updateRequest = useUpdateRequest(itemId);
|
||||||
@@ -554,74 +573,99 @@ const SidebarItem = forwardRef(function SidebarItem(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSelect = useCallback(() => onSelect(itemId), [onSelect, itemId]);
|
const handleSelect = useCallback(() => onSelect(itemId), [onSelect, itemId]);
|
||||||
|
const [showContextMenu, setShowContextMenu] = useState<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const handleContextMenu = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log('CONTEXT MENU');
|
||||||
|
setShowContextMenu({ x: e.clientX, y: e.clientY });
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li ref={ref}>
|
<li ref={ref}>
|
||||||
<div className={classNames(className, 'block relative group/item px-2 pb-0.5')}>
|
<div className={classNames(className, 'block relative group/item px-2 pb-0.5')}>
|
||||||
{itemModel === 'folder' && (
|
<ContextMenu
|
||||||
<Dropdown
|
show={showContextMenu}
|
||||||
items={[
|
items={
|
||||||
{
|
itemModel === 'folder'
|
||||||
key: 'sendAll',
|
? [
|
||||||
label: 'Send All',
|
{
|
||||||
leftSlot: <Icon icon="paperPlane" />,
|
key: 'sendAll',
|
||||||
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
|
label: 'Send All',
|
||||||
},
|
leftSlot: <Icon icon="paperPlane" />,
|
||||||
{ type: 'separator', label: itemName },
|
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
|
||||||
{
|
},
|
||||||
key: 'rename',
|
{ type: 'separator', label: itemName },
|
||||||
label: 'Rename',
|
{
|
||||||
leftSlot: <Icon icon="pencil" />,
|
key: 'rename',
|
||||||
onSelect: async () => {
|
label: 'Rename',
|
||||||
const name = await prompt({
|
leftSlot: <Icon icon="pencil" />,
|
||||||
title: 'Rename Folder',
|
onSelect: async () => {
|
||||||
description: (
|
const name = await prompt({
|
||||||
<>
|
title: 'Rename Folder',
|
||||||
Enter a new name for <InlineCode>{itemName}</InlineCode>
|
description: (
|
||||||
</>
|
<>
|
||||||
),
|
Enter a new name for <InlineCode>{itemName}</InlineCode>
|
||||||
name: 'name',
|
</>
|
||||||
label: 'Name',
|
),
|
||||||
defaultValue: itemName,
|
name: 'name',
|
||||||
});
|
label: 'Name',
|
||||||
updateAnyFolder.mutate({ id: itemId, update: (f) => ({ ...f, name }) });
|
defaultValue: itemName,
|
||||||
},
|
});
|
||||||
},
|
updateAnyFolder.mutate({ id: itemId, update: (f) => ({ ...f, name }) });
|
||||||
{
|
},
|
||||||
key: 'deleteFolder',
|
},
|
||||||
label: 'Delete',
|
{
|
||||||
variant: 'danger',
|
key: 'deleteFolder',
|
||||||
leftSlot: <Icon icon="trash" />,
|
label: 'Delete',
|
||||||
onSelect: () => deleteFolder.mutate(),
|
variant: 'danger',
|
||||||
},
|
leftSlot: <Icon icon="trash" />,
|
||||||
{ type: 'separator' },
|
onSelect: () => deleteFolder.mutate(),
|
||||||
{
|
},
|
||||||
key: 'createRequest',
|
{ type: 'separator' },
|
||||||
label: 'New Request',
|
{
|
||||||
leftSlot: <Icon icon="plus" />,
|
key: 'createRequest',
|
||||||
onSelect: () => createRequest.mutate({ folderId: itemId, sortPriority: -1 }),
|
label: 'New Request',
|
||||||
},
|
hotkeyAction: 'request.create',
|
||||||
{
|
leftSlot: <Icon icon="plus" />,
|
||||||
key: 'createFolder',
|
onSelect: () => createRequest.mutate({ folderId: itemId, sortPriority: -1 }),
|
||||||
label: 'New Folder',
|
},
|
||||||
leftSlot: <Icon icon="plus" />,
|
{
|
||||||
onSelect: () => createFolder.mutate({ folderId: itemId, sortPriority: -1 }),
|
key: 'createFolder',
|
||||||
},
|
label: 'New Folder',
|
||||||
]}
|
leftSlot: <Icon icon="plus" />,
|
||||||
>
|
onSelect: () => createFolder.mutate({ folderId: itemId, sortPriority: -1 }),
|
||||||
<IconButton
|
},
|
||||||
title="Folder options"
|
]
|
||||||
size="xs"
|
: [
|
||||||
icon="dotsV"
|
{
|
||||||
className="ml-auto !bg-transparent absolute right-2 opacity-0 group-hover/item:opacity-70 transition-opacity"
|
key: 'duplicateRequest',
|
||||||
/>
|
label: 'Duplicate',
|
||||||
</Dropdown>
|
hotkeyAction: 'request.duplicate',
|
||||||
)}
|
leftSlot: <Icon icon="copy" />,
|
||||||
|
onSelect: () => duplicateRequest.mutate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'deleteRequest',
|
||||||
|
variant: 'danger',
|
||||||
|
label: 'Delete',
|
||||||
|
leftSlot: <Icon icon="trash" />,
|
||||||
|
onSelect: () => deleteRequest.mutate(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
onClose={() => setShowContextMenu(null)}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
// tabIndex={-1} // Will prevent drag-n-drop
|
// tabIndex={-1} // Will prevent drag-n-drop
|
||||||
disabled={editing}
|
disabled={editing}
|
||||||
onClick={handleSelect}
|
onClick={handleSelect}
|
||||||
onDoubleClick={handleStartEditing}
|
onDoubleClick={handleStartEditing}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
data-selected={selected}
|
data-selected={selected}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -710,7 +754,13 @@ function DraggableSidebarItem({
|
|||||||
[onMove],
|
[onMove],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [{ isDragging }, connectDrag] = useDrag<DragItem, unknown, { isDragging: boolean }>(
|
const [{ isDragging }, connectDrag] = useDrag<
|
||||||
|
DragItem,
|
||||||
|
unknown,
|
||||||
|
{
|
||||||
|
isDragging: boolean;
|
||||||
|
}
|
||||||
|
>(
|
||||||
() => ({
|
() => ({
|
||||||
type: ItemTypes.REQUEST,
|
type: ItemTypes.REQUEST,
|
||||||
item: () => {
|
item: () => {
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useCreateFolder } from '../hooks/useCreateFolder';
|
import { useCreateFolder } from '../hooks/useCreateFolder';
|
||||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||||
|
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
||||||
import { useHotkey } from '../hooks/useHotkey';
|
import { useHotkey } from '../hooks/useHotkey';
|
||||||
|
import { usePrompt } from '../hooks/usePrompt';
|
||||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||||
import { Dropdown } from './core/Dropdown';
|
import { Dropdown } from './core/Dropdown';
|
||||||
import { Icon } from './core/Icon';
|
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import { HStack } from './core/Stacks';
|
import { HStack } from './core/Stacks';
|
||||||
|
|
||||||
export const SidebarActions = memo(function SidebarActions() {
|
export const SidebarActions = memo(function SidebarActions() {
|
||||||
const createRequest = useCreateRequest();
|
const createRequest = useCreateRequest();
|
||||||
const createFolder = useCreateFolder();
|
const createFolder = useCreateFolder();
|
||||||
|
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
||||||
|
const prompt = usePrompt();
|
||||||
const { hidden, toggle } = useSidebarHidden();
|
const { hidden, toggle } = useSidebarHidden();
|
||||||
|
|
||||||
useHotkey('request.create', () => createRequest.mutate({}));
|
useHotkey('request.create', () => createRequest.mutate({}));
|
||||||
@@ -22,6 +25,7 @@ export const SidebarActions = memo(function SidebarActions() {
|
|||||||
className="pointer-events-auto"
|
className="pointer-events-auto"
|
||||||
size="sm"
|
size="sm"
|
||||||
title="Show sidebar"
|
title="Show sidebar"
|
||||||
|
hotkeyAction="sidebar.toggle"
|
||||||
icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'}
|
icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'}
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -29,15 +33,27 @@ export const SidebarActions = memo(function SidebarActions() {
|
|||||||
{
|
{
|
||||||
key: 'create-request',
|
key: 'create-request',
|
||||||
label: 'New Request',
|
label: 'New Request',
|
||||||
leftSlot: <Icon icon="plus" />,
|
hotkeyAction: 'request.create',
|
||||||
onSelect: () => createRequest.mutate({}),
|
onSelect: () => createRequest.mutate({}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'create-folder',
|
key: 'create-folder',
|
||||||
label: 'New Folder',
|
label: 'New Folder',
|
||||||
leftSlot: <Icon icon="plus" />,
|
|
||||||
onSelect: () => createFolder.mutate({}),
|
onSelect: () => createFolder.mutate({}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'create-workspace',
|
||||||
|
label: 'New Workspace',
|
||||||
|
onSelect: async () => {
|
||||||
|
const name = await prompt({
|
||||||
|
name: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
defaultValue: 'My Workspace',
|
||||||
|
title: 'New Workspace',
|
||||||
|
});
|
||||||
|
createWorkspace.mutate({ name });
|
||||||
|
},
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<IconButton size="sm" icon="plusCircle" title="Add Resource" />
|
<IconButton size="sm" icon="plusCircle" title="Add Resource" />
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import type {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useWindowSize } from 'react-use';
|
import { useWindowSize } from 'react-use';
|
||||||
import { useHotkey } from '../hooks/useHotkey';
|
|
||||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
|
||||||
import { useOsInfo } from '../hooks/useOsInfo';
|
import { useOsInfo } from '../hooks/useOsInfo';
|
||||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||||
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
||||||
@@ -31,7 +29,7 @@ const WINDOW_FLOATING_SIDEBAR_WIDTH = 600;
|
|||||||
|
|
||||||
export default function Workspace() {
|
export default function Workspace() {
|
||||||
const { setWidth, width, resetWidth } = useSidebarWidth();
|
const { setWidth, width, resetWidth } = useSidebarWidth();
|
||||||
const { hide, show, hidden, toggle } = useSidebarHidden();
|
const { hide, show, hidden } = useSidebarHidden();
|
||||||
|
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
const [floating, setFloating] = useState<boolean>(false);
|
const [floating, setFloating] = useState<boolean>(false);
|
||||||
@@ -40,8 +38,6 @@ export default function Workspace() {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkey('sidebar.toggle', toggle);
|
|
||||||
|
|
||||||
// float/un-float sidebar on window resize
|
// float/un-float sidebar on window resize
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH;
|
const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH;
|
||||||
|
|||||||
@@ -3,15 +3,9 @@ import classNames from 'classnames';
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||||
import { useAppVersion } from '../hooks/useAppVersion';
|
|
||||||
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
|
||||||
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
||||||
import { useExportData } from '../hooks/useExportData';
|
|
||||||
import { useImportData } from '../hooks/useImportData';
|
|
||||||
import { usePrompt } from '../hooks/usePrompt';
|
import { usePrompt } from '../hooks/usePrompt';
|
||||||
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||||
import { useTheme } from '../hooks/useTheme';
|
|
||||||
import { useUpdateMode } from '../hooks/useUpdateMode';
|
|
||||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||||
import type { ButtonProps } from './core/Button';
|
import type { ButtonProps } from './core/Button';
|
||||||
@@ -32,17 +26,11 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
const activeWorkspace = useActiveWorkspace();
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
||||||
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
|
||||||
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
||||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
||||||
const importData = useImportData();
|
|
||||||
const exportData = useExportData();
|
|
||||||
const { appearance, toggleAppearance } = useTheme();
|
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const prompt = usePrompt();
|
const prompt = usePrompt();
|
||||||
const routes = useAppRoutes();
|
const routes = useAppRoutes();
|
||||||
const appVersion = useAppVersion();
|
|
||||||
const [updateMode, setUpdateMode] = useUpdateMode();
|
|
||||||
|
|
||||||
const items: DropdownItem[] = useMemo(() => {
|
const items: DropdownItem[] = useMemo(() => {
|
||||||
const workspaceItems: DropdownItem[] = workspaces.map((w) => ({
|
const workspaceItems: DropdownItem[] = workspaces.map((w) => ({
|
||||||
@@ -134,67 +122,14 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
onSelect: deleteWorkspace.mutate,
|
onSelect: deleteWorkspace.mutate,
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
key: 'create-workspace',
|
|
||||||
label: 'New Workspace',
|
|
||||||
leftSlot: <Icon icon="plus" />,
|
|
||||||
onSelect: async () => {
|
|
||||||
const name = await prompt({
|
|
||||||
name: 'name',
|
|
||||||
label: 'Name',
|
|
||||||
defaultValue: 'My Workspace',
|
|
||||||
title: 'New Workspace',
|
|
||||||
});
|
|
||||||
createWorkspace.mutate({ name });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'import-data',
|
|
||||||
label: 'Import Data',
|
|
||||||
leftSlot: <Icon icon="download" />,
|
|
||||||
onSelect: () => importData.mutate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'export-data',
|
|
||||||
label: 'Export Data',
|
|
||||||
leftSlot: <Icon icon="upload" />,
|
|
||||||
onSelect: () => exportData.mutate(),
|
|
||||||
},
|
|
||||||
{ type: 'separator', label: `v${appVersion.data}` },
|
|
||||||
{
|
|
||||||
key: 'appearance',
|
|
||||||
label: 'Toggle Theme',
|
|
||||||
onSelect: toggleAppearance,
|
|
||||||
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'update-mode',
|
|
||||||
label: updateMode === 'stable' ? 'Enable Beta' : 'Disable Beta',
|
|
||||||
onSelect: () => setUpdateMode(updateMode === 'stable' ? 'beta' : 'stable'),
|
|
||||||
leftSlot: <Icon icon="camera" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'update-check',
|
|
||||||
label: 'Check for Updates',
|
|
||||||
onSelect: () => invoke('check_for_updates'),
|
|
||||||
leftSlot: <Icon icon="update" />,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}, [
|
}, [
|
||||||
activeWorkspace?.name,
|
activeWorkspace?.name,
|
||||||
activeWorkspaceId,
|
activeWorkspaceId,
|
||||||
appearance,
|
|
||||||
createWorkspace,
|
|
||||||
deleteWorkspace.mutate,
|
deleteWorkspace.mutate,
|
||||||
dialog,
|
dialog,
|
||||||
exportData,
|
|
||||||
importData,
|
|
||||||
prompt,
|
prompt,
|
||||||
routes,
|
routes,
|
||||||
setUpdateMode,
|
|
||||||
toggleAppearance,
|
|
||||||
updateMode,
|
|
||||||
updateWorkspace,
|
updateWorkspace,
|
||||||
workspaces,
|
workspaces,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { IconButton } from './core/IconButton';
|
|||||||
import { HStack } from './core/Stacks';
|
import { HStack } from './core/Stacks';
|
||||||
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
|
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
|
||||||
import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
||||||
import { RequestActionsDropdown } from './RequestActionsDropdown';
|
import { SettingsDropdown } from './SettingsDropdown';
|
||||||
import { SidebarActions } from './SidebarActions';
|
import { SidebarActions } from './SidebarActions';
|
||||||
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||||
|
|
||||||
@@ -36,14 +36,14 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
|||||||
<RecentRequestsDropdown />
|
<RecentRequestsDropdown />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex justify-end -mr-2 pointer-events-none">
|
<div className="flex-1 flex justify-end -mr-2 pointer-events-none">
|
||||||
<RequestActionsDropdown requestId={activeRequest?.id ?? null}>
|
<SettingsDropdown requestId={activeRequest?.id ?? null}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
title="Request Options"
|
title="Request Options"
|
||||||
icon="gear"
|
icon="gear"
|
||||||
className="pointer-events-auto"
|
className="pointer-events-auto"
|
||||||
/>
|
/>
|
||||||
</RequestActionsDropdown>
|
</SettingsDropdown>
|
||||||
</div>
|
</div>
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
|||||||
import type { HTMLAttributes, ReactNode } from 'react';
|
import type { HTMLAttributes, ReactNode } from 'react';
|
||||||
import { forwardRef, memo, useImperativeHandle, useMemo, useRef } from 'react';
|
import { forwardRef, memo, useImperativeHandle, useMemo, useRef } from 'react';
|
||||||
import type { HotkeyAction } from '../../hooks/useHotkey';
|
import type { HotkeyAction } from '../../hooks/useHotkey';
|
||||||
import { useHotkey } from '../../hooks/useHotkey';
|
import { useFormattedHotkey, useHotkey } from '../../hooks/useHotkey';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
const colorStyles = {
|
const colorStyles = {
|
||||||
@@ -47,11 +47,15 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|||||||
rightSlot,
|
rightSlot,
|
||||||
disabled,
|
disabled,
|
||||||
hotkeyAction,
|
hotkeyAction,
|
||||||
|
title,
|
||||||
onClick,
|
onClick,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps,
|
}: ButtonProps,
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
|
const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null);
|
||||||
|
const fullTitle = hotkeyTrigger ? `${title} ${hotkeyTrigger}` : title;
|
||||||
|
|
||||||
const classes = useMemo(
|
const classes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
classNames(
|
classNames(
|
||||||
@@ -88,6 +92,7 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|||||||
className={classes}
|
className={classes}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
title={fullTitle}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useKey, useKeyPressEvent, useWindowSize } from 'react-use';
|
import { useKey, useKeyPressEvent, useWindowSize } from 'react-use';
|
||||||
|
import type { HotkeyAction } from '../../hooks/useHotkey';
|
||||||
import { Overlay } from '../Overlay';
|
import { Overlay } from '../Overlay';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
|
import { HotKey } from './HotKey';
|
||||||
import { Separator } from './Separator';
|
import { Separator } from './Separator';
|
||||||
import { VStack } from './Stacks';
|
import { VStack } from './Stacks';
|
||||||
|
|
||||||
@@ -30,19 +32,20 @@ export type DropdownItemSeparator = {
|
|||||||
label?: string;
|
label?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DropdownItem =
|
export type DropdownItemDefault = {
|
||||||
| {
|
key: string;
|
||||||
key: string;
|
type?: 'default';
|
||||||
type?: 'default';
|
label: ReactNode;
|
||||||
label: ReactNode;
|
hotkeyAction?: HotkeyAction;
|
||||||
variant?: 'danger';
|
variant?: 'danger';
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
leftSlot?: ReactNode;
|
leftSlot?: ReactNode;
|
||||||
rightSlot?: ReactNode;
|
rightSlot?: ReactNode;
|
||||||
onSelect?: () => void;
|
onSelect?: () => void;
|
||||||
}
|
};
|
||||||
| DropdownItemSeparator;
|
|
||||||
|
export type DropdownItem = DropdownItemDefault | DropdownItemSeparator;
|
||||||
|
|
||||||
export interface DropdownProps {
|
export interface DropdownProps {
|
||||||
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
|
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
|
||||||
@@ -126,9 +129,10 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
|||||||
{open && triggerRect && (
|
{open && triggerRect && (
|
||||||
<Menu
|
<Menu
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
|
showTriangle
|
||||||
defaultSelectedIndex={defaultSelectedIndex}
|
defaultSelectedIndex={defaultSelectedIndex}
|
||||||
items={items}
|
items={items}
|
||||||
triggerRect={triggerRect}
|
triggerShape={triggerRect}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -136,16 +140,53 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface ContextMenuProps {
|
||||||
|
show: { x: number; y: number } | null;
|
||||||
|
className?: string;
|
||||||
|
items: DropdownProps['items'];
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContextMenu = forwardRef<DropdownRef, ContextMenuProps>(function ContextMenu(
|
||||||
|
{ show, className, items, onClose },
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
const triggerShape = useMemo(
|
||||||
|
() => ({
|
||||||
|
top: show?.y ?? 0,
|
||||||
|
bottom: show?.y ?? 0,
|
||||||
|
left: show?.x ?? 0,
|
||||||
|
right: show?.x ?? 0,
|
||||||
|
}),
|
||||||
|
[show],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (show === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
className={className}
|
||||||
|
ref={ref}
|
||||||
|
items={items}
|
||||||
|
onClose={onClose}
|
||||||
|
triggerShape={triggerShape}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
interface MenuProps {
|
interface MenuProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
defaultSelectedIndex?: number;
|
defaultSelectedIndex?: number;
|
||||||
items: DropdownProps['items'];
|
items: DropdownProps['items'];
|
||||||
triggerRect: DOMRect;
|
triggerShape: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right'>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
showTriangle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuProps>(function Menu(
|
const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuProps>(function Menu(
|
||||||
{ className, items, onClose, triggerRect, defaultSelectedIndex }: MenuProps,
|
{ className, items, onClose, triggerShape, defaultSelectedIndex, showTriangle }: MenuProps,
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -248,21 +289,27 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
|||||||
|
|
||||||
const { containerStyles, triangleStyles } = useMemo<{
|
const { containerStyles, triangleStyles } = useMemo<{
|
||||||
containerStyles: CSSProperties;
|
containerStyles: CSSProperties;
|
||||||
triangleStyles: CSSProperties;
|
triangleStyles: CSSProperties | null;
|
||||||
}>(() => {
|
}>(() => {
|
||||||
const docWidth = document.documentElement.getBoundingClientRect().width;
|
const docRect = document.documentElement.getBoundingClientRect();
|
||||||
const spaceRemaining = docWidth - triggerRect.left;
|
const width = triggerShape.right - triggerShape.left;
|
||||||
const top = triggerRect?.bottom + 5;
|
const hSpaceRemaining = docRect.width - triggerShape.left;
|
||||||
const onRight = spaceRemaining < 200;
|
const vSpaceRemaining = docRect.height - triggerShape.bottom;
|
||||||
const containerStyles = onRight
|
const top = triggerShape?.bottom + 5;
|
||||||
? { top, right: docWidth - triggerRect?.right }
|
const onRight = hSpaceRemaining < 200;
|
||||||
: { top, left: triggerRect?.left };
|
const upsideDown = vSpaceRemaining < 200;
|
||||||
|
const containerStyles = {
|
||||||
|
top: !upsideDown ? top : undefined,
|
||||||
|
bottom: upsideDown ? top : undefined,
|
||||||
|
right: onRight ? docRect.width - triggerShape?.right : undefined,
|
||||||
|
left: !onRight ? triggerShape?.left : undefined,
|
||||||
|
};
|
||||||
const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' };
|
const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' };
|
||||||
const triangleStyles = onRight
|
const triangleStyles = onRight
|
||||||
? { right: triggerRect.width / 2, marginRight: '-0.2rem', ...size }
|
? { right: width / 2, marginRight: '-0.2rem', ...size }
|
||||||
: { left: triggerRect.width / 2, marginLeft: '-0.2rem', ...size };
|
: { left: width / 2, marginLeft: '-0.2rem', ...size };
|
||||||
return { containerStyles, triangleStyles };
|
return { containerStyles, triangleStyles };
|
||||||
}, [triggerRect]);
|
}, [triggerShape]);
|
||||||
|
|
||||||
const handleFocus = useCallback(
|
const handleFocus = useCallback(
|
||||||
(i: DropdownItem) => {
|
(i: DropdownItem) => {
|
||||||
@@ -290,11 +337,13 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
|||||||
style={containerStyles}
|
style={containerStyles}
|
||||||
className={classNames(className, 'outline-none mt-1 pointer-events-auto fixed z-50')}
|
className={classNames(className, 'outline-none mt-1 pointer-events-auto fixed z-50')}
|
||||||
>
|
>
|
||||||
<span
|
{triangleStyles && showTriangle && (
|
||||||
aria-hidden
|
<span
|
||||||
style={triangleStyles}
|
aria-hidden
|
||||||
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
|
style={triangleStyles}
|
||||||
/>
|
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{containerStyles && (
|
{containerStyles && (
|
||||||
<VStack
|
<VStack
|
||||||
space={0.5}
|
space={0.5}
|
||||||
@@ -333,9 +382,9 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
|||||||
|
|
||||||
interface MenuItemProps {
|
interface MenuItemProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
item: DropdownItem;
|
item: DropdownItemDefault;
|
||||||
onSelect: (item: DropdownItem) => void;
|
onSelect: (item: DropdownItemDefault) => void;
|
||||||
onFocus: (item: DropdownItem) => void;
|
onFocus: (item: DropdownItemDefault) => void;
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,7 +408,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
|||||||
[focused],
|
[focused],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (item.type === 'separator') return <Separator className="my-1.5" />;
|
const rightSlot = item.rightSlot ?? <HotKey action={item.hotkeyAction ?? null} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -373,7 +422,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
justify="start"
|
justify="start"
|
||||||
leftSlot={item.leftSlot && <div className="pr-2 flex justify-start">{item.leftSlot}</div>}
|
leftSlot={item.leftSlot && <div className="pr-2 flex justify-start">{item.leftSlot}</div>}
|
||||||
rightSlot={item.rightSlot && <div className="ml-auto pl-3">{item.rightSlot}</div>}
|
rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap',
|
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap',
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import type { HotkeyAction } from '../../hooks/useHotkey';
|
||||||
|
import { useFormattedHotkey } from '../../hooks/useHotkey';
|
||||||
|
import { useOsInfo } from '../../hooks/useOsInfo';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modifier: 'Meta' | 'Control' | 'Shift';
|
action: HotkeyAction | null;
|
||||||
keyName: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys: Record<Props['modifier'], string> = {
|
export function HotKey({ action }: Props) {
|
||||||
Control: '⌃',
|
const osinfo = useOsInfo();
|
||||||
Meta: '⌘',
|
const label = useFormattedHotkey(action);
|
||||||
Shift: '⇧',
|
if (label === null || osinfo == null) {
|
||||||
};
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function HotKey({ modifier, keyName }: Props) {
|
|
||||||
return (
|
return (
|
||||||
<span className={classNames('text-sm text-gray-600')}>
|
<span className={classNames('text-sm text-gray-1000 text-opacity-disabled')}>{label}</span>
|
||||||
{keys[modifier]}
|
|
||||||
{keyName}
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,4 @@
|
|||||||
import {
|
import * as ReactIcons from '@radix-ui/react-icons';
|
||||||
ArchiveIcon,
|
|
||||||
CameraIcon,
|
|
||||||
CheckboxIcon,
|
|
||||||
CheckIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
ClockIcon,
|
|
||||||
CodeIcon,
|
|
||||||
ColorWheelIcon,
|
|
||||||
CopyIcon,
|
|
||||||
Cross2Icon,
|
|
||||||
DividerHorizontalIcon,
|
|
||||||
DotsHorizontalIcon,
|
|
||||||
DotsVerticalIcon,
|
|
||||||
DownloadIcon,
|
|
||||||
DragHandleDots2Icon,
|
|
||||||
EyeClosedIcon,
|
|
||||||
EyeOpenIcon,
|
|
||||||
GearIcon,
|
|
||||||
HamburgerMenuIcon,
|
|
||||||
HomeIcon,
|
|
||||||
ListBulletIcon,
|
|
||||||
MagicWandIcon,
|
|
||||||
MagnifyingGlassIcon,
|
|
||||||
MoonIcon,
|
|
||||||
OpenInNewWindowIcon,
|
|
||||||
PaperPlaneIcon,
|
|
||||||
Pencil2Icon,
|
|
||||||
PlusCircledIcon,
|
|
||||||
PlusIcon,
|
|
||||||
QuestionMarkIcon,
|
|
||||||
RowsIcon,
|
|
||||||
SunIcon,
|
|
||||||
TrashIcon,
|
|
||||||
TriangleDownIcon,
|
|
||||||
TriangleLeftIcon,
|
|
||||||
TriangleRightIcon,
|
|
||||||
UpdateIcon,
|
|
||||||
UploadIcon,
|
|
||||||
} from '@radix-ui/react-icons';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@@ -46,47 +6,49 @@ import { ReactComponent as LeftPanelHiddenIcon } from '../../assets/icons/LeftPa
|
|||||||
import { ReactComponent as LeftPanelVisibleIcon } from '../../assets/icons/LeftPanelVisibleIcon.svg';
|
import { ReactComponent as LeftPanelVisibleIcon } from '../../assets/icons/LeftPanelVisibleIcon.svg';
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
archive: ArchiveIcon,
|
archive: ReactIcons.ArchiveIcon,
|
||||||
camera: CameraIcon,
|
camera: ReactIcons.CameraIcon,
|
||||||
check: CheckIcon,
|
check: ReactIcons.CheckIcon,
|
||||||
checkbox: CheckboxIcon,
|
checkbox: ReactIcons.CheckboxIcon,
|
||||||
clock: ClockIcon,
|
clock: ReactIcons.ClockIcon,
|
||||||
chevronDown: ChevronDownIcon,
|
chevronDown: ReactIcons.ChevronDownIcon,
|
||||||
chevronRight: ChevronRightIcon,
|
chevronRight: ReactIcons.ChevronRightIcon,
|
||||||
code: CodeIcon,
|
code: ReactIcons.CodeIcon,
|
||||||
colorWheel: ColorWheelIcon,
|
colorWheel: ReactIcons.ColorWheelIcon,
|
||||||
copy: CopyIcon,
|
copy: ReactIcons.CopyIcon,
|
||||||
dividerH: DividerHorizontalIcon,
|
dividerH: ReactIcons.DividerHorizontalIcon,
|
||||||
dotsH: DotsHorizontalIcon,
|
dotsH: ReactIcons.DotsHorizontalIcon,
|
||||||
dotsV: DotsVerticalIcon,
|
dotsV: ReactIcons.DotsVerticalIcon,
|
||||||
download: DownloadIcon,
|
download: ReactIcons.DownloadIcon,
|
||||||
drag: DragHandleDots2Icon,
|
drag: ReactIcons.DragHandleDots2Icon,
|
||||||
eye: EyeOpenIcon,
|
eye: ReactIcons.EyeOpenIcon,
|
||||||
eyeClosed: EyeClosedIcon,
|
eyeClosed: ReactIcons.EyeClosedIcon,
|
||||||
gear: GearIcon,
|
gear: ReactIcons.GearIcon,
|
||||||
hamburger: HamburgerMenuIcon,
|
hamburger: ReactIcons.HamburgerMenuIcon,
|
||||||
home: HomeIcon,
|
home: ReactIcons.HomeIcon,
|
||||||
|
listBullet: ReactIcons.ListBulletIcon,
|
||||||
|
magicWand: ReactIcons.MagicWandIcon,
|
||||||
|
magnifyingGlass: ReactIcons.MagnifyingGlassIcon,
|
||||||
|
moon: ReactIcons.MoonIcon,
|
||||||
|
openNewWindow: ReactIcons.OpenInNewWindowIcon,
|
||||||
|
paperPlane: ReactIcons.PaperPlaneIcon,
|
||||||
|
pencil: ReactIcons.Pencil2Icon,
|
||||||
|
plus: ReactIcons.PlusIcon,
|
||||||
|
plusCircle: ReactIcons.PlusCircledIcon,
|
||||||
|
question: ReactIcons.QuestionMarkIcon,
|
||||||
|
rows: ReactIcons.RowsIcon,
|
||||||
|
sun: ReactIcons.SunIcon,
|
||||||
|
trash: ReactIcons.TrashIcon,
|
||||||
|
triangleDown: ReactIcons.TriangleDownIcon,
|
||||||
|
triangleLeft: ReactIcons.TriangleLeftIcon,
|
||||||
|
triangleRight: ReactIcons.TriangleRightIcon,
|
||||||
|
update: ReactIcons.UpdateIcon,
|
||||||
|
upload: ReactIcons.UploadIcon,
|
||||||
|
x: ReactIcons.Cross2Icon,
|
||||||
|
|
||||||
|
// Custom
|
||||||
leftPanelHidden: LeftPanelHiddenIcon,
|
leftPanelHidden: LeftPanelHiddenIcon,
|
||||||
leftPanelVisible: LeftPanelVisibleIcon,
|
leftPanelVisible: LeftPanelVisibleIcon,
|
||||||
listBullet: ListBulletIcon,
|
|
||||||
magicWand: MagicWandIcon,
|
|
||||||
magnifyingGlass: MagnifyingGlassIcon,
|
|
||||||
moon: MoonIcon,
|
|
||||||
openNewWindow: OpenInNewWindowIcon,
|
|
||||||
paperPlane: PaperPlaneIcon,
|
|
||||||
pencil: Pencil2Icon,
|
|
||||||
plus: PlusIcon,
|
|
||||||
plusCircle: PlusCircledIcon,
|
|
||||||
question: QuestionMarkIcon,
|
|
||||||
rows: RowsIcon,
|
|
||||||
sun: SunIcon,
|
|
||||||
trash: TrashIcon,
|
|
||||||
triangleDown: TriangleDownIcon,
|
|
||||||
triangleLeft: TriangleLeftIcon,
|
|
||||||
triangleRight: TriangleRightIcon,
|
|
||||||
update: UpdateIcon,
|
|
||||||
upload: UploadIcon,
|
|
||||||
x: Cross2Icon,
|
|
||||||
empty: (props: HTMLAttributes<HTMLSpanElement>) => <span {...props} />,
|
empty: (props: HTMLAttributes<HTMLSpanElement>) => <span {...props} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
|||||||
},
|
},
|
||||||
[onClick, setConfirmed, showConfirm],
|
[onClick, setConfirmed, showConfirm],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -24,11 +24,7 @@ export function useCreateEnvironment() {
|
|||||||
label: 'Name',
|
label: 'Name',
|
||||||
defaultValue: 'My Environment',
|
defaultValue: 'My Environment',
|
||||||
});
|
});
|
||||||
const variables =
|
return invoke('create_environment', { name, variables: [], workspaceId });
|
||||||
environments.length === 0 && workspaces.length === 1
|
|
||||||
? [{ name: 'first_variable', value: 'some reusable value' }]
|
|
||||||
: [];
|
|
||||||
return invoke('create_environment', { name, variables, workspaceId });
|
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('environment', 'create'),
|
onSettled: () => trackEvent('environment', 'create'),
|
||||||
onSuccess: async (environment) => {
|
onSuccess: async (environment) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useOsInfo } from './useOsInfo';
|
||||||
|
|
||||||
export type HotkeyAction =
|
export type HotkeyAction =
|
||||||
| 'request.send'
|
| 'request.send'
|
||||||
@@ -7,7 +8,7 @@ export type HotkeyAction =
|
|||||||
| 'sidebar.toggle'
|
| 'sidebar.toggle'
|
||||||
| 'sidebar.focus'
|
| 'sidebar.focus'
|
||||||
| 'urlBar.focus'
|
| 'urlBar.focus'
|
||||||
| 'environmentEditor.show';
|
| 'environmentEditor.toggle';
|
||||||
|
|
||||||
const hotkeys: Record<HotkeyAction, string[]> = {
|
const hotkeys: Record<HotkeyAction, string[]> = {
|
||||||
'request.send': ['Meta+Enter', 'Meta+r'],
|
'request.send': ['Meta+Enter', 'Meta+r'],
|
||||||
@@ -16,10 +17,18 @@ const hotkeys: Record<HotkeyAction, string[]> = {
|
|||||||
'sidebar.toggle': ['Meta+b'],
|
'sidebar.toggle': ['Meta+b'],
|
||||||
'sidebar.focus': ['Meta+1'],
|
'sidebar.focus': ['Meta+1'],
|
||||||
'urlBar.focus': ['Meta+l'],
|
'urlBar.focus': ['Meta+l'],
|
||||||
'environmentEditor.show': ['Meta+e'],
|
'environmentEditor.toggle': ['Meta+e'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useHotkey(action: HotkeyAction | null, callback: (e: KeyboardEvent) => void) {
|
export function useHotkey(action: HotkeyAction | null, callback: (e: KeyboardEvent) => void) {
|
||||||
|
useAnyHotkey((hkAction, e) => {
|
||||||
|
if (hkAction === action) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAnyHotkey(callback: (action: HotkeyAction, e: KeyboardEvent) => void) {
|
||||||
const currentKeys = useRef<Set<string>>(new Set());
|
const currentKeys = useRef<Set<string>>(new Set());
|
||||||
const callbackRef = useRef(callback);
|
const callbackRef = useRef(callback);
|
||||||
|
|
||||||
@@ -30,19 +39,17 @@ export function useHotkey(action: HotkeyAction | null, callback: (e: KeyboardEve
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
currentKeys.current.add(e.key);
|
currentKeys.current.add(e.key);
|
||||||
for (const [hkAction, hkKeys] of Object.entries(hotkeys)) {
|
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
|
||||||
for (const hkKey of hkKeys) {
|
for (const hkKey of hkKeys) {
|
||||||
const keys = hkKey.split('+');
|
const keys = hkKey.split('+');
|
||||||
if (
|
if (
|
||||||
keys.length === currentKeys.current.size &&
|
keys.length === currentKeys.current.size &&
|
||||||
keys.every((key) => currentKeys.current.has(key)) &&
|
keys.every((key) => currentKeys.current.has(key))
|
||||||
hkAction === action
|
|
||||||
) {
|
) {
|
||||||
// Triggered hotkey!
|
// Triggered hotkey!
|
||||||
console.log('TRIGGER!', action);
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
callbackRef.current(e);
|
callbackRef.current(hkAction, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,6 +63,45 @@ export function useHotkey(action: HotkeyAction | null, callback: (e: KeyboardEve
|
|||||||
window.removeEventListener('keydown', down);
|
window.removeEventListener('keydown', down);
|
||||||
window.removeEventListener('keyup', up);
|
window.removeEventListener('keyup', up);
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, []);
|
||||||
}, [action, callback]);
|
}
|
||||||
|
|
||||||
|
export function useFormattedHotkey(action: HotkeyAction | null): string | null {
|
||||||
|
const osInfo = useOsInfo();
|
||||||
|
const trigger = action != null ? hotkeys[action]?.[0] ?? null : null;
|
||||||
|
if (trigger == null || osInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const os = osInfo.osType;
|
||||||
|
const parts = trigger.split('+');
|
||||||
|
const labelParts: string[] = [];
|
||||||
|
|
||||||
|
for (const p of parts) {
|
||||||
|
if (os === 'Darwin') {
|
||||||
|
if (p === 'Meta') {
|
||||||
|
labelParts.push('⌘');
|
||||||
|
} else if (p === 'Shift') {
|
||||||
|
labelParts.push('⇧');
|
||||||
|
} else if (p === 'Control') {
|
||||||
|
labelParts.push('⌃');
|
||||||
|
} else if (p === 'Enter') {
|
||||||
|
labelParts.push('↩');
|
||||||
|
} else {
|
||||||
|
labelParts.push(p.toUpperCase());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (p === 'Meta') {
|
||||||
|
labelParts.push('Ctrl');
|
||||||
|
} else {
|
||||||
|
labelParts.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (os === 'Darwin') {
|
||||||
|
return labelParts.join('');
|
||||||
|
} else {
|
||||||
|
return labelParts.join('+');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user