A bunch of changes, including moving prompt/confirm out of context

This commit is contained in:
Gregory Schier
2025-01-07 06:56:51 -08:00
parent 4776bbc753
commit 2f7b66fc92
41 changed files with 315 additions and 353 deletions

View File

@@ -1,21 +0,0 @@
import type { ReactNode } from 'react';
import { Button } from '../components/core/Button';
import { HStack, VStack } from '../components/core/Stacks';
export interface AlertProps {
onHide: () => void;
body: ReactNode;
}
export function Alert({ onHide, body }: AlertProps) {
return (
<VStack space={3} className="pb-4">
<div>{body}</div>
<HStack space={2} justifyContent="end">
<Button className="focus" color="primary" onClick={onHide}>
Okay
</Button>
</HStack>
</VStack>
);
}

View File

@@ -1,43 +0,0 @@
import type { ButtonProps } from '../components/core/Button';
import { Button } from '../components/core/Button';
import { HStack } from '../components/core/Stacks';
export interface ConfirmProps {
onHide: () => void;
onResult: (result: boolean) => void;
variant?: 'delete' | 'confirm';
confirmText?: string;
}
const colors: Record<NonNullable<ConfirmProps['variant']>, ButtonProps['color']> = {
delete: 'danger',
confirm: 'primary',
};
const confirmButtonTexts: Record<NonNullable<ConfirmProps['variant']>, string> = {
delete: 'Delete',
confirm: 'Confirm',
};
export function Confirm({ onHide, onResult, confirmText, variant = 'confirm' }: ConfirmProps) {
const handleHide = () => {
onResult(false);
onHide();
};
const handleSuccess = () => {
onResult(true);
onHide();
};
return (
<HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse">
<Button color={colors[variant]} onClick={handleSuccess}>
{confirmText ?? confirmButtonTexts[variant]}
</Button>
<Button onClick={handleHide} variant="border">
Cancel
</Button>
</HStack>
);
}

View File

@@ -1,57 +0,0 @@
import type { PromptTextRequest } from '@yaakapp-internal/plugins';
import type { FormEvent, ReactNode } from 'react';
import { useCallback, useState } from 'react';
import { Button } from '../components/core/Button';
import { PlainInput } from '../components/core/PlainInput';
import { HStack } from '../components/core/Stacks';
export type PromptProps = Omit<PromptTextRequest, 'id' | 'title' | 'description'> & {
description?: ReactNode;
onCancel: () => void;
onResult: (value: string | null) => void;
};
export function Prompt({
onCancel,
label,
defaultValue,
placeholder,
onResult,
require,
confirmText,
cancelText,
}: PromptProps) {
const [value, setValue] = useState<string>(defaultValue ?? '');
const handleSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onResult(value);
},
[onResult, value],
);
return (
<form
className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-4 mb-4"
onSubmit={handleSubmit}
>
<PlainInput
hideLabel
autoSelect
require={require}
placeholder={placeholder ?? 'Enter text'}
label={label}
defaultValue={defaultValue}
onChange={setValue}
/>
<HStack space={2} justifyContent="end">
<Button onClick={onCancel} variant="border" color="secondary">
{cancelText || 'Cancel'}
</Button>
<Button type="submit" color="primary">
{confirmText || 'Done'}
</Button>
</HStack>
</form>
);
}

View File

