Performance sweep (#147)

This commit is contained in:
Gregory Schier
2024-12-20 17:31:15 -08:00
committed by GitHub
parent 42bf016e90
commit 27134a52ad
85 changed files with 2337 additions and 1413 deletions

1134
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +0,0 @@
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { MotionConfig } from 'framer-motion';
import React, { Suspense } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { HelmetProvider } from 'react-helmet-async';
import { AppRouter } from './AppRouter';
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (err, query) => {
console.log('Query client error', { err, query });
},
}),
defaultOptions: {
queries: {
retry: false,
networkMode: 'always',
refetchOnWindowFocus: true,
refetchOnReconnect: false,
refetchOnMount: false, // Don't refetch when a hook mounts
},
},
});
const ENABLE_REACT_QUERY_DEVTOOLS = false;
export function App() {
return (
<QueryClientProvider client={queryClient}>
{ENABLE_REACT_QUERY_DEVTOOLS && <ReactQueryDevtools buttonPosition="bottom-left" />}
<MotionConfig transition={{ duration: 0.1 }}>
<HelmetProvider>
<DndProvider backend={HTML5Backend}>
<Suspense>
<AppRouter />
</Suspense>
</DndProvider>
</HelmetProvider>
</MotionConfig>
</QueryClientProvider>
);
}

View File

@@ -1,85 +0,0 @@
import { lazy } from 'react';
import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom';
import { paths, useAppRoutes } from '../hooks/useAppRoutes';
import { DefaultLayout } from './DefaultLayout';
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
import RouteError from './RouteError';
const LazyWorkspace = lazy(() => import('./Workspace'));
const LazySettings = lazy(() => import('./Settings/Settings'));
const router = createBrowserRouter([
{
path: '/',
errorElement: <RouteError />,
element: <DefaultLayout />,
children: [
{
path: '/',
element: <RedirectToLatestWorkspace />,
},
{
path: paths.workspaces(),
element: <RedirectToLatestWorkspace />,
},
{
path: paths.workspace({
workspaceId: ':workspaceId',
environmentId: null,
cookieJarId: null,
}),
element: <LazyWorkspace />,
},
{
path: paths.request({
workspaceId: ':workspaceId',
requestId: ':requestId',
environmentId: null,
cookieJarId: null,
}),
element: <LazyWorkspace />,
},
{
path: '/workspaces/:workspaceId/environments/:environmentId/requests/:requestId',
element: <RedirectLegacyEnvironmentURLs />,
},
{
path: paths
.workspaceSettings({
workspaceId: ':workspaceId',
})
.replace(/\?.*/, ''),
element: <LazySettings />,
},
],
},
]);
export function AppRouter() {
return <RouterProvider router={router} />;
}
function RedirectLegacyEnvironmentURLs() {
const routes = useAppRoutes();
const {
requestId,
environmentId: rawEnvironmentId,
workspaceId,
} = useParams<{
requestId?: string;
workspaceId?: string;
environmentId?: string;
}>();
const environmentId = (rawEnvironmentId === '__default__' ? undefined : rawEnvironmentId) ?? null;
let to;
if (workspaceId != null && requestId != null) {
to = routes.paths.request({ workspaceId, environmentId, requestId, cookieJarId: null });
} else if (workspaceId != null) {
to = routes.paths.workspace({ workspaceId, environmentId, cookieJarId: null });
} else {
to = routes.paths.workspaces();
}
return <Navigate to={to} />;
}

View File

@@ -1,12 +1,10 @@
import type { KeyboardEvent, ReactNode } from 'react';
import type { HotkeyAction } from '../hooks/useHotKey';
import classNames from 'classnames'; import classNames from 'classnames';
import { fuzzyFilter } from 'fuzzbunny'; import { fuzzyFilter } from 'fuzzbunny';
import type { KeyboardEvent, ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar'; import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment'; import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest'; import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest'; import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
@@ -14,6 +12,7 @@ import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDebouncedState } from '../hooks/useDebouncedState'; import { useDebouncedState } from '../hooks/useDebouncedState';
import { useDeleteRequest } from '../hooks/useDeleteRequest'; import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useEnvironments } from '../hooks/useEnvironments'; import { useEnvironments } from '../hooks/useEnvironments';
import type { HotkeyAction } from '../hooks/useHotKey';
import { useHotKey } from '../hooks/useHotKey'; import { useHotKey } from '../hooks/useHotKey';
import { useHttpRequestActions } from '../hooks/useHttpRequestActions'; import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
import { useOpenSettings } from '../hooks/useOpenSettings'; import { useOpenSettings } from '../hooks/useOpenSettings';
@@ -28,6 +27,8 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useWorkspaces } from '../hooks/useWorkspaces'; import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName'; import { fallbackRequestName } from '../lib/fallbackRequestName';
import { router } from '../main';
import { Route } from '../routes/workspaces/$workspaceId/requests/$requestId';
import { CookieDialog } from './CookieDialog'; import { CookieDialog } from './CookieDialog';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { Heading } from './core/Heading'; import { Heading } from './core/Heading';
@@ -58,7 +59,6 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const [selectedItemKey, setSelectedItemKey] = useState<string | null>(null); const [selectedItemKey, setSelectedItemKey] = useState<string | null>(null);
const [activeEnvironment, setActiveEnvironmentId] = useActiveEnvironment(); const [activeEnvironment, setActiveEnvironmentId] = useActiveEnvironment();
const httpRequestActions = useHttpRequestActions(); const httpRequestActions = useHttpRequestActions();
const routes = useAppRoutes();
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const environments = useEnvironments(); const environments = useEnvironments();
const recentEnvironments = useRecentEnvironments(); const recentEnvironments = useRecentEnvironments();
@@ -268,11 +268,13 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
</HStack> </HStack>
), ),
onSelect: () => { onSelect: () => {
return routes.navigate('request', { router.navigate({
workspaceId: r.workspaceId, to: Route.fullPath,
requestId: r.id, params: {
environmentId: activeEnvironment?.id ?? null, workspaceId: r.workspaceId,
cookieJarId: activeCookieJar?.id ?? null, requestId: r.id,
},
search: (prev) => ({ ...prev }),
}); });
}, },
}); });
@@ -313,9 +315,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
}, [ }, [
workspaceCommands, workspaceCommands,
sortedRequests, sortedRequests,
routes,
activeEnvironment?.id, activeEnvironment?.id,
activeCookieJar?.id,
sortedEnvironments, sortedEnvironments,
setActiveEnvironmentId, setActiveEnvironmentId,
sortedWorkspaces, sortedWorkspaces,

View File

@@ -1,30 +0,0 @@
import classNames from 'classnames';
import { Outlet } from 'react-router-dom';
import { useOsInfo } from '../hooks/useOsInfo';
import { DialogProvider, Dialogs } from './DialogContext';
import { ToastProvider, Toasts } from './ToastContext';
import { GlobalHooks } from './GlobalHooks';
export function DefaultLayout() {
const osInfo = useOsInfo();
return (
<DialogProvider>
<ToastProvider>
<>
{/* Must be inside all the providers, so they have access to them */}
<Toasts />
<Dialogs />
</>
<div
className={classNames(
'w-full h-full',
osInfo?.osType === 'linux' && 'border border-border-subtle',
)}
>
<Outlet />
</div>
<GlobalHooks />
</ToastProvider>
</DialogProvider>
);
}

View File

@@ -1,8 +1,13 @@
import { emit } from '@tauri-apps/api/event'; import { emit } from '@tauri-apps/api/event';
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin'; import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin';
import { useEnsureActiveCookieJar } from '../hooks/useActiveCookieJar'; import { useEnsureActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveRequest } from '../hooks/useActiveRequest';
import {useSubscribeActiveRequestId} from "../hooks/useActiveRequestId";
import {useSubscribeActiveWorkspaceId} from "../hooks/useActiveWorkspace";
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast'; import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import {useGenerateThemeCss} from "../hooks/useGenerateThemeCss"; import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
import { useGenerateThemeCss } from '../hooks/useGenerateThemeCss';
import { useHotKey } from '../hooks/useHotKey'; import { useHotKey } from '../hooks/useHotKey';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast'; import { useNotificationToast } from '../hooks/useNotificationToast';
@@ -11,10 +16,11 @@ import { useRecentCookieJars } from '../hooks/useRecentCookieJars';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import {useSyncFontSizeSetting} from "../hooks/useSyncFontSizeSetting"; import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
import {useSyncModelStores} from "../hooks/useSyncModelStores"; import { useSyncModelStores } from '../hooks/useSyncModelStores';
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels'; import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
import {useSyncZoomSetting} from "../hooks/useSyncZoomSetting"; import {useSyncWorkspaceRequestTitle} from "../hooks/useSyncWorkspaceRequestTitle";
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette'; import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
export function GlobalHooks() { export function GlobalHooks() {
@@ -22,6 +28,9 @@ export function GlobalHooks() {
useSyncZoomSetting(); useSyncZoomSetting();
useSyncFontSizeSetting(); useSyncFontSizeSetting();
useGenerateThemeCss(); useGenerateThemeCss();
useSyncWorkspaceRequestTitle();
useSubscribeActiveWorkspaceId();
useSubscribeActiveRequestId();
// Include here so they always update, even if no component references them // Include here so they always update, even if no component references them
useRecentWorkspaces(); useRecentWorkspaces();
@@ -35,6 +44,23 @@ export function GlobalHooks() {
useActiveWorkspaceChangedToast(); useActiveWorkspaceChangedToast();
useEnsureActiveCookieJar(); useEnsureActiveCookieJar();
const activeRequest = useActiveRequest();
const duplicateHttpRequest = useDuplicateHttpRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const duplicateGrpcRequest = useDuplicateGrpcRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
useHotKey('http_request.duplicate', async () => {
if (activeRequest?.model === 'http_request') {
await duplicateHttpRequest.mutateAsync();
} else {
await duplicateGrpcRequest.mutateAsync();
}
});
const toggleCommandPalette = useToggleCommandPalette(); const toggleCommandPalette = useToggleCommandPalette();
useHotKey('command_palette.toggle', toggleCommandPalette); useHotKey('command_palette.toggle', toggleCommandPalette);

View File

@@ -1,10 +1,11 @@
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models'; import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest'; import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useWorkspaces } from '../hooks/useWorkspaces'; import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName'; import { fallbackRequestName } from '../lib/fallbackRequestName';
import { router } from '../main';
import { Route } from '../routes/workspaces/$workspaceId/index';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode'; import { InlineCode } from './core/InlineCode';
import { Select } from './core/Select'; import { Select } from './core/Select';
@@ -22,7 +23,6 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
const updateHttpRequest = useUpdateAnyHttpRequest(); const updateHttpRequest = useUpdateAnyHttpRequest();
const updateGrpcRequest = useUpdateAnyGrpcRequest(); const updateGrpcRequest = useUpdateAnyGrpcRequest();
const toast = useToast(); const toast = useToast();
const routes = useAppRoutes();
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>(activeWorkspaceId); const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>(activeWorkspaceId);
return ( return (
@@ -71,10 +71,9 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
className="mr-auto min-w-[5rem]" className="mr-auto min-w-[5rem]"
onClick={() => { onClick={() => {
toast.hide('workspace-moved'); toast.hide('workspace-moved');
routes.navigate('workspace', { router.navigate({
workspaceId: selectedWorkspaceId, to: Route.fullPath,
cookieJarId: null, params: { workspaceId: selectedWorkspaceId },
environmentId: null,
}); });
}} }}
> >

View File

@@ -1,15 +1,14 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { useKeyPressEvent } from 'react-use'; import { useKeyPressEvent } from 'react-use';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useHotKey } from '../hooks/useHotKey'; import { useHotKey } from '../hooks/useHotKey';
import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRequests } from '../hooks/useRequests'; import { useRequests } from '../hooks/useRequests';
import { fallbackRequestName } from '../lib/fallbackRequestName'; import { fallbackRequestName } from '../lib/fallbackRequestName';
import { router } from '../main';
import { Route } from '../routes/workspaces/$workspaceId/requests/$requestId';
import type { ButtonProps } from './core/Button'; import type { ButtonProps } from './core/Button';
import { Button } from './core/Button'; import { Button } from './core/Button';
import type { DropdownItem, DropdownRef } from './core/Dropdown'; import type { DropdownItem, DropdownRef } from './core/Dropdown';
@@ -20,9 +19,6 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
const dropdownRef = useRef<DropdownRef>(null); const dropdownRef = useRef<DropdownRef>(null);
const activeRequest = useActiveRequest(); const activeRequest = useActiveRequest();
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
const routes = useAppRoutes();
const allRecentRequestIds = useRecentRequests(); const allRecentRequestIds = useRecentRequests();
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]); const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
const requests = useRequests(); const requests = useRequests();
@@ -57,11 +53,13 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
// leftSlot: <CountBadge className="!ml-0 px-0 w-5" count={recentRequestItems.length} />, // leftSlot: <CountBadge className="!ml-0 px-0 w-5" count={recentRequestItems.length} />,
leftSlot: <HttpMethodTag className="text-right" shortNames request={request} />, leftSlot: <HttpMethodTag className="text-right" shortNames request={request} />,
onSelect: () => { onSelect: () => {
routes.navigate('request', { router.navigate({
requestId: request.id, to: Route.fullPath,
workspaceId: activeWorkspace.id, params: {
environmentId: activeEnvironment?.id ?? null, requestId: request.id,
cookieJarId: activeCookieJar?.id ?? null, workspaceId: activeWorkspace.id,
},
search: (prev) => ({ ...prev }),
}); });
}, },
}); });
@@ -79,7 +77,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
} }
return recentRequestItems.slice(0, 20); return recentRequestItems.slice(0, 20);
}, [activeWorkspace, recentRequestIds, requests, routes, activeEnvironment?.id, activeCookieJar?.id]); }, [activeWorkspace, recentRequestIds, requests]);
return ( return (
<Dropdown ref={dropdownRef} items={items}> <Dropdown ref={dropdownRef} items={items}>

View File

@@ -1,15 +1,14 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { getRecentCookieJars } from '../hooks/useRecentCookieJars'; import { getRecentCookieJars } from '../hooks/useRecentCookieJars';
import { getRecentEnvironments } from '../hooks/useRecentEnvironments'; import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
import { getRecentRequests } from '../hooks/useRecentRequests'; import { getRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useWorkspaces } from '../hooks/useWorkspaces'; import { useWorkspaces } from '../hooks/useWorkspaces';
import { router } from '../main';
import { Route as WorkspaceRoute } from '../routes/workspaces/$workspaceId';
import { Route as RequestRoute } from '../routes/workspaces/$workspaceId/requests/$requestId';
export function RedirectToLatestWorkspace() { export function RedirectToLatestWorkspace() {
const navigate = useNavigate();
const routes = useAppRoutes();
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const recentWorkspaces = useRecentWorkspaces(); const recentWorkspaces = useRecentWorkspaces();
@@ -26,12 +25,20 @@ export function RedirectToLatestWorkspace() {
const requestId = (await getRecentRequests(workspaceId))[0] ?? null; const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
if (workspaceId != null && requestId != null) { if (workspaceId != null && requestId != null) {
navigate(routes.paths.request({ workspaceId, environmentId, requestId, cookieJarId })); await router.navigate({
to: RequestRoute.fullPath,
params: { workspaceId, requestId },
search: { cookieJarId, environmentId },
});
} else { } else {
navigate(routes.paths.workspace({ workspaceId, environmentId, cookieJarId })); await router.navigate({
to: WorkspaceRoute.fullPath,
params: { workspaceId },
search: { cookieJarId, environmentId },
});
} }
})(); })();
}, [navigate, recentWorkspaces, routes.paths, workspaces, workspaces.length]); }, [recentWorkspaces, workspaces, workspaces.length]);
return <></>; return <></>;
} }

