Remove useNavigate everywhere, and make request a query param. And convert dialog to Jotai

This commit is contained in:
Gregory Schier
2025-01-06 16:54:07 -08:00
parent 806a8eb801
commit bc50891edb
54 changed files with 592 additions and 545 deletions

View File

@@ -1,4 +1,3 @@
import { useNavigate } from '@tanstack/react-router';
import classNames from 'classnames';
import { fuzzyFilter } from 'fuzzbunny';
import type { KeyboardEvent, ReactNode } from 'react';
@@ -13,7 +12,6 @@ import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDebouncedState } from '../hooks/useDebouncedState';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDialog } from '../hooks/useDialog';
import { useEnvironments } from '../hooks/useEnvironments';
import type { HotkeyAction } from '../hooks/useHotKey';
import { useHotKey } from '../hooks/useHotKey';
@@ -29,7 +27,9 @@ import { useScrollIntoView } from '../hooks/useScrollIntoView';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { showDialog, toggleDialog } from '../lib/dialog';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { router } from '../lib/router';
import { CookieDialog } from './CookieDialog';
import { Button } from './core/Button';
import { Heading } from './core/Heading';
@@ -70,16 +70,14 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const openWorkspace = useOpenWorkspace();
const createHttpRequest = useCreateHttpRequest();
const { createFolder } = useCommands();
const [activeCookieJar] = useActiveCookieJar();
const activeCookieJar = useActiveCookieJar();
const createGrpcRequest = useCreateGrpcRequest();
const createEnvironment = useCreateEnvironment();
const dialog = useDialog();
const sendRequest = useSendAnyHttpRequest();
const renameRequest = useRenameRequest(activeRequest?.id ?? null);
const deleteRequest = useDeleteRequest(activeRequest?.id ?? null);
const [, setSidebarHidden] = useSidebarHidden();
const openSettings = useOpenSettings();
const navigate = useNavigate();
const { baseEnvironment } = useEnvironments();
const workspaceCommands = useMemo<CommandPaletteItem[]>(() => {
@@ -109,7 +107,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
key: 'cookies.show',
label: 'Show Cookies',
onSelect: async () => {
dialog.show({
showDialog({
id: 'cookies',
title: 'Manage Cookies',
size: 'full',
@@ -127,7 +125,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
label: 'Edit Environment',
action: 'environmentEditor.toggle',
onSelect: () => {
dialog.toggle({
toggleDialog({
id: 'environment-editor',
noPadding: true,
size: 'lg',
@@ -195,7 +193,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
createHttpRequest,
createWorkspace,
deleteRequest.mutate,
dialog,
httpRequestActions,
openSettings.mutate,
renameRequest.mutate,
@@ -284,13 +281,10 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
</HStack>
),
onSelect: async () => {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: {
workspaceId: r.workspaceId,
requestId: r.id,
},
search: (prev) => ({ ...prev }),
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: r.workspaceId },
search: (prev) => ({ ...prev, request_id: r.id }),
});
},
});
@@ -331,7 +325,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
}, [
workspaceCommands,
sortedRequests,
navigate,
sortedEnvironments,
activeEnvironment?.id,
setActiveEnvironmentId,

View File

@@ -1,11 +1,11 @@
import { memo, useCallback } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { memo, useMemo } from 'react';
import { setActiveCookieJar, useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { cookieJarsAtom } from '../hooks/useCookieJars';
import { useCreateCookieJar } from '../hooks/useCreateCookieJar';
import { useDeleteCookieJar } from '../hooks/useDeleteCookieJar';
import { useDialog } from '../hooks/useDialog';
import { usePrompt } from '../hooks/usePrompt';
import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar';
import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai';
import { CookieDialog } from './CookieDialog';
import { Dropdown, type DropdownItem } from './core/Dropdown';
@@ -14,21 +14,20 @@ import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
export const CookieDropdown = memo(function CookieDropdown() {
const [activeCookieJar, setActiveCookieJarId] = useActiveCookieJar();
const activeCookieJar = useActiveCookieJar();
const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null);
const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null);
const createCookieJar = useCreateCookieJar();
const dialog = useDialog();
const prompt = usePrompt();
const getItems = useCallback((): DropdownItem[] => {
const items = useMemo((): DropdownItem[] => {
const cookieJars = jotaiStore.get(cookieJarsAtom) ?? [];
return [
...cookieJars.map((j) => ({
key: j.id,
label: j.name,
leftSlot: <Icon icon={j.id === activeCookieJar?.id ? 'check' : 'empty'} />,
onSelect: () => setActiveCookieJarId(j.id),
onSelect: () => setActiveCookieJar(j),
})),
...((cookieJars.length > 0 && activeCookieJar != null
? [
@@ -39,7 +38,7 @@ export const CookieDropdown = memo(function CookieDropdown() {
leftSlot: <Icon icon="cookie" />,
onSelect: () => {
if (activeCookieJar == null) return;
dialog.show({
showDialog({
id: 'cookies',
title: 'Manage Cookies',
size: 'full',
@@ -90,18 +89,10 @@ export const CookieDropdown = memo(function CookieDropdown() {
onSelect: () => createCookieJar.mutate(),
},
];
}, [
activeCookieJar,
createCookieJar,
deleteCookieJar,
dialog,
prompt,
setActiveCookieJarId,
updateCookieJar,
]);
}, [activeCookieJar, createCookieJar, deleteCookieJar, prompt, updateCookieJar]);
return (
<Dropdown items={getItems}>
<Dropdown items={items}>
<IconButton size="sm" icon="cookie" title="Cookie Jar" />
</Dropdown>
);

View File

@@ -1,5 +0,0 @@
import { createContext } from 'react';
import type { DialogState } from './Dialogs';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const DialogContext = createContext<DialogState>({} as DialogState);

View File

@@ -1,74 +1,29 @@
import React, { useContext, useMemo, useState } from 'react';
import { trackEvent } from '../lib/analytics';
import { useAtomValue } from 'jotai';
import React from 'react';
import { dialogsAtom, hideDialog } from '../lib/dialog';
import { Dialog, type DialogProps } from './core/Dialog';
import { DialogContext } from './DialogContext';
type DialogEntry = {
export type DialogInstance = {
id: string;
render: ({ hide }: { hide: () => void }) => React.ReactNode;
} & Omit<DialogProps, 'open' | 'children'>;
export interface DialogState {
dialogs: DialogEntry[];
actions: Actions;
}
interface Actions {
show: (d: DialogEntry) => void;
toggle: (d: DialogEntry) => void;
hide: (id: string) => void;
}
export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
const [dialogs, setDialogs] = useState<DialogState['dialogs']>([]);
const actions = useMemo<Actions>(
() => ({
show({ id, ...props }: DialogEntry) {
trackEvent('dialog', 'show', { id });
setDialogs((a) => [...a.filter((d) => d.id !== id), { id, ...props }]);
},
toggle({ id, ...props }: DialogEntry) {
if (dialogs.some((d) => d.id === id)) this.hide(id);
else this.show({ id, ...props });
},
hide: (id: string) => {
setDialogs((a) => a.filter((d) => d.id !== id));
},
}),
[dialogs],
);
const state: DialogState = {
dialogs,
actions,
};
return <DialogContext.Provider value={state}>{children}</DialogContext.Provider>;
};
function DialogInstance({ id, render, onClose, ...props }: DialogEntry) {
const { actions } = useContext(DialogContext);
const children = render({ hide: () => actions.hide(id) });
return (
<Dialog
open
onClose={() => {
onClose?.();
actions.hide(id);
}}
{...props}
>
{children}
</Dialog>
);
}
export function Dialogs() {
const { dialogs } = useContext(DialogContext);
const dialogs = useAtomValue(dialogsAtom);
return (
<>
{dialogs.map((props: DialogEntry) => (
<DialogInstance key={props.id} {...props} />
{dialogs.map(({ render, onClose, id, ...props }: DialogInstance) => (
<Dialog
open
key={id}
onClose={() => {
onClose?.();
hideDialog(id);
}}
{...props}
>
{render({ hide: () => hideDialog(id) })}
</Dialog>
))}
</>
);

View File

@@ -2,12 +2,12 @@ import classNames from 'classnames';
import { memo, useCallback, useMemo } from 'react';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useEnvironments } from '../hooks/useEnvironments';
import { toggleDialog } from '../lib/dialog';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { useDialog } from '../hooks/useDialog';
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
type Props = {
@@ -20,17 +20,16 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
}: Props) {
const { subEnvironments, baseEnvironment } = useEnvironments();
const [activeEnvironment, setActiveEnvironmentId] = useActiveEnvironment();
const dialog = useDialog();
const showEnvironmentDialog = useCallback(() => {
dialog.toggle({
toggleDialog({
id: 'environment-editor',
noPadding: true,
size: 'lg',
className: 'h-[80vh]',
render: () => <EnvironmentEditDialog initialEnvironment={activeEnvironment} />,
});
}, [dialog, activeEnvironment]);
}, [activeEnvironment]);
const items: DropdownItem[] = useMemo(
() => [

View File

@@ -7,7 +7,7 @@ import {
useSubscribeActiveCookieJarId,
} from '../hooks/useActiveCookieJar';
import { useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { getActiveRequest, useActiveRequest } from '../hooks/useActiveRequest';
import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId';
import { useActiveWorkspace, useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
@@ -78,6 +78,7 @@ export function GlobalHooks() {
navigateAfter: true,
});
useHotKey('http_request.duplicate', async () => {
const activeRequest = getActiveRequest();
if (activeRequest?.model === 'http_request') {
await duplicateHttpRequest.mutateAsync();
} else {

View File

@@ -5,8 +5,8 @@ import type { EditorView } from 'codemirror';
import { formatSdl } from 'format-graphql';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useLocalStorage } from 'react-use';
import { useDialog } from '../hooks/useDialog';
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
import { showDialog } from '../lib/dialog';
import { tryFormatJson } from '../lib/formatters';
import { Button } from './core/Button';
import { Dropdown } from './core/Dropdown';
@@ -62,8 +62,6 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
updateSchema(editorViewRef.current, schema ?? undefined);
}, [schema]);
const dialog = useDialog();
const actions = useMemo<EditorProps['actions']>(
() => [
<div key="introspection" className="!opacity-100">
@@ -122,7 +120,7 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
color="danger"
isLoading={isLoading}
onClick={() => {
dialog.show({
showDialog({
title: 'Introspection Failed',
size: 'dynamic',
id: 'introspection-failed',
@@ -161,7 +159,6 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
clear,
schema,
setAutoIntrospectDisabled,
dialog,
],
);

View File

@@ -1,5 +1,6 @@
import { jsonLanguage } from '@codemirror/lang-json';
import { linter } from '@codemirror/lint';
import type { GrpcRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import {
@@ -10,19 +11,18 @@ import {
updateSchema,
} from 'codemirror-json-schema';
import { useEffect, useMemo, useRef } from 'react';
import { useAlert } from '../hooks/useAlert';
import type { ReflectResponseService } from '../hooks/useGrpc';
import { showAlert } from '../lib/alert';
import { showDialog } from '../lib/dialog';
import { tryFormatJson } from '../lib/formatters';
import type { GrpcRequest } from '@yaakapp-internal/models';
import { pluralizeCount } from '../lib/pluralize';
import { Button } from './core/Button';
import type { EditorProps } from './core/Editor/Editor';
import { Editor } from './core/Editor/Editor';
import { FormattedError } from './core/FormattedError';
import { InlineCode } from './core/InlineCode';
import { VStack } from './core/Stacks';
import { useDialog } from '../hooks/useDialog';
import { GrpcProtoSelection } from './GrpcProtoSelection';
import type { EditorProps} from './core/Editor/Editor';
import {Editor} from './core/Editor/Editor';
type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'className'> & {
services: ReflectResponseService[] | null;
@@ -42,9 +42,6 @@ export function GrpcEditor({
}: Props) {
const editorViewRef = useRef<EditorView>(null);
const alert = useAlert();
const dialog = useDialog();
// Find the schema for the selected service and method and update the editor
useEffect(() => {
if (
@@ -59,7 +56,7 @@ export function GrpcEditor({
const s = services.find((s) => s.name === request.service);
if (s == null) {
console.log('Failed to find service', { service: request.service, services });
alert({
showAlert({
id: 'grpc-find-service-error',
title: "Couldn't Find Service",
body: (
@@ -74,7 +71,7 @@ export function GrpcEditor({
const schema = s.methods.find((m) => m.name === request.method)?.schema;
if (request.method != null && schema == null) {
console.log('Failed to find method', { method: request.method, methods: s?.methods });
alert({
showAlert({
id: 'grpc-find-schema-error',
title: "Couldn't Find Method",
body: (
@@ -94,7 +91,7 @@ export function GrpcEditor({
try {
updateSchema(editorViewRef.current, JSON.parse(schema));
} catch (err) {
alert({
showAlert({
id: 'grpc-parse-schema-error',
title: 'Failed to Parse Schema',
body: (
@@ -108,7 +105,7 @@ export function GrpcEditor({
),
});
}
}, [alert, services, request.method, request.service]);
}, [services, request.method, request.service]);
const extraExtensions = useMemo(
() => [
@@ -143,7 +140,7 @@ export function GrpcEditor({
}
isLoading={reflectionLoading}
onClick={() => {
dialog.show({
showDialog({
title: 'Configure Schema',
size: 'md',
id: 'reflection-failed',
@@ -172,7 +169,6 @@ export function GrpcEditor({
</div>,
],
[
dialog,
protoFiles.length,
reflectionError,
reflectionLoading,

View File

@@ -1,11 +1,11 @@
import { useNavigate } from '@tanstack/react-router';
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import React, { useState } from 'react';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import {showToast} from "../lib/toast";
import { router } from '../lib/router';
import { showToast } from '../lib/toast';
import { Button } from './core/Button';
import { InlineCode } from './core/InlineCode';
import { Select } from './core/Select';
@@ -21,7 +21,6 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
const workspaces = useWorkspaces();
const updateHttpRequest = useUpdateAnyHttpRequest();
const updateGrpcRequest = useUpdateAnyGrpcRequest();
const navigate = useNavigate();
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>(activeWorkspaceId);
return (
@@ -69,7 +68,7 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
color="secondary"
className="mr-auto min-w-[5rem]"
onClick={async () => {
await navigate({
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: selectedWorkspaceId },
});

View File

@@ -1,6 +1,5 @@
import { useNavigate } from '@tanstack/react-router';
import classNames from 'classnames';
import { useCallback, useMemo, useRef } from 'react';
import { useMemo, useRef } from 'react';
import { useKeyPressEvent } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
@@ -10,6 +9,7 @@ import { httpRequestsAtom } from '../hooks/useHttpRequests';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { jotaiStore } from '../lib/jotai';
import { router } from '../lib/router';
import { Button } from './core/Button';
import type { DropdownItem, DropdownRef } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
@@ -24,7 +24,6 @@ export function RecentRequestsDropdown({ className }: Props) {
const dropdownRef = useRef<DropdownRef>(null);
const [allRecentRequestIds] = useRecentRequests();
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
const navigate = useNavigate();
// Handle key-up
useKeyPressEvent('Control', undefined, () => {
@@ -42,7 +41,7 @@ export function RecentRequestsDropdown({ className }: Props) {
dropdownRef.current?.prev?.();
});
const getItems = useCallback(() => {
const items = useMemo(() => {
const activeWorkspaceId = getActiveWorkspaceId();
if (activeWorkspaceId === null) return [];
@@ -58,13 +57,10 @@ export function RecentRequestsDropdown({ className }: Props) {
// leftSlot: <CountBadge className="!ml-0 px-0 w-5" count={recentRequestItems.length} />,
leftSlot: <HttpMethodTag className="text-right" shortNames request={request} />,
onSelect: async () => {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: {
requestId: request.id,
workspaceId: activeWorkspaceId,
},
search: (prev) => ({ ...prev }),
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: activeWorkspaceId },
search: (prev) => ({ ...prev, request_id: request.id }),
});
},
});
@@ -82,10 +78,10 @@ export function RecentRequestsDropdown({ className }: Props) {
}
return recentRequestItems.slice(0, 20);
}, [navigate, recentRequestIds]);
}, [recentRequestIds]);
return (
<Dropdown ref={dropdownRef} items={getItems}>
<Dropdown ref={dropdownRef} items={items}>
<Button
data-tauri-drag-region
size="sm"

View File

@@ -1,15 +1,14 @@
import { useNavigate } from '@tanstack/react-router';
import { useEffect } from 'react';
import { getRecentCookieJars } from '../hooks/useRecentCookieJars';
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
import { getRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { router } from '../lib/router';
export function RedirectToLatestWorkspace() {
const workspaces = useWorkspaces();
const recentWorkspaces = useRecentWorkspaces();
const navigate = useNavigate();
useEffect(() => {
if (workspaces.length === 0 || recentWorkspaces == null) {
@@ -22,23 +21,16 @@ export function RedirectToLatestWorkspace() {
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
const search = { cookie_jar_id: cookieJarId, environment_id: environmentId };
const params = { workspaceId };
const search = {
cookie_jar_id: cookieJarId,
environment_id: environmentId,
requestId: requestId,
};
if (workspaceId != null && requestId != null) {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: { workspaceId, requestId },
search,
});
} else {
await navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId },
search,
});
}
await router.navigate({ to: '/workspaces/$workspaceId', params, search });
})();
}, [navigate, recentWorkspaces, workspaces, workspaces.length]);
}, [recentWorkspaces, workspaces, workspaces.length]);
return <></>;
}

View File

@@ -2,11 +2,11 @@ import { openUrl } from '@tauri-apps/plugin-opener';
import { useRef } from 'react';
import { useAppInfo } from '../hooks/useAppInfo';
import { useCheckForUpdates } from '../hooks/useCheckForUpdates';
import { useDialog } from '../hooks/useDialog';
import { useExportData } from '../hooks/useExportData';
import { useImportData } from '../hooks/useImportData';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useOpenSettings } from '../hooks/useOpenSettings';
import { showDialog } from '../lib/dialog';
import type { DropdownRef } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
@@ -18,7 +18,6 @@ export function SettingsDropdown() {
const exportData = useExportData();
const appInfo = useAppInfo();
const dropdownRef = useRef<DropdownRef>(null);
const dialog = useDialog();
const checkForUpdates = useCheckForUpdates();
const openSettings = useOpenSettings();
@@ -41,7 +40,7 @@ export function SettingsDropdown() {
hotKeyAction: 'hotkeys.showHelp',
leftSlot: <Icon icon="keyboard" />,
onSelect: () => {
dialog.show({
showDialog({
id: 'hotkey',
title: 'Keyboard Shortcuts',
size: 'dynamic',

View File

@@ -1,5 +1,4 @@
import { useNavigate } from '@tanstack/react-router';
import type { AnyModel, Folder, GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import type { Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtom, useAtomValue } from 'jotai';
import React, { useCallback, useRef, useState } from 'react';
@@ -15,6 +14,7 @@ import { getSidebarCollapsedMap } from '../hooks/useSidebarItemCollapsed';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { router } from '../lib/router';
import { ContextMenu } from './core/Dropdown';
import { sidebarSelectedIdAtom, sidebarTreeAtom } from './SidebarAtoms';
import type { SidebarItemProps } from './SidebarItem';
@@ -24,10 +24,12 @@ interface Props {
className?: string;
}
export type SidebarModel = Folder | GrpcRequest | HttpRequest | Workspace;
export interface SidebarTreeNode {
id: string;
name: string;
model: AnyModel['model'];
model: SidebarModel['model'];
sortPriority?: number;
workspaceId?: string;
folderId?: string | null;
@@ -35,8 +37,6 @@ export interface SidebarTreeNode {
depth: number;
}
export type SidebarModel = Folder | GrpcRequest | HttpRequest;
export function Sidebar({ className }: Props) {
const [hidden, setHidden] = useSidebarHidden();
const sidebarRef = useRef<HTMLElement>(null);
@@ -52,7 +52,6 @@ export function Sidebar({ className }: Props) {
const [draggingId, setDraggingId] = useState<string | null>(null);
const [hoveredTree, setHoveredTree] = useState<SidebarTreeNode | null>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const navigate = useNavigate();
const { tree, treeParentMap, selectableRequests } = useAtomValue(sidebarTreeAtom);
@@ -95,14 +94,13 @@ export function Sidebar({ className }: Props) {
return;
}
if (node.model === 'http_request' || node.model === 'grpc_request') {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: {
requestId: id,
workspaceId: node.workspaceId ?? 'n/a',
},
search: (prev) => ({ ...prev }),
// NOTE: I'm not sure why, but TS thinks workspaceId is (string | undefined) here
if ((node.model === 'http_request' || node.model === 'grpc_request') && node.workspaceId) {
const workspaceId = node.workspaceId;
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId },
search: (prev) => ({ ...prev, request_id: node.id }),
});
setHasFocus(true);
@@ -110,7 +108,7 @@ export function Sidebar({ className }: Props) {
setSelectedTree(tree);
}
},
[treeParentMap, navigate, setSelectedId],
[treeParentMap, setSelectedId],
);
const handleClearSelected = useCallback(() => {
@@ -153,13 +151,10 @@ export function Sidebar({ className }: Props) {
}
e.preventDefault();
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: {
requestId: selected.id,
workspaceId: activeWorkspace?.id ?? null,
},
search: (prev) => ({ ...prev }),
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: activeWorkspace?.id ?? null },
search: (prev) => ({ ...prev, request_id: selected.id }),
});
});

View File

@@ -1,8 +1,7 @@
import React, { useCallback } from 'react';
import React, { useMemo } from 'react';
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDialog } from '../hooks/useDialog';
import { useDuplicateFolder } from '../hooks/useDuplicateFolder';
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
@@ -12,6 +11,8 @@ import { useRenameRequest } from '../hooks/useRenameRequest';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSendManyRequests } from '../hooks/useSendManyRequests';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { showDialog } from '../lib/dialog';
import { getHttpRequest } from '../lib/store';
import type { DropdownItem } from './core/Dropdown';
import { ContextMenu } from './core/Dropdown';
@@ -32,7 +33,6 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
const httpRequestActions = useHttpRequestActions();
const sendRequest = useSendAnyHttpRequest();
const workspaces = useWorkspaces();
const dialog = useDialog();
const deleteRequest = useDeleteRequest(child.id);
const renameRequest = useRenameRequest(child.id);
const duplicateHttpRequest = useDuplicateHttpRequest({ id: child.id, navigateAfter: true });
@@ -42,7 +42,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
folderId: child.model === 'folder' ? child.id : null,
});
const items = useCallback((): DropdownItem[] => {
const items = useMemo((): DropdownItem[] => {
if (child.model === 'folder') {
return [
{
@@ -56,7 +56,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
label: 'Settings',
leftSlot: <Icon icon="settings" />,
onSelect: () =>
dialog.show({
showDialog({
id: 'folder-settings',
title: 'Folder Settings',
size: 'md',
@@ -77,7 +77,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
onSelect: () => deleteFolder.mutate(),
},
{ type: 'separator' },
...createDropdownItems(),
...createDropdownItems,
];
} else {
const requestItems: DropdownItem[] =
@@ -146,7 +146,6 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
createDropdownItems,
deleteFolder,
deleteRequest,
dialog,
duplicateFolder,
duplicateGrpcRequest,
duplicateHttpRequest,

View File

@@ -5,41 +5,30 @@ import { hideToast, toastsAtom } from '../lib/toast';
import { Toast, type ToastProps } from './core/Toast';
import { Portal } from './Portal';
export type ToastEntry = {
id?: string;
export type ToastInstance = {
id: string;
message: ReactNode;
timeout?: 3000 | 5000 | 8000 | null;
timeout: 3000 | 5000 | 8000 | null;
onClose?: ToastProps['onClose'];
} & Omit<ToastProps, 'onClose' | 'open' | 'children' | 'timeout'>;
export type PrivateToastEntry = ToastEntry & {
id: string;
timeout: number | null;
};
function ToastInstance({ id, message, timeout, ...props }: PrivateToastEntry) {
return (
<Toast
open
timeout={timeout}
{...props}
// We call onClose inside actions.hide instead of passing to toast so that
// it gets called from external close calls as well
onClose={() => hideToast(id)}
>
{message}
</Toast>
);
}
export const Toasts = () => {
const toasts = useAtomValue(toastsAtom);
return (
<Portal name="toasts">
<div className="absolute right-0 bottom-0 z-20">
<AnimatePresence>
{toasts.map((props: PrivateToastEntry) => (
<ToastInstance key={props.id} {...props} />
{toasts.map(({ message, ...props }: ToastInstance) => (
<Toast
key={props.id}
open
{...props}
// We call onClose inside actions.hide instead of passing to toast so that
// it gets called from external close calls as well
onClose={() => hideToast(props.id)}
>
{message}
</Toast>
))}
</AnimatePresence>
</div>

View File

@@ -3,11 +3,11 @@ import { memo, useCallback, useMemo } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDeleteSendHistory } from '../hooks/useDeleteSendHistory';
import { useDialog } from '../hooks/useDialog';
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
import { useSettings } from '../hooks/useSettings';
import { useSyncWorkspace } from '../hooks/useSyncWorkspace';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { showDialog } from '../lib/dialog';
import { getWorkspace } from '../lib/store';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
@@ -28,7 +28,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const activeWorkspace = useActiveWorkspace();
const createWorkspace = useCreateWorkspace();
const { mutate: deleteSendHistory } = useDeleteSendHistory();
const dialog = useDialog();
const settings = useSettings();
const openWorkspace = useOpenWorkspace();
const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null;
@@ -57,7 +56,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
leftSlot: <Icon icon="settings" />,
hotKeyAction: 'workspace_settings.show',
onSelect: async () => {
dialog.show({
showDialog({
id: 'workspace-settings',
title: 'Workspace Settings',
size: 'md',
@@ -97,7 +96,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
sync,
deleteSendHistory,
createWorkspace,
dialog,
]);
const handleChange = useCallback(
@@ -112,14 +110,14 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const workspace = await getWorkspace(workspaceId);
if (workspace == null) return;
dialog.show({
showDialog({
id: 'open-workspace',
size: 'sm',
title: 'Open Workspace',
render: ({ hide }) => <OpenWorkspaceDialog workspace={workspace} hide={hide} />,
});
},
[dialog, openWorkspace, openWorkspaceNewWindow],
[openWorkspace, openWorkspaceNewWindow],
);
return (

View File

@@ -57,7 +57,7 @@ export type DropdownItem = DropdownItemDefault | DropdownItemSeparator;
export interface DropdownProps {
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
items: DropdownItem[] | (() => DropdownItem[]);
items: DropdownItem[];
onOpen?: () => void;
onClose?: () => void;
fullWidth?: boolean;
@@ -75,7 +75,7 @@ export interface DropdownRef {
}
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
{ children, items: itemsGetter, onOpen, onClose, hotKeyAction, fullWidth }: DropdownProps,
{ children, items, onOpen, onClose, hotKeyAction, fullWidth }: DropdownProps,
ref,
) {
const [isOpen, _setIsOpen] = useState<boolean>(false);
@@ -83,8 +83,6 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
const buttonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
const [items, setItems] = useState<DropdownItem[]>([]);
const setIsOpen = useCallback(
(o: SetStateAction<boolean>) => {
_setIsOpen(o);
@@ -103,8 +101,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
const openDropdown = useCallback(() => {
setIsOpen((o) => !o);
setItems(typeof itemsGetter === 'function' ? itemsGetter() : itemsGetter);
}, [itemsGetter, setIsOpen]);
}, [setIsOpen]);
useImperativeHandle(
ref,
@@ -205,7 +202,7 @@ export const ContextMenu = forwardRef<DropdownRef, ContextMenuProps>(function Co
isOpen={true} // Always open because we return null if not
className={className}
ref={ref}
items={typeof items === 'function' ? items() : items}
items={items}
onClose={onClose}
triggerShape={triggerShape}
/>
@@ -417,11 +414,9 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
[filteredItems, setSelectedIndex],
);
if (items.length === 0) return null;
return (
<>
{filteredItems.map(
{items.map(
(item) =>
item.type !== 'separator' &&
!item.hotKeyLabelOnly && (

View File

@@ -20,7 +20,6 @@ import {
useRef,
} from 'react';
import { useActiveEnvironmentVariables } from '../../../hooks/useActiveEnvironmentVariables';
import { useDialog } from '../../../hooks/useDialog';
import { parseTemplate } from '../../../hooks/useParseTemplate';
import { useRequestEditor } from '../../../hooks/useRequestEditor';
import { useSettings } from '../../../hooks/useSettings';
@@ -28,6 +27,7 @@ import {
useTemplateFunctions,
useTwigCompletionOptions,
} from '../../../hooks/useTemplateFunctions';
import { showDialog } from '../../../lib/dialog';
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
import { IconButton } from '../IconButton';
@@ -195,11 +195,10 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
[wrapLines],
);
const dialog = useDialog();
const onClickFunction = useCallback(
async (fn: TemplateFunction, tagValue: string, startPos: number) => {
const initialTokens = await parseTemplate(tagValue);
dialog.show({
showDialog({
id: 'template-function',
size: 'sm',
title: 'Configure Function',
@@ -218,13 +217,13 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
),
});
},
[dialog],
[],
);
const onClickVariable = useCallback(
async (_v: EnvironmentVariable, tagValue: string, startPos: number) => {
const initialTokens = await parseTemplate(tagValue);
dialog.show({
showDialog({
size: 'dynamic',
id: 'template-variable',
title: 'Change Variable',
@@ -241,13 +240,13 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
),
});
},
[dialog],
[],
);
const onClickMissingVariable = useCallback(
async (_name: string, tagValue: string, startPos: number) => {
const initialTokens = await parseTemplate(tagValue);
dialog.show({
showDialog({
size: 'dynamic',
id: 'template-variable',
title: 'Configure Variable',
@@ -264,7 +263,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
),
});
},
[dialog],
[],
);
const [, { focusParamValue }] = useRequestEditor();

View File

@@ -338,7 +338,7 @@ function PairEditorRow({
const handleFocus = useCallback(() => onFocus?.(pair), [onFocus, pair]);
const handleDelete = useCallback(() => onDelete?.(pair, false), [onDelete, pair]);
const getDeleteItems = useCallback(
const deleteItems = useMemo(
(): DropdownItem[] => [
{
key: 'delete',
@@ -525,7 +525,7 @@ function PairEditorRow({
onDelete={handleDelete}
/>
) : (
<Dropdown items={getDeleteItems}>
<Dropdown items={deleteItems}>
<IconButton
iconSize="sm"
size="xs"

View File

@@ -1,8 +1,9 @@
import { useNavigate, useSearch } from '@tanstack/react-router';
import { useSearch } from '@tanstack/react-router';
import type { CookieJar } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai/index';
import { useCallback, useEffect } from 'react';
import { useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';
import { router } from '../lib/router';
import { cookieJarsAtom, useCookieJars } from './useCookieJars';
export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id';
@@ -14,33 +15,15 @@ export const activeCookieJarAtom = atom<CookieJar | null>((get) => {
return get(cookieJarsAtom)?.find((e) => e.id === activeId) ?? null;
});
export function useActiveCookieJar() {
const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
const setId = useCallback(
(id: string) =>
navigate({
search: (prev) => ({ ...prev, cookie_jar_id: id }),
}),
[navigate],
);
const cookieJar = useAtomValue(activeCookieJarAtom);
return [cookieJar, setId] as const;
export function setActiveCookieJar(cookieJar: CookieJar) {
router.navigate({
from: '/workspaces/$workspaceId',
search: (prev) => ({ ...prev, cookie_jar_id: cookieJar.id }),
});
}
function useActiveCookieJarId() {
// NOTE: This query param is accessed from Rust side, so do not change
const { cookie_jar_id: id } = useSearch({ strict: false });
const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
const setId = useCallback(
(id: string) =>
navigate({
search: (prev) => ({ ...prev, cookie_jar_id: id }),
}),
[navigate],
);
return [id, setId] as const;
export function useActiveCookieJar() {
return useAtomValue(activeCookieJarAtom);
}
export function useSubscribeActiveCookieJarId() {
@@ -56,12 +39,12 @@ export function getActiveCookieJar() {
export function useEnsureActiveCookieJar() {
const cookieJars = useCookieJars();
const [activeCookieJarId, setActiveCookieJarId] = useActiveCookieJarId();
const activeCookieJar = useActiveCookieJar();
// Set the active cookie jar to the first one, if none set
useEffect(() => {
if (cookieJars == null) return; // Hasn't loaded yet
if (cookieJars.find((j) => j.id === activeCookieJarId)) {
if (cookieJars.find((j) => j.id === activeCookieJar?.id)) {
return; // There's an active jar
}
@@ -73,6 +56,6 @@ export function useEnsureActiveCookieJar() {
// There's no active jar, so set it to the first one
console.log('Setting active cookie jar to', firstJar.id);
setActiveCookieJarId(firstJar.id).catch(console.error);
}, [activeCookieJarId, cookieJars, setActiveCookieJarId]);
setActiveCookieJar(firstJar);
}, [activeCookieJar?.id, cookieJars]);
}

View File

@@ -1,9 +1,10 @@
import { useNavigate, useSearch } from '@tanstack/react-router';
import { useSearch } from '@tanstack/react-router';
import type { Environment } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import { useCallback, useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';
import { router } from '../lib/router';
import { environmentsAtom } from './useEnvironments';
export const QUERY_ENVIRONMENT_ID = 'environment_id';
@@ -16,13 +17,13 @@ export const activeEnvironmentAtom = atom<Environment | null>((get) => {
});
export function useActiveEnvironment() {
const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
const setId = useCallback(
(id: string | null) =>
navigate({
router.navigate({
from: '/workspaces/$workspaceId',
search: (prev) => ({ ...prev, environment_id: id }),
}),
[navigate],
[],
);
const environment = useAtomValue(activeEnvironmentAtom);
return [environment, setId] as const;

View File

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

View File

@@ -1,28 +0,0 @@
import { useCallback } from 'react';
import type { DialogProps } from '../components/core/Dialog';
import type { AlertProps } from './Alert';
import { Alert } from './Alert';
import {useDialog} from "./useDialog";
interface AlertArg {
id: string;
title: DialogProps['title'];
body: AlertProps['body'];
size?: DialogProps['size'];
}
export function useAlert() {
const dialog = useDialog();
return useCallback<(a: AlertArg) => void>(
({ id, title, body, size = 'sm' }: AlertArg) =>
dialog.show({
id,
title,
hideX: true,
size,
render: ({ hide }) => Alert({ onHide: hide, body }),
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
}

View File

@@ -1,12 +1,11 @@
import { useMutation } from '@tanstack/react-query';
import { InlineCode } from '../components/core/InlineCode';
import { showAlert } from '../lib/alert';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import { invokeCmd } from '../lib/tauri';
import { useAlert } from './useAlert';
import { useAppInfo } from './useAppInfo';
export function useCheckForUpdates() {
const alert = useAlert();
const appInfo = useAppInfo();
return useMutation({
@@ -14,7 +13,7 @@ export function useCheckForUpdates() {
mutationFn: async () => {
const hasUpdate: boolean = await minPromiseMillis(invokeCmd('cmd_check_for_updates'), 500);
if (!hasUpdate) {
alert({
showAlert({
id: 'no-updates',
title: 'No Update Available',
body: (

View File

@@ -1,25 +1,19 @@
import { useNavigate } from '@tanstack/react-router';
import type { Folder, Workspace } from '@yaakapp-internal/models';
import { useMemo } from 'react';
import { trackEvent } from '../lib/analytics';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { createFastMutation } from './useFastMutation';
import { usePrompt } from './usePrompt';
function makeCommands({
navigate,
prompt,
}: {
navigate: ReturnType<typeof useNavigate>;
prompt: ReturnType<typeof usePrompt>;
}) {
function makeCommands({ prompt }: { prompt: ReturnType<typeof usePrompt> }) {
return {
createWorkspace: createFastMutation<Workspace, void, Partial<Workspace>>({
mutationKey: ['create_workspace'],
mutationFn: (patch) => invokeCmd<Workspace>('cmd_update_workspace', { workspace: patch }),
onSuccess: async (workspace) => {
await navigate({
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: workspace.id },
});
@@ -62,7 +56,6 @@ function makeCommands({
}
export function useCommands() {
const navigate = useNavigate();
const prompt = usePrompt();
return useMemo(() => makeCommands({ navigate, prompt }), [navigate, prompt]);
return useMemo(() => makeCommands({ prompt }), [prompt]);
}

View File

@@ -1,11 +1,10 @@
import { useCallback } from 'react';
import type { DialogProps } from '../components/core/Dialog';
import { showDialog } from '../lib/dialog';
import type { ConfirmProps } from './Confirm';
import { Confirm } from './Confirm';
import { useDialog } from './useDialog';
export function useConfirm() {
const dialog = useDialog();
return useCallback(
({
id,
@@ -21,7 +20,7 @@ export function useConfirm() {
confirmText?: ConfirmProps['confirmText'];
}) =>
new Promise((onResult: ConfirmProps['onResult']) => {
dialog.show({
showDialog({
id,
title,
description,
@@ -30,6 +29,6 @@ export function useConfirm() {
render: ({ hide }) => Confirm({ onHide: hide, variant, onResult, confirmText }),
});
}),
[dialog],
[],
);
}

View File

@@ -1,10 +1,10 @@
import { useCallback } from 'react';
import { useMemo } from 'react';
import type { DropdownItem } from '../components/core/Dropdown';
import { Icon } from '../components/core/Icon';
import { generateId } from '../lib/generateId';
import { BODY_TYPE_GRAPHQL } from '../lib/model_util';
import { getActiveRequest } from './useActiveRequest';
import {useCommands} from "./useCommands";
import { useCommands } from './useCommands';
import { useCreateGrpcRequest } from './useCreateGrpcRequest';
import { useCreateHttpRequest } from './useCreateHttpRequest';
@@ -16,12 +16,12 @@ export function useCreateDropdownItems({
hideFolder?: boolean;
hideIcons?: boolean;
folderId?: string | null | 'active-folder';
} = {}): () => DropdownItem[] {
} = {}): DropdownItem[] {
const { mutate: createHttpRequest } = useCreateHttpRequest();
const { mutate: createGrpcRequest } = useCreateGrpcRequest();
const { createFolder } = useCommands();
return useCallback((): DropdownItem[] => {
return useMemo((): DropdownItem[] => {
const folderId =
folderIdOption === 'active-folder' ? getActiveRequest()?.folderId : folderIdOption;

View File

@@ -1,4 +1,3 @@
import { useNavigate } from '@tanstack/react-router';
import type { GrpcRequest } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics';
import { jotaiStore } from '../lib/jotai';
@@ -6,10 +5,9 @@ import { invokeCmd } from '../lib/tauri';
import { getActiveRequest } from './useActiveRequest';
import { activeWorkspaceAtom } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
import { router } from '../lib/router';
export function useCreateGrpcRequest() {
const navigate = useNavigate();
return useFastMutation<
GrpcRequest,
unknown,
@@ -40,13 +38,10 @@ export function useCreateGrpcRequest() {
},
onSettled: () => trackEvent('grpc_request', 'create'),
onSuccess: async (request) => {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: {
workspaceId: request.workspaceId,
requestId: request.id,
},
search: (prev) => ({ ...prev }),
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: request.workspaceId },
search: (prev) => ({ ...prev, request_id: request.id }),
});
},
});

View File

@@ -1,14 +1,12 @@
import { useNavigate } from '@tanstack/react-router';
import type { HttpRequest } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { getActiveRequest } from './useActiveRequest';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
export function useCreateHttpRequest() {
const navigate = useNavigate();
return useFastMutation<HttpRequest, unknown, Partial<HttpRequest>>({
mutationKey: ['create_http_request'],
mutationFn: async (patch = {}) => {
@@ -34,10 +32,10 @@ export function useCreateHttpRequest() {
},
onSettled: () => trackEvent('http_request', 'create'),
onSuccess: async (request) => {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: { workspaceId: request.workspaceId, requestId: request.id },
search: (prev) => ({ ...prev }),
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: request.workspaceId },
search: (prev) => ({ ...prev, request_id: request.id }),
});
},
});

View File

@@ -1,16 +1,14 @@
import { useCallback } from 'react';
import { CreateWorkspaceDialog } from '../components/CreateWorkspaceDialog';
import { useDialog } from './useDialog';
import { showDialog } from '../lib/dialog';
export function useCreateWorkspace() {
const dialog = useDialog();
return useCallback(() => {
dialog.show({
showDialog({
id: 'create-workspace',
title: 'Create Workspace',
size: 'md',
render: ({ hide }) => <CreateWorkspaceDialog hide={hide} />,
});
}, [dialog]);
}, []);
}

View File

@@ -1,7 +1,7 @@
import { useNavigate } from '@tanstack/react-router';
import type { Workspace } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspace } from './useActiveWorkspace';
import { useConfirm } from './useConfirm';
@@ -9,7 +9,6 @@ import { useFastMutation } from './useFastMutation';
export function useDeleteActiveWorkspace() {
const confirm = useConfirm();
const navigate = useNavigate();
return useFastMutation<Workspace | null, string>({
mutationKey: ['delete_workspace'],
@@ -31,7 +30,7 @@ export function useDeleteActiveWorkspace() {
onSettled: () => trackEvent('workspace', 'delete'),
onSuccess: async (workspace) => {
if (workspace === null) return;
await navigate({ to: '/workspaces' });
await router.navigate({ to: '/workspaces' });
},
});
}

View File

@@ -1,8 +1,8 @@
import { useSetAtom } from 'jotai/index';
import { showAlert } from '../lib/alert';
import { pluralizeCount } from '../lib/pluralize';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { useAlert } from './useAlert';
import { useConfirm } from './useConfirm';
import { useFastMutation } from './useFastMutation';
import { useGrpcConnections } from './useGrpcConnections';
@@ -10,7 +10,6 @@ import { httpResponsesAtom, useHttpResponses } from './useHttpResponses';
export function useDeleteSendHistory() {
const confirm = useConfirm();
const alert = useAlert();
const setHttpResponses = useSetAtom(httpResponsesAtom);
const httpResponses = useHttpResponses();
const grpcConnections = useGrpcConnections();
@@ -23,7 +22,7 @@ export function useDeleteSendHistory() {
mutationKey: ['delete_send_history'],
mutationFn: async () => {
if (labels.length === 0) {
alert({
showAlert({
id: 'no-responses',
title: 'Nothing to Delete',
body: 'There are no Http Response or Grpc Connections to delete',

View File

@@ -1,6 +0,0 @@
import { useContext } from 'react';
import { DialogContext } from '../components/DialogContext';
export function useDialog() {
return useContext(DialogContext).actions;
}

View File

@@ -1,6 +1,6 @@
import { useNavigate } from '@tanstack/react-router';
import type { GrpcRequest } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
import { getGrpcProtoFiles, setGrpcProtoFiles } from './useGrpcProtoFiles';
@@ -12,7 +12,6 @@ export function useDuplicateGrpcRequest({
id: string | null;
navigateAfter: boolean;
}) {
const navigate = useNavigate();
return useFastMutation<GrpcRequest, string>({
mutationKey: ['duplicate_grpc_request', id],
mutationFn: async () => {
@@ -28,10 +27,10 @@ export function useDuplicateGrpcRequest({
await setGrpcProtoFiles(request.id, protoFiles);
if (navigateAfter) {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: { workspaceId: request.workspaceId, requestId: request.id },
search: (prev) => ({ ...prev }),
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: request.workspaceId },
search: (prev) => ({ ...prev, request_id: request.id }),
});
}
},

View File

@@ -1,6 +1,6 @@
import { useNavigate } from '@tanstack/react-router';
import type { HttpRequest } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
@@ -11,7 +11,6 @@ export function useDuplicateHttpRequest({
id: string | null;
navigateAfter: boolean;
}) {
const navigate = useNavigate();
return useFastMutation<HttpRequest, string>({
mutationKey: ['duplicate_http_request', id],
mutationFn: async () => {
@@ -21,13 +20,10 @@ export function useDuplicateHttpRequest({
onSettled: () => trackEvent('http_request', 'duplicate'),
onSuccess: async (request) => {
if (navigateAfter) {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: {
workspaceId: request.workspaceId,
requestId: request.id,
},
search: (prev) => ({ ...prev }),
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: request.workspaceId },
search: (prev) => ({ ...prev, request_id: request.id }),
});
}
},

View File

@@ -1,20 +1,17 @@
import { ExportDataDialog } from '../components/ExportDataDialog';
import { showAlert } from '../lib/alert';
import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai';
import { getActiveWorkspace } from './useActiveWorkspace';
import { useAlert } from './useAlert';
import { useDialog } from './useDialog';
import { useFastMutation } from './useFastMutation';
import { showToast } from '../lib/toast';
import { getActiveWorkspace } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
import { workspacesAtom } from './useWorkspaces';
export function useExportData() {
const alert = useAlert();
const dialog = useDialog();
return useFastMutation({
mutationKey: ['export_data'],
onError: (err: string) => {
alert({ id: 'export-failed', title: 'Export Failed', body: err });
showAlert({ id: 'export-failed', title: 'Export Failed', body: err });
},
mutationFn: async () => {
const activeWorkspace = getActiveWorkspace();
@@ -22,7 +19,7 @@ export function useExportData() {
if (activeWorkspace == null || workspaces.length === 0) return;
dialog.show({
showDialog({
id: 'export-data',
title: 'Export App Data',
size: 'md',

View File

@@ -1,4 +1,3 @@
import { useNavigate } from '@tanstack/react-router';
import type {
Environment,
Folder,
@@ -10,18 +9,15 @@ import { Button } from '../components/core/Button';
import { FormattedError } from '../components/core/FormattedError';
import { VStack } from '../components/core/Stacks';
import { ImportDataDialog } from '../components/ImportDataDialog';
import { showAlert } from '../lib/alert';
import { showDialog } from '../lib/dialog';
import { pluralizeCount } from '../lib/pluralize';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspace } from './useActiveWorkspace';
import { useAlert } from './useAlert';
import { useDialog } from './useDialog';
import { useFastMutation } from './useFastMutation';
export function useImportData() {
const dialog = useDialog();
const alert = useAlert();
const navigate = useNavigate();
const importData = async (filePath: string): Promise<boolean> => {
const activeWorkspace = getActiveWorkspace();
const imported: {
@@ -37,7 +33,7 @@ export function useImportData() {
const importedWorkspace = imported.workspaces[0];
dialog.show({
showDialog({
id: 'import-complete',
title: 'Import Complete',
size: 'sm',
@@ -65,7 +61,7 @@ export function useImportData() {
if (importedWorkspace != null) {
const environmentId = imported.environments[0]?.id ?? null;
await navigate({
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: importedWorkspace.id },
search: { environment_id: environmentId },
@@ -78,7 +74,7 @@ export function useImportData() {
return useFastMutation({
mutationKey: ['import_data'],
onError: (err: string) => {
alert({
showAlert({
id: 'import-failed',
title: 'Import Failed',
size: 'md',
@@ -87,7 +83,7 @@ export function useImportData() {
},
mutationFn: async () => {
return new Promise<void>((resolve, reject) => {
dialog.show({
showDialog({
id: 'import',
title: 'Import Data',
size: 'sm',

View File

@@ -1,22 +1,21 @@
import React from 'react';
import { MoveToWorkspaceDialog } from '../components/MoveToWorkspaceDialog';
import { showDialog } from '../lib/dialog';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { useDialog } from './useDialog';
import { useFastMutation } from './useFastMutation';
import { useRequests } from './useRequests';
import { getRequests } from './useRequests';
export function useMoveToWorkspace(id: string) {
const dialog = useDialog();
const requests = useRequests();
const request = requests.find((r) => r.id === id);
return useFastMutation<void, unknown>({
mutationKey: ['move_workspace', id],
mutationFn: async () => {
const activeWorkspaceId = getActiveWorkspaceId();
if (request == null || activeWorkspaceId == null) return;
if (activeWorkspaceId == null) return;
dialog.show({
const request = getRequests().find((r) => r.id === id);
if (request == null) return;
showDialog({
id: 'change-workspace',
title: 'Move Workspace',
size: 'sm',

View File

@@ -1,4 +1,4 @@
import { useNavigate, useRouter } from '@tanstack/react-router';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
import { getRecentCookieJars } from './useRecentCookieJars';
@@ -6,9 +6,6 @@ import { getRecentEnvironments } from './useRecentEnvironments';
import { getRecentRequests } from './useRecentRequests';
export function useOpenWorkspace() {
const router = useRouter();
const navigate = useNavigate();
return useFastMutation({
mutationKey: ['open_workspace'],
mutationFn: async ({
@@ -21,7 +18,11 @@ export function useOpenWorkspace() {
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? undefined;
const requestId = (await getRecentRequests(workspaceId))[0] ?? undefined;
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? undefined;
const search = { environment_id: environmentId, cookie_jar_id: cookieJarId };
const search = {
environment_id: environmentId,
cookie_jar_id: cookieJarId,
request_id: requestId,
};
if (inNewWindow) {
const location = router.buildLocation({
@@ -33,15 +34,11 @@ export function useOpenWorkspace() {
return;
}
if (requestId != null) {
await navigate({
to: '/workspaces/$workspaceId/requests/$requestId',
params: { workspaceId, requestId },
search,
});
} else {
await navigate({ to: '/workspaces/$workspaceId', params: { workspaceId }, search });
}
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId },
search,
});
},
});
}

View File

@@ -1,16 +1,15 @@
import type { DialogProps } from '../components/core/Dialog';
import { showDialog } from '../lib/dialog';
import type { PromptProps } from './Prompt';
import { Prompt } from './Prompt';
import {useDialog} from "./useDialog";
type Props = Pick<DialogProps, 'title' | 'description'> &
Omit<PromptProps, 'onClose' | 'onCancel' | 'onResult'> & { id: string };
export function usePrompt() {
const dialog = useDialog();
return ({ id, title, description, ...props }: Props) =>
new Promise((resolve: PromptProps['onResult']) => {
dialog.show({
showDialog({
id,
title,
description,

View File

@@ -1,9 +1,14 @@
import { useMemo } from 'react';
import { useGrpcRequests } from './useGrpcRequests';
import { useHttpRequests } from './useHttpRequests';
import { atom, useAtomValue } from 'jotai';
import {jotaiStore} from "../lib/jotai";
import { grpcRequestsAtom } from './useGrpcRequests';
import { httpRequestsAtom } from './useHttpRequests';
const requestsAtom = atom((get) => [...get(httpRequestsAtom), ...get(grpcRequestsAtom)]);
export function useRequests() {
const httpRequests = useHttpRequests();
const grpcRequests = useGrpcRequests();
return useMemo(() => [...httpRequests, ...grpcRequests], [httpRequests, grpcRequests]);
return useAtomValue(requestsAtom);
}
export function getRequests() {
return jotaiStore.get(requestsAtom);
}

View File

@@ -1,14 +1,13 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import { showAlert } from '../lib/alert';
import { trackEvent } from '../lib/analytics';
import { getHttpRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { getActiveCookieJar } from './useActiveCookieJar';
import { getActiveEnvironment } from './useActiveEnvironment';
import { useAlert } from './useAlert';
import { useFastMutation } from './useFastMutation';
export function useSendAnyHttpRequest() {
const alert = useAlert();
return useFastMutation<HttpResponse | null, string, string | null>({
mutationKey: ['send_any_request'],
mutationFn: async (id) => {
@@ -24,6 +23,6 @@ export function useSendAnyHttpRequest() {
});
},
onSettled: () => trackEvent('http_request', 'send'),
onError: (err) => alert({ id: 'send-failed', title: 'Send Failed', body: err }),
onError: (err) => showAlert({ id: 'send-failed', title: 'Send Failed', body: err }),
});
}

View File

@@ -1,11 +1,10 @@
import { useCallback } from 'react';
import { CommandPaletteDialog } from '../components/CommandPaletteDialog';
import { useDialog } from './useDialog';
import { toggleDialog } from '../lib/dialog';
export function useToggleCommandPalette() {
const dialog = useDialog();
const togglePalette = useCallback(() => {
dialog.toggle({
toggleDialog({
id: 'command_palette',
size: 'dynamic',
hideX: true,
@@ -15,7 +14,7 @@ export function useToggleCommandPalette() {
noScroll: true,
render: ({ hide }) => <CommandPaletteDialog onClose={hide} />,
});
}, [dialog]);
}, []);
return togglePalette;
}

21
src-web/lib/alert.ts Normal file
View File

@@ -0,0 +1,21 @@
import type { DialogProps } from '../components/core/Dialog';
import type { AlertProps } from '../hooks/Alert';
import { Alert } from '../hooks/Alert';
import { showDialog } from './dialog';
interface AlertArgs {
id: string;
title: DialogProps['title'];
body: AlertProps['body'];
size?: DialogProps['size'];
}
export function showAlert({ id, title, body, size = 'sm' }: AlertArgs) {
showDialog({
id,
title,
hideX: true,
size,
render: ({ hide }) => Alert({ onHide: hide, body }),
});
}

21
src-web/lib/dialog.ts Normal file
View File

@@ -0,0 +1,21 @@
import { atom } from 'jotai/index';
import type { DialogInstance } from '../components/Dialogs';
import { trackEvent } from './analytics';
import { jotaiStore } from './jotai';
export const dialogsAtom = atom<DialogInstance[]>([]);
export function showDialog({ id, ...props }: DialogInstance) {
trackEvent('dialog', 'show', { id });
jotaiStore.set(dialogsAtom, (a) => [...a.filter((d) => d.id !== id), { id, ...props }]);
}
export function toggleDialog({ id, ...props }: DialogInstance) {
const dialogs = jotaiStore.get(dialogsAtom);
if (dialogs.some((d) => d.id === id)) hideDialog(id);
else showDialog({ id, ...props });
}
export function hideDialog(id: string) {
jotaiStore.set(dialogsAtom, (a) => a.filter((d) => d.id !== id));
}

View File

@@ -1,11 +1,18 @@
import { atom } from 'jotai/index';
import type { PrivateToastEntry, ToastEntry } from '../components/Toasts';
import type { ToastInstance } from '../components/Toasts';
import { generateId } from './generateId';
import { jotaiStore } from './jotai';
export const toastsAtom = atom<PrivateToastEntry[]>([]);
export const toastsAtom = atom<ToastInstance[]>([]);
export function showToast({ id, timeout = 5000, ...props }: ToastEntry) {
export function showToast({
id,
timeout = 5000,
...props
}: Omit<ToastInstance, 'id' | 'timeout'> & {
id?: ToastInstance['id'];
timeout?: ToastInstance['timeout'];
}) {
id = id ?? generateId();
if (timeout != null) {
setTimeout(() => hideToast(id), timeout);

View File

@@ -21,8 +21,8 @@
"@lezer/lr": "^1.3.3",
"@react-hook/size": "^2.1.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.62.8",
"@tanstack/react-router": "^1.91.3",
"@tanstack/react-query": "^5.62.16",
"@tanstack/react-router": "^1.95.1",
"@tanstack/react-virtual": "^3.11.2",
"@tauri-apps/api": "^2.0.1",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",

View File

@@ -7,7 +7,7 @@ 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/Dialogs';
import { Dialogs } from '../components/Dialogs';
import { GlobalHooks } from '../components/GlobalHooks';
import RouteError from '../components/RouteError';
import { Toasts } from '../components/Toasts';
@@ -65,19 +65,17 @@ function RouteComponent() {
<HelmetProvider>
<DndProvider backend={HTML5Backend}>
<Suspense>
<DialogProvider>
<GlobalHooks />
<Toasts />
<Dialogs />
<div
className={classNames(
'w-full h-full',
osInfo?.osType === 'linux' && 'border border-border-subtle',
)}
>
<Outlet />
</div>
</DialogProvider>
<GlobalHooks />
<Toasts />
<Dialogs />
<div
className={classNames(
'w-full h-full',
osInfo?.osType === 'linux' && 'border border-border-subtle',
)}
>
<Outlet />
</div>
</Suspense>
</DndProvider>
</HelmetProvider>

View File

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

View File

@@ -1,10 +1,25 @@
import { createFileRoute } from '@tanstack/react-router';
import { Workspace } from '../../../../components/Workspace';
import { createFileRoute, Navigate, useParams } from '@tanstack/react-router'
export const Route = createFileRoute('/workspaces/$workspaceId/requests/$requestId')({
// -----------------------------------------------------------------------------------
// IMPORTANT: This is a deprecated route. Since the active request is optional, it was
// moved from a path param to a query parameter. This route does a redirect to the
// parent, while preserving the active request.
export const Route = createFileRoute(
'/workspaces/$workspaceId/requests/$requestId',
)({
component: RouteComponent,
});
})
function RouteComponent() {
return <Workspace />;
const { workspaceId, requestId } = useParams({
from: '/workspaces/$workspaceId/requests/$requestId',
})
return (
<Navigate
to="/workspaces/$workspaceId"
params={{ workspaceId }}
search={(prev) => ({ ...prev, requestId })}
/>
)
}