From c0d9740a7d1f751623ef4700fdcfec21113528c6 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sat, 18 Mar 2023 18:49:01 -0700 Subject: [PATCH] Optimized a few components --- src-web/components/RequestPane.tsx | 70 ++++++++++++++---------- src-web/components/Sidebar.tsx | 77 +++++++++++++++++---------- src-web/components/UrlBar.tsx | 11 +++- src-web/components/Workspace.tsx | 2 +- src-web/components/core/Dropdown.tsx | 44 ++++++++++----- src-web/components/core/Icon.tsx | 5 +- src-web/components/core/Stacks.tsx | 71 ++++++------------------ src-web/components/core/Tabs/Tabs.tsx | 20 +++---- src-web/hooks/useActiveRequest.ts | 4 +- src-web/hooks/useActiveRequestId.ts | 6 +++ src-web/hooks/useActiveWorkspace.ts | 4 +- src-web/hooks/useActiveWorkspaceId.ts | 6 +++ src-web/hooks/useDeleteRequest.ts | 13 ++--- src-web/hooks/useRequest.ts | 7 +++ src-web/hooks/useSendRequest.ts | 11 ++-- src-web/hooks/useTheme.ts | 1 - src-web/hooks/useUpdateRequest.ts | 4 +- 17 files changed, 200 insertions(+), 156 deletions(-) create mode 100644 src-web/hooks/useActiveRequestId.ts create mode 100644 src-web/hooks/useActiveWorkspaceId.ts create mode 100644 src-web/hooks/useRequest.ts diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index e7d39653..eb65ef89 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -1,13 +1,15 @@ import classnames from 'classnames'; -import { act } from 'react-dom/test-utils'; +import { useCallback, useMemo } from 'react'; import { useActiveRequest } from '../hooks/useActiveRequest'; import { useIsResponseLoading } from '../hooks/useIsResponseLoading'; import { useKeyValue } from '../hooks/useKeyValue'; import { useSendRequest } from '../hooks/useSendRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { tryFormatJson } from '../lib/formatters'; +import type { HttpHeader } from '../lib/models'; import { Editor } from './core/Editor'; import { PairEditor } from './core/PairEditor'; +import type { TabItem } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs'; import { GraphQLEditor } from './editors/GraphQLEditor'; import { UrlBar } from './UrlBar'; @@ -19,14 +21,45 @@ interface Props { export function RequestPane({ fullHeight, className }: Props) { const activeRequest = useActiveRequest(); - const updateRequest = useUpdateRequest(activeRequest); - const sendRequest = useSendRequest(activeRequest); + const activeRequestId = activeRequest?.id ?? null; + const updateRequest = useUpdateRequest(activeRequestId); + const sendRequest = useSendRequest(activeRequestId); const responseLoading = useIsResponseLoading(); const activeTab = useKeyValue({ - key: ['active_request_body_tab', activeRequest?.id ?? 'n/a'], + key: ['active_request_body_tab', activeRequestId ?? 'n/a'], initialValue: 'body', }); + const tabs: TabItem[] = useMemo( + () => [ + { + value: 'body', + label: activeRequest?.bodyType ?? 'NoBody', + options: { + onValueChange: (t) => updateRequest.mutate({ bodyType: t.value }), + value: activeRequest?.bodyType ?? 'nobody', + items: [ + { label: 'No Body', value: 'nobody' }, + { label: 'JSON', value: 'json' }, + { label: 'GraphQL', value: 'graphql' }, + ], + }, + }, + { value: 'params', label: 'Params' }, + { value: 'headers', label: 'Headers' }, + { value: 'auth', label: 'Auth' }, + ], + [], + ); + + const handleMethodChange = useCallback((method: string) => updateRequest.mutate({ method }), []); + const handleUrlChange = useCallback((url: string) => updateRequest.mutate({ url }), []); + const handleBodyChange = useCallback((body: string) => updateRequest.mutate({ body }), []); + const handleHeadersChange = useCallback( + (headers: HttpHeader[]) => updateRequest.mutate({ headers }), + [], + ); + if (activeRequest === null) return null; return ( @@ -35,32 +68,15 @@ export function RequestPane({ fullHeight, className }: Props) { key={activeRequest.id} method={activeRequest.method} url={activeRequest.url} - onMethodChange={(method) => updateRequest.mutate({ method })} - onUrlChange={(url) => updateRequest.mutate({ url })} + onMethodChange={handleMethodChange} + onUrlChange={handleUrlChange} sendRequest={sendRequest} loading={responseLoading} /> updateRequest.mutate({ bodyType: bodyType.value }), - value: activeRequest.bodyType ?? 'nobody', - items: [ - { label: 'No Body', value: 'nobody' }, - { label: 'JSON', value: 'json' }, - { label: 'GraphQL', value: 'graphql' }, - ], - }, - }, - { value: 'params', label: 'Params' }, - { value: 'headers', label: 'Headers' }, - { value: 'auth', label: 'Auth' }, - ]} + tabs={tabs} className="mt-2" label="Request body" > @@ -68,7 +84,7 @@ export function RequestPane({ fullHeight, className }: Props) { updateRequest.mutate({ headers })} + onChange={handleHeadersChange} /> @@ -80,7 +96,7 @@ export function RequestPane({ fullHeight, className }: Props) { heightMode={fullHeight ? 'full' : 'auto'} defaultValue={activeRequest.body ?? ''} contentType="application/json" - onChange={(body) => updateRequest.mutate({ body })} + onChange={handleBodyChange} format={activeRequest.bodyType === 'json' ? (v) => tryFormatJson(v) : undefined} /> ) : activeRequest.bodyType === 'graphql' ? ( @@ -88,7 +104,7 @@ export function RequestPane({ fullHeight, className }: Props) { key={activeRequest.id} className="!bg-gray-50" defaultValue={activeRequest?.body ?? ''} - onChange={(body) => updateRequest.mutate({ body })} + onChange={handleBodyChange} /> ) : (
No Body
diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 0cb807ba..160a4450 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -79,7 +79,6 @@ export function Container({ className }: Props) { }, []); const sidebarStyles = useMemo(() => ({ width: width.value }), [width.value]); - console.log('RENDER SIDEBAR'); return (
{ setItems(requests.map((r) => ({ request: r, left: 0, top: 0 }))); - }, [requests.length]); + }, [requests]); const handleMove = useCallback((id: string, hoverId: string) => { setItems((oldItems) => { const dragIndex = oldItems.findIndex((i) => i.request.id === id); const index = oldItems.findIndex((i) => i.request.id === hoverId); const newItems = [...oldItems]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const b = newItems[index]!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion newItems[index] = newItems[dragIndex]!; newItems[dragIndex] = b; return newItems; @@ -164,7 +165,9 @@ function SidebarItems({ {items.map(({ request }) => ( (false); const handleSubmitNameEdit = useCallback(async (el: HTMLInputElement) => { @@ -197,6 +208,8 @@ function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) { const itemStyles = useMemo(() => ({ width: sidebarWidth }), [sidebarWidth]); + if (workspaceId === null) return null; + return (
  • @@ -220,7 +233,7 @@ function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) { setEditing(true); } }} - // to={`/workspaces/${request.workspaceId}/requests/${request.id}`} + to={`/workspaces/${workspaceId}/requests/${requestId}`} onDoubleClick={() => setEditing(true)} onClick={active ? () => setEditing(true) : undefined} justify="start" @@ -228,7 +241,7 @@ function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) { {editing ? ( handleSubmitNameEdit(e.currentTarget)} onKeyDown={async (e) => { @@ -243,24 +256,22 @@ function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) { }} /> ) : ( - - {request.name || request.url || 'New Request'} + + {requestName || 'New Request'} )} , - }, - ]} + items={useMemo( + () => [ + { + label: 'Delete Request', + onSelect: deleteRequest.mutate, + leftSlot: , + }, + ], + [], + )} >
  • ); -} +}); type DraggableSidebarItemProps = SidebarItemProps & { onMove: (id: string, hoverId: string) => void; @@ -291,7 +302,9 @@ type DragItem = { }; const DraggableSidebarItem = memo(function DraggableSidebarItem({ - request, + requestName, + requestId, + workspaceId, active, sidebarWidth, onMove, @@ -302,15 +315,15 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({ accept: ItemTypes.REQUEST, collect: (m) => ({ handlerId: m.getHandlerId(), isOver: m.isOver() }), hover: (item) => { - if (item.id !== request.id) { - onMove(request.id, item.id); + if (item.id !== requestId) { + onMove(requestId, item.id); } }, }); const [{ isDragging }, connectDrag] = useDrag(() => ({ type: ItemTypes.REQUEST, - item: () => ({ id: request.id }), + item: () => ({ id: requestId }), collect: (m) => ({ isDragging: m.isDragging() }), })); @@ -319,7 +332,13 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({ return (
    - +
    ); }); diff --git a/src-web/components/UrlBar.tsx b/src-web/components/UrlBar.tsx index c98c7025..b7ac462a 100644 --- a/src-web/components/UrlBar.tsx +++ b/src-web/components/UrlBar.tsx @@ -1,8 +1,9 @@ -import { useSendRequest } from '../hooks/useSendRequest'; +import { useCallback } from 'react'; import { Button } from './core/Button'; import { DropdownMenuRadio, DropdownMenuTrigger } from './core/Dropdown'; import { IconButton } from './core/IconButton'; import { Input } from './core/Input'; +import type { TabItem } from './core/Tabs/Tabs'; interface Props { sendRequest: () => void; @@ -14,6 +15,12 @@ interface Props { } export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChange, url }: Props) { + const handleMethodChange = useCallback( + (v: TabItem) => { + onMethodChange(v.value); + }, + [onMethodChange], + ); return (
    { @@ -38,7 +45,7 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan placeholder="Enter a URL..." leftSlot={ onMethodChange(v.value)} + onValueChange={handleMethodChange} value={method.toUpperCase()} items={[ { label: 'GET', value: 'GET' }, diff --git a/src-web/components/Workspace.tsx b/src-web/components/Workspace.tsx index a0dc25f4..54d642fb 100644 --- a/src-web/components/Workspace.tsx +++ b/src-web/components/Workspace.tsx @@ -19,7 +19,7 @@ export default function Workspace() { const navigate = useNavigate(); const activeRequest = useActiveRequest(); const activeWorkspace = useActiveWorkspace(); - const deleteRequest = useDeleteRequest(activeRequest); + const deleteRequest = useDeleteRequest(activeRequest?.id ?? null); const workspaces = useWorkspaces(); const { width } = useWindowSize(); const isSideBySide = width > 900; diff --git a/src-web/components/core/Dropdown.tsx b/src-web/components/core/Dropdown.tsx index 7e112b54..325c509e 100644 --- a/src-web/components/core/Dropdown.tsx +++ b/src-web/components/core/Dropdown.tsx @@ -3,7 +3,14 @@ import { CheckIcon } from '@radix-ui/react-icons'; import classnames from 'classnames'; import { motion } from 'framer-motion'; import type { ForwardedRef, ReactElement, ReactNode } from 'react'; -import { forwardRef, memo, useImperativeHandle, useLayoutEffect, useState } from 'react'; +import { + forwardRef, + memo, + useCallback, + useImperativeHandle, + useLayoutEffect, + useState, +} from 'react'; export interface DropdownMenuRadioItem { label: string; @@ -18,19 +25,22 @@ export interface DropdownMenuRadioProps { items: DropdownMenuRadioItem[]; } -export function DropdownMenuRadio({ +export const DropdownMenuRadio = memo(function DropdownMenuRadio({ children, items, onValueChange, label, value, }: DropdownMenuRadioProps) { - const handleChange = (value: string) => { - const item = items.find((item) => item.value === value); - if (item && onValueChange) { - onValueChange(item); - } - }; + const handleChange = useCallback( + (value: string) => { + const item = items.find((item) => item.value === value); + if (item && onValueChange) { + onValueChange(item); + } + }, + [items, onValueChange], + ); return ( @@ -49,7 +59,7 @@ export function DropdownMenuRadio({ ); -} +}); export interface DropdownProps { children: ReactElement; @@ -212,7 +222,11 @@ function DropdownMenuItem({ type DropdownMenuRadioItemProps = Omit; -function DropdownMenuRadioItem({ rightSlot, children, ...props }: DropdownMenuRadioItemProps) { +const DropdownMenuRadioItem = memo(function DropdownMenuRadioItem({ + rightSlot, + children, + ...props +}: DropdownMenuRadioItemProps) { return ( ); -} +}); // const DropdownMenuSubContent = forwardRef( // function DropdownMenuSubContent( @@ -270,13 +284,17 @@ type DropdownMenuTriggerProps = D.DropdownMenuTriggerProps & { className?: string; }; -export function DropdownMenuTrigger({ children, className, ...props }: DropdownMenuTriggerProps) { +export const DropdownMenuTrigger = memo(function DropdownMenuTrigger({ + children, + className, + ...props +}: DropdownMenuTriggerProps) { return ( {children} ); -} +}); interface ItemInnerProps { leftSlot?: ReactNode; diff --git a/src-web/components/core/Icon.tsx b/src-web/components/core/Icon.tsx index 214734d0..b5489b89 100644 --- a/src-web/components/core/Icon.tsx +++ b/src-web/components/core/Icon.tsx @@ -28,6 +28,7 @@ import { UpdateIcon, } from '@radix-ui/react-icons'; import classnames from 'classnames'; +import { memo } from 'react'; const icons = { archive: ArchiveIcon, @@ -67,7 +68,7 @@ export interface IconProps { spin?: boolean; } -export function Icon({ icon, spin, size = 'md', className }: IconProps) { +export const Icon = memo(function Icon({ icon, spin, size = 'md', className }: IconProps) { const Component = icons[icon] ?? icons.question; return ( ); -} +}); diff --git a/src-web/components/core/Stacks.tsx b/src-web/components/core/Stacks.tsx index e14116b5..9974fa29 100644 --- a/src-web/components/core/Stacks.tsx +++ b/src-web/components/core/Stacks.tsx @@ -1,85 +1,46 @@ import classnames from 'classnames'; import type { ComponentType, ReactNode } from 'react'; -import { Children, Fragment } from 'react'; -const spaceClassesX = { - 0: 'pr-0', - 1: 'pr-1', - 2: 'pr-2', - 3: 'pr-3', - 4: 'pr-4', - 5: 'pr-5', - 6: 'pr-6', -}; - -const spaceClassesY = { - 0: 'pt-0', - 1: 'pt-1', - 2: 'pt-2', - 3: 'pt-3', - 4: 'pt-4', - 5: 'pt-5', - 6: 'pt-6', +const gapClasses = { + 0: 'gap-0', + 1: 'gap-1', + 2: 'gap-2', + 3: 'gap-3', + 4: 'gap-4', + 5: 'gap-5', + 6: 'gap-6', }; interface HStackProps extends BaseStackProps { - space?: keyof typeof spaceClassesX; children?: ReactNode; } export function HStack({ className, space, children, ...props }: HStackProps) { return ( - - {space - ? Children.toArray(children) - .filter(Boolean) // Remove null/false/undefined children - .map((c, i) => ( - - {i > 0 ? ( -
    - ) : null} - {c} - - )) - : children} + + {children} ); } export interface VStackProps extends BaseStackProps { - space?: keyof typeof spaceClassesY; children: ReactNode; } export function VStack({ className, space, children, ...props }: VStackProps) { return ( - - {space - ? Children.toArray(children) - .filter(Boolean) // Remove null/false/undefined children - .map((c, i) => ( - - {i > 0 ? ( -
    - ) : null} - {c} - - )) - : children} + + {children} ); } interface BaseStackProps { as?: ComponentType | 'ul'; + space?: keyof typeof gapClasses; alignItems?: 'start' | 'center'; justifyContent?: 'start' | 'center' | 'end'; className?: string; diff --git a/src-web/components/core/Tabs/Tabs.tsx b/src-web/components/core/Tabs/Tabs.tsx index d3212261..6a0c18ba 100644 --- a/src-web/components/core/Tabs/Tabs.tsx +++ b/src-web/components/core/Tabs/Tabs.tsx @@ -9,19 +9,21 @@ import { HStack } from '../Stacks'; import './Tabs.css'; +export type TabItem = { + value: string; + label: string; + options?: { + onValueChange: DropdownMenuRadioProps['onValueChange']; + value: string; + items: DropdownMenuRadioItem[]; + }; +}; + interface Props { label: string; onChangeValue: (value: string) => void; value: string; - tabs: { - value: string; - label: string; - options?: { - onValueChange: DropdownMenuRadioProps['onValueChange']; - value: string; - items: DropdownMenuRadioItem[]; - }; - }[]; + tabs: TabItem[]; tabListClassName?: string; className?: string; children: ReactNode; diff --git a/src-web/hooks/useActiveRequest.ts b/src-web/hooks/useActiveRequest.ts index 2f57dba9..5e05c3eb 100644 --- a/src-web/hooks/useActiveRequest.ts +++ b/src-web/hooks/useActiveRequest.ts @@ -1,11 +1,11 @@ import { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; import type { HttpRequest } from '../lib/models'; +import { useActiveRequestId } from './useActiveRequestId'; import { useRequests } from './useRequests'; export function useActiveRequest(): HttpRequest | null { const requests = useRequests(); - const { requestId } = useParams<{ requestId?: string }>(); + const requestId = useActiveRequestId(); const [activeRequest, setActiveRequest] = useState(null); useEffect(() => { diff --git a/src-web/hooks/useActiveRequestId.ts b/src-web/hooks/useActiveRequestId.ts new file mode 100644 index 00000000..3f417064 --- /dev/null +++ b/src-web/hooks/useActiveRequestId.ts @@ -0,0 +1,6 @@ +import { useParams } from 'react-router-dom'; + +export function useActiveRequestId(): string | null { + const { requestId } = useParams<{ requestId?: string }>(); + return requestId ?? null; +} diff --git a/src-web/hooks/useActiveWorkspace.ts b/src-web/hooks/useActiveWorkspace.ts index b6cb12bf..479ed710 100644 --- a/src-web/hooks/useActiveWorkspace.ts +++ b/src-web/hooks/useActiveWorkspace.ts @@ -1,11 +1,11 @@ import { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; import type { Workspace } from '../lib/models'; +import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useWorkspaces } from './useWorkspaces'; export function useActiveWorkspace(): Workspace | null { const workspaces = useWorkspaces(); - const { workspaceId } = useParams<{ workspaceId?: string }>(); + const workspaceId = useActiveWorkspaceId(); const [activeWorkspace, setActiveWorkspace] = useState(null); useEffect(() => { diff --git a/src-web/hooks/useActiveWorkspaceId.ts b/src-web/hooks/useActiveWorkspaceId.ts new file mode 100644 index 00000000..1df8ef41 --- /dev/null +++ b/src-web/hooks/useActiveWorkspaceId.ts @@ -0,0 +1,6 @@ +import { useParams } from 'react-router-dom'; + +export function useActiveWorkspaceId(): string | null { + const { workspaceId } = useParams<{ workspaceId?: string }>(); + return workspaceId ?? null; +} diff --git a/src-web/hooks/useDeleteRequest.ts b/src-web/hooks/useDeleteRequest.ts index 8d6427bc..941c5f6b 100644 --- a/src-web/hooks/useDeleteRequest.ts +++ b/src-web/hooks/useDeleteRequest.ts @@ -1,18 +1,19 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; -import type { HttpRequest } from '../lib/models'; +import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { requestsQueryKey } from './useRequests'; -export function useDeleteRequest(request: HttpRequest | null) { +export function useDeleteRequest(id: string | null) { + const workspaceId = useActiveWorkspaceId(); const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { - if (!request) return; - await invoke('delete_request', { requestId: request.id }); + if (id === null) return; + await invoke('delete_request', { requestId: id }); }, onSuccess: async () => { - if (!request) return; - await queryClient.invalidateQueries(requestsQueryKey(request.workspaceId)); + if (workspaceId === null || id === null) return; + await queryClient.invalidateQueries(requestsQueryKey(workspaceId)); }, }); } diff --git a/src-web/hooks/useRequest.ts b/src-web/hooks/useRequest.ts new file mode 100644 index 00000000..fe0f717a --- /dev/null +++ b/src-web/hooks/useRequest.ts @@ -0,0 +1,7 @@ +import type { HttpRequest } from '../lib/models'; +import { useRequests } from './useRequests'; + +export function useRequest(id: string | null): HttpRequest | null { + const requests = useRequests(); + return requests.find((r) => r.id === id) ?? null; +} diff --git a/src-web/hooks/useSendRequest.ts b/src-web/hooks/useSendRequest.ts index 7b6c3261..0ef798ad 100644 --- a/src-web/hooks/useSendRequest.ts +++ b/src-web/hooks/useSendRequest.ts @@ -1,18 +1,17 @@ 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) { +export function useSendRequest(id: string | null) { const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { - if (request == null) return; - await invoke('send_request', { requestId: request.id }); + if (id === null) return; + await invoke('send_request', { requestId: id }); }, onSuccess: async () => { - if (request == null) return; - await queryClient.invalidateQueries(responsesQueryKey(request.id)); + if (id === null) return; + await queryClient.invalidateQueries(responsesQueryKey(id)); }, }).mutate; } diff --git a/src-web/hooks/useTheme.ts b/src-web/hooks/useTheme.ts index de47c89e..632017c7 100644 --- a/src-web/hooks/useTheme.ts +++ b/src-web/hooks/useTheme.ts @@ -1,4 +1,3 @@ -import { app } from '@tauri-apps/api'; import { useEffect } from 'react'; import type { Appearance } from '../lib/theme/window'; import { diff --git a/src-web/hooks/useUpdateRequest.ts b/src-web/hooks/useUpdateRequest.ts index 9295b41d..9a83ec88 100644 --- a/src-web/hooks/useUpdateRequest.ts +++ b/src-web/hooks/useUpdateRequest.ts @@ -1,8 +1,10 @@ import { useMutation } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; import type { HttpRequest } from '../lib/models'; +import { useRequest } from './useRequest'; -export function useUpdateRequest(request: HttpRequest | null) { +export function useUpdateRequest(id: string | null) { + const request = useRequest(id); return useMutation>({ mutationFn: async (patch) => { if (request == null) {