View File

@@ -0,0 +1,162 @@
import React, { useMemo } from 'react';
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDuplicateFolder } from '../hooks/useDuplicateFolder';
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
import { useMoveToWorkspace } from '../hooks/useMoveToWorkspace';
import { useRenameRequest } from '../hooks/useRenameRequest';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSendManyRequests } from '../hooks/useSendManyRequests';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { getHttpRequest } from '../lib/store';
import type { DropdownItem } from './core/Dropdown';
import { ContextMenu } from './core/Dropdown';
import { Icon } from './core/Icon';
import { useDialog } from './DialogContext';
import { FolderSettingsDialog } from './FolderSettingsDialog';
import type { SidebarTreeNode } from './Sidebar';
interface Props {
child: SidebarTreeNode;
show: { x: number; y: number } | null;
close: () => void;
}
export function RequestContextMenu({ child, show, close }: Props) {
const sendManyRequests = useSendManyRequests();
const duplicateFolder = useDuplicateFolder(child.item.id);
const deleteFolder = useDeleteFolder(child.item.id);
const httpRequestActions = useHttpRequestActions();
const sendRequest = useSendAnyHttpRequest();
const workspaces = useWorkspaces();
const dialog = useDialog();
const deleteRequest = useDeleteRequest(child.item.id);
const renameRequest = useRenameRequest(child.item.id);
const duplicateHttpRequest = useDuplicateHttpRequest({ id: child.item.id, navigateAfter: true });
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: child.item.id, navigateAfter: true });
const moveToWorkspace = useMoveToWorkspace(child.item.id);
const createDropdownItems = useCreateDropdownItems({
folderId: child.item.model === 'folder' ? child.item.id : null,
});
const items = useMemo<DropdownItem[]>(() => {
if (child.item.model === 'folder') {
return [
{
key: 'send-all',
label: 'Send All',
leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
},
{
key: 'folder-settings',
label: 'Settings',
leftSlot: <Icon icon="settings" />,
onSelect: () =>
dialog.show({
id: 'folder-settings',
title: 'Folder Settings',
size: 'md',
render: () => <FolderSettingsDialog folderId={child.item.id} />,
}),
},
{
key: 'duplicateFolder',
label: 'Duplicate',
leftSlot: <Icon icon="copy" />,
onSelect: () => duplicateFolder.mutate(),
},
{
key: 'delete-folder',
label: 'Delete',
variant: 'danger',
leftSlot: <Icon icon="trash" />,
onSelect: () => deleteFolder.mutate(),
},
{ type: 'separator' },
...createDropdownItems,
];
} else {
const requestItems: DropdownItem[] =
child.item.model === 'http_request'
? [
{
key: 'send-request',
label: 'Send',
hotKeyAction: 'http_request.send',
hotKeyLabelOnly: true, // Already bound in URL bar
leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => sendRequest.mutate(child.item.id),
},
...httpRequestActions.map((a) => ({
key: a.key,
label: a.label,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
onSelect: async () => {
const request = await getHttpRequest(child.item.id);
if (request != null) await a.call(request);
},
})),
{ type: 'separator' },
]
: [];
return [
...requestItems,
{
key: 'rename-request',
label: 'Rename',
leftSlot: <Icon icon="pencil" />,
onSelect: renameRequest.mutate,
},
{
key: 'duplicate-request',
label: 'Duplicate',
hotKeyAction: 'http_request.duplicate',
hotKeyLabelOnly: true, // Would trigger for every request (bad)
leftSlot: <Icon icon="copy" />,
onSelect: () =>
child.item.model === 'http_request'
? duplicateHttpRequest.mutate()
: duplicateGrpcRequest.mutate(),
},
{
key: 'move-workspace',
label: 'Move',
leftSlot: <Icon icon="arrow_right_circle" />,
hidden: workspaces.length <= 1,
onSelect: moveToWorkspace.mutate,
},
{
key: 'delete-request',
variant: 'danger',
label: 'Delete',
leftSlot: <Icon icon="trash" />,
onSelect: () => deleteRequest.mutate(),
},
];
}
}, [
child.children,
child.item.id,
child.item.model,
createDropdownItems,
deleteFolder,
deleteRequest,
dialog,
duplicateFolder,
duplicateGrpcRequest,
duplicateHttpRequest,
httpRequestActions,
moveToWorkspace.mutate,
renameRequest.mutate,
sendManyRequests,
sendRequest,
workspaces.length,
]);
return <ContextMenu triggerPosition={show} items={items} onClose={close} />;
}

View File

