diff --git a/package-lock.json b/package-lock.json index 71c4c501..8ff7d9f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2597,9 +2597,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.59.6", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.6.tgz", - "integrity": "sha512-g58YTHe4ClRrjJ50GY9fas/0zARJVozY0Hs+hcSBOmwZaeKY+to0/LX8wKnnH/EJiLYcC1sHmE11CAS3ncfZBg==", + "version": "5.59.16", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.16.tgz", + "integrity": "sha512-crHn+G3ltqb5JG0oUv6q+PMz1m1YkjpASrXTU+sYWW9pLk0t2GybUHNRqYPZWhxgjPaVGC4yp92gSFEJgYEsPw==", "license": "MIT", "funding": { "type": "github", @@ -2618,12 +2618,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.59.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.6.tgz", - "integrity": "sha512-sGg2sNKg8cYf6aS1dzDf4weN+Vt9PfUu+0btwerrbtYysdNBbcGD4rPe9jhPgMtpDDlvi4cbLv+j1Qo814Kf+Q==", + "version": "5.59.16", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.16.tgz", + "integrity": "sha512-MuyWheG47h6ERd4PKQ6V8gDyBu3ThNG22e1fRVwvq6ap3EqsFhyuxCAwhNP/03m/mLg+DAb0upgbPaX6VB+CkQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.59.6" + "@tanstack/query-core": "5.59.16" }, "funding": { "type": "github", @@ -13767,7 +13767,7 @@ "@lezer/lr": "^1.3.3", "@react-hook/resize-observer": "^2.0.2", "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^5.55.4", + "@tanstack/react-query": "^5.59.16", "@tanstack/react-virtual": "^3.10.8", "@tauri-apps/api": "^2.0.1", "@tauri-apps/plugin-clipboard-manager": "^2.0.0", diff --git a/src-web/components/EnvironmentEditDialog.tsx b/src-web/components/EnvironmentEditDialog.tsx index 4fa75012..0e7b6d50 100644 --- a/src-web/components/EnvironmentEditDialog.tsx +++ b/src-web/components/EnvironmentEditDialog.tsx @@ -45,6 +45,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) { const handleCreateEnvironment = async () => { const e = await createEnvironment.mutateAsync(); + if (e == null) return; setSelectedEnvironmentId(e.id); }; diff --git a/src-web/components/RecentResponsesDropdown.tsx b/src-web/components/RecentResponsesDropdown.tsx index 4f40e7ec..9879122b 100644 --- a/src-web/components/RecentResponsesDropdown.tsx +++ b/src-web/components/RecentResponsesDropdown.tsx @@ -41,7 +41,7 @@ export const RecentResponsesDropdown = function ResponsePane({ }, { key: 'copy', - label: 'Copy to Clipboard', + label: 'Copy Body', onSelect: copyResponse.mutate, leftSlot: , hidden: responses.length === 0, diff --git a/src-web/hooks/useCreateCookieJar.ts b/src-web/hooks/useCreateCookieJar.ts index 2a1f6879..e1793ced 100644 --- a/src-web/hooks/useCreateCookieJar.ts +++ b/src-web/hooks/useCreateCookieJar.ts @@ -1,15 +1,19 @@ import { useMutation } from '@tanstack/react-query'; import type { CookieJar } from '@yaakapp-internal/models'; +import {useSetAtom} from "jotai"; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; import { useActiveWorkspace } from './useActiveWorkspace'; +import {cookieJarsAtom} from "./useCookieJars"; import { usePrompt } from './usePrompt'; +import {updateModelList} from "./useSyncModelStores"; export function useCreateCookieJar() { const workspace = useActiveWorkspace(); const prompt = usePrompt(); + const setCookieJars = useSetAtom(cookieJarsAtom); - return useMutation({ + return useMutation({ mutationKey: ['create_cookie_jar'], mutationFn: async () => { if (workspace === null) { @@ -23,8 +27,16 @@ export function useCreateCookieJar() { label: 'Name', defaultValue: 'My Jar', }); + if (name == null) return null; + return invokeCmd('cmd_create_cookie_jar', { workspaceId: workspace.id, name }); }, + onSuccess: (cookieJar) => { + if (cookieJar == null) return; + + // Optimistic update + setCookieJars(updateModelList(cookieJar)); + }, onSettled: () => trackEvent('cookie_jar', 'create'), }); } diff --git a/src-web/hooks/useCreateEnvironment.ts b/src-web/hooks/useCreateEnvironment.ts index f5738334..31b74723 100644 --- a/src-web/hooks/useCreateEnvironment.ts +++ b/src-web/hooks/useCreateEnvironment.ts @@ -1,17 +1,21 @@ import { useMutation } from '@tanstack/react-query'; import type { Environment } from '@yaakapp-internal/models'; +import {useSetAtom} from "jotai"; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; import { useActiveEnvironment } from './useActiveEnvironment'; import { useActiveWorkspace } from './useActiveWorkspace'; +import {environmentsAtom} from "./useEnvironments"; import { usePrompt } from './usePrompt'; +import {updateModelList} from "./useSyncModelStores"; export function useCreateEnvironment() { const [, setActiveEnvironmentId] = useActiveEnvironment(); const prompt = usePrompt(); const workspace = useActiveWorkspace(); + const setEnvironments = useSetAtom(environmentsAtom); - return useMutation({ + return useMutation({ mutationKey: ['create_environment'], mutationFn: async () => { const name = await prompt({ @@ -23,6 +27,8 @@ export function useCreateEnvironment() { defaultValue: 'My Environment', confirmText: 'Create', }); + if (name == null) return null; + return invokeCmd('cmd_create_environment', { name, variables: [], @@ -31,7 +37,11 @@ export function useCreateEnvironment() { }, onSettled: () => trackEvent('environment', 'create'), onSuccess: async (environment) => { - if (workspace == null) return; + if (environment == null) return; + + // Optimistic update + setEnvironments(updateModelList(environment)); + setActiveEnvironmentId(environment.id); }, }); diff --git a/src-web/hooks/useCreateFolder.ts b/src-web/hooks/useCreateFolder.ts index 7b6749c8..3804add4 100644 --- a/src-web/hooks/useCreateFolder.ts +++ b/src-web/hooks/useCreateFolder.ts @@ -1,15 +1,23 @@ import { useMutation } from '@tanstack/react-query'; import type { Folder } from '@yaakapp-internal/models'; +import { useSetAtom } from 'jotai'; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; import { useActiveWorkspace } from './useActiveWorkspace'; +import { foldersAtom } from './useFolders'; import { usePrompt } from './usePrompt'; +import { updateModelList } from './useSyncModelStores'; export function useCreateFolder() { const workspace = useActiveWorkspace(); const prompt = usePrompt(); + const setFolders = useSetAtom(foldersAtom); - return useMutation>>({ + return useMutation< + Folder | null, + unknown, + Partial> + >({ mutationKey: ['create_folder'], mutationFn: async (patch) => { if (workspace === null) { @@ -25,14 +33,19 @@ export function useCreateFolder() { confirmText: 'Create', placeholder: 'Name', }); - if (name == null) { - return; - } + if (name == null) return null; + patch.name = name; } patch.sortPriority = patch.sortPriority || -Date.now(); - await invokeCmd('cmd_create_folder', { workspaceId: workspace.id, ...patch }); + return await invokeCmd('cmd_create_folder', { workspaceId: workspace.id, ...patch }); + }, + onSuccess: (folder) => { + if (folder == null) return; + + // Optimistic update + setFolders(updateModelList(folder)); }, onSettled: () => trackEvent('folder', 'create'), }); diff --git a/src-web/hooks/useCreateGrpcRequest.ts b/src-web/hooks/useCreateGrpcRequest.ts index fdc177bb..5571601e 100644 --- a/src-web/hooks/useCreateGrpcRequest.ts +++ b/src-web/hooks/useCreateGrpcRequest.ts @@ -1,17 +1,21 @@ import { useMutation } from '@tanstack/react-query'; import type { GrpcRequest } from '@yaakapp-internal/models'; +import {useSetAtom} from "jotai"; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; import { useActiveEnvironment } from './useActiveEnvironment'; import { useActiveRequest } from './useActiveRequest'; import { useActiveWorkspace } from './useActiveWorkspace'; import { useAppRoutes } from './useAppRoutes'; +import {grpcRequestsAtom} from "./useGrpcRequests"; +import {updateModelList} from "./useSyncModelStores"; export function useCreateGrpcRequest() { const workspace = useActiveWorkspace(); const [activeEnvironment] = useActiveEnvironment(); const activeRequest = useActiveRequest(); const routes = useAppRoutes(); + const setGrpcRequests = useSetAtom(grpcRequestsAtom); return useMutation< GrpcRequest, @@ -33,19 +37,17 @@ export function useCreateGrpcRequest() { } } patch.folderId = patch.folderId || activeRequest?.folderId; - const request = await invokeCmd('cmd_create_grpc_request', { + return invokeCmd('cmd_create_grpc_request', { workspaceId: workspace.id, name: '', ...patch, }); - - // Give some time for the workspace to sync to the local store - await new Promise((resolve) => setTimeout(resolve, 100)); - - return request; }, onSettled: () => trackEvent('grpc_request', 'create'), onSuccess: async (request) => { + // Optimistic update + setGrpcRequests(updateModelList(request)); + routes.navigate('request', { workspaceId: request.workspaceId, requestId: request.id, diff --git a/src-web/hooks/useCreateHttpRequest.ts b/src-web/hooks/useCreateHttpRequest.ts index 13693895..809fe1c7 100644 --- a/src-web/hooks/useCreateHttpRequest.ts +++ b/src-web/hooks/useCreateHttpRequest.ts @@ -1,17 +1,21 @@ import { useMutation } from '@tanstack/react-query'; import type { HttpRequest } from '@yaakapp-internal/models'; +import { useSetAtom } from 'jotai/index'; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; import { useActiveEnvironment } from './useActiveEnvironment'; import { useActiveRequest } from './useActiveRequest'; import { useActiveWorkspace } from './useActiveWorkspace'; import { useAppRoutes } from './useAppRoutes'; +import { httpRequestsAtom } from './useHttpRequests'; +import { updateModelList } from './useSyncModelStores'; export function useCreateHttpRequest() { const workspace = useActiveWorkspace(); const [activeEnvironment] = useActiveEnvironment(); const activeRequest = useActiveRequest(); const routes = useAppRoutes(); + const setHttpRequests = useSetAtom(httpRequestsAtom); return useMutation>({ mutationKey: ['create_http_request'], @@ -29,17 +33,15 @@ export function useCreateHttpRequest() { } } patch.folderId = patch.folderId || activeRequest?.folderId; - const request = await invokeCmd('cmd_create_http_request', { + return invokeCmd('cmd_create_http_request', { request: { workspaceId: workspace.id, ...patch }, }); - - // Give some time for the workspace to sync to the local store - await new Promise((resolve) => setTimeout(resolve, 100)); - - return request; }, onSettled: () => trackEvent('http_request', 'create'), onSuccess: async (request) => { + // Optimistic update + setHttpRequests(updateModelList(request)); + routes.navigate('request', { workspaceId: request.workspaceId, requestId: request.id, diff --git a/src-web/hooks/useCreateWorkspace.ts b/src-web/hooks/useCreateWorkspace.ts index 7b8c8179..456f6f8a 100644 --- a/src-web/hooks/useCreateWorkspace.ts +++ b/src-web/hooks/useCreateWorkspace.ts @@ -1,13 +1,18 @@ import { useMutation } from '@tanstack/react-query'; import type { Workspace } from '@yaakapp-internal/models'; +import { useSetAtom } from 'jotai/index'; import { invokeCmd } from '../lib/tauri'; import { useAppRoutes } from './useAppRoutes'; import { usePrompt } from './usePrompt'; +import {updateModelList} from "./useSyncModelStores"; +import { workspacesAtom } from './useWorkspaces'; export function useCreateWorkspace() { const routes = useAppRoutes(); const prompt = usePrompt(); - return useMutation({ + const setWorkspaces = useSetAtom(workspacesAtom); + + return useMutation({ mutationKey: ['create_workspace'], mutationFn: async () => { const name = await prompt({ @@ -18,14 +23,17 @@ export function useCreateWorkspace() { placeholder: 'My Workspace', confirmText: 'Create', }); - const workspace = await invokeCmd('cmd_create_workspace', { name }); - - // Give some time for the workspace to sync to the local store - await new Promise((resolve) => setTimeout(resolve, 100)); - - return workspace; + if (name == null) { + return null; + } + return invokeCmd('cmd_create_workspace', { name }); }, onSuccess: async (workspace) => { + if (workspace == null) return; + + // Optimistic update + setWorkspaces(updateModelList(workspace)); + routes.navigate('workspace', { workspaceId: workspace.id }); }, }); diff --git a/src-web/hooks/useDeleteAnyGrpcRequest.tsx b/src-web/hooks/useDeleteAnyGrpcRequest.tsx index 68d5872e..46dbef1c 100644 --- a/src-web/hooks/useDeleteAnyGrpcRequest.tsx +++ b/src-web/hooks/useDeleteAnyGrpcRequest.tsx @@ -1,14 +1,18 @@ import { useMutation } from '@tanstack/react-query'; import type { GrpcRequest } from '@yaakapp-internal/models'; +import {useSetAtom} from "jotai"; import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; import { fallbackRequestName } from '../lib/fallbackRequestName'; import { getGrpcRequest } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; import { useConfirm } from './useConfirm'; +import {grpcRequestsAtom} from "./useGrpcRequests"; +import {removeModelById} from "./useSyncModelStores"; export function useDeleteAnyGrpcRequest() { const confirm = useConfirm(); + const setGrpcRequests = useSetAtom(grpcRequestsAtom); return useMutation({ mutationKey: ['delete_any_grpc_request'], @@ -29,6 +33,12 @@ export function useDeleteAnyGrpcRequest() { if (!confirmed) return null; return invokeCmd('cmd_delete_grpc_request', { requestId: id }); }, + onSuccess: (request) => { + if (request == null) return; + + // Optimistic update + setGrpcRequests(removeModelById(request)); + }, onSettled: () => trackEvent('grpc_request', 'delete'), }); } diff --git a/src-web/hooks/useDeleteAnyHttpRequest.tsx b/src-web/hooks/useDeleteAnyHttpRequest.tsx index bdeb358a..1d37de54 100644 --- a/src-web/hooks/useDeleteAnyHttpRequest.tsx +++ b/src-web/hooks/useDeleteAnyHttpRequest.tsx @@ -1,14 +1,18 @@ import { useMutation } from '@tanstack/react-query'; import type { HttpRequest } from '@yaakapp-internal/models'; +import { useSetAtom } from 'jotai'; import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; import { fallbackRequestName } from '../lib/fallbackRequestName'; import { getHttpRequest } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; import { useConfirm } from './useConfirm'; +import { httpRequestsAtom } from './useHttpRequests'; +import { removeModelById } from './useSyncModelStores'; export function useDeleteAnyHttpRequest() { const confirm = useConfirm(); + const setHttpRequests = useSetAtom(httpRequestsAtom); return useMutation({ mutationKey: ['delete_any_http_request'], @@ -27,7 +31,13 @@ export function useDeleteAnyHttpRequest() { ), }); if (!confirmed) return null; - return invokeCmd('cmd_delete_http_request', { requestId: id }); + return invokeCmd('cmd_delete_http_request', { requestId: id }); + }, + onSuccess: (request) => { + if (request == null) return; + + // Optimistic update + setHttpRequests(removeModelById(request)); }, onSettled: () => trackEvent('http_request', 'delete'), }); diff --git a/src-web/hooks/useDeleteCookieJar.tsx b/src-web/hooks/useDeleteCookieJar.tsx index 31ad3a42..45ad397f 100644 --- a/src-web/hooks/useDeleteCookieJar.tsx +++ b/src-web/hooks/useDeleteCookieJar.tsx @@ -1,12 +1,16 @@ import { useMutation } from '@tanstack/react-query'; import type { CookieJar } from '@yaakapp-internal/models'; +import {useSetAtom} from "jotai"; import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; import { useConfirm } from './useConfirm'; +import {cookieJarsAtom} from "./useCookieJars"; +import {removeModelById} from "./useSyncModelStores"; export function useDeleteCookieJar(cookieJar: CookieJar | null) { const confirm = useConfirm(); + const setCookieJars = useSetAtom(cookieJarsAtom); return useMutation({ mutationKey: ['delete_cookie_jar', cookieJar?.id], @@ -25,5 +29,10 @@ export function useDeleteCookieJar(cookieJar: CookieJar | null) { return invokeCmd('cmd_delete_cookie_jar', { cookieJarId: cookieJar?.id }); }, onSettled: () => trackEvent('cookie_jar', 'delete'), + onSuccess: (cookieJar) => { + if (cookieJar == null) return; + + setCookieJars(removeModelById(cookieJar)); + } }); } diff --git a/src-web/hooks/useDeleteEnvironment.tsx b/src-web/hooks/useDeleteEnvironment.tsx index 0f5213eb..ea32e637 100644 --- a/src-web/hooks/useDeleteEnvironment.tsx +++ b/src-web/hooks/useDeleteEnvironment.tsx @@ -1,12 +1,16 @@ import { useMutation } from '@tanstack/react-query'; import type { Environment } from '@yaakapp-internal/models'; +import {useSetAtom} from "jotai"; import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; import { useConfirm } from './useConfirm'; +import {environmentsAtom} from "./useEnvironments"; +import {removeModelById} from "./useSyncModelStores"; export function useDeleteEnvironment(environment: Environment | null) { const confirm = useConfirm(); + const setEnvironments = useSetAtom(environmentsAtom); return useMutation({ mutationKey: ['delete_environment', environment?.id], @@ -25,5 +29,10 @@ export function useDeleteEnvironment(environment: Environment | null) { return invokeCmd('cmd_delete_environment', { environmentId: environment?.id }); }, onSettled: () => trackEvent('environment', 'delete'), + onSuccess: (environment) => { + if (environment == null) return; + + setEnvironments(removeModelById(environment)); + } }); } diff --git a/src-web/hooks/useDeleteFolder.tsx b/src-web/hooks/useDeleteFolder.tsx index a3b6be4f..550a4dbb 100644 --- a/src-web/hooks/useDeleteFolder.tsx +++ b/src-web/hooks/useDeleteFolder.tsx @@ -1,13 +1,17 @@ import { useMutation } from '@tanstack/react-query'; import type { Folder } from '@yaakapp-internal/models'; +import { useSetAtom } from 'jotai'; import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; import { getFolder } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; import { useConfirm } from './useConfirm'; +import { foldersAtom } from './useFolders'; +import { removeModelById } from './useSyncModelStores'; export function useDeleteFolder(id: string | null) { const confirm = useConfirm(); + const setFolders = useSetAtom(foldersAtom); return useMutation({ mutationKey: ['delete_folder', id], @@ -27,5 +31,10 @@ export function useDeleteFolder(id: string | null) { return invokeCmd('cmd_delete_folder', { folderId: id }); }, onSettled: () => trackEvent('folder', 'delete'), + onSuccess: (folder) => { + if (folder == null) return; + + setFolders(removeModelById(folder)); + }, }); } diff --git a/src-web/hooks/useDeleteGrpcConnection.ts b/src-web/hooks/useDeleteGrpcConnection.ts index b191357c..7145cc7f 100644 --- a/src-web/hooks/useDeleteGrpcConnection.ts +++ b/src-web/hooks/useDeleteGrpcConnection.ts @@ -1,14 +1,23 @@ import { useMutation } from '@tanstack/react-query'; import type { GrpcConnection } from '@yaakapp-internal/models'; +import {useSetAtom} from "jotai"; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; +import {grpcConnectionsAtom} from "./useGrpcConnections"; +import {removeModelById} from "./useSyncModelStores"; export function useDeleteGrpcConnection(id: string | null) { + const setGrpcConnections = useSetAtom(grpcConnectionsAtom); return useMutation({ mutationKey: ['delete_grpc_connection', id], mutationFn: async () => { return await invokeCmd('cmd_delete_grpc_connection', { id: id }); }, onSettled: () => trackEvent('grpc_connection', 'delete'), + onSuccess: (connection) => { + if (connection == null) return; + + setGrpcConnections(removeModelById(connection)); + } }); } diff --git a/src-web/hooks/useDeleteGrpcConnections.ts b/src-web/hooks/useDeleteGrpcConnections.ts index 173e734e..b12be4f2 100644 --- a/src-web/hooks/useDeleteGrpcConnections.ts +++ b/src-web/hooks/useDeleteGrpcConnections.ts @@ -1,8 +1,11 @@ import { useMutation } from '@tanstack/react-query'; +import { useSetAtom } from 'jotai'; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; +import { grpcConnectionsAtom } from './useGrpcConnections'; export function useDeleteGrpcConnections(requestId?: string) { + const setGrpcConnections = useSetAtom(grpcConnectionsAtom); return useMutation({ mutationKey: ['delete_grpc_connections', requestId], mutationFn: async () => { @@ -10,5 +13,8 @@ export function useDeleteGrpcConnections(requestId?: string) { await invokeCmd('cmd_delete_all_grpc_connections', { requestId }); }, onSettled: () => trackEvent('grpc_connection', 'delete_many'), + onSuccess: () => { + setGrpcConnections((all) => all.filter((r) => r.requestId !== requestId)); + }, }); } diff --git a/src-web/hooks/useDeleteHttpResponse.ts b/src-web/hooks/useDeleteHttpResponse.ts index 4fd1617a..5d7fe641 100644 --- a/src-web/hooks/useDeleteHttpResponse.ts +++ b/src-web/hooks/useDeleteHttpResponse.ts @@ -1,14 +1,21 @@ import { useMutation } from '@tanstack/react-query'; import type { HttpResponse } from '@yaakapp-internal/models'; +import {useSetAtom} from "jotai"; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; +import {httpResponsesAtom} from "./useHttpResponses"; +import {removeModelById} from "./useSyncModelStores"; export function useDeleteHttpResponse(id: string | null) { + const setHttpResponses = useSetAtom(httpResponsesAtom); return useMutation({ mutationKey: ['delete_http_response', id], mutationFn: async () => { return await invokeCmd('cmd_delete_http_response', { id: id }); }, onSettled: () => trackEvent('http_response', 'delete'), + onSuccess: (response) => { + setHttpResponses(removeModelById(response)); + } }); } diff --git a/src-web/hooks/useDeleteHttpResponses.ts b/src-web/hooks/useDeleteHttpResponses.ts index 728c64d8..362a49db 100644 --- a/src-web/hooks/useDeleteHttpResponses.ts +++ b/src-web/hooks/useDeleteHttpResponses.ts @@ -1,14 +1,20 @@ import { useMutation } from '@tanstack/react-query'; +import { useSetAtom } from 'jotai'; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; +import { httpResponsesAtom } from './useHttpResponses'; export function useDeleteHttpResponses(requestId?: string) { + const setHttpResponses = useSetAtom(httpResponsesAtom); return useMutation({ 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)); + }, onSettled: () => trackEvent('http_response', 'delete_many'), }); } diff --git a/src-web/hooks/useDeleteSendHistory.tsx b/src-web/hooks/useDeleteSendHistory.tsx index fb46f36c..ec9973c3 100644 --- a/src-web/hooks/useDeleteSendHistory.tsx +++ b/src-web/hooks/useDeleteSendHistory.tsx @@ -1,15 +1,17 @@ import { useMutation } from '@tanstack/react-query'; +import { useSetAtom } from 'jotai/index'; import { count } from '../lib/pluralize'; import { invokeCmd } from '../lib/tauri'; import { useActiveWorkspace } from './useActiveWorkspace'; import { useAlert } from './useAlert'; import { useConfirm } from './useConfirm'; import { useGrpcConnections } from './useGrpcConnections'; -import { useHttpResponses } from './useHttpResponses'; +import { httpResponsesAtom, useHttpResponses } from './useHttpResponses'; export function useDeleteSendHistory() { const confirm = useConfirm(); const alert = useAlert(); + const setHttpResponses = useSetAtom(httpResponsesAtom); const activeWorkspace = useActiveWorkspace(); const httpResponses = useHttpResponses(); const grpcConnections = useGrpcConnections(); @@ -36,8 +38,15 @@ export function useDeleteSendHistory() { variant: 'delete', description: <>Delete {labels.join(' and ')}?, }); - if (!confirmed) return; + if (!confirmed) return false; + await invokeCmd('cmd_delete_send_history', { workspaceId: activeWorkspace?.id ?? 'n/a' }); + return true; + }, + onSuccess: async (confirmed) => { + if (!confirmed) return; + + setHttpResponses((all) => all.filter((r) => r.workspaceId !== activeWorkspace?.id)); }, }); } diff --git a/src-web/hooks/useDeleteWorkspace.tsx b/src-web/hooks/useDeleteWorkspace.tsx index 468d81f0..701b759a 100644 --- a/src-web/hooks/useDeleteWorkspace.tsx +++ b/src-web/hooks/useDeleteWorkspace.tsx @@ -1,16 +1,20 @@ import { useMutation } from '@tanstack/react-query'; import type { Workspace } from '@yaakapp-internal/models'; +import {useSetAtom} from "jotai"; import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; import { useActiveWorkspace } from './useActiveWorkspace'; import { useAppRoutes } from './useAppRoutes'; import { useConfirm } from './useConfirm'; +import {removeModelById} from "./useSyncModelStores"; +import {workspacesAtom} from "./useWorkspaces"; export function useDeleteWorkspace(workspace: Workspace | null) { const activeWorkspace = useActiveWorkspace(); const routes = useAppRoutes(); const confirm = useConfirm(); + const setWorkspaces = useSetAtom(workspacesAtom); return useMutation({ mutationKey: ['delete_workspace', workspace?.id], @@ -32,6 +36,9 @@ export function useDeleteWorkspace(workspace: Workspace | null) { onSuccess: async (workspace) => { if (workspace === null) return; + // Optimistic update + setWorkspaces(removeModelById(workspace)); + const { id: workspaceId } = workspace; if (workspaceId === activeWorkspace?.id) { routes.navigate('workspaces'); diff --git a/src-web/hooks/useSyncModelStores.ts b/src-web/hooks/useSyncModelStores.ts index 5cbb0888..1a4e9474 100644 --- a/src-web/hooks/useSyncModelStores.ts +++ b/src-web/hooks/useSyncModelStores.ts @@ -60,29 +60,26 @@ export function useSyncModelStores() { return; } - // Mark these models as DESC instead of ASC - const pushToFront = model.model === 'http_response' || model.model === 'grpc_connection'; - if (shouldIgnoreModel(model, windowLabel)) return; if (model.model === 'workspace') { - setWorkspaces(updateModelList(model, pushToFront)); + setWorkspaces(updateModelList(model)); } else if (model.model === 'plugin') { - setPlugins(updateModelList(model, pushToFront)); + setPlugins(updateModelList(model)); } else if (model.model === 'http_request') { - setHttpRequests(updateModelList(model, pushToFront)); + setHttpRequests(updateModelList(model)); } else if (model.model === 'folder') { - setFolders(updateModelList(model, pushToFront)); + setFolders(updateModelList(model)); } else if (model.model === 'http_response') { - setHttpResponses(updateModelList(model, pushToFront)); + setHttpResponses(updateModelList(model)); } else if (model.model === 'grpc_request') { - setGrpcRequests(updateModelList(model, pushToFront)); + setGrpcRequests(updateModelList(model)); } else if (model.model === 'grpc_connection') { - setGrpcConnections(updateModelList(model, pushToFront)); + setGrpcConnections(updateModelList(model)); } else if (model.model === 'environment') { - setEnvironments(updateModelList(model, pushToFront)); + setEnvironments(updateModelList(model)); } else if (model.model === 'cookie_jar') { - setCookieJars(updateModelList(model, pushToFront)); + setCookieJars(updateModelList(model)); } else if (model.model === 'settings') { setSettings(model); } else if (queryKey != null) { @@ -94,7 +91,7 @@ export function useSyncModelStores() { } if (Array.isArray(current)) { - return updateModelList(model, pushToFront)(current); + return updateModelList(model)(current); } }); } @@ -107,32 +104,35 @@ export function useSyncModelStores() { console.log('Delete model', payload); if (model.model === 'workspace') { - setWorkspaces(removeById(model)); + setWorkspaces(removeModelById(model)); } else if (model.model === 'plugin') { - setPlugins(removeById(model)); + setPlugins(removeModelById(model)); } else if (model.model === 'http_request') { - setHttpRequests(removeById(model)); + setHttpRequests(removeModelById(model)); } else if (model.model === 'http_response') { - setHttpResponses(removeById(model)); + setHttpResponses(removeModelById(model)); } else if (model.model === 'folder') { - setFolders(removeById(model)); + setFolders(removeModelById(model)); } else if (model.model === 'environment') { - setEnvironments(removeById(model)); + setEnvironments(removeModelById(model)); } else if (model.model === 'grpc_request') { - setGrpcRequests(removeById(model)); + setGrpcRequests(removeModelById(model)); } else if (model.model === 'grpc_connection') { - setGrpcConnections(removeById(model)); + setGrpcConnections(removeModelById(model)); } else if (model.model === 'grpc_event') { - queryClient.setQueryData(grpcEventsQueryKey(model), removeById(model)); + queryClient.setQueryData(grpcEventsQueryKey(model), removeModelById(model)); } else if (model.model === 'key_value') { queryClient.setQueryData(keyValueQueryKey(model), undefined); } else if (model.model === 'cookie_jar') { - setCookieJars(removeById(model)); + setCookieJars(removeModelById(model)); } }); } -function updateModelList(model: T, pushToFront: boolean) { +export function updateModelList(model: T) { + // Mark these models as DESC instead of ASC + const pushToFront = model.model === 'http_response' || model.model === 'grpc_connection'; + return (current: T[] | undefined): T[] => { const index = current?.findIndex((v) => modelsEq(v, model)) ?? -1; if (index >= 0) { @@ -143,7 +143,7 @@ function updateModelList(model: T, pushToFront: boolean) { }; } -function removeById(model: T) { +export function removeModelById(model: T) { return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? []; } diff --git a/src-web/hooks/useUpdateAnyFolder.ts b/src-web/hooks/useUpdateAnyFolder.ts index 50e23403..1abe625c 100644 --- a/src-web/hooks/useUpdateAnyFolder.ts +++ b/src-web/hooks/useUpdateAnyFolder.ts @@ -1,10 +1,14 @@ import { useMutation } from '@tanstack/react-query'; import type { Folder } from '@yaakapp-internal/models'; +import {useSetAtom} from "jotai/index"; import { getFolder } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; +import {foldersAtom} from "./useFolders"; +import {updateModelList} from "./useSyncModelStores"; export function useUpdateAnyFolder() { - return useMutation Folder }>({ + const setFolders = useSetAtom(foldersAtom); + return useMutation Folder }>({ mutationKey: ['update_any_folder'], mutationFn: async ({ id, update }) => { const folder = await getFolder(id); @@ -12,7 +16,10 @@ export function useUpdateAnyFolder() { throw new Error("Can't update a null folder"); } - await invokeCmd('cmd_update_folder', { folder: update(folder) }); + return invokeCmd('cmd_update_folder', { folder: update(folder) }); }, + onSuccess: async (folder) => { + setFolders(updateModelList(folder)); + } }); } diff --git a/src-web/hooks/useUpdateAnyGrpcRequest.ts b/src-web/hooks/useUpdateAnyGrpcRequest.ts index 88fce22e..a6479b56 100644 --- a/src-web/hooks/useUpdateAnyGrpcRequest.ts +++ b/src-web/hooks/useUpdateAnyGrpcRequest.ts @@ -1,11 +1,15 @@ import { useMutation } from '@tanstack/react-query'; import type { GrpcRequest } from '@yaakapp-internal/models'; +import { useSetAtom } from 'jotai/index'; import { getGrpcRequest } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; +import { grpcRequestsAtom } from './useGrpcRequests'; +import { updateModelList } from './useSyncModelStores'; export function useUpdateAnyGrpcRequest() { + const setGrpcRequests = useSetAtom(grpcRequestsAtom); return useMutation< - void, + GrpcRequest, unknown, { id: string; update: Partial | ((r: GrpcRequest) => GrpcRequest) } >({ @@ -18,7 +22,10 @@ export function useUpdateAnyGrpcRequest() { const patchedRequest = typeof update === 'function' ? update(request) : { ...request, ...update }; - await invokeCmd('cmd_update_grpc_request', { request: patchedRequest }); + return invokeCmd('cmd_update_grpc_request', { request: patchedRequest }); + }, + onSuccess: (request) => { + setGrpcRequests(updateModelList(request)); }, }); } diff --git a/src-web/hooks/useUpdateAnyHttpRequest.ts b/src-web/hooks/useUpdateAnyHttpRequest.ts index 40987b65..3f4b6f40 100644 --- a/src-web/hooks/useUpdateAnyHttpRequest.ts +++ b/src-web/hooks/useUpdateAnyHttpRequest.ts @@ -1,11 +1,15 @@ import { useMutation } from '@tanstack/react-query'; import type { HttpRequest } from '@yaakapp-internal/models'; +import {useSetAtom} from "jotai/index"; import { getHttpRequest } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; +import {httpRequestsAtom} from "./useHttpRequests"; +import {updateModelList} from "./useSyncModelStores"; export function useUpdateAnyHttpRequest() { + const setHttpRequests = useSetAtom(httpRequestsAtom); return useMutation< - void, + HttpRequest, unknown, { id: string; update: Partial | ((r: HttpRequest) => HttpRequest) } >({ @@ -18,7 +22,10 @@ export function useUpdateAnyHttpRequest() { const patchedRequest = typeof update === 'function' ? update(request) : { ...request, ...update }; - await invokeCmd('cmd_update_http_request', { request: patchedRequest }); + return invokeCmd('cmd_update_http_request', { request: patchedRequest }); }, + onSuccess: async (request) => { + setHttpRequests(updateModelList(request)); + } }); } diff --git a/src-web/hooks/useUpdateCookieJar.ts b/src-web/hooks/useUpdateCookieJar.ts index 05806ab3..c37ebfd8 100644 --- a/src-web/hooks/useUpdateCookieJar.ts +++ b/src-web/hooks/useUpdateCookieJar.ts @@ -1,10 +1,14 @@ import { useMutation } from '@tanstack/react-query'; import type { CookieJar } from '@yaakapp-internal/models'; +import { useSetAtom } from 'jotai/index'; import { getCookieJar } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; +import { cookieJarsAtom } from './useCookieJars'; +import { updateModelList } from './useSyncModelStores'; export function useUpdateCookieJar(id: string | null) { - return useMutation | ((j: CookieJar) => CookieJar)>({ + const setCookieJars = useSetAtom(cookieJarsAtom); + return useMutation | ((j: CookieJar) => CookieJar)>({ mutationKey: ['update_cookie_jar', id], mutationFn: async (v) => { const cookieJar = await getCookieJar(id); @@ -13,7 +17,10 @@ export function useUpdateCookieJar(id: string | null) { } const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v }; - await invokeCmd('cmd_update_cookie_jar', { cookieJar: newCookieJar }); + return invokeCmd('cmd_update_cookie_jar', { cookieJar: newCookieJar }); + }, + onSuccess: (cookieJar) => { + setCookieJars(updateModelList(cookieJar)); }, }); } diff --git a/src-web/hooks/useUpdateEnvironment.ts b/src-web/hooks/useUpdateEnvironment.ts index 3982fcc3..3ce2981c 100644 --- a/src-web/hooks/useUpdateEnvironment.ts +++ b/src-web/hooks/useUpdateEnvironment.ts @@ -1,10 +1,18 @@ import { useMutation } from '@tanstack/react-query'; import type { Environment } from '@yaakapp-internal/models'; +import { useSetAtom } from 'jotai/index'; import { getEnvironment } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; +import { environmentsAtom } from './useEnvironments'; +import {updateModelList} from "./useSyncModelStores"; export function useUpdateEnvironment(id: string | null) { - return useMutation | ((r: Environment) => Environment)>({ + const setEnvironments = useSetAtom(environmentsAtom); + return useMutation< + Environment, + unknown, + Partial | ((r: Environment) => Environment) + >({ mutationKey: ['update_environment', id], mutationFn: async (v) => { const environment = await getEnvironment(id); @@ -13,7 +21,10 @@ export function useUpdateEnvironment(id: string | null) { } const newEnvironment = typeof v === 'function' ? v(environment) : { ...environment, ...v }; - await invokeCmd('cmd_update_environment', { environment: newEnvironment }); + return invokeCmd('cmd_update_environment', { environment: newEnvironment }); + }, + onSuccess: async (environment) => { + setEnvironments(updateModelList(environment)); }, }); } diff --git a/src-web/hooks/useUpdateSettings.ts b/src-web/hooks/useUpdateSettings.ts index 5681267b..f43b6260 100644 --- a/src-web/hooks/useUpdateSettings.ts +++ b/src-web/hooks/useUpdateSettings.ts @@ -1,15 +1,21 @@ import { useMutation } from '@tanstack/react-query'; import type { Settings } from '@yaakapp-internal/models'; +import { useSetAtom } from 'jotai'; import { getSettings } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; +import { settingsAtom } from './useSettings'; export function useUpdateSettings() { - return useMutation>({ + const setSettings = useSetAtom(settingsAtom); + return useMutation>({ mutationKey: ['update_settings'], mutationFn: async (patch) => { const settings = await getSettings(); const newSettings: Settings = { ...settings, ...patch }; - await invokeCmd('cmd_update_settings', { settings: newSettings }); + return invokeCmd('cmd_update_settings', { settings: newSettings }); + }, + onSuccess: (settings) => { + setSettings(settings); }, }); } diff --git a/src-web/hooks/useUpdateWorkspace.ts b/src-web/hooks/useUpdateWorkspace.ts index bd9fb210..de9b3777 100644 --- a/src-web/hooks/useUpdateWorkspace.ts +++ b/src-web/hooks/useUpdateWorkspace.ts @@ -1,10 +1,14 @@ import { useMutation } from '@tanstack/react-query'; import type { Workspace } from '@yaakapp-internal/models'; +import {useSetAtom} from "jotai/index"; import { getWorkspace } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; +import {updateModelList} from "./useSyncModelStores"; +import {workspacesAtom} from "./useWorkspaces"; export function useUpdateWorkspace(id: string | null) { - return useMutation | ((w: Workspace) => Workspace)>({ + const setWorkspaces = useSetAtom(workspacesAtom); + return useMutation | ((w: Workspace) => Workspace)>({ mutationKey: ['update_workspace', id], mutationFn: async (v) => { const workspace = await getWorkspace(id); @@ -13,7 +17,10 @@ export function useUpdateWorkspace(id: string | null) { } const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v }; - await invokeCmd('cmd_update_workspace', { workspace: newWorkspace }); + return invokeCmd('cmd_update_workspace', { workspace: newWorkspace }); + }, + onSuccess: async (workspace) => { + setWorkspaces(updateModelList(workspace)); }, }); } diff --git a/src-web/package.json b/src-web/package.json index a6efce06..16b60eb7 100644 --- a/src-web/package.json +++ b/src-web/package.json @@ -19,7 +19,7 @@ "@lezer/lr": "^1.3.3", "@react-hook/resize-observer": "^2.0.2", "@tailwindcss/container-queries": "^0.1.1", - "@tanstack/react-query": "^5.55.4", + "@tanstack/react-query": "^5.59.16", "@tanstack/react-virtual": "^3.10.8", "@tauri-apps/api": "^2.0.1", "@tauri-apps/plugin-clipboard-manager": "^2.0.0",