Optimized a few components

This commit is contained in:
Gregory Schier
2023-03-18 18:49:01 -07:00
parent 6fc9b5a185
commit b1835561a8
17 changed files with 200 additions and 156 deletions

View File

@@ -1,13 +1,15 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { act } from 'react-dom/test-utils'; import { useCallback, useMemo } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading'; import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
import { useKeyValue } from '../hooks/useKeyValue'; import { useKeyValue } from '../hooks/useKeyValue';
import { useSendRequest } from '../hooks/useSendRequest'; import { useSendRequest } from '../hooks/useSendRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { tryFormatJson } from '../lib/formatters'; import { tryFormatJson } from '../lib/formatters';
import type { HttpHeader } from '../lib/models';
import { Editor } from './core/Editor'; import { Editor } from './core/Editor';
import { PairEditor } from './core/PairEditor'; import { PairEditor } from './core/PairEditor';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs';
import { GraphQLEditor } from './editors/GraphQLEditor'; import { GraphQLEditor } from './editors/GraphQLEditor';
import { UrlBar } from './UrlBar'; import { UrlBar } from './UrlBar';
@@ -19,14 +21,45 @@ interface Props {
export function RequestPane({ fullHeight, className }: Props) { export function RequestPane({ fullHeight, className }: Props) {
const activeRequest = useActiveRequest(); const activeRequest = useActiveRequest();
const updateRequest = useUpdateRequest(activeRequest); const activeRequestId = activeRequest?.id ?? null;
const sendRequest = useSendRequest(activeRequest); const updateRequest = useUpdateRequest(activeRequestId);
const sendRequest = useSendRequest(activeRequestId);
const responseLoading = useIsResponseLoading(); const responseLoading = useIsResponseLoading();
const activeTab = useKeyValue<string>({ const activeTab = useKeyValue<string>({
key: ['active_request_body_tab', activeRequest?.id ?? 'n/a'], key: ['active_request_body_tab', activeRequestId ?? 'n/a'],
initialValue: 'body', 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; if (activeRequest === null) return null;
return ( return (
@@ -35,32 +68,15 @@ export function RequestPane({ fullHeight, className }: Props) {
key={activeRequest.id} key={activeRequest.id}
method={activeRequest.method} method={activeRequest.method}
url={activeRequest.url} url={activeRequest.url}
onMethodChange={(method) => updateRequest.mutate({ method })} onMethodChange={handleMethodChange}
onUrlChange={(url) => updateRequest.mutate({ url })} onUrlChange={handleUrlChange}
sendRequest={sendRequest} sendRequest={sendRequest}
loading={responseLoading} loading={responseLoading}
/> />
<Tabs <Tabs
value={activeTab.value} value={activeTab.value}
onChangeValue={activeTab.set} onChangeValue={activeTab.set}
tabs={[ tabs={tabs}
{
value: 'body',
label: activeRequest.bodyType ?? 'NoBody',
options: {
onValueChange: (bodyType) => 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' },
]}
className="mt-2" className="mt-2"
label="Request body" label="Request body"
> >
@@ -68,7 +84,7 @@ export function RequestPane({ fullHeight, className }: Props) {
<PairEditor <PairEditor
key={activeRequest.id} key={activeRequest.id}
pairs={activeRequest.headers} pairs={activeRequest.headers}
onChange={(headers) => updateRequest.mutate({ headers })} onChange={handleHeadersChange}
/> />
</TabContent> </TabContent>
<TabContent value="body"> <TabContent value="body">
@@ -80,7 +96,7 @@ export function RequestPane({ fullHeight, className }: Props) {
heightMode={fullHeight ? 'full' : 'auto'} heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={activeRequest.body ?? ''} defaultValue={activeRequest.body ?? ''}
contentType="application/json" contentType="application/json"
onChange={(body) => updateRequest.mutate({ body })} onChange={handleBodyChange}
format={activeRequest.bodyType === 'json' ? (v) => tryFormatJson(v) : undefined} format={activeRequest.bodyType === 'json' ? (v) => tryFormatJson(v) : undefined}
/> />
) : activeRequest.bodyType === 'graphql' ? ( ) : activeRequest.bodyType === 'graphql' ? (
@@ -88,7 +104,7 @@ export function RequestPane({ fullHeight, className }: Props) {
key={activeRequest.id} key={activeRequest.id}
className="!bg-gray-50" className="!bg-gray-50"
defaultValue={activeRequest?.body ?? ''} defaultValue={activeRequest?.body ?? ''}
onChange={(body) => updateRequest.mutate({ body })} onChange={handleBodyChange}
/> />
) : ( ) : (
<div className="h-full text-gray-400 flex items-center justify-center">No Body</div> <div className="h-full text-gray-400 flex items-center justify-center">No Body</div>

View File

@@ -79,7 +79,6 @@ export function Container({ className }: Props) {
}, []); }, []);
const sidebarStyles = useMemo(() => ({ width: width.value }), [width.value]); const sidebarStyles = useMemo(() => ({ width: width.value }), [width.value]);
console.log('RENDER SIDEBAR');
return ( return (
<div <div
ref={sidebarRef} ref={sidebarRef}
@@ -145,14 +144,16 @@ function SidebarItems({
useEffect(() => { useEffect(() => {
setItems(requests.map((r) => ({ request: r, left: 0, top: 0 }))); setItems(requests.map((r) => ({ request: r, left: 0, top: 0 })));
}, [requests.length]); }, [requests]);
const handleMove = useCallback((id: string, hoverId: string) => { const handleMove = useCallback((id: string, hoverId: string) => {
setItems((oldItems) => { setItems((oldItems) => {
const dragIndex = oldItems.findIndex((i) => i.request.id === id); const dragIndex = oldItems.findIndex((i) => i.request.id === id);
const index = oldItems.findIndex((i) => i.request.id === hoverId); const index = oldItems.findIndex((i) => i.request.id === hoverId);
const newItems = [...oldItems]; const newItems = [...oldItems];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const b = newItems[index]!; const b = newItems[index]!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
newItems[index] = newItems[dragIndex]!; newItems[index] = newItems[dragIndex]!;
newItems[dragIndex] = b; newItems[dragIndex] = b;
return newItems; return newItems;
@@ -164,7 +165,9 @@ function SidebarItems({
{items.map(({ request }) => ( {items.map(({ request }) => (
<DraggableSidebarItem <DraggableSidebarItem
key={request.id} key={request.id}
request={request} requestId={request.id}
requestName={request.name}
workspaceId={request.workspaceId}
active={request.id === activeRequestId} active={request.id === activeRequestId}
sidebarWidth={sidebarWidth} sidebarWidth={sidebarWidth}
onMove={handleMove} onMove={handleMove}
@@ -175,14 +178,22 @@ function SidebarItems({
} }
type SidebarItemProps = { type SidebarItemProps = {
request: HttpRequest; requestId: string;
requestName: string;
workspaceId: string;
sidebarWidth: number; sidebarWidth: number;
active?: boolean; active?: boolean;
}; };
function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) { const SidebarItem = memo(function SidebarItem({
const deleteRequest = useDeleteRequest(request); requestName,
const updateRequest = useUpdateRequest(request); requestId,
workspaceId,
active,
sidebarWidth,
}: SidebarItemProps) {
const deleteRequest = useDeleteRequest(requestId);
const updateRequest = useUpdateRequest(requestId);
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const handleSubmitNameEdit = useCallback(async (el: HTMLInputElement) => { const handleSubmitNameEdit = useCallback(async (el: HTMLInputElement) => {
@@ -197,6 +208,8 @@ function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) {
const itemStyles = useMemo(() => ({ width: sidebarWidth }), [sidebarWidth]); const itemStyles = useMemo(() => ({ width: sidebarWidth }), [sidebarWidth]);
if (workspaceId === null) return null;
return ( return (
<li className={classnames('block group/item px-2')} style={itemStyles}> <li className={classnames('block group/item px-2')} style={itemStyles}>
<div className="relative w-full"> <div className="relative w-full">
@@ -220,7 +233,7 @@ function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) {
setEditing(true); setEditing(true);
} }
}} }}
// to={`/workspaces/${request.workspaceId}/requests/${request.id}`} to={`/workspaces/${workspaceId}/requests/${requestId}`}
onDoubleClick={() => setEditing(true)} onDoubleClick={() => setEditing(true)}
onClick={active ? () => setEditing(true) : undefined} onClick={active ? () => setEditing(true) : undefined}
justify="start" justify="start"
@@ -228,7 +241,7 @@ function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) {
{editing ? ( {editing ? (
<input <input
ref={handleFocus} ref={handleFocus}
defaultValue={request.name} defaultValue={requestName}
className="bg-transparent outline-none w-full" className="bg-transparent outline-none w-full"
onBlur={(e) => handleSubmitNameEdit(e.currentTarget)} onBlur={(e) => handleSubmitNameEdit(e.currentTarget)}
onKeyDown={async (e) => { onKeyDown={async (e) => {
@@ -243,24 +256,22 @@ function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) {
}} }}
/> />
) : ( ) : (
<span <span className={classnames('truncate', !requestName && 'text-gray-400 italic')}>
className={classnames( {requestName || 'New Request'}
'truncate',
!(request.name || request.url) && 'text-gray-400 italic',
)}
>
{request.name || request.url || 'New Request'}
</span> </span>
)} )}
</Button> </Button>
<Dropdown <Dropdown
items={[ items={useMemo(
{ () => [
label: 'Delete Request', {
onSelect: deleteRequest.mutate, label: 'Delete Request',
leftSlot: <Icon icon="trash" />, onSelect: deleteRequest.mutate,
}, leftSlot: <Icon icon="trash" />,
]} },
],
[],
)}
> >
<DropdownMenuTrigger <DropdownMenuTrigger
className={classnames( className={classnames(
@@ -280,7 +291,7 @@ function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) {
</div> </div>
</li> </li>
); );
} });
type DraggableSidebarItemProps = SidebarItemProps & { type DraggableSidebarItemProps = SidebarItemProps & {
onMove: (id: string, hoverId: string) => void; onMove: (id: string, hoverId: string) => void;
@@ -291,7 +302,9 @@ type DragItem = {
}; };
const DraggableSidebarItem = memo(function DraggableSidebarItem({ const DraggableSidebarItem = memo(function DraggableSidebarItem({
request, requestName,
requestId,
workspaceId,
active, active,
sidebarWidth, sidebarWidth,
onMove, onMove,
@@ -302,15 +315,15 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({
accept: ItemTypes.REQUEST, accept: ItemTypes.REQUEST,
collect: (m) => ({ handlerId: m.getHandlerId(), isOver: m.isOver() }), collect: (m) => ({ handlerId: m.getHandlerId(), isOver: m.isOver() }),
hover: (item) => { hover: (item) => {
if (item.id !== request.id) { if (item.id !== requestId) {
onMove(request.id, item.id); onMove(requestId, item.id);
} }
}, },
}); });
const [{ isDragging }, connectDrag] = useDrag<DragItem, unknown, { isDragging: boolean }>(() => ({ const [{ isDragging }, connectDrag] = useDrag<DragItem, unknown, { isDragging: boolean }>(() => ({
type: ItemTypes.REQUEST, type: ItemTypes.REQUEST,
item: () => ({ id: request.id }), item: () => ({ id: requestId }),
collect: (m) => ({ isDragging: m.isDragging() }), collect: (m) => ({ isDragging: m.isDragging() }),
})); }));
@@ -319,7 +332,13 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({
return ( return (
<div ref={ref} className={classnames(isDragging && 'opacity-0')}> <div ref={ref} className={classnames(isDragging && 'opacity-0')}>
<SidebarItem request={request} active={active} sidebarWidth={sidebarWidth} /> <SidebarItem
requestName={requestName}
requestId={requestId}
workspaceId={workspaceId}
active={active}
sidebarWidth={sidebarWidth}
/>
</div> </div>
); );
}); });

View File

@@ -1,8 +1,9 @@
import { useSendRequest } from '../hooks/useSendRequest'; import { useCallback } from 'react';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { DropdownMenuRadio, DropdownMenuTrigger } from './core/Dropdown'; import { DropdownMenuRadio, DropdownMenuTrigger } from './core/Dropdown';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { Input } from './core/Input'; import { Input } from './core/Input';
import type { TabItem } from './core/Tabs/Tabs';
interface Props { interface Props {
sendRequest: () => void; sendRequest: () => void;
@@ -14,6 +15,12 @@ interface Props {
} }
export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChange, url }: Props) { export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChange, url }: Props) {
const handleMethodChange = useCallback(
(v: TabItem) => {
onMethodChange(v.value);
},
[onMethodChange],
);
return ( return (
<form <form
onSubmit={async (e) => { onSubmit={async (e) => {
@@ -38,7 +45,7 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
placeholder="Enter a URL..." placeholder="Enter a URL..."
leftSlot={ leftSlot={
<DropdownMenuRadio <DropdownMenuRadio
onValueChange={(v) => onMethodChange(v.value)} onValueChange={handleMethodChange}
value={method.toUpperCase()} value={method.toUpperCase()}
items={[ items={[
{ label: 'GET', value: 'GET' }, { label: 'GET', value: 'GET' },

View File

@@ -19,7 +19,7 @@ export default function Workspace() {
const navigate = useNavigate(); const navigate = useNavigate();
const activeRequest = useActiveRequest(); const activeRequest = useActiveRequest();
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const deleteRequest = useDeleteRequest(activeRequest); const deleteRequest = useDeleteRequest(activeRequest?.id ?? null);
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const { width } = useWindowSize(); const { width } = useWindowSize();
const isSideBySide = width > 900; const isSideBySide = width > 900;

View File

@@ -3,7 +3,14 @@ import { CheckIcon } from '@radix-ui/react-icons';
import classnames from 'classnames'; import classnames from 'classnames';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ForwardedRef, ReactElement, ReactNode } from 'react'; 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 { export interface DropdownMenuRadioItem {
label: string; label: string;
@@ -18,19 +25,22 @@ export interface DropdownMenuRadioProps {
items: DropdownMenuRadioItem[]; items: DropdownMenuRadioItem[];
} }
export function DropdownMenuRadio({ export const DropdownMenuRadio = memo(function DropdownMenuRadio({
children, children,
items, items,
onValueChange, onValueChange,
label, label,
value, value,
}: DropdownMenuRadioProps) { }: DropdownMenuRadioProps) {
const handleChange = (value: string) => { const handleChange = useCallback(
const item = items.find((item) => item.value === value); (value: string) => {
if (item && onValueChange) { const item = items.find((item) => item.value === value);
onValueChange(item); if (item && onValueChange) {
} onValueChange(item);
}; }
},
[items, onValueChange],
);
return ( return (
<D.Root> <D.Root>
@@ -49,7 +59,7 @@ export function DropdownMenuRadio({
</DropdownMenuPortal> </DropdownMenuPortal>
</D.Root> </D.Root>
); );
} });
export interface DropdownProps { export interface DropdownProps {
children: ReactElement<typeof DropdownMenuTrigger>; children: ReactElement<typeof DropdownMenuTrigger>;
@@ -212,7 +222,11 @@ function DropdownMenuItem({
type DropdownMenuRadioItemProps = Omit<D.DropdownMenuRadioItemProps & ItemInnerProps, 'leftSlot'>; type DropdownMenuRadioItemProps = Omit<D.DropdownMenuRadioItemProps & ItemInnerProps, 'leftSlot'>;
function DropdownMenuRadioItem({ rightSlot, children, ...props }: DropdownMenuRadioItemProps) { const DropdownMenuRadioItem = memo(function DropdownMenuRadioItem({
rightSlot,
children,
...props
}: DropdownMenuRadioItemProps) {
return ( return (
<D.RadioItem asChild {...props}> <D.RadioItem asChild {...props}>
<ItemInner <ItemInner
@@ -227,7 +241,7 @@ function DropdownMenuRadioItem({ rightSlot, children, ...props }: DropdownMenuRa
</ItemInner> </ItemInner>
</D.RadioItem> </D.RadioItem>
); );
} });
// const DropdownMenuSubContent = forwardRef<HTMLDivElement, DropdownMenu.DropdownMenuSubContentProps>( // const DropdownMenuSubContent = forwardRef<HTMLDivElement, DropdownMenu.DropdownMenuSubContentProps>(
// function DropdownMenuSubContent( // function DropdownMenuSubContent(
@@ -270,13 +284,17 @@ type DropdownMenuTriggerProps = D.DropdownMenuTriggerProps & {
className?: string; className?: string;
}; };
export function DropdownMenuTrigger({ children, className, ...props }: DropdownMenuTriggerProps) { export const DropdownMenuTrigger = memo(function DropdownMenuTrigger({
children,
className,
...props
}: DropdownMenuTriggerProps) {
return ( return (
<D.Trigger asChild className={classnames(className)} {...props}> <D.Trigger asChild className={classnames(className)} {...props}>
{children} {children}
</D.Trigger> </D.Trigger>
); );
} });
interface ItemInnerProps { interface ItemInnerProps {
leftSlot?: ReactNode; leftSlot?: ReactNode;

View File

@@ -28,6 +28,7 @@ import {
UpdateIcon, UpdateIcon,
} from '@radix-ui/react-icons'; } from '@radix-ui/react-icons';
import classnames from 'classnames'; import classnames from 'classnames';
import { memo } from 'react';
const icons = { const icons = {
archive: ArchiveIcon, archive: ArchiveIcon,
@@ -67,7 +68,7 @@ export interface IconProps {
spin?: boolean; 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; const Component = icons[icon] ?? icons.question;
return ( return (
<Component <Component
@@ -81,4 +82,4 @@ export function Icon({ icon, spin, size = 'md', className }: IconProps) {
)} )}
/> />
); );
} });

View File

@@ -1,85 +1,46 @@
import classnames from 'classnames'; import classnames from 'classnames';
import type { ComponentType, ReactNode } from 'react'; import type { ComponentType, ReactNode } from 'react';
import { Children, Fragment } from 'react';
const spaceClassesX = { const gapClasses = {
0: 'pr-0', 0: 'gap-0',
1: 'pr-1', 1: 'gap-1',
2: 'pr-2', 2: 'gap-2',
3: 'pr-3', 3: 'gap-3',
4: 'pr-4', 4: 'gap-4',
5: 'pr-5', 5: 'gap-5',
6: 'pr-6', 6: 'gap-6',
};
const spaceClassesY = {
0: 'pt-0',
1: 'pt-1',
2: 'pt-2',
3: 'pt-3',
4: 'pt-4',
5: 'pt-5',
6: 'pt-6',
}; };
interface HStackProps extends BaseStackProps { interface HStackProps extends BaseStackProps {
space?: keyof typeof spaceClassesX;
children?: ReactNode; children?: ReactNode;
} }
export function HStack({ className, space, children, ...props }: HStackProps) { export function HStack({ className, space, children, ...props }: HStackProps) {
return ( return (
<BaseStack className={classnames(className, 'flex-row')} {...props}> <BaseStack className={classnames(className, 'flex-row', space && gapClasses[space])} {...props}>
{space {children}
? Children.toArray(children)
.filter(Boolean) // Remove null/false/undefined children
.map((c, i) => (
<Fragment key={i}>
{i > 0 ? (
<div
className={classnames(spaceClassesX[space], 'pointer-events-none')}
data-spacer=""
aria-hidden
/>
) : null}
{c}
</Fragment>
))
: children}
</BaseStack> </BaseStack>
); );
} }
export interface VStackProps extends BaseStackProps { export interface VStackProps extends BaseStackProps {
space?: keyof typeof spaceClassesY;
children: ReactNode; children: ReactNode;
} }
export function VStack({ className, space, children, ...props }: VStackProps) { export function VStack({ className, space, children, ...props }: VStackProps) {
return ( return (
<BaseStack className={classnames(className, 'w-full h-full flex-col')} {...props}> <BaseStack
{space className={classnames(className, 'w-full h-full flex-col', space && gapClasses[space])}
? Children.toArray(children) {...props}
.filter(Boolean) // Remove null/false/undefined children >
.map((c, i) => ( {children}
<Fragment key={i}>
{i > 0 ? (
<div
className={classnames(spaceClassesY[space], 'pointer-events-none')}
data-spacer=""
aria-hidden
/>
) : null}
{c}
</Fragment>
))
: children}
</BaseStack> </BaseStack>
); );
} }
interface BaseStackProps { interface BaseStackProps {
as?: ComponentType | 'ul'; as?: ComponentType | 'ul';
space?: keyof typeof gapClasses;
alignItems?: 'start' | 'center'; alignItems?: 'start' | 'center';
justifyContent?: 'start' | 'center' | 'end'; justifyContent?: 'start' | 'center' | 'end';
className?: string; className?: string;

View File

@@ -9,19 +9,21 @@ import { HStack } from '../Stacks';
import './Tabs.css'; import './Tabs.css';
export type TabItem = {
value: string;
label: string;
options?: {
onValueChange: DropdownMenuRadioProps['onValueChange'];
value: string;
items: DropdownMenuRadioItem[];
};
};
interface Props { interface Props {
label: string; label: string;
onChangeValue: (value: string) => void; onChangeValue: (value: string) => void;
value: string; value: string;
tabs: { tabs: TabItem[];
value: string;
label: string;
options?: {
onValueChange: DropdownMenuRadioProps['onValueChange'];
value: string;
items: DropdownMenuRadioItem[];
};
}[];
tabListClassName?: string; tabListClassName?: string;
className?: string; className?: string;
children: ReactNode; children: ReactNode;

View File

@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { useActiveRequestId } from './useActiveRequestId';
import { useRequests } from './useRequests'; import { useRequests } from './useRequests';
export function useActiveRequest(): HttpRequest | null { export function useActiveRequest(): HttpRequest | null {
const requests = useRequests(); const requests = useRequests();
const { requestId } = useParams<{ requestId?: string }>(); const requestId = useActiveRequestId();
const [activeRequest, setActiveRequest] = useState<HttpRequest | null>(null); const [activeRequest, setActiveRequest] = useState<HttpRequest | null>(null);
useEffect(() => { useEffect(() => {

View File

@@ -0,0 +1,6 @@
import { useParams } from 'react-router-dom';
export function useActiveRequestId(): string | null {
const { requestId } = useParams<{ requestId?: string }>();
return requestId ?? null;
}

View File

@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import type { Workspace } from '../lib/models'; import type { Workspace } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useWorkspaces } from './useWorkspaces'; import { useWorkspaces } from './useWorkspaces';
export function useActiveWorkspace(): Workspace | null { export function useActiveWorkspace(): Workspace | null {
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const { workspaceId } = useParams<{ workspaceId?: string }>(); const workspaceId = useActiveWorkspaceId();
const [activeWorkspace, setActiveWorkspace] = useState<Workspace | null>(null); const [activeWorkspace, setActiveWorkspace] = useState<Workspace | null>(null);
useEffect(() => { useEffect(() => {

View File

@@ -0,0 +1,6 @@
import { useParams } from 'react-router-dom';
export function useActiveWorkspaceId(): string | null {
const { workspaceId } = useParams<{ workspaceId?: string }>();
return workspaceId ?? null;
}

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { app } from '@tauri-apps/api';
import { useEffect } from 'react'; import { useEffect } from 'react';
import type { Appearance } from '../lib/theme/window'; import type { Appearance } from '../lib/theme/window';
import { import {

View File

@@ -1,8 +1,10 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import type { HttpRequest } from '../lib/models'; 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<void, unknown, Partial<HttpRequest>>({ return useMutation<void, unknown, Partial<HttpRequest>>({
mutationFn: async (patch) => { mutationFn: async (patch) => {
if (request == null) { if (request == null) {