@@ -3,34 +3,26 @@ import type { CookieJar } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai/index';
import { useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';
import { router } from '../lib/router';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import { cookieJarsAtom, useCookieJars } from './useCookieJars';
export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id';
export const activeCookieJarIdAtom = atom<string>();
export const activeCookieJarAtom = atom<CookieJar | null>((get) => {
const activeId = get(activeCookieJarIdAtom);
return get(cookieJarsAtom)?.find((e) => e.id === activeId) ?? null;
});
export function setActiveCookieJar(cookieJar: CookieJar) {
router.navigate({
from: '/workspaces/$workspaceId',
search: (prev) => ({ ...prev, cookie_jar_id: cookieJar.id }),
});
}
export const activeCookieJarAtom = atom<CookieJar | null>(null);
export function useActiveCookieJar() {
return useAtomValue(activeCookieJarAtom);
}
export function useSubscribeActiveCookieJarId() {
const { cookie_jar_id } = useSearch({ strict: false });
const search = useSearch({ strict: false });
const cookieJarId = search.cookie_jar_id;
const cookieJars = useAtomValue(cookieJarsAtom);
useEffect(() => {
jotaiStore.set(activeCookieJarIdAtom, cookie_jar_id ?? undefined);
}, [cookie_jar_id]);
if (search == null) return; // Happens during Vite hot reload
const activeCookieJar = cookieJars?.find((j) => j.id == cookieJarId) ?? null;
jotaiStore.set(activeCookieJarAtom, activeCookieJar);
}, [cookieJarId, cookieJars, search]);
}
export function getActiveCookieJar() {
@@ -39,12 +31,12 @@ export function getActiveCookieJar() {
export function useEnsureActiveCookieJar() {
const cookieJars = useCookieJars();
const activeCookieJar = useActiveCookieJar();
const { cookie_jar_id: activeCookieJarId } = useSearch({ from: '/workspaces/$workspaceId/' });
// 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 === activeCookieJar?.id)) {
if (cookieJars.find((j) => j.id === activeCookieJarId)) {
return; // There's an active jar
}
@@ -55,7 +47,7 @@ export function useEnsureActiveCookieJar() {
}
// There's no active jar, so set it to the first one
console.log('Setting active cookie jar to', firstJar.id);
setActiveCookieJar(firstJar);
}, [activeCookieJar?.id, cookieJars]);
console.log('Setting active cookie jar to', cookieJars, activeCookieJarId, firstJar.id);
setWorkspaceSearchParams({ cookie_jar_id: firstJar.id });
}, [activeCookieJarId, cookieJars]);
}

View File

@@ -4,7 +4,7 @@ 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 { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import { environmentsAtom } from './useEnvironments';
export const QUERY_ENVIRONMENT_ID = 'environment_id';
@@ -18,11 +18,7 @@ export const activeEnvironmentAtom = atom<Environment | null>((get) => {
export function useActiveEnvironment() {
const setId = useCallback(
(id: string | null) =>
router.navigate({
from: '/workspaces/$workspaceId',
search: (prev) => ({ ...prev, environment_id: id }),
}),
(id: string | null) => setWorkspaceSearchParams({ environment_id: id }),
[],
);
const environment = useAtomValue(activeEnvironmentAtom);

View File

@@ -1,61 +0,0 @@
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({ 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 router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: workspace.id },
});
},
onSettled: () => trackEvent('workspace', 'create'),
}),
createFolder: createFastMutation<
Folder | null,
void,
Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>
>({
mutationKey: ['create_folder'],
mutationFn: async (patch) => {
const workspaceId = getActiveWorkspaceId();
if (workspaceId == null) {
throw new Error("Cannot create folder when there's no active workspace");
}
if (!patch.name) {
const name = await prompt({
id: 'new-folder',
label: 'Name',
defaultValue: 'Folder',
title: 'New Folder',
confirmText: 'Create',
placeholder: 'Name',
});
if (name == null) return null;
patch.name = name;
}
patch.sortPriority = patch.sortPriority || -Date.now();
return invokeCmd<Folder>('cmd_update_folder', { folder: { workspaceId, ...patch } });
},
onSettled: () => trackEvent('folder', 'create'),
}),
} as const;
}
export function useCommands() {
const prompt = usePrompt();
return useMemo(() => makeCommands({ prompt }), [prompt]);
}

View File

@@ -1,34 +0,0 @@
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';
export function useConfirm() {
return useCallback(
({
id,
title,
description,
variant,
confirmText,
}: {
id: string;
title: DialogProps['title'];
description?: DialogProps['description'];
variant?: ConfirmProps['variant'];
confirmText?: ConfirmProps['confirmText'];
}) =>
new Promise((onResult: ConfirmProps['onResult']) => {
showDialog({
id,
title,
description,
hideX: true,
size: 'sm',
render: ({ hide }) => Confirm({ onHide: hide, variant, onResult, confirmText }),
});
}),
[],
);
}