@@ -1,5 +1,6 @@
import { useRouteError } from 'react-router-dom'; import { useRouteError } from 'react-router-dom';
import { useAppRoutes } from '../hooks/useAppRoutes'; import { router } from '../main';
import { Route } from '../routes/workspaces';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { FormattedError } from './core/FormattedError'; import { FormattedError } from './core/FormattedError';
import { Heading } from './core/Heading'; import { Heading } from './core/Heading';
@@ -11,7 +12,6 @@ export default function RouteError() {
const stringified = JSON.stringify(error); const stringified = JSON.stringify(error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const message = (error as any).message ?? stringified; const message = (error as any).message ?? stringified;
const routes = useAppRoutes();
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<VStack space={5} className="max-w-[50rem] !h-auto"> <VStack space={5} className="max-w-[50rem] !h-auto">
@@ -21,7 +21,7 @@ export default function RouteError() {
<Button <Button
color="primary" color="primary"
onClick={() => { onClick={() => {
routes.navigate('workspaces'); router.navigate({ to: Route.fullPath });
}} }}
> >
Go Home Go Home

View File

@@ -1,7 +1,6 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useKeyPressEvent } from 'react-use'; import { useKeyPressEvent } from 'react-use';
import { useOsInfo } from '../../hooks/useOsInfo'; import { useOsInfo } from '../../hooks/useOsInfo';
import { capitalize } from '../../lib/capitalize'; import { capitalize } from '../../lib/capitalize';
@@ -16,6 +15,7 @@ import { SettingsProxy } from './SettingsProxy';
interface Props { interface Props {
hide?: () => void; hide?: () => void;
defaultTab?: SettingsTab;
} }
export enum SettingsTab { export enum SettingsTab {
@@ -34,10 +34,9 @@ const tabs = [
SettingsTab.License, SettingsTab.License,
]; ];
export default function Settings({ hide }: Props) { export default function Settings({ hide, defaultTab }: Props) {
const osInfo = useOsInfo(); const osInfo = useOsInfo();
const [params] = useSearchParams(); const [tab, setTab] = useState<string>(defaultTab ?? SettingsTab.General);
const [tab, setTab] = useState<string>(params.get('tab') ?? SettingsTab.General);
// Close settings window on escape // Close settings window on escape
// TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window // TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window

View File

@@ -1,162 +1,105 @@
import type { import type { Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp-internal/models';
AnyModel,
Folder,
GrpcConnection,
GrpcRequest,
HttpRequest,
HttpResponse,
Workspace,
} from '@yaakapp-internal/models';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import { atom, useAtom } from 'jotai';
import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react'; import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { useKey, useKeyPressEvent } from 'react-use'; import { useKey, useKeyPressEvent } from 'react-use';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar'; import { getActiveRequest } from '../hooks/useActiveRequest';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems'; import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDuplicateFolder } from '../hooks/useDuplicateFolder';
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
import { useFolders } from '../hooks/useFolders'; import { useFolders } from '../hooks/useFolders';
import { useGrpcConnections } from '../hooks/useGrpcConnections'; import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useHotKey } from '../hooks/useHotKey'; import { useHotKey } from '../hooks/useHotKey';
import type { CallableHttpRequestAction } from '../hooks/useHttpRequestActions';
import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
import { useHttpResponses } from '../hooks/useHttpResponses'; import { useHttpResponses } from '../hooks/useHttpResponses';
import { useKeyValue } from '../hooks/useKeyValue'; import { useKeyValue } from '../hooks/useKeyValue';
import { useMoveToWorkspace } from '../hooks/useMoveToWorkspace';
import { useRenameRequest } from '../hooks/useRenameRequest';
import { useRequests } from '../hooks/useRequests'; import { useRequests } from '../hooks/useRequests';
import { useScrollIntoView } from '../hooks/useScrollIntoView';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSendManyRequests } from '../hooks/useSendManyRequests';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder'; import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest'; import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useWorkspaces } from '../hooks/useWorkspaces'; import { router } from '../main';
import { fallbackRequestName } from '../lib/fallbackRequestName'; import { Route } from '../routes/workspaces/$workspaceId/requests/$requestId';
import { isResponseLoading } from '../lib/model_util';
import { getHttpRequest } from '../lib/store';
import type { DropdownItem } from './core/Dropdown';
import { ContextMenu } from './core/Dropdown'; import { ContextMenu } from './core/Dropdown';
import { HttpMethodTag } from './core/HttpMethodTag'; import type { SidebarItemProps } from './SidebarItem';
import { Icon } from './core/Icon'; import { SidebarItems } from './SidebarItems';
import { VStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag';
import { useDialog } from './DialogContext';
import { DropMarker } from './DropMarker';
import { FolderSettingsDialog } from './FolderSettingsDialog';
interface Props { interface Props {
className?: string; className?: string;
} }
enum ItemTypes { export interface SidebarTreeNode {
REQUEST = 'request',
}
interface TreeNode {
item: Workspace | Folder | HttpRequest | GrpcRequest; item: Workspace | Folder | HttpRequest | GrpcRequest;
children: TreeNode[]; children: SidebarTreeNode[];
depth: number; depth: number;
} }
export function Sidebar({ className }: Props) { // This is an atom so we can use it in the child items to avoid re-rendering the entire list
export const sidebarSelectedIdAtom = atom<string | null>(null);
export const Sidebar = memo(function Sidebar({ className }: Props) {
const [hidden, setHidden] = useSidebarHidden(); const [hidden, setHidden] = useSidebarHidden();
const sidebarRef = useRef<HTMLLIElement>(null); const sidebarRef = useRef<HTMLLIElement>(null);
const activeRequest = useActiveRequest();
const [activeEnvironment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
const folders = useFolders(); const folders = useFolders();
const requests = useRequests(); const requests = useRequests();
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const httpRequestActions = useHttpRequestActions();
const httpResponses = useHttpResponses(); const httpResponses = useHttpResponses();
const grpcConnections = useGrpcConnections(); const grpcConnections = useGrpcConnections();
const duplicateHttpRequest = useDuplicateHttpRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const duplicateGrpcRequest = useDuplicateGrpcRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const routes = useAppRoutes();
const [hasFocus, setHasFocus] = useState<boolean>(false); const [hasFocus, setHasFocus] = useState<boolean>(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useAtom(sidebarSelectedIdAtom);
const [selectedTree, setSelectedTree] = useState<TreeNode | null>(null); const [selectedTree, setSelectedTree] = useState<SidebarTreeNode | null>(null);
const updateAnyHttpRequest = useUpdateAnyHttpRequest(); const { mutateAsync: updateAnyHttpRequest } = useUpdateAnyHttpRequest();
const updateAnyGrpcRequest = useUpdateAnyGrpcRequest(); const { mutateAsync: updateAnyGrpcRequest } = useUpdateAnyGrpcRequest();
const updateAnyFolder = useUpdateAnyFolder(); const { mutateAsync: updateAnyFolder } = useUpdateAnyFolder();
const [draggingId, setDraggingId] = useState<string | null>(null); const [draggingId, setDraggingId] = useState<string | null>(null);
const [hoveredTree, setHoveredTree] = useState<TreeNode | null>(null); const [hoveredTree, setHoveredTree] = useState<SidebarTreeNode | null>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null); const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const collapsed = useKeyValue<Record<string, boolean>>({ const { value: collapsed, set: setCollapsed } = useKeyValue<Record<string, boolean>>({
key: ['sidebar_collapsed', activeWorkspace?.id ?? 'n/a'], key: ['sidebar_collapsed', activeWorkspace?.id ?? 'n/a'],
fallback: {}, fallback: {},
namespace: 'no_sync', namespace: 'no_sync',
}); });
useHotKey('http_request.duplicate', async () => { const isCollapsed = useCallback((id: string) => collapsed?.[id] ?? false, [collapsed]);
if (activeRequest?.model === 'http_request') {
await duplicateHttpRequest.mutateAsync();
} else {
await duplicateGrpcRequest.mutateAsync();
}
});
const isCollapsed = useCallback(
(id: string) => collapsed.value?.[id] ?? false,
[collapsed.value],
);
const { tree, treeParentMap, selectableRequests } = useMemo<{ const { tree, treeParentMap, selectableRequests } = useMemo<{
tree: TreeNode | null; tree: SidebarTreeNode | null;
treeParentMap: Record<string, TreeNode>; treeParentMap: Record<string, SidebarTreeNode>;
selectedRequest: HttpRequest | GrpcRequest | null;
selectableRequests: { selectableRequests: {
id: string; id: string;
index: number; index: number;
tree: TreeNode; tree: SidebarTreeNode;
}[]; }[];
}>(() => { }>(() => {
const treeParentMap: Record<string, TreeNode> = {}; const childrenMap: Record<string, (HttpRequest | GrpcRequest | Folder)[]> = {};
for (const item of [...requests, ...folders]) {
if (item.folderId == null) {
childrenMap[item.workspaceId] = childrenMap[item.workspaceId] ?? [];
childrenMap[item.workspaceId]!.push(item);
} else {
childrenMap[item.folderId] = childrenMap[item.folderId] ?? [];
childrenMap[item.folderId]!.push(item);
}
}
const treeParentMap: Record<string, SidebarTreeNode> = {};
const selectableRequests: { const selectableRequests: {
id: string; id: string;
index: number; index: number;
tree: TreeNode; tree: SidebarTreeNode;
}[] = []; }[] = [];
if (activeWorkspace == null) { if (activeWorkspace == null) {
return { tree: null, treeParentMap, selectableRequests, selectedRequest: null }; return { tree: null, treeParentMap, selectableRequests };
} }
let selectedRequest: HttpRequest | GrpcRequest | null = null; const selectedRequest: HttpRequest | GrpcRequest | null = null;
let selectableRequestIndex = 0; let selectableRequestIndex = 0;
// Put requests and folders into a tree structure // Put requests and folders into a tree structure
const next = (node: TreeNode): TreeNode => { const next = (node: SidebarTreeNode): SidebarTreeNode => {
if ( const childItems = childrenMap[node.item.id] ?? [];
node.item.id === selectedId &&
(node.item.model === 'http_request' || node.item.model === 'grpc_request')
) {
selectedRequest = node.item;
}
const childItems = [...requests, ...folders].filter((f) =>
node.item.model === 'workspace' ? f.folderId == null : f.folderId === node.item.id,
);
// Recurse to children // Recurse to children
const isCollapsed = collapsed.value?.[node.item.id]; const isCollapsed = collapsed?.[node.item.id];
const depth = node.depth + 1; const depth = node.depth + 1;
childItems.sort((a, b) => a.sortPriority - b.sortPriority); childItems.sort((a, b) => a.sortPriority - b.sortPriority);
for (const item of childItems) { for (const item of childItems) {
@@ -175,18 +118,19 @@ export function Sidebar({ className }: Props) {
const tree = next({ item: activeWorkspace, children: [], depth: 0 }); const tree = next({ item: activeWorkspace, children: [], depth: 0 });
return { tree, treeParentMap, selectableRequests, selectedRequest }; return { tree, treeParentMap, selectableRequests, selectedRequest };
}, [activeWorkspace, selectedId, requests, folders, collapsed.value]); }, [activeWorkspace, requests, folders, collapsed]);
const focusActiveRequest = useCallback( const focusActiveRequest = useCallback(
( (
args: { args: {
forced?: { forced?: {
id: string; id: string;
tree: TreeNode; tree: SidebarTreeNode;
}; };
noFocusSidebar?: boolean; noFocusSidebar?: boolean;
} = {}, } = {},
) => { ) => {
const activeRequest = getActiveRequest();
const { forced, noFocusSidebar } = args; const { forced, noFocusSidebar } = args;
const tree = forced?.tree ?? treeParentMap[activeRequest?.id ?? 'n/a'] ?? null; const tree = forced?.tree ?? treeParentMap[activeRequest?.id ?? 'n/a'] ?? null;
const children = tree?.children ?? []; const children = tree?.children ?? [];
@@ -204,11 +148,11 @@ export function Sidebar({ className }: Props) {
sidebarRef.current?.focus(); sidebarRef.current?.focus();
} }
}, },
[activeRequest, treeParentMap], [setHasFocus, setSelectedId, treeParentMap],
); );
const handleSelect = useCallback( const handleSelect = useCallback(
async (id: string, opts: { noFocus?: boolean } = {}) => { async (id: string) => {
const tree = treeParentMap[id ?? 'n/a'] ?? null; const tree = treeParentMap[id ?? 'n/a'] ?? null;
const children = tree?.children ?? []; const children = tree?.children ?? [];
const node = children.find((m) => m.item.id === id) ?? null; const node = children.find((m) => m.item.id === id) ?? null;
@@ -219,40 +163,36 @@ export function Sidebar({ className }: Props) {
const { item } = node; const { item } = node;
if (item.model === 'folder') { if (item.model === 'folder') {
await collapsed.set((c) => ({ ...c, [item.id]: !c[item.id] })); await setCollapsed((c) => ({ ...c, [item.id]: !c[item.id] }));
} else { } else {
routes.navigate('request', { router.navigate({
requestId: id, to: Route.fullPath,
workspaceId: item.workspaceId, params: {
environmentId: activeEnvironment?.id ?? null, requestId: id,
cookieJarId: activeCookieJar?.id ?? null, workspaceId: item.workspaceId,
},
search: (prev) => ({ ...prev }),
}); });
setHasFocus(true);
setSelectedId(id); setSelectedId(id);
setSelectedTree(tree); setSelectedTree(tree);
if (!opts.noFocus) focusActiveRequest({ forced: { id, tree } });
} }
}, },
[ [treeParentMap, setCollapsed, setHasFocus, setSelectedId],
treeParentMap,
collapsed,
routes,
activeEnvironment?.id,
activeCookieJar?.id,
focusActiveRequest,
],
); );
const handleClearSelected = useCallback(() => { const handleClearSelected = useCallback(() => {
setSelectedId(null); setSelectedId(null);
setSelectedTree(null); setSelectedTree(null);
}, []); }, [setSelectedId]);
const handleFocus = useCallback(() => { const handleFocus = useCallback(() => {
if (hasFocus) return; if (hasFocus) return;
focusActiveRequest({ noFocusSidebar: true }); focusActiveRequest({ noFocusSidebar: true });
}, [focusActiveRequest, hasFocus]); }, [focusActiveRequest, hasFocus]);
const handleBlur = useCallback(() => setHasFocus(false), []); const handleBlur = useCallback(() => setHasFocus(false), [setHasFocus]);
useHotKey('sidebar.focus', async () => { useHotKey('sidebar.focus', async () => {
// Hide the sidebar if it's already focused // Hide the sidebar if it's already focused
@@ -277,16 +217,18 @@ export function Sidebar({ className }: Props) {
useKeyPressEvent('Enter', (e) => { useKeyPressEvent('Enter', (e) => {
if (!hasFocus) return; if (!hasFocus) return;
const selected = selectableRequests.find((r) => r.id === selectedId); const selected = selectableRequests.find((r) => r.id === selectedId);
if (!selected || selected.id === activeRequest?.id || activeWorkspace == null) { if (!selected || activeWorkspace == null) {
return; return;
} }
e.preventDefault(); e.preventDefault();
routes.navigate('request', { router.navigate({
requestId: selected.id, to: Route.fullPath,
workspaceId: activeWorkspace?.id ?? null, params: {
environmentId: activeEnvironment?.id ?? null, requestId: selected.id,
cookieJarId: activeCookieJar?.id ?? null, workspaceId: activeWorkspace?.id ?? null,
},
search: (prev) => ({ ...prev }),
}); });
}); });
@@ -395,13 +337,13 @@ export function Sidebar({ className }: Props) {
const sortPriority = i * 1000; const sortPriority = i * 1000;
if (child.item.model === 'folder') { if (child.item.model === 'folder') {
const updateFolder = (f: Folder) => ({ ...f, sortPriority, folderId }); const updateFolder = (f: Folder) => ({ ...f, sortPriority, folderId });
return updateAnyFolder.mutateAsync({ id: child.item.id, update: updateFolder }); return updateAnyFolder({ id: child.item.id, update: updateFolder });
} else if (child.item.model === 'grpc_request') { } else if (child.item.model === 'grpc_request') {
const updateRequest = (r: GrpcRequest) => ({ ...r, sortPriority, folderId }); const updateRequest = (r: GrpcRequest) => ({ ...r, sortPriority, folderId });
return updateAnyGrpcRequest.mutateAsync({ id: child.item.id, update: updateRequest }); return updateAnyGrpcRequest({ id: child.item.id, update: updateRequest });
} else if (child.item.model === 'http_request') { } else if (child.item.model === 'http_request') {
const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId }); const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId });
return updateAnyHttpRequest.mutateAsync({ id: child.item.id, update: updateRequest }); return updateAnyHttpRequest({ id: child.item.id, update: updateRequest });
} }
}), }),
); );
@@ -409,13 +351,13 @@ export function Sidebar({ className }: Props) {
const sortPriority = afterPriority - (afterPriority - beforePriority) / 2; const sortPriority = afterPriority - (afterPriority - beforePriority) / 2;
if (child.item.model === 'folder') { if (child.item.model === 'folder') {
const updateFolder = (f: Folder) => ({ ...f, sortPriority, folderId }); const updateFolder = (f: Folder) => ({ ...f, sortPriority, folderId });
await updateAnyFolder.mutateAsync({ id: child.item.id, update: updateFolder }); await updateAnyFolder({ id: child.item.id, update: updateFolder });
} else if (child.item.model === 'grpc_request') { } else if (child.item.model === 'grpc_request') {
const updateRequest = (r: GrpcRequest) => ({ ...r, sortPriority, folderId }); const updateRequest = (r: GrpcRequest) => ({ ...r, sortPriority, folderId });
await updateAnyGrpcRequest.mutateAsync({ id: child.item.id, update: updateRequest }); await updateAnyGrpcRequest({ id: child.item.id, update: updateRequest });
} else if (child.item.model === 'http_request') { } else if (child.item.model === 'http_request') {
const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId }); const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId });
await updateAnyHttpRequest.mutateAsync({ id: child.item.id, update: updateRequest }); await updateAnyHttpRequest({ id: child.item.id, update: updateRequest });
} }
} }
setDraggingId(null); setDraggingId(null);
@@ -445,7 +387,7 @@ export function Sidebar({ className }: Props) {
const mainContextMenuItems = useCreateDropdownItems(); const mainContextMenuItems = useCreateDropdownItems();
// Not ready to render yet // Not ready to render yet
if (tree == null || collapsed.value == null) { if (tree == null || collapsed == null) {
return null; return null;
} }
@@ -457,7 +399,14 @@ export function Sidebar({ className }: Props) {
onBlur={handleBlur} onBlur={handleBlur}
tabIndex={hidden ? -1 : 0} tabIndex={hidden ? -1 : 0}
onContextMenu={handleMainContextMenu} onContextMenu={handleMainContextMenu}
className={classNames(className, 'h-full grid grid-rows-[minmax(0,1fr)_auto]')} data-focused={hasFocus}
className={classNames(
className,
// Style item selection color here, because it's very hard to do in an efficient
// way in the item itself (selection ID makes it hard)
hasFocus && '[&_[data-selected=true]]:bg-surface-active',
'h-full grid grid-rows-[minmax(0,1fr)_auto]',
)}
> >
<div className="pb-3 overflow-x-visible overflow-y-scroll pt-2"> <div className="pb-3 overflow-x-visible overflow-y-scroll pt-2">
<ContextMenu <ContextMenu
@@ -467,15 +416,11 @@ export function Sidebar({ className }: Props) {
/> />
<SidebarItems <SidebarItems
treeParentMap={treeParentMap} treeParentMap={treeParentMap}
activeId={activeRequest?.id ?? null}
selectedId={selectedId}
selectedTree={selectedTree} selectedTree={selectedTree}
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
httpRequestActions={httpRequestActions}
httpResponses={httpResponses} httpResponses={httpResponses}
grpcConnections={grpcConnections} grpcConnections={grpcConnections}
tree={tree} tree={tree}
focused={hasFocus}
draggingId={draggingId} draggingId={draggingId}
onSelect={handleSelect} onSelect={handleSelect}
hoveredIndex={hoveredIndex} hoveredIndex={hoveredIndex}
@@ -487,485 +432,4 @@ export function Sidebar({ className }: Props) {
</div> </div>
</aside> </aside>
); );
} });
interface SidebarItemsProps {
tree: TreeNode;
focused: boolean;
draggingId: string | null;
activeId: string | null;
selectedId: string | null;
selectedTree: TreeNode | null;
treeParentMap: Record<string, TreeNode>;
hoveredTree: TreeNode | null;
hoveredIndex: number | null;
handleMove: (id: string, side: 'above' | 'below') => void;
handleEnd: (id: string) => void;
handleDragStart: (id: string) => void;
onSelect: (requestId: string) => void;
isCollapsed: (id: string) => boolean;
httpRequestActions: CallableHttpRequestAction[];
httpResponses: HttpResponse[];
grpcConnections: GrpcConnection[];
}
function SidebarItems({
tree,
focused,
activeId,
selectedId,
selectedTree,
draggingId,
onSelect,
treeParentMap,
isCollapsed,
hoveredTree,
hoveredIndex,
handleEnd,
handleMove,
handleDragStart,
httpRequestActions,
httpResponses,
grpcConnections,
}: SidebarItemsProps) {
return (
<VStack
as="ul"
role="menu"
aria-orientation="vertical"
dir="ltr"
className={classNames(
tree.depth > 0 && 'border-l border-border-subtle',
tree.depth === 0 && 'ml-0',
tree.depth >= 1 && 'ml-[1.2rem]',
)}
>
{tree.children.map((child, i) => {
const selected = selectedId === child.item.id;
const active = activeId === child.item.id;
return (
<Fragment key={child.item.id}>
{hoveredIndex === i && hoveredTree?.item.id === tree.item.id && <DropMarker />}
<SidebarItem
selected={selected}
itemId={child.item.id}
itemName={child.item.name}
itemFallbackName={
child.item.model === 'http_request' || child.item.model === 'grpc_request'
? fallbackRequestName(child.item)
: 'New Folder'
}
itemModel={child.item.model}
itemPrefix={
(child.item.model === 'http_request' || child.item.model === 'grpc_request') && (
<HttpMethodTag
request={child.item}
className={classNames(!(active || selected) && 'text-text-subtlest')}
/>
)
}
httpRequestActions={httpRequestActions}
latestHttpResponse={httpResponses.find((r) => r.requestId === child.item.id) ?? null}
latestGrpcConnection={
grpcConnections.find((c) => c.requestId === child.item.id) ?? null
}
onMove={handleMove}
onEnd={handleEnd}
onSelect={onSelect}
onDragStart={handleDragStart}
useProminentStyles={focused}
isCollapsed={isCollapsed}
child={child}
>
{child.item.model === 'folder' &&
!isCollapsed(child.item.id) &&
draggingId !== child.item.id && (
<SidebarItems
activeId={activeId}
draggingId={draggingId}
focused={focused}
handleDragStart={handleDragStart}
handleEnd={handleEnd}
handleMove={handleMove}
hoveredIndex={hoveredIndex}
hoveredTree={hoveredTree}
httpRequestActions={httpRequestActions}
httpResponses={httpResponses}
grpcConnections={grpcConnections}
isCollapsed={isCollapsed}
onSelect={onSelect}
selectedId={selectedId}
selectedTree={selectedTree}
tree={child}
treeParentMap={treeParentMap}
/>
)}
</SidebarItem>
</Fragment>
);
})}
{hoveredIndex === tree.children.length && hoveredTree?.item.id === tree.item.id && (
<DropMarker />
)}
</VStack>
);
}
type SidebarItemProps = {
className?: string;
itemId: string;
itemName: string;
itemFallbackName: string;
itemModel: AnyModel['model'];
itemPrefix: ReactNode;
useProminentStyles?: boolean;
selected: boolean;
onMove: (id: string, side: 'above' | 'below') => void;
onEnd: (id: string) => void;
onDragStart: (id: string) => void;
children?: ReactNode;
child: TreeNode;
latestHttpResponse: HttpResponse | null;
latestGrpcConnection: GrpcConnection | null;
} & Pick<SidebarItemsProps, 'isCollapsed' | 'onSelect' | 'httpRequestActions'>;
type DragItem = {
id: string;
itemName: string;
};
function SidebarItem({
itemName,
itemId,
itemModel,
child,
onMove,
onEnd,
onDragStart,
onSelect,
isCollapsed,
itemPrefix,
className,
selected,
itemFallbackName,
useProminentStyles,
latestHttpResponse,
latestGrpcConnection,
httpRequestActions,
children,
}: SidebarItemProps) {
const ref = useRef<HTMLLIElement>(null);
const [, connectDrop] = useDrop<DragItem, void>(
{
accept: ItemTypes.REQUEST,
hover: (_, monitor) => {
if (!ref.current) return;
const hoverBoundingRect = ref.current?.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
onMove(itemId, hoverClientY < hoverMiddleY ? 'above' : 'below');
},
},
[onMove],
);
const [, connectDrag] = useDrag<
DragItem,
unknown,
{
isDragging: boolean;
}
>(
() => ({
type: ItemTypes.REQUEST,
item: () => {
// Cancel drag when editing
if (editing) return null;
onDragStart(itemId);
return { id: itemId, itemName };
},
collect: (m) => ({ isDragging: m.isDragging() }),
options: { dropEffect: 'move' },
end: () => onEnd(itemId),
}),
[onEnd],
);
connectDrag(connectDrop(ref));
const dialog = useDialog();
const activeRequest = useActiveRequest();
const deleteFolder = useDeleteFolder(itemId);
const deleteRequest = useDeleteRequest(itemId);
const renameRequest = useRenameRequest(itemId);
const duplicateFolder = useDuplicateFolder(itemId);
const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
const sendRequest = useSendAnyHttpRequest();
const moveToWorkspace = useMoveToWorkspace(itemId);
const sendManyRequests = useSendManyRequests();
const updateHttpRequest = useUpdateAnyHttpRequest();
const workspaces = useWorkspaces();
const updateGrpcRequest = useUpdateAnyGrpcRequest();
const [editing, setEditing] = useState<boolean>(false);
const isActive = activeRequest?.id === itemId;
const createDropdownItems = useCreateDropdownItems({ folderId: itemId });
useScrollIntoView(ref.current, isActive);
const handleSubmitNameEdit = useCallback(
async (el: HTMLInputElement) => {
if (itemModel === 'http_request') {
await updateHttpRequest.mutateAsync({
id: itemId,
update: (r) => ({ ...r, name: el.value }),
});
} else if (itemModel === 'grpc_request') {
await updateGrpcRequest.mutateAsync({
id: itemId,
update: (r) => ({ ...r, name: el.value }),
});
}
setEditing(false);
},
[itemId, itemModel, updateGrpcRequest, updateHttpRequest],
);
const handleFocus = useCallback((el: HTMLInputElement | null) => {
el?.focus();
el?.select();
}, []);
const handleInputKeyDown = useCallback(
async (e: React.KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation();
switch (e.key) {
case 'Enter':
e.preventDefault();
await handleSubmitNameEdit(e.currentTarget);
break;
case 'Escape':
e.preventDefault();
setEditing(false);
break;
}
},
[handleSubmitNameEdit],
);
const handleStartEditing = useCallback(() => {
if (itemModel !== 'http_request' && itemModel !== 'grpc_request') return;
setEditing(true);
}, [setEditing, itemModel]);
const handleBlur = useCallback(
async (e: React.FocusEvent<HTMLInputElement>) => {
await handleSubmitNameEdit(e.currentTarget);
},
[handleSubmitNameEdit],
);
const handleSelect = useCallback(() => onSelect(itemId), [onSelect, itemId]);
const [showContextMenu, setShowContextMenu] = useState<{
x: number;
y: number;
} | null>(null);
const handleCloseContextMenu = useCallback(() => {
setShowContextMenu(null);
}, []);
const handleContextMenu = useCallback((e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
setShowContextMenu({ x: e.clientX, y: e.clientY });
}, []);
const items = useMemo<DropdownItem[]>(() => {
if (itemModel === 'folder') {
return [
{
key: 'send-all',
label: 'Send All',
leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
},
{
key: 'folder-settings',
label: 'Settings',
leftSlot: <Icon icon="settings" />,
onSelect: () =>
dialog.show({
id: 'folder-settings',
title: 'Folder Settings',
size: 'md',
render: () => <FolderSettingsDialog folderId={itemId} />,
}),
},
{
key: 'duplicateFolder',
label: 'Duplicate',
leftSlot: <Icon icon="copy" />,
onSelect: () => duplicateFolder.mutate(),
},
{
key: 'delete-folder',
label: 'Delete',
variant: 'danger',
leftSlot: <Icon icon="trash" />,
onSelect: () => deleteFolder.mutate(),
},
{ type: 'separator' },
...createDropdownItems,
];
} else {
const requestItems: DropdownItem[] =
itemModel === 'http_request'
? [
{
key: 'send-request',
label: 'Send',
hotKeyAction: 'http_request.send',
hotKeyLabelOnly: true, // Already bound in URL bar
leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => sendRequest.mutate(itemId),
},
...httpRequestActions.map((a) => ({
key: a.key,
label: a.label,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
onSelect: async () => {
const request = await getHttpRequest(itemId);
if (request != null) await a.call(request);
},
})),
{ type: 'separator' },
]
: [];
return [
...requestItems,
{
key: 'rename-request',
label: 'Rename',
leftSlot: <Icon icon="pencil" />,
onSelect: renameRequest.mutate,
},
{
key: 'duplicate-request',
label: 'Duplicate',
hotKeyAction: 'http_request.duplicate',
hotKeyLabelOnly: true, // Would trigger for every request (bad)
leftSlot: <Icon icon="copy" />,
onSelect: () =>
itemModel === 'http_request'
? duplicateHttpRequest.mutate()
: duplicateGrpcRequest.mutate(),
},
{
key: 'move-workspace',
label: 'Move',
leftSlot: <Icon icon="arrow_right_circle" />,
hidden: workspaces.length <= 1,
onSelect: moveToWorkspace.mutate,
},
{
key: 'delete-request',
variant: 'danger',
label: 'Delete',
leftSlot: <Icon icon="trash" />,
onSelect: () => deleteRequest.mutate(),
},
];
}
}, [
child.children,
createDropdownItems,
deleteFolder,
deleteRequest,
duplicateFolder,
duplicateGrpcRequest,
duplicateHttpRequest,
httpRequestActions,
itemId,
itemModel,
moveToWorkspace.mutate,
renameRequest.mutate,
sendManyRequests,
sendRequest,
workspaces.length,
]);
return (
<li ref={ref} draggable>
<div className={classNames(className, 'block relative group/item px-1.5 pb-0.5')}>
<ContextMenu
triggerPosition={showContextMenu}
items={items}
onClose={handleCloseContextMenu}
/>
<button
// tabIndex={-1} // Will prevent drag-n-drop
disabled={editing}
onClick={handleSelect}
onDoubleClick={handleStartEditing}
onContextMenu={handleContextMenu}
data-active={isActive}
data-selected={selected}
className={classNames(
'w-full flex gap-1.5 items-center h-xs px-1.5 rounded-md focus-visible:ring focus-visible:ring-border-focus outline-0',
editing && 'ring-1 focus-within:ring-focus',
isActive && 'bg-surface-highlight text-text',
!isActive && 'text-text-subtle group-hover/item:text-text',
showContextMenu && '!text-text', // Show as "active" when context menu is open
selected && useProminentStyles && '!bg-surface-active',
)}
>
{itemModel === 'folder' && (
<Icon
size="sm"
icon="chevron_right"
className={classNames(
'text-text-subtlest',
'transition-transform',
!isCollapsed(itemId) && 'transform rotate-90',
)}
/>
)}
<div className="flex items-center gap-2 min-w-0">
{itemPrefix}
{editing ? (
<input
ref={handleFocus}
defaultValue={itemName}
className="bg-transparent outline-none w-full cursor-text"
onBlur={handleBlur}
onKeyDown={handleInputKeyDown}
/>
) : (
<span className="truncate">{itemName || itemFallbackName}</span>
)}
</div>
{latestGrpcConnection ? (
<div className="ml-auto">
{isResponseLoading(latestGrpcConnection) && (
<Icon spin size="sm" icon="update" className="text-text-subtlest" />
)}
</div>
) : latestHttpResponse ? (
<div className="ml-auto">
{isResponseLoading(latestHttpResponse) ? (
<Icon spin size="sm" icon="refresh" className="text-text-subtlest" />
) : (
<StatusTag className="text-xs" response={latestHttpResponse} />
)}
</div>
) : null}
</button>
</div>
{children}
</li>
);
}

View File

@@ -0,0 +1,278 @@
import type { AnyModel, GrpcConnection, HttpResponse } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { activeRequestAtom } from '../hooks/useActiveRequest';
import { useScrollIntoView } from '../hooks/useScrollIntoView';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { isResponseLoading } from '../lib/model_util';
import { jotaiStore } from '../routes/__root';
import { HttpMethodTag } from './core/HttpMethodTag';
import { Icon } from './core/Icon';
import { StatusTag } from './core/StatusTag';
import { RequestContextMenu } from './RequestContextMenu';
import type { SidebarTreeNode } from './Sidebar';
import { sidebarSelectedIdAtom } from './Sidebar';
import type { SidebarItemsProps } from './SidebarItems';
enum ItemTypes {
REQUEST = 'request',
}
export type SidebarItemProps = {
className?: string;
itemId: string;
itemName: string;
itemFallbackName: string;
itemModel: AnyModel['model'];
onMove: (id: string, side: 'above' | 'below') => void;
onEnd: (id: string) => void;
onDragStart: (id: string) => void;
children?: ReactNode;
child: SidebarTreeNode;
latestHttpResponse: HttpResponse | null;
latestGrpcConnection: GrpcConnection | null;
} & Pick<SidebarItemsProps, 'isCollapsed' | 'onSelect'>;
type DragItem = {
id: string;
itemName: string;
};
function SidebarItem_({
itemName,
itemId,
itemModel,
child,
onMove,
onEnd,
onDragStart,
onSelect,
isCollapsed,
className,
itemFallbackName,
latestHttpResponse,
latestGrpcConnection,
children,
}: SidebarItemProps) {
const ref = useRef<HTMLLIElement>(null);
const [, connectDrop] = useDrop<DragItem, void>(
{
accept: ItemTypes.REQUEST,
hover: (_, monitor) => {
if (!ref.current) return;
const hoverBoundingRect = ref.current?.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
onMove(itemId, hoverClientY < hoverMiddleY ? 'above' : 'below');
},
},
[onMove],
);
const [, connectDrag] = useDrag<
DragItem,
unknown,
{
isDragging: boolean;
}
>(
() => ({
type: ItemTypes.REQUEST,
item: () => {
// Cancel drag when editing
if (editing) return null;
onDragStart(itemId);
return { id: itemId, itemName };
},
collect: (m) => ({ isDragging: m.isDragging() }),
options: { dropEffect: 'move' },
end: () => onEnd(itemId),
}),
[onEnd],
);
connectDrag(connectDrop(ref));
const updateHttpRequest = useUpdateAnyHttpRequest();
const updateGrpcRequest = useUpdateAnyGrpcRequest();
const [editing, setEditing] = useState<boolean>(false);
const [selected, setSelected] = useState<boolean>(
jotaiStore.get(sidebarSelectedIdAtom) == itemId,
);
useEffect(() => {
jotaiStore.sub(sidebarSelectedIdAtom, () => {
const value = jotaiStore.get(sidebarSelectedIdAtom);
setSelected(value === itemId);
});
}, [itemId]);
const [active, setActive] = useState<boolean>(jotaiStore.get(activeRequestAtom)?.id === itemId);
useEffect(() => {
jotaiStore.sub(activeRequestAtom, () => {
const value = jotaiStore.get(activeRequestAtom);
setActive(value?.id === itemId);
});
}, [itemId]);
useScrollIntoView(ref.current, active);
const handleSubmitNameEdit = useCallback(
async (el: HTMLInputElement) => {
if (itemModel === 'http_request') {
await updateHttpRequest.mutateAsync({
id: itemId,
update: (r) => ({ ...r, name: el.value }),
});
} else if (itemModel === 'grpc_request') {
await updateGrpcRequest.mutateAsync({
id: itemId,
update: (r) => ({ ...r, name: el.value }),
});
}
setEditing(false);
},
[itemId, itemModel, updateGrpcRequest, updateHttpRequest],
);
const handleFocus = useCallback((el: HTMLInputElement | null) => {
el?.focus();
el?.select();
}, []);
const handleInputKeyDown = useCallback(
async (e: React.KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation();
switch (e.key) {
case 'Enter':
e.preventDefault();
await handleSubmitNameEdit(e.currentTarget);
break;
case 'Escape':
e.preventDefault();
setEditing(false);
break;
}
},
[handleSubmitNameEdit],
);
const handleStartEditing = useCallback(() => {
if (itemModel !== 'http_request' && itemModel !== 'grpc_request') return;
setEditing(true);
}, [setEditing, itemModel]);
const handleBlur = useCallback(
async (e: React.FocusEvent<HTMLInputElement>) => {
await handleSubmitNameEdit(e.currentTarget);
},
[handleSubmitNameEdit],
);
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();
setShowContextMenu({ x: e.clientX, y: e.clientY });
}, []);
const handleCloseContextMenu = useCallback(() => setShowContextMenu(null), []);
const itemPrefix = (child.item.model === 'http_request' ||
child.item.model === 'grpc_request') && (
<HttpMethodTag
request={child.item}
className={classNames(!(active || selected) && 'text-text-subtlest')}
/>
);
return (
<li ref={ref} draggable>
<div className={classNames(className, 'block relative group/item px-1.5 pb-0.5')}>
<RequestContextMenu child={child} show={showContextMenu} close={handleCloseContextMenu} />
<button
// tabIndex={-1} // Will prevent drag-n-drop
disabled={editing}
onClick={handleSelect}
onDoubleClick={handleStartEditing}
onContextMenu={handleContextMenu}
data-active={active}
data-selected={selected}
className={classNames(
'w-full flex gap-1.5 items-center h-xs px-1.5 rounded-md focus-visible:ring focus-visible:ring-border-focus outline-0',
editing && 'ring-1 focus-within:ring-focus',
active && 'bg-surface-highlight text-text',
!active && 'text-text-subtle group-hover/item:text-text',
showContextMenu && '!text-text', // Show as "active" when context menu is open
)}
>
{itemModel === 'folder' && (
<Icon
size="sm"
icon="chevron_right"
className={classNames(
'text-text-subtlest',
'transition-transform',
!isCollapsed(itemId) && 'transform rotate-90',
)}
/>
)}
<div className="flex items-center gap-2 min-w-0">
{itemPrefix}
{editing ? (
<input
ref={handleFocus}
defaultValue={itemName}
className="bg-transparent outline-none w-full cursor-text"
onBlur={handleBlur}
onKeyDown={handleInputKeyDown}
/>
) : (
<span className="truncate">{itemName || itemFallbackName}</span>
)}
</div>
{latestGrpcConnection ? (
<div className="ml-auto">
{isResponseLoading(latestGrpcConnection) && (
<Icon spin size="sm" icon="update" className="text-text-subtlest" />
)}
</div>
) : latestHttpResponse ? (
<div className="ml-auto">
{isResponseLoading(latestHttpResponse) ? (
<Icon spin size="sm" icon="refresh" className="text-text-subtlest" />
) : (
<StatusTag className="text-xs" response={latestHttpResponse} />
)}
</div>
) : null}
</button>
</div>
{children}
</li>
);
}
export const SidebarItem = memo<SidebarItemProps>(SidebarItem_, (a, b) => {
const different = [];
for (const key of Object.keys(a) as (keyof SidebarItemProps)[]) {
if (a[key] !== b[key]) {
different.push(key);
}
}
if (different.length > 0) {
console.log('ITEM DIFFERENT -------------------', different.join(', '));
}
return different.length === 0;
});

View File

@@ -0,0 +1,118 @@
import type { GrpcConnection, HttpResponse } from '@yaakapp-internal/models';
import classNames from 'classnames';
import React, { Fragment, memo } from 'react';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { VStack } from './core/Stacks';
import { DropMarker } from './DropMarker';
import type { SidebarTreeNode } from './Sidebar';
import { SidebarItem } from './SidebarItem';
export interface SidebarItemsProps {
tree: SidebarTreeNode;
draggingId: string | null;
selectedTree: SidebarTreeNode | null;
treeParentMap: Record<string, SidebarTreeNode>;
hoveredTree: SidebarTreeNode | null;
hoveredIndex: number | null;
handleMove: (id: string, side: 'above' | 'below') => void;
handleEnd: (id: string) => void;
handleDragStart: (id: string) => void;
onSelect: (requestId: string) => void;
isCollapsed: (id: string) => boolean;
httpResponses: HttpResponse[];
grpcConnections: GrpcConnection[];
}
function SidebarItems_({
tree,
selectedTree,
draggingId,
onSelect,
treeParentMap,
isCollapsed,
hoveredTree,
hoveredIndex,
handleEnd,
handleMove,
handleDragStart,
httpResponses,
grpcConnections,
}: SidebarItemsProps) {
return (
<VStack
as="ul"
role="menu"
aria-orientation="vertical"
dir="ltr"
className={classNames(
tree.depth > 0 && 'border-l border-border-subtle',
tree.depth === 0 && 'ml-0',
tree.depth >= 1 && 'ml-[1.2rem]',
)}
>
{tree.children.map((child, i) => {
return (
<Fragment key={child.item.id}>
{hoveredIndex === i && hoveredTree?.item.id === tree.item.id && <DropMarker />}
<SidebarItem
itemId={child.item.id}
itemName={child.item.name}
itemFallbackName={
child.item.model === 'http_request' || child.item.model === 'grpc_request'
? fallbackRequestName(child.item)
: 'New Folder'
}
itemModel={child.item.model}
latestHttpResponse={httpResponses.find((r) => r.requestId === child.item.id) ?? null}
latestGrpcConnection={
grpcConnections.find((c) => c.requestId === child.item.id) ?? null
}
onMove={handleMove}
onEnd={handleEnd}
onSelect={onSelect}
onDragStart={handleDragStart}
isCollapsed={isCollapsed}
child={child}
>
{child.item.model === 'folder' &&
!isCollapsed(child.item.id) &&
draggingId !== child.item.id && (
<SidebarItems
draggingId={draggingId}
handleDragStart={handleDragStart}
handleEnd={handleEnd}
handleMove={handleMove}
hoveredIndex={hoveredIndex}
hoveredTree={hoveredTree}
httpResponses={httpResponses}
grpcConnections={grpcConnections}
isCollapsed={isCollapsed}
onSelect={onSelect}
selectedTree={selectedTree}
tree={child}
treeParentMap={treeParentMap}
/>
)}
</SidebarItem>
</Fragment>
);
})}
{hoveredIndex === tree.children.length && hoveredTree?.item.id === tree.item.id && (
<DropMarker />
)}
</VStack>
);
}
export const SidebarItems = memo<SidebarItemsProps>(SidebarItems_, (a, b) => {
const different = [];
for (const key of Object.keys(a) as (keyof SidebarItemsProps)[]) {
if (a[key] !== b[key]) {
different.push(key);
}
}
if (different.length > 0) {
console.log('ITEMS DIFFERENT -------------------', different.join(', '));
}
return different.length === 0;
});

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden'; import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
@@ -9,7 +9,6 @@ import { useImportData } from '../hooks/useImportData';
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar'; import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useSidebarWidth } from '../hooks/useSidebarWidth'; import { useSidebarWidth } from '../hooks/useSidebarWidth';
import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTitle';
import { useWorkspaces } from '../hooks/useWorkspaces'; import { useWorkspaces } from '../hooks/useWorkspaces';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
import { Button } from './core/Button'; import { Button } from './core/Button';
@@ -31,22 +30,21 @@ const head = { gridArea: 'head' };
const body = { gridArea: 'body' }; const body = { gridArea: 'body' };
const drag = { gridArea: 'drag' }; const drag = { gridArea: 'drag' };
export default function Workspace() { export function Workspace() {
useSyncWorkspaceRequestTitle();
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const activeWorkspace = useActiveWorkspace();
const { setWidth, width, resetWidth } = useSidebarWidth(); const { setWidth, width, resetWidth } = useSidebarWidth();
const [sidebarHidden, setSidebarHidden] = useSidebarHidden(); const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden(); const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
const activeRequest = useActiveRequest();
const importData = useImportData();
const floating = useShouldFloatSidebar(); const floating = useShouldFloatSidebar();
const [isResizing, setIsResizing] = useState<boolean>(false); const [isResizing, setIsResizing] = useState<boolean>(false);
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
null, null,
); );
useEffect(() => {
console.log('RENDER WORKSPACE');
}, []);
const unsub = () => { const unsub = () => {
if (moveState.current !== null) { if (moveState.current !== null) {
document.documentElement.removeEventListener('mousemove', moveState.current.move); document.documentElement.removeEventListener('mousemove', moveState.current.move);
@@ -161,34 +159,50 @@ export default function Workspace() {
> >
<WorkspaceHeader className="pointer-events-none" /> <WorkspaceHeader className="pointer-events-none" />
</HeaderSize> </HeaderSize>
{activeWorkspace == null ? ( <WorkspaceBody />
<div className="m-auto">
<Banner color="warning" className="max-w-[30rem]">
The active workspace was not found. Select a workspace from the header menu or report
this bug to <FeedbackLink />
</Banner>
</div>
) : activeRequest == null ? (
<HotKeyList
hotkeys={['http_request.create', 'sidebar.focus', 'settings.show']}
bottomSlot={
<HStack space={1} justifyContent="center" className="mt-3">
<Button variant="border" size="sm" onClick={() => importData.mutate()}>
Import
</Button>
<CreateDropdown hideFolder>
<Button variant="border" forDropdown size="sm">
New Request
</Button>
</CreateDropdown>
</HStack>
}
/>
) : activeRequest.model === 'grpc_request' ? (
<GrpcConnectionLayout style={body} />
) : (
<HttpRequestLayout activeRequest={activeRequest} style={body} />
)}
</div> </div>
); );
} }
function WorkspaceBody() {
const activeRequest = useActiveRequest();
const activeWorkspace = useActiveWorkspace();
const importData = useImportData();
if (activeWorkspace == null) {
return (
<div className="m-auto">
<Banner color="warning" className="max-w-[30rem]">
The active workspace was not found. Select a workspace from the header menu or report this
bug to <FeedbackLink />
</Banner>
</div>
);
}
if (activeRequest == null) {
return (
<HotKeyList
hotkeys={['http_request.create', 'sidebar.focus', 'settings.show']}
bottomSlot={
<HStack space={1} justifyContent="center" className="mt-3">
<Button variant="border" size="sm" onClick={() => importData.mutate()}>
Import
</Button>
<CreateDropdown hideFolder>
<Button variant="border" forDropdown size="sm">
New Request
</Button>
</CreateDropdown>
</HStack>
}
/>
);
}
if (activeRequest.model === 'grpc_request') {
return <GrpcConnectionLayout style={body} />;
}
return <HttpRequestLayout activeRequest={activeRequest} style={body} />;
}

View File

@@ -86,7 +86,7 @@
&.cm-singleline { &.cm-singleline {
.cm-editor { .cm-editor {
@apply w-full h-auto; @apply w-full h-full;
} }
.cm-scroller { .cm-scroller {

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import { Link as RouterLink } from 'react-router-dom'; import { Link as RouterLink } from '@tanstack/react-router';
import { trackEvent } from '../../lib/analytics'; import { trackEvent } from '../../lib/analytics';
import { Icon } from './Icon'; import { Icon } from './Icon';

View File

@@ -1,5 +1,6 @@
import { useSearch } from '@tanstack/react-router';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom'; import { Route } from '../routes/workspaces/$workspaceId';
import { useCookieJars } from './useCookieJars'; import { useCookieJars } from './useCookieJars';
export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id'; export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id';
@@ -34,23 +35,18 @@ export function useEnsureActiveCookieJar() {
// There's no active jar, so set it to the first one // There's no active jar, so set it to the first one
console.log('Setting active cookie jar to', firstJar.id); console.log('Setting active cookie jar to', firstJar.id);
setActiveCookieJarId(firstJar.id); setActiveCookieJarId(firstJar.id).catch(console.error);
}, [activeCookieJarId, cookieJars, setActiveCookieJarId]); }, [activeCookieJarId, cookieJars, setActiveCookieJarId]);
} }
function useActiveCookieJarId() { function useActiveCookieJarId() {
// NOTE: This query param is accessed from Rust side, so do not change // NOTE: This query param is accessed from Rust side, so do not change
const [params, setParams] = useSearchParams(); const navigate = Route.useNavigate();
const id = params.get(QUERY_COOKIE_JAR_ID); const { cookieJarId: id } = useSearch({ strict: false });
const setId = useCallback( const setId = useCallback(
(id: string) => { (id: string) => navigate({ search: (prev) => ({ ...prev, cookieJarId: id }) }),
setParams((p) => { [navigate],
const existing = Object.fromEntries(p);
return { ...existing, [QUERY_COOKIE_JAR_ID]: id };
});
},
[setParams],
); );
return [id, setId] as const; return [id, setId] as const;

View File

@@ -1,5 +1,6 @@
import { useSearch } from '@tanstack/react-router';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom'; import { Route } from '../routes/workspaces/$workspaceId';
import { useEnvironments } from './useEnvironments'; import { useEnvironments } from './useEnvironments';
export function useActiveEnvironment() { export function useActiveEnvironment() {
@@ -16,22 +17,13 @@ export const QUERY_ENVIRONMENT_ID = 'environment_id';
function useActiveEnvironmentId() { function useActiveEnvironmentId() {
// NOTE: This query param is accessed from Rust side, so do not change // NOTE: This query param is accessed from Rust side, so do not change
const [params, setParams] = useSearchParams(); const navigate = Route.useNavigate();
const id = params.get(QUERY_ENVIRONMENT_ID); const { environmentId: id } = useSearch({ strict: false });
const setId = useCallback( const setId = useCallback(
(id: string | null) => { (environment_id: string | null) =>
setParams((p) => { navigate({ search: (prev) => ({ ...prev, environment_id: environment_id ?? undefined }) }),
const existing = Object.fromEntries(p); [navigate],
if (id == null) {
delete existing[QUERY_ENVIRONMENT_ID];
} else {
existing[QUERY_ENVIRONMENT_ID] = id;
}
return existing;
});
},
[setParams],
); );
return [id, setId] as const; return [id, setId] as const;

View File

@@ -1,24 +1,30 @@
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models'; import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import { useActiveRequestId } from './useActiveRequestId'; import { atom, useAtomValue } from 'jotai';
import { useRequests } from './useRequests'; import { jotaiStore } from '../routes/__root';
import { activeRequestIdAtom } from './useActiveRequestId';
import { grpcRequestsAtom } from './useGrpcRequests';
import { httpRequestsAtom } from './useHttpRequests';
interface TypeMap { interface TypeMap {
http_request: HttpRequest; http_request: HttpRequest;
grpc_request: GrpcRequest; grpc_request: GrpcRequest;
} }
export const activeRequestAtom = atom<HttpRequest | GrpcRequest | null>((get) => {
const activeRequestId = get(activeRequestIdAtom);
const requests = [...get(httpRequestsAtom), ...get(grpcRequestsAtom)];
return requests.find((r) => r.id === activeRequestId) ?? null;
});
export function getActiveRequest() {
return jotaiStore.get(activeRequestAtom);
}
export function useActiveRequest<T extends keyof TypeMap>( export function useActiveRequest<T extends keyof TypeMap>(
model?: T | undefined, model?: T | undefined,
): TypeMap[T] | null { ): TypeMap[T] | null {
const requestId = useActiveRequestId(); const activeRequest = useAtomValue(activeRequestAtom);
const requests = useRequests(); if (model == null) return activeRequest as TypeMap[T];
if (activeRequest?.model === model) return activeRequest as TypeMap[T];
for (const request of requests) {
const modelMatch = model == null ? true : request.model === model;
if (modelMatch && request.id === requestId) {
return request as TypeMap[T];
}
}
return null; return null;
} }

View File

@@ -1,6 +1,17 @@
import { useParams } from 'react-router-dom'; import { useParams } from '@tanstack/react-router';
import { atom, useAtomValue } from 'jotai';
import { useEffect } from 'react';
import { jotaiStore } from '../routes/__root';
export const activeRequestIdAtom = atom<string>();
export function useActiveRequestId(): string | null { export function useActiveRequestId(): string | null {
const { requestId } = useParams(); return useAtomValue(activeRequestIdAtom) ?? null;
return requestId ?? null; }
export function useSubscribeActiveRequestId() {
const { requestId } = useParams({ strict: false });
useEffect(() => {
jotaiStore.set(activeRequestIdAtom, requestId);
}, [requestId]);
} }

View File

@@ -1,19 +1,25 @@
import { useParams } from '@tanstack/react-router';
import type { Workspace } from '@yaakapp-internal/models'; import type { Workspace } from '@yaakapp-internal/models';
import { useMemo } from 'react'; import { atom, useAtomValue } from 'jotai/index';
import { useParams } from 'react-router-dom'; import { useEffect } from 'react';
import { jotaiStore } from '../routes/__root';
import { useWorkspaces } from './useWorkspaces'; import { useWorkspaces } from './useWorkspaces';
export const activeWorkspaceIdAtom = atom<string>();
export function useActiveWorkspace(): Workspace | null { export function useActiveWorkspace(): Workspace | null {
const workspaceId = useActiveWorkspaceId(); const workspaceId = useActiveWorkspaceId();
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
return workspaces.find((w) => w.id === workspaceId) ?? null;
return useMemo(
() => workspaces.find((w) => w.id === workspaceId) ?? null,
[workspaces, workspaceId],
);
} }
function useActiveWorkspaceId(): string | null { function useActiveWorkspaceId(): string | null {
const { workspaceId } = useParams(); return useAtomValue(activeWorkspaceIdAtom) ?? null;
return workspaceId ?? null; }
export function useSubscribeActiveWorkspaceId() {
const { workspaceId } = useParams({ strict: false });
useEffect(() => {
jotaiStore.set(activeWorkspaceIdAtom, workspaceId);
}, [workspaceId]);
} }

View File

@@ -1,76 +0,0 @@
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { SettingsTab } from '../components/Settings/Settings';
import { QUERY_COOKIE_JAR_ID } from './useActiveCookieJar';
import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironment';
export type RouteParamsWorkspace = {
workspaceId: string;
environmentId: string | null;
cookieJarId: string | null;
};
export type RouteParamsRequest = RouteParamsWorkspace & {
requestId: string;
};
export type RouteParamsSettings = {
workspaceId: string;
tab?: SettingsTab;
};
export const paths = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
workspaces(_ = {}) {
return '/workspaces';
},
workspaceSettings({ workspaceId, tab } = { workspaceId: ':workspaceId' } as RouteParamsSettings) {
return `/workspaces/${workspaceId}/settings?tab=${tab ?? SettingsTab.General}`;
},
workspace(
{ workspaceId, environmentId, cookieJarId } = {
workspaceId: ':workspaceId',
environmentId: ':environmentId',
cookieJarId: ':cookieJarId',
} as RouteParamsWorkspace,
) {
const path = `/workspaces/${workspaceId}`;
const params = new URLSearchParams();
if (environmentId != null) params.set(QUERY_ENVIRONMENT_ID, environmentId);
if (cookieJarId != null) params.set(QUERY_COOKIE_JAR_ID, cookieJarId);
return `${path}?${params}`;
},
request(
{ workspaceId, environmentId, requestId, cookieJarId } = {
workspaceId: ':workspaceId',
environmentId: ':environmentId',
requestId: ':requestId',
} as RouteParamsRequest,
) {
const path = `/workspaces/${workspaceId}/requests/${requestId}`;
const params = new URLSearchParams();
if (environmentId != null) params.set(QUERY_ENVIRONMENT_ID, environmentId);
if (cookieJarId != null) params.set(QUERY_COOKIE_JAR_ID, cookieJarId);
return `${path}?${params}`;
},
};
export function useAppRoutes() {
const nav = useNavigate();
const navigate = useCallback(
<T extends keyof typeof paths>(path: T, ...params: Parameters<(typeof paths)[T]>) => {
// Not sure how to make TS work here, but it's good from the
// outside caller perspective.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolvedPath = paths[path](...(params as any));
nav(resolvedPath);
},
[nav],
);
return {
paths,
navigate,
};
}

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import { event } from '@tauri-apps/api'; import { event } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import { InlineCode } from '../components/core/InlineCode'; import { InlineCode } from '../components/core/InlineCode';
import { minPromiseMillis } from '../lib/minPromiseMillis'; import { minPromiseMillis } from '../lib/minPromiseMillis';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from '@yaakapp-internal/models';
import { useCopy } from './useCopy'; import { useCopy } from './useCopy';
import { getResponseBodyText } from '../lib/responseBody'; import { getResponseBodyText } from '../lib/responseBody';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { CookieJar } from '@yaakapp-internal/models'; import type { CookieJar } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai"; import {useSetAtom} from "jotai";
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';

View File

@@ -15,9 +15,9 @@ export function useCreateDropdownItems({
hideIcons?: boolean; hideIcons?: boolean;
folderId?: string | null; folderId?: string | null;
} = {}): DropdownItem[] { } = {}): DropdownItem[] {
const createHttpRequest = useCreateHttpRequest(); const { mutate: createHttpRequest } = useCreateHttpRequest();
const createGrpcRequest = useCreateGrpcRequest(); const { mutate: createGrpcRequest } = useCreateGrpcRequest();
const createFolder = useCreateFolder(); const { mutate: createFolder } = useCreateFolder();
return useMemo<DropdownItem[]>( return useMemo<DropdownItem[]>(
() => [ () => [
@@ -25,14 +25,14 @@ export function useCreateDropdownItems({
key: 'create-http-request', key: 'create-http-request',
label: 'HTTP Request', label: 'HTTP Request',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />, leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => createHttpRequest.mutate({ folderId }), onSelect: () => createHttpRequest({ folderId }),
}, },
{ {
key: 'create-graphql-request', key: 'create-graphql-request',
label: 'GraphQL Query', label: 'GraphQL Query',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />, leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => onSelect: () =>
createHttpRequest.mutate({ createHttpRequest({
folderId, folderId,
bodyType: BODY_TYPE_GRAPHQL, bodyType: BODY_TYPE_GRAPHQL,
method: 'POST', method: 'POST',
@@ -43,7 +43,7 @@ export function useCreateDropdownItems({
key: 'create-grpc-request', key: 'create-grpc-request',
label: 'gRPC Call', label: 'gRPC Call',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />, leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => createGrpcRequest.mutate({ folderId }), onSelect: () => createGrpcRequest({ folderId }),
}, },
...((hideFolder ...((hideFolder
? [] ? []
@@ -55,7 +55,7 @@ export function useCreateDropdownItems({
key: 'create-folder', key: 'create-folder',
label: 'Folder', label: 'Folder',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />, leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => createFolder.mutate({ folderId }), onSelect: () => createFolder({ folderId }),
}, },
]) as DropdownItem[]), ]) as DropdownItem[]),
], ],

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { Environment } from '@yaakapp-internal/models'; import type { Environment } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai"; import {useSetAtom} from "jotai";
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { Folder } from '@yaakapp-internal/models'; import type { Folder } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
@@ -20,6 +20,7 @@ export function useCreateFolder() {
>({ >({
mutationKey: ['create_folder'], mutationKey: ['create_folder'],
mutationFn: async (patch) => { mutationFn: async (patch) => {
console.log("FOLDER", workspace);
if (workspace === null) { if (workspace === null) {
throw new Error("Cannot create folder when there's no active workspace"); throw new Error("Cannot create folder when there's no active workspace");
} }

View File

@@ -1,22 +1,17 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { GrpcRequest } from '@yaakapp-internal/models'; import type { GrpcRequest } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai"; import { useSetAtom } from 'jotai';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import {useActiveCookieJar} from "./useActiveCookieJar"; import { router } from '../main';
import { useActiveEnvironment } from './useActiveEnvironment'; import { Route } from '../routes/workspaces/$workspaceId/requests/$requestId';
import { useActiveRequest } from './useActiveRequest'; import { getActiveRequest } from './useActiveRequest';
import { useActiveWorkspace } from './useActiveWorkspace'; import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes'; import { grpcRequestsAtom } from './useGrpcRequests';
import {grpcRequestsAtom} from "./useGrpcRequests"; import { updateModelList } from './useSyncModelStores';
import {updateModelList} from "./useSyncModelStores";
export function useCreateGrpcRequest() { export function useCreateGrpcRequest() {
const workspace = useActiveWorkspace(); const workspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
const activeRequest = useActiveRequest();
const routes = useAppRoutes();
const setGrpcRequests = useSetAtom(grpcRequestsAtom); const setGrpcRequests = useSetAtom(grpcRequestsAtom);
return useMutation< return useMutation<
@@ -29,6 +24,7 @@ export function useCreateGrpcRequest() {
if (workspace === null) { if (workspace === null) {
throw new Error("Cannot create grpc request when there's no active workspace"); throw new Error("Cannot create grpc request when there's no active workspace");
} }
const activeRequest = getActiveRequest();
if (patch.sortPriority === undefined) { if (patch.sortPriority === undefined) {
if (activeRequest != null) { if (activeRequest != null) {
// Place above currently active request // Place above currently active request
@@ -50,11 +46,13 @@ export function useCreateGrpcRequest() {
// Optimistic update // Optimistic update
setGrpcRequests(updateModelList(request)); setGrpcRequests(updateModelList(request));
routes.navigate('request', { router.navigate({
workspaceId: request.workspaceId, to: Route.fullPath,
requestId: request.id, params: {
environmentId: activeEnvironment?.id ?? null, workspaceId: request.workspaceId,
cookieJarId: activeCookieJar?.id ?? null, requestId: request.id,
},
search: (prev) => ({ ...prev }),
}); });
}, },
}); });

View File

@@ -1,28 +1,24 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index'; import { useSetAtom } from 'jotai/index';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { useActiveCookieJar } from './useActiveCookieJar'; import { router } from '../main';
import { useActiveEnvironment } from './useActiveEnvironment'; import { Route } from '../routes/workspaces/$workspaceId/requests/$requestId';
import { useActiveRequest } from './useActiveRequest'; import { getActiveRequest } from './useActiveRequest';
import { useActiveWorkspace } from './useActiveWorkspace'; import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
import { httpRequestsAtom } from './useHttpRequests'; import { httpRequestsAtom } from './useHttpRequests';
import { updateModelList } from './useSyncModelStores'; import { updateModelList } from './useSyncModelStores';
export function useCreateHttpRequest() { export function useCreateHttpRequest() {
const workspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
const activeRequest = useActiveRequest();
const routes = useAppRoutes();
const setHttpRequests = useSetAtom(httpRequestsAtom); const setHttpRequests = useSetAtom(httpRequestsAtom);
return useMutation<HttpRequest, unknown, Partial<HttpRequest>>({ return useMutation<HttpRequest, unknown, Partial<HttpRequest>>({
mutationKey: ['create_http_request'], mutationKey: ['create_http_request'],
mutationFn: async (patch = {}) => { mutationFn: async (patch = {}) => {
if (workspace === null) { const activeRequest = getActiveRequest();
if (activeWorkspace === null) {
throw new Error("Cannot create request when there's no active workspace"); throw new Error("Cannot create request when there's no active workspace");
} }
if (patch.sortPriority === undefined) { if (patch.sortPriority === undefined) {
@@ -36,7 +32,7 @@ export function useCreateHttpRequest() {
} }
patch.folderId = patch.folderId || activeRequest?.folderId; patch.folderId = patch.folderId || activeRequest?.folderId;
return invokeCmd<HttpRequest>('cmd_create_http_request', { return invokeCmd<HttpRequest>('cmd_create_http_request', {
request: { workspaceId: workspace.id, ...patch }, request: { workspaceId: activeWorkspace.id, ...patch },
}); });
}, },
onSettled: () => trackEvent('http_request', 'create'), onSettled: () => trackEvent('http_request', 'create'),
@@ -44,11 +40,10 @@ export function useCreateHttpRequest() {
// Optimistic update // Optimistic update
setHttpRequests(updateModelList(request)); setHttpRequests(updateModelList(request));
routes.navigate('request', { await router.navigate({
workspaceId: request.workspaceId, to: Route.fullPath,
requestId: request.id, params: { workspaceId: request.workspaceId, requestId: request.id },
environmentId: activeEnvironment?.id ?? null, search: (prev) => ({ ...prev }),
cookieJarId: activeCookieJar?.id ?? null,
}); });
}, },
}); });

View File

@@ -1,14 +1,14 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { Workspace } from '@yaakapp-internal/models'; import type { Workspace } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index'; import { useSetAtom } from 'jotai/index';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { useAppRoutes } from './useAppRoutes'; import { router } from '../main';
import { Route } from '../routes/workspaces/$workspaceId';
import { usePrompt } from './usePrompt'; import { usePrompt } from './usePrompt';
import { updateModelList } from './useSyncModelStores'; import { updateModelList } from './useSyncModelStores';
import { workspacesAtom } from './useWorkspaces'; import { workspacesAtom } from './useWorkspaces';
export function useCreateWorkspace() { export function useCreateWorkspace() {
const routes = useAppRoutes();
const prompt = usePrompt(); const prompt = usePrompt();
const setWorkspaces = useSetAtom(workspacesAtom); const setWorkspaces = useSetAtom(workspacesAtom);
@@ -34,10 +34,9 @@ export function useCreateWorkspace() {
// Optimistic update // Optimistic update
setWorkspaces(updateModelList(workspace)); setWorkspaces(updateModelList(workspace));
routes.navigate('workspace', { router.navigate({
workspaceId: workspace.id, to: Route.fullPath,
environmentId: null, params: { workspaceId: workspace.id },
cookieJarId: null,
}); });
}, },
}); });

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { GrpcRequest } from '@yaakapp-internal/models'; import type { GrpcRequest } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai"; import {useSetAtom} from "jotai";
import { InlineCode } from '../components/core/InlineCode'; import { InlineCode } from '../components/core/InlineCode';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { InlineCode } from '../components/core/InlineCode'; import { InlineCode } from '../components/core/InlineCode';

View File

@@ -1,12 +1,12 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { CookieJar } from '@yaakapp-internal/models'; import type { CookieJar } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai"; import { useSetAtom } from 'jotai';
import { InlineCode } from '../components/core/InlineCode'; import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm'; import { useConfirm } from './useConfirm';
import {cookieJarsAtom} from "./useCookieJars"; import { cookieJarsAtom } from './useCookieJars';
import {removeModelById} from "./useSyncModelStores"; import { removeModelById } from './useSyncModelStores';
export function useDeleteCookieJar(cookieJar: CookieJar | null) { export function useDeleteCookieJar(cookieJar: CookieJar | null) {
const confirm = useConfirm(); const confirm = useConfirm();
@@ -33,6 +33,6 @@ export function useDeleteCookieJar(cookieJar: CookieJar | null) {
if (cookieJar == null) return; if (cookieJar == null) return;
setCookieJars(removeModelById(cookieJar)); setCookieJars(removeModelById(cookieJar));
} },
}); });
} }

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { Environment } from '@yaakapp-internal/models'; import type { Environment } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai"; import {useSetAtom} from "jotai";
import { InlineCode } from '../components/core/InlineCode'; import { InlineCode } from '../components/core/InlineCode';

View File

@@ -1,4 +1,3 @@
import { useMutation } from '@tanstack/react-query';
import type { Folder } from '@yaakapp-internal/models'; import type { Folder } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { InlineCode } from '../components/core/InlineCode'; import { InlineCode } from '../components/core/InlineCode';
@@ -8,6 +7,7 @@ import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm'; import { useConfirm } from './useConfirm';
import { foldersAtom } from './useFolders'; import { foldersAtom } from './useFolders';
import { removeModelById } from './useSyncModelStores'; import { removeModelById } from './useSyncModelStores';
import { useMutation } from './useMutation';
export function useDeleteFolder(id: string | null) { export function useDeleteFolder(id: string | null) {
const confirm = useConfirm(); const confirm = useConfirm();

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { GrpcConnection } from '@yaakapp-internal/models'; import type { GrpcConnection } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai"; import {useSetAtom} from "jotai";
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai"; import {useSetAtom} from "jotai";
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';

View File

@@ -1,6 +1,6 @@
import { useMutation } from '@tanstack/react-query';
import { useDeleteAnyGrpcRequest } from './useDeleteAnyGrpcRequest'; import { useDeleteAnyGrpcRequest } from './useDeleteAnyGrpcRequest';
import { useDeleteAnyHttpRequest } from './useDeleteAnyHttpRequest'; import { useDeleteAnyHttpRequest } from './useDeleteAnyHttpRequest';
import { useMutation } from './useMutation';
export function useDeleteRequest(id: string | null) { export function useDeleteRequest(id: string | null) {
const deleteAnyHttpRequest = useDeleteAnyHttpRequest(); const deleteAnyHttpRequest = useDeleteAnyHttpRequest();

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import { useSetAtom } from 'jotai/index'; import { useSetAtom } from 'jotai/index';
import { count } from '../lib/pluralize'; import { count } from '../lib/pluralize';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';

View File

@@ -1,18 +1,18 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { Workspace } from '@yaakapp-internal/models'; import type { Workspace } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai"; import { useSetAtom } from 'jotai';
import { InlineCode } from '../components/core/InlineCode'; import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { router } from '../main';
import { Route } from '../routes/workspaces';
import { useActiveWorkspace } from './useActiveWorkspace'; import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
import { useConfirm } from './useConfirm'; import { useConfirm } from './useConfirm';
import {removeModelById} from "./useSyncModelStores"; import { removeModelById } from './useSyncModelStores';
import {workspacesAtom} from "./useWorkspaces"; import { workspacesAtom } from './useWorkspaces';
export function useDeleteWorkspace(workspace: Workspace | null) { export function useDeleteWorkspace(workspace: Workspace | null) {
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const routes = useAppRoutes();
const confirm = useConfirm(); const confirm = useConfirm();
const setWorkspaces = useSetAtom(workspacesAtom); const setWorkspaces = useSetAtom(workspacesAtom);
@@ -41,7 +41,7 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
const { id: workspaceId } = workspace; const { id: workspaceId } = workspace;
if (workspaceId === activeWorkspace?.id) { if (workspaceId === activeWorkspace?.id) {
routes.navigate('workspaces'); router.navigate({ to: Route.fullPath });
} }
}, },
}); });

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';

View File

@@ -1,11 +1,9 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { GrpcRequest } from '@yaakapp-internal/models'; import type { GrpcRequest } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import {useActiveCookieJar} from "./useActiveCookieJar"; import { router } from '../main';
import { useActiveEnvironment } from './useActiveEnvironment'; import { Route } from '../routes/workspaces/$workspaceId/requests/$requestId';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
import { getGrpcProtoFiles, setGrpcProtoFiles } from './useGrpcProtoFiles'; import { getGrpcProtoFiles, setGrpcProtoFiles } from './useGrpcProtoFiles';
export function useDuplicateGrpcRequest({ export function useDuplicateGrpcRequest({
@@ -15,11 +13,6 @@ export function useDuplicateGrpcRequest({
id: string | null; id: string | null;
navigateAfter: boolean; navigateAfter: boolean;
}) { }) {
const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
const routes = useAppRoutes();
return useMutation<GrpcRequest, string>({ return useMutation<GrpcRequest, string>({
mutationKey: ['duplicate_grpc_request', id], mutationKey: ['duplicate_grpc_request', id],
mutationFn: async () => { mutationFn: async () => {
@@ -34,12 +27,11 @@ export function useDuplicateGrpcRequest({
const protoFiles = await getGrpcProtoFiles(id); const protoFiles = await getGrpcProtoFiles(id);
await setGrpcProtoFiles(request.id, protoFiles); await setGrpcProtoFiles(request.id, protoFiles);
if (navigateAfter && activeWorkspace !== null) { if (navigateAfter) {
routes.navigate('request', { await router.navigate({
workspaceId: activeWorkspace.id, to: Route.fullPath,
requestId: request.id, params: { workspaceId: request.workspaceId, requestId: request.id },
environmentId: activeEnvironment?.id ?? null, search: (prev) => ({ ...prev }),
cookieJarId: activeCookieJar?.id ?? null,
}); });
} }
}, },

View File

@@ -1,11 +1,9 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import {useActiveCookieJar} from "./useActiveCookieJar"; import { router } from '../main';
import { useActiveEnvironment } from './useActiveEnvironment'; import { Route } from '../routes/workspaces/$workspaceId/requests/$requestId';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
export function useDuplicateHttpRequest({ export function useDuplicateHttpRequest({
id, id,
@@ -14,10 +12,6 @@ export function useDuplicateHttpRequest({
id: string | null; id: string | null;
navigateAfter: boolean; navigateAfter: boolean;
}) { }) {
const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
const routes = useAppRoutes();
return useMutation<HttpRequest, string>({ return useMutation<HttpRequest, string>({
mutationKey: ['duplicate_http_request', id], mutationKey: ['duplicate_http_request', id],
mutationFn: async () => { mutationFn: async () => {
@@ -26,12 +20,14 @@ export function useDuplicateHttpRequest({
}, },
onSettled: () => trackEvent('http_request', 'duplicate'), onSettled: () => trackEvent('http_request', 'duplicate'),
onSuccess: async (request) => { onSuccess: async (request) => {
if (navigateAfter && activeWorkspace !== null) { if (navigateAfter) {
routes.navigate('request', { router.navigate({
workspaceId: activeWorkspace.id, to: Route.fullPath,
requestId: request.id, params: {
environmentId: activeEnvironment?.id ?? null, workspaceId: request.workspaceId,
cookieJarId: activeCookieJar?.id ?? null, requestId: request.id,
},
search: (prev) => ({ ...prev }),
}); });
} }
}, },

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import { useDialog } from '../components/DialogContext'; import { useDialog } from '../components/DialogContext';
import { ExportDataDialog } from '../components/ExportDataDialog'; import { ExportDataDialog } from '../components/ExportDataDialog';
import { useActiveWorkspace } from './useActiveWorkspace'; import { useActiveWorkspace } from './useActiveWorkspace';

View File

@@ -7,6 +7,7 @@ import type {
} from '@yaakapp-internal/plugin'; } from '@yaakapp-internal/plugin';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { usePluginsKey } from './usePlugins'; import { usePluginsKey } from './usePlugins';
import { useMemo } from 'react';
export type CallableHttpRequestAction = Pick<HttpRequestAction, 'key' | 'label' | 'icon'> & { export type CallableHttpRequestAction = Pick<HttpRequestAction, 'key' | 'label' | 'icon'> & {
call: (httpRequest: HttpRequest) => Promise<void>; call: (httpRequest: HttpRequest) => Promise<void>;
@@ -15,32 +16,35 @@ export type CallableHttpRequestAction = Pick<HttpRequestAction, 'key' | 'label'
export function useHttpRequestActions() { export function useHttpRequestActions() {
const pluginsKey = usePluginsKey(); const pluginsKey = usePluginsKey();
const httpRequestActions = useQuery({ const actionsResult = useQuery<CallableHttpRequestAction[]>({
queryKey: ['http_request_actions', pluginsKey], queryKey: ['http_request_actions', pluginsKey],
queryFn: async () => { queryFn: async () => {
const responses = (await invokeCmd( const responses = await invokeCmd<GetHttpRequestActionsResponse[]>(
'cmd_http_request_actions', 'cmd_http_request_actions',
)) as GetHttpRequestActionsResponse[]; );
return responses;
return responses.flatMap((r) =>
r.actions.map((a) => ({
key: a.key,
label: a.label,
icon: a.icon,
call: async (httpRequest: HttpRequest) => {
const payload: CallHttpRequestActionRequest = {
key: a.key,
pluginRefId: r.pluginRefId,
args: { httpRequest },
};
await invokeCmd('cmd_call_http_request_action', { req: payload });
},
})),
);
}, },
}); });
const actions: CallableHttpRequestAction[] = const actions = useMemo(() => {
httpRequestActions.data?.flatMap((r) => return actionsResult.data ?? [];
r.actions.map((a) => ({ // eslint-disable-next-line react-hooks/exhaustive-deps
key: a.key, }, [JSON.stringify(actionsResult.data)]);
label: a.label,
icon: a.icon,
call: async (httpRequest: HttpRequest) => {
const payload: CallHttpRequestActionRequest = {
key: a.key,
pluginRefId: r.pluginRefId,
args: { httpRequest },
};
await invokeCmd('cmd_call_http_request_action', { req: payload });
},
})),
) ?? [];
return actions; return actions;
} }

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from '@yaakapp-internal/models';
import { useToast } from '../components/ToastContext'; import { useToast } from '../components/ToastContext';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { import type {
Environment, Environment,
Folder, Folder,
@@ -13,12 +13,12 @@ import { useDialog } from '../components/DialogContext';
import { ImportDataDialog } from '../components/ImportDataDialog'; import { ImportDataDialog } from '../components/ImportDataDialog';
import { count } from '../lib/pluralize'; import { count } from '../lib/pluralize';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { Route } from '../routes/workspaces/$workspaceId';
import { useActiveWorkspace } from './useActiveWorkspace'; import { useActiveWorkspace } from './useActiveWorkspace';
import { useAlert } from './useAlert'; import { useAlert } from './useAlert';
import { useAppRoutes } from './useAppRoutes'; import { router } from '../main';
export function useImportData() { export function useImportData() {
const routes = useAppRoutes();
const dialog = useDialog(); const dialog = useDialog();
const alert = useAlert(); const alert = useAlert();
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
@@ -64,10 +64,11 @@ export function useImportData() {
}); });
if (importedWorkspace != null) { if (importedWorkspace != null) {
routes.navigate('workspace', { const environmentId = imported.environments[0]?.id ?? null;
workspaceId: importedWorkspace.id, router.navigate({
environmentId: imported.environments[0]?.id ?? null, to: Route.fullPath,
cookieJarId: null, params: { workspaceId: importedWorkspace.id },
search: { environmentId },
}); });
} }

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { HttpUrlParameter } from '@yaakapp-internal/models'; import type { HttpUrlParameter } from '@yaakapp-internal/models';
import { useToast } from '../components/ToastContext'; import { useToast } from '../components/ToastContext';
import { pluralize } from '../lib/pluralize'; import { pluralize } from '../lib/pluralize';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';

View File

@@ -49,7 +49,7 @@ export function useKeyValue<T extends object | boolean | number | string | null>
} }
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[fallback, key, namespace], [typeof key === 'string' ? key : key.join('::'), namespace],
); );
const reset = useCallback(async () => mutate.mutateAsync(fallback), [mutate, fallback]); const reset = useCallback(async () => mutate.mutateAsync(fallback), [mutate, fallback]);

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import React from 'react'; import React from 'react';
import { useDialog } from '../components/DialogContext'; import { useDialog } from '../components/DialogContext';
import { MoveToWorkspaceDialog } from '../components/MoveToWorkspaceDialog'; import { MoveToWorkspaceDialog } from '../components/MoveToWorkspaceDialog';

View File

@@ -0,0 +1,42 @@
import type { MutationKey } from '@tanstack/react-query';
import { useCallback } from 'react';
export function useMutation<TData = unknown, TError = unknown, TVariables = void>({
mutationKey,
mutationFn,
onSuccess,
onSettled,
}: {
mutationKey: MutationKey;
mutationFn: (vars: TVariables) => Promise<TData>;
onSettled?: () => void;
onSuccess?: (data: TData) => void;
}) {
const mutateAsync = useCallback(
async (variables: TVariables) => {
try {
const data = await mutationFn(variables);
onSuccess?.(data);
} catch (err: unknown) {
const e = err as TError;
console.log('MUTATION FAILED', mutationKey, e);
} finally {
onSettled?.();
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
mutationKey,
);
const mutate = useCallback(
(variables: TVariables) => {
setTimeout(() => mutateAsync(variables));
},
[mutateAsync],
);
return {
mutate,
mutateAsync,
};
}

View File

@@ -1,21 +1,27 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import { SettingsTab } from '../components/Settings/Settings'; import { SettingsTab } from '../components/Settings/Settings';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { router } from '../main';
import { Route as SettingsRoute } from '../routes/workspaces/settings';
import { useActiveWorkspace } from './useActiveWorkspace'; import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
export function useOpenSettings(tab = SettingsTab.General) { export function useOpenSettings(tab = SettingsTab.General) {
const routes = useAppRoutes();
const workspace = useActiveWorkspace(); const workspace = useActiveWorkspace();
return useMutation({ return useMutation({
mutationKey: ['open_settings'], mutationKey: ['open_settings'],
mutationFn: async () => { mutationFn: async () => {
if (workspace == null) return; if (workspace == null) return;
trackEvent('dialog', 'show', { id: 'settings', tab: `${tab}` }); trackEvent('dialog', 'show', { id: 'settings', tab: `${tab}` });
const location = router.buildLocation({
to: SettingsRoute.fullPath,
params: { workspaceId: workspace.id },
search: { tab },
});
await invokeCmd('cmd_new_child_window', { await invokeCmd('cmd_new_child_window', {
url: routes.paths.workspaceSettings({ workspaceId: workspace.id, tab }), url: location,
label: 'settings', label: 'settings',
title: 'Yaak Settings', title: 'Yaak Settings',
innerSize: [600, 550], innerSize: [600, 550],

View File

@@ -1,13 +1,13 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { useAppRoutes } from './useAppRoutes'; import { router } from '../main';
import { Route as WorkspaceRoute } from '../routes/workspaces/$workspaceId';
import { Route as RequestRoute } from '../routes/workspaces/$workspaceId/requests/$requestId';
import { getRecentCookieJars } from './useRecentCookieJars'; import { getRecentCookieJars } from './useRecentCookieJars';
import { getRecentEnvironments } from './useRecentEnvironments'; import { getRecentEnvironments } from './useRecentEnvironments';
import { getRecentRequests } from './useRecentRequests'; import { getRecentRequests } from './useRecentRequests';
export function useOpenWorkspace() { export function useOpenWorkspace() {
const routes = useAppRoutes();
return useMutation({ return useMutation({
mutationKey: ['open_workspace'], mutationKey: ['open_workspace'],
mutationFn: async ({ mutationFn: async ({
@@ -17,22 +17,25 @@ export function useOpenWorkspace() {
workspaceId: string; workspaceId: string;
inNewWindow: boolean; inNewWindow: boolean;
}) => { }) => {
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null; const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? undefined;
const requestId = (await getRecentRequests(workspaceId))[0] ?? null; const requestId = (await getRecentRequests(workspaceId))[0] ?? undefined;
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null; const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? undefined;
const baseArgs = { workspaceId, environmentId, cookieJarId } as const; const search = { environmentId, cookieJarId };
if (inNewWindow) { if (inNewWindow) {
const path = const location = router.buildLocation({
requestId != null to: WorkspaceRoute.fullPath,
? routes.paths.request({ ...baseArgs, requestId }) params: { workspaceId },
: routes.paths.workspace({ ...baseArgs }); search,
await invokeCmd('cmd_new_main_window', { url: path }); });
await invokeCmd('cmd_new_main_window', { url: location });
return;
}
if (requestId != null) {
router.navigate({ to: RequestRoute.fullPath, params: { workspaceId, requestId }, search });
} else { } else {
if (requestId != null) { router.navigate({ to: WorkspaceRoute.fullPath, params: { workspaceId }, search });
routes.navigate('request', { ...baseArgs, requestId });
} else {
routes.navigate('workspace', { ...baseArgs });
}
} }
}, },
}); });

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { Plugin } from '@yaakapp-internal/models'; import type { Plugin } from '@yaakapp-internal/models';
import { atom, useAtomValue, useSetAtom } from 'jotai'; import { atom, useAtomValue, useSetAtom } from 'jotai';
import { minPromiseMillis } from '../lib/minPromiseMillis'; import { minPromiseMillis } from '../lib/minPromiseMillis';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models'; import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode'; import { InlineCode } from '../components/core/InlineCode';
import { usePrompt } from './usePrompt'; import { usePrompt } from './usePrompt';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import { save } from '@tauri-apps/plugin-dialog'; import { save } from '@tauri-apps/plugin-dialog';
import mime from 'mime'; import mime from 'mime';
import slugify from 'slugify'; import slugify from 'slugify';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { getHttpRequest } from '../lib/store'; import { getHttpRequest } from '../lib/store';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import { useSendAnyHttpRequest } from './useSendAnyHttpRequest'; import { useSendAnyHttpRequest } from './useSendAnyHttpRequest';
export function useSendManyRequests() { export function useSendManyRequests() {

View File

@@ -1,15 +1,14 @@
import { emit } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { fallbackRequestName } from '../lib/fallbackRequestName'; import { fallbackRequestName } from '../lib/fallbackRequestName';
import { useActiveEnvironment } from './useActiveEnvironment'; import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveRequest } from './useActiveRequest'; import { getActiveRequest } from './useActiveRequest';
import { useActiveWorkspace } from './useActiveWorkspace'; import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppInfo } from './useAppInfo'; import { useAppInfo } from './useAppInfo';
import { useOsInfo } from './useOsInfo'; import { useOsInfo } from './useOsInfo';
import { emit } from '@tauri-apps/api/event';
export function useSyncWorkspaceRequestTitle() { export function useSyncWorkspaceRequestTitle() {
const activeRequest = useActiveRequest();
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment(); const [activeEnvironment] = useActiveEnvironment();
const osInfo = useOsInfo(); const osInfo = useOsInfo();
@@ -24,6 +23,7 @@ export function useSyncWorkspaceRequestTitle() {
if (activeEnvironment) { if (activeEnvironment) {
newTitle += ` [${activeEnvironment.name}]`; newTitle += ` [${activeEnvironment.name}]`;
} }
const activeRequest = getActiveRequest();
if (activeRequest) { if (activeRequest) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
newTitle += ` ${fallbackRequestName(activeRequest)}`; newTitle += ` ${fallbackRequestName(activeRequest)}`;
@@ -40,5 +40,5 @@ export function useSyncWorkspaceRequestTitle() {
} else { } else {
emit('yaak_title_changed', newTitle).catch(console.error); emit('yaak_title_changed', newTitle).catch(console.error);
} }
}, [activeEnvironment, activeRequest, activeWorkspace, appInfo.isDev, osInfo.osType]); }, [activeEnvironment, activeWorkspace, appInfo.isDev, osInfo.osType]);
} }

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { Plugin } from '@yaakapp-internal/models'; import type { Plugin } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { Folder } from '@yaakapp-internal/models'; import type { Folder } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai/index"; import {useSetAtom} from "jotai/index";
import { getFolder } from '../lib/store'; import { getFolder } from '../lib/store';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { GrpcRequest } from '@yaakapp-internal/models'; import type { GrpcRequest } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index'; import { useSetAtom } from 'jotai/index';
import { getGrpcRequest } from '../lib/store'; import { getGrpcRequest } from '../lib/store';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai/index"; import {useSetAtom} from "jotai/index";
import { getHttpRequest } from '../lib/store'; import { getHttpRequest } from '../lib/store';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { CookieJar } from '@yaakapp-internal/models'; import type { CookieJar } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index'; import { useSetAtom } from 'jotai/index';
import { getCookieJar } from '../lib/store'; import { getCookieJar } from '../lib/store';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { Environment } from '@yaakapp-internal/models'; import type { Environment } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index'; import { useSetAtom } from 'jotai/index';
import { getEnvironment } from '../lib/store'; import { getEnvironment } from '../lib/store';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { Settings } from '@yaakapp-internal/models'; import type { Settings } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { getSettings } from '../lib/store'; import { getSettings } from '../lib/store';

View File

@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from './useMutation';
import type { Workspace } from '@yaakapp-internal/models'; import type { Workspace } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai/index"; import {useSetAtom} from "jotai/index";
import { getWorkspace } from '../lib/store'; import { getWorkspace } from '../lib/store';

View File

@@ -1,9 +1,10 @@
import './main.css';
import { createRouter, RouterProvider } from '@tanstack/react-router';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { type } from '@tauri-apps/plugin-os'; import { type } from '@tauri-apps/plugin-os';
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { App } from './components/App'; import { routeTree } from './routeTree.gen';
import './main.css';
import('react-pdf').then(({ pdfjs }) => { import('react-pdf').then(({ pdfjs }) => {
pdfjs.GlobalWorkerOptions.workerSrc = new URL( pdfjs.GlobalWorkerOptions.workerSrc = new URL(
@@ -24,8 +25,20 @@ window.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && e.target === document.body) e.preventDefault(); if (e.key === 'Backspace' && e.target === document.body) e.preventDefault();
}); });
// Create a new router instance
export const router = createRouter({
routeTree,
});
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
createRoot(document.getElementById('root') as HTMLElement).render( createRoot(document.getElementById('root') as HTMLElement).render(
<StrictMode> <StrictMode>
<App /> <RouterProvider router={router} />
</StrictMode>, </StrictMode>,
); );

View File

@@ -20,8 +20,9 @@
"@lezer/lr": "^1.3.3", "@lezer/lr": "^1.3.3",
"@react-hook/size": "^2.1.2", "@react-hook/size": "^2.1.2",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.59.16", "@tanstack/react-query": "^5.62.8",
"@tanstack/react-virtual": "^3.10.8", "@tanstack/react-router": "^1.91.3",
"@tanstack/react-virtual": "^3.11.2",
"@tauri-apps/api": "^2.0.1", "@tauri-apps/api": "^2.0.1",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0", "@tauri-apps/plugin-clipboard-manager": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.0", "@tauri-apps/plugin-dialog": "^2.0.0",
@@ -40,6 +41,7 @@
"format-graphql": "^1.5.0", "format-graphql": "^1.5.0",
"framer-motion": "^11.5.4", "framer-motion": "^11.5.4",
"fuzzbunny": "^1.0.1", "fuzzbunny": "^1.0.1",
"history": "^5.3.0",
"jotai": "^2.9.3", "jotai": "^2.9.3",
"lucide-react": "^0.439.0", "lucide-react": "^0.439.0",
"mime": "^4.0.4", "mime": "^4.0.4",
@@ -63,7 +65,9 @@
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.7.1", "@lezer/generator": "^1.7.1",
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e", "@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tanstack/react-query-devtools": "^5.55.4", "@tanstack/react-query-devtools": "^5.62.8",
"@tanstack/router-devtools": "^1.91.3",
"@tanstack/router-plugin": "^1.91.1",
"@types/node": "^22.5.4", "@types/node": "^22.5.4",
"@types/papaparse": "^5.3.14", "@types/papaparse": "^5.3.14",
"@types/parse-color": "^1.0.3", "@types/parse-color": "^1.0.3",

200
src-web/routeTree.gen.ts Normal file
View File

@@ -0,0 +1,200 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as IndexImport } from './routes/index'
import { Route as WorkspacesIndexImport } from './routes/workspaces/index'
import { Route as WorkspacesSettingsImport } from './routes/workspaces/settings'
import { Route as WorkspacesWorkspaceIdIndexImport } from './routes/workspaces/$workspaceId/index'
import { Route as WorkspacesWorkspaceIdRequestsRequestIdImport } from './routes/workspaces/$workspaceId/requests/$requestId'
// Create/Update Routes
const IndexRoute = IndexImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
const WorkspacesIndexRoute = WorkspacesIndexImport.update({
id: '/workspaces/',
path: '/workspaces/',
getParentRoute: () => rootRoute,
} as any)
const WorkspacesSettingsRoute = WorkspacesSettingsImport.update({
id: '/workspaces/settings',
path: '/workspaces/settings',
getParentRoute: () => rootRoute,
} as any)
const WorkspacesWorkspaceIdIndexRoute = WorkspacesWorkspaceIdIndexImport.update(
{
id: '/workspaces/$workspaceId/',
path: '/workspaces/$workspaceId/',
getParentRoute: () => rootRoute,
} as any,
)
const WorkspacesWorkspaceIdRequestsRequestIdRoute =
WorkspacesWorkspaceIdRequestsRequestIdImport.update({
id: '/workspaces/$workspaceId/requests/$requestId',
path: '/workspaces/$workspaceId/requests/$requestId',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/workspaces/settings': {
id: '/workspaces/settings'
path: '/workspaces/settings'
fullPath: '/workspaces/settings'
preLoaderRoute: typeof WorkspacesSettingsImport
parentRoute: typeof rootRoute
}
'/workspaces/': {
id: '/workspaces/'
path: '/workspaces'
fullPath: '/workspaces'
preLoaderRoute: typeof WorkspacesIndexImport
parentRoute: typeof rootRoute
}
'/workspaces/$workspaceId/': {
id: '/workspaces/$workspaceId/'
path: '/workspaces/$workspaceId'
fullPath: '/workspaces/$workspaceId'
preLoaderRoute: typeof WorkspacesWorkspaceIdIndexImport
parentRoute: typeof rootRoute
}
'/workspaces/$workspaceId/requests/$requestId': {
id: '/workspaces/$workspaceId/requests/$requestId'
path: '/workspaces/$workspaceId/requests/$requestId'
fullPath: '/workspaces/$workspaceId/requests/$requestId'
preLoaderRoute: typeof WorkspacesWorkspaceIdRequestsRequestIdImport
parentRoute: typeof rootRoute
}
}
}
// Create and export the route tree
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/workspaces/settings': typeof WorkspacesSettingsRoute
'/workspaces': typeof WorkspacesIndexRoute
'/workspaces/$workspaceId': typeof WorkspacesWorkspaceIdIndexRoute
'/workspaces/$workspaceId/requests/$requestId': typeof WorkspacesWorkspaceIdRequestsRequestIdRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/workspaces/settings': typeof WorkspacesSettingsRoute
'/workspaces': typeof WorkspacesIndexRoute
'/workspaces/$workspaceId': typeof WorkspacesWorkspaceIdIndexRoute
'/workspaces/$workspaceId/requests/$requestId': typeof WorkspacesWorkspaceIdRequestsRequestIdRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/workspaces/settings': typeof WorkspacesSettingsRoute
'/workspaces/': typeof WorkspacesIndexRoute
'/workspaces/$workspaceId/': typeof WorkspacesWorkspaceIdIndexRoute
'/workspaces/$workspaceId/requests/$requestId': typeof WorkspacesWorkspaceIdRequestsRequestIdRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/workspaces/settings'
| '/workspaces'
| '/workspaces/$workspaceId'
| '/workspaces/$workspaceId/requests/$requestId'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/workspaces/settings'
| '/workspaces'
| '/workspaces/$workspaceId'
| '/workspaces/$workspaceId/requests/$requestId'
id:
| '__root__'
| '/'
| '/workspaces/settings'
| '/workspaces/'
| '/workspaces/$workspaceId/'
| '/workspaces/$workspaceId/requests/$requestId'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
WorkspacesSettingsRoute: typeof WorkspacesSettingsRoute
WorkspacesIndexRoute: typeof WorkspacesIndexRoute
WorkspacesWorkspaceIdIndexRoute: typeof WorkspacesWorkspaceIdIndexRoute
WorkspacesWorkspaceIdRequestsRequestIdRoute: typeof WorkspacesWorkspaceIdRequestsRequestIdRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
WorkspacesSettingsRoute: WorkspacesSettingsRoute,
WorkspacesIndexRoute: WorkspacesIndexRoute,
WorkspacesWorkspaceIdIndexRoute: WorkspacesWorkspaceIdIndexRoute,
WorkspacesWorkspaceIdRequestsRequestIdRoute:
WorkspacesWorkspaceIdRequestsRequestIdRoute,
}
export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/",
"/workspaces/settings",
"/workspaces/",
"/workspaces/$workspaceId/",
"/workspaces/$workspaceId/requests/$requestId"
]
},
"/": {
"filePath": "index.tsx"
},
"/workspaces/settings": {
"filePath": "workspaces/settings.tsx"
},
"/workspaces/": {
"filePath": "workspaces/index.tsx"
},
"/workspaces/$workspaceId/": {
"filePath": "workspaces/$workspaceId/index.tsx"
},
"/workspaces/$workspaceId/requests/$requestId": {
"filePath": "workspaces/$workspaceId/requests/$requestId.tsx"
}
}
}
ROUTE_MANIFEST_END */

91
src-web/routes/__root.tsx Normal file
View File

@@ -0,0 +1,91 @@
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createRootRoute, Outlet } from '@tanstack/react-router';
import classNames from 'classnames';
import { MotionConfig } from 'framer-motion';
import { createStore, Provider as JotaiProvider } from 'jotai';
import React, { Suspense } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { HelmetProvider } from 'react-helmet-async';
import { DialogProvider, Dialogs } from '../components/DialogContext';
import { GlobalHooks } from '../components/GlobalHooks';
import { ToastProvider, Toasts } from '../components/ToastContext';
import { useOsInfo } from '../hooks/useOsInfo';
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (err, query) => {
console.log('Query client error', { err, query });
},
}),
defaultOptions: {
queries: {
retry: false,
networkMode: 'always',
refetchOnWindowFocus: true,
refetchOnReconnect: false,
refetchOnMount: false, // Don't refetch when a hook mounts
},
},
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const TanStackRouterDevtools =
process.env.NODE_ENV === 'production'
? () => null // Render nothing in production
: React.lazy(() =>
import('@tanstack/router-devtools').then((res) => ({
default: res.TanStackRouterDevtools,
})),
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ReactQueryDevtools =
process.env.NODE_ENV === 'production'
? () => null // Render nothing in production
: React.lazy(() =>
import('@tanstack/react-query-devtools').then((res) => ({
default: res.ReactQueryDevtools,
})),
);
export const Route = createRootRoute({
component: RouteComponent,
});
export const jotaiStore = createStore();
function RouteComponent() {
const osInfo = useOsInfo();
return (
<JotaiProvider store={jotaiStore}>
<QueryClientProvider client={queryClient}>
<MotionConfig transition={{ duration: 0.1 }}>
<HelmetProvider>
<DndProvider backend={HTML5Backend}>
<Suspense>
<DialogProvider>
<ToastProvider>
<GlobalHooks />
<Toasts />
<Dialogs />
<div
className={classNames(
'w-full h-full',
osInfo?.osType === 'linux' && 'border border-border-subtle',
)}
>
<Outlet />
</div>
</ToastProvider>
</DialogProvider>
</Suspense>
</DndProvider>
</HelmetProvider>
</MotionConfig>
{/*<ReactQueryDevtools initialIsOpen />*/}
{/*<TanStackRouterDevtools initialIsOpen />*/}
</QueryClientProvider>
</JotaiProvider>
);
}

10
src-web/routes/index.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import { RedirectToLatestWorkspace } from '../components/RedirectToLatestWorkspace'
export const Route = createFileRoute('/')({
component: RouteComponent,
})
function RouteComponent() {
return <RedirectToLatestWorkspace />
}

View File

@@ -0,0 +1,19 @@
import { createFileRoute } from '@tanstack/react-router';
import { Workspace } from '../../../components/Workspace';
interface WorkspaceSearchSchema {
cookieJarId?: string | null;
environmentId?: string | null;
}
export const Route = createFileRoute('/workspaces/$workspaceId/')({
component: RouteComponent,
validateSearch: (search: Record<string, unknown>): WorkspaceSearchSchema => ({
environmentId: search.environment_id as string,
cookieJarId: search.cookie_jar_id as string,
}),
});
function RouteComponent() {
return <Workspace />;
}

View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router';
import { Workspace } from '../../../../components/Workspace';
export const Route = createFileRoute('/workspaces/$workspaceId/requests/$requestId')({
component: RouteComponent,
});
function RouteComponent() {
return <Workspace />;
}

View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import { RedirectToLatestWorkspace } from '../../components/RedirectToLatestWorkspace'
export const Route = createFileRoute('/workspaces/')({
component: RouteComponent,
})
function RouteComponent() {
return <RedirectToLatestWorkspace />
}

View File

@@ -0,0 +1,17 @@
import { createFileRoute } from '@tanstack/react-router'
import Settings, { SettingsTab } from '../../components/Settings/Settings'
interface SettingsSearchSchema {
tab?: SettingsTab
}
export const Route = createFileRoute('/workspaces/settings')({
component: RouteComponent,
validateSearch: (search: Record<string, unknown>): SettingsSearchSchema => ({
tab: (search.tab ?? SettingsTab.General) as SettingsTab,
}),
})
function RouteComponent() {
return <Settings />
}

3
src-web/tsr.config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"autoCodeSplitting": true
}

View File

@@ -1,7 +1,8 @@
import path from 'node:path'; import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { internalIpV4 } from 'internal-ip'; import { internalIpV4 } from 'internal-ip';
import { createRequire } from 'node:module'; import { createRequire } from 'node:module';
import path from 'node:path';
import { defineConfig, normalizePath } from 'vite'; import { defineConfig, normalizePath } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy'; import { viteStaticCopy } from 'vite-plugin-static-copy';
import svgr from 'vite-plugin-svgr'; import svgr from 'vite-plugin-svgr';
@@ -20,6 +21,10 @@ const mobile = !!/android|ios/.exec(process.env.TAURI_ENV_PLATFORM ?? '');
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(async () => ({ export default defineConfig(async () => ({
plugins: [ plugins: [
TanStackRouterVite({
routesDirectory: './routes',
generatedRouteTree: './routeTree.gen.ts',
}),
svgr(), svgr(),
react(), react(),
topLevelAwait(), topLevelAwait(),