Hotkeys and view mode kvs

This commit is contained in:
Gregory Schier
2023-03-16 09:24:28 -07:00
parent 5a6acb24d9
commit 0949de66bf
14 changed files with 239 additions and 38 deletions

View File

@@ -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>

View File

@@ -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 />,
},
],

View File

@@ -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={[

View File

@@ -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,
},

View File

@@ -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';

View File

@@ -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), []);
},
});
}

View 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);
}

View File

@@ -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('::');
}

View 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];
}

View File

@@ -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
View File

@@ -0,0 +1,6 @@
export function pluralize(word: string, count: number): string {
if (count === 1) {
return word;
}
return `${word}s`;
}