Generalized frontend model store (#193)

This commit is contained in:
Gregory Schier
2025-03-31 11:56:17 -07:00
committed by GitHub
parent ce885c3551
commit f1757ae427
201 changed files with 2185 additions and 2865 deletions

View File

@@ -1,12 +1,10 @@
import { useSearch } from '@tanstack/react-router';
import type { CookieJar } from '@yaakapp-internal/models';
import { cookieJarsAtom } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai/index';
import { useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import { cookieJarsAtom, useCookieJars } from './useCookieJars';
export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id';
export const activeCookieJarAtom = atom<CookieJar | null>(null);
@@ -18,6 +16,7 @@ export function useSubscribeActiveCookieJarId() {
const search = useSearch({ strict: false });
const cookieJarId = search.cookie_jar_id;
const cookieJars = useAtomValue(cookieJarsAtom);
useEffect(() => {
if (search == null) return; // Happens during Vite hot reload
const activeCookieJar = cookieJars?.find((j) => j.id == cookieJarId) ?? null;
@@ -30,7 +29,7 @@ export function getActiveCookieJar() {
}
export function useEnsureActiveCookieJar() {
const cookieJars = useCookieJars();
const cookieJars = useAtomValue(cookieJarsAtom);
const { cookie_jar_id: activeCookieJarId } = useSearch({ from: '/workspaces/$workspaceId/' });
// Set the active cookie jar to the first one, if none set

View File

@@ -1,12 +1,10 @@
import { useSearch } from '@tanstack/react-router';
import type { Environment } from '@yaakapp-internal/models';
import { environmentsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import { useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';
import { environmentsAtom } from './useEnvironments';
export const QUERY_ENVIRONMENT_ID = 'environment_id';
export const activeEnvironmentIdAtom = atom<string>();

View File

@@ -1,26 +1,24 @@
import type { EnvironmentVariable } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
import { activeEnvironmentAtom } from './useActiveEnvironment';
import { environmentsBreakdownAtom } from './useEnvironments';
const activeEnvironmentVariablesAtom = atom((get) => {
const { baseEnvironment } = get(environmentsBreakdownAtom);
const activeEnvironment = get(activeEnvironmentAtom);
const varMap: Record<string, EnvironmentVariable> = {};
const allVariables = [
...(baseEnvironment?.variables ?? []),
...(activeEnvironment?.variables ?? []),
];
for (const v of allVariables) {
if (!v.enabled || !v.name) continue;
varMap[v.name] = v;
}
return Object.values(varMap);
});
import { useEnvironmentsBreakdown } from './useEnvironmentsBreakdown';
export function useActiveEnvironmentVariables() {
return useAtomValue(activeEnvironmentVariablesAtom);
const { baseEnvironment } = useEnvironmentsBreakdown();
const activeEnvironment = useAtomValue(activeEnvironmentAtom);
return useMemo(() => {
const varMap: Record<string, EnvironmentVariable> = {};
const allVariables = [
...(baseEnvironment?.variables ?? []),
...(activeEnvironment?.variables ?? []),
];
for (const v of allVariables) {
if (!v.enabled || !v.name) continue;
varMap[v.name] = v;
}
return Object.values(varMap);
}, [activeEnvironment, baseEnvironment]);
}

View File

@@ -1,8 +1,13 @@
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
import { jotaiStore } from '../lib/jotai';
import { activeRequestIdAtom } from './useActiveRequestId';
import { requestsAtom } from './useRequests';
import { allRequestsAtom } from './useAllRequests';
export const activeRequestAtom = atom((get) => {
const activeRequestId = get(activeRequestIdAtom);
const requests = get(allRequestsAtom);
return requests.find((r) => r.id === activeRequestId) ?? null;
});
interface TypeMap {
http_request: HttpRequest;
@@ -10,16 +15,6 @@ interface TypeMap {
websocket_request: WebsocketRequest;
}
export const activeRequestAtom = atom((get) => {
const activeRequestId = get(activeRequestIdAtom);
const requests = get(requestsAtom);
return requests.find((r) => r.id === activeRequestId) ?? null;
});
export function getActiveRequest() {
return jotaiStore.get(activeRequestAtom);
}
export function useActiveRequest<T extends keyof TypeMap>(
model?: T | undefined,
): TypeMap[T] | null {

View File

@@ -1,31 +1,24 @@
import {useParams} from '@tanstack/react-router';
import type {Workspace} from '@yaakapp-internal/models';
import {atom, useAtomValue} from 'jotai/index';
import {useEffect} from 'react';
import {jotaiStore} from '../lib/jotai';
import {workspacesAtom} from './useWorkspaces';
import { useParams } from '@tanstack/react-router';
import { workspaceMetasAtom, workspacesAtom } from '@yaakapp-internal/models';
import { atom } from 'jotai/index';
import { useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';
export const activeWorkspaceIdAtom = atom<string>();
export const activeWorkspaceIdAtom = atom<string | null>(null);
export const activeWorkspaceAtom = atom<Workspace | null>((get) => {
const activeWorkspaceId = get(activeWorkspaceIdAtom);
const workspaces = get(workspacesAtom);
return workspaces.find((w) => w.id === activeWorkspaceId) ?? null;
export const activeWorkspaceAtom = atom((get) => {
const activeWorkspaceId = get(activeWorkspaceIdAtom);
const workspaces = get(workspacesAtom);
return workspaces.find((w) => w.id === activeWorkspaceId) ?? null;
});
export function useActiveWorkspace(): Workspace | null {
return useAtomValue(activeWorkspaceAtom);
}
export function getActiveWorkspaceId() {
return jotaiStore.get(activeWorkspaceIdAtom) ?? null;
}
export function getActiveWorkspace() {
return jotaiStore.get(activeWorkspaceAtom) ?? null;
}
export const activeWorkspaceMetaAtom = atom((get) => {
const activeWorkspaceId = get(activeWorkspaceIdAtom);
const workspaceMetas = get(workspaceMetasAtom);
return workspaceMetas.find((m) => m.workspaceId === activeWorkspaceId) ?? null;
});
export function useSubscribeActiveWorkspaceId() {
const {workspaceId} = useParams({strict: false});
useEffect(() => jotaiStore.set(activeWorkspaceIdAtom, workspaceId), [workspaceId]);
const { workspaceId } = useParams({ strict: false });
useEffect(() => jotaiStore.set(activeWorkspaceIdAtom, workspaceId ?? null), [workspaceId]);
}

View File

@@ -1,10 +1,11 @@
import { useAtomValue } from 'jotai';
import { useEffect, useState } from 'react';
import { InlineCode } from '../components/core/InlineCode';
import { useActiveWorkspace } from './useActiveWorkspace';
import { showToast } from '../lib/toast';
import { activeWorkspaceAtom } from './useActiveWorkspace';
export function useActiveWorkspaceChangedToast() {
const activeWorkspace = useActiveWorkspace();
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
const [id, setId] = useState<string | null>(activeWorkspace?.id ?? null);
useEffect(() => {

View File

@@ -0,0 +1,14 @@
import {
grpcRequestsAtom,
httpRequestsAtom,
websocketRequestsAtom,
} from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai/index';
export const allRequestsAtom = atom(function (get) {
return [...get(httpRequestsAtom), ...get(grpcRequestsAtom), ...get(websocketRequestsAtom)];
});
export function useAllRequests() {
return useAtomValue(allRequestsAtom);
}

View File

@@ -1,17 +0,0 @@
import type { CookieJar } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
import { jotaiStore } from '../lib/jotai';
export const cookieJarsAtom = atom<CookieJar[] | undefined>();
export const sortedCookieJars = atom((get) => {
return get(cookieJarsAtom)?.sort((a, b) => a.name.localeCompare(b.name));
});
export function useCookieJars() {
return useAtomValue(sortedCookieJars);
}
export function getCookieJar(id: string | null) {
return jotaiStore.get(cookieJarsAtom)?.find((e) => e.id === id) ?? null;
}

View File

@@ -1,17 +1,19 @@
import type { CookieJar } from '@yaakapp-internal/models';
import { createWorkspaceModel } from '@yaakapp-internal/models';
import { jotaiStore } from '../lib/jotai';
import { showPrompt } from '../lib/prompt';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
export function useCreateCookieJar() {
return useFastMutation<CookieJar | null>({
return useFastMutation({
mutationKey: ['create_cookie_jar'],
mutationFn: async () => {
const workspaceId = getActiveWorkspaceId();
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) {
throw new Error("Cannot create cookie jar when there's no active workspace");
}
const name = await showPrompt({
id: 'new-cookie-jar',
title: 'New CookieJar',
@@ -22,7 +24,10 @@ export function useCreateCookieJar() {
});
if (name == null) return null;
return invokeCmd('cmd_create_cookie_jar', { workspaceId, name });
return createWorkspaceModel({ model: 'cookie_jar', workspaceId, name });
},
onSuccess: async (cookieJarId) => {
setWorkspaceSearchParams({ cookie_jar_id: cookieJarId });
},
});
}

View File

@@ -1,13 +1,14 @@
import { createWorkspaceModel } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
import { createFolder } from '../commands/commands';
import { upsertWebsocketRequest } from '../commands/upsertWebsocketRequest';
import type { DropdownItem } from '../components/core/Dropdown';
import { Icon } from '../components/core/Icon';
import { generateId } from '../lib/generateId';
import { jotaiStore } from '../lib/jotai';
import { BODY_TYPE_GRAPHQL } from '../lib/model_util';
import { getActiveRequest } from './useActiveRequest';
import { getActiveWorkspace } from './useActiveWorkspace';
import { useCreateGrpcRequest } from './useCreateGrpcRequest';
import { activeRequestAtom } from './useActiveRequest';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useCreateHttpRequest } from './useCreateHttpRequest';
export function useCreateDropdownItems({
@@ -20,14 +21,13 @@ export function useCreateDropdownItems({
folderId?: string | null | 'active-folder';
} = {}): DropdownItem[] {
const { mutate: createHttpRequest } = useCreateHttpRequest();
const { mutate: createGrpcRequest } = useCreateGrpcRequest();
const activeWorkspace = getActiveWorkspace();
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const items = useMemo((): DropdownItem[] => {
const activeRequest = getActiveRequest();
const activeRequest = jotaiStore.get(activeRequestAtom);
const folderId =
(folderIdOption === 'active-folder' ? activeRequest?.folderId : folderIdOption) ?? null;
if (activeWorkspace == null) return [];
if (workspaceId == null) return [];
return [
{
@@ -51,13 +51,12 @@ export function useCreateDropdownItems({
{
label: 'gRPC',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => createGrpcRequest({ folderId }),
onSelect: () => createWorkspaceModel({ model: 'grpc_request', workspaceId, folderId }),
},
{
label: 'WebSocket',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () =>
upsertWebsocketRequest.mutate({ folderId, workspaceId: activeWorkspace.id }),
onSelect: () => createWorkspaceModel({ model: 'websocket_request', workspaceId, folderId }),
},
...((hideFolder
? []
@@ -70,14 +69,7 @@ export function useCreateDropdownItems({
},
]) as DropdownItem[]),
];
}, [
activeWorkspace,
createGrpcRequest,
createHttpRequest,
folderIdOption,
hideFolder,
hideIcons,
]);
}, [createHttpRequest, folderIdOption, hideFolder, hideIcons, workspaceId]);
return items;
}

View File

@@ -1,19 +1,24 @@
import type { Environment } from '@yaakapp-internal/models';
import { createWorkspaceModel } from '@yaakapp-internal/models';
import { jotaiStore } from '../lib/jotai';
import { showPrompt } from '../lib/prompt';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
export function useCreateEnvironment() {
return useFastMutation<Environment | null, unknown, Environment | null>({
return useFastMutation<string, unknown, Environment | null>({
mutationKey: ['create_environment'],
mutationFn: async (baseEnvironment) => {
if (baseEnvironment == null) {
throw new Error('No base environment passed');
}
const workspaceId = getActiveWorkspaceId();
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) {
throw new Error('Cannot create environment when no active workspace');
}
const name = await showPrompt({
id: 'new-environment',
title: 'New Environment',
@@ -23,18 +28,18 @@ export function useCreateEnvironment() {
defaultValue: 'My Environment',
confirmText: 'Create',
});
if (name == null) return null;
if (name == null) throw new Error('No name provided to create environment');
return invokeCmd('cmd_create_environment', {
return createWorkspaceModel({
model: 'environment',
name,
variables: [],
workspaceId,
environmentId: baseEnvironment.id,
});
},
onSuccess: async (environment) => {
if (environment == null) return;
setWorkspaceSearchParams({ environment_id: environment.id });
onSuccess: async (environmentId) => {
setWorkspaceSearchParams({ environment_id: environmentId });
},
});
}

View File

@@ -1,24 +1,24 @@
import type { GrpcRequest } from '@yaakapp-internal/models';
import { createWorkspaceModel } from '@yaakapp-internal/models';
import { jotaiStore } from '../lib/jotai';
import { invokeCmd } from '../lib/tauri';
import { getActiveRequest } from './useActiveRequest';
import { activeWorkspaceAtom } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
import { router } from '../lib/router';
import { activeRequestAtom } from './useActiveRequest';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
export function useCreateGrpcRequest() {
return useFastMutation<
GrpcRequest,
string,
unknown,
Partial<Pick<GrpcRequest, 'name' | 'sortPriority' | 'folderId'>>
>({
mutationKey: ['create_grpc_request'],
mutationFn: async (patch) => {
const workspace = jotaiStore.get(activeWorkspaceAtom);
if (workspace === null) {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId === null) {
throw new Error("Cannot create grpc request when there's no active workspace");
}
const activeRequest = getActiveRequest();
const activeRequest = jotaiStore.get(activeRequestAtom);
if (patch.sortPriority === undefined) {
if (activeRequest != null) {
// Place above currently active request
@@ -29,17 +29,16 @@ export function useCreateGrpcRequest() {
}
}
patch.folderId = patch.folderId || activeRequest?.folderId;
return invokeCmd<GrpcRequest>('cmd_create_grpc_request', {
workspaceId: workspace.id,
name: '',
...patch,
});
return createWorkspaceModel({ model: 'grpc_request', workspaceId, ...patch });
},
onSuccess: async (request) => {
onSuccess: async (requestId) => {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) return;
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: request.workspaceId },
search: (prev) => ({ ...prev, request_id: request.id }),
params: { workspaceId },
search: (prev) => ({ ...prev, request_id: requestId }),
});
},
});

View File

@@ -1,20 +1,21 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { createWorkspaceModel } from '@yaakapp-internal/models';
import { jotaiStore } from '../lib/jotai';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { getActiveRequest } from './useActiveRequest';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { activeRequestAtom } from './useActiveRequest';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
export function useCreateHttpRequest() {
return useFastMutation<HttpRequest, unknown, Partial<HttpRequest>>({
return useFastMutation<string, unknown, Partial<HttpRequest>>({
mutationKey: ['create_http_request'],
mutationFn: async (patch = {}) => {
const workspaceId = getActiveWorkspaceId();
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) {
throw new Error("Cannot create request when there's no active workspace");
}
const activeRequest = getActiveRequest();
const activeRequest = jotaiStore.get(activeRequestAtom);
if (patch.sortPriority === undefined) {
if (activeRequest != null) {
// Place above currently active request
@@ -25,15 +26,15 @@ export function useCreateHttpRequest() {
}
}
patch.folderId = patch.folderId || activeRequest?.folderId;
return invokeCmd<HttpRequest>('cmd_upsert_http_request', {
request: { workspaceId, ...patch },
});
return createWorkspaceModel({ model: 'http_request', workspaceId, ...patch });
},
onSuccess: async (request) => {
onSuccess: async (requestId) => {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) return;
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: request.workspaceId },
search: (prev) => ({ ...prev, request_id: request.id }),
params: { workspaceId },
search: (prev) => ({ ...prev, request_id: requestId }),
});
},
});

View File

@@ -1,31 +0,0 @@
import type { Workspace } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { showConfirmDelete } from '../lib/confirm';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspace } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
export function useDeleteActiveWorkspace() {
return useFastMutation<Workspace | null, string>({
mutationKey: ['delete_workspace'],
mutationFn: async () => {
const workspace = getActiveWorkspace();
const confirmed = await showConfirmDelete({
id: 'delete-workspace',
title: 'Delete Workspace',
description: (
<>
Permanently delete <InlineCode>{workspace?.name}</InlineCode>?
</>
),
});
if (!confirmed) return null;
return invokeCmd('cmd_delete_workspace', { workspaceId: workspace?.id });
},
onSuccess: async (workspace) => {
if (workspace === null) return;
await router.navigate({ to: '/workspaces' });
},
});
}

View File

@@ -1,27 +0,0 @@
import type { GrpcRequest } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { showConfirmDelete } from '../lib/confirm';
import { resolvedModelName } from '../lib/resolvedModelName';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
export function useDeleteAnyGrpcRequest() {
return useFastMutation<GrpcRequest | null, string, GrpcRequest>({
mutationKey: ['delete_any_grpc_request'],
mutationFn: async (request) => {
const confirmed = await showConfirmDelete({
id: 'delete-grpc-request',
title: 'Delete Request',
description: (
<>
Permanently delete <InlineCode>{resolvedModelName(request)}</InlineCode>?
</>
),
});
if (!confirmed) {
return null;
}
return invokeCmd('cmd_delete_grpc_request', { requestId: request.id });
},
});
}

View File

@@ -1,27 +0,0 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { showConfirmDelete } from '../lib/confirm';
import { resolvedModelName } from '../lib/resolvedModelName';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
export function useDeleteAnyHttpRequest() {
return useFastMutation<HttpRequest | null, string, HttpRequest>({
mutationKey: ['delete_any_http_request'],
mutationFn: async (request) => {
const confirmed = await showConfirmDelete({
id: 'delete-request',
title: 'Delete Request',
description: (
<>
Permanently delete <InlineCode>{resolvedModelName(request)}</InlineCode>?
</>
),
});
if (!confirmed) {
return null;
}
return invokeCmd<HttpRequest>('cmd_delete_http_request', { requestId: request.id });
},
});
}

View File

@@ -1,29 +0,0 @@
import { deleteWebsocketRequest } from '../commands/deleteWebsocketRequest';
import { jotaiStore } from '../lib/jotai';
import { useDeleteAnyGrpcRequest } from './useDeleteAnyGrpcRequest';
import { useDeleteAnyHttpRequest } from './useDeleteAnyHttpRequest';
import { useFastMutation } from './useFastMutation';
import { requestsAtom } from './useRequests';
export function useDeleteAnyRequest() {
const deleteAnyHttpRequest = useDeleteAnyHttpRequest();
const deleteAnyGrpcRequest = useDeleteAnyGrpcRequest();
return useFastMutation<void, string, string>({
mutationKey: ['delete_request'],
mutationFn: async (id) => {
if (id == null) return;
const request = jotaiStore.get(requestsAtom).find((r) => r.id === id);
if (request?.model === 'websocket_request') {
deleteWebsocketRequest.mutate(request);
} else if (request?.model === 'http_request') {
deleteAnyHttpRequest.mutate(request);
} else if (request?.model === 'grpc_request') {
deleteAnyGrpcRequest.mutate(request);
} else {
console.log('Failed to delete request', id, request);
}
},
});
}

View File

@@ -1,24 +0,0 @@
import type { CookieJar } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { showConfirmDelete } from '../lib/confirm';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
export function useDeleteCookieJar(cookieJar: CookieJar | null) {
return useFastMutation<CookieJar | null, string>({
mutationKey: ['delete_cookie_jar', cookieJar?.id],
mutationFn: async () => {
const confirmed = await showConfirmDelete({
id: 'delete-cookie-jar',
title: 'Delete CookieJar',
description: (
<>
Permanently delete <InlineCode>{cookieJar?.name}</InlineCode>?
</>
),
});
if (!confirmed) return null;
return invokeCmd('cmd_delete_cookie_jar', { cookieJarId: cookieJar?.id });
},
});
}

View File

@@ -1,24 +0,0 @@
import type { Environment } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { showConfirmDelete } from '../lib/confirm';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
export function useDeleteEnvironment(environment: Environment | null) {
return useFastMutation<Environment | null, string>({
mutationKey: ['delete_environment', environment?.id],
mutationFn: async () => {
const confirmed = await showConfirmDelete({
id: 'delete-environment',
title: 'Delete Environment',
description: (
<>
Permanently delete <InlineCode>{environment?.name}</InlineCode>?
</>
),
});
if (!confirmed) return null;
return invokeCmd('cmd_delete_environment', { environmentId: environment?.id });
},
});
}

View File

@@ -1,26 +0,0 @@
import type { Folder } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { showConfirmDelete } from '../lib/confirm';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
import { getFolder } from './useFolders';
export function useDeleteFolder(id: string | null) {
return useFastMutation<Folder | null, string>({
mutationKey: ['delete_folder', id],
mutationFn: async () => {
const folder = getFolder(id);
const confirmed = await showConfirmDelete({
id: 'delete-folder',
title: 'Delete Folder',
description: (
<>
Permanently delete <InlineCode>{folder?.name}</InlineCode> and everything in it?
</>
),
});
if (!confirmed) return null;
return invokeCmd('cmd_delete_folder', { folderId: id });
},
});
}

View File

@@ -1,12 +0,0 @@
import type { GrpcConnection } from '@yaakapp-internal/models';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
export function useDeleteGrpcConnection(id: string | null) {
return useFastMutation<GrpcConnection>({
mutationKey: ['delete_grpc_connection', id],
mutationFn: async () => {
return await invokeCmd('cmd_delete_grpc_connection', { id: id });
},
});
}

View File

@@ -1,18 +1,12 @@
import { useFastMutation } from './useFastMutation';
import { useSetAtom } from 'jotai';
import { invokeCmd } from '../lib/tauri';
import { grpcConnectionsAtom } from './useGrpcConnections';
import { useFastMutation } from './useFastMutation';
export function useDeleteGrpcConnections(requestId?: string) {
const setGrpcConnections = useSetAtom(grpcConnectionsAtom);
return useFastMutation({
mutationKey: ['delete_grpc_connections', requestId],
mutationFn: async () => {
if (requestId === undefined) return;
await invokeCmd('cmd_delete_all_grpc_connections', { requestId });
},
onSuccess: () => {
setGrpcConnections((all) => all.filter((r) => r.requestId !== requestId));
},
});
}

View File

@@ -1,12 +0,0 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
export function useDeleteHttpResponse(id: string | null) {
return useFastMutation<HttpResponse>({
mutationKey: ['delete_http_response', id],
mutationFn: async () => {
return await invokeCmd('cmd_delete_http_response', { id: id });
},
});
}

View File

@@ -1,18 +1,12 @@
import { useFastMutation } from './useFastMutation';
import { useSetAtom } from 'jotai';
import { invokeCmd } from '../lib/tauri';
import { httpResponsesAtom } from './useHttpResponses';
import { useFastMutation } from './useFastMutation';
export function useDeleteHttpResponses(requestId?: string) {
const setHttpResponses = useSetAtom(httpResponsesAtom);
return useFastMutation({
mutationKey: ['delete_http_responses', requestId],
mutationFn: async () => {
if (requestId === undefined) return;
await invokeCmd('cmd_delete_all_http_responses', { requestId });
},
onSuccess: () => {
setHttpResponses((all) => all.filter((r) => r.requestId !== requestId));
},
});
}

View File

@@ -1,19 +1,22 @@
import { useSetAtom } from 'jotai/index';
import {
grpcConnectionsAtom,
httpResponsesAtom,
websocketConnectionsAtom,
} from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
import { showAlert } from '../lib/alert';
import { showConfirmDelete } from '../lib/confirm';
import { jotaiStore } from '../lib/jotai';
import { pluralizeCount } from '../lib/pluralize';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
import { useGrpcConnections } from './useGrpcConnections';
import { httpResponsesAtom, useHttpResponses } from './useHttpResponses';
import { useWebsocketConnections } from './useWebsocketConnections';
export function useDeleteSendHistory() {
const setHttpResponses = useSetAtom(httpResponsesAtom);
const httpResponses = useHttpResponses();
const grpcConnections = useGrpcConnections();
const websocketConnections = useWebsocketConnections();
const httpResponses = useAtomValue(httpResponsesAtom);
const grpcConnections = useAtomValue(grpcConnectionsAtom);
const websocketConnections = useAtomValue(websocketConnectionsAtom);
const labels = [
httpResponses.length > 0 ? pluralizeCount('Http Response', httpResponses.length) : null,
grpcConnections.length > 0 ? pluralizeCount('Grpc Connection', grpcConnections.length) : null,
@@ -41,14 +44,9 @@ export function useDeleteSendHistory() {
});
if (!confirmed) return false;
const workspaceId = getActiveWorkspaceId();
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
await invokeCmd('cmd_delete_send_history', { workspaceId });
return true;
},
onSuccess: async (confirmed) => {
if (!confirmed) return;
const activeWorkspaceId = getActiveWorkspaceId();
setHttpResponses((all) => all.filter((r) => r.workspaceId !== activeWorkspaceId));
},
});
}

View File

@@ -1,9 +0,0 @@
import { useFastMutation } from './useFastMutation';
import { invokeCmd } from '../lib/tauri';
export function useDuplicateFolder(id: string) {
return useFastMutation<void, string>({
mutationKey: ['duplicate_folder', id],
mutationFn: () => invokeCmd('cmd_duplicate_folder', { id }),
});
}

View File

@@ -1,36 +0,0 @@
import type { GrpcRequest } from '@yaakapp-internal/models';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
import { getGrpcProtoFiles, setGrpcProtoFiles } from './useGrpcProtoFiles';
export function useDuplicateGrpcRequest({
id,
navigateAfter,
}: {
id: string | null;
navigateAfter: boolean;
}) {
return useFastMutation<GrpcRequest, string>({
mutationKey: ['duplicate_grpc_request', id],
mutationFn: async () => {
if (id === null) throw new Error("Can't duplicate a null grpc request");
return invokeCmd('cmd_duplicate_grpc_request', { id });
},
onSuccess: async (request) => {
if (id == null) return;
// Also copy proto files to new request
const protoFiles = await getGrpcProtoFiles(id);
await setGrpcProtoFiles(request.id, protoFiles);
if (navigateAfter) {
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: request.workspaceId },
search: (prev) => ({ ...prev, request_id: request.id }),
});
}
},
});
}

View File

@@ -1,29 +0,0 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
export function useDuplicateHttpRequest({
id,
navigateAfter,
}: {
id: string | null;
navigateAfter: boolean;
}) {
return useFastMutation<HttpRequest, string>({
mutationKey: ['duplicate_http_request', id],
mutationFn: async () => {
if (id === null) throw new Error("Can't duplicate a null request");
return invokeCmd('cmd_duplicate_http_request', { id });
},
onSuccess: async (request) => {
if (navigateAfter) {
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: request.workspaceId },
search: (prev) => ({ ...prev, request_id: request.id }),
});
}
},
});
}

View File

@@ -1,30 +0,0 @@
import type { Environment } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import { jotaiStore } from '../lib/jotai';
export const environmentsAtom = atom<Environment[]>([]);
export const sortedEnvironmentsAtom = atom((get) =>
get(environmentsAtom).sort((a, b) => a.name.localeCompare(b.name)),
);
export const environmentsBreakdownAtom = atom<{
baseEnvironment: Environment | null;
allEnvironments: Environment[];
subEnvironments: Environment[];
}>((get) => {
const allEnvironments = get(sortedEnvironmentsAtom);
const baseEnvironment = allEnvironments.find((e) => e.environmentId == null) ?? null;
const subEnvironments =
allEnvironments.filter((e) => e.environmentId === (baseEnvironment?.id ?? 'n/a')) ?? [];
return { baseEnvironment, subEnvironments, allEnvironments } as const;
});
export function useEnvironments() {
return useAtomValue(environmentsBreakdownAtom);
}
export function getEnvironment(id: string | null) {
return jotaiStore.get(environmentsAtom).find((e) => e.id === id) ?? null;
}

View File

@@ -0,0 +1,13 @@
import { environmentsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
import { useMemo } from 'react';
export function useEnvironmentsBreakdown() {
const allEnvironments = useAtomValue(environmentsAtom);
return useMemo(() => {
const baseEnvironment = allEnvironments.find((e) => e.environmentId == null) ?? null;
const subEnvironments =
allEnvironments.filter((e) => e.environmentId === (baseEnvironment?.id ?? 'n/a')) ?? [];
return { allEnvironments, baseEnvironment, subEnvironments };
}, [allEnvironments]);
}

View File

@@ -1,11 +1,11 @@
import { workspacesAtom } from '@yaakapp-internal/models';
import { ExportDataDialog } from '../components/ExportDataDialog';
import { showAlert } from '../lib/alert';
import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai';
import { showToast } from '../lib/toast';
import { getActiveWorkspace } from './useActiveWorkspace';
import { activeWorkspaceAtom } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
import { workspacesAtom } from './useWorkspaces';
export function useExportData() {
return useFastMutation({
@@ -14,7 +14,7 @@ export function useExportData() {
showAlert({ id: 'export-failed', title: 'Export Failed', body: err });
},
mutationFn: async () => {
const activeWorkspace = getActiveWorkspace();
const activeWorkspace = jotaiStore.get(activeWorkspaceAtom);
const workspaces = jotaiStore.get(workspacesAtom);
if (activeWorkspace == null || workspaces.length === 0) return;

View File

@@ -1,8 +1,9 @@
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAtomValue } from 'jotai';
import { activeWorkspaceAtom } from './useActiveWorkspace';
import { useKeyValue } from './useKeyValue';
export function useFloatingSidebarHidden() {
const activeWorkspace = useActiveWorkspace();
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
const { set, value } = useKeyValue<boolean>({
namespace: 'no_sync',
key: ['floating_sidebar_hidden', activeWorkspace?.id ?? 'n/a'],

View File

@@ -1,14 +0,0 @@
import type { Folder } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import { jotaiStore } from '../lib/jotai';
export const foldersAtom = atom<Folder[]>([]);
export function useFolders() {
return useAtomValue(foldersAtom);
}
export function getFolder(id: string | null) {
return jotaiStore.get(foldersAtom).find((v) => v.id === id) ?? null;
}

View File

@@ -1,8 +0,0 @@
import type { GrpcConnection } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai/index';
export const grpcConnectionsAtom = atom<GrpcConnection[]>([]);
export function useGrpcConnections() {
return useAtomValue(grpcConnectionsAtom);
}

View File

@@ -1,23 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import type { GrpcEvent } from '@yaakapp-internal/models';
import { invokeCmd } from '../lib/tauri';
export function grpcEventsQueryKey({ connectionId }: { connectionId: string }) {
return ['grpc_events', { connectionId }];
}
export function useGrpcEvents(connectionId: string | null) {
return (
useQuery<GrpcEvent[]>({
enabled: connectionId !== null,
initialData: [],
queryKey: grpcEventsQueryKey({ connectionId: connectionId ?? 'n/a' }),
queryFn: async () => {
return (await invokeCmd('cmd_list_grpc_events', {
connectionId,
limit: 200,
})) as GrpcEvent[];
},
}).data ?? []
);
}

View File

@@ -1,4 +1,3 @@
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
import { useKeyValue } from './useKeyValue';
export function protoFilesArgs(requestId: string | null) {
@@ -11,11 +10,3 @@ export function protoFilesArgs(requestId: string | null) {
export function useGrpcProtoFiles(activeRequestId: string | null) {
return useKeyValue<string[]>({ ...protoFilesArgs(activeRequestId), fallback: [] });
}
export async function getGrpcProtoFiles(requestId: string) {
return getKeyValue<string[]>({ ...protoFilesArgs(requestId), fallback: [] });
}
export async function setGrpcProtoFiles(requestId: string, protoFiles: string[]) {
return setKeyValue<string[]>({ ...protoFilesArgs(requestId), value: protoFiles });
}

View File

@@ -1,7 +0,0 @@
import type { GrpcRequest } from '@yaakapp-internal/models';
import { useGrpcRequests } from './useGrpcRequests';
export function useGrpcRequest(id: string | null): GrpcRequest | null {
const requests = useGrpcRequests();
return requests.find((r) => r.id === id) ?? null;
}

View File

@@ -1,13 +0,0 @@
import type { GrpcRequest } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
import { jotaiStore } from '../lib/jotai';
export const grpcRequestsAtom = atom<GrpcRequest[]>([]);
export function useGrpcRequests() {
return useAtomValue(grpcRequestsAtom);
}
export function getGrpcRequest(id: string) {
return jotaiStore.get(grpcRequestsAtom).find((r) => r.id === id) ?? null;
}

View File

@@ -15,13 +15,13 @@ export type HotkeyAction =
| 'grpc_request.send'
| 'hotkeys.showHelp'
| 'http_request.create'
| 'http_request.delete'
| 'http_request.duplicate'
| 'http_request.send'
| 'request_switcher.next'
| 'request_switcher.prev'
| 'request_switcher.toggle'
| 'settings.show'
| 'sidebar.delete_selected_item'
| 'sidebar.focus'
| 'url_bar.focus'
| 'workspace_settings.show';
@@ -35,13 +35,13 @@ const hotkeys: Record<HotkeyAction, string[]> = {
'grpc_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
'hotkeys.showHelp': ['CmdCtrl+Shift+/', 'CmdCtrl+Shift+?'], // when shift is pressed, it might be a question mark
'http_request.create': ['CmdCtrl+n'],
'http_request.delete': ['Backspace'],
'http_request.duplicate': ['CmdCtrl+d'],
'http_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
'request_switcher.next': ['Control+Shift+Tab'],
'request_switcher.prev': ['Control+Tab'],
'request_switcher.toggle': ['CmdCtrl+p'],
'settings.show': ['CmdCtrl+,'],
'sidebar.delete_selected_item': ['Backspace'],
'sidebar.focus': ['CmdCtrl+b'],
'url_bar.focus': ['CmdCtrl+l'],
'workspace_settings.show': ['CmdCtrl+;'],
@@ -56,13 +56,13 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
'grpc_request.send': 'Send Message',
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
'http_request.create': 'New Request',
'http_request.delete': 'Delete Request',
'http_request.duplicate': 'Duplicate Request',
'http_request.send': 'Send Request',
'request_switcher.next': 'Go To Previous Request',
'request_switcher.prev': 'Go To Next Request',
'request_switcher.toggle': 'Toggle Request Switcher',
'settings.show': 'Open Settings',
'sidebar.delete_selected_item': 'Delete Request',
'sidebar.focus': 'Focus or Toggle Sidebar',
'url_bar.focus': 'Focus URL',
'workspace_settings.show': 'Open Workspace Settings',

View File

@@ -1,17 +1,18 @@
import { useQuery } from '@tanstack/react-query';
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import { httpResponsesAtom } from '@yaakapp-internal/models';
import type { GetHttpAuthenticationConfigResponse, JsonPrimitive } from '@yaakapp-internal/plugins';
import { useAtomValue } from 'jotai';
import { md5 } from 'js-md5';
import { useState } from 'react';
import { invokeCmd } from '../lib/tauri';
import { useHttpResponses } from './useHttpResponses';
export function useHttpAuthenticationConfig(
authName: string | null,
values: Record<string, JsonPrimitive>,
requestId: string,
) {
const responses = useHttpResponses();
const responses = useAtomValue(httpResponsesAtom);
const [forceRefreshCounter, setForceRefreshCounter] = useState<number>(0);
// Some auth handlers like OAuth 2.0 show the current token after a successful request. To

View File

@@ -1,7 +0,0 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { useHttpRequests } from './useHttpRequests';
export function useHttpRequest(id: string | null): HttpRequest | null {
const requests = useHttpRequests();
return requests.find((r) => r.id === id) ?? null;
}

View File

@@ -1,13 +0,0 @@
import type {HttpRequest} from '@yaakapp-internal/models';
import {atom, useAtomValue} from 'jotai';
import {jotaiStore} from "../lib/jotai";
export const httpRequestsAtom = atom<HttpRequest[]>([]);
export function useHttpRequests() {
return useAtomValue(httpRequestsAtom);
}
export function getHttpRequest(id: string) {
return jotaiStore.get(httpRequestsAtom).find(r => r.id === id) ?? null;
}

View File

@@ -1,9 +0,0 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
export const httpResponsesAtom = atom<HttpResponse[]>([]);
export function useHttpResponses() {
return useAtomValue(httpResponsesAtom);
}

View File

@@ -1,17 +1,13 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import type { HttpRequest} from '@yaakapp-internal/models';
import { createWorkspaceModel, patchModelById } from '@yaakapp-internal/models';
import { jotaiStore } from '../lib/jotai';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { useCreateHttpRequest } from './useCreateHttpRequest';
import { useFastMutation } from './useFastMutation';
import { useRequestUpdateKey } from './useRequestUpdateKey';
import { showToast } from '../lib/toast';
import { useUpdateAnyHttpRequest } from './useUpdateAnyHttpRequest';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
import { wasUpdatedExternally } from './useRequestUpdateKey';
export function useImportCurl() {
const updateRequest = useUpdateAnyHttpRequest();
const createRequest = useCreateHttpRequest();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
return useFastMutation({
mutationKey: ['import_curl'],
mutationFn: async ({
@@ -21,8 +17,8 @@ export function useImportCurl() {
overwriteRequestId?: string;
command: string;
}) => {
const workspaceId = getActiveWorkspaceId();
const request: HttpRequest = await invokeCmd('cmd_curl_to_request', {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
const importedRequest: HttpRequest = await invokeCmd('cmd_curl_to_request', {
command,
workspaceId,
});
@@ -30,21 +26,18 @@ export function useImportCurl() {
let verb;
if (overwriteRequestId == null) {
verb = 'Created';
await createRequest.mutateAsync(request);
await createWorkspaceModel(importedRequest);
} else {
verb = 'Updated';
await updateRequest.mutateAsync({
id: overwriteRequestId,
update: (r: HttpRequest) => ({
...request,
id: r.id,
createdAt: r.createdAt,
workspaceId: r.workspaceId,
folderId: r.folderId,
name: r.name,
sortPriority: r.sortPriority,
}),
});
await patchModelById(importedRequest.model, overwriteRequestId, (r: HttpRequest) => ({
...importedRequest,
id: r.id,
createdAt: r.createdAt,
workspaceId: r.workspaceId,
folderId: r.folderId,
name: r.name,
sortPriority: r.sortPriority,
}));
setTimeout(() => wasUpdatedExternally(overwriteRequestId), 100);
}

View File

@@ -11,15 +11,16 @@ import { VStack } from '../components/core/Stacks';
import { ImportDataDialog } from '../components/ImportDataDialog';
import { showAlert } from '../lib/alert';
import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai';
import { pluralizeCount } from '../lib/pluralize';
import { router } from '../lib/router';
import { invokeCmd } from '../lib/tauri';
import { getActiveWorkspace } from './useActiveWorkspace';
import { activeWorkspaceAtom } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
export function useImportData() {
const importData = async (filePath: string): Promise<boolean> => {
const activeWorkspace = getActiveWorkspace();
const activeWorkspace = jotaiStore.get(activeWorkspaceAtom);
const imported: {
workspaces: Workspace[];
environments: Environment[];

View File

@@ -1,25 +1,12 @@
import { useMutation } from '@tanstack/react-query';
import type { KeyValue } from '@yaakapp-internal/models';
import { keyValuesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import { useCallback, useMemo } from 'react';
import { jotaiStore } from '../lib/jotai';
import { buildKeyValueKey, extractKeyValueOrFallback, setKeyValue } from '../lib/keyValueStore';
const DEFAULT_NAMESPACE = 'global';
export const keyValuesAtom = atom<KeyValue[] | null>(null);
export function keyValueQueryKey({
namespace = DEFAULT_NAMESPACE,
key,
}: {
namespace?: string;
key: string | string[];
}) {
return ['key_value', { namespace, key: buildKeyValueKey(key) }];
}
export function useKeyValue<T extends object | boolean | number | string | null>({
namespace = DEFAULT_NAMESPACE,
key,

View File

@@ -1,6 +1,7 @@
import type { GrpcConnection } from '@yaakapp-internal/models';
import { useGrpcConnections } from './useGrpcConnections';
import type { GrpcConnection} from '@yaakapp-internal/models';
import { grpcConnectionsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
export function useLatestGrpcConnection(requestId: string | null): GrpcConnection | null {
return useGrpcConnections().find((c) => c.requestId === requestId) ?? null;
return useAtomValue(grpcConnectionsAtom).find((c) => c.requestId === requestId) ?? null;
}

View File

@@ -1,6 +1,7 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import { useHttpResponses } from './useHttpResponses';
import type { HttpResponse} from '@yaakapp-internal/models';
import { httpResponsesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
export function useLatestHttpResponse(requestId: string | null): HttpResponse | null {
return useHttpResponses().find((r) => r.requestId === requestId) ?? null;
return useAtomValue(httpResponsesAtom).find((r) => r.requestId === requestId) ?? null;
}

View File

@@ -2,18 +2,18 @@ import React from 'react';
import { MoveToWorkspaceDialog } from '../components/MoveToWorkspaceDialog';
import { showDialog } from '../lib/dialog';
import { jotaiStore } from '../lib/jotai';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
import { requestsAtom } from './useRequests';
import { allRequestsAtom } from './useAllRequests';
export function useMoveToWorkspace(id: string) {
return useFastMutation<void, unknown>({
mutationKey: ['move_workspace', id],
mutationFn: async () => {
const activeWorkspaceId = getActiveWorkspaceId();
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (activeWorkspaceId == null) return;
const request = jotaiStore.get(requestsAtom).find((r) => r.id === id);
const request = jotaiStore.get(allRequestsAtom).find((r) => r.id === id);
if (request == null) return;
showDialog({

View File

@@ -1,19 +1,72 @@
import type { GrpcConnection, GrpcRequest } from '@yaakapp-internal/models';
import { useGrpcConnections } from './useGrpcConnections';
import { useKeyValue } from './useKeyValue';
import { useLatestGrpcConnection } from './useLatestGrpcConnection';
import { invoke } from '@tauri-apps/api/core';
import type { GrpcConnection, GrpcEvent } from '@yaakapp-internal/models';
import {
grpcConnectionsAtom,
grpcEventsAtom,
replaceModelsInStore,
} from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import { useEffect } from 'react';
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
import { activeRequestIdAtom } from './useActiveRequestId';
export function usePinnedGrpcConnection(activeRequest: GrpcRequest) {
const latestConnection = useLatestGrpcConnection(activeRequest.id);
const { set: setPinnedConnectionId, value: pinnedConnectionId } = useKeyValue<string | null>({
// Key on latest connection instead of activeRequest because connections change out of band of active request
key: ['pinned_grpc_connection_id', latestConnection?.id ?? 'n/a'],
fallback: null,
namespace: 'global',
});
const connections = useGrpcConnections().filter((c) => c.requestId === activeRequest.id);
const activeConnection: GrpcConnection | null =
connections.find((r) => r.id === pinnedConnectionId) ?? latestConnection;
const pinnedGrpcConnectionIdsAtom = atomWithKVStorage<Record<string, string | null>>(
'pinned-grpc-connection-ids',
{},
);
return { activeConnection, setPinnedConnectionId, pinnedConnectionId, connections } as const;
export const pinnedGrpcConnectionIdAtom = atom(
(get) => {
const activeRequestId = get(activeRequestIdAtom);
const activeConnections = get(activeGrpcConnections);
const latestConnection = activeConnections[0] ?? null;
if (!activeRequestId) return null;
const key = recordKey(activeRequestId, latestConnection);
return get(pinnedGrpcConnectionIdsAtom)[key] ?? null;
},
(get, set, id: string | null) => {
const activeRequestId = get(activeRequestIdAtom);
const activeConnections = get(activeGrpcConnections);
const latestConnection = activeConnections[0] ?? null;
if (!activeRequestId) return;
const key = recordKey(activeRequestId, latestConnection);
set(pinnedGrpcConnectionIdsAtom, (prev) => ({
...prev,
[key]: id,
}));
},
);
function recordKey(activeRequestId: string | null, latestConnection: GrpcConnection | null) {
return activeRequestId + '-' + (latestConnection?.id ?? 'none');
}
export const activeGrpcConnections = atom<GrpcConnection[]>((get) => {
const activeRequestId = get(activeRequestIdAtom) ?? 'n/a';
return get(grpcConnectionsAtom).filter((c) => c.requestId === activeRequestId) ?? [];
});
export const activeGrpcConnectionAtom = atom<GrpcConnection | null>((get) => {
const activeRequestId = get(activeRequestIdAtom) ?? 'n/a';
const activeConnections = get(activeGrpcConnections);
const latestConnection = activeConnections[0] ?? null;
const pinnedConnectionId = get(pinnedGrpcConnectionIdsAtom)[
recordKey(activeRequestId, latestConnection)
];
return activeConnections.find((c) => c.id === pinnedConnectionId) ?? activeConnections[0] ?? null;
});
export function useGrpcEvents(connectionId: string | null) {
const events = useAtomValue(grpcEventsAtom);
useEffect(() => {
invoke<GrpcEvent[]>('plugin:yaak-models|grpc_events', { connectionId }).then((events) => {
replaceModelsInStore('grpc_event', events);
});
}, [connectionId]);
return events;
}

View File

@@ -1,5 +1,6 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import { useHttpResponses } from './useHttpResponses';
import type { HttpResponse} from '@yaakapp-internal/models';
import { httpResponsesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
import { useKeyValue } from './useKeyValue';
import { useLatestHttpResponse } from './useLatestHttpResponse';
@@ -11,7 +12,7 @@ export function usePinnedHttpResponse(activeRequestId: string) {
fallback: null,
namespace: 'global',
});
const allResponses = useHttpResponses();
const allResponses = useAtomValue(httpResponsesAtom);
const responses = allResponses.filter((r) => r.requestId === activeRequestId);
const activeResponse: HttpResponse | null =
responses.find((r) => r.id === pinnedResponseId) ?? latestResponse;

View File

@@ -1,18 +1,65 @@
import type { WebsocketConnection, WebsocketRequest } from '@yaakapp-internal/models';
import { useKeyValue } from './useKeyValue';
import { useLatestWebsocketConnection, useWebsocketConnections } from './useWebsocketConnections';
import { invoke } from '@tauri-apps/api/core';
import type { WebsocketConnection, WebsocketEvent } from '@yaakapp-internal/models';
import {
replaceModelsInStore,
websocketConnectionsAtom,
websocketEventsAtom,
} from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai/index';
import { useEffect } from 'react';
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
import { jotaiStore } from '../lib/jotai';
import { activeRequestIdAtom } from './useActiveRequestId';
export function usePinnedWebsocketConnection(activeRequest: WebsocketRequest) {
const latestConnection = useLatestWebsocketConnection(activeRequest.id);
const { set: setPinnedConnectionId, value: pinnedConnectionId } = useKeyValue<string | null>({
// Key on latest connection instead of activeRequest because connections change out of band of active request
key: ['pinned_websocket_connection_id', latestConnection?.id ?? 'n/a'],
fallback: null,
namespace: 'global',
});
const connections = useWebsocketConnections().filter((c) => c.requestId === activeRequest.id);
const activeConnection: WebsocketConnection | null =
connections.find((r) => r.id === pinnedConnectionId) ?? latestConnection;
const pinnedWebsocketConnectionIdAtom = atomWithKVStorage<Record<string, string | null>>(
'pinned-websocket-connection-ids',
{},
);
return { activeConnection, setPinnedConnectionId, pinnedConnectionId, connections } as const;
function recordKey(activeRequestId: string | null, latestConnection: WebsocketConnection | null) {
return activeRequestId + '-' + (latestConnection?.id ?? 'none');
}
export const activeWebsocketConnectionsAtom = atom<WebsocketConnection[]>((get) => {
const activeRequestId = get(activeRequestIdAtom) ?? 'n/a';
return get(websocketConnectionsAtom).filter((c) => c.requestId === activeRequestId) ?? [];
});
export const activeWebsocketConnectionAtom = atom<WebsocketConnection | null>((get) => {
const activeRequestId = get(activeRequestIdAtom) ?? 'n/a';
const activeConnections = get(activeWebsocketConnectionsAtom);
const latestConnection = activeConnections[0] ?? null;
const pinnedConnectionId = get(pinnedWebsocketConnectionIdAtom)[
recordKey(activeRequestId, latestConnection)
];
return activeConnections.find((c) => c.id === pinnedConnectionId) ?? activeConnections[0] ?? null;
});
export const activeWebsocketEventsAtom = atom(async (get) => {
const connection = get(activeWebsocketConnectionAtom);
return invoke<WebsocketEvent[]>('plugin:yaak-models|websocket_events', {
connectionId: connection?.id ?? 'n/a',
});
});
export function setPinnedWebsocketConnectionId(id: string | null) {
const activeRequestId = jotaiStore.get(activeRequestIdAtom);
const activeConnections = jotaiStore.get(activeWebsocketConnectionsAtom);
const latestConnection = activeConnections[0] ?? null;
if (activeRequestId == null) return;
jotaiStore.set(pinnedWebsocketConnectionIdAtom, (prev) => {
return { ...prev, [recordKey(activeRequestId, latestConnection)]: id };
});
}
export function useWebsocketEvents(connectionId: string | null) {
const events = useAtomValue(websocketEventsAtom);
useEffect(() => {
invoke<WebsocketEvent[]>('plugin:yaak-models|websocket_events', { connectionId }).then(
(events) => replaceModelsInStore('websocket_event', events),
);
}, [connectionId]);
return events;
}

View File

@@ -1,13 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import type { BootResponse } from '@yaakapp-internal/plugins';
import { queryClient } from '../lib/queryClient';
import { invokeCmd } from '../lib/tauri';
function pluginInfoKey(id: string) {
return ['plugin_info', id];
}
export function usePluginInfo(id: string) {
return useQuery({
queryKey: ['plugin_info', id],
queryKey: pluginInfoKey(id),
queryFn: async () => {
const info = (await invokeCmd('cmd_plugin_info', { id })) as BootResponse;
return info;
},
});
}
export function invalidateAllPluginInfo() {
queryClient.invalidateQueries({ queryKey: ['plugin_info'] }).catch(console.error);
}

View File

@@ -1,15 +1,11 @@
import {useMutation} from "@tanstack/react-query";
import type { Plugin } from '@yaakapp-internal/models';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import { useMutation } from '@tanstack/react-query';
import { changeModelStoreWorkspace, pluginsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { jotaiStore } from '../lib/jotai';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import { invokeCmd } from '../lib/tauri';
const plugins = await listPlugins();
export const pluginsAtom = atom<Plugin[]>(plugins);
export function usePlugins() {
return useAtomValue(pluginsAtom);
}
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { invalidateAllPluginInfo } from './usePluginInfo';
export function usePluginsKey() {
return useAtomValue(pluginsAtom)
@@ -21,22 +17,17 @@ export function usePluginsKey() {
* Reload all plugins and refresh the list of plugins
*/
export function useRefreshPlugins() {
const setPlugins = useSetAtom(pluginsAtom);
return useMutation({
mutationKey: ['refresh_plugins'],
mutationFn: async () => {
const plugins = await minPromiseMillis(
await minPromiseMillis(
(async function () {
await invokeCmd('cmd_reload_plugins');
return listPlugins();
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
await changeModelStoreWorkspace(workspaceId); // Force refresh models
invalidateAllPluginInfo();
})(),
);
setPlugins(plugins);
},
});
}
async function listPlugins(): Promise<Plugin[]> {
const plugins: Plugin[] = (await invokeCmd('cmd_list_plugins')) ?? [];
return plugins;
}

View File

@@ -1,9 +1,10 @@
import { cookieJarsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useEffect, useMemo } from 'react';
import { jotaiStore } from '../lib/jotai';
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
import {activeCookieJarAtom} from "./useActiveCookieJar";
import { activeWorkspaceIdAtom, useActiveWorkspace } from './useActiveWorkspace';
import { useCookieJars } from './useCookieJars';
import { activeCookieJarAtom } from './useActiveCookieJar';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useKeyValue } from './useKeyValue';
const kvKey = (workspaceId: string) => 'recent_cookie_jars::' + workspaceId;
@@ -11,10 +12,10 @@ const namespace = 'global';
const fallback: string[] = [];
export function useRecentCookieJars() {
const cookieJars = useCookieJars();
const activeWorkspace = useActiveWorkspace();
const cookieJars = useAtomValue(cookieJarsAtom);
const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom);
const kv = useKeyValue<string[]>({
key: kvKey(activeWorkspace?.id ?? 'n/a'),
key: kvKey(activeWorkspaceId ?? 'n/a'),
namespace,
fallback,
});
@@ -37,7 +38,7 @@ export function useSubscribeRecentCookieJars() {
const key = kvKey(activeWorkspaceId);
const recentIds = await getKeyValue<string[]>({ namespace, key, fallback });
const recentIds = getKeyValue<string[]>({ namespace, key, fallback });
if (recentIds[0] === activeCookieJarId) return; // Short-circuit
const withoutActiveId = recentIds.filter((id) => id !== activeCookieJarId);

View File

@@ -1,9 +1,10 @@
import { useAtomValue } from 'jotai';
import { useEffect, useMemo } from 'react';
import { jotaiStore } from '../lib/jotai';
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
import { activeEnvironmentIdAtom } from './useActiveEnvironment';
import { activeWorkspaceIdAtom, useActiveWorkspace } from './useActiveWorkspace';
import { useEnvironments } from './useEnvironments';
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useEnvironmentsBreakdown } from './useEnvironmentsBreakdown';
import { useKeyValue } from './useKeyValue';
const kvKey = (workspaceId: string) => 'recent_environments::' + workspaceId;
@@ -11,8 +12,8 @@ const namespace = 'global';
const fallback: string[] = [];
export function useRecentEnvironments() {
const { subEnvironments } = useEnvironments();
const activeWorkspace = useActiveWorkspace();
const { subEnvironments } = useEnvironmentsBreakdown();
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
const kv = useKeyValue<string[]>({
key: kvKey(activeWorkspace?.id ?? 'n/a'),
namespace,
@@ -37,7 +38,7 @@ export function useSubscribeRecentEnvironments() {
const key = kvKey(activeWorkspaceId);
const recentIds = await getKeyValue<string[]>({ namespace, key, fallback });
const recentIds = getKeyValue<string[]>({ namespace, key, fallback });
if (recentIds[0] === activeEnvironmentId) return; // Short-circuit
const withoutActiveId = recentIds.filter((id) => id !== activeEnvironmentId);

View File

@@ -1,21 +1,22 @@
import { useAtomValue } from 'jotai';
import { useEffect, useMemo } from 'react';
import { jotaiStore } from '../lib/jotai';
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
import { activeRequestIdAtom } from './useActiveRequestId';
import { activeWorkspaceIdAtom, useActiveWorkspace } from './useActiveWorkspace';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useKeyValue } from './useKeyValue';
import { useRequests } from './useRequests';
import { useAllRequests } from './useAllRequests';
const kvKey = (workspaceId: string) => 'recent_requests::' + workspaceId;
const namespace = 'global';
const fallback: string[] = [];
export function useRecentRequests() {
const requests = useRequests();
const activeWorkspace = useActiveWorkspace();
const requests = useAllRequests();
const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom);
const { set: setRecentRequests, value: recentRequests } = useKeyValue<string[]>({
key: kvKey(activeWorkspace?.id ?? 'n/a'),
key: kvKey(activeWorkspaceId ?? 'n/a'),
namespace,
fallback,
});
@@ -38,7 +39,7 @@ export function useSubscribeRecentRequests() {
const key = kvKey(activeWorkspaceId);
const recentIds = await getKeyValue<string[]>({ namespace, key, fallback });
const recentIds = getKeyValue<string[]>({ namespace, key, fallback });
if (recentIds[0] === activeRequestId) return; // Short-circuit
const withoutActiveId = recentIds.filter((id) => id !== activeRequestId);

View File

@@ -1,16 +1,17 @@
import { workspacesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useEffect, useMemo } from 'react';
import { jotaiStore } from '../lib/jotai';
import { getKeyValue, setKeyValue } from '../lib/keyValueStore';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useKeyValue } from './useKeyValue';
import { useWorkspaces } from './useWorkspaces';
const kvKey = () => 'recent_workspaces';
const namespace = 'global';
const fallback: string[] = [];
export function useRecentWorkspaces() {
const workspaces = useWorkspaces();
const workspaces = useAtomValue(workspacesAtom);
const { value, isLoading } = useKeyValue<string[]>({ key: kvKey(), namespace, fallback });
const onlyValidIds = useMemo(
@@ -31,7 +32,7 @@ export function useSubscribeRecentWorkspaces() {
const key = kvKey();
const recentIds = await getKeyValue<string[]>({ namespace, key, fallback });
const recentIds = getKeyValue<string[]>({ namespace, key, fallback });
if (recentIds[0] === activeWorkspaceId) return; // Short-circuit
const withoutActiveId = recentIds.filter((id) => id !== activeWorkspaceId);

View File

@@ -1,46 +0,0 @@
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
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 updateHttpRequest = useUpdateAnyHttpRequest();
const updateGrpcRequest = useUpdateAnyGrpcRequest();
const requests = useRequests();
return useFastMutation({
mutationKey: ['rename_request'],
mutationFn: async () => {
const request = requests.find((r) => r.id === requestId);
if (request == null) return;
const name = await showPrompt({
id: 'rename-request',
title: 'Rename Request',
description:
request.name === '' ? (
'Enter a new name'
) : (
<>
Enter a new name for <InlineCode>{request.name}</InlineCode>
</>
),
label: 'Name',
placeholder: 'New Name',
defaultValue: request.name,
confirmText: 'Save',
});
if (name == null) return;
if (request.model === 'http_request') {
updateHttpRequest.mutate({ id: request.id, update: (r: HttpRequest) => ({ ...r, name }) });
} else {
updateGrpcRequest.mutate({ id: request.id, update: (r: GrpcRequest) => ({ ...r, name }) });
}
},
});
}

View File

@@ -1,10 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { useAtomValue } from 'jotai';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveWorkspace } from './useActiveWorkspace';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
export function useRenderTemplate(template: string) {
const workspaceId = useActiveWorkspace()?.id ?? 'n/a';
const workspaceId = useAtomValue(activeWorkspaceIdAtom) ?? 'n/a';
const environmentId = useActiveEnvironment()?.id ?? null;
return useQuery<string>({
placeholderData: (prev) => prev, // Keep previous data on refetch

View File

@@ -1,16 +1,32 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import type { ModelPayload } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
import { generateId } from '../lib/generateId';
import { jotaiStore } from '../lib/jotai';
const keyAtom = atom<Record<string, string>>({});
const requestUpdateKeyAtom = atom<Record<string, string>>({});
getCurrentWebviewWindow()
.listen<ModelPayload>('upserted_model', ({ payload }) => {
if (
(payload.model.model === 'http_request' ||
payload.model.model === 'grpc_request' ||
payload.model.model === 'websocket_request') &&
((payload.updateSource.type === 'window' &&
payload.updateSource.label !== getCurrentWebviewWindow().label) ||
payload.updateSource.type !== 'window')
) {
wasUpdatedExternally(payload.model.id);
}
})
.catch(console.error);
export function wasUpdatedExternally(changedRequestId: string) {
jotaiStore.set(requestUpdateKeyAtom, (m) => ({ ...m, [changedRequestId]: generateId() }));
}
export function useRequestUpdateKey(requestId: string | null) {
const keys = useAtomValue(keyAtom);
const keys = useAtomValue(requestUpdateKeyAtom);
const key = keys[requestId ?? 'n/a'];
return {
updateKey: `${requestId}::${key ?? 'default'}`,
wasUpdatedExternally: (changedRequestId: string) => {
jotaiStore.set(keyAtom, (m) => ({ ...m, [changedRequestId]: generateId() }));
},
};
return `${requestId}::${key ?? 'default'}`;
}

View File

@@ -1,14 +0,0 @@
import { atom, useAtomValue } from 'jotai';
import { grpcRequestsAtom } from './useGrpcRequests';
import { httpRequestsAtom } from './useHttpRequests';
import { websocketRequestsAtom } from './useWebsocketRequests';
export const requestsAtom = atom((get) => [
...get(httpRequestsAtom),
...get(grpcRequestsAtom),
...get(websocketRequestsAtom),
]);
export function useRequests() {
return useAtomValue(requestsAtom);
}

View File

@@ -1,9 +1,10 @@
import { settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { resolveAppearance } from '../lib/theme/appearance';
import { usePreferredAppearance } from './usePreferredAppearance';
import { useSettings } from './useSettings';
export function useResolvedAppearance() {
const preferredAppearance = usePreferredAppearance();
const settings = useSettings();
const settings = useAtomValue(settingsAtom);
return resolveAppearance(preferredAppearance, settings.appearance);
}

View File

@@ -1,10 +1,11 @@
import { settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { getResolvedTheme } from '../lib/theme/themes';
import { usePreferredAppearance } from './usePreferredAppearance';
import { useSettings } from './useSettings';
export function useResolvedTheme() {
const preferredAppearance = usePreferredAppearance();
const settings = useSettings();
const settings = useAtomValue(settingsAtom);
return getResolvedTheme(
preferredAppearance,
settings.appearance,

View File

@@ -1,5 +1,6 @@
import { save } from '@tauri-apps/plugin-dialog';
import type { HttpResponse } from '@yaakapp-internal/models';
import { getModel } from '@yaakapp-internal/models';
import mime from 'mime';
import slugify from 'slugify';
import { InlineCode } from '../components/core/InlineCode';
@@ -7,13 +8,12 @@ import { getContentTypeFromHeaders } from '../lib/model_util';
import { invokeCmd } from '../lib/tauri';
import { showToast } from '../lib/toast';
import { useFastMutation } from './useFastMutation';
import { getHttpRequest } from './useHttpRequests';
export function useSaveResponse(response: HttpResponse) {
return useFastMutation({
mutationKey: ['save_response', response.id],
mutationFn: async () => {
const request = getHttpRequest(response.requestId);
const request = getModel('http_request', response.requestId);
if (request == null) return null;
const contentType = getContentTypeFromHeaders(response.headers) ?? 'unknown';

View File

@@ -1,15 +1,15 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import { getModel } from '@yaakapp-internal/models';
import { invokeCmd } from '../lib/tauri';
import { getActiveCookieJar } from './useActiveCookieJar';
import { getActiveEnvironment } from './useActiveEnvironment';
import { useFastMutation } from './useFastMutation';
import { getHttpRequest } from './useHttpRequests';
export function useSendAnyHttpRequest() {
return useFastMutation<HttpResponse | null, string, string | null>({
mutationKey: ['send_any_request'],
mutationFn: async (id) => {
const request = getHttpRequest(id ?? 'n/a');
const request = getModel('http_request', id ?? 'n/a');
if (request == null) {
return null;
}

View File

@@ -1,16 +0,0 @@
import type { Settings } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import {jotaiStore} from "../lib/jotai";
import { invokeCmd } from '../lib/tauri';
const settings = await invokeCmd<Settings>('cmd_get_settings');
export const settingsAtom = atom<Settings>(settings);
export function useSettings() {
return useAtomValue(settingsAtom);
}
export function getSettings() {
return jotaiStore.get(settingsAtom);
}

View File

@@ -1,11 +1,12 @@
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAtomValue } from 'jotai';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useKeyValue } from './useKeyValue';
export function useSidebarHidden() {
const activeWorkspace = useActiveWorkspace();
const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom);
const { set, value } = useKeyValue<boolean>({
namespace: 'no_sync',
key: ['sidebar_hidden', activeWorkspace?.id ?? 'n/a'],
key: ['sidebar_hidden', activeWorkspaceId ?? 'n/a'],
fallback: false,
});

View File

@@ -1,8 +1,9 @@
import { keyValuesAtom } from '@yaakapp-internal/models';
import { useCallback, useEffect, useState } from 'react';
import { jotaiStore } from '../lib/jotai';
import { setKeyValue } from '../lib/keyValueStore';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { getKeyValue, keyValuesAtom } from './useKeyValue';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { getKeyValue } from './useKeyValue';
function kvKey(workspaceId: string | null) {
return ['sidebar_collapsed', workspaceId ?? 'n/a'];
@@ -22,7 +23,7 @@ export function useSidebarItemCollapsed(itemId: string) {
const toggle = useCallback(() => {
setKeyValue({
key: kvKey(getActiveWorkspaceId()),
key: kvKey(jotaiStore.get(activeWorkspaceIdAtom)),
namespace: 'no_sync',
value: { ...getSidebarCollapsedMap(), [itemId]: !isCollapsed },
}).catch(console.error);
@@ -32,11 +33,8 @@ export function useSidebarItemCollapsed(itemId: string) {
}
export function getSidebarCollapsedMap() {
const activeWorkspaceId = getActiveWorkspaceId();
if (activeWorkspaceId == null) return {};
const value = getKeyValue<Record<string, boolean>>({
key: kvKey(activeWorkspaceId),
key: kvKey(jotaiStore.get(activeWorkspaceIdAtom)),
fallback: {},
namespace: 'no_sync',
});

View File

@@ -1,11 +1,12 @@
import { useAtomValue } from 'jotai';
import { useCallback, useMemo } from 'react';
import { useLocalStorage } from 'react-use';
import { useActiveWorkspace } from './useActiveWorkspace';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
export function useSidebarWidth() {
const activeWorkspace = useActiveWorkspace();
const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom);
const [width, setWidth] = useLocalStorage<number>(
`sidebar_width::${activeWorkspace?.id ?? 'n/a'}`,
`sidebar_width::${activeWorkspaceId ?? 'n/a'}`,
250,
);
const resetWidth = useCallback(() => setWidth(250), [setWidth]);

View File

@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
/**
* Like useState, except it will update the value when the default value changes
*/
export function useStateWithDeps<T>(defaultValue: T, deps: DependencyList) {
export function useStateWithDeps<T>(defaultValue: T | (() => T), deps: DependencyList) {
const [value, setValue] = useState(defaultValue);
useEffect(() => {
setValue(defaultValue);

View File

@@ -1,9 +1,10 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useEffect } from 'react';
import { useSettings } from './useSettings';
export function useSyncFontSizeSetting() {
const settings = useSettings();
const settings = useAtomValue(settingsAtom);
useEffect(() => {
if (settings == null) {
return;

View File

@@ -1,202 +0,0 @@
import deepEqual from '@gilbarbara/deep-equal';
import { useQueryClient } from '@tanstack/react-query';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import type { AnyModel, KeyValue, ModelPayload } from '@yaakapp-internal/models';
import { jotaiStore } from '../lib/jotai';
import { buildKeyValueKey } from '../lib/keyValueStore';
import { modelsEq } from '../lib/model_util';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { cookieJarsAtom } from './useCookieJars';
import { environmentsAtom } from './useEnvironments';
import { foldersAtom } from './useFolders';
import { grpcConnectionsAtom } from './useGrpcConnections';
import { grpcEventsQueryKey } from './useGrpcEvents';
import { grpcRequestsAtom } from './useGrpcRequests';
import { httpRequestsAtom } from './useHttpRequests';
import { httpResponsesAtom } from './useHttpResponses';
import { keyValueQueryKey, keyValuesAtom } from './useKeyValue';
import { useListenToTauriEvent } from './useListenToTauriEvent';
import { pluginsAtom } from './usePlugins';
import { useRequestUpdateKey } from './useRequestUpdateKey';
import { settingsAtom } from './useSettings';
import { websocketConnectionsAtom } from './useWebsocketConnections';
import { websocketEventsQueryKey } from './useWebsocketEvents';
import { websocketRequestsAtom } from './useWebsocketRequests';
import { workspaceMetaAtom } from './useWorkspaceMeta';
import { workspacesAtom } from './useWorkspaces';
export function useSyncModelStores() {
const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
useListenToTauriEvent<ModelPayload>('upserted_model', ({ payload }) => {
const queryKey =
payload.model.model === 'grpc_event'
? grpcEventsQueryKey(payload.model)
: payload.model.model === 'websocket_event'
? websocketEventsQueryKey(payload.model)
: payload.model.model === 'key_value'
? keyValueQueryKey(payload.model)
: null;
// TODO: Move this logic to useRequestEditor() hook
if (
(payload.model.model === 'http_request' ||
payload.model.model === 'grpc_request' ||
payload.model.model === 'websocket_request') &&
((payload.updateSource.type === 'window' &&
payload.updateSource.label !== getCurrentWebviewWindow().label) ||
payload.updateSource.type !== 'window')
) {
wasUpdatedExternally(payload.model.id);
}
if (shouldIgnoreModel(payload)) return;
if (payload.model.model === 'workspace') {
jotaiStore.set(workspacesAtom, updateModelList(payload.model));
} else if (payload.model.model === 'workspace_meta') {
jotaiStore.set(workspaceMetaAtom, payload.model);
} else if (payload.model.model === 'plugin') {
jotaiStore.set(pluginsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'http_request') {
jotaiStore.set(httpRequestsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'folder') {
jotaiStore.set(foldersAtom, updateModelList(payload.model));
} else if (payload.model.model === 'http_response') {
jotaiStore.set(httpResponsesAtom, updateModelList(payload.model));
} else if (payload.model.model === 'grpc_request') {
jotaiStore.set(grpcRequestsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'websocket_request') {
jotaiStore.set(websocketRequestsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'websocket_connection') {
jotaiStore.set(websocketConnectionsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'grpc_connection') {
jotaiStore.set(grpcConnectionsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'environment') {
jotaiStore.set(environmentsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'cookie_jar') {
jotaiStore.set(cookieJarsAtom, updateModelList(payload.model));
} else if (payload.model.model === 'settings') {
jotaiStore.set(settingsAtom, payload.model);
} else if (payload.model.model === 'key_value') {
jotaiStore.set(keyValuesAtom, updateModelList(payload.model));
} else if (queryKey != null) {
// TODO: Convert all models to use Jotai
queryClient.setQueryData(queryKey, (current: unknown) => {
if (Array.isArray(current)) {
return updateModelList(payload.model)(current);
}
});
}
});
useListenToTauriEvent<ModelPayload>('deleted_model', ({ payload }) => {
if (shouldIgnoreModel(payload)) return;
console.log('Delete model', payload);
if (payload.model.model === 'workspace') {
jotaiStore.set(workspacesAtom, removeModelById(payload.model));
} else if (payload.model.model === 'plugin') {
jotaiStore.set(pluginsAtom, removeModelById(payload.model));
} else if (payload.model.model === 'http_request') {
jotaiStore.set(httpRequestsAtom, removeModelById(payload.model));
} else if (payload.model.model === 'http_response') {
jotaiStore.set(httpResponsesAtom, removeModelById(payload.model));
} else if (payload.model.model === 'folder') {
jotaiStore.set(foldersAtom, removeModelById(payload.model));
} else if (payload.model.model === 'environment') {
jotaiStore.set(environmentsAtom, removeModelById(payload.model));
} else if (payload.model.model === 'grpc_request') {
jotaiStore.set(grpcRequestsAtom, removeModelById(payload.model));
} else if (payload.model.model === 'websocket_request') {
jotaiStore.set(websocketRequestsAtom, removeModelById(payload.model));
} else if (payload.model.model === 'websocket_connection') {
jotaiStore.set(websocketConnectionsAtom, removeModelById(payload.model));
} else if (payload.model.model === 'websocket_event') {
queryClient.setQueryData(
websocketEventsQueryKey(payload.model),
removeModelById(payload.model),
);
} else if (payload.model.model === 'grpc_connection') {
jotaiStore.set(grpcConnectionsAtom, removeModelById(payload.model));
} else if (payload.model.model === 'grpc_event') {
queryClient.setQueryData(grpcEventsQueryKey(payload.model), removeModelById(payload.model));
} else if (payload.model.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(payload.model), removeModelByKv(payload.model));
} else if (payload.model.model === 'cookie_jar') {
jotaiStore.set(cookieJarsAtom, removeModelById(payload.model));
}
});
}
function updateModelList<T extends AnyModel>(model: T) {
// Mark these models as DESC instead of ASC
const pushToFront =
model.model === 'http_response' ||
model.model === 'grpc_connection' ||
model.model === 'websocket_connection';
return (current: T[] | undefined | null): T[] => {
const index = current?.findIndex((v) => modelsEq(v, model)) ?? -1;
const existingModel = current?.[index];
if (existingModel && deepEqual(existingModel, model)) {
// We already have the exact model, so do nothing
return current;
} else if (existingModel) {
return [...(current ?? []).slice(0, index), model, ...(current ?? []).slice(index + 1)];
} else {
return pushToFront ? [model, ...(current ?? [])] : [...(current ?? []), model];
}
};
}
function removeModelById<T extends { id: string }>(model: T) {
return (prevEntries: T[] | undefined) => {
const entries = prevEntries?.filter((e) => e.id !== model.id) ?? [];
// Don't trigger an update if we didn't remove anything
if (entries.length === (prevEntries ?? []).length) {
return prevEntries ?? [];
}
return entries;
};
}
function removeModelByKv(model: KeyValue) {
return (prevEntries: KeyValue[] | undefined) =>
prevEntries?.filter(
(e) =>
!(
e.namespace === model.namespace &&
buildKeyValueKey(e.key) === buildKeyValueKey(model.key) &&
e.value == model.value
),
) ?? [];
}
function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
// Never ignore updates from non-user sources
if (updateSource.type !== 'window') {
return false;
}
// Never ignore same-window updates
if (updateSource.label === getCurrentWebviewWindow().label) {
return false;
}
const activeWorkspaceId = getActiveWorkspaceId();
// Only sync models that belong to this workspace, if a workspace ID is present
if ('workspaceId' in model && model.workspaceId !== activeWorkspaceId) {
return;
}
if (model.model === 'key_value') {
return model.namespace === 'no_sync';
}
return false;
}

View File

@@ -1,19 +1,7 @@
import {listWebsocketConnections, listWebsocketRequests} from '@yaakapp-internal/ws';
import { changeModelStoreWorkspace } from '@yaakapp-internal/models';
import { useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';
import { invokeCmd } from '../lib/tauri';
import { activeWorkspaceIdAtom, getActiveWorkspaceId } from './useActiveWorkspace';
import { cookieJarsAtom } from './useCookieJars';
import { environmentsAtom } from './useEnvironments';
import { foldersAtom } from './useFolders';
import { grpcConnectionsAtom } from './useGrpcConnections';
import { grpcRequestsAtom } from './useGrpcRequests';
import { httpRequestsAtom } from './useHttpRequests';
import { httpResponsesAtom } from './useHttpResponses';
import { keyValuesAtom } from './useKeyValue';
import {websocketConnectionsAtom} from "./useWebsocketConnections";
import { websocketRequestsAtom } from './useWebsocketRequests';
import { workspaceMetaAtom } from './useWorkspaceMeta';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
export function useSyncWorkspaceChildModels() {
useEffect(() => {
@@ -24,27 +12,6 @@ export function useSyncWorkspaceChildModels() {
}
async function sync() {
// Doesn't need a workspace ID, so sync it right away
jotaiStore.set(keyValuesAtom, await invokeCmd('cmd_list_key_values'));
const workspaceId = getActiveWorkspaceId();
if (workspaceId == null) return;
const args = { workspaceId };
// Set the things we need first, first
jotaiStore.set(httpRequestsAtom, await invokeCmd('cmd_list_http_requests', args));
jotaiStore.set(grpcRequestsAtom, await invokeCmd('cmd_list_grpc_requests', args));
jotaiStore.set(foldersAtom, await invokeCmd('cmd_list_folders', args));
jotaiStore.set(websocketRequestsAtom, await listWebsocketRequests(args));
// Then, set the rest
jotaiStore.set(cookieJarsAtom, await invokeCmd('cmd_list_cookie_jars', args));
jotaiStore.set(httpResponsesAtom, await invokeCmd('cmd_list_http_responses', args));
jotaiStore.set(grpcConnectionsAtom, await invokeCmd('cmd_list_grpc_connections', args));
jotaiStore.set(websocketConnectionsAtom, await listWebsocketConnections(args));
jotaiStore.set(environmentsAtom, await invokeCmd('cmd_list_environments', args));
// Single models
jotaiStore.set(workspaceMetaAtom, await invokeCmd('cmd_get_workspace_meta', { workspaceId }));
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom) ?? null;
changeModelStoreWorkspace(workspaceId).catch(console.error);
}

View File

@@ -1,15 +1,17 @@
import { emit } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { useAtomValue } from 'jotai';
import { useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';
import { resolvedModelName } from '../lib/resolvedModelName';
import { useActiveEnvironment } from './useActiveEnvironment';
import { getActiveRequest } from './useActiveRequest';
import { useActiveWorkspace } from './useActiveWorkspace';
import { activeRequestAtom } from './useActiveRequest';
import { activeWorkspaceAtom } from './useActiveWorkspace';
import { useAppInfo } from './useAppInfo';
import { useOsInfo } from './useOsInfo';
export function useSyncWorkspaceRequestTitle() {
const activeWorkspace = useActiveWorkspace();
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
const activeEnvironment = useActiveEnvironment();
const osInfo = useOsInfo();
const appInfo = useAppInfo();
@@ -23,7 +25,7 @@ export function useSyncWorkspaceRequestTitle() {
if (activeEnvironment) {
newTitle += ` [${activeEnvironment.name}]`;
}
const activeRequest = getActiveRequest();
const activeRequest = jotaiStore.get(activeRequestAtom);
if (activeRequest) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
newTitle += ` ${resolvedModelName(activeRequest)}`;

View File

@@ -1,18 +0,0 @@
import type { Folder } from '@yaakapp-internal/models';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
import { getFolder } from './useFolders';
export function useUpdateAnyFolder() {
return useFastMutation<Folder, unknown, { id: string; update: (r: Folder) => Folder }>({
mutationKey: ['update_any_folder'],
mutationFn: async ({ id, update }) => {
const folder = getFolder(id);
if (folder === null) {
throw new Error("Can't update a null folder");
}
return invokeCmd<Folder>('cmd_update_folder', { folder: update(folder) });
},
});
}

View File

@@ -1,24 +0,0 @@
import type { GrpcRequest } from '@yaakapp-internal/models';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
import { getGrpcRequest } from './useGrpcRequests';
export function useUpdateAnyGrpcRequest() {
return useFastMutation<
GrpcRequest,
unknown,
{ id: string; update: Partial<GrpcRequest> | ((r: GrpcRequest) => GrpcRequest) }
>({
mutationKey: ['update_any_grpc_request'],
mutationFn: async ({ id, update }) => {
const request = getGrpcRequest(id);
if (request === null) {
throw new Error("Can't update a null request");
}
const patchedRequest =
typeof update === 'function' ? update(request) : { ...request, ...update };
return invokeCmd<GrpcRequest>('cmd_update_grpc_request', { request: patchedRequest });
},
});
}

View File

@@ -1,24 +0,0 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { upsertAnyModel } from '@yaakapp-internal/models';
import { useFastMutation } from './useFastMutation';
import { getHttpRequest } from './useHttpRequests';
export function useUpdateAnyHttpRequest() {
return useFastMutation<
void,
unknown,
{ id: string; update: Partial<HttpRequest> | ((r: HttpRequest) => HttpRequest) }
>({
mutationKey: ['update_any_http_request'],
mutationFn: async ({ id, update }) => {
const request = getHttpRequest(id);
if (request === null) {
throw new Error("Can't update a null request");
}
const patchedRequest =
typeof update === 'function' ? update(request) : { ...request, ...update };
await upsertAnyModel(patchedRequest);
},
});
}

View File

@@ -1,19 +0,0 @@
import type { CookieJar } from '@yaakapp-internal/models';
import { invokeCmd } from '../lib/tauri';
import { getCookieJar } from './useCookieJars';
import { useFastMutation } from './useFastMutation';
export function useUpdateCookieJar(id: string | null) {
return useFastMutation<CookieJar, unknown, Partial<CookieJar> | ((j: CookieJar) => CookieJar)>({
mutationKey: ['update_cookie_jar', id],
mutationFn: async (v) => {
const cookieJar = getCookieJar(id);
if (cookieJar == null) {
throw new Error("Can't update a null workspace");
}
const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v };
return invokeCmd<CookieJar>('cmd_update_cookie_jar', { cookieJar: newCookieJar });
},
});
}

View File

@@ -1,23 +0,0 @@
import type { Environment } from '@yaakapp-internal/models';
import { invokeCmd } from '../lib/tauri';
import { getEnvironment } from './useEnvironments';
import { useFastMutation } from './useFastMutation';
export function useUpdateEnvironment(id: string | null) {
return useFastMutation<
Environment,
unknown,
Partial<Environment> | ((r: Environment) => Environment)
>({
mutationKey: ['update_environment', id],
mutationFn: async (v) => {
const environment = getEnvironment(id);
if (environment == null) {
throw new Error("Can't update a null environment");
}
const newEnvironment = typeof v === 'function' ? v(environment) : { ...environment, ...v };
return invokeCmd<Environment>('cmd_update_environment', { environment: newEnvironment });
},
});
}

View File

@@ -1,20 +0,0 @@
import type { Settings } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
import { getSettings, settingsAtom } from './useSettings';
export function useUpdateSettings() {
const setSettings = useSetAtom(settingsAtom);
return useFastMutation<Settings, unknown, Partial<Settings>>({
mutationKey: ['update_settings'],
mutationFn: async (patch) => {
const settings = getSettings();
const newSettings: Settings = { ...settings, ...patch };
return invokeCmd<Settings>('cmd_update_settings', { settings: newSettings });
},
onSuccess: (settings) => {
setSettings(settings);
},
});
}

View File

@@ -1,17 +0,0 @@
import type { WebsocketConnection } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
import { jotaiStore } from '../lib/jotai';
export const websocketConnectionsAtom = atom<WebsocketConnection[]>([]);
export function useWebsocketConnections() {
return useAtomValue(websocketConnectionsAtom);
}
export function useLatestWebsocketConnection(requestId: string | null): WebsocketConnection | null {
return useWebsocketConnections().find((r) => r.requestId === requestId) ?? null;
}
export function getWebsocketConnection(id: string) {
return jotaiStore.get(websocketConnectionsAtom).find((r) => r.id === id) ?? null;
}

View File

@@ -1,21 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import type { WebsocketEvent } from '@yaakapp-internal/models';
import { listWebsocketEvents } from '@yaakapp-internal/ws';
export function websocketEventsQueryKey({ connectionId }: { connectionId: string }) {
return ['websocket_events', { connectionId }];
}
export function useWebsocketEvents(connectionId: string | null) {
return (
useQuery<WebsocketEvent[]>({
enabled: connectionId !== null,
initialData: [],
queryKey: websocketEventsQueryKey({ connectionId: connectionId ?? 'n/a' }),
queryFn: () => {
if (connectionId == null) return [] as WebsocketEvent[];
return listWebsocketEvents({ connectionId });
},
}).data ?? []
);
}

View File

@@ -1,13 +0,0 @@
import type { WebsocketRequest } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
import { jotaiStore } from '../lib/jotai';
export const websocketRequestsAtom = atom<WebsocketRequest[]>([]);
export function useWebsocketRequests() {
return useAtomValue(websocketRequestsAtom);
}
export function getWebsocketRequest(id: string) {
return jotaiStore.get(websocketRequestsAtom).find((r) => r.id === id) ?? null;
}

View File

@@ -1,13 +0,0 @@
import type { WorkspaceMeta } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
import { jotaiStore } from '../lib/jotai';
export const workspaceMetaAtom = atom<WorkspaceMeta | null>(null);
export function useWorkspaceMeta() {
return useAtomValue(workspaceMetaAtom);
}
export function getWorkspaceMeta() {
return jotaiStore.get(workspaceMetaAtom);
}

View File

@@ -1,20 +0,0 @@
import type { Workspace } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai';
import { jotaiStore } from '../lib/jotai';
import { invokeCmd } from '../lib/tauri';
export const workspacesAtom = atom<Workspace[]>(
await invokeCmd<Workspace[]>('cmd_list_workspaces'),
);
export const sortedWorkspacesAtom = atom((get) =>
get(workspacesAtom).sort((a, b) => a.name.localeCompare(b.name)),
);
export function useWorkspaces() {
return useAtomValue(sortedWorkspacesAtom);
}
export function getWorkspace(id: string | null) {
return jotaiStore.get(workspacesAtom).find((v) => v.id === id) ?? null;
}

View File

@@ -1,28 +1,27 @@
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { useSettings } from './useSettings';
import { useUpdateSettings } from './useUpdateSettings';
export function useZoom() {
const settings = useSettings();
const updateSettings = useUpdateSettings();
const settings = useAtomValue(settingsAtom);
const zoomIn = useCallback(() => {
const zoomIn = useCallback(async () => {
if (!settings) return;
updateSettings.mutate({
await patchModel(settings, {
interfaceScale: Math.min(1.8, settings.interfaceScale * 1.1),
});
}, [settings, updateSettings]);
}, [settings]);
const zoomOut = useCallback(() => {
const zoomOut = useCallback(async () => {
if (!settings) return;
updateSettings.mutate({
await patchModel(settings, {
interfaceScale: Math.max(0.4, settings.interfaceScale * 0.9),
});
}, [settings, updateSettings]);
}, [settings]);
const zoomReset = useCallback(() => {
updateSettings.mutate({ ...settings, interfaceScale: 1 });
}, [settings, updateSettings]);
const zoomReset = useCallback(async () => {
await patchModel(settings, { interfaceScale: 1 });
}, [settings]);
return { zoomIn, zoomOut, zoomReset };
}