Cookie Support (#19)

This commit is contained in:
Gregory Schier
2024-01-28 14:39:51 -08:00
committed by GitHub
parent 0555420ad9
commit 7d183c6580
45 changed files with 1152 additions and 145 deletions

View File

@@ -0,0 +1,74 @@
import { useCookieJars } from '../hooks/useCookieJars';
import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar';
import { cookieDomain } from '../lib/models';
import { Banner } from './core/Banner';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
interface Props {
cookieJarId: string | null;
}
export const CookieDialog = function ({ cookieJarId }: Props) {
const updateCookieJar = useUpdateCookieJar(cookieJarId ?? null);
const cookieJars = useCookieJars();
const cookieJar = cookieJars.find((c) => c.id === cookieJarId);
if (cookieJar == null) {
return <div>No cookie jar selected</div>;
}
if (cookieJar.cookies.length === 0) {
return (
<Banner>
Cookies will appear when a response contains the <InlineCode>Set-Cookie</InlineCode> header
</Banner>
);
}
return (
<div className="pb-2">
<table className="w-full text-xs mb-auto min-w-full max-w-full divide-y">
<thead>
<tr>
<th className="py-2 text-left">Domain</th>
<th className="py-2 text-left pl-4">Cookie</th>
<th className="py-2 pl-4"></th>
</tr>
</thead>
<tbody className="divide-y">
{cookieJar?.cookies.map((c) => (
<tr key={c.domain + c.raw_cookie}>
<td className="py-2 select-text cursor-text font-mono font-semibold max-w-0">
{cookieDomain(c)}
</td>
<td className="py-2 pl-4 select-text cursor-text font-mono text-gray-700 whitespace-nowrap overflow-x-auto max-w-[200px] hide-scrollbars">
{c.raw_cookie}
</td>
<td className="max-w-0 w-10">
<IconButton
icon="trash"
size="xs"
iconSize="sm"
title="Delete"
className="ml-auto"
onClick={async () => {
console.log(
'DELETE COOKIE',
c,
cookieJar.cookies.filter((c2) => c2 !== c).length,
);
await updateCookieJar.mutateAsync({
...cookieJar,
cookies: cookieJar.cookies.filter((c2) => c2 !== c),
});
}}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};

View File

@@ -0,0 +1,93 @@
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useCookieJars } from '../hooks/useCookieJars';
import { useCreateCookieJar } from '../hooks/useCreateCookieJar';
import { useDeleteCookieJar } from '../hooks/useDeleteCookieJar';
import { usePrompt } from '../hooks/usePrompt';
import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar';
import { CookieDialog } from './CookieDialog';
import { Dropdown, type DropdownItem } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import { useDialog } from './DialogContext';
export function CookieDropdown() {
const cookieJars = useCookieJars();
const { activeCookieJar, setActiveCookieJarId } = useActiveCookieJar();
const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null);
const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null);
const createCookieJar = useCreateCookieJar();
const dialog = useDialog();
const prompt = usePrompt();
return (
<Dropdown
items={[
...cookieJars.map((j) => ({
key: j.id,
label: j.name,
leftSlot: <Icon icon={j.id === activeCookieJar?.id ? 'check' : 'empty'} />,
onSelect: () => setActiveCookieJarId(j.id),
})),
...((cookieJars.length > 0 && activeCookieJar != null
? [
{ type: 'separator', label: activeCookieJar.name },
{
key: 'manage',
label: 'Manage Cookies',
leftSlot: <Icon icon="cookie" />,
onSelect: () => {
if (activeCookieJar == null) return;
dialog.show({
id: 'cookies',
title: 'Manage Cookies',
size: 'full',
render: () => <CookieDialog cookieJarId={activeCookieJar.id} />,
});
},
},
{
key: 'rename',
label: 'Rename',
leftSlot: <Icon icon="pencil" />,
onSelect: async () => {
const name = await prompt({
title: 'Rename Cookie Jar',
description: (
<>
Enter a new name for <InlineCode>{activeCookieJar?.name}</InlineCode>
</>
),
name: 'name',
label: 'Name',
defaultValue: activeCookieJar?.name,
});
updateCookieJar.mutate({ name });
},
},
...((cookieJars.length > 1 // Never delete the last one
? [
{
key: 'delete',
label: 'Delete',
leftSlot: <Icon icon="trash" />,
variant: 'danger',
onSelect: () => deleteCookieJar.mutateAsync(),
},
]
: []) as DropdownItem[]),
]
: []) as DropdownItem[]),
{ type: 'separator' },
{
key: 'create-cookie-jar',
label: 'New Cookie Jar',
leftSlot: <Icon icon="plus" />,
onSelect: () => createCookieJar.mutate(),
},
]}
>
<IconButton size="sm" icon="cookie" title="Cookie Jar" />
</Dropdown>
);
}

View File

@@ -54,23 +54,26 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
...((environments.length > 0
? [{ type: 'separator', label: 'Environments' }]
: []) as DropdownItem[]),
environments.length
? {
key: 'edit',
label: 'Manage Environments',
hotKeyAction: 'environmentEditor.toggle',
leftSlot: <Icon icon="box" />,
onSelect: showEnvironmentDialog,
}
: {
key: 'new',
label: 'New Environment',
leftSlot: <Icon icon="plus" />,
onSelect: async () => {
await createEnvironment.mutateAsync();
showEnvironmentDialog();
...((environments.length > 0
? [
{
key: 'edit',
label: 'Manage Environments',
hotKeyAction: 'environmentEditor.toggle',
leftSlot: <Icon icon="box" />,
onSelect: showEnvironmentDialog,
},
},
]
: []) as DropdownItem[]),
{
key: 'new',
label: 'New Environment',
leftSlot: <Icon icon="plus" />,
onSelect: async () => {
await createEnvironment.mutateAsync();
showEnvironmentDialog();
},
},
],
[activeEnvironment?.id, createEnvironment, environments, routes, showEnvironmentDialog],
);

View File

@@ -51,12 +51,12 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
return (
<div
className={classNames(
'h-full grid gap-x-8 grid-rows-[minmax(0,1fr)]',
'h-full pt-1 grid gap-x-8 grid-rows-[minmax(0,1fr)]',
showSidebar ? 'grid-cols-[auto_minmax(0,1fr)]' : 'grid-cols-[minmax(0,1fr)]',
)}
>
{showSidebar && (
<aside className="grid grid-rows-[minmax(0,1fr)_auto] gap-y-0.5 h-full max-w-[250px] pr-3 border-r border-gray-100 -ml-2">
<aside className="grid grid-rows-[minmax(0,1fr)_auto] gap-y-0.5 h-full max-w-[250px] pr-3 border-r border-gray-100 -ml-2 pb-4">
<div className="min-w-0 h-full w-full overflow-y-scroll">
{environments.map((e) => (
<SidebarButton

View File

@@ -2,6 +2,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { appWindow } from '@tauri-apps/api/window';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
@@ -55,6 +56,8 @@ export function GlobalHooks() {
? keyValueQueryKey(payload)
: payload.model === 'settings'
? settingsQueryKey()
: payload.model === 'cookie_jar'
? cookieJarsQueryKey(payload)
: null;
if (queryKey === null) {
@@ -80,6 +83,8 @@ export function GlobalHooks() {
? workspacesQueryKey(payload)
: payload.model === 'key_value'
? keyValueQueryKey(payload)
: payload.model === 'cookie_jar'
? cookieJarsQueryKey(payload)
: payload.model === 'settings'
? settingsQueryKey()
: null;
@@ -115,6 +120,8 @@ export function GlobalHooks() {
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(payload), removeById(payload));
} else if (payload.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
} else if (payload.model === 'cookie_jar') {
queryClient.setQueryData(cookieJarsQueryKey(payload), undefined);
} else if (payload.model === 'settings') {
queryClient.setQueryData(settingsQueryKey(), undefined);
}

View File

@@ -84,39 +84,41 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
actions={
error || isLoading
? [
<Button
key="introspection"
size="xs"
color={error ? 'danger' : 'gray'}
isLoading={isLoading}
onClick={() => {
dialog.show({
title: 'Introspection Failed',
size: 'dynamic',
id: 'introspection-failed',
render: () => (
<>
<FormattedError>{error ?? 'unknown'}</FormattedError>
<div className="w-full mt-3">
<Button
onClick={() => {
dialog.hide('introspection-failed');
refetch();
}}
className="ml-auto"
color="secondary"
size="sm"
>
Try Again
</Button>
</div>
</>
),
});
}}
>
{error ? 'Introspection Failed' : 'Introspecting'}
</Button>,
<div key="introspection" className="!opacity-100">
<Button
key="introspection"
size="xs"
color={error ? 'danger' : 'gray'}
isLoading={isLoading}
onClick={() => {
dialog.show({
title: 'Introspection Failed',
size: 'dynamic',
id: 'introspection-failed',
render: () => (
<>
<FormattedError>{error ?? 'unknown'}</FormattedError>
<div className="w-full my-4">
<Button
onClick={() => {
dialog.hide('introspection-failed');
refetch();
}}
className="ml-auto"
color="secondary"
size="sm"
>
Try Again
</Button>
</div>
</>
),
});
}}
>
{error ? 'Introspection Failed' : 'Introspecting'}
</Button>
</div>,
]
: []
}

View File

@@ -3,7 +3,7 @@ import { HotKeyList } from './core/HotKeyList';
export const KeyboardShortcutsDialog = () => {
return (
<div className="h-full w-full">
<div className="h-full w-full pb-2">
<HotKeyList hotkeys={hotkeyActions} />
</div>
);

View File

@@ -99,7 +99,11 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
'shadow shadow-gray-100 dark:shadow-gray-0 relative',
)}
>
{activeResponse?.error && <Banner className="m-2">{activeResponse.error}</Banner>}
{activeResponse?.error && (
<Banner color="danger" className="m-2">
{activeResponse.error}
</Banner>
)}
{!activeResponse && (
<>
<span />

View File

@@ -20,7 +20,7 @@ export const SettingsDialog = () => {
}
return (
<VStack space={2} className="mb-2">
<VStack space={2} className="mb-4">
<Select
name="appearance"
label="Appearance"

View File

@@ -71,7 +71,7 @@ export function SettingsDropdown() {
size: 'sm',
render: ({ hide }) => {
return (
<VStack space={3}>
<VStack space={3} className="pb-4">
<p>Insomnia or Postman Collection v2/v2.1 formats are supported</p>
<Button
size="sm"

View File

@@ -51,7 +51,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
),
render: ({ hide }) => {
return (
<HStack space={2} justifyContent="end" alignItems="center" className="mt-6">
<HStack space={2} justifyContent="end" alignItems="center" className="mt-4 mb-6">
<Button
className="focus"
color="gray"
@@ -135,6 +135,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
label: 'Name',
defaultValue: 'My Workspace',
title: 'New Workspace',
confirmLabel: 'Create',
});
createWorkspace.mutate({ name });
},
@@ -143,6 +144,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
}, [
activeWorkspace?.name,
activeWorkspaceId,
createWorkspace,
deleteWorkspace.mutate,
dialog,
prompt,

View File

@@ -1,5 +1,6 @@
import classNames from 'classnames';
import React, { memo, useState } from 'react';
import { CookieDropdown } from './CookieDropdown';
import { Icon } from './core/Icon';
import { HStack } from './core/Stacks';
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
@@ -27,6 +28,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
>
<HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center">
<SidebarActions />
<CookieDropdown />
<HStack alignItems="center">
<WorkspaceActionsDropdown />
<Icon icon="chevronRight" className="text-gray-900 text-opacity-disabled" />

View File

@@ -4,14 +4,18 @@ import type { ReactNode } from 'react';
interface Props {
children: ReactNode;
className?: string;
color?: 'danger' | 'success' | 'gray';
}
export function Banner({ children, className }: Props) {
export function Banner({ children, className, color = 'gray' }: Props) {
return (
<div>
<div
className={classNames(
className,
'border border-red-500 bg-red-300/10 text-red-800 px-3 py-2 rounded select-auto cursor-text',
'border border-dashed italic px-3 py-2 rounded select-auto cursor-text',
color === 'gray' && 'border-gray-500/60 bg-gray-300/10 text-gray-800',
color === 'danger' && 'border-red-500/60 bg-red-300/10 text-red-800',
color === 'success' && 'border-green-500/60 bg-green-300/10 text-green-800',
)}
>
{children}

View File

@@ -5,20 +5,9 @@ import type { HotkeyAction } from '../../hooks/useHotKey';
import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey';
import { Icon } from './Icon';
const colorStyles = {
custom: 'ring-blue-500/50',
default:
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-1000 ring-blue-500/50',
gray: 'text-gray-800 bg-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50',
primary: 'bg-blue-400 text-white enabled:hocus:bg-blue-500 ring-blue-500/50',
secondary: 'bg-violet-400 text-white enabled:hocus:bg-violet-500 ring-violet-500/50',
warning: 'bg-orange-400 text-white enabled:hocus:bg-orange-500 ring-orange-500/50',
danger: 'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50',
};
export type ButtonProps = HTMLAttributes<HTMLButtonElement> & {
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & {
innerClassName?: string;
color?: keyof typeof colorStyles;
color?: 'custom' | 'default' | 'gray' | 'primary' | 'secondary' | 'warning' | 'danger';
isLoading?: boolean;
size?: 'sm' | 'md' | 'xs';
justify?: 'start' | 'center';
@@ -64,7 +53,17 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
'flex-shrink-0 flex items-center',
'focus-visible-or-class:ring rounded-md',
disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto',
colorStyles[color || 'default'],
color === 'custom' && 'ring-blue-500/50',
color === 'default' &&
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-1000 ring-blue-500/50',
color === 'gray' &&
'text-gray-800 bg-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50',
color === 'primary' && 'bg-blue-400 text-white enabled:hocus:bg-blue-500 ring-blue-500/50',
color === 'secondary' &&
'bg-violet-400 text-white enabled:hocus:bg-violet-500 ring-violet-500/50',
color === 'warning' &&
'bg-orange-400 text-white enabled:hocus:bg-orange-500 ring-orange-500/50',
color === 'danger' && 'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50',
justify === 'start' && 'justify-start',
justify === 'center' && 'justify-center',
size === 'md' && 'h-md px-3',

View File

@@ -54,10 +54,10 @@ export function Dialog({
className={classNames(
className,
'gap-2 grid grid-rows-[auto_minmax(0,1fr)]',
'relative bg-gray-50 pointer-events-auto',
'px-6 py-4 rounded-lg overflow-auto',
'pt-4 relative bg-gray-50 pointer-events-auto',
'rounded-lg',
'dark:border border-highlight shadow shadow-black/10',
'max-w-[90vw] max-h-[calc(100vh-8em)]',
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
size === 'sm' && 'w-[25rem] max-h-[80vh]',
size === 'md' && 'w-[45rem] max-h-[80vh]',
size === 'full' && 'w-[100vw] h-[100vh]',
@@ -65,19 +65,26 @@ export function Dialog({
)}
>
{title ? (
<Heading size={1} id={titleId}>
<Heading className="px-6 pt-4" size={1} id={titleId}>
{title}
</Heading>
) : (
<span />
)}
{description && <p id={descriptionId}>{description}</p>}
<div className="h-full w-full grid grid-cols-[minmax(0,1fr)]">{children}</div>
{description && (
<p className="px-6" id={descriptionId}>
{description}
</p>
)}
<div className="h-full w-full grid grid-cols-[minmax(0,1fr)] overflow-y-auto px-6 py-2">
{children}
</div>
{/*Put close at the end so that it's the last thing to be tabbed to*/}
{!hideX && (
<div className="ml-auto absolute right-1 top-1">
<IconButton
className="opacity-70 hover:opacity-100"
onClick={onClose}
title="Close dialog (Esc)"
aria-label="Close"

View File

@@ -10,7 +10,7 @@ export function FormattedError({ children }: Props) {
<pre
className={classNames(
'w-full text-sm select-auto cursor-text bg-gray-100 p-3 rounded',
'whitespace-pre border border-red-500 border-dashed overflow-x-auto',
'whitespace-pre-wrap border border-red-500 border-dashed overflow-x-auto',
)}
>
{children}

View File

@@ -11,6 +11,7 @@ const icons = {
check: lucide.CheckIcon,
chevronDown: lucide.ChevronDownIcon,
chevronRight: lucide.ChevronRightIcon,
cookie: lucide.CookieIcon,
code: lucide.CodeIcon,
copy: lucide.CopyIcon,
download: lucide.DownloadIcon,

View File

@@ -9,7 +9,7 @@ export interface AlertProps {
export function Alert({ onHide, body }: AlertProps) {
return (
<VStack space={3}>
<VStack space={3} className="pb-4">
<div>{body}</div>
<HStack space={2} justifyContent="end">
<Button className="focus" color="primary" onClick={onHide}>

View File

@@ -30,7 +30,7 @@ export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps)
};
return (
<HStack space={2} justifyContent="end" className="mt-6">
<HStack space={2} justifyContent="end" className="mt-2 mb-4">
<Button className="focus" color="gray" onClick={handleHide}>
Cancel
</Button>

View File

@@ -12,9 +12,18 @@ export interface PromptProps {
name: InputProps['name'];
defaultValue?: InputProps['defaultValue'];
placeholder?: InputProps['placeholder'];
confirmLabel?: string;
}
export function Prompt({ onHide, label, name, defaultValue, placeholder, onResult }: PromptProps) {
export function Prompt({
onHide,
label,
name,
defaultValue,
placeholder,
onResult,
confirmLabel = 'Save',
}: PromptProps) {
const [value, setValue] = useState<string>(defaultValue ?? '');
const handleSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
@@ -27,7 +36,7 @@ export function Prompt({ onHide, label, name, defaultValue, placeholder, onResul
return (
<form
className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-6"
className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-4 mb-4"
onSubmit={handleSubmit}
>
<Input
@@ -45,7 +54,7 @@ export function Prompt({ onHide, label, name, defaultValue, placeholder, onResul
Cancel
</Button>
<Button type="submit" className="focus" color="primary">
Save
{confirmLabel}
</Button>
</HStack>
</form>

View File

@@ -0,0 +1,22 @@
import { NAMESPACE_GLOBAL } from '../lib/keyValueStore';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useCookieJars } from './useCookieJars';
import { useKeyValue } from './useKeyValue';
export function useActiveCookieJar() {
const workspaceId = useActiveWorkspaceId();
const cookieJars = useCookieJars();
const kv = useKeyValue<string | null>({
namespace: NAMESPACE_GLOBAL,
key: ['activeCookieJar', workspaceId ?? 'n/a'],
defaultValue: null,
});
const activeCookieJar = cookieJars.find((cookieJar) => cookieJar.id === kv.value);
return {
activeCookieJar: activeCookieJar ?? null,
setActiveCookieJarId: kv.set,
};
}

View File

@@ -0,0 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { CookieJar } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
export function cookieJarsQueryKey({ workspaceId }: { workspaceId: string }) {
return ['cookie_jars', { workspaceId }];
}
export function useCookieJars() {
const workspaceId = useActiveWorkspaceId();
return (
useQuery({
enabled: workspaceId != null,
queryKey: cookieJarsQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
queryFn: async () => {
if (workspaceId == null) return [];
return (await invoke('list_cookie_jars', { workspaceId })) as CookieJar[];
},
}).data ?? []
);
}

View File

@@ -0,0 +1,35 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import type { CookieJar, HttpRequest } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { usePrompt } from './usePrompt';
import { requestsQueryKey } from './useRequests';
export function useCreateCookieJar() {
const workspaceId = useActiveWorkspaceId();
const queryClient = useQueryClient();
const prompt = usePrompt();
return useMutation<HttpRequest>({
mutationFn: async () => {
if (workspaceId === null) {
throw new Error("Cannot create cookie jar when there's no active workspace");
}
const name = await prompt({
name: 'name',
title: 'New CookieJar',
label: 'Name',
defaultValue: 'My Jar',
});
return invoke('create_cookie_jar', { workspaceId, name });
},
onSettled: () => trackEvent('CookieJar', 'Create'),
onSuccess: async (request) => {
queryClient.setQueryData<HttpRequest[]>(
requestsQueryKey({ workspaceId: request.workspaceId }),
(requests) => [...(requests ?? []), request],
);
},
});
}

View File

@@ -0,0 +1,37 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import type { CookieJar, Workspace } from '../lib/models';
import { useConfirm } from './useConfirm';
import { cookieJarsQueryKey } from './useCookieJars';
export function useDeleteCookieJar(cookieJar: CookieJar | null) {
const queryClient = useQueryClient();
const confirm = useConfirm();
return useMutation<CookieJar | null, string>({
mutationFn: async () => {
const confirmed = await confirm({
title: 'Delete CookieJar',
variant: 'delete',
description: (
<>
Permanently delete <InlineCode>{cookieJar?.name}</InlineCode>?
</>
),
});
if (!confirmed) return null;
return invoke('delete_cookie_jar', { cookieJarId: cookieJar?.id });
},
onSettled: () => trackEvent('CookieJar', 'Delete'),
onSuccess: async (cookieJar) => {
if (cookieJar === null) return;
const { id: cookieJarId, workspaceId } = cookieJar;
queryClient.setQueryData<CookieJar[]>(cookieJarsQueryKey({ workspaceId }), (cookieJars) =>
cookieJars?.filter((e) => e.id !== cookieJarId),
);
},
});
}

View File

@@ -47,7 +47,7 @@ export function useImportData() {
render: ({ hide }) => {
const { workspaces, environments, folders, requests } = imported;
return (
<VStack space={3}>
<VStack space={3} className="pb-4">
<ul className="list-disc pl-6">
<li>{count('Workspace', workspaces.length)}</li>
<li>{count('Environment', environments.length)}</li>

View File

@@ -1,4 +1,3 @@
import { dialog } from '@tauri-apps/api';
import type { DialogProps } from '../components/core/Dialog';
import { useDialog } from '../components/DialogContext';
import type { PromptProps } from './Prompt';
@@ -13,8 +12,8 @@ export function usePrompt() {
label,
defaultValue,
placeholder,
}: Pick<DialogProps, 'title' | 'description'> &
Pick<PromptProps, 'name' | 'label' | 'defaultValue' | 'placeholder'>) =>
confirmLabel,
}: Pick<DialogProps, 'title' | 'description'> & Omit<PromptProps, 'onResult' | 'onHide'>) =>
new Promise((onResult: PromptProps['onResult']) => {
dialog.show({
title,
@@ -22,7 +21,7 @@ export function usePrompt() {
hideX: true,
size: 'sm',
render: ({ hide }) =>
Prompt({ onHide: hide, onResult, name, label, defaultValue, placeholder }),
Prompt({ onHide: hide, onResult, name, label, defaultValue, placeholder, confirmLabel }),
});
});
}

View File

@@ -5,12 +5,14 @@ import slugify from 'slugify';
import { trackEvent } from '../lib/analytics';
import type { HttpResponse } from '../lib/models';
import { getRequest } from '../lib/store';
import { useActiveCookieJar } from './useActiveCookieJar';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useAlert } from './useAlert';
export function useSendAnyRequest(options: { download?: boolean } = {}) {
const environmentId = useActiveEnvironmentId();
const alert = useAlert();
const { activeCookieJar } = useActiveCookieJar();
return useMutation<HttpResponse | null, string, string | null>({
mutationFn: async (id) => {
const request = await getRequest(id);
@@ -33,6 +35,7 @@ export function useSendAnyRequest(options: { download?: boolean } = {}) {
requestId: id,
environmentId,
downloadDir: downloadDir,
cookieJarId: activeCookieJar?.id,
});
},
onSettled: () => trackEvent('HttpRequest', 'Send'),

View File

@@ -17,7 +17,6 @@ export function useSyncWindowTitle() {
newTitle += ` ${fallbackRequestName(activeRequest)}`;
}
console.log('Skipping setting window title to ', newTitle);
// TODO: This resets the stoplight position so we can't use it yet
// appWindow.setTitle(newTitle).catch(console.error);
}, [activeEnvironment, activeRequest, activeWorkspace]);

View File

@@ -0,0 +1,30 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { CookieJar } from '../lib/models';
import { getCookieJar } from '../lib/store';
import { cookieJarsQueryKey } from './useCookieJars';
export function useUpdateCookieJar(id: string | null) {
const queryClient = useQueryClient();
return useMutation<void, unknown, Partial<CookieJar> | ((j: CookieJar) => CookieJar)>({
mutationFn: async (v) => {
const cookieJar = await getCookieJar(id);
if (cookieJar == null) {
throw new Error("Can't update a null workspace");
}
const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v };
console.log('NEW COOKIE JAR', newCookieJar.cookies.length);
await invoke('update_cookie_jar', { cookieJar: newCookieJar });
},
onMutate: async (v) => {
const cookieJar = await getCookieJar(id);
if (cookieJar === null) return;
const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v };
queryClient.setQueryData<CookieJar[]>(cookieJarsQueryKey(cookieJar), (cookieJars) =>
(cookieJars ?? []).map((j) => (j.id === newCookieJar.id ? newCookieJar : j)),
);
},
});
}

View File

@@ -21,7 +21,6 @@ export function useUpdateWorkspace(id: string | null) {
if (workspace === null) return;
const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v };
console.log('NEW WORKSPACE', newWorkspace);
queryClient.setQueryData<Workspace[]>(workspacesQueryKey(workspace), (workspaces) =>
(workspaces ?? []).map((w) => (w.id === newWorkspace.id ? newWorkspace : w)),
);

View File

@@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api';
export function trackEvent(
resource:
| 'App'
| 'CookieJar'
| 'Sidebar'
| 'Workspace'
| 'Environment'

View File

@@ -9,7 +9,14 @@ export const AUTH_TYPE_NONE = null;
export const AUTH_TYPE_BASIC = 'basic';
export const AUTH_TYPE_BEARER = 'bearer';
export type Model = Settings | Workspace | HttpRequest | HttpResponse | KeyValue | Environment;
export type Model =
| Settings
| Workspace
| HttpRequest
| HttpResponse
| KeyValue
| Environment
| CookieJar;
export interface BaseModel {
readonly id: string;
@@ -34,6 +41,33 @@ export interface Workspace extends BaseModel {
settingRequestTimeout: number;
}
export interface CookieJar extends BaseModel {
readonly model: 'cookie_jar';
workspaceId: string;
name: string;
cookies: Cookie[];
}
export interface Cookie {
raw_cookie: string;
domain: { HostOnly: string } | { Suffix: string } | 'NotPresent' | 'Empty';
expires: { AtUtc: string } | 'SessionEnd';
path: [string, boolean];
}
export function cookieDomain(cookie: Cookie): string {
if (cookie.domain === 'NotPresent' || cookie.domain === 'Empty') {
return 'n/a';
}
if ('HostOnly' in cookie.domain) {
return cookie.domain.HostOnly;
}
if ('Suffix' in cookie.domain) {
return cookie.domain.Suffix;
}
return 'unknown';
}
export interface EnvironmentVariable {
name: string;
value: string;

View File

@@ -1,5 +1,13 @@
import { invoke } from '@tauri-apps/api';
import type { Environment, Folder, HttpRequest, Settings, Workspace } from './models';
import type {
Cookie,
CookieJar,
Environment,
Folder,
HttpRequest,
Settings,
Workspace,
} from './models';
export async function getSettings(): Promise<Settings> {
return invoke('get_settings', {});
@@ -40,3 +48,12 @@ export async function getWorkspace(id: string | null): Promise<Workspace | null>
}
return workspace;
}
export async function getCookieJar(id: string | null): Promise<CookieJar | null> {
if (id === null) return null;
const cookieJar: CookieJar = (await invoke('get_cookie_jar', { id })) ?? null;
if (cookieJar == null) {
return null;
}
return cookieJar;
}