Refactor hooks to be easier to use

This commit is contained in:
Gregory Schier
2023-03-13 23:25:41 -07:00
parent aa66f957f2
commit 5f947ac983
19 changed files with 314 additions and 258 deletions

View File

@@ -3,7 +3,7 @@ import { listen } from '@tauri-apps/api/event';
import { MotionConfig } from 'framer-motion'; import { MotionConfig } from 'framer-motion';
import { HelmetProvider } from 'react-helmet-async'; import { HelmetProvider } from 'react-helmet-async';
import { AppRouter } from './components/AppRouter'; import { AppRouter } from './components/AppRouter';
import { requestsQueryKey } from './hooks/useRequest'; import { requestsQueryKey } from './hooks/useRequests';
import { responsesQueryKey } from './hooks/useResponses'; import { responsesQueryKey } from './hooks/useResponses';
import { DEFAULT_FONT_SIZE } from './lib/constants'; import { DEFAULT_FONT_SIZE } from './lib/constants';
import type { HttpRequest, HttpResponse } from './lib/models'; import type { HttpRequest, HttpResponse } from './lib/models';

View File

@@ -1,6 +1,6 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRequestUpdate } from '../hooks/useRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { HttpHeader, HttpRequest } from '../lib/models'; import type { HttpHeader, HttpRequest } from '../lib/models';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { Input } from './Input'; import { Input } from './Input';
@@ -14,7 +14,7 @@ interface Props {
type PairWithId = { header: Partial<HttpHeader>; id: string }; type PairWithId = { header: Partial<HttpHeader>; id: string };
export function HeaderEditor({ request, className }: Props) { export function HeaderEditor({ request, className }: Props) {
const updateRequest = useRequestUpdate(request); const updateRequest = useUpdateRequest(request);
const saveHeaders = (pairs: PairWithId[]) => { const saveHeaders = (pairs: PairWithId[]) => {
const headers = pairs.map((p) => ({ name: '', value: '', ...p.header })); const headers = pairs.map((p) => ({ name: '', value: '', ...p.header }));
updateRequest.mutate({ headers }); updateRequest.mutate({ headers });

View File

@@ -1,27 +1,31 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { useRequestUpdate, useSendRequest } from '../hooks/useRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import type { HttpRequest } from '../lib/models'; import { useSendRequest } from '../hooks/useSendRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { Editor } from './Editor'; import { Editor } from './Editor';
import { HeaderEditor } from './HeaderEditor'; import { HeaderEditor } from './HeaderEditor';
import { TabContent, Tabs } from './Tabs'; import { TabContent, Tabs } from './Tabs';
import { UrlBar } from './UrlBar'; import { UrlBar } from './UrlBar';
interface Props { interface Props {
request: HttpRequest;
fullHeight: boolean; fullHeight: boolean;
className?: string; className?: string;
} }
export function RequestPane({ fullHeight, request, className }: Props) { export function RequestPane({ fullHeight, className }: Props) {
const updateRequest = useRequestUpdate(request ?? null); const activeRequest = useActiveRequest();
const sendRequest = useSendRequest(request ?? null); const updateRequest = useUpdateRequest(activeRequest);
const sendRequest = useSendRequest(activeRequest);
if (activeRequest === null) return null;
return ( return (
<div className={classnames(className, 'py-2 grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}> <div className={classnames(className, 'py-2 grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}>
<div className="pl-2"> <div className="pl-2">
<UrlBar <UrlBar
key={request.id} key={activeRequest.id}
method={request.method} method={activeRequest.method}
url={request.url} url={activeRequest.url}
loading={sendRequest.isLoading} loading={sendRequest.isLoading}
onMethodChange={(method) => updateRequest.mutate({ method })} onMethodChange={(method) => updateRequest.mutate({ method })}
onUrlChange={(url) => updateRequest.mutate({ url })} onUrlChange={(url) => updateRequest.mutate({ url })}
@@ -41,15 +45,15 @@ export function RequestPane({ fullHeight, request, className }: Props) {
label="Request body" label="Request body"
> >
<TabContent value="headers" className="pl-2"> <TabContent value="headers" className="pl-2">
<HeaderEditor key={request.id} request={request} /> <HeaderEditor key={activeRequest.id} request={activeRequest} />
</TabContent> </TabContent>
<TabContent value="body"> <TabContent value="body">
<Editor <Editor
key={request.id} key={activeRequest.id}
className="!bg-gray-50" className="!bg-gray-50"
heightMode={fullHeight ? 'full' : 'auto'} heightMode={fullHeight ? 'full' : 'auto'}
useTemplating useTemplating
defaultValue={request.body ?? ''} defaultValue={activeRequest.body ?? ''}
contentType="application/graphql+json" contentType="application/graphql+json"
onChange={(body) => updateRequest.mutate({ body })} onChange={(body) => updateRequest.mutate({ body })}
/> />

View File

@@ -1,6 +1,8 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { memo, useEffect, useMemo, useState } from 'react'; import { memo, useEffect, useMemo, useState } from 'react';
import { useDeleteAllResponses, useDeleteResponse, useResponses } from '../hooks/useResponses'; import { useDeleteResponses } from '../hooks/useDeleteResponses';
import { useDeleteResponse } from '../hooks/useResponseDelete';
import { useResponses } from '../hooks/useResponses';
import { tryFormatJson } from '../lib/formatters'; import { tryFormatJson } from '../lib/formatters';
import { Dropdown } from './Dropdown'; import { Dropdown } from './Dropdown';
import { Editor } from './Editor'; import { Editor } from './Editor';
@@ -11,23 +13,22 @@ import { StatusColor } from './StatusColor';
import { Webview } from './Webview'; import { Webview } from './Webview';
interface Props { interface Props {
requestId: string;
className?: string; className?: string;
} }
export const ResponsePane = memo(function ResponsePane({ requestId, className }: Props) { export const ResponsePane = memo(function ResponsePane({ className }: Props) {
const [activeResponseId, setActiveResponseId] = useState<string | null>(null); const [activeResponseId, setActiveResponseId] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<'pretty' | 'raw'>('pretty'); const [viewMode, setViewMode] = useState<'pretty' | 'raw'>('pretty');
const responses = useResponses(requestId); const responses = useResponses();
const response = activeResponseId const response = activeResponseId
? responses.data.find((r) => r.id === activeResponseId) ? responses.find((r) => r.id === activeResponseId)
: responses.data[responses.data.length - 1]; : responses[responses.length - 1];
const deleteResponse = useDeleteResponse(response); const deleteResponse = useDeleteResponse(response);
const deleteAllResponses = useDeleteAllResponses(response?.requestId); const deleteAllResponses = useDeleteResponses(response?.requestId);
useEffect(() => { useEffect(() => {
setActiveResponseId(null); setActiveResponseId(null);
}, [responses.data?.length]); }, [responses.length]);
const contentType = useMemo( const contentType = useMemo(
() => () =>
@@ -35,10 +36,6 @@ export const ResponsePane = memo(function ResponsePane({ requestId, className }:
[response], [response],
); );
if (!response) {
return null;
}
return ( return (
<div className="p-2"> <div className="p-2">
<div <div
@@ -51,56 +48,56 @@ export const ResponsePane = memo(function ResponsePane({ requestId, className }:
> >
{/*<HStack as={WindowDragRegion} items="center" className="pl-1.5 pr-1">*/} {/*<HStack as={WindowDragRegion} items="center" className="pl-1.5 pr-1">*/}
{/*</HStack>*/} {/*</HStack>*/}
<HStack
alignItems="center"
className="italic text-gray-700 text-sm w-full mb-1 flex-shrink-0 pl-2"
>
{response && response.status > 0 && (
<div className="whitespace-nowrap">
<StatusColor statusCode={response.status}>
{response.status}
{response.statusReason && ` ${response.statusReason}`}
</StatusColor>
&nbsp;&bull;&nbsp;
{response.elapsed}ms &nbsp;&bull;&nbsp;
{Math.round(response.body.length / 1000)} KB
</div>
)}
<HStack alignItems="center" className="ml-auto h-8">
<IconButton
icon={viewMode === 'pretty' ? 'eye' : 'code'}
size="sm"
className="ml-1"
onClick={() => setViewMode((m) => (m === 'pretty' ? 'raw' : 'pretty'))}
/>
<Dropdown
items={[
{
label: 'Clear Response',
onSelect: deleteResponse.mutate,
disabled: responses.length === 0,
},
{
label: 'Clear All Responses',
onSelect: deleteAllResponses.mutate,
disabled: responses.length === 0,
},
'-----',
...responses.slice(0, 10).map((r) => ({
label: r.status + ' - ' + r.elapsed + ' ms',
leftSlot: response?.id === r.id ? <Icon icon="check" /> : <></>,
onSelect: () => setActiveResponseId(r.id),
})),
]}
>
<IconButton icon="clock" className="ml-auto" size="sm" />
</Dropdown>
</HStack>
</HStack>
{response && ( {response && (
<> <>
<HStack
alignItems="center"
className="italic text-gray-700 text-sm w-full mb-1 flex-shrink-0 pl-2"
>
{response.status > 0 && (
<div className="whitespace-nowrap">
<StatusColor statusCode={response.status}>
{response.status}
{response.statusReason && ` ${response.statusReason}`}
</StatusColor>
&nbsp;&bull;&nbsp;
{response.elapsed}ms &nbsp;&bull;&nbsp;
{Math.round(response.body.length / 1000)} KB
</div>
)}
<HStack alignItems="center" className="ml-auto h-8">
<IconButton
icon={viewMode === 'pretty' ? 'eye' : 'code'}
size="sm"
className="ml-1"
onClick={() => setViewMode((m) => (m === 'pretty' ? 'raw' : 'pretty'))}
/>
<Dropdown
items={[
{
label: 'Clear Response',
onSelect: deleteResponse.mutate,
disabled: responses.data.length === 0,
},
{
label: 'Clear All Responses',
onSelect: deleteAllResponses.mutate,
disabled: responses.data.length === 0,
},
'-----',
...responses.data.slice(0, 10).map((r) => ({
label: r.status + ' - ' + r.elapsed + ' ms',
leftSlot: response?.id === r.id ? <Icon icon="check" /> : <></>,
onSelect: () => setActiveResponseId(r.id),
})),
]}
>
<IconButton icon="clock" className="ml-auto" size="sm" />
</Dropdown>
</HStack>
</HStack>
{response?.error ? ( {response?.error ? (
<div className="p-1"> <div className="p-1">
<div className="text-white bg-red-500 px-3 py-2 rounded">{response.error}</div> <div className="text-white bg-red-500 px-3 py-2 rounded">{response.error}</div>

View File

@@ -1,7 +1,10 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { useState } from 'react'; import { useState } from 'react';
import { useRequestCreate, useRequestUpdate } from '../hooks/useRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useCreateRequest } from '../hooks/useCreateRequest';
import { useRequests } from '../hooks/useRequests';
import { useTheme } from '../hooks/useTheme'; import { useTheme } from '../hooks/useTheme';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { ButtonLink } from './ButtonLink'; import { ButtonLink } from './ButtonLink';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
@@ -9,14 +12,13 @@ import { HStack, VStack } from './Stacks';
import { WindowDragRegion } from './WindowDragRegion'; import { WindowDragRegion } from './WindowDragRegion';
interface Props { interface Props {
workspaceId: string;
requests: HttpRequest[];
activeRequestId?: string;
className?: string; className?: string;
} }
export function Sidebar({ className, activeRequestId, workspaceId, requests }: Props) { export function Sidebar({ className }: Props) {
const createRequest = useRequestCreate({ workspaceId, navigateAfter: true }); const requests = useRequests();
const activeRequest = useActiveRequest();
const createRequest = useCreateRequest({ navigateAfter: true });
const { appearance, toggleAppearance } = useTheme(); const { appearance, toggleAppearance } = useTheme();
return ( return (
<div <div
@@ -36,7 +38,7 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests }: P
</HStack> </HStack>
<VStack as="ul" className="py-3 px-2 overflow-auto h-full" space={1}> <VStack as="ul" className="py-3 px-2 overflow-auto h-full" space={1}>
{requests.map((r) => ( {requests.map((r) => (
<SidebarItem key={r.id} request={r} active={r.id === activeRequestId} /> <SidebarItem key={r.id} request={r} active={r.id === activeRequest?.id} />
))} ))}
{/*<Colors />*/} {/*<Colors />*/}
@@ -53,7 +55,7 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests }: P
} }
function SidebarItem({ request, active }: { request: HttpRequest; active: boolean }) { function SidebarItem({ request, active }: { request: HttpRequest; active: boolean }) {
const updateRequest = useRequestUpdate(request); const updateRequest = useUpdateRequest(request);
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const handleSubmitNameEdit = async (el: HTMLInputElement) => { const handleSubmitNameEdit = async (el: HTMLInputElement) => {
await updateRequest.mutate({ name: el.value }); await updateRequest.mutate({ name: el.value });

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import type { HttpRequest } from '../lib/models';
import { useRequests } from './useRequests';
export function useActiveRequest(): HttpRequest | null {
const params = useParams<{ requestId?: string }>();
const requests = useRequests();
const [activeRequest, setActiveRequest] = useState<HttpRequest | null>(null);
useEffect(() => {
if (requests.length === 0) {
setActiveRequest(null);
} else {
setActiveRequest(requests.find((r) => r.id === params.requestId) ?? null);
}
}, [requests, params.requestId]);
return activeRequest;
}

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import type { Workspace } from '../lib/models';
import { useWorkspaces } from './useWorkspaces';
export function useActiveWorkspace(): Workspace | null {
const params = useParams<{ workspaceId?: string }>();
const workspaces = useWorkspaces();
const [activeWorkspace, setActiveWorkspace] = useState<Workspace | null>(null);
useEffect(() => {
if (workspaces.length === 0) {
setActiveWorkspace(null);
} else {
setActiveWorkspace(workspaces.find((w) => w.id === params.workspaceId) ?? null);
}
}, [workspaces, params.workspaceId]);
return activeWorkspace;
}

View File

@@ -0,0 +1,23 @@
import { useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { useNavigate } from 'react-router-dom';
import type { HttpRequest } from '../lib/models';
import { useActiveWorkspace } from './useActiveWorkspace';
export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean }) {
const workspace = useActiveWorkspace();
const navigate = useNavigate();
return useMutation<string, unknown, Pick<HttpRequest, 'name'>>({
mutationFn: async (patch) => {
if (workspace === null) {
throw new Error("Cannot create request when there's no active workspace");
}
return invoke('create_request', { ...patch, workspaceId: workspace?.id });
},
onSuccess: async (requestId) => {
if (navigateAfter) {
navigate(`/workspaces/${workspace?.id}/requests/${requestId}`);
}
},
});
}

View File

@@ -0,0 +1,18 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { HttpRequest } from '../lib/models';
import { requestsQueryKey } from './useRequests';
export function useDeleteRequest(request: HttpRequest | null) {
const queryClient = useQueryClient();
return useMutation<void, string>({
mutationFn: async () => {
if (request == null) return;
await invoke('delete_request', { requestId: request.id });
},
onSuccess: async () => {
if (request == null) return;
await queryClient.invalidateQueries(requestsQueryKey(request.workspaceId));
},
});
}

View File

@@ -0,0 +1,16 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
export function useDeleteResponses(requestId?: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
if (requestId == null) return;
await invoke('delete_all_responses', { requestId });
},
onSuccess: () => {
if (requestId == null) return;
queryClient.setQueryData(['responses', { requestId: requestId }], []);
},
});
}

View File

@@ -1,87 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { useNavigate, useNavigation } from 'react-router-dom';
import type { HttpRequest } from '../lib/models';
import { convertDates } from '../lib/models';
import { responsesQueryKey } from './useResponses';
export function requestsQueryKey(workspaceId: string) {
return ['requests', { workspaceId }];
}
export function useRequests(workspaceId: string) {
return useQuery({
queryKey: requestsQueryKey(workspaceId),
queryFn: async () => {
const requests = (await invoke('requests', { workspaceId })) as HttpRequest[];
return requests.map(convertDates);
},
});
}
export function useRequestUpdate(request: HttpRequest | null) {
return useMutation<void, unknown, Partial<HttpRequest>>({
mutationFn: async (patch) => {
if (request == null) {
throw new Error("Can't update a null request");
}
const updatedRequest = { ...request, ...patch };
console.log('UPDATE REQUEST', updatedRequest.url);
await invoke('update_request', {
request: {
...updatedRequest,
createdAt: updatedRequest.createdAt.toISOString().replace('Z', ''),
updatedAt: updatedRequest.updatedAt.toISOString().replace('Z', ''),
},
});
},
});
}
export function useRequestCreate({
workspaceId,
navigateAfter,
}: {
workspaceId: string;
navigateAfter: boolean;
}) {
const navigate = useNavigate();
return useMutation<string, unknown, Pick<HttpRequest, 'name'>>({
mutationFn: async (patch) => invoke('create_request', { ...patch, workspaceId }),
onSuccess: async (requestId) => {
if (navigateAfter) {
navigate(`/workspaces/${workspaceId}/requests/${requestId}`);
}
},
});
}
export function useSendRequest(request: HttpRequest | null) {
const queryClient = useQueryClient();
return useMutation<void, string>({
mutationFn: async () => {
if (request == null) return;
await invoke('send_request', { requestId: request.id });
},
onSuccess: async () => {
if (request == null) return;
await queryClient.invalidateQueries(responsesQueryKey(request.id));
},
});
}
export function useDeleteRequest(request: HttpRequest | null) {
const queryClient = useQueryClient();
return useMutation<void, string>({
mutationFn: async () => {
if (request == null) return;
await invoke('delete_request', { requestId: request.id });
},
onSuccess: async () => {
if (request == null) return;
await queryClient.invalidateQueries(requestsQueryKey(request.workspaceId));
},
});
}

View File

@@ -0,0 +1,24 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { HttpRequest } from '../lib/models';
import { convertDates } from '../lib/models';
import { useActiveWorkspace } from './useActiveWorkspace';
export function requestsQueryKey(workspaceId: string) {
return ['requests', { workspaceId }];
}
export function useRequests() {
const workspace = useActiveWorkspace();
return (
useQuery({
enabled: workspace != null,
queryKey: requestsQueryKey(workspace?.id ?? 'n/a'),
queryFn: async () => {
if (workspace == null) return [];
const requests = (await invoke('requests', { workspaceId: workspace.id })) as HttpRequest[];
return requests.map(convertDates);
},
}).data ?? []
);
}

View File

@@ -0,0 +1,20 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { HttpResponse } from '../lib/models';
export function useDeleteResponse(response?: HttpResponse) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
if (response == null) return;
await invoke('delete_response', { id: response.id });
},
onSuccess: () => {
if (response == null) return;
queryClient.setQueryData(
['responses', { requestId: response.requestId }],
(responses: HttpResponse[] = []) => responses.filter((r) => r.id !== response.id),
);
},
});
}

View File

@@ -1,50 +1,26 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import type { HttpResponse } from '../lib/models'; import type { HttpResponse } from '../lib/models';
import { convertDates } from '../lib/models'; import { convertDates } from '../lib/models';
import { useActiveRequest } from './useActiveRequest';
export function responsesQueryKey(requestId: string) { export function responsesQueryKey(requestId: string) {
return ['responses', { requestId }]; return ['responses', { requestId }];
} }
export function useResponses(requestId: string) { export function useResponses() {
return useQuery<HttpResponse[]>({ const activeRequest = useActiveRequest();
initialData: [], return (
queryKey: responsesQueryKey(requestId), useQuery<HttpResponse[]>({
queryFn: async () => { enabled: activeRequest != null,
const responses = (await invoke('responses', { requestId })) as HttpResponse[]; initialData: [],
return responses.map(convertDates); queryKey: responsesQueryKey(activeRequest?.id ?? 'n/a'),
}, queryFn: async () => {
}); const responses = (await invoke('responses', {
} requestId: activeRequest?.id,
})) as HttpResponse[];
export function useDeleteResponse(response?: HttpResponse) { return responses.map(convertDates);
const queryClient = useQueryClient(); },
return useMutation({ }).data ?? []
mutationFn: async () => { );
if (response == null) return;
await invoke('delete_response', { id: response.id });
},
onSuccess: () => {
if (response == null) return;
queryClient.setQueryData(
['responses', { requestId: response.requestId }],
(responses: HttpResponse[] = []) => responses.filter((r) => r.id !== response.id),
);
},
});
}
export function useDeleteAllResponses(requestId?: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
if (requestId == null) return;
await invoke('delete_all_responses', { requestId });
},
onSuccess: () => {
if (requestId == null) return;
queryClient.setQueryData(['responses', { requestId: requestId }], []);
},
});
} }

View File

@@ -0,0 +1,18 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { HttpRequest } from '../lib/models';
import { responsesQueryKey } from './useResponses';
export function useSendRequest(request: HttpRequest | null) {
const queryClient = useQueryClient();
return useMutation<void, string>({
mutationFn: async () => {
if (request == null) return;
await invoke('send_request', { requestId: request.id });
},
onSuccess: async () => {
if (request == null) return;
await queryClient.invalidateQueries(responsesQueryKey(request.id));
},
});
}

View File

@@ -0,0 +1,23 @@
import { useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { HttpRequest } from '../lib/models';
export function useUpdateRequest(request: HttpRequest | null) {
return useMutation<void, unknown, Partial<HttpRequest>>({
mutationFn: async (patch) => {
if (request == null) {
throw new Error("Can't update a null request");
}
const updatedRequest = { ...request, ...patch };
await invoke('update_request', {
request: {
...updatedRequest,
createdAt: updatedRequest.createdAt.toISOString().replace('Z', ''),
updatedAt: updatedRequest.updatedAt.toISOString().replace('Z', ''),
},
});
},
});
}

View File

@@ -4,8 +4,10 @@ import { convertDates } from '../lib/models';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
export function useWorkspaces() { export function useWorkspaces() {
return useQuery(['workspaces'], async () => { return (
const workspaces = (await invoke('workspaces')) as Workspace[]; useQuery(['workspaces'], async () => {
return workspaces.map(convertDates); const workspaces = (await invoke('workspaces')) as Workspace[];
}); return workspaces.map(convertDates);
}).data ?? []
);
} }

View File

@@ -1,60 +1,40 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { useParams } from 'react-router-dom';
import { useWindowSize } from 'react-use'; import { useWindowSize } from 'react-use';
import { HeaderEditor } from '../components/HeaderEditor';
import { RequestPane } from '../components/RequestPane'; import { RequestPane } from '../components/RequestPane';
import { ResponsePane } from '../components/ResponsePane'; import { ResponsePane } from '../components/ResponsePane';
import { Sidebar } from '../components/Sidebar'; import { Sidebar } from '../components/Sidebar';
import { HStack } from '../components/Stacks'; import { HStack } from '../components/Stacks';
import { WindowDragRegion } from '../components/WindowDragRegion'; import { WindowDragRegion } from '../components/WindowDragRegion';
import { useRequests } from '../hooks/useRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
type Params = {
workspaceId: string;
requestId?: string;
};
export default function Workspace() { export default function Workspace() {
const params = useParams<Params>(); const activeRequest = useActiveRequest();
const workspaceId = params?.workspaceId ?? '';
const { data: requests } = useRequests(workspaceId);
const request = requests?.find((r) => r.id === params?.requestId);
const { width } = useWindowSize(); const { width } = useWindowSize();
const isH = width > 900; const isH = width > 900;
return ( return (
<div className="grid grid-cols-[auto_1fr] grid-rows-1 h-full text-gray-900"> <div className="grid grid-cols-[auto_1fr] grid-rows-1 h-full text-gray-900">
<Sidebar <Sidebar />
requests={requests ?? []} <div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
workspaceId={workspaceId} <HStack
activeRequestId={params?.requestId} as={WindowDragRegion}
/> className="px-3 bg-gray-50 text-gray-900 border-b border-b-gray-200 pt-[1px]"
{request && ( alignItems="center"
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full"> >
<HStack {activeRequest?.name}
as={WindowDragRegion} </HStack>
className="px-3 bg-gray-50 text-gray-900 border-b border-b-gray-200 pt-[1px]" <div
alignItems="center" className={classnames(
> 'grid',
{request.name} isH
</HStack> ? 'grid-cols-[1fr_1fr] grid-rows-1'
<div : 'grid-cols-1 grid-rows-[minmax(0,auto)_minmax(0,100%)]',
className={classnames( )}
'grid', >
isH <RequestPane fullHeight={isH} className={classnames(!isH && 'pr-2 pb-0')} />
? 'grid-cols-[1fr_1fr] grid-rows-1' <ResponsePane />
: 'grid-cols-1 grid-rows-[minmax(0,auto)_minmax(0,100%)]',
)}
>
<RequestPane
fullHeight={isH}
request={request}
className={classnames(!isH && 'pr-2 pb-0')}
/>
<ResponsePane requestId={request.id} />
</div>
</div> </div>
)} </div>
</div> </div>
); );
} }

View File

@@ -8,7 +8,7 @@ export default function Workspaces() {
return ( return (
<VStack as="ul" className="p-12"> <VStack as="ul" className="p-12">
<Heading>Workspaces</Heading> <Heading>Workspaces</Heading>
{workspaces.data?.map((w) => ( {workspaces.map((w) => (
<ButtonLink key={w.id} color="gray" to={`/workspaces/${w.id}`}> <ButtonLink key={w.id} color="gray" to={`/workspaces/${w.id}`}>
{w.name} {w.name}
</ButtonLink> </ButtonLink>