mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-17 23:14:03 +01:00
Hotkeys and view mode kvs
This commit is contained in:
@@ -1,19 +1,22 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { MotionConfig } from 'framer-motion';
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
import { keyValueQueryKey } from '../hooks/useKeyValues';
|
||||
import { requestsQueryKey } from '../hooks/useRequests';
|
||||
import { responsesQueryKey } from '../hooks/useResponses';
|
||||
import { DEFAULT_FONT_SIZE } from '../lib/constants';
|
||||
import type { HttpRequest, HttpResponse, KeyValue } from '../lib/models';
|
||||
import { convertDates } from '../lib/models';
|
||||
import { AppRouter } from './AppRouter';
|
||||
import { AppRouter, WORKSPACE_REQUEST_PATH } from './AppRouter';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
await listen('updated_key_value', ({ payload: keyValue }: { payload: KeyValue }) => {
|
||||
queryClient.setQueryData(keyValueQueryKey(keyValue.namespace, keyValue.key), keyValue);
|
||||
queryClient.setQueryData(keyValueQueryKey(keyValue), keyValue);
|
||||
});
|
||||
|
||||
await listen('updated_request', ({ payload: request }: { payload: HttpRequest }) => {
|
||||
@@ -66,6 +69,19 @@ await listen('updated_response', ({ payload: response }: { payload: HttpResponse
|
||||
);
|
||||
});
|
||||
|
||||
await listen('send_request', async () => {
|
||||
const params = matchPath(WORKSPACE_REQUEST_PATH, window.location.pathname);
|
||||
const requestId = params?.params.requestId;
|
||||
if (typeof requestId !== 'string') {
|
||||
return;
|
||||
}
|
||||
await invoke('send_request', { requestId });
|
||||
});
|
||||
|
||||
await listen('refresh', () => {
|
||||
location.reload();
|
||||
});
|
||||
|
||||
await listen('zoom', ({ payload: zoomDelta }: { payload: number }) => {
|
||||
const fontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize);
|
||||
|
||||
@@ -87,6 +103,7 @@ export function App() {
|
||||
<MotionConfig transition={{ duration: 0.1 }}>
|
||||
<HelmetProvider>
|
||||
<AppRouter />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</HelmetProvider>
|
||||
</MotionConfig>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -5,6 +5,9 @@ const Workspaces = lazy(() => import('./Workspaces'));
|
||||
const Workspace = lazy(() => import('./Workspace'));
|
||||
const RouteError = lazy(() => import('./RouteError'));
|
||||
|
||||
export const WORKSPACE_PATH = '/workspaces/:workspaceId';
|
||||
export const WORKSPACE_REQUEST_PATH = '/workspaces/:workspaceId/requests/:requestId';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
@@ -15,11 +18,11 @@ const router = createBrowserRouter([
|
||||
element: <Workspaces />,
|
||||
},
|
||||
{
|
||||
path: '/workspaces/:workspaceId',
|
||||
path: WORKSPACE_PATH,
|
||||
element: <Workspace />,
|
||||
},
|
||||
{
|
||||
path: '/workspaces/:workspaceId/requests/:requestId',
|
||||
path: WORKSPACE_REQUEST_PATH,
|
||||
element: <Workspace />,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import classnames from 'classnames';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
||||
import { useSendRequest } from '../hooks/useSendRequest';
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import { Editor } from './core/Editor';
|
||||
import { PairEditor } from './core/PairEditor';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { GraphQLEditor } from './editors/GraphQLEditor';
|
||||
import { PairEditor } from './core/PairEditor';
|
||||
import { UrlBar } from './UrlBar';
|
||||
|
||||
interface Props {
|
||||
@@ -18,6 +19,7 @@ export function RequestPane({ fullHeight, className }: Props) {
|
||||
const activeRequest = useActiveRequest();
|
||||
const updateRequest = useUpdateRequest(activeRequest);
|
||||
const sendRequest = useSendRequest(activeRequest);
|
||||
const responseLoading = useIsResponseLoading();
|
||||
|
||||
if (activeRequest === null) return null;
|
||||
|
||||
@@ -27,10 +29,10 @@ export function RequestPane({ fullHeight, className }: Props) {
|
||||
key={activeRequest.id}
|
||||
method={activeRequest.method}
|
||||
url={activeRequest.url}
|
||||
loading={sendRequest.isLoading}
|
||||
onMethodChange={(method) => updateRequest.mutate({ method })}
|
||||
onUrlChange={(url) => updateRequest.mutate({ url })}
|
||||
sendRequest={sendRequest.mutate}
|
||||
sendRequest={sendRequest}
|
||||
loading={responseLoading}
|
||||
/>
|
||||
<Tabs
|
||||
tabs={[
|
||||
|
||||
@@ -3,8 +3,10 @@ import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { useDeleteResponses } from '../hooks/useDeleteResponses';
|
||||
import { useDeleteResponse } from '../hooks/useResponseDelete';
|
||||
import { useResponses } from '../hooks/useResponses';
|
||||
import { useResponseViewMode } from '../hooks/useResponseViewMode';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { pluralize } from '../lib/pluralize';
|
||||
import { Dropdown, DropdownMenuTrigger } from './core/Dropdown';
|
||||
import { Editor } from './core/Editor';
|
||||
import { Icon } from './core/Icon';
|
||||
@@ -19,11 +21,11 @@ interface Props {
|
||||
|
||||
export const ResponsePane = memo(function ResponsePane({ className }: Props) {
|
||||
const [activeResponseId, setActiveResponseId] = useState<string | null>(null);
|
||||
const [viewMode, setViewMode] = useState<'pretty' | 'raw'>('pretty');
|
||||
const responses = useResponses();
|
||||
const activeResponse: HttpResponse | null = activeResponseId
|
||||
? responses.find((r) => r.id === activeResponseId) ?? null
|
||||
: responses[responses.length - 1] ?? null;
|
||||
const [viewMode, toggleViewMode] = useResponseViewMode(activeResponse?.requestId);
|
||||
const deleteResponse = useDeleteResponse(activeResponse);
|
||||
const deleteAllResponses = useDeleteResponses(activeResponse?.requestId);
|
||||
|
||||
@@ -74,7 +76,7 @@ export const ResponsePane = memo(function ResponsePane({ className }: Props) {
|
||||
items={[
|
||||
{
|
||||
label: viewMode === 'pretty' ? 'View Raw' : 'View Prettified',
|
||||
onSelect: () => setViewMode((m) => (m === 'pretty' ? 'raw' : 'pretty')),
|
||||
onSelect: toggleViewMode,
|
||||
},
|
||||
'-----',
|
||||
{
|
||||
@@ -83,7 +85,7 @@ export const ResponsePane = memo(function ResponsePane({ className }: Props) {
|
||||
disabled: responses.length === 0,
|
||||
},
|
||||
{
|
||||
label: 'Clear All Responses',
|
||||
label: `Clear ${responses.length} ${pluralize('Response', responses.length)}`,
|
||||
onSelect: deleteAllResponses.mutate,
|
||||
disabled: responses.length === 0,
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useSendRequest } from '../hooks/useSendRequest';
|
||||
import { Button } from './core/Button';
|
||||
import { DropdownMenuRadio, DropdownMenuTrigger } from './core/Dropdown';
|
||||
import { IconButton } from './core/IconButton';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { responsesQueryKey } from './useResponses';
|
||||
|
||||
export function useDeleteResponses(requestId?: string) {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -10,7 +11,7 @@ export function useDeleteResponses(requestId?: string) {
|
||||
},
|
||||
onSuccess: () => {
|
||||
if (!requestId) return;
|
||||
queryClient.setQueryData(['responses', { requestId: requestId }], []);
|
||||
queryClient.setQueryData(responsesQueryKey(requestId), []);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
8
src-web/hooks/useIsResponseLoading.ts
Normal file
8
src-web/hooks/useIsResponseLoading.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { useResponses } from './useResponses';
|
||||
|
||||
export function useIsResponseLoading(): boolean {
|
||||
const responses = useResponses();
|
||||
const response = responses[responses.length - 1];
|
||||
if (!response) return false;
|
||||
return !(response.body || response.error);
|
||||
}
|
||||
@@ -2,25 +2,46 @@ import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { KeyValue } from '../lib/models';
|
||||
|
||||
export function keyValueQueryKey(namespace: string, key: string) {
|
||||
return ['key_value', { namespace, key }];
|
||||
const DEFAULT_NAMESPACE = 'app';
|
||||
|
||||
export function keyValueQueryKey({
|
||||
namespace = DEFAULT_NAMESPACE,
|
||||
key,
|
||||
}: {
|
||||
namespace: string;
|
||||
key: string | string[];
|
||||
}) {
|
||||
return ['key_value', { namespace, key: buildKey(key) }];
|
||||
}
|
||||
|
||||
export function useKeyValues(namespace: string, key: string) {
|
||||
export function useKeyValues({
|
||||
namespace = DEFAULT_NAMESPACE,
|
||||
key,
|
||||
initialValue,
|
||||
}: {
|
||||
namespace: string;
|
||||
key: string | string[];
|
||||
initialValue: string;
|
||||
}) {
|
||||
const query = useQuery<KeyValue | null>({
|
||||
initialData: null,
|
||||
queryKey: keyValueQueryKey(namespace, key),
|
||||
queryFn: async () => invoke('get_key_value', { namespace, key }),
|
||||
queryKey: keyValueQueryKey({ namespace, key }),
|
||||
queryFn: async () => invoke('get_key_value', { namespace, key: buildKey(key) }),
|
||||
});
|
||||
|
||||
const mutate = useMutation<KeyValue, unknown, KeyValue['value']>({
|
||||
const mutate = useMutation<KeyValue, unknown, string>({
|
||||
mutationFn: (value) => {
|
||||
return invoke('set_key_value', { namespace, key, value });
|
||||
return invoke('set_key_value', { namespace, key: buildKey(key), value });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
value: query.data?.value ?? null,
|
||||
set: (value: KeyValue['value']) => mutate.mutate(value),
|
||||
value: query.data?.value ?? initialValue,
|
||||
set: (value: string) => mutate.mutate(value),
|
||||
};
|
||||
}
|
||||
|
||||
function buildKey(key: string | string[]): string {
|
||||
if (typeof key === 'string') return key;
|
||||
return key.join('::');
|
||||
}
|
||||
|
||||
15
src-web/hooks/useResponseViewMode.ts
Normal file
15
src-web/hooks/useResponseViewMode.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useKeyValues } from './useKeyValues';
|
||||
|
||||
export function useResponseViewMode(requestId?: string): [string, () => void] {
|
||||
const v = useKeyValues({
|
||||
namespace: 'app',
|
||||
key: ['response_view_mode', requestId ?? 'n/a'],
|
||||
initialValue: 'pretty',
|
||||
});
|
||||
|
||||
const toggle = () => {
|
||||
v.set(v.value === 'pretty' ? 'raw' : 'pretty');
|
||||
};
|
||||
|
||||
return [v.value, toggle];
|
||||
}
|
||||
@@ -14,5 +14,5 @@ export function useSendRequest(request: HttpRequest | null) {
|
||||
if (request == null) return;
|
||||
await queryClient.invalidateQueries(responsesQueryKey(request.id));
|
||||
},
|
||||
});
|
||||
}).mutate;
|
||||
}
|
||||
|
||||
6
src-web/lib/pluralize.ts
Normal file
6
src-web/lib/pluralize.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function pluralize(word: string, count: number): string {
|
||||
if (count === 1) {
|
||||
return word;
|
||||
}
|
||||
return `${word}s`;
|
||||
}
|
||||
Reference in New Issue
Block a user