diff --git a/src-web/App.tsx b/src-web/App.tsx index cd24ca62..b9489754 100644 --- a/src-web/App.tsx +++ b/src-web/App.tsx @@ -3,7 +3,7 @@ import { listen } from '@tauri-apps/api/event'; import { MotionConfig } from 'framer-motion'; import { HelmetProvider } from 'react-helmet-async'; import { AppRouter } from './components/AppRouter'; -import { requestsQueryKey } from './hooks/useRequest'; +import { requestsQueryKey } from './hooks/useRequests'; import { responsesQueryKey } from './hooks/useResponses'; import { DEFAULT_FONT_SIZE } from './lib/constants'; import type { HttpRequest, HttpResponse } from './lib/models'; diff --git a/src-web/components/HeaderEditor.tsx b/src-web/components/HeaderEditor.tsx index f6390de3..18179fd6 100644 --- a/src-web/components/HeaderEditor.tsx +++ b/src-web/components/HeaderEditor.tsx @@ -1,6 +1,6 @@ import classnames from 'classnames'; import { useEffect, useState } from 'react'; -import { useRequestUpdate } from '../hooks/useRequest'; +import { useUpdateRequest } from '../hooks/useUpdateRequest'; import type { HttpHeader, HttpRequest } from '../lib/models'; import { IconButton } from './IconButton'; import { Input } from './Input'; @@ -14,7 +14,7 @@ interface Props { type PairWithId = { header: Partial; id: string }; export function HeaderEditor({ request, className }: Props) { - const updateRequest = useRequestUpdate(request); + const updateRequest = useUpdateRequest(request); const saveHeaders = (pairs: PairWithId[]) => { const headers = pairs.map((p) => ({ name: '', value: '', ...p.header })); updateRequest.mutate({ headers }); diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index cac5aa4d..059507e2 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -1,27 +1,31 @@ import classnames from 'classnames'; -import { useRequestUpdate, useSendRequest } from '../hooks/useRequest'; -import type { HttpRequest } from '../lib/models'; +import { useActiveRequest } from '../hooks/useActiveRequest'; +import { useSendRequest } from '../hooks/useSendRequest'; +import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { Editor } from './Editor'; import { HeaderEditor } from './HeaderEditor'; import { TabContent, Tabs } from './Tabs'; import { UrlBar } from './UrlBar'; interface Props { - request: HttpRequest; fullHeight: boolean; className?: string; } -export function RequestPane({ fullHeight, request, className }: Props) { - const updateRequest = useRequestUpdate(request ?? null); - const sendRequest = useSendRequest(request ?? null); +export function RequestPane({ fullHeight, className }: Props) { + const activeRequest = useActiveRequest(); + const updateRequest = useUpdateRequest(activeRequest); + const sendRequest = useSendRequest(activeRequest); + + if (activeRequest === null) return null; + return (
updateRequest.mutate({ method })} onUrlChange={(url) => updateRequest.mutate({ url })} @@ -41,15 +45,15 @@ export function RequestPane({ fullHeight, request, className }: Props) { label="Request body" > - + updateRequest.mutate({ body })} /> diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index eed51c2e..794ac6b9 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -1,6 +1,8 @@ import classnames from 'classnames'; 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 { Dropdown } from './Dropdown'; import { Editor } from './Editor'; @@ -11,23 +13,22 @@ import { StatusColor } from './StatusColor'; import { Webview } from './Webview'; interface Props { - requestId: string; className?: string; } -export const ResponsePane = memo(function ResponsePane({ requestId, className }: Props) { +export const ResponsePane = memo(function ResponsePane({ className }: Props) { const [activeResponseId, setActiveResponseId] = useState(null); const [viewMode, setViewMode] = useState<'pretty' | 'raw'>('pretty'); - const responses = useResponses(requestId); + const responses = useResponses(); const response = activeResponseId - ? responses.data.find((r) => r.id === activeResponseId) - : responses.data[responses.data.length - 1]; + ? responses.find((r) => r.id === activeResponseId) + : responses[responses.length - 1]; const deleteResponse = useDeleteResponse(response); - const deleteAllResponses = useDeleteAllResponses(response?.requestId); + const deleteAllResponses = useDeleteResponses(response?.requestId); useEffect(() => { setActiveResponseId(null); - }, [responses.data?.length]); + }, [responses.length]); const contentType = useMemo( () => @@ -35,10 +36,6 @@ export const ResponsePane = memo(function ResponsePane({ requestId, className }: [response], ); - if (!response) { - return null; - } - return (
{/**/} {/**/} + + {response && response.status > 0 && ( +
+ + {response.status} + {response.statusReason && ` ${response.statusReason}`} + +  •  + {response.elapsed}ms  •  + {Math.round(response.body.length / 1000)} KB +
+ )} + + + setViewMode((m) => (m === 'pretty' ? 'raw' : 'pretty'))} + /> + ({ + label: r.status + ' - ' + r.elapsed + ' ms', + leftSlot: response?.id === r.id ? : <>, + onSelect: () => setActiveResponseId(r.id), + })), + ]} + > + + + +
+ {response && ( <> - - {response.status > 0 && ( -
- - {response.status} - {response.statusReason && ` ${response.statusReason}`} - -  •  - {response.elapsed}ms  •  - {Math.round(response.body.length / 1000)} KB -
- )} - - - setViewMode((m) => (m === 'pretty' ? 'raw' : 'pretty'))} - /> - ({ - label: r.status + ' - ' + r.elapsed + ' ms', - leftSlot: response?.id === r.id ? : <>, - onSelect: () => setActiveResponseId(r.id), - })), - ]} - > - - - -
- {response?.error ? (
{response.error}
diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 2c5a5e74..c6147aad 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -1,7 +1,10 @@ import classnames from 'classnames'; 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 { useUpdateRequest } from '../hooks/useUpdateRequest'; import type { HttpRequest } from '../lib/models'; import { ButtonLink } from './ButtonLink'; import { IconButton } from './IconButton'; @@ -9,14 +12,13 @@ import { HStack, VStack } from './Stacks'; import { WindowDragRegion } from './WindowDragRegion'; interface Props { - workspaceId: string; - requests: HttpRequest[]; - activeRequestId?: string; className?: string; } -export function Sidebar({ className, activeRequestId, workspaceId, requests }: Props) { - const createRequest = useRequestCreate({ workspaceId, navigateAfter: true }); +export function Sidebar({ className }: Props) { + const requests = useRequests(); + const activeRequest = useActiveRequest(); + const createRequest = useCreateRequest({ navigateAfter: true }); const { appearance, toggleAppearance } = useTheme(); return (
{requests.map((r) => ( - + ))} {/**/} @@ -53,7 +55,7 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests }: P } function SidebarItem({ request, active }: { request: HttpRequest; active: boolean }) { - const updateRequest = useRequestUpdate(request); + const updateRequest = useUpdateRequest(request); const [editing, setEditing] = useState(false); const handleSubmitNameEdit = async (el: HTMLInputElement) => { await updateRequest.mutate({ name: el.value }); diff --git a/src-web/hooks/useActiveRequest.ts b/src-web/hooks/useActiveRequest.ts new file mode 100644 index 00000000..3d833070 --- /dev/null +++ b/src-web/hooks/useActiveRequest.ts @@ -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(null); + + useEffect(() => { + if (requests.length === 0) { + setActiveRequest(null); + } else { + setActiveRequest(requests.find((r) => r.id === params.requestId) ?? null); + } + }, [requests, params.requestId]); + + return activeRequest; +} diff --git a/src-web/hooks/useActiveWorkspace.ts b/src-web/hooks/useActiveWorkspace.ts new file mode 100644 index 00000000..a0515443 --- /dev/null +++ b/src-web/hooks/useActiveWorkspace.ts @@ -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(null); + + useEffect(() => { + if (workspaces.length === 0) { + setActiveWorkspace(null); + } else { + setActiveWorkspace(workspaces.find((w) => w.id === params.workspaceId) ?? null); + } + }, [workspaces, params.workspaceId]); + + return activeWorkspace; +} diff --git a/src-web/hooks/useCreateRequest.ts b/src-web/hooks/useCreateRequest.ts new file mode 100644 index 00000000..e20f61f1 --- /dev/null +++ b/src-web/hooks/useCreateRequest.ts @@ -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>({ + 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}`); + } + }, + }); +} diff --git a/src-web/hooks/useDeleteRequest.ts b/src-web/hooks/useDeleteRequest.ts new file mode 100644 index 00000000..6e31f5a7 --- /dev/null +++ b/src-web/hooks/useDeleteRequest.ts @@ -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({ + 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)); + }, + }); +} diff --git a/src-web/hooks/useDeleteResponses.ts b/src-web/hooks/useDeleteResponses.ts new file mode 100644 index 00000000..da0e45f5 --- /dev/null +++ b/src-web/hooks/useDeleteResponses.ts @@ -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 }], []); + }, + }); +} diff --git a/src-web/hooks/useRequest.ts b/src-web/hooks/useRequest.ts deleted file mode 100644 index dc880ec4..00000000 --- a/src-web/hooks/useRequest.ts +++ /dev/null @@ -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>({ - 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>({ - 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({ - 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({ - 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)); - }, - }); -} diff --git a/src-web/hooks/useRequests.ts b/src-web/hooks/useRequests.ts new file mode 100644 index 00000000..69bbd2e7 --- /dev/null +++ b/src-web/hooks/useRequests.ts @@ -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 ?? [] + ); +} diff --git a/src-web/hooks/useResponseDelete.ts b/src-web/hooks/useResponseDelete.ts new file mode 100644 index 00000000..405d78ea --- /dev/null +++ b/src-web/hooks/useResponseDelete.ts @@ -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), + ); + }, + }); +} diff --git a/src-web/hooks/useResponses.ts b/src-web/hooks/useResponses.ts index 8ed7ee15..fce592f4 100644 --- a/src-web/hooks/useResponses.ts +++ b/src-web/hooks/useResponses.ts @@ -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 type { HttpResponse } from '../lib/models'; import { convertDates } from '../lib/models'; +import { useActiveRequest } from './useActiveRequest'; export function responsesQueryKey(requestId: string) { return ['responses', { requestId }]; } -export function useResponses(requestId: string) { - return useQuery({ - initialData: [], - queryKey: responsesQueryKey(requestId), - queryFn: async () => { - const responses = (await invoke('responses', { requestId })) as HttpResponse[]; - return responses.map(convertDates); - }, - }); -} - -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), - ); - }, - }); -} - -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 }], []); - }, - }); +export function useResponses() { + const activeRequest = useActiveRequest(); + return ( + useQuery({ + enabled: activeRequest != null, + initialData: [], + queryKey: responsesQueryKey(activeRequest?.id ?? 'n/a'), + queryFn: async () => { + const responses = (await invoke('responses', { + requestId: activeRequest?.id, + })) as HttpResponse[]; + return responses.map(convertDates); + }, + }).data ?? [] + ); } diff --git a/src-web/hooks/useSendRequest.ts b/src-web/hooks/useSendRequest.ts new file mode 100644 index 00000000..e35b48c6 --- /dev/null +++ b/src-web/hooks/useSendRequest.ts @@ -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({ + 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)); + }, + }); +} diff --git a/src-web/hooks/useUpdateRequest.ts b/src-web/hooks/useUpdateRequest.ts new file mode 100644 index 00000000..9295b41d --- /dev/null +++ b/src-web/hooks/useUpdateRequest.ts @@ -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>({ + 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', ''), + }, + }); + }, + }); +} diff --git a/src-web/hooks/useWorkspaces.ts b/src-web/hooks/useWorkspaces.ts index 84561bc4..bb882b28 100644 --- a/src-web/hooks/useWorkspaces.ts +++ b/src-web/hooks/useWorkspaces.ts @@ -4,8 +4,10 @@ import { convertDates } from '../lib/models'; import { useQuery } from '@tanstack/react-query'; export function useWorkspaces() { - return useQuery(['workspaces'], async () => { - const workspaces = (await invoke('workspaces')) as Workspace[]; - return workspaces.map(convertDates); - }); + return ( + useQuery(['workspaces'], async () => { + const workspaces = (await invoke('workspaces')) as Workspace[]; + return workspaces.map(convertDates); + }).data ?? [] + ); } diff --git a/src-web/pages/Workspace.tsx b/src-web/pages/Workspace.tsx index 33c0ff51..7771593a 100644 --- a/src-web/pages/Workspace.tsx +++ b/src-web/pages/Workspace.tsx @@ -1,60 +1,40 @@ import classnames from 'classnames'; -import { useParams } from 'react-router-dom'; import { useWindowSize } from 'react-use'; -import { HeaderEditor } from '../components/HeaderEditor'; import { RequestPane } from '../components/RequestPane'; import { ResponsePane } from '../components/ResponsePane'; import { Sidebar } from '../components/Sidebar'; import { HStack } from '../components/Stacks'; import { WindowDragRegion } from '../components/WindowDragRegion'; -import { useRequests } from '../hooks/useRequest'; - -type Params = { - workspaceId: string; - requestId?: string; -}; +import { useActiveRequest } from '../hooks/useActiveRequest'; export default function Workspace() { - const params = useParams(); - const workspaceId = params?.workspaceId ?? ''; - const { data: requests } = useRequests(workspaceId); - const request = requests?.find((r) => r.id === params?.requestId); + const activeRequest = useActiveRequest(); const { width } = useWindowSize(); const isH = width > 900; return (
- - {request && ( -
- - {request.name} - -
- - -
+ +
+ + {activeRequest?.name} + +
+ +
- )} +
); } diff --git a/src-web/pages/Workspaces.tsx b/src-web/pages/Workspaces.tsx index 4c5040a5..076b8612 100644 --- a/src-web/pages/Workspaces.tsx +++ b/src-web/pages/Workspaces.tsx @@ -8,7 +8,7 @@ export default function Workspaces() { return ( Workspaces - {workspaces.data?.map((w) => ( + {workspaces.map((w) => ( {w.name}