View File

@@ -1,13 +1,11 @@
import type { CookieJar } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics';
import { showPrompt } from '../lib/prompt';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
import { usePrompt } from './usePrompt';
export function useCreateCookieJar() {
const prompt = usePrompt();
return useFastMutation<CookieJar | null>({
mutationKey: ['create_cookie_jar'],
mutationFn: async () => {
@@ -15,7 +13,7 @@ export function useCreateCookieJar() {
if (workspaceId == null) {
throw new Error("Cannot create cookie jar when there's no active workspace");
}
const name = await prompt({
const name = await showPrompt({
id: 'new-cookie-jar',
title: 'New CookieJar',
placeholder: 'My Jar',

View File

@@ -1,10 +1,10 @@
import { useMemo } from 'react';
import type { DropdownItem } from '../components/core/Dropdown';
import { Icon } from '../components/core/Icon';
import { createFolder } from '../lib/commands';
import { generateId } from '../lib/generateId';
import { BODY_TYPE_GRAPHQL } from '../lib/model_util';
import { getActiveRequest } from './useActiveRequest';
import { useCommands } from './useCommands';
import { useCreateGrpcRequest } from './useCreateGrpcRequest';
import { useCreateHttpRequest } from './useCreateHttpRequest';
@@ -19,7 +19,6 @@ export function useCreateDropdownItems({
} = {}): DropdownItem[] {
const { mutate: createHttpRequest } = useCreateHttpRequest();
const { mutate: createGrpcRequest } = useCreateGrpcRequest();
const { createFolder } = useCommands();
return useMemo((): DropdownItem[] => {
const folderId =
@@ -66,5 +65,5 @@ export function useCreateDropdownItems({
},
]) as DropdownItem[]),
];
}, [createFolder, createGrpcRequest, createHttpRequest, folderIdOption, hideFolder, hideIcons]);
}, [createGrpcRequest, createHttpRequest, folderIdOption, hideFolder, hideIcons]);
}

View File

@@ -1,14 +1,13 @@
import type { Environment } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics';
import { showPrompt } from '../lib/prompt';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironment } from './useActiveEnvironment';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
import { usePrompt } from './usePrompt';
export function useCreateEnvironment() {
const [, setActiveEnvironmentId] = useActiveEnvironment();
const prompt = usePrompt();
return useFastMutation<Environment | null, unknown, Environment | null>({
mutationKey: ['create_environment'],
@@ -18,7 +17,7 @@ export function useCreateEnvironment() {
}
const workspaceId = getActiveWorkspaceId();
const name = await prompt({
const name = await showPrompt({
id: 'new-environment',
title: 'New Environment',
description: 'Create multiple environments with different sets of variables',

View File

@@ -1,20 +1,18 @@
import type { Workspace } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { showConfirm } from '../lib/confirm';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspace } from './useActiveWorkspace';
import { useConfirm } from './useConfirm';
import { useFastMutation } from './useFastMutation';
export function useDeleteActiveWorkspace() {
const confirm = useConfirm();
return useFastMutation<Workspace | null, string>({
mutationKey: ['delete_workspace'],
mutationFn: async () => {
const workspace = getActiveWorkspace();
const confirmed = await confirm({
const confirmed = await showConfirm({
id: 'delete-workspace',
title: 'Delete Workspace',
variant: 'delete',

View File

@@ -1,22 +1,20 @@
import type { GrpcRequest } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { showConfirm } from '../lib/confirm';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { getGrpcRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import { useFastMutation } from './useFastMutation';
export function useDeleteAnyGrpcRequest() {
const confirm = useConfirm();
return useFastMutation<GrpcRequest | null, string, string>({
mutationKey: ['delete_any_grpc_request'],
mutationFn: async (id) => {
const request = await getGrpcRequest(id);
if (request == null) return null;
const confirmed = await confirm({
const confirmed = await showConfirm({
id: 'delete-grpc-request',
title: 'Delete Request',
variant: 'delete',

View File

@@ -1,22 +1,20 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { showConfirm } from '../lib/confirm';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { getHttpRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import { useFastMutation } from './useFastMutation';
export function useDeleteAnyHttpRequest() {
const confirm = useConfirm();
return useFastMutation<HttpRequest | null, string, string>({
mutationKey: ['delete_any_http_request'],
mutationFn: async (id) => {
const request = await getHttpRequest(id);
if (request == null) return null;
const confirmed = await confirm({
const confirmed = await showConfirm({
id: 'delete-request',
title: 'Delete Request',
variant: 'delete',

View File

@@ -1,21 +1,20 @@
import { useFastMutation } from './useFastMutation';
import type { CookieJar } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { showConfirm } from '../lib/confirm';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import { cookieJarsAtom } from './useCookieJars';
import { useFastMutation } from './useFastMutation';
import { removeModelById } from './useSyncModelStores';
export function useDeleteCookieJar(cookieJar: CookieJar | null) {
const confirm = useConfirm();
const setCookieJars = useSetAtom(cookieJarsAtom);
return useFastMutation<CookieJar | null, string>({
mutationKey: ['delete_cookie_jar', cookieJar?.id],
mutationFn: async () => {
const confirmed = await confirm({
const confirmed = await showConfirm({
id: 'delete-cookie-jar',
title: 'Delete CookieJar',
variant: 'delete',

View File

@@ -1,21 +1,20 @@
import { useFastMutation } from './useFastMutation';
import type { Environment } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai";
import { useSetAtom } from 'jotai';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { showConfirm } from '../lib/confirm';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import {environmentsAtom} from "./useEnvironments";
import {removeModelById} from "./useSyncModelStores";
import { environmentsAtom } from './useEnvironments';
import { useFastMutation } from './useFastMutation';
import { removeModelById } from './useSyncModelStores';
export function useDeleteEnvironment(environment: Environment | null) {
const confirm = useConfirm();
const setEnvironments = useSetAtom(environmentsAtom);
return useFastMutation<Environment | null, string>({
mutationKey: ['delete_environment', environment?.id],
mutationFn: async () => {
const confirmed = await confirm({
const confirmed = await showConfirm({
id: 'delete-environment',
title: 'Delete Environment',
variant: 'delete',
@@ -33,6 +32,6 @@ export function useDeleteEnvironment(environment: Environment | null) {
if (environment == null) return;
setEnvironments(removeModelById(environment));
}
},
});
}

View File

@@ -2,22 +2,21 @@ import type { Folder } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { showConfirm } from '../lib/confirm';
import { getFolder } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import { useFastMutation } from './useFastMutation';
import { foldersAtom } from './useFolders';
import { removeModelById } from './useSyncModelStores';
import { useFastMutation } from './useFastMutation';
export function useDeleteFolder(id: string | null) {
const confirm = useConfirm();
const setFolders = useSetAtom(foldersAtom);
return useFastMutation<Folder | null, string>({
mutationKey: ['delete_folder', id],
mutationFn: async () => {
const folder = await getFolder(id);
const confirmed = await confirm({
const confirmed = await showConfirm({
id: 'delete-folder',
title: 'Delete Folder',
variant: 'delete',

View File

@@ -1,15 +1,14 @@
import { useSetAtom } from 'jotai/index';
import { showAlert } from '../lib/alert';
import { showConfirm } from '../lib/confirm';
import { pluralizeCount } from '../lib/pluralize';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { useConfirm } from './useConfirm';
import { useFastMutation } from './useFastMutation';
import { useGrpcConnections } from './useGrpcConnections';
import { httpResponsesAtom, useHttpResponses } from './useHttpResponses';
export function useDeleteSendHistory() {
const confirm = useConfirm();
const setHttpResponses = useSetAtom(httpResponsesAtom);
const httpResponses = useHttpResponses();
const grpcConnections = useGrpcConnections();
@@ -30,7 +29,7 @@ export function useDeleteSendHistory() {
return;
}
const confirmed = await confirm({
const confirmed = await showConfirm({
id: 'delete-send-history',
title: 'Clear Send History',
variant: 'delete',

View File

@@ -1,37 +0,0 @@
import type { DialogProps } from '../components/core/Dialog';
import { showDialog } from '../lib/dialog';
import type { PromptProps } from './Prompt';
import { Prompt } from './Prompt';
type Props = Pick<DialogProps, 'title' | 'description'> &
Omit<PromptProps, 'onClose' | 'onCancel' | 'onResult'> & { id: string };
export function usePrompt() {
return ({ id, title, description, ...props }: Props) =>
new Promise((resolve: PromptProps['onResult']) => {
showDialog({
id,
title,
description,
hideX: true,
size: 'sm',
onClose: () => {
// Click backdrop, close, or escape
resolve(null);
},
render: ({ hide }) =>
Prompt({
onCancel: () => {
// Click cancel button within dialog
resolve(null);
hide();
},
onResult: (v) => {
resolve(v);
hide();
},
...props,
}),
});
});
}

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo } from 'react';
import { jotaiStore } from '../lib/jotai';
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
import { activeCookieJarIdAtom } from './useActiveCookieJar';
import {activeCookieJarAtom} from "./useActiveCookieJar";
import { activeWorkspaceIdAtom, useActiveWorkspace } from './useActiveWorkspace';
import { useCookieJars } from './useCookieJars';
import { useKeyValue } from './useKeyValue';
@@ -29,9 +29,9 @@ export function useRecentCookieJars() {
export function useSubscribeRecentCookieJars() {
useEffect(() => {
return jotaiStore.sub(activeCookieJarIdAtom, async () => {
return jotaiStore.sub(activeCookieJarAtom, async () => {
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
const activeCookieJarId = jotaiStore.get(activeCookieJarIdAtom);
const activeCookieJarId = jotaiStore.get(activeCookieJarAtom)?.id ?? null;
if (activeWorkspaceId == null) return;
if (activeCookieJarId == null) return;

View File

@@ -1,13 +1,12 @@
import { useFastMutation } from './useFastMutation';
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { usePrompt } from './usePrompt';
import {showPrompt} from "../lib/prompt";
import { useFastMutation } from './useFastMutation';
import { useRequests } from './useRequests';
import { useUpdateAnyGrpcRequest } from './useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from './useUpdateAnyHttpRequest';
export function useRenameRequest(requestId: string | null) {
const prompt = usePrompt();
const updateHttpRequest = useUpdateAnyHttpRequest();
const updateGrpcRequest = useUpdateAnyGrpcRequest();
const requests = useRequests();
@@ -18,7 +17,7 @@ export function useRenameRequest(requestId: string | null) {
const request = requests.find((r) => r.id === requestId);
if (request == null) return;
const name = await prompt({
const name = await showPrompt({
id: 'rename-request',
title: 'Rename Request',
description:

View File

@@ -4,9 +4,9 @@ import { applySync, calculateSync } from '@yaakapp-internal/sync';
import { useCallback, useMemo } from 'react';
import { InlineCode } from '../components/core/InlineCode';
import { VStack } from '../components/core/Stacks';
import {showConfirm} from "../lib/confirm";
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { pluralizeCount } from '../lib/pluralize';
import { useConfirm } from './useConfirm';
export function useSyncWorkspace(
workspace: Workspace | null,
@@ -16,12 +16,10 @@ export function useSyncWorkspace(
debounceMillis?: number;
} = {},
) {
const confirm = useConfirm();
const sync = useCallback(async () => {
if (workspace == null) return;
if (workspace == null || workspace.settingSyncDir) return;
const ops = await calculateSync(workspace);
const ops = await calculateSync(workspace) ?? [];
if (ops.length === 0) {
return;
}
@@ -33,7 +31,7 @@ export function useSyncWorkspace(
return;
}
const confirmed = await confirm({
const confirmed = await showConfirm({
id: 'commit-sync',
title: 'Filesystem Changes Detected',
confirmText: 'Apply Changes',
@@ -92,7 +90,7 @@ export function useSyncWorkspace(
if (confirmed) {
await applySync(workspace, ops);
}
}, [confirm, workspace]);
}, [workspace]);
const debouncedSync = useMemo(() => {
return debounce(sync, debounceMillis);