diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a2e244bd..d48e3225 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -46,12 +46,25 @@ use yaak_models::models::{ CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Plugin, Settings, Workspace, }; -use yaak_models::queries::{cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_plugin, delete_workspace, duplicate_grpc_request, duplicate_http_request, generate_id, generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_plugin, get_workspace, list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests, list_http_responses, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace}; +use yaak_models::queries::{ + cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response, + delete_all_grpc_connections, delete_all_http_responses_for_request, delete_cookie_jar, + delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, + delete_http_request, delete_http_response, delete_plugin, delete_workspace, + duplicate_grpc_request, duplicate_http_request, generate_id, generate_model_id, get_cookie_jar, + get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, + get_http_response, get_key_value_raw, get_or_create_settings, get_plugin, get_workspace, + list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, + list_grpc_requests, list_http_requests, list_http_responses, list_http_responses_for_request, + list_plugins, list_workspaces, set_key_value_raw, update_response_if_id, update_settings, + upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, + upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace, +}; use yaak_plugin_runtime::events::{ BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse, GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse, Icon, - InternalEvent, InternalEventPayload, RenderHttpRequestResponse, RenderPurpose, - SendHttpRequestResponse, PromptTextResponse, ShowToastRequest, TemplateRenderResponse, + InternalEvent, InternalEventPayload, PromptTextResponse, RenderHttpRequestResponse, + RenderPurpose, SendHttpRequestResponse, ShowToastRequest, TemplateRenderResponse, WindowContext, }; use yaak_plugin_runtime::plugin_handle::PluginHandle; @@ -1080,7 +1093,7 @@ async fn cmd_send_http_request( let _ = cancel_tx.send(true); }, ); - + let environment = match environment_id { Some(id) => match get_environment(&window, id).await { Ok(env) => Some(env), @@ -1454,10 +1467,10 @@ async fn cmd_delete_environment( #[tauri::command] async fn cmd_list_grpc_connections( - request_id: &str, + workspace_id: &str, w: WebviewWindow, ) -> Result, String> { - list_grpc_connections(&w, request_id) + list_grpc_connections(&w, workspace_id) .await .map_err(|e| e.to_string()) } @@ -1604,11 +1617,11 @@ async fn cmd_get_workspace(id: &str, w: WebviewWindow) -> Result, w: WebviewWindow, ) -> Result, String> { - list_http_responses(&w, request_id, limit) + list_http_responses(&w, workspace_id, limit) .await .map_err(|e| e.to_string()) } @@ -1636,7 +1649,7 @@ async fn cmd_delete_all_grpc_connections(request_id: &str, w: WebviewWindow) -> #[tauri::command] async fn cmd_delete_all_http_responses(request_id: &str, w: WebviewWindow) -> Result<(), String> { - delete_all_http_responses(&w, request_id) + delete_all_http_responses_for_request(&w, request_id) .await .map_err(|e| e.to_string()) } @@ -2215,7 +2228,7 @@ async fn handle_plugin_event( Some(InternalEventPayload::PromptTextResponse(resp)) } InternalEventPayload::FindHttpResponsesRequest(req) => { - let http_responses = list_http_responses( + let http_responses = list_http_responses_for_request( app_handle, req.request_id.as_str(), req.limit.map(|l| l as i64), diff --git a/src-tauri/yaak_models/src/queries.rs b/src-tauri/yaak_models/src/queries.rs index b9f2e7f0..c2b2c50e 100644 --- a/src-tauri/yaak_models/src/queries.rs +++ b/src-tauri/yaak_models/src/queries.rs @@ -1,7 +1,12 @@ use std::fs; use crate::error::Result; -use crate::models::{CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection, GrpcConnectionIden, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader, HttpResponseIden, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden, Settings, SettingsIden, Workspace, WorkspaceIden}; +use crate::models::{ + CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection, + GrpcConnectionIden, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest, + HttpRequestIden, HttpResponse, HttpResponseHeader, HttpResponseIden, KeyValue, KeyValueIden, + ModelType, Plugin, PluginIden, Settings, SettingsIden, Workspace, WorkspaceIden, +}; use crate::plugin::SqliteConnection; use log::{debug, error}; use rand::distributions::{Alphanumeric, DistString}; @@ -12,6 +17,9 @@ use sea_query_rusqlite::RusqliteBinder; use serde::Serialize; use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow}; +const MAX_GRPC_CONNECTIONS_PER_REQUEST: usize = 20; +const MAX_HTTP_RESPONSES_PER_REQUEST: usize = MAX_GRPC_CONNECTIONS_PER_REQUEST; + pub async fn set_key_value_string( mgr: &WebviewWindow, namespace: &str, @@ -423,6 +431,13 @@ pub async fn upsert_grpc_connection( window: &WebviewWindow, connection: &GrpcConnection, ) -> Result { + let connections = + list_http_responses_for_request(window, connection.request_id.as_str(), None).await?; + for c in connections.iter().skip(MAX_GRPC_CONNECTIONS_PER_REQUEST - 1) { + debug!("Deleting old grpc connection {}", c.id); + delete_grpc_connection(window, c.id.as_str()).await?; + } + let id = match connection.id.as_str() { "" => generate_model_id(ModelType::TypeGrpcConnection), _ => connection.id.to_string(), @@ -497,6 +512,24 @@ pub async fn get_grpc_connection( } pub async fn list_grpc_connections( + mgr: &impl Manager, + workspace_id: &str, +) -> Result> { + let dbm = &*mgr.state::(); + let db = dbm.0.lock().await.get().unwrap(); + + let (sql, params) = Query::select() + .from(GrpcConnectionIden::Table) + .cond_where(Expr::col(GrpcConnectionIden::WorkspaceId).eq(workspace_id)) + .column(Asterisk) + .order_by(GrpcConnectionIden::CreatedAt, Order::Desc) + .build_rusqlite(SqliteQueryBuilder); + let mut stmt = db.prepare(sql.as_str())?; + let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?; + Ok(items.map(|v| v.unwrap()).collect()) +} + +pub async fn list_grpc_connections_for_request( mgr: &impl Manager, request_id: &str, ) -> Result> { @@ -536,7 +569,7 @@ pub async fn delete_all_grpc_connections( window: &WebviewWindow, request_id: &str, ) -> Result<()> { - for r in list_grpc_connections(window, request_id).await? { + for r in list_grpc_connections_for_request(window, request_id).await? { delete_grpc_connection(window, &r.id).await?; } Ok(()) @@ -794,10 +827,7 @@ pub async fn update_settings( SettingsIden::EditorSoftWrap, settings.editor_soft_wrap.into(), ), - ( - SettingsIden::Telemetry, - settings.telemetry.into(), - ), + (SettingsIden::Telemetry, settings.telemetry.into()), ( SettingsIden::OpenWorkspaceNewWindow, settings.open_workspace_new_window.into(), @@ -872,10 +902,7 @@ pub async fn get_environment(mgr: &impl Manager, id: &str) -> Res Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?) } -pub async fn get_plugin( - mgr: &impl Manager, - id: &str -) -> Result { +pub async fn get_plugin(mgr: &impl Manager, id: &str) -> Result { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -888,9 +915,7 @@ pub async fn get_plugin( Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?) } -pub async fn list_plugins( - mgr: &impl Manager, -) -> Result> { +pub async fn list_plugins(mgr: &impl Manager) -> Result> { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -1185,7 +1210,7 @@ pub async fn delete_http_request( let req = get_http_request(window, id).await?; // DB deletes will cascade but this will delete the files - delete_all_http_responses(window, id).await?; + delete_all_http_responses_for_request(window, id).await?; let dbm = &*window.app_handle().state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -1234,6 +1259,12 @@ pub async fn create_http_response( version: Option<&str>, remote_addr: Option<&str>, ) -> Result { + let responses = list_http_responses_for_request(window, request_id, None).await?; + for response in responses.iter().skip(MAX_HTTP_RESPONSES_PER_REQUEST - 1) { + debug!("Deleting old response {}", response.id); + delete_http_response(window, response.id.as_str()).await?; + } + let req = get_http_request(window, request_id).await?; let id = generate_model_id(ModelType::TypeHttpResponse); let dbm = &*window.app_handle().state::(); @@ -1418,17 +1449,37 @@ pub async fn delete_http_response( emit_deleted_model(window, resp) } -pub async fn delete_all_http_responses( +pub async fn delete_all_http_responses_for_request( window: &WebviewWindow, request_id: &str, ) -> Result<()> { - for r in list_http_responses(window, request_id, None).await? { + for r in list_http_responses_for_request(window, request_id, None).await? { delete_http_response(window, &r.id).await?; } Ok(()) } pub async fn list_http_responses( + mgr: &impl Manager, + workspace_id: &str, + limit: Option, +) -> Result> { + let limit_unwrapped = limit.unwrap_or_else(|| i64::MAX); + let dbm = mgr.state::(); + let db = dbm.0.lock().await.get().unwrap(); + let (sql, params) = Query::select() + .from(HttpResponseIden::Table) + .cond_where(Expr::col(HttpResponseIden::WorkspaceId).eq(workspace_id)) + .column(Asterisk) + .order_by(HttpResponseIden::CreatedAt, Order::Desc) + .limit(limit_unwrapped as u64) + .build_rusqlite(SqliteQueryBuilder); + let mut stmt = db.prepare(sql.as_str())?; + let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?; + Ok(items.map(|v| v.unwrap()).collect()) +} + +pub async fn list_http_responses_for_request( mgr: &impl Manager, request_id: &str, limit: Option, diff --git a/src-web/components/App.tsx b/src-web/components/App.tsx index 3f8f1544..7ae18b1d 100644 --- a/src-web/components/App.tsx +++ b/src-web/components/App.tsx @@ -13,8 +13,10 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, - refetchOnWindowFocus: true, networkMode: 'offlineFirst', + refetchOnWindowFocus: true, + refetchOnReconnect: false, + refetchOnMount: false, // Don't refetch when a hook mounts }, }, }); diff --git a/src-web/components/AppRouter.tsx b/src-web/components/AppRouter.tsx index d65f75b9..2ee984e2 100644 --- a/src-web/components/AppRouter.tsx +++ b/src-web/components/AppRouter.tsx @@ -1,6 +1,10 @@ import { lazy } from 'react'; import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom'; import { routePaths, useAppRoutes } from '../hooks/useAppRoutes'; +import { useGenerateThemeCss } from '../hooks/useGenerateThemeCss'; +import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting'; +import { useSyncModelStores } from '../hooks/useSyncModelStores'; +import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting'; import { DefaultLayout } from './DefaultLayout'; import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace'; import RouteError from './RouteError'; @@ -50,6 +54,12 @@ const router = createBrowserRouter([ ]); export function AppRouter() { + // Add some global hooks that should remain persistent + useSyncModelStores(); + useSyncZoomSetting(); + useSyncFontSizeSetting(); + useGenerateThemeCss(); + return ; } diff --git a/src-web/components/CookieDialog.tsx b/src-web/components/CookieDialog.tsx index 4eff065a..8311667b 100644 --- a/src-web/components/CookieDialog.tsx +++ b/src-web/components/CookieDialog.tsx @@ -12,7 +12,7 @@ interface Props { export const CookieDialog = function ({ cookieJarId }: Props) { const updateCookieJar = useUpdateCookieJar(cookieJarId ?? null); - const cookieJars = useCookieJars().data ?? []; + const cookieJars = useCookieJars(); const cookieJar = cookieJars.find((c) => c.id === cookieJarId); if (cookieJar == null) { diff --git a/src-web/components/CookieDropdown.tsx b/src-web/components/CookieDropdown.tsx index c9843729..603f4d85 100644 --- a/src-web/components/CookieDropdown.tsx +++ b/src-web/components/CookieDropdown.tsx @@ -12,7 +12,7 @@ import { InlineCode } from './core/InlineCode'; import { useDialog } from './DialogContext'; export function CookieDropdown() { - const cookieJars = useCookieJars().data ?? []; + const cookieJars = useCookieJars(); const [activeCookieJar, setActiveCookieJarId] = useActiveCookieJar(); const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null); const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null); diff --git a/src-web/components/DefaultLayout.tsx b/src-web/components/DefaultLayout.tsx index 2bb5e110..e14d6797 100644 --- a/src-web/components/DefaultLayout.tsx +++ b/src-web/components/DefaultLayout.tsx @@ -2,8 +2,8 @@ import classNames from 'classnames'; import { Outlet } from 'react-router-dom'; import { useOsInfo } from '../hooks/useOsInfo'; import { DialogProvider, Dialogs } from './DialogContext'; -import { GlobalHooks } from './GlobalHooks'; import { ToastProvider, Toasts } from './ToastContext'; +import { GlobalHooks } from './GlobalHooks'; export function DefaultLayout() { const osInfo = useOsInfo(); diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx index f191bd65..94aec54d 100644 --- a/src-web/components/GlobalHooks.tsx +++ b/src-web/components/GlobalHooks.tsx @@ -1,50 +1,17 @@ -import { useQueryClient } from '@tanstack/react-query'; import { emit } from '@tauri-apps/api/event'; -import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; -import type { AnyModel } from '@yaakapp-internal/models'; import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin'; -import { useSetAtom } from 'jotai'; -import { useEffect } from 'react'; -import { useEnsureActiveCookieJar, useMigrateActiveCookieJarId } from '../hooks/useActiveCookieJar'; +import { useEnsureActiveCookieJar } from '../hooks/useActiveCookieJar'; import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast'; -import { cookieJarsQueryKey } from '../hooks/useCookieJars'; -import { useCopy } from '../hooks/useCopy'; -import { environmentsAtom } from '../hooks/useEnvironments'; -import { foldersQueryKey } from '../hooks/useFolders'; -import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections'; -import { grpcEventsQueryKey } from '../hooks/useGrpcEvents'; -import { grpcRequestsAtom } from '../hooks/useGrpcRequests'; import { useHotKey } from '../hooks/useHotKey'; -import { httpRequestsAtom } from '../hooks/useHttpRequests'; -import { httpResponsesQueryKey } from '../hooks/useHttpResponses'; -import { keyValueQueryKey } from '../hooks/useKeyValue'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; import { useNotificationToast } from '../hooks/useNotificationToast'; -import { pluginsAtom } from '../hooks/usePlugins'; import { usePrompt } from '../hooks/usePrompt'; import { useRecentCookieJars } from '../hooks/useRecentCookieJars'; import { useRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; -import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; -import { settingsAtom, useSettings } from '../hooks/useSettings'; +import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels'; import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette'; -import { workspacesAtom } from '../hooks/useWorkspaces'; -import { useZoom } from '../hooks/useZoom'; -import { extractKeyValue } from '../lib/keyValueStore'; -import { modelsEq } from '../lib/model_util'; -import { catppuccinMacchiato } from '../lib/theme/themes/catppuccin'; -import { githubLight } from '../lib/theme/themes/github'; -import { hotdogStandDefault } from '../lib/theme/themes/hotdog-stand'; -import { monokaiProDefault } from '../lib/theme/themes/monokai-pro'; -import { rosePineDefault } from '../lib/theme/themes/rose-pine'; -import { yaakDark } from '../lib/theme/themes/yaak'; -import { getThemeCSS } from '../lib/theme/window'; - -export interface ModelPayload { - model: AnyModel; - windowLabel: string; -} export function GlobalHooks() { // Include here so they always update, even if no component references them @@ -52,136 +19,16 @@ export function GlobalHooks() { useRecentEnvironments(); useRecentCookieJars(); useRecentRequests(); + useSyncWorkspaceChildModels(); // Other useful things useNotificationToast(); useActiveWorkspaceChangedToast(); useEnsureActiveCookieJar(); - // TODO: Remove in future version - useMigrateActiveCookieJarId(); - const toggleCommandPalette = useToggleCommandPalette(); useHotKey('command_palette.toggle', toggleCommandPalette); - const queryClient = useQueryClient(); - const { wasUpdatedExternally } = useRequestUpdateKey(null); - - const setSettings = useSetAtom(settingsAtom); - const setWorkspaces = useSetAtom(workspacesAtom); - const setPlugins = useSetAtom(pluginsAtom); - const setHttpRequests = useSetAtom(httpRequestsAtom); - const setGrpcRequests = useSetAtom(grpcRequestsAtom); - const setEnvironments = useSetAtom(environmentsAtom); - - useListenToTauriEvent('upserted_model', ({ payload }) => { - console.log('Upserted model', payload.model); - const { model, windowLabel } = payload; - const queryKey = - model.model === 'http_response' - ? httpResponsesQueryKey(model) - : model.model === 'folder' - ? foldersQueryKey(model) - : model.model === 'grpc_connection' - ? grpcConnectionsQueryKey(model) - : model.model === 'grpc_event' - ? grpcEventsQueryKey(model) - : model.model === 'key_value' - ? keyValueQueryKey(model) - : model.model === 'cookie_jar' - ? cookieJarsQueryKey(model) - : null; - - if (model.model === 'http_request' && windowLabel !== getCurrentWebviewWindow().label) { - wasUpdatedExternally(model.id); - } - - const pushToFront = (['http_response', 'grpc_connection'] as AnyModel['model'][]).includes( - model.model, - ); - - if (shouldIgnoreModel(model, windowLabel)) return; - - if (model.model === 'workspace') { - setWorkspaces(updateModelList(model, pushToFront)); - } else if (model.model === 'plugin') { - setPlugins(updateModelList(model, pushToFront)); - } else if (model.model === 'http_request') { - setHttpRequests(updateModelList(model, pushToFront)); - } else if (model.model === 'grpc_request') { - setGrpcRequests(updateModelList(model, pushToFront)); - } else if (model.model === 'environment') { - setEnvironments(updateModelList(model, pushToFront)); - } else if (model.model === 'settings') { - setSettings(model); - } else if (queryKey != null) { - // TODO: Convert all models to use Jotai - queryClient.setQueryData(queryKey, (current: unknown) => { - if (model.model === 'key_value') { - // Special-case for KeyValue - return extractKeyValue(model); - } - - if (Array.isArray(current)) { - return updateModelList(model, pushToFront)(current); - } - }); - } - }); - - useListenToTauriEvent('deleted_model', ({ payload }) => { - const { model, windowLabel } = payload; - if (shouldIgnoreModel(model, windowLabel)) return; - - console.log('Delete model', payload.model); - - if (model.model === 'workspace') { - setWorkspaces(removeById(model)); - } else if (model.model === 'plugin') { - setPlugins(removeById(model)); - } else if (model.model === 'http_request') { - setHttpRequests(removeById(model)); - } else if (model.model === 'http_response') { - queryClient.setQueryData(httpResponsesQueryKey(model), removeById(model)); - } else if (model.model === 'folder') { - queryClient.setQueryData(foldersQueryKey(model), removeById(model)); - } else if (model.model === 'environment') { - setEnvironments(removeById(model)); - } else if (model.model === 'grpc_request') { - setGrpcRequests(removeById(model)); - } else if (model.model === 'grpc_connection') { - queryClient.setQueryData(grpcConnectionsQueryKey(model), removeById(model)); - } else if (model.model === 'grpc_event') { - queryClient.setQueryData(grpcEventsQueryKey(model), removeById(model)); - } else if (model.model === 'key_value') { - queryClient.setQueryData(keyValueQueryKey(model), undefined); - } else if (model.model === 'cookie_jar') { - queryClient.setQueryData(cookieJarsQueryKey(model), undefined); - } - }); - - const settings = useSettings(); - useEffect(() => { - if (settings == null) { - return; - } - - const { interfaceScale, editorFontSize } = settings; - getCurrentWebviewWindow().setZoom(interfaceScale).catch(console.error); - document.documentElement.style.setProperty('--editor-font-size', `${editorFontSize}px`); - }, [settings]); - - // Handle Zoom. - // Note, Mac handles it in the app menu, so need to also handle keyboard - // shortcuts for Windows/Linux - const zoom = useZoom(); - useHotKey('app.zoom_in', zoom.zoomIn); - useListenToTauriEvent('zoom_in', zoom.zoomIn); - useHotKey('app.zoom_out', zoom.zoomOut); - useListenToTauriEvent('zoom_out', zoom.zoomOut); - useHotKey('app.zoom_reset', zoom.zoomReset); - useListenToTauriEvent('zoom_reset', zoom.zoomReset); - const prompt = usePrompt(); useListenToTauriEvent<{ replyId: string; args: PromptTextRequest }>( 'show_prompt', @@ -192,46 +39,5 @@ export function GlobalHooks() { }, ); - const copy = useCopy(); - useListenToTauriEvent('generate_theme_css', () => { - const themesCss = [ - yaakDark, - monokaiProDefault, - rosePineDefault, - catppuccinMacchiato, - githubLight, - hotdogStandDefault, - ] - .map(getThemeCSS) - .join('\n\n'); - copy(themesCss); - }); - return null; } - -function updateModelList(model: T, pushToFront: boolean) { - return (current: T[]): T[] => { - const index = current.findIndex((v) => modelsEq(v, model)) ?? -1; - if (index >= 0) { - return [...current.slice(0, index), model, ...current.slice(index + 1)]; - } else { - return pushToFront ? [model, ...(current ?? [])] : [...(current ?? []), model]; - } - }; -} - -function removeById(model: T) { - return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? []; -} - -const shouldIgnoreModel = (payload: AnyModel, windowLabel: string) => { - if (windowLabel === getCurrentWebviewWindow().label) { - // Never ignore same-window updates - return false; - } - if (payload.model === 'key_value') { - return payload.namespace === 'no_sync'; - } - return false; -}; diff --git a/src-web/components/GrpcConnectionLayout.tsx b/src-web/components/GrpcConnectionLayout.tsx index e124ea26..c0d73726 100644 --- a/src-web/components/GrpcConnectionLayout.tsx +++ b/src-web/components/GrpcConnectionLayout.tsx @@ -22,7 +22,7 @@ const emptyArray: string[] = []; export function GrpcConnectionLayout({ style }: Props) { const activeRequest = useActiveRequest('grpc_request'); const updateRequest = useUpdateAnyGrpcRequest(); - const connections = useGrpcConnections(activeRequest?.id ?? null); + const connections = useGrpcConnections().filter((c) => c.requestId === activeRequest?.id); const activeConnection = connections[0] ?? null; const messages = useGrpcEvents(activeConnection?.id ?? null); const protoFilesKv = useGrpcProtoFiles(activeRequest?.id ?? null); diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 98236f99..68a726e9 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -1,8 +1,10 @@ import type { AnyModel, Folder, + GrpcConnection, GrpcRequest, HttpRequest, + HttpResponse, Workspace, } from '@yaakapp-internal/models'; import classNames from 'classnames'; @@ -22,18 +24,19 @@ import { useDeleteRequest } from '../hooks/useDeleteRequest'; import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest'; import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest'; import { useFolders } from '../hooks/useFolders'; +import { useGrpcConnections } from '../hooks/useGrpcConnections'; import { useHotKey } from '../hooks/useHotKey'; +import type { CallableHttpRequestAction } from '../hooks/useHttpRequestActions'; import { useHttpRequestActions } from '../hooks/useHttpRequestActions'; +import { useHttpResponses } from '../hooks/useHttpResponses'; import { useKeyValue } from '../hooks/useKeyValue'; -import { useLatestGrpcConnection } from '../hooks/useLatestGrpcConnection'; -import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse'; import { useMoveToWorkspace } from '../hooks/useMoveToWorkspace'; import { usePrompt } from '../hooks/usePrompt'; import { useRenameRequest } from '../hooks/useRenameRequest'; import { useRequests } from '../hooks/useRequests'; import { useScrollIntoView } from '../hooks/useScrollIntoView'; import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; -import { useSendManyRequests } from '../hooks/useSendFolder'; +import { useSendManyRequests } from '../hooks/useSendManyRequests'; import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder'; import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest'; @@ -73,6 +76,9 @@ export function Sidebar({ className }: Props) { const folders = useFolders(); const requests = useRequests(); const activeWorkspace = useActiveWorkspace(); + const httpRequestActions = useHttpRequestActions(); + const httpResponses = useHttpResponses(); + const grpcConnections = useGrpcConnections(); const duplicateHttpRequest = useDuplicateHttpRequest({ id: activeRequest?.id ?? null, navigateAfter: true, @@ -453,6 +459,9 @@ export function Sidebar({ className }: Props) { selectedId={selectedId} selectedTree={selectedTree} isCollapsed={isCollapsed} + httpRequestActions={httpRequestActions} + httpResponses={httpResponses} + grpcConnections={grpcConnections} tree={tree} focused={hasFocus} draggingId={draggingId} @@ -483,6 +492,9 @@ interface SidebarItemsProps { handleDragStart: (id: string) => void; onSelect: (requestId: string) => void; isCollapsed: (id: string) => boolean; + httpRequestActions: CallableHttpRequestAction[]; + httpResponses: HttpResponse[]; + grpcConnections: GrpcConnection[]; } function SidebarItems({ @@ -500,6 +512,9 @@ function SidebarItems({ handleEnd, handleMove, handleDragStart, + httpRequestActions, + httpResponses, + grpcConnections, }: SidebarItemsProps) { return ( ) } + httpRequestActions={httpRequestActions} + latestHttpResponse={httpResponses.find((r) => r.requestId === child.item.id) ?? null} + latestGrpcConnection={ + grpcConnections.find((c) => c.requestId === child.item.id) ?? null + } onMove={handleMove} onEnd={handleEnd} onSelect={onSelect} @@ -549,20 +569,23 @@ function SidebarItems({ !isCollapsed(child.item.id) && draggingId !== child.item.id && ( )} @@ -590,7 +613,9 @@ type SidebarItemProps = { onDragStart: (id: string) => void; children?: ReactNode; child: TreeNode; -} & Pick; + latestHttpResponse: HttpResponse | null; + latestGrpcConnection: GrpcConnection | null; +} & Pick; type DragItem = { id: string; @@ -612,6 +637,9 @@ function SidebarItem({ selected, itemFallbackName, useProminentStyles, + latestHttpResponse, + latestGrpcConnection, + httpRequestActions, children, }: SidebarItemProps) { const ref = useRef(null); @@ -659,14 +687,9 @@ function SidebarItem({ const renameRequest = useRenameRequest(itemId); const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true }); const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true }); - const httpRequestActions = useHttpRequestActions(); const sendRequest = useSendAnyHttpRequest(); const moveToWorkspace = useMoveToWorkspace(itemId); const sendManyRequests = useSendManyRequests(); - const latestHttpResponse = useLatestHttpResponse(itemModel === 'http_request' ? itemId : null); - const latestGrpcConnection = useLatestGrpcConnection( - itemModel === 'grpc_request' ? itemId : null, - ); const updateHttpRequest = useUpdateAnyHttpRequest(); const workspaces = useWorkspaces(); const updateGrpcRequest = useUpdateAnyGrpcRequest(); diff --git a/src-web/font-size.ts b/src-web/font-size.ts index 6f2f921d..522b26a5 100644 --- a/src-web/font-size.ts +++ b/src-web/font-size.ts @@ -1,6 +1,6 @@ // Listen for settings changes, the re-compute theme import { listen } from '@tauri-apps/api/event'; -import type { ModelPayload } from './components/GlobalHooks'; +import type { ModelPayload } from './hooks/useSyncModelStores'; import { getSettings } from './lib/store'; function setFontSizeOnDocument(fontSize: number) { diff --git a/src-web/hooks/useActiveCookieJar.ts b/src-web/hooks/useActiveCookieJar.ts index 5e39fe07..81152a24 100644 --- a/src-web/hooks/useActiveCookieJar.ts +++ b/src-web/hooks/useActiveCookieJar.ts @@ -1,8 +1,6 @@ import { useCallback, useEffect, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { useActiveWorkspace } from './useActiveWorkspace'; import { useCookieJars } from './useCookieJars'; -import { useKeyValue } from './useKeyValue'; export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id'; @@ -11,9 +9,8 @@ export function useActiveCookieJar() { const cookieJars = useCookieJars(); const activeCookieJar = useMemo(() => { - if (cookieJars.data == null) return undefined; - return cookieJars.data.find((cookieJar) => cookieJar.id === activeCookieJarId) ?? null; - }, [activeCookieJarId, cookieJars.data]); + return cookieJars.find((cookieJar) => cookieJar.id === activeCookieJarId) ?? null; + }, [activeCookieJarId, cookieJars]); return [activeCookieJar ?? null, setActiveCookieJarId] as const; } @@ -22,13 +19,11 @@ export function useEnsureActiveCookieJar() { const cookieJars = useCookieJars(); const [activeCookieJarId, setActiveCookieJarId] = useActiveCookieJarId(); useEffect(() => { - if (cookieJars.data == null) return; - - if (cookieJars.data.find((j) => j.id === activeCookieJarId)) { + if (cookieJars.find((j) => j.id === activeCookieJarId)) { return; // There's an active jar } - const firstJar = cookieJars.data[0]; + const firstJar = cookieJars[0]; if (firstJar == null) { console.log("Workspace doesn't have any cookie jars to activate"); return; @@ -37,35 +32,7 @@ export function useEnsureActiveCookieJar() { // There's no active jar, so set it to the first one console.log('Setting active cookie jar to', firstJar.id); setActiveCookieJarId(firstJar.id); - }, [activeCookieJarId, cookieJars, cookieJars.data, setActiveCookieJarId]); -} - -export function useMigrateActiveCookieJarId() { - const workspace = useActiveWorkspace(); - const [, setActiveCookieJarId] = useActiveCookieJarId(); - const { - set: setLegacyActiveCookieJarId, - value: legacyActiveCookieJarId, - isLoading: isLoadingLegacyActiveCookieJarId, - } = useKeyValue({ - namespace: 'global', - key: ['activeCookieJar', workspace?.id ?? 'n/a'], - fallback: null, - }); - - useEffect(() => { - if (legacyActiveCookieJarId == null || isLoadingLegacyActiveCookieJarId) return; - - console.log('Migrating active cookie jar ID to query param', legacyActiveCookieJarId); - setActiveCookieJarId(legacyActiveCookieJarId); - setLegacyActiveCookieJarId(null).catch(console.error); - }, [ - workspace, - isLoadingLegacyActiveCookieJarId, - legacyActiveCookieJarId, - setActiveCookieJarId, - setLegacyActiveCookieJarId, - ]); + }, [activeCookieJarId, cookieJars, setActiveCookieJarId]); } function useActiveCookieJarId() { diff --git a/src-web/hooks/useCookieJars.ts b/src-web/hooks/useCookieJars.ts index a85a55d9..2e9fbf2d 100644 --- a/src-web/hooks/useCookieJars.ts +++ b/src-web/hooks/useCookieJars.ts @@ -1,22 +1,8 @@ -import { useQuery } from '@tanstack/react-query'; import type { CookieJar } from '@yaakapp-internal/models'; -import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspace } from './useActiveWorkspace'; +import { atom, useAtomValue } from 'jotai'; -export function cookieJarsQueryKey({ workspaceId }: { workspaceId: string }) { - return ['cookie_jars', { workspaceId }]; -} +export const cookieJarsAtom = atom([]); export function useCookieJars() { - const workspace = useActiveWorkspace(); - return useQuery({ - enabled: workspace != null, - queryKey: cookieJarsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }), - queryFn: async () => { - if (workspace == null) return []; - return (await invokeCmd('cmd_list_cookie_jars', { - workspaceId: workspace.id, - })) as CookieJar[]; - }, - }); + return useAtomValue(cookieJarsAtom); } diff --git a/src-web/hooks/useEnvironments.ts b/src-web/hooks/useEnvironments.ts index a1f7b396..469afce8 100644 --- a/src-web/hooks/useEnvironments.ts +++ b/src-web/hooks/useEnvironments.ts @@ -1,20 +1,9 @@ import type { Environment } from '@yaakapp-internal/models'; -import { atom, useAtom } from 'jotai/index'; -import { useEffect } from 'react'; -import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspace } from './useActiveWorkspace'; +import { useAtomValue } from 'jotai'; +import { atom } from 'jotai/index'; export const environmentsAtom = atom([]); export function useEnvironments() { - const [items, setItems] = useAtom(environmentsAtom); - const workspace = useActiveWorkspace(); - - // Fetch new requests when workspace changes - useEffect(() => { - if (workspace == null) return; - invokeCmd('cmd_list_environments', { workspaceId: workspace.id }).then(setItems); - }, [setItems, workspace]); - - return items; + return useAtomValue(environmentsAtom); } diff --git a/src-web/hooks/useFolders.ts b/src-web/hooks/useFolders.ts index 2dbfd504..4589f84f 100644 --- a/src-web/hooks/useFolders.ts +++ b/src-web/hooks/useFolders.ts @@ -1,22 +1,9 @@ -import { useQuery } from '@tanstack/react-query'; import type { Folder } from '@yaakapp-internal/models'; -import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspace } from './useActiveWorkspace'; +import { useAtomValue } from 'jotai'; +import { atom } from 'jotai/index'; -export function foldersQueryKey({ workspaceId }: { workspaceId: string }) { - return ['folders', { workspaceId }]; -} +export const foldersAtom = atom([]); export function useFolders() { - const workspace = useActiveWorkspace(); - return ( - useQuery({ - enabled: workspace != null, - queryKey: foldersQueryKey({ workspaceId: workspace?.id ?? 'n/a' }), - queryFn: async () => { - if (workspace == null) return []; - return (await invokeCmd('cmd_list_folders', { workspaceId: workspace.id })) as Folder[]; - }, - }).data ?? [] - ); + return useAtomValue(foldersAtom); } diff --git a/src-web/hooks/useGenerateThemeCss.ts b/src-web/hooks/useGenerateThemeCss.ts new file mode 100644 index 00000000..0c209449 --- /dev/null +++ b/src-web/hooks/useGenerateThemeCss.ts @@ -0,0 +1,26 @@ +import { catppuccinMacchiato } from '../lib/theme/themes/catppuccin'; +import { githubLight } from '../lib/theme/themes/github'; +import { hotdogStandDefault } from '../lib/theme/themes/hotdog-stand'; +import { monokaiProDefault } from '../lib/theme/themes/monokai-pro'; +import { rosePineDefault } from '../lib/theme/themes/rose-pine'; +import { yaakDark } from '../lib/theme/themes/yaak'; +import { getThemeCSS } from '../lib/theme/window'; +import { useCopy } from './useCopy'; +import { useListenToTauriEvent } from './useListenToTauriEvent'; + +export function useGenerateThemeCss() { + const copy = useCopy(); + useListenToTauriEvent('generate_theme_css', () => { + const themesCss = [ + yaakDark, + monokaiProDefault, + rosePineDefault, + catppuccinMacchiato, + githubLight, + hotdogStandDefault, + ] + .map(getThemeCSS) + .join('\n\n'); + copy(themesCss); + }); +} diff --git a/src-web/hooks/useGrpcConnections.ts b/src-web/hooks/useGrpcConnections.ts index 83a8e0f0..5fbe5909 100644 --- a/src-web/hooks/useGrpcConnections.ts +++ b/src-web/hooks/useGrpcConnections.ts @@ -1,24 +1,8 @@ -import { useQuery } from '@tanstack/react-query'; import type { GrpcConnection } from '@yaakapp-internal/models'; -import { invokeCmd } from '../lib/tauri'; +import { atom, useAtomValue } from 'jotai/index'; -export function grpcConnectionsQueryKey({ requestId }: { requestId: string }) { - return ['grpc_connections', { requestId }]; -} +export const grpcConnectionsAtom = atom([]); -export function useGrpcConnections(requestId: string | null) { - return ( - useQuery({ - enabled: requestId !== null, - initialData: [], - queryKey: grpcConnectionsQueryKey({ requestId: requestId ?? 'n/a' }), - queryFn: async () => { - if (requestId == null) return []; - return (await invokeCmd('cmd_list_grpc_connections', { - requestId, - limit: 200, - })) as GrpcConnection[]; - }, - }).data ?? [] - ); +export function useGrpcConnections() { + return useAtomValue(grpcConnectionsAtom); } diff --git a/src-web/hooks/useGrpcRequests.ts b/src-web/hooks/useGrpcRequests.ts index f7efa5cc..53259607 100644 --- a/src-web/hooks/useGrpcRequests.ts +++ b/src-web/hooks/useGrpcRequests.ts @@ -1,22 +1,8 @@ import type { GrpcRequest } from '@yaakapp-internal/models'; -import { atom, useAtom } from 'jotai'; -import { useEffect } from 'react'; -import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspace } from './useActiveWorkspace'; +import { atom, useAtomValue } from 'jotai'; export const grpcRequestsAtom = atom([]); export function useGrpcRequests() { - const [items, setItems] = useAtom(grpcRequestsAtom); - const workspace = useActiveWorkspace(); - - // Fetch new requests when workspace changes - useEffect(() => { - if (workspace == null) return; - invokeCmd('cmd_list_grpc_requests', { workspaceId: workspace.id }).then( - setItems, - ); - }, [setItems, workspace]); - - return items; + return useAtomValue(grpcRequestsAtom); } diff --git a/src-web/hooks/useHttpRequestActions.ts b/src-web/hooks/useHttpRequestActions.ts index ada41322..0c4bfcb3 100644 --- a/src-web/hooks/useHttpRequestActions.ts +++ b/src-web/hooks/useHttpRequestActions.ts @@ -3,10 +3,15 @@ import type { HttpRequest } from '@yaakapp-internal/models'; import type { CallHttpRequestActionRequest, GetHttpRequestActionsResponse, + HttpRequestAction, } from '@yaakapp-internal/plugin'; import { invokeCmd } from '../lib/tauri'; import { usePluginsKey } from './usePlugins'; +export type CallableHttpRequestAction = Pick & { + call: (httpRequest: HttpRequest) => Promise; +}; + export function useHttpRequestActions() { const pluginsKey = usePluginsKey(); @@ -20,7 +25,7 @@ export function useHttpRequestActions() { }, }); - return ( + const actions: CallableHttpRequestAction[] = httpRequestActions.data?.flatMap((r) => r.actions.map((a) => ({ key: a.key, @@ -35,6 +40,7 @@ export function useHttpRequestActions() { await invokeCmd('cmd_call_http_request_action', { req: payload }); }, })), - ) ?? [] - ); + ) ?? []; + + return actions; } diff --git a/src-web/hooks/useHttpRequests.ts b/src-web/hooks/useHttpRequests.ts index a6445759..920fae8d 100644 --- a/src-web/hooks/useHttpRequests.ts +++ b/src-web/hooks/useHttpRequests.ts @@ -1,21 +1,8 @@ import type { HttpRequest } from '@yaakapp-internal/models'; -import { atom, useAtom } from 'jotai'; -import { useEffect } from 'react'; -import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspace } from './useActiveWorkspace'; +import { atom, useAtomValue } from 'jotai'; export const httpRequestsAtom = atom([]); export function useHttpRequests() { - const [items, setItems] = useAtom(httpRequestsAtom); - const workspace = useActiveWorkspace(); - - useEffect(() => { - if (workspace == null) return; - invokeCmd('cmd_list_http_requests', { workspaceId: workspace.id }).then( - setItems, - ); - }, [setItems, workspace]); - - return items; + return useAtomValue(httpRequestsAtom); } diff --git a/src-web/hooks/useHttpResponses.ts b/src-web/hooks/useHttpResponses.ts index d67726eb..a4eda493 100644 --- a/src-web/hooks/useHttpResponses.ts +++ b/src-web/hooks/useHttpResponses.ts @@ -1,24 +1,9 @@ -import { useQuery } from '@tanstack/react-query'; import type { HttpResponse } from '@yaakapp-internal/models'; -import { invokeCmd } from '../lib/tauri'; +import { useAtomValue } from 'jotai'; +import { atom } from 'jotai/index'; -export function httpResponsesQueryKey({ requestId }: { requestId: string }) { - return ['http_responses', { requestId }]; -} +export const httpResponsesAtom = atom([]); -export function useHttpResponses(requestId: string | null) { - return ( - useQuery({ - enabled: requestId !== null, - initialData: [], - queryKey: httpResponsesQueryKey({ requestId: requestId ?? 'n/a' }), - queryFn: async () => { - if (requestId == null) return []; - return (await invokeCmd('cmd_list_http_responses', { - requestId, - limit: 200, - })) as HttpResponse[]; - }, - }).data ?? [] - ); +export function useHttpResponses() { + return useAtomValue(httpResponsesAtom); } diff --git a/src-web/hooks/useLatestGrpcConnection.ts b/src-web/hooks/useLatestGrpcConnection.ts index b7fe9d8f..7c0b9db9 100644 --- a/src-web/hooks/useLatestGrpcConnection.ts +++ b/src-web/hooks/useLatestGrpcConnection.ts @@ -2,6 +2,5 @@ import type { GrpcConnection } from '@yaakapp-internal/models'; import { useGrpcConnections } from './useGrpcConnections'; export function useLatestGrpcConnection(requestId: string | null): GrpcConnection | null { - const connections = useGrpcConnections(requestId); - return connections[0] ?? null; + return useGrpcConnections().find((c) => c.requestId === requestId) ?? null; } diff --git a/src-web/hooks/useLatestHttpResponse.ts b/src-web/hooks/useLatestHttpResponse.ts index adfdbe6c..f29846aa 100644 --- a/src-web/hooks/useLatestHttpResponse.ts +++ b/src-web/hooks/useLatestHttpResponse.ts @@ -2,6 +2,5 @@ import type { HttpResponse } from '@yaakapp-internal/models'; import { useHttpResponses } from './useHttpResponses'; export function useLatestHttpResponse(requestId: string | null): HttpResponse | null { - const responses = useHttpResponses(requestId); - return responses[0] ?? null; + return useHttpResponses().find((r) => r.requestId === requestId) ?? null; } diff --git a/src-web/hooks/usePinnedGrpcConnection.ts b/src-web/hooks/usePinnedGrpcConnection.ts index a062a924..3c848fdf 100644 --- a/src-web/hooks/usePinnedGrpcConnection.ts +++ b/src-web/hooks/usePinnedGrpcConnection.ts @@ -11,7 +11,7 @@ export function usePinnedGrpcConnection(activeRequest: GrpcRequest) { fallback: null, namespace: 'global', }); - const connections = useGrpcConnections(activeRequest.id); + const connections = useGrpcConnections().filter((c) => c.requestId === activeRequest.id); const activeConnection: GrpcConnection | null = connections.find((r) => r.id === pinnedConnectionId) ?? latestConnection; diff --git a/src-web/hooks/usePinnedHttpResponse.ts b/src-web/hooks/usePinnedHttpResponse.ts index 00f44731..037d605e 100644 --- a/src-web/hooks/usePinnedHttpResponse.ts +++ b/src-web/hooks/usePinnedHttpResponse.ts @@ -6,12 +6,13 @@ import { useLatestHttpResponse } from './useLatestHttpResponse'; export function usePinnedHttpResponse(activeRequest: HttpRequest) { const latestResponse = useLatestHttpResponse(activeRequest.id); const { set, value: pinnedResponseId } = useKeyValue({ - // Key on latest response instead of activeRequest because responses change out of band of active request + // Key on the latest response instead of activeRequest because responses change out of band of active request key: ['pinned_http_response_id', latestResponse?.id ?? 'n/a'], fallback: null, namespace: 'global', }); - const responses = useHttpResponses(activeRequest.id); + const allResponses = useHttpResponses(); + const responses = allResponses.filter((r) => r.requestId === activeRequest.id); const activeResponse: HttpResponse | null = responses.find((r) => r.id === pinnedResponseId) ?? latestResponse; diff --git a/src-web/hooks/useRecentCookieJars.ts b/src-web/hooks/useRecentCookieJars.ts index 26bbcbea..00a97668 100644 --- a/src-web/hooks/useRecentCookieJars.ts +++ b/src-web/hooks/useRecentCookieJars.ts @@ -31,7 +31,7 @@ export function useRecentCookieJars() { }, [activeCookieJarId]); const onlyValidIds = useMemo( - () => kv.value?.filter((id) => cookieJars.data?.some((e) => e.id === id)) ?? [], + () => kv.value?.filter((id) => cookieJars.some((e) => e.id === id)) ?? [], [kv.value, cookieJars], ); diff --git a/src-web/hooks/useSendAnyHttpRequest.ts b/src-web/hooks/useSendAnyHttpRequest.ts index f0b007fa..1bbe2bf5 100644 --- a/src-web/hooks/useSendAnyHttpRequest.ts +++ b/src-web/hooks/useSendAnyHttpRequest.ts @@ -1,21 +1,20 @@ import { useMutation } from '@tanstack/react-query'; import type { HttpResponse } from '@yaakapp-internal/models'; import { trackEvent } from '../lib/analytics'; +import { getHttpRequest } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; import { useActiveCookieJar } from './useActiveCookieJar'; import { useActiveEnvironment } from './useActiveEnvironment'; import { useAlert } from './useAlert'; -import { useHttpRequests } from './useHttpRequests'; export function useSendAnyHttpRequest() { - const [environment] = useActiveEnvironment(); const alert = useAlert(); + const [environment] = useActiveEnvironment(); const [activeCookieJar] = useActiveCookieJar(); - const requests = useHttpRequests(); return useMutation({ mutationKey: ['send_any_request'], mutationFn: async (id) => { - const request = requests.find((r) => r.id === id) ?? null; + const request = await getHttpRequest(id); if (request == null) { return null; } diff --git a/src-web/hooks/useSendFolder.ts b/src-web/hooks/useSendManyRequests.ts similarity index 100% rename from src-web/hooks/useSendFolder.ts rename to src-web/hooks/useSendManyRequests.ts diff --git a/src-web/hooks/useSyncFontSizeSetting.ts b/src-web/hooks/useSyncFontSizeSetting.ts new file mode 100644 index 00000000..7ddeba2e --- /dev/null +++ b/src-web/hooks/useSyncFontSizeSetting.ts @@ -0,0 +1,16 @@ +import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; +import { useEffect } from 'react'; +import { useSettings } from './useSettings'; + +export function useSyncFontSizeSetting() { + const settings = useSettings(); + useEffect(() => { + if (settings == null) { + return; + } + + const { interfaceScale, editorFontSize } = settings; + getCurrentWebviewWindow().setZoom(interfaceScale).catch(console.error); + document.documentElement.style.setProperty('--editor-font-size', `${editorFontSize}px`); + }, [settings]); +} diff --git a/src-web/hooks/useSyncModelStores.ts b/src-web/hooks/useSyncModelStores.ts new file mode 100644 index 00000000..7cb99196 --- /dev/null +++ b/src-web/hooks/useSyncModelStores.ts @@ -0,0 +1,155 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; +import type { AnyModel } from '@yaakapp-internal/models'; +import { useSetAtom } from 'jotai/index'; +import { extractKeyValue } from '../lib/keyValueStore'; +import { modelsEq } from '../lib/model_util'; +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 } from './useKeyValue'; +import { useListenToTauriEvent } from './useListenToTauriEvent'; +import { pluginsAtom } from './usePlugins'; +import { useRequestUpdateKey } from './useRequestUpdateKey'; +import { settingsAtom } from './useSettings'; +import { workspacesAtom } from './useWorkspaces'; + +export interface ModelPayload { + model: AnyModel; + windowLabel: string; +} + +export function useSyncModelStores() { + const queryClient = useQueryClient(); + const { wasUpdatedExternally } = useRequestUpdateKey(null); + + const setSettings = useSetAtom(settingsAtom); + const setWorkspaces = useSetAtom(workspacesAtom); + const setCookieJars = useSetAtom(cookieJarsAtom); + const setFolders = useSetAtom(foldersAtom); + const setPlugins = useSetAtom(pluginsAtom); + const setHttpRequests = useSetAtom(httpRequestsAtom); + const setHttpResponses = useSetAtom(httpResponsesAtom); + const setGrpcConnections = useSetAtom(grpcConnectionsAtom); + const setGrpcRequests = useSetAtom(grpcRequestsAtom); + const setEnvironments = useSetAtom(environmentsAtom); + + useListenToTauriEvent('upserted_model', ({ payload }) => { + if (payload.model.model !== 'key_value') { + console.log('Upserted model', payload.model); + } + const { model, windowLabel } = payload; + const queryKey = + model.model === 'grpc_event' + ? grpcEventsQueryKey(model) + : model.model === 'key_value' + ? keyValueQueryKey(model) + : null; + + if (model.model === 'http_request' && windowLabel !== getCurrentWebviewWindow().label) { + wasUpdatedExternally(model.id); + } + + const pushToFront = (['http_response', 'grpc_connection'] as AnyModel['model'][]).includes( + model.model, + ); + + if (shouldIgnoreModel(model, windowLabel)) return; + + if (model.model === 'workspace') { + setWorkspaces(updateModelList(model, pushToFront)); + } else if (model.model === 'plugin') { + setPlugins(updateModelList(model, pushToFront)); + } else if (model.model === 'http_request') { + setHttpRequests(updateModelList(model, pushToFront)); + } else if (model.model === 'folder') { + setFolders(updateModelList(model, pushToFront)); + } else if (model.model === 'http_response') { + setHttpResponses(updateModelList(model, pushToFront)); + } else if (model.model === 'grpc_request') { + setGrpcRequests(updateModelList(model, pushToFront)); + } else if (model.model === 'grpc_connection') { + setGrpcConnections(updateModelList(model, pushToFront)); + } else if (model.model === 'environment') { + setEnvironments(updateModelList(model, pushToFront)); + } else if (model.model === 'cookie_jar') { + setCookieJars(updateModelList(model, pushToFront)); + } else if (model.model === 'settings') { + setSettings(model); + } else if (queryKey != null) { + // TODO: Convert all models to use Jotai + queryClient.setQueryData(queryKey, (current: unknown) => { + if (model.model === 'key_value') { + // Special-case for KeyValue + return extractKeyValue(model); + } + + if (Array.isArray(current)) { + return updateModelList(model, pushToFront)(current); + } + }); + } + }); + + useListenToTauriEvent('deleted_model', ({ payload }) => { + const { model, windowLabel } = payload; + if (shouldIgnoreModel(model, windowLabel)) return; + + console.log('Delete model', payload.model); + + if (model.model === 'workspace') { + setWorkspaces(removeById(model)); + } else if (model.model === 'plugin') { + setPlugins(removeById(model)); + } else if (model.model === 'http_request') { + setHttpRequests(removeById(model)); + } else if (model.model === 'http_response') { + setHttpResponses(removeById(model)); + } else if (model.model === 'folder') { + setFolders(removeById(model)); + } else if (model.model === 'environment') { + setEnvironments(removeById(model)); + } else if (model.model === 'grpc_request') { + setGrpcRequests(removeById(model)); + } else if (model.model === 'grpc_connection') { + setGrpcConnections(removeById(model)); + } else if (model.model === 'grpc_event') { + queryClient.setQueryData(grpcEventsQueryKey(model), removeById(model)); + } else if (model.model === 'key_value') { + queryClient.setQueryData(keyValueQueryKey(model), undefined); + } else if (model.model === 'cookie_jar') { + setCookieJars(removeById(model)); + } + }); +} + +function updateModelList(model: T, pushToFront: boolean) { + return (current: T[]): T[] => { + const index = current.findIndex((v) => modelsEq(v, model)) ?? -1; + if (index >= 0) { + return [...current.slice(0, index), model, ...current.slice(index + 1)]; + } else { + return pushToFront ? [model, ...(current ?? [])] : [...(current ?? []), model]; + } + }; +} + +function removeById(model: T) { + return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? []; +} + +const shouldIgnoreModel = (payload: AnyModel, windowLabel: string) => { + if (windowLabel === getCurrentWebviewWindow().label) { + // Never ignore same-window updates + return false; + } + if (payload.model === 'key_value') { + return payload.namespace === 'no_sync'; + } + return false; +}; diff --git a/src-web/hooks/useSyncWorkspaceChildModels.ts b/src-web/hooks/useSyncWorkspaceChildModels.ts new file mode 100644 index 00000000..ef87cf07 --- /dev/null +++ b/src-web/hooks/useSyncWorkspaceChildModels.ts @@ -0,0 +1,40 @@ +import { useSetAtom } from 'jotai/index'; +import { useEffect } from 'react'; +import { invokeCmd } from '../lib/tauri'; +import { useActiveWorkspace } 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'; + +export function useSyncWorkspaceChildModels() { + const setCookieJars = useSetAtom(cookieJarsAtom); + const setFolders = useSetAtom(foldersAtom); + const setHttpRequests = useSetAtom(httpRequestsAtom); + const setHttpResponses = useSetAtom(httpResponsesAtom); + const setGrpcConnections = useSetAtom(grpcConnectionsAtom); + const setGrpcRequests = useSetAtom(grpcRequestsAtom); + const setEnvironments = useSetAtom(environmentsAtom); + + const workspace = useActiveWorkspace(); + const workspaceId = workspace?.id ?? 'n/a'; + useEffect(() => { + (async function () { + // Set the things we need first, first + setHttpRequests(await invokeCmd('cmd_list_http_requests', { workspaceId })); + setGrpcRequests(await invokeCmd('cmd_list_grpc_requests', { workspaceId })); + setFolders(await invokeCmd('cmd_list_folders', { workspaceId })); + + // Then, set the rest + setCookieJars(await invokeCmd('cmd_list_cookie_jars', { workspaceId })); + setHttpResponses(await invokeCmd('cmd_list_http_responses', { workspaceId })); + setGrpcConnections(await invokeCmd('cmd_list_grpc_connections', { workspaceId })); + setEnvironments(await invokeCmd('cmd_list_environments', { workspaceId })); + })().catch(console.error); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [workspaceId]); +} diff --git a/src-web/hooks/useSyncZoomSetting.ts b/src-web/hooks/useSyncZoomSetting.ts new file mode 100644 index 00000000..89f44b3b --- /dev/null +++ b/src-web/hooks/useSyncZoomSetting.ts @@ -0,0 +1,16 @@ +import { useHotKey } from './useHotKey'; +import { useListenToTauriEvent } from './useListenToTauriEvent'; +import { useZoom } from './useZoom'; + +export function useSyncZoomSetting() { + // Handle Zoom. + // Note, Mac handles it in the app menu, so need to also handle keyboard + // shortcuts for Windows/Linux + const zoom = useZoom(); + useHotKey('app.zoom_in', zoom.zoomIn); + useListenToTauriEvent('zoom_in', zoom.zoomIn); + useHotKey('app.zoom_out', zoom.zoomOut); + useListenToTauriEvent('zoom_out', zoom.zoomOut); + useHotKey('app.zoom_reset', zoom.zoomReset); + useListenToTauriEvent('zoom_reset', zoom.zoomReset); +} diff --git a/src-web/package.json b/src-web/package.json index 2de35657..832486e2 100644 --- a/src-web/package.json +++ b/src-web/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "type": "module", "scripts": { - "dev": "vite dev", + "dev": "vite dev --force", "build": "vite build", "lint": "tsc && eslint . --ext .ts,.tsx" }, diff --git a/src-web/theme.ts b/src-web/theme.ts index e376235f..77cb0e52 100644 --- a/src-web/theme.ts +++ b/src-web/theme.ts @@ -1,6 +1,6 @@ import { emit, listen } from '@tauri-apps/api/event'; import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; -import type { ModelPayload } from './components/GlobalHooks'; +import type { ModelPayload } from './hooks/useSyncModelStores'; import { getSettings } from './lib/store'; import type { Appearance } from './lib/theme/appearance'; import { getCSSAppearance, subscribeToPreferredAppearance } from './lib/theme/appearance';