Optimize sidebar collapsing

This commit is contained in:
Gregory Schier
2024-12-23 05:05:04 -08:00
parent 61d094d9fd
commit 31f2bff0f6
35 changed files with 402 additions and 238 deletions
+11 -9
View File
@@ -44,11 +44,7 @@ use crate::render::{render_grpc_request, render_http_request, render_json_value,
use crate::template_callback::PluginTemplateCallback; use crate::template_callback::PluginTemplateCallback;
use crate::updates::{UpdateMode, YaakUpdater}; use crate::updates::{UpdateMode, YaakUpdater};
use crate::window_menu::app_menu; use crate::window_menu::app_menu;
use yaak_models::models::{ use yaak_models::models::{CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, KeyValue, KeyValueIden, ModelType, Plugin, Settings, Workspace};
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcConnectionState,
GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, KeyValue,
ModelType, Plugin, Settings, Workspace,
};
use yaak_models::queries::{ use yaak_models::queries::{
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response, cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
delete_all_grpc_connections, delete_all_grpc_connections_for_workspace, delete_all_grpc_connections, delete_all_grpc_connections_for_workspace,
@@ -61,10 +57,10 @@ use yaak_models::queries::{
get_key_value_raw, get_or_create_settings, get_plugin, get_workspace, list_cookie_jars, get_key_value_raw, get_or_create_settings, get_plugin, get_workspace, list_cookie_jars,
list_environments, list_folders, list_grpc_connections_for_workspace, list_grpc_events, list_environments, list_folders, list_grpc_connections_for_workspace, list_grpc_events,
list_grpc_requests, list_http_requests, list_http_responses_for_request, list_grpc_requests, list_http_requests, list_http_responses_for_request,
list_http_responses_for_workspace, list_plugins, list_workspaces, set_key_value_raw, list_http_responses_for_workspace, list_key_values_raw, list_plugins, list_workspaces,
update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar,
upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event,
upsert_plugin, upsert_workspace, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
}; };
use yaak_plugin_runtime::events::{ use yaak_plugin_runtime::events::{
BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse, BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
@@ -1513,6 +1509,11 @@ async fn cmd_list_cookie_jars(
} }
} }
#[tauri::command]
async fn cmd_list_key_values(w: WebviewWindow) -> Result<Vec<KeyValue>, String> {
list_key_values_raw(&w).await.map_err(|e| e.to_string())
}
#[tauri::command] #[tauri::command]
async fn cmd_get_environment(id: &str, w: WebviewWindow) -> Result<Environment, String> { async fn cmd_get_environment(id: &str, w: WebviewWindow) -> Result<Environment, String> {
get_environment(&w, id).await.map_err(|e| e.to_string()) get_environment(&w, id).await.map_err(|e| e.to_string())
@@ -1797,6 +1798,7 @@ pub fn run() {
cmd_list_grpc_connections, cmd_list_grpc_connections,
cmd_list_grpc_events, cmd_list_grpc_events,
cmd_list_grpc_requests, cmd_list_grpc_requests,
cmd_list_key_values,
cmd_list_http_requests, cmd_list_http_requests,
cmd_list_http_responses, cmd_list_http_responses,
cmd_list_plugins, cmd_list_plugins,
+19 -7
View File
@@ -126,6 +126,18 @@ pub async fn set_key_value_raw<R: Runtime>(
(emit_upserted_model(w, kv), existing.is_none()) (emit_upserted_model(w, kv), existing.is_none())
} }
pub async fn list_key_values_raw<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<KeyValue>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
.from(KeyValueIden::Table)
.column(Asterisk)
.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 get_key_value_raw<R: Runtime>( pub async fn get_key_value_raw<R: Runtime>(
mgr: &impl Manager<R>, mgr: &impl Manager<R>,
namespace: &str, namespace: &str,
@@ -767,7 +779,7 @@ pub async fn list_environments<R: Runtime>(
..Default::default() ..Default::default()
}, },
) )
.await?; .await?;
environments.push(base_environment); environments.push(base_environment);
} }
@@ -857,7 +869,7 @@ pub async fn update_settings<R: Runtime>(
None => None, None => None,
Some(p) => Some(serde_json::to_string(&p)?), Some(p) => Some(serde_json::to_string(&p)?),
}) })
.into(), .into(),
), ),
]) ])
.returning_all() .returning_all()
@@ -1182,7 +1194,7 @@ pub async fn duplicate_folder<R: Runtime>(
..src_folder.clone() ..src_folder.clone()
}, },
) )
.await?; .await?;
for m in http_requests { for m in http_requests {
upsert_http_request( upsert_http_request(
@@ -1194,7 +1206,7 @@ pub async fn duplicate_folder<R: Runtime>(
..m ..m
}, },
) )
.await?; .await?;
} }
for m in grpc_requests { for m in grpc_requests {
upsert_grpc_request( upsert_grpc_request(
@@ -1206,7 +1218,7 @@ pub async fn duplicate_folder<R: Runtime>(
..m ..m
}, },
) )
.await?; .await?;
} }
for m in folders { for m in folders {
// Recurse down // Recurse down
@@ -1217,7 +1229,7 @@ pub async fn duplicate_folder<R: Runtime>(
..m ..m
}, },
)) ))
.await?; .await?;
} }
Ok(()) Ok(())
} }
@@ -1376,7 +1388,7 @@ pub async fn create_default_http_response<R: Runtime>(
None, None,
None, None,
) )
.await .await
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
+6 -3
View File
@@ -64,7 +64,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const recentWorkspaces = useRecentWorkspaces(); const recentWorkspaces = useRecentWorkspaces();
const requests = useRequests(); const requests = useRequests();
const activeRequest = useActiveRequest(); const activeRequest = useActiveRequest();
const recentRequests = useRecentRequests(); const [recentRequests] = useRecentRequests();
const openWorkspace = useOpenWorkspace(); const openWorkspace = useOpenWorkspace();
const createWorkspace = useCreateWorkspace(); const createWorkspace = useCreateWorkspace();
const createHttpRequest = useCreateHttpRequest(); const createHttpRequest = useCreateHttpRequest();
@@ -78,6 +78,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const [, setSidebarHidden] = useSidebarHidden(); const [, setSidebarHidden] = useSidebarHidden();
const openSettings = useOpenSettings(); const openSettings = useOpenSettings();
const navigate = useNavigate(); const navigate = useNavigate();
const { baseEnvironment } = useEnvironments();
const workspaceCommands = useMemo<CommandPaletteItem[]>(() => { const workspaceCommands = useMemo<CommandPaletteItem[]>(() => {
const commands: CommandPaletteItem[] = [ const commands: CommandPaletteItem[] = [
@@ -131,7 +132,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
{ {
key: 'environment.create', key: 'environment.create',
label: 'Create Environment', label: 'Create Environment',
onSelect: createEnvironment.mutate, onSelect: () => createEnvironment.mutate(baseEnvironment),
}, },
{ {
key: 'sidebar.toggle', key: 'sidebar.toggle',
@@ -180,7 +181,8 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
activeCookieJar?.id, activeCookieJar?.id,
activeEnvironment, activeEnvironment,
activeRequest, activeRequest,
createEnvironment.mutate, baseEnvironment,
createEnvironment,
createGrpcRequest, createGrpcRequest,
createHttpRequest, createHttpRequest,
createWorkspace.mutate, createWorkspace.mutate,
@@ -375,6 +377,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => { (e: KeyboardEvent<HTMLInputElement>) => {
const index = filteredAllItems.findIndex((v) => v.key === selectedItem?.key); const index = filteredAllItems.findIndex((v) => v.key === selectedItem?.key);
console.log("ENDER", e.key);
if (e.key === 'ArrowDown' || (e.ctrlKey && e.key === 'n')) { if (e.key === 'ArrowDown' || (e.ctrlKey && e.key === 'n')) {
const next = filteredAllItems[index + 1] ?? filteredAllItems[0]; const next = filteredAllItems[index + 1] ?? filteredAllItems[0];
+11 -6
View File
@@ -1,9 +1,10 @@
import { emit } from '@tauri-apps/api/event'; import { emit } from '@tauri-apps/api/event';
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin'; import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin';
import { useEnsureActiveCookieJar } from '../hooks/useActiveCookieJar'; import { useEnsureActiveCookieJar, useSubscribeActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import {useSubscribeActiveRequestId} from "../hooks/useActiveRequestId"; import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId';
import {useSubscribeActiveWorkspaceId} from "../hooks/useActiveWorkspace"; import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast'; import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest'; import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest'; import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
@@ -14,13 +15,14 @@ import { useNotificationToast } from '../hooks/useNotificationToast';
import { usePrompt } from '../hooks/usePrompt'; import { usePrompt } from '../hooks/usePrompt';
import { useRecentCookieJars } from '../hooks/useRecentCookieJars'; import { useRecentCookieJars } from '../hooks/useRecentCookieJars';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests'; import { useSubscribeRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting'; import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
import { useSyncModelStores } from '../hooks/useSyncModelStores'; import { useSyncModelStores } from '../hooks/useSyncModelStores';
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels'; import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
import {useSyncWorkspaceRequestTitle} from "../hooks/useSyncWorkspaceRequestTitle"; import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTitle';
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting'; import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions';
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette'; import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
export function GlobalHooks() { export function GlobalHooks() {
@@ -36,8 +38,11 @@ export function GlobalHooks() {
useRecentWorkspaces(); useRecentWorkspaces();
useRecentEnvironments(); useRecentEnvironments();
useRecentCookieJars(); useRecentCookieJars();
useRecentRequests(); useSubscribeRecentRequests();
useSyncWorkspaceChildModels(); useSyncWorkspaceChildModels();
useSubscribeTemplateFunctions();
useSubscribeActiveEnvironmentId();
useSubscribeActiveCookieJar();
// Other useful things // Other useful things
useNotificationToast(); useNotificationToast();
@@ -18,7 +18,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
const dropdownRef = useRef<DropdownRef>(null); const dropdownRef = useRef<DropdownRef>(null);
const activeRequest = useActiveRequest(); const activeRequest = useActiveRequest();
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const allRecentRequestIds = useRecentRequests(); const [allRecentRequestIds] = useRecentRequests();
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]); const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
const requests = useRequests(); const requests = useRequests();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -22,18 +22,19 @@ export function RedirectToLatestWorkspace() {
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null; const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null; const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
const requestId = (await getRecentRequests(workspaceId))[0] ?? null; const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
const search = { cookie_jar_id: cookieJarId, environment_id: environmentId };
if (workspaceId != null && requestId != null) { if (workspaceId != null && requestId != null) {
await navigate({ await navigate({
to: '/workspaces/$workspaceId/requests/$requestId', to: '/workspaces/$workspaceId/requests/$requestId',
params: { workspaceId, requestId }, params: { workspaceId, requestId },
search: { cookieJarId, environmentId }, search,
}); });
} else { } else {
await navigate({ await navigate({
to: '/workspaces/$workspaceId', to: '/workspaces/$workspaceId',
params: { workspaceId }, params: { workspaceId },
search: { cookieJarId, environmentId }, search,
}); });
} }
})(); })();
+23 -2
View File
@@ -140,8 +140,28 @@ export const RequestPane = memo(function RequestPane({
value: activeRequest.bodyType, value: activeRequest.bodyType,
items: [ items: [
{ type: 'separator', label: 'Form Data' }, { type: 'separator', label: 'Form Data' },
{ label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED }, {
{ label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART }, label: (
<>
Url Encoded
<CountBadge
count={'form' in activeRequest.body && activeRequest.body.form.length}
/>
</>
),
value: BODY_TYPE_FORM_URLENCODED,
},
{
label: (
<>
Url Encoded
<CountBadge
count={'form' in activeRequest.body && activeRequest.body.form.length}
/>
</>
),
value: BODY_TYPE_FORM_MULTIPART,
},
{ type: 'separator', label: 'Text Content' }, { type: 'separator', label: 'Text Content' },
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL }, { label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
{ label: 'JSON', value: BODY_TYPE_JSON }, { label: 'JSON', value: BODY_TYPE_JSON },
@@ -252,6 +272,7 @@ export const RequestPane = memo(function RequestPane({
[ [
activeRequest.authentication, activeRequest.authentication,
activeRequest.authenticationType, activeRequest.authenticationType,
activeRequest.body,
activeRequest.bodyType, activeRequest.bodyType,
activeRequest.description, activeRequest.description,
activeRequest.headers, activeRequest.headers,
+14 -8
View File
@@ -1,27 +1,33 @@
import { useNavigate } from '@tanstack/react-router';
import { useRouteError } from 'react-router-dom';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { FormattedError } from './core/FormattedError'; import { FormattedError } from './core/FormattedError';
import { Heading } from './core/Heading'; import { Heading } from './core/Heading';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
export default function RouteError() { export default function RouteError({ error }: { error: unknown; reset: () => void }) {
const navigate = useNavigate();
const error = useRouteError();
console.log('Error', error); console.log('Error', error);
const stringified = JSON.stringify(error); const stringified = JSON.stringify(error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const message = (error as any).message ?? stringified; const message = (error as any).message ?? stringified;
const stack =
typeof error === 'object' && error != null && 'stack' in error ? String(error.stack) : null;
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<VStack space={5} className="max-w-[50rem] !h-auto"> <VStack space={5} className="w-[50rem] !h-auto">
<Heading>Route Error 🔥</Heading> <Heading>Route Error 🔥</Heading>
<FormattedError>{message}</FormattedError> <FormattedError>
{message}
{stack && (
<details className="mt-3 select-autotext-xs">
<summary className="!cursor-default !select-none">Stack Trace</summary>
<div className="mt-2 text-xs">{stack}</div>
</details>
)}
</FormattedError>
<VStack space={2}> <VStack space={2}>
<Button <Button
color="primary" color="primary"
onClick={async () => { onClick={async () => {
await navigate({ to: '/workspaces' }); window.location.assign('/');
}} }}
> >
Go Home Go Home
+10 -20
View File
@@ -11,9 +11,9 @@ import { useFolders } from '../hooks/useFolders';
import { useGrpcConnections } from '../hooks/useGrpcConnections'; import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useHotKey } from '../hooks/useHotKey'; import { useHotKey } from '../hooks/useHotKey';
import { useHttpResponses } from '../hooks/useHttpResponses'; import { useHttpResponses } from '../hooks/useHttpResponses';
import { useKeyValue } from '../hooks/useKeyValue';
import { useRequests } from '../hooks/useRequests'; import { useRequests } from '../hooks/useRequests';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { getSidebarCollapsedMap } from '../hooks/useSidebarItemCollapsed';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder'; import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest'; import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
@@ -50,13 +50,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const [hoveredTree, setHoveredTree] = useState<SidebarTreeNode | null>(null); const [hoveredTree, setHoveredTree] = useState<SidebarTreeNode | null>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null); const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const { value: collapsed, set: setCollapsed } = useKeyValue<Record<string, boolean>>({
key: ['sidebar_collapsed', activeWorkspace?.id ?? 'n/a'],
fallback: {},
namespace: 'no_sync',
});
const isCollapsed = useCallback((id: string) => collapsed?.[id] ?? false, [collapsed]);
const { tree, treeParentMap, selectableRequests } = useMemo<{ const { tree, treeParentMap, selectableRequests } = useMemo<{
tree: SidebarTreeNode | null; tree: SidebarTreeNode | null;
@@ -97,7 +90,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const childItems = childrenMap[node.item.id] ?? []; const childItems = childrenMap[node.item.id] ?? [];
// Recurse to children // Recurse to children
const isCollapsed = collapsed?.[node.item.id];
const depth = node.depth + 1; const depth = node.depth + 1;
childItems.sort((a, b) => a.sortPriority - b.sortPriority); childItems.sort((a, b) => a.sortPriority - b.sortPriority);
for (const item of childItems) { for (const item of childItems) {
@@ -105,7 +97,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
// Add to children // Add to children
node.children.push(next({ item, children: [], depth })); node.children.push(next({ item, children: [], depth }));
// Add to selectable requests // Add to selectable requests
if (item.model !== 'folder' && !isCollapsed) { if (item.model !== 'folder') {
selectableRequests.push({ id: item.id, index: selectableRequestIndex++, tree: node }); selectableRequests.push({ id: item.id, index: selectableRequestIndex++, tree: node });
} }
} }
@@ -116,7 +108,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const tree = next({ item: activeWorkspace, children: [], depth: 0 }); const tree = next({ item: activeWorkspace, children: [], depth: 0 });
return { tree, treeParentMap, selectableRequests, selectedRequest }; return { tree, treeParentMap, selectableRequests, selectedRequest };
}, [activeWorkspace, requests, folders, collapsed]); }, [activeWorkspace, requests, folders]);
const focusActiveRequest = useCallback( const focusActiveRequest = useCallback(
( (
@@ -160,9 +152,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const { item } = node; const { item } = node;
if (item.model === 'folder') { if (item.model === 'http_request' || item.model === 'grpc_request') {
await setCollapsed((c) => ({ ...c, [item.id]: !c[item.id] }));
} else {
await navigate({ await navigate({
to: '/workspaces/$workspaceId/requests/$requestId', to: '/workspaces/$workspaceId/requests/$requestId',
params: { params: {
@@ -177,7 +167,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
setSelectedTree(tree); setSelectedTree(tree);
} }
}, },
[treeParentMap, setCollapsed, navigate, setSelectedId], [treeParentMap, navigate, setSelectedId],
); );
const handleClearSelected = useCallback(() => { const handleClearSelected = useCallback(() => {
@@ -267,13 +257,14 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
); );
const handleMove = useCallback<SidebarItemProps['onMove']>( const handleMove = useCallback<SidebarItemProps['onMove']>(
(id, side) => { async (id, side) => {
let hoveredTree = treeParentMap[id] ?? null; let hoveredTree = treeParentMap[id] ?? null;
const dragIndex = hoveredTree?.children.findIndex((n) => n.item.id === id) ?? -99; const dragIndex = hoveredTree?.children.findIndex((n) => n.item.id === id) ?? -99;
const hoveredItem = hoveredTree?.children[dragIndex]?.item ?? null; const hoveredItem = hoveredTree?.children[dragIndex]?.item ?? null;
let hoveredIndex = dragIndex + (side === 'above' ? 0 : 1); let hoveredIndex = dragIndex + (side === 'above' ? 0 : 1);
if (hoveredItem?.model === 'folder' && side === 'below' && !isCollapsed(hoveredItem.id)) { const isHoveredItemCollapsed = hoveredItem != null ? getSidebarCollapsedMap()[hoveredItem.id] : false;
if (hoveredItem?.model === 'folder' && side === 'below' && !isHoveredItemCollapsed) {
// Move into the folder if it's open and we're moving below it // Move into the folder if it's open and we're moving below it
hoveredTree = hoveredTree?.children.find((n) => n.item.id === id) ?? null; hoveredTree = hoveredTree?.children.find((n) => n.item.id === id) ?? null;
hoveredIndex = 0; hoveredIndex = 0;
@@ -282,7 +273,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
setHoveredTree(hoveredTree); setHoveredTree(hoveredTree);
setHoveredIndex(hoveredIndex); setHoveredIndex(hoveredIndex);
}, },
[isCollapsed, treeParentMap], [treeParentMap],
); );
const handleDragStart = useCallback<SidebarItemProps['onDragStart']>((id: string) => { const handleDragStart = useCallback<SidebarItemProps['onDragStart']>((id: string) => {
@@ -385,7 +376,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const mainContextMenuItems = useCreateDropdownItems(); const mainContextMenuItems = useCreateDropdownItems();
// Not ready to render yet // Not ready to render yet
if (tree == null || collapsed == null) { if (tree == null) {
return null; return null;
} }
@@ -415,7 +406,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
<SidebarItems <SidebarItems
treeParentMap={treeParentMap} treeParentMap={treeParentMap}
selectedTree={selectedTree} selectedTree={selectedTree}
isCollapsed={isCollapsed}
httpResponses={httpResponses} httpResponses={httpResponses}
grpcConnections={grpcConnections} grpcConnections={grpcConnections}
tree={tree} tree={tree}
+20 -15
View File
@@ -6,6 +6,7 @@ import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd';
import { activeRequestAtom } from '../hooks/useActiveRequest'; import { activeRequestAtom } from '../hooks/useActiveRequest';
import { useScrollIntoView } from '../hooks/useScrollIntoView'; import { useScrollIntoView } from '../hooks/useScrollIntoView';
import { useSidebarItemCollapsed } from '../hooks/useSidebarItemCollapsed';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest'; import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from '../lib/jotai';
@@ -15,7 +16,7 @@ import { Icon } from './core/Icon';
import { StatusTag } from './core/StatusTag'; import { StatusTag } from './core/StatusTag';
import { RequestContextMenu } from './RequestContextMenu'; import { RequestContextMenu } from './RequestContextMenu';
import type { SidebarTreeNode } from './Sidebar'; import type { SidebarTreeNode } from './Sidebar';
import {sidebarSelectedIdAtom} from "./SidebarAtoms"; import { sidebarSelectedIdAtom } from './SidebarAtoms';
import type { SidebarItemsProps } from './SidebarItems'; import type { SidebarItemsProps } from './SidebarItems';
enum ItemTypes { enum ItemTypes {
@@ -35,7 +36,7 @@ export type SidebarItemProps = {
child: SidebarTreeNode; child: SidebarTreeNode;
latestHttpResponse: HttpResponse | null; latestHttpResponse: HttpResponse | null;
latestGrpcConnection: GrpcConnection | null; latestGrpcConnection: GrpcConnection | null;
} & Pick<SidebarItemsProps, 'isCollapsed' | 'onSelect'>; } & Pick<SidebarItemsProps, 'onSelect'>;
type DragItem = { type DragItem = {
id: string; id: string;
@@ -51,7 +52,6 @@ export const SidebarItem = memo(function SidebarItem({
onEnd, onEnd,
onDragStart, onDragStart,
onSelect, onSelect,
isCollapsed,
className, className,
itemFallbackName, itemFallbackName,
latestHttpResponse, latestHttpResponse,
@@ -59,6 +59,7 @@ export const SidebarItem = memo(function SidebarItem({
children, children,
}: SidebarItemProps) { }: SidebarItemProps) {
const ref = useRef<HTMLLIElement>(null); const ref = useRef<HTMLLIElement>(null);
const [collapsed, toggleCollapsed] = useSidebarItemCollapsed(itemId);
const [, connectDrop] = useDrop<DragItem, void>( const [, connectDrop] = useDrop<DragItem, void>(
{ {
@@ -106,20 +107,21 @@ export const SidebarItem = memo(function SidebarItem({
const [selected, setSelected] = useState<boolean>( const [selected, setSelected] = useState<boolean>(
jotaiStore.get(sidebarSelectedIdAtom) == itemId, jotaiStore.get(sidebarSelectedIdAtom) == itemId,
); );
useEffect(() => { useEffect(() => {
jotaiStore.sub(sidebarSelectedIdAtom, () => { return jotaiStore.sub(sidebarSelectedIdAtom, () => {
const value = jotaiStore.get(sidebarSelectedIdAtom); const value = jotaiStore.get(sidebarSelectedIdAtom);
setSelected(value === itemId); setSelected(value === itemId);
}); });
}, [itemId]); }, [itemId]);
const [active, setActive] = useState<boolean>(jotaiStore.get(activeRequestAtom)?.id === itemId); const [active, setActive] = useState<boolean>(jotaiStore.get(activeRequestAtom)?.id === itemId);
useEffect(() => { useEffect(
jotaiStore.sub(activeRequestAtom, () => { () =>
const value = jotaiStore.get(activeRequestAtom); jotaiStore.sub(activeRequestAtom, () =>
setActive(value?.id === itemId); setActive(jotaiStore.get(activeRequestAtom)?.id === itemId),
}); ),
}, [itemId]); [itemId],
);
useScrollIntoView(ref.current, active); useScrollIntoView(ref.current, active);
@@ -175,7 +177,10 @@ export const SidebarItem = memo(function SidebarItem({
[handleSubmitNameEdit], [handleSubmitNameEdit],
); );
const handleSelect = useCallback(() => onSelect(itemId), [onSelect, itemId]); const handleSelect = useCallback(async () => {
if (itemModel === 'folder') toggleCollapsed();
else onSelect(itemId);
}, [itemModel, toggleCollapsed, onSelect, itemId]);
const [showContextMenu, setShowContextMenu] = useState<{ const [showContextMenu, setShowContextMenu] = useState<{
x: number; x: number;
y: number; y: number;
@@ -214,7 +219,7 @@ export const SidebarItem = memo(function SidebarItem({
editing && 'ring-1 focus-within:ring-focus', editing && 'ring-1 focus-within:ring-focus',
active && 'bg-surface-highlight text-text', active && 'bg-surface-highlight text-text',
!active && 'text-text-subtle group-hover/item:text-text', !active && 'text-text-subtle group-hover/item:text-text',
showContextMenu && '!text-text', // Show as "active" when context menu is open showContextMenu && '!text-text', // Show as "active" when the context menu is open
)} )}
> >
{itemModel === 'folder' && ( {itemModel === 'folder' && (
@@ -224,7 +229,7 @@ export const SidebarItem = memo(function SidebarItem({
className={classNames( className={classNames(
'text-text-subtlest', 'text-text-subtlest',
'transition-transform', 'transition-transform',
!isCollapsed(itemId) && 'transform rotate-90', !collapsed && 'transform rotate-90',
)} )}
/> />
)} )}
@@ -259,7 +264,7 @@ export const SidebarItem = memo(function SidebarItem({
) : null} ) : null}
</button> </button>
</div> </div>
{children} {collapsed ? null : children}
</li> </li>
); );
}); });
-5
View File
@@ -18,7 +18,6 @@ export interface SidebarItemsProps {
handleEnd: (id: string) => void; handleEnd: (id: string) => void;
handleDragStart: (id: string) => void; handleDragStart: (id: string) => void;
onSelect: (requestId: string) => void; onSelect: (requestId: string) => void;
isCollapsed: (id: string) => boolean;
httpResponses: HttpResponse[]; httpResponses: HttpResponse[];
grpcConnections: GrpcConnection[]; grpcConnections: GrpcConnection[];
} }
@@ -29,7 +28,6 @@ export const SidebarItems = memo(function SidebarItems({
draggingId, draggingId,
onSelect, onSelect,
treeParentMap, treeParentMap,
isCollapsed,
hoveredTree, hoveredTree,
hoveredIndex, hoveredIndex,
handleEnd, handleEnd,
@@ -71,11 +69,9 @@ export const SidebarItems = memo(function SidebarItems({
onEnd={handleEnd} onEnd={handleEnd}
onSelect={onSelect} onSelect={onSelect}
onDragStart={handleDragStart} onDragStart={handleDragStart}
isCollapsed={isCollapsed}
child={child} child={child}
> >
{child.item.model === 'folder' && {child.item.model === 'folder' &&
!isCollapsed(child.item.id) &&
draggingId !== child.item.id && ( draggingId !== child.item.id && (
<SidebarItems <SidebarItems
draggingId={draggingId} draggingId={draggingId}
@@ -86,7 +82,6 @@ export const SidebarItems = memo(function SidebarItems({
hoveredTree={hoveredTree} hoveredTree={hoveredTree}
httpResponses={httpResponses} httpResponses={httpResponses}
grpcConnections={grpcConnections} grpcConnections={grpcConnections}
isCollapsed={isCollapsed}
onSelect={onSelect} onSelect={onSelect}
selectedTree={selectedTree} selectedTree={selectedTree}
tree={child} tree={child}
+1 -1
View File
@@ -19,7 +19,7 @@ import {
useRef, useRef,
} from 'react'; } from 'react';
import { useActiveEnvironmentVariables } from '../../../hooks/useActiveEnvironmentVariables'; import { useActiveEnvironmentVariables } from '../../../hooks/useActiveEnvironmentVariables';
import {useDialog} from "../../../hooks/useDialog"; import { useDialog } from '../../../hooks/useDialog';
import { parseTemplate } from '../../../hooks/useParseTemplate'; import { parseTemplate } from '../../../hooks/useParseTemplate';
import { useRequestEditor } from '../../../hooks/useRequestEditor'; import { useRequestEditor } from '../../../hooks/useRequestEditor';
import { useSettings } from '../../../hooks/useSettings'; import { useSettings } from '../../../hooks/useSettings';
+3 -1
View File
@@ -9,7 +9,9 @@ export function FormattedError({ children }: Props) {
return ( return (
<pre <pre
className={classNames( className={classNames(
'font-mono text-sm w-full select-auto cursor-text bg-surface-highlight p-3 rounded', 'cursor-text select-auto',
'[&_*]:cursor-text [&_*]:select-auto',
'font-mono text-sm w-full bg-surface-highlight p-3 rounded',
'whitespace-pre-wrap border border-danger border-dashed overflow-x-auto', 'whitespace-pre-wrap border border-danger border-dashed overflow-x-auto',
)} )}
> >
+2 -8
View File
@@ -27,16 +27,13 @@ export function PlainInput({
onChange, onChange,
onFocus, onFocus,
onPaste, onPaste,
placeholder,
require, require,
rightSlot, rightSlot,
size = 'md', size = 'md',
type = 'text', type = 'text',
validate, validate,
autoSelect, autoSelect,
step, ...props
autoFocus,
readOnly,
}: PlainInputProps) { }: PlainInputProps) {
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]); const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
const [currentValue, setCurrentValue] = useState(defaultValue ?? ''); const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
@@ -130,7 +127,6 @@ export function PlainInput({
id={id} id={id}
type={type === 'password' && !obscured ? 'text' : type} type={type === 'password' && !obscured ? 'text' : type}
defaultValue={defaultValue} defaultValue={defaultValue}
placeholder={placeholder}
autoComplete="off" autoComplete="off"
autoCapitalize="off" autoCapitalize="off"
autoCorrect="off" autoCorrect="off"
@@ -139,9 +135,7 @@ export function PlainInput({
className={classNames(commonClassName, 'h-auto')} className={classNames(commonClassName, 'h-auto')}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
autoFocus={autoFocus} {...props}
step={step}
readOnly={readOnly}
/> />
</HStack> </HStack>
{type === 'password' && ( {type === 'password' && (
+1 -1
View File
@@ -7,7 +7,7 @@ import { Icon } from './Icon';
export type RadioDropdownItem<T = string | null> = export type RadioDropdownItem<T = string | null> =
| { | {
type?: 'default'; type?: 'default';
label: string; label: ReactNode;
shortLabel?: string; shortLabel?: string;
value: T; value: T;
rightSlot?: ReactNode; rightSlot?: ReactNode;
+48 -24
View File
@@ -1,18 +1,58 @@
import { useNavigate, useSearch } from '@tanstack/react-router'; import { useNavigate, useSearch } from '@tanstack/react-router';
import { useCallback, useEffect, useMemo } from 'react'; import type { CookieJar } from '@yaakapp-internal/models';
import { useCookieJars } from './useCookieJars'; import { atom, useAtomValue } from 'jotai/index';
import { useCallback, useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';
import { cookieJarsAtom, useCookieJars } from './useCookieJars';
export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id'; export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id';
export const activeCookieJarIdAtom = atom<string>();
export const activeCookieJarAtom = atom<CookieJar | null>((get) => {
const activeId = get(activeCookieJarIdAtom);
return get(cookieJarsAtom)?.find((e) => e.id === activeId) ?? null;
});
export function useActiveCookieJar() { export function useActiveCookieJar() {
const [activeCookieJarId, setActiveCookieJarId] = useActiveCookieJarId(); const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
const cookieJars = useCookieJars(); const setId = useCallback(
(id: string) =>
navigate({
search: (prev) => ({ ...prev, cookie_jar_id: id }),
}),
[navigate],
);
const cookieJar = useAtomValue(activeCookieJarAtom);
return [cookieJar, setId] as const;
}
const activeCookieJar = useMemo(() => { function useActiveCookieJarId() {
return cookieJars?.find((cookieJar) => cookieJar.id === activeCookieJarId) ?? null; // NOTE: This query param is accessed from Rust side, so do not change
}, [activeCookieJarId, cookieJars]); const { cookie_jar_id: id } = useSearch({ strict: false });
const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
return [activeCookieJar ?? null, setActiveCookieJarId] as const; const setId = useCallback(
(id: string) =>
navigate({
search: (prev) => ({ ...prev, cookie_jar_id: id }),
}),
[navigate],
);
return [id, setId] as const;
}
export function useSubscribeActiveCookieJar() {
const { cookie_jar_id } = useSearch({ strict: false });
useEffect(
() => jotaiStore.set(activeCookieJarIdAtom, cookie_jar_id ?? undefined),
[cookie_jar_id],
);
}
export function getActiveCookieJar() {
return jotaiStore.get(activeCookieJarAtom);
} }
export function useEnsureActiveCookieJar() { export function useEnsureActiveCookieJar() {
@@ -37,19 +77,3 @@ export function useEnsureActiveCookieJar() {
setActiveCookieJarId(firstJar.id).catch(console.error); setActiveCookieJarId(firstJar.id).catch(console.error);
}, [activeCookieJarId, cookieJars, setActiveCookieJarId]); }, [activeCookieJarId, cookieJars, setActiveCookieJarId]);
} }
function useActiveCookieJarId() {
// NOTE: This query param is accessed from Rust side, so do not change
const { cookie_jar_id: id } = useSearch({ strict: false });
const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
const setId = useCallback(
(id: string) =>
navigate({
search: (prev) => ({ ...prev, cookie_jar_id: id }),
}),
[navigate],
);
return [id, setId] as const;
}
+30 -17
View File
@@ -1,28 +1,41 @@
import { useNavigate, useSearch } from '@tanstack/react-router'; import { useNavigate, useSearch } from '@tanstack/react-router';
import { useCallback } from 'react'; import type { Environment } from '@yaakapp-internal/models';
import { useEnvironments } from './useEnvironments'; import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
export function useActiveEnvironment() { import { useCallback, useEffect } from 'react';
const [id, setId] = useActiveEnvironmentId(); import { jotaiStore } from '../lib/jotai';
const { subEnvironments } = useEnvironments(); import { environmentsAtom } from './useEnvironments';
const environment = subEnvironments.find((w) => w.id === id) ?? null;
return [environment, setId] as const;
}
export const QUERY_ENVIRONMENT_ID = 'environment_id'; export const QUERY_ENVIRONMENT_ID = 'environment_id';
function useActiveEnvironmentId() { export const activeEnvironmentIdAtom = atom<string>();
// NOTE: This query param is accessed from Rust side, so do not change
const { environment_id: id} = useSearch({ strict: false });
const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
export const activeEnvironmentAtom = atom<Environment | null>((get) => {
const activeEnvironmentId = get(activeEnvironmentIdAtom);
return get(environmentsAtom).find((e) => e.id === activeEnvironmentId) ?? null;
});
export function useActiveEnvironment() {
const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
const setId = useCallback( const setId = useCallback(
(environmentId: string | null) => (id: string | null) =>
navigate({ navigate({
search: (prev) => ({ ...prev, environment_id: environmentId ?? undefined }), search: (prev) => ({ ...prev, environment_id: id }),
}), }),
[navigate], [navigate],
); );
const environment = useAtomValue(activeEnvironmentAtom);
return [id, setId] as const; return [environment, setId] as const;
}
export function getActiveEnvironment() {
return jotaiStore.get(activeEnvironmentAtom);
}
export function useSubscribeActiveEnvironmentId() {
const { environment_id } = useSearch({ strict: false });
useEffect(
() => jotaiStore.set(activeEnvironmentIdAtom, environment_id ?? undefined),
[environment_id],
);
} }
+2 -4
View File
@@ -1,7 +1,7 @@
import { useParams } from '@tanstack/react-router'; import { useParams } from '@tanstack/react-router';
import { atom, useAtomValue } from 'jotai'; import { atom, useAtomValue } from 'jotai';
import { useEffect } from 'react'; import { useEffect } from 'react';
import {jotaiStore} from "../lib/jotai"; import { jotaiStore } from '../lib/jotai';
export const activeRequestIdAtom = atom<string>(); export const activeRequestIdAtom = atom<string>();
@@ -11,7 +11,5 @@ export function useActiveRequestId(): string | null {
export function useSubscribeActiveRequestId() { export function useSubscribeActiveRequestId() {
const { requestId } = useParams({ strict: false }); const { requestId } = useParams({ strict: false });
useEffect(() => { useEffect(() => jotaiStore.set(activeRequestIdAtom, requestId), [requestId]);
jotaiStore.set(activeRequestIdAtom, requestId);
}, [requestId]);
} }
+1 -1
View File
@@ -18,7 +18,7 @@ function useActiveWorkspaceId(): string | null {
} }
export function getActiveWorkspaceId() { export function getActiveWorkspaceId() {
return jotaiStore.get(activeWorkspaceIdAtom); return jotaiStore.get(activeWorkspaceIdAtom) ?? null;
} }
export function useSubscribeActiveWorkspaceId() { export function useSubscribeActiveWorkspaceId() {
+6 -1
View File
@@ -15,9 +15,14 @@ export function useCreateEnvironment() {
const workspace = useActiveWorkspace(); const workspace = useActiveWorkspace();
const setEnvironments = useSetAtom(environmentsAtom); const setEnvironments = useSetAtom(environmentsAtom);
return useFastMutation<Environment | null, unknown, Environment>({ return useFastMutation<Environment | null, unknown, Environment | null>({
toastyError: true,
mutationKey: ['create_environment'], mutationKey: ['create_environment'],
mutationFn: async (baseEnvironment) => { mutationFn: async (baseEnvironment) => {
if (baseEnvironment == null) {
throw new Error('No base environment passed');
}
const name = await prompt({ const name = await prompt({
id: 'new-environment', id: 'new-environment',
title: 'New Environment', title: 'New Environment',
+5 -6
View File
@@ -1,15 +1,14 @@
import { useFastMutation } from './useFastMutation';
import type { Folder } from '@yaakapp-internal/models'; import type { Folder } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace'; import { getActiveWorkspaceId } from './useActiveWorkspace';
import { useFastMutation } from './useFastMutation';
import { foldersAtom } from './useFolders'; import { foldersAtom } from './useFolders';
import { usePrompt } from './usePrompt'; import { usePrompt } from './usePrompt';
import { updateModelList } from './useSyncModelStores'; import { updateModelList } from './useSyncModelStores';
export function useCreateFolder() { export function useCreateFolder() {
const workspace = useActiveWorkspace();
const prompt = usePrompt(); const prompt = usePrompt();
const setFolders = useSetAtom(foldersAtom); const setFolders = useSetAtom(foldersAtom);
@@ -20,8 +19,8 @@ export function useCreateFolder() {
>({ >({
mutationKey: ['create_folder'], mutationKey: ['create_folder'],
mutationFn: async (patch) => { mutationFn: async (patch) => {
console.log("FOLDER", workspace); const workspaceId = getActiveWorkspaceId();
if (workspace === null) { if (workspaceId == null) {
throw new Error("Cannot create folder when there's no active workspace"); throw new Error("Cannot create folder when there's no active workspace");
} }
@@ -40,7 +39,7 @@ export function useCreateFolder() {
} }
patch.sortPriority = patch.sortPriority || -Date.now(); patch.sortPriority = patch.sortPriority || -Date.now();
return await invokeCmd('cmd_create_folder', { workspaceId: workspace.id, ...patch }); return await invokeCmd('cmd_create_folder', { workspaceId, ...patch });
}, },
onSuccess: (folder) => { onSuccess: (folder) => {
if (folder == null) return; if (folder == null) return;
+1 -1
View File
@@ -6,7 +6,7 @@ export const environmentsAtom = atom<Environment[]>([]);
export function useEnvironments() { export function useEnvironments() {
const allEnvironments = useAtomValue(environmentsAtom); const allEnvironments = useAtomValue(environmentsAtom);
const baseEnvironment = allEnvironments.find((e) => e.environmentId == null); const baseEnvironment = allEnvironments.find((e) => e.environmentId == null) ?? null;
const subEnvironments = const subEnvironments =
allEnvironments.filter((e) => e.environmentId === (baseEnvironment?.id ?? 'n/a')) ?? []; allEnvironments.filter((e) => e.environmentId === (baseEnvironment?.id ?? 'n/a')) ?? [];
+2
View File
@@ -32,6 +32,8 @@ export function useFastMutation<TData = unknown, TError = unknown, TVariables =
if (toastyError) { if (toastyError) {
toast.show({ toast.show({
id: 'error-' + mutationKey.join('.'), id: 'error-' + mutationKey.join('.'),
color: 'danger',
timeout: 8000,
message: String(e), message: String(e),
}); });
} }
+1 -1
View File
@@ -68,7 +68,7 @@ export function useImportData() {
await navigate({ await navigate({
to: '/workspaces/$workspaceId', to: '/workspaces/$workspaceId',
params: { workspaceId: importedWorkspace.id }, params: { workspaceId: importedWorkspace.id },
search: { environmentId }, search: { environment_id: environmentId },
}); });
} }
+43 -21
View File
@@ -1,9 +1,15 @@
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import type { KeyValue } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { buildKeyValueKey, getKeyValue, setKeyValue } from '../lib/keyValueStore'; import { jotaiStore } from '../lib/jotai';
import { buildKeyValueKey, extractKeyValueOrFallback, setKeyValue } from '../lib/keyValueStore';
const DEFAULT_NAMESPACE = 'global'; const DEFAULT_NAMESPACE = 'global';
export const keyValuesAtom = atom<KeyValue[]>([]);
export function keyValueQueryKey({ export function keyValueQueryKey({
namespace = DEFAULT_NAMESPACE, namespace = DEFAULT_NAMESPACE,
key, key,
@@ -23,44 +29,60 @@ export function useKeyValue<T extends object | boolean | number | string | null>
key: string | string[]; key: string | string[];
fallback: T; fallback: T;
}) { }) {
const query = useQuery<T>({ const keyValues = useAtomValue(keyValuesAtom);
queryKey: keyValueQueryKey({ namespace, key }), const keyValue =
queryFn: async () => getKeyValue({ namespace, key, fallback }), keyValues?.find((kv) => buildKeyValueKey(kv.key) === buildKeyValueKey(key)) ?? null;
refetchOnWindowFocus: false, const value = extractKeyValueOrFallback(keyValue, fallback);
}); const isLoading = keyValues == null;
const mutate = useMutation<void, unknown, T>({ const { mutateAsync } = useMutation<void, unknown, T>({
mutationKey: ['set_key_value', namespace, key], mutationKey: ['set_key_value', namespace, key],
mutationFn: (value) => setKeyValue<T>({ namespace, key, value }), mutationFn: (value) => setKeyValue<T>({ namespace, key, value }),
}); });
const set = useCallback( const set = useCallback(
async (value: ((v: T) => T) | T) => { async (valueOrUpdate: ((v: T) => T) | T) => {
if (typeof value === 'function') { if (typeof valueOrUpdate === 'function') {
await getKeyValue({ namespace, key, fallback }).then((kv) => { const newV = valueOrUpdate(value);
const newV = value(kv); if (newV === value) return;
if (newV === kv) return; await mutateAsync(newV);
return mutate.mutateAsync(newV);
});
} else { } else {
// TODO: Make this only update if the value is different. I tried this but it seems query.data // TODO: Make this only update if the value is different. I tried this but it seems query.data
// is stale. // is stale.
await mutate.mutateAsync(value); await mutateAsync(valueOrUpdate);
} }
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[typeof key === 'string' ? key : key.join('::'), namespace], [typeof key === 'string' ? key : key.join('::'), namespace, value],
); );
const reset = useCallback(async () => mutate.mutateAsync(fallback), [mutate, fallback]); const reset = useCallback(async () => mutateAsync(fallback), [fallback, mutateAsync]);
return useMemo( return useMemo(
() => ({ () => ({
value: query.data, value,
isLoading: query.isLoading, isLoading,
set, set,
reset, reset,
}), }),
[query.data, query.isLoading, reset, set], [isLoading, reset, set, value],
); );
} }
export function getKeyValue<T extends object | boolean | number | string | null>({
namespace,
key,
fallback,
}: {
namespace?: 'global' | 'no_sync' | 'license';
key: string | string[];
fallback: T;
}) {
const keyValues = jotaiStore.get(keyValuesAtom);
const keyValue =
keyValues?.find(
(kv) => kv.namespace === namespace && buildKeyValueKey(kv.key) === buildKeyValueKey(key),
) ?? null;
const value = extractKeyValueOrFallback(keyValue, fallback);
return value;
}
+1 -1
View File
@@ -21,7 +21,7 @@ export function useOpenWorkspace() {
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? undefined; const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? undefined;
const requestId = (await getRecentRequests(workspaceId))[0] ?? undefined; const requestId = (await getRecentRequests(workspaceId))[0] ?? undefined;
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? undefined; const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? undefined;
const search = { environmentId, cookieJarId }; const search = { environment_id: environmentId, cookie_jar_id: cookieJarId };
if (inNewWindow) { if (inNewWindow) {
const location = router.buildLocation({ const location = router.buildLocation({
+25 -16
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { jotaiStore } from '../lib/jotai';
import { getKeyValue } from '../lib/keyValueStore'; import { getKeyValue } from '../lib/keyValueStore';
import { useActiveRequestId } from './useActiveRequestId'; import { activeRequestIdAtom } from './useActiveRequestId';
import { useActiveWorkspace } from './useActiveWorkspace'; import { useActiveWorkspace } from './useActiveWorkspace';
import { useKeyValue } from './useKeyValue'; import { useKeyValue } from './useKeyValue';
import { useRequests } from './useRequests'; import { useRequests } from './useRequests';
@@ -12,30 +13,38 @@ const fallback: string[] = [];
export function useRecentRequests() { export function useRecentRequests() {
const requests = useRequests(); const requests = useRequests();
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const activeRequestId = useActiveRequestId();
const kv = useKeyValue<string[]>({ const { set: setRecentRequests, value: recentRequests } = useKeyValue<string[]>({
key: kvKey(activeWorkspace?.id ?? 'n/a'), key: kvKey(activeWorkspace?.id ?? 'n/a'),
namespace, namespace,
fallback, fallback,
}); });
// Set history when active request changes
useEffect(() => {
kv.set((currentHistory) => {
if (activeRequestId === null) return currentHistory;
const withoutCurrentRequest = currentHistory.filter((id) => id !== activeRequestId);
return [activeRequestId, ...withoutCurrentRequest];
}).catch(console.error);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeRequestId]);
const onlyValidIds = useMemo( const onlyValidIds = useMemo(
() => kv.value?.filter((id) => requests.some((r) => r.id === id)) ?? [], () => recentRequests?.filter((id) => requests.some((r) => r.id === id)) ?? [],
[kv.value, requests], [recentRequests, requests],
); );
return onlyValidIds; return [onlyValidIds, setRecentRequests] as const;
}
export function useSubscribeRecentRequests() {
const [recentRequests, setRecentRequests] = useRecentRequests();
useEffect(() => {
return jotaiStore.sub(activeRequestIdAtom, () => {
const activeRequestId = jotaiStore.get(activeRequestIdAtom) ?? null;
if (recentRequests[0] === activeRequestId) {
// Nothing to do
return;
}
setRecentRequests((currentHistory) => {
if (activeRequestId === null) return currentHistory;
const withoutCurrentRequest = currentHistory.filter((id) => id !== activeRequestId);
return [activeRequestId, ...withoutCurrentRequest];
}).catch(console.error);
});
}, [recentRequests, setRecentRequests]);
} }
export async function getRecentRequests(workspaceId: string) { export async function getRecentRequests(workspaceId: string) {
+5 -7
View File
@@ -1,16 +1,14 @@
import { useFastMutation } from './useFastMutation';
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { getHttpRequest } from '../lib/store'; import { getHttpRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { useActiveCookieJar } from './useActiveCookieJar'; import { getActiveCookieJar } from './useActiveCookieJar';
import { useActiveEnvironment } from './useActiveEnvironment'; import { getActiveEnvironment } from './useActiveEnvironment';
import { useAlert } from './useAlert'; import { useAlert } from './useAlert';
import { useFastMutation } from './useFastMutation';
export function useSendAnyHttpRequest() { export function useSendAnyHttpRequest() {
const alert = useAlert(); const alert = useAlert();
const [environment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
return useFastMutation<HttpResponse | null, string, string | null>({ return useFastMutation<HttpResponse | null, string, string | null>({
mutationKey: ['send_any_request'], mutationKey: ['send_any_request'],
mutationFn: async (id) => { mutationFn: async (id) => {
@@ -21,8 +19,8 @@ export function useSendAnyHttpRequest() {
return invokeCmd('cmd_send_http_request', { return invokeCmd('cmd_send_http_request', {
request, request,
environmentId: environment?.id, environmentId: getActiveEnvironment()?.id,
cookieJarId: activeCookieJar?.id, cookieJarId: getActiveCookieJar()?.id,
}); });
}, },
onSettled: () => trackEvent('http_request', 'send'), onSettled: () => trackEvent('http_request', 'send'),
+45
View File
@@ -0,0 +1,45 @@
import { useCallback, useEffect, useState } from 'react';
import { jotaiStore } from '../lib/jotai';
import { setKeyValue } from '../lib/keyValueStore';
import { getActiveWorkspaceId } from './useActiveWorkspace';
import { getKeyValue, keyValuesAtom } from './useKeyValue';
function kvKey(workspaceId: string | null) {
return ['sidebar_collapsed', workspaceId ?? 'n/a'];
}
export function useSidebarItemCollapsed(itemId: string) {
const [isCollapsed, setIsCollapsed] = useState<boolean>(
getSidebarCollapsedMap()[itemId] === true,
);
useEffect(
() =>
jotaiStore.sub(keyValuesAtom, () => {
setIsCollapsed(getSidebarCollapsedMap()[itemId] === true);
}),
[itemId],
);
const toggle = useCallback(() => {
setKeyValue({
key: kvKey(getActiveWorkspaceId()),
namespace: 'no_sync',
value: { ...getSidebarCollapsedMap(), [itemId]: !isCollapsed },
}).catch(console.error);
}, [isCollapsed, itemId]);
return [isCollapsed, toggle] as const;
}
export function getSidebarCollapsedMap() {
const activeWorkspaceId = getActiveWorkspaceId();
if (activeWorkspaceId == null) return {};
const value = getKeyValue<Record<string, boolean>>({
key: kvKey(activeWorkspaceId),
fallback: {},
namespace: 'no_sync',
});
return value;
}
+18 -9
View File
@@ -1,8 +1,8 @@
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import type { AnyModel } from '@yaakapp-internal/models'; import type { AnyModel, KeyValue } from '@yaakapp-internal/models';
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from '../lib/jotai';
import { extractKeyValue } from '../lib/keyValueStore'; import { buildKeyValueKey } from '../lib/keyValueStore';
import { modelsEq } from '../lib/model_util'; import { modelsEq } from '../lib/model_util';
import { useActiveWorkspace } from './useActiveWorkspace'; import { useActiveWorkspace } from './useActiveWorkspace';
import { cookieJarsAtom } from './useCookieJars'; import { cookieJarsAtom } from './useCookieJars';
@@ -13,7 +13,7 @@ import { grpcEventsQueryKey } from './useGrpcEvents';
import { grpcRequestsAtom } from './useGrpcRequests'; import { grpcRequestsAtom } from './useGrpcRequests';
import { httpRequestsAtom } from './useHttpRequests'; import { httpRequestsAtom } from './useHttpRequests';
import { httpResponsesAtom } from './useHttpResponses'; import { httpResponsesAtom } from './useHttpResponses';
import { keyValueQueryKey } from './useKeyValue'; import { keyValueQueryKey, keyValuesAtom } from './useKeyValue';
import { useListenToTauriEvent } from './useListenToTauriEvent'; import { useListenToTauriEvent } from './useListenToTauriEvent';
import { pluginsAtom } from './usePlugins'; import { pluginsAtom } from './usePlugins';
import { useRequestUpdateKey } from './useRequestUpdateKey'; import { useRequestUpdateKey } from './useRequestUpdateKey';
@@ -71,14 +71,11 @@ export function useSyncModelStores() {
jotaiStore.set(cookieJarsAtom, updateModelList(model)); jotaiStore.set(cookieJarsAtom, updateModelList(model));
} else if (model.model === 'settings') { } else if (model.model === 'settings') {
jotaiStore.set(settingsAtom, model); jotaiStore.set(settingsAtom, model);
} else if (model.model === 'key_value') {
jotaiStore.set(keyValuesAtom, updateModelList(model));
} else if (queryKey != null) { } else if (queryKey != null) {
// TODO: Convert all models to use Jotai // TODO: Convert all models to use Jotai
queryClient.setQueryData(queryKey, (current: unknown) => { queryClient.setQueryData(queryKey, (current: unknown) => {
if (model.model === 'key_value') {
// Special-case for KeyValue
return extractKeyValue(model);
}
if (Array.isArray(current)) { if (Array.isArray(current)) {
return updateModelList(model)(current); return updateModelList(model)(current);
} }
@@ -111,7 +108,7 @@ export function useSyncModelStores() {
} else if (model.model === 'grpc_event') { } else if (model.model === 'grpc_event') {
queryClient.setQueryData(grpcEventsQueryKey(model), removeModelById(model)); queryClient.setQueryData(grpcEventsQueryKey(model), removeModelById(model));
} else if (model.model === 'key_value') { } else if (model.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(model), undefined); queryClient.setQueryData(keyValueQueryKey(model), removeModelByKeyValue(model));
} else if (model.model === 'cookie_jar') { } else if (model.model === 'cookie_jar') {
jotaiStore.set(cookieJarsAtom, removeModelById(model)); jotaiStore.set(cookieJarsAtom, removeModelById(model));
} }
@@ -136,6 +133,18 @@ export function removeModelById<T extends { id: string }>(model: T) {
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? []; return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? [];
} }
export function removeModelByKeyValue(model: KeyValue) {
return (entries: KeyValue[] | undefined) =>
entries?.filter(
(e) =>
!(
e.namespace === model.namespace &&
buildKeyValueKey(e.key) === buildKeyValueKey(model.key) &&
e.value == model.value
),
) ?? [];
}
const shouldIgnoreModel = (payload: AnyModel, windowLabel: string) => { const shouldIgnoreModel = (payload: AnyModel, windowLabel: string) => {
if (windowLabel === getCurrentWebviewWindow().label) { if (windowLabel === getCurrentWebviewWindow().label) {
// Never ignore same-window updates // Never ignore same-window updates
+23 -31
View File
@@ -1,7 +1,7 @@
import { useSetAtom } from 'jotai/index';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace'; import { activeWorkspaceIdAtom, getActiveWorkspaceId } from './useActiveWorkspace';
import { cookieJarsAtom } from './useCookieJars'; import { cookieJarsAtom } from './useCookieJars';
import { environmentsAtom } from './useEnvironments'; import { environmentsAtom } from './useEnvironments';
import { foldersAtom } from './useFolders'; import { foldersAtom } from './useFolders';
@@ -9,36 +9,28 @@ import { grpcConnectionsAtom } from './useGrpcConnections';
import { grpcRequestsAtom } from './useGrpcRequests'; import { grpcRequestsAtom } from './useGrpcRequests';
import { httpRequestsAtom } from './useHttpRequests'; import { httpRequestsAtom } from './useHttpRequests';
import { httpResponsesAtom } from './useHttpResponses'; import { httpResponsesAtom } from './useHttpResponses';
import { keyValuesAtom } from './useKeyValue';
export function useSyncWorkspaceChildModels() { 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;
useEffect(() => { useEffect(() => {
if (workspaceId == null) { jotaiStore.sub(activeWorkspaceIdAtom, sync);
return; sync().catch(console.error);
} }, []);
(async function () { }
console.log('Syncing model stores', { workspaceId });
// Set the things we need first, first async function sync() {
setHttpRequests(await invokeCmd('cmd_list_http_requests', { workspaceId })); const workspaceId = getActiveWorkspaceId();
setGrpcRequests(await invokeCmd('cmd_list_grpc_requests', { workspaceId })); const args = { workspaceId };
setFolders(await invokeCmd('cmd_list_folders', { workspaceId })); console.log('Syncing model stores', args);
// Set the things we need first, first
// Then, set the rest jotaiStore.set(httpRequestsAtom, await invokeCmd('cmd_list_http_requests', args));
setCookieJars(await invokeCmd('cmd_list_cookie_jars', { workspaceId })); jotaiStore.set(grpcRequestsAtom, await invokeCmd('cmd_list_grpc_requests', args));
setHttpResponses(await invokeCmd('cmd_list_http_responses', { workspaceId })); jotaiStore.set(foldersAtom, await invokeCmd('cmd_list_folders', args));
setGrpcConnections(await invokeCmd('cmd_list_grpc_connections', { workspaceId }));
setEnvironments(await invokeCmd('cmd_list_environments', { workspaceId })); // Then, set the rest
})().catch(console.error); jotaiStore.set(keyValuesAtom, await invokeCmd('cmd_list_key_values', args));
jotaiStore.set(cookieJarsAtom, await invokeCmd('cmd_list_cookie_jars', args));
// eslint-disable-next-line react-hooks/exhaustive-deps jotaiStore.set(httpResponsesAtom, await invokeCmd('cmd_list_http_responses', args));
}, [workspaceId]); jotaiStore.set(grpcConnectionsAtom, await invokeCmd('cmd_list_grpc_connections', args));
jotaiStore.set(environmentsAtom, await invokeCmd('cmd_list_environments', args));
} }
+14 -5
View File
@@ -1,14 +1,23 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import type { GetTemplateFunctionsResponse } from '@yaakapp-internal/plugin'; import type { GetTemplateFunctionsResponse, TemplateFunction } from '@yaakapp-internal/plugin';
import { atom, useAtomValue } from 'jotai';
import { useSetAtom } from 'jotai/index';
import { useState } from 'react'; import { useState } from 'react';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { usePluginsKey } from './usePlugins'; import { usePluginsKey } from './usePlugins';
const templateFunctionsAtom = atom<TemplateFunction[]>([]);
export function useTemplateFunctions() { export function useTemplateFunctions() {
return useAtomValue(templateFunctionsAtom);
}
export function useSubscribeTemplateFunctions() {
const pluginsKey = usePluginsKey(); const pluginsKey = usePluginsKey();
const [numFns, setNumFns] = useState<number>(0); const [numFns, setNumFns] = useState<number>(0);
const setAtom = useSetAtom(templateFunctionsAtom);
const result = useQuery({ useQuery({
queryKey: ['template_functions', pluginsKey], queryKey: ['template_functions', pluginsKey],
// Fetch periodically until functions are returned // Fetch periodically until functions are returned
// NOTE: visibilitychange (refetchOnWindowFocus) does not work on Windows, so we'll rely on this logic // NOTE: visibilitychange (refetchOnWindowFocus) does not work on Windows, so we'll rely on this logic
@@ -19,9 +28,9 @@ export function useTemplateFunctions() {
queryFn: async () => { queryFn: async () => {
const result = await invokeCmd<GetTemplateFunctionsResponse[]>('cmd_template_functions'); const result = await invokeCmd<GetTemplateFunctionsResponse[]>('cmd_template_functions');
setNumFns(result.length); setNumFns(result.length);
return result; const functions = result.flatMap((r) => r.functions) ?? [];
setAtom(functions);
return functions;
}, },
}); });
return result.data?.flatMap((r) => r.functions) ?? [];
} }
+1 -1
View File
@@ -54,7 +54,7 @@ export function extractKeyValue<T>(kv: KeyValue | null): T | undefined {
} }
} }
function extractKeyValueOrFallback<T>(kv: KeyValue | null, fallback: T): T { export function extractKeyValueOrFallback<T>(kv: KeyValue | null, fallback: T): T {
const v = extractKeyValue<T>(kv); const v = extractKeyValue<T>(kv);
if (v === undefined) return fallback; if (v === undefined) return fallback;
return v; return v;
+2 -1
View File
@@ -45,6 +45,7 @@ type TauriCmd =
| 'cmd_import_data' | 'cmd_import_data'
| 'cmd_install_plugin' | 'cmd_install_plugin'
| 'cmd_list_cookie_jars' | 'cmd_list_cookie_jars'
| 'cmd_list_key_values'
| 'cmd_list_environments' | 'cmd_list_environments'
| 'cmd_list_folders' | 'cmd_list_folders'
| 'cmd_list_grpc_connections' | 'cmd_list_grpc_connections'
@@ -79,7 +80,7 @@ type TauriCmd =
| 'cmd_write_file_dev'; | 'cmd_write_file_dev';
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> { export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {
// console.log('RUN COMMAND', cmd, args); console.log('RUN COMMAND', cmd, args);
try { try {
return await invoke(cmd, args); return await invoke(cmd, args);
} catch (err) { } catch (err) {
+4 -2
View File
@@ -7,11 +7,12 @@ import React, { Suspense } from 'react';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import { HelmetProvider } from 'react-helmet-async'; import { HelmetProvider } from 'react-helmet-async';
import { DialogProvider, Dialogs } from '../components/Dialogs';
import { GlobalHooks } from '../components/GlobalHooks'; import { GlobalHooks } from '../components/GlobalHooks';
import RouteError from '../components/RouteError';
import { ToastProvider, Toasts } from '../components/Toasts';
import { useOsInfo } from '../hooks/useOsInfo'; import { useOsInfo } from '../hooks/useOsInfo';
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from '../lib/jotai';
import { ToastProvider, Toasts } from '../components/Toasts';
import { DialogProvider, Dialogs } from '../components/Dialogs';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
queryCache: new QueryCache({ queryCache: new QueryCache({
@@ -52,6 +53,7 @@ const ReactQueryDevtools =
export const Route = createRootRoute({ export const Route = createRootRoute({
component: RouteComponent, component: RouteComponent,
errorComponent: RouteError,
}); });
function RouteComponent() { function RouteComponent() {