mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-24 18:31:16 +01:00
Request history navigator
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Yaak App</title>
|
<title>Yaak App</title>
|
||||||
<!-- <script src="http://localhost:8097"></script>-->
|
<script src="http://localhost:8097"></script>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createBrowserRouter, Navigate, Outlet, RouterProvider } from 'react-router-dom';
|
import { createBrowserRouter, Navigate, Outlet, RouterProvider } from 'react-router-dom';
|
||||||
import { routePaths } from '../hooks/useRoutes';
|
import { routePaths } from '../hooks/useAppRoutes';
|
||||||
|
import { GlobalHooks } from './GlobalHooks';
|
||||||
import RouteError from './RouteError';
|
import RouteError from './RouteError';
|
||||||
import { TauriListeners } from './TauriListeners';
|
|
||||||
import Workspace from './Workspace';
|
import Workspace from './Workspace';
|
||||||
import Workspaces from './Workspaces';
|
import Workspaces from './Workspaces';
|
||||||
|
|
||||||
@@ -9,12 +9,7 @@ const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
errorElement: <RouteError />,
|
errorElement: <RouteError />,
|
||||||
element: (
|
element: <Layout />,
|
||||||
<>
|
|
||||||
<Outlet />
|
|
||||||
<TauriListeners />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -42,3 +37,12 @@ const router = createBrowserRouter([
|
|||||||
export function AppRouter() {
|
export function AppRouter() {
|
||||||
return <RouterProvider router={router} />;
|
return <RouterProvider router={router} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Layout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Outlet />
|
||||||
|
<GlobalHooks />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
|||||||
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
|
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
|
||||||
import { modelsEq } from '../lib/models';
|
import { modelsEq } from '../lib/models';
|
||||||
|
|
||||||
export function TauriListeners() {
|
export function GlobalHooks() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { wasUpdatedExternally } = useRequestUpdateKey(null);
|
const { wasUpdatedExternally } = useRequestUpdateKey(null);
|
||||||
|
|
||||||
106
src-web/components/RecentRequestsDropdown.tsx
Normal file
106
src-web/components/RecentRequestsDropdown.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
import { useKeyPressEvent } from 'react-use';
|
||||||
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
|
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||||
|
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||||
|
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||||
|
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||||
|
import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||||
|
import { useRequests } from '../hooks/useRequests';
|
||||||
|
import { Button } from './core/Button';
|
||||||
|
import { CountBadge } from './core/CountBadge';
|
||||||
|
import type { DropdownItem, DropdownRef } from './core/Dropdown';
|
||||||
|
import { Dropdown } from './core/Dropdown';
|
||||||
|
|
||||||
|
export function RecentRequestsDropdown() {
|
||||||
|
const dropdownRef = useRef<DropdownRef>(null);
|
||||||
|
|
||||||
|
useKeyPressEvent('Control', undefined, () => {
|
||||||
|
// Key up
|
||||||
|
dropdownRef.current?.select?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeyPressEvent('Tab', (e) => {
|
||||||
|
if (!e.ctrlKey) return;
|
||||||
|
if (!dropdownRef.current?.isOpen) {
|
||||||
|
// Set to 1 because the first item is the active request
|
||||||
|
dropdownRef.current?.open(e.shiftKey ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
dropdownRef.current?.prev?.();
|
||||||
|
} else {
|
||||||
|
dropdownRef.current?.next?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeRequest = useActiveRequest();
|
||||||
|
const activeWorkspaceId = useActiveWorkspaceId();
|
||||||
|
const recentRequestIds = useRecentRequests();
|
||||||
|
const requests = useRequests();
|
||||||
|
const routes = useAppRoutes();
|
||||||
|
const deleteRequest = useDeleteRequest(activeRequest?.id ?? null);
|
||||||
|
const duplicateRequest = useDuplicateRequest({
|
||||||
|
id: activeRequest?.id ?? null,
|
||||||
|
navigateAfter: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = useMemo<DropdownItem[]>(() => {
|
||||||
|
if (activeWorkspaceId === null) return [];
|
||||||
|
|
||||||
|
const recentRequestItems: DropdownItem[] = [];
|
||||||
|
for (const id of recentRequestIds) {
|
||||||
|
const request = requests.find((r) => r.id === id);
|
||||||
|
if (request === undefined) continue;
|
||||||
|
|
||||||
|
recentRequestItems.push({
|
||||||
|
label: request.name,
|
||||||
|
leftSlot: <CountBadge className="!mx-0" count={recentRequestItems.length + 1} />,
|
||||||
|
onSelect: () => {
|
||||||
|
routes.navigate('request', {
|
||||||
|
requestId: request.id,
|
||||||
|
workspaceId: activeWorkspaceId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show max 30 items
|
||||||
|
const fixedItems: DropdownItem[] = [
|
||||||
|
// {
|
||||||
|
// label: 'Duplicate',
|
||||||
|
// onSelect: duplicateRequest.mutate,
|
||||||
|
// leftSlot: <Icon icon="copy" />,
|
||||||
|
// rightSlot: <HotKey modifier="Meta" keyName="D" />,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: 'Delete',
|
||||||
|
// onSelect: deleteRequest.mutate,
|
||||||
|
// variant: 'danger',
|
||||||
|
// leftSlot: <Icon icon="trash" />,
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
// No recent requests to show
|
||||||
|
if (recentRequestItems.length === 0) {
|
||||||
|
return fixedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
// ...fixedItems,
|
||||||
|
// { type: 'separator', label: 'Recent Requests' },
|
||||||
|
...recentRequestItems.slice(0, 20),
|
||||||
|
];
|
||||||
|
}, [activeWorkspaceId, recentRequestIds, requests, routes]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown ref={dropdownRef} items={items}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="pointer-events-auto flex-[2] text-center text-gray-800 text-sm truncate pointer-events-none"
|
||||||
|
>
|
||||||
|
{activeRequest?.name}
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ export function RequestActionsDropdown({ requestId, children }: Props) {
|
|||||||
{
|
{
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
onSelect: deleteRequest.mutate,
|
onSelect: deleteRequest.mutate,
|
||||||
|
variant: 'danger',
|
||||||
leftSlot: <Icon icon="trash" />,
|
leftSlot: <Icon icon="trash" />,
|
||||||
},
|
},
|
||||||
{ type: 'separator', label: 'Yaak Settings' },
|
{ type: 'separator', label: 'Yaak Settings' },
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
|||||||
label="Request"
|
label="Request"
|
||||||
onChangeValue={setActiveTab}
|
onChangeValue={setActiveTab}
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
tabListClassName="mt-2"
|
tabListClassName="mt-2 !mb-1.5"
|
||||||
>
|
>
|
||||||
<TabContent value="auth">
|
<TabContent value="auth">
|
||||||
{activeRequest.authenticationType === AUTH_TYPE_BASIC ? (
|
{activeRequest.authenticationType === AUTH_TYPE_BASIC ? (
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
|||||||
<HStack
|
<HStack
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
className={classnames(
|
className={classnames(
|
||||||
'italic text-gray-700 text-sm w-full flex-shrink-0',
|
'text-gray-700 text-sm w-full flex-shrink-0',
|
||||||
// Remove a bit of space because the tabs have lots too
|
// Remove a bit of space because the tabs have lots too
|
||||||
'-mb-1.5',
|
'-mb-1.5',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import classnames from 'classnames';
|
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 { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
||||||
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
||||||
import { usePrompt } from '../hooks/usePrompt';
|
import { usePrompt } from '../hooks/usePrompt';
|
||||||
import { useRoutes } from '../hooks/useRoutes';
|
|
||||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
@@ -25,7 +25,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
|
|||||||
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
||||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
||||||
const prompt = usePrompt();
|
const prompt = usePrompt();
|
||||||
const routes = useRoutes();
|
const routes = useAppRoutes();
|
||||||
|
|
||||||
const items: DropdownItem[] = useMemo(() => {
|
const items: DropdownItem[] = useMemo(() => {
|
||||||
const workspaceItems = workspaces.map((w) => ({
|
const workspaceItems = workspaces.map((w) => ({
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { memo } from 'react';
|
|||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import { HStack } from './core/Stacks';
|
import { HStack } from './core/Stacks';
|
||||||
|
import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
||||||
import { RequestActionsDropdown } from './RequestActionsDropdown';
|
import { RequestActionsDropdown } from './RequestActionsDropdown';
|
||||||
import { SidebarActions } from './SidebarActions';
|
import { SidebarActions } from './SidebarActions';
|
||||||
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||||
@@ -24,8 +25,8 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
|||||||
<SidebarActions />
|
<SidebarActions />
|
||||||
<WorkspaceActionsDropdown className="pointer-events-auto" />
|
<WorkspaceActionsDropdown className="pointer-events-auto" />
|
||||||
</HStack>
|
</HStack>
|
||||||
<div className="flex-[2] text-center text-gray-800 text-sm truncate pointer-events-none">
|
<div>
|
||||||
{activeRequest?.name}
|
<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">
|
||||||
{activeRequest && (
|
{activeRequest && (
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import { useRoutes } from '../hooks/useRoutes';
|
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||||
import { Heading } from './core/Heading';
|
import { Heading } from './core/Heading';
|
||||||
|
|
||||||
export default function Workspaces() {
|
export default function Workspaces() {
|
||||||
const routes = useRoutes();
|
const routes = useAppRoutes();
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
const workspace = workspaces[0];
|
const workspace = workspaces[0];
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
count: number;
|
count: number;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CountBadge({ count }: Props) {
|
export function CountBadge({ count, className }: Props) {
|
||||||
if (count === 0) return null;
|
if (count === 0) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
aria-hidden
|
||||||
aria-hidden
|
className={classnames(
|
||||||
className="opacity-70 border border-highlight text-3xs rounded mb-0.5 px-1 ml-1 h-4 font-mono"
|
className,
|
||||||
>
|
'opacity-70 border border-highlight text-3xs rounded mb-0.5 px-1 ml-1 h-4 font-mono',
|
||||||
{count}
|
)}
|
||||||
</div>
|
>
|
||||||
</>
|
{count}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode
|
|||||||
import React, {
|
import React, {
|
||||||
Children,
|
Children,
|
||||||
cloneElement,
|
cloneElement,
|
||||||
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@@ -39,21 +41,50 @@ export interface DropdownProps {
|
|||||||
items: DropdownItem[];
|
items: DropdownItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dropdown({ children, items }: DropdownProps) {
|
export interface DropdownRef {
|
||||||
|
isOpen: boolean;
|
||||||
|
open: (activeIndex?: number) => void;
|
||||||
|
close?: () => void;
|
||||||
|
next?: () => void;
|
||||||
|
prev?: () => void;
|
||||||
|
select?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
|
||||||
|
{ children, items }: DropdownProps,
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
const ref = useRef<HTMLButtonElement>(null);
|
const [defaultSelectedIndex, setDefaultSelectedIndex] = useState<number>();
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
...menuRef.current,
|
||||||
|
isOpen: open,
|
||||||
|
open: (activeIndex?: number) => {
|
||||||
|
if (activeIndex === undefined) {
|
||||||
|
setDefaultSelectedIndex(undefined);
|
||||||
|
} else {
|
||||||
|
setDefaultSelectedIndex(activeIndex >= 0 ? activeIndex : items.length + activeIndex);
|
||||||
|
}
|
||||||
|
setOpen(true);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const child = useMemo(() => {
|
const child = useMemo(() => {
|
||||||
const existingChild = Children.only(children);
|
const existingChild = Children.only(children);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const props: any = {
|
const props: any = {
|
||||||
...existingChild.props,
|
...existingChild.props,
|
||||||
ref,
|
ref: buttonRef,
|
||||||
'aria-haspopup': 'true',
|
'aria-haspopup': 'true',
|
||||||
onClick:
|
onClick:
|
||||||
existingChild.props?.onClick ??
|
existingChild.props?.onClick ??
|
||||||
((e: MouseEvent<HTMLButtonElement>) => {
|
((e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
setDefaultSelectedIndex(undefined);
|
||||||
setOpen((o) => !o);
|
setOpen((o) => !o);
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -62,37 +93,48 @@ export function Dropdown({ children, items }: DropdownProps) {
|
|||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
ref.current?.focus();
|
buttonRef.current?.focus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ref.current?.setAttribute('aria-expanded', open.toString());
|
buttonRef.current?.setAttribute('aria-expanded', open.toString());
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const triggerRect = useMemo(() => {
|
const triggerRect = useMemo(() => {
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
return ref.current?.getBoundingClientRect();
|
return buttonRef.current?.getBoundingClientRect();
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{child}
|
{child}
|
||||||
{open && triggerRect && (
|
{open && triggerRect && (
|
||||||
<Menu items={items} triggerRect={triggerRect} onClose={handleClose} />
|
<Menu
|
||||||
|
ref={menuRef}
|
||||||
|
defaultSelectedIndex={defaultSelectedIndex}
|
||||||
|
items={items}
|
||||||
|
triggerRect={triggerRect}
|
||||||
|
onClose={handleClose}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
interface MenuProps {
|
interface MenuProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
defaultSelectedIndex?: number;
|
||||||
items: DropdownProps['items'];
|
items: DropdownProps['items'];
|
||||||
triggerRect: DOMRect;
|
triggerRect: DOMRect;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen'>, MenuProps>(function Menu(
|
||||||
|
{ className, items, onClose, triggerRect, defaultSelectedIndex }: MenuProps,
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(defaultSelectedIndex ?? null);
|
||||||
const [menuStyles, setMenuStyles] = useState<CSSProperties>({});
|
const [menuStyles, setMenuStyles] = useState<CSSProperties>({});
|
||||||
|
|
||||||
// Calculate the max height so we can scroll
|
// Calculate the max height so we can scroll
|
||||||
@@ -119,8 +161,7 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
|||||||
onClose();
|
onClose();
|
||||||
});
|
});
|
||||||
|
|
||||||
useKeyPressEvent('ArrowUp', (e) => {
|
const handlePrev = useCallback(() => {
|
||||||
e.preventDefault();
|
|
||||||
setSelectedIndex((currIndex) => {
|
setSelectedIndex((currIndex) => {
|
||||||
let nextIndex = (currIndex ?? 0) - 1;
|
let nextIndex = (currIndex ?? 0) - 1;
|
||||||
const maxTries = items.length;
|
const maxTries = items.length;
|
||||||
@@ -135,10 +176,9 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
|||||||
}
|
}
|
||||||
return nextIndex;
|
return nextIndex;
|
||||||
});
|
});
|
||||||
});
|
}, [items]);
|
||||||
|
|
||||||
useKeyPressEvent('ArrowDown', (e) => {
|
const handleNext = useCallback(() => {
|
||||||
e.preventDefault();
|
|
||||||
setSelectedIndex((currIndex) => {
|
setSelectedIndex((currIndex) => {
|
||||||
let nextIndex = (currIndex ?? -1) + 1;
|
let nextIndex = (currIndex ?? -1) + 1;
|
||||||
const maxTries = items.length;
|
const maxTries = items.length;
|
||||||
@@ -153,8 +193,44 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
|||||||
}
|
}
|
||||||
return nextIndex;
|
return nextIndex;
|
||||||
});
|
});
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
useKeyPressEvent('ArrowUp', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePrev();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useKeyPressEvent('ArrowDown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleNext();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSelect = useCallback(
|
||||||
|
(i: DropdownItem) => {
|
||||||
|
onClose();
|
||||||
|
setSelectedIndex(null);
|
||||||
|
if (i.type !== 'separator') {
|
||||||
|
i.onSelect?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
close: onClose,
|
||||||
|
prev: handlePrev,
|
||||||
|
next: handleNext,
|
||||||
|
select: () => {
|
||||||
|
const item = items[selectedIndex ?? -1] ?? null;
|
||||||
|
if (!item) return;
|
||||||
|
handleSelect(item);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[handleNext, handlePrev, handleSelect, items, onClose, selectedIndex],
|
||||||
|
);
|
||||||
|
|
||||||
const { containerStyles, triangleStyles } = useMemo<{
|
const { containerStyles, triangleStyles } = useMemo<{
|
||||||
containerStyles: CSSProperties;
|
containerStyles: CSSProperties;
|
||||||
triangleStyles: CSSProperties;
|
triangleStyles: CSSProperties;
|
||||||
@@ -173,17 +249,6 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
|||||||
return { containerStyles, triangleStyles };
|
return { containerStyles, triangleStyles };
|
||||||
}, [triggerRect]);
|
}, [triggerRect]);
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
|
||||||
(i: DropdownItem) => {
|
|
||||||
onClose();
|
|
||||||
setSelectedIndex(null);
|
|
||||||
if (i.type !== 'separator') {
|
|
||||||
i.onSelect?.();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onClose],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFocus = useCallback(
|
const handleFocus = useCallback(
|
||||||
(i: DropdownItem) => {
|
(i: DropdownItem) => {
|
||||||
const index = items.findIndex((item) => item === i) ?? null;
|
const index = items.findIndex((item) => item === i) ?? null;
|
||||||
@@ -192,8 +257,6 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
|||||||
[items],
|
[items],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal name="dropdown">
|
<Portal name="dropdown">
|
||||||
<FocusTrap>
|
<FocusTrap>
|
||||||
@@ -251,7 +314,7 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
|||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
interface MenuItemProps {
|
interface MenuItemProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -293,7 +356,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{item.leftSlot && <div className="w-6">{item.leftSlot}</div>}
|
{item.leftSlot && <div className="w-6 flex justify-start">{item.leftSlot}</div>}
|
||||||
<div>{item.label}</div>
|
<div>{item.label}</div>
|
||||||
{item.rightSlot && <div className="ml-auto pl-3">{item.rightSlot}</div>}
|
{item.rightSlot && <div className="ml-auto pl-3">{item.rightSlot}</div>}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import type { RouteParamsRequest } from './useRoutes';
|
import type { RouteParamsRequest } from './useAppRoutes';
|
||||||
|
|
||||||
export function useActiveRequestId(): string | null {
|
export function useActiveRequestId(): string | null {
|
||||||
const { requestId } = useParams<RouteParamsRequest>();
|
const { requestId } = useParams<RouteParamsRequest>();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import type { RouteParamsWorkspace } from './useRoutes';
|
import type { RouteParamsWorkspace } from './useAppRoutes';
|
||||||
|
|
||||||
export function useActiveWorkspaceId(): string | null {
|
export function useActiveWorkspaceId(): string | null {
|
||||||
const { workspaceId } = useParams<RouteParamsWorkspace>();
|
const { workspaceId } = useParams<RouteParamsWorkspace>();
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const routePaths = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useRoutes() {
|
export function useAppRoutes() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -2,12 +2,12 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import type { HttpRequest } from '../lib/models';
|
import type { HttpRequest } from '../lib/models';
|
||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
|
import { useAppRoutes } from './useAppRoutes';
|
||||||
import { requestsQueryKey, useRequests } from './useRequests';
|
import { requestsQueryKey, useRequests } from './useRequests';
|
||||||
import { useRoutes } from './useRoutes';
|
|
||||||
|
|
||||||
export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean }) {
|
export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean }) {
|
||||||
const workspaceId = useActiveWorkspaceId();
|
const workspaceId = useActiveWorkspaceId();
|
||||||
const routes = useRoutes();
|
const routes = useAppRoutes();
|
||||||
const requests = useRequests();
|
const requests = useRequests();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import type { Workspace } from '../lib/models';
|
import type { Workspace } from '../lib/models';
|
||||||
import { useRoutes } from './useRoutes';
|
import { useAppRoutes } from './useAppRoutes';
|
||||||
import { workspacesQueryKey } from './useWorkspaces';
|
import { workspacesQueryKey } from './useWorkspaces';
|
||||||
|
|
||||||
export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }) {
|
export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }) {
|
||||||
const routes = useRoutes();
|
const routes = useAppRoutes();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<Workspace, unknown, Pick<Workspace, 'name'>>({
|
return useMutation<Workspace, unknown, Pick<Workspace, 'name'>>({
|
||||||
mutationFn: (patch) => {
|
mutationFn: (patch) => {
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import { invoke } from '@tauri-apps/api';
|
|||||||
import { InlineCode } from '../components/core/InlineCode';
|
import { InlineCode } from '../components/core/InlineCode';
|
||||||
import type { Workspace } from '../lib/models';
|
import type { Workspace } from '../lib/models';
|
||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
|
import { useAppRoutes } from './useAppRoutes';
|
||||||
import { useConfirm } from './useConfirm';
|
import { useConfirm } from './useConfirm';
|
||||||
import { requestsQueryKey } from './useRequests';
|
import { requestsQueryKey } from './useRequests';
|
||||||
import { useRoutes } from './useRoutes';
|
|
||||||
import { workspacesQueryKey } from './useWorkspaces';
|
import { workspacesQueryKey } from './useWorkspaces';
|
||||||
|
|
||||||
export function useDeleteWorkspace(workspace: Workspace | null) {
|
export function useDeleteWorkspace(workspace: Workspace | null) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const activeWorkspaceId = useActiveWorkspaceId();
|
const activeWorkspaceId = useActiveWorkspaceId();
|
||||||
const routes = useRoutes();
|
const routes = useAppRoutes();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
return useMutation<Workspace | null, string>({
|
return useMutation<Workspace | null, string>({
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import type { HttpRequest } from '../lib/models';
|
import type { HttpRequest } from '../lib/models';
|
||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
|
import { useAppRoutes } from './useAppRoutes';
|
||||||
import { requestsQueryKey } from './useRequests';
|
import { requestsQueryKey } from './useRequests';
|
||||||
import { useRoutes } from './useRoutes';
|
|
||||||
|
|
||||||
export function useDuplicateRequest({
|
export function useDuplicateRequest({
|
||||||
id,
|
id,
|
||||||
@@ -13,7 +13,7 @@ export function useDuplicateRequest({
|
|||||||
navigateAfter: boolean;
|
navigateAfter: boolean;
|
||||||
}) {
|
}) {
|
||||||
const workspaceId = useActiveWorkspaceId();
|
const workspaceId = useActiveWorkspaceId();
|
||||||
const routes = useRoutes();
|
const routes = useAppRoutes();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<HttpRequest, string>({
|
return useMutation<HttpRequest, string>({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
|
|||||||
31
src-web/hooks/useRecentRequests.ts
Normal file
31
src-web/hooks/useRecentRequests.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { createGlobalState, useEffectOnce, useLocalStorage } from 'react-use';
|
||||||
|
import { useActiveRequestId } from './useActiveRequestId';
|
||||||
|
|
||||||
|
const useHistoryState = createGlobalState<string[]>([]);
|
||||||
|
|
||||||
|
export function useRecentRequests() {
|
||||||
|
const [history, setHistory] = useHistoryState();
|
||||||
|
const activeRequestId = useActiveRequestId();
|
||||||
|
const [lsState, setLSState] = useLocalStorage<string[]>('recent_requests', []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLSState(history);
|
||||||
|
}, [history, setLSState]);
|
||||||
|
|
||||||
|
useEffectOnce(() => {
|
||||||
|
if (lsState) {
|
||||||
|
setHistory(lsState);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHistory((h: string[]) => {
|
||||||
|
if (activeRequestId === null) return h;
|
||||||
|
const withoutCurrentRequest = h.filter((id) => id !== activeRequestId);
|
||||||
|
return [activeRequestId, ...withoutCurrentRequest];
|
||||||
|
});
|
||||||
|
}, [activeRequestId, setHistory]);
|
||||||
|
|
||||||
|
return history;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user