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

@@ -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)),
);