mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-23 18:01:08 +01:00
Optimized a few components
This commit is contained in:
@@ -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<string>({
|
||||
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}
|
||||
/>
|
||||
<Tabs
|
||||
value={activeTab.value}
|
||||
onChangeValue={activeTab.set}
|
||||
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' },
|
||||
]}
|
||||
tabs={tabs}
|
||||
className="mt-2"
|
||||
label="Request body"
|
||||
>
|
||||
@@ -68,7 +84,7 @@ export function RequestPane({ fullHeight, className }: Props) {
|
||||
<PairEditor
|
||||
key={activeRequest.id}
|
||||
pairs={activeRequest.headers}
|
||||
onChange={(headers) => updateRequest.mutate({ headers })}
|
||||
onChange={handleHeadersChange}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value="body">
|
||||
@@ -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}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full text-gray-400 flex items-center justify-center">No Body</div>
|
||||
|
||||
@@ -79,7 +79,6 @@ export function Container({ className }: Props) {
|
||||
}, []);
|
||||
const sidebarStyles = useMemo(() => ({ width: width.value }), [width.value]);
|
||||
|
||||
console.log('RENDER SIDEBAR');
|
||||
return (
|
||||
<div
|
||||
ref={sidebarRef}
|
||||
@@ -145,14 +144,16 @@ function SidebarItems({
|
||||
|
||||
useEffect(() => {
|
||||
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 }) => (
|
||||
<DraggableSidebarItem
|
||||
key={request.id}
|
||||
request={request}
|
||||
requestId={request.id}
|
||||
requestName={request.name}
|
||||
workspaceId={request.workspaceId}
|
||||
active={request.id === activeRequestId}
|
||||
sidebarWidth={sidebarWidth}
|
||||
onMove={handleMove}
|
||||
@@ -175,14 +178,22 @@ function SidebarItems({
|
||||
}
|
||||
|
||||
type SidebarItemProps = {
|
||||
request: HttpRequest;
|
||||
requestId: string;
|
||||
requestName: string;
|
||||
workspaceId: string;
|
||||
sidebarWidth: number;
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) {
|
||||
const deleteRequest = useDeleteRequest(request);
|
||||
const updateRequest = useUpdateRequest(request);
|
||||
const SidebarItem = memo(function SidebarItem({
|
||||
requestName,
|
||||
requestId,
|
||||
workspaceId,
|
||||
active,
|
||||
sidebarWidth,
|
||||
}: SidebarItemProps) {
|
||||
const deleteRequest = useDeleteRequest(requestId);
|
||||
const updateRequest = useUpdateRequest(requestId);
|
||||
const [editing, setEditing] = useState<boolean>(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 (
|
||||
<li className={classnames('block group/item px-2')} style={itemStyles}>
|
||||
<div className="relative w-full">
|
||||
@@ -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 ? (
|
||||
<input
|
||||
ref={handleFocus}
|
||||
defaultValue={request.name}
|
||||
defaultValue={requestName}
|
||||
className="bg-transparent outline-none w-full"
|
||||
onBlur={(e) => handleSubmitNameEdit(e.currentTarget)}
|
||||
onKeyDown={async (e) => {
|
||||
@@ -243,24 +256,22 @@ function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={classnames(
|
||||
'truncate',
|
||||
!(request.name || request.url) && 'text-gray-400 italic',
|
||||
)}
|
||||
>
|
||||
{request.name || request.url || 'New Request'}
|
||||
<span className={classnames('truncate', !requestName && 'text-gray-400 italic')}>
|
||||
{requestName || 'New Request'}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
label: 'Delete Request',
|
||||
onSelect: deleteRequest.mutate,
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
},
|
||||
]}
|
||||
items={useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Delete Request',
|
||||
onSelect: deleteRequest.mutate,
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
},
|
||||
],
|
||||
[],
|
||||
)}
|
||||
>
|
||||
<DropdownMenuTrigger
|
||||
className={classnames(
|
||||
@@ -280,7 +291,7 @@ function SidebarItem({ request, active, sidebarWidth }: SidebarItemProps) {
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
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<DragItem, unknown, { isDragging: boolean }>(() => ({
|
||||
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 (
|
||||
<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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
@@ -38,7 +45,7 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
|
||||
placeholder="Enter a URL..."
|
||||
leftSlot={
|
||||
<DropdownMenuRadio
|
||||
onValueChange={(v) => onMethodChange(v.value)}
|
||||
onValueChange={handleMethodChange}
|
||||
value={method.toUpperCase()}
|
||||
items={[
|
||||
{ label: 'GET', value: 'GET' },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<D.Root>
|
||||
@@ -49,7 +59,7 @@ export function DropdownMenuRadio({
|
||||
</DropdownMenuPortal>
|
||||
</D.Root>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export interface DropdownProps {
|
||||
children: ReactElement<typeof DropdownMenuTrigger>;
|
||||
@@ -212,7 +222,11 @@ function DropdownMenuItem({
|
||||
|
||||
type DropdownMenuRadioItemProps = Omit<D.DropdownMenuRadioItemProps & ItemInnerProps, 'leftSlot'>;
|
||||
|
||||
function DropdownMenuRadioItem({ rightSlot, children, ...props }: DropdownMenuRadioItemProps) {
|
||||
const DropdownMenuRadioItem = memo(function DropdownMenuRadioItem({
|
||||
rightSlot,
|
||||
children,
|
||||
...props
|
||||
}: DropdownMenuRadioItemProps) {
|
||||
return (
|
||||
<D.RadioItem asChild {...props}>
|
||||
<ItemInner
|
||||
@@ -227,7 +241,7 @@ function DropdownMenuRadioItem({ rightSlot, children, ...props }: DropdownMenuRa
|
||||
</ItemInner>
|
||||
</D.RadioItem>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// const DropdownMenuSubContent = forwardRef<HTMLDivElement, DropdownMenu.DropdownMenuSubContentProps>(
|
||||
// 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 (
|
||||
<D.Trigger asChild className={classnames(className)} {...props}>
|
||||
{children}
|
||||
</D.Trigger>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
interface ItemInnerProps {
|
||||
leftSlot?: ReactNode;
|
||||
|
||||
@@ -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 (
|
||||
<Component
|
||||
@@ -81,4 +82,4 @@ export function Icon({ icon, spin, size = 'md', className }: IconProps) {
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<BaseStack className={classnames(className, 'flex-row')} {...props}>
|
||||
{space
|
||||
? 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 className={classnames(className, 'flex-row', space && gapClasses[space])} {...props}>
|
||||
{children}
|
||||
</BaseStack>
|
||||
);
|
||||
}
|
||||
|
||||
export interface VStackProps extends BaseStackProps {
|
||||
space?: keyof typeof spaceClassesY;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function VStack({ className, space, children, ...props }: VStackProps) {
|
||||
return (
|
||||
<BaseStack className={classnames(className, 'w-full h-full flex-col')} {...props}>
|
||||
{space
|
||||
? Children.toArray(children)
|
||||
.filter(Boolean) // Remove null/false/undefined children
|
||||
.map((c, i) => (
|
||||
<Fragment key={i}>
|
||||
{i > 0 ? (
|
||||
<div
|
||||
className={classnames(spaceClassesY[space], 'pointer-events-none')}
|
||||
data-spacer=""
|
||||
aria-hidden
|
||||
/>
|
||||
) : null}
|
||||
{c}
|
||||
</Fragment>
|
||||
))
|
||||
: children}
|
||||
<BaseStack
|
||||
className={classnames(className, 'w-full h-full flex-col', space && gapClasses[space])}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</BaseStack>
|
||||
);
|
||||
}
|
||||
|
||||
interface BaseStackProps {
|
||||
as?: ComponentType | 'ul';
|
||||
space?: keyof typeof gapClasses;
|
||||
alignItems?: 'start' | 'center';
|
||||
justifyContent?: 'start' | 'center' | 'end';
|
||||
className?: string;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<HttpRequest | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
6
src-web/hooks/useActiveRequestId.ts
Normal file
6
src-web/hooks/useActiveRequestId.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
export function useActiveRequestId(): string | null {
|
||||
const { requestId } = useParams<{ requestId?: string }>();
|
||||
return requestId ?? null;
|
||||
}
|
||||
@@ -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<Workspace | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
6
src-web/hooks/useActiveWorkspaceId.ts
Normal file
6
src-web/hooks/useActiveWorkspaceId.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
export function useActiveWorkspaceId(): string | null {
|
||||
const { workspaceId } = useParams<{ workspaceId?: string }>();
|
||||
return workspaceId ?? null;
|
||||
}
|
||||
@@ -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<void, string>({
|
||||
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));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
7
src-web/hooks/useRequest.ts
Normal file
7
src-web/hooks/useRequest.ts
Normal 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;
|
||||
}
|
||||
@@ -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<void, string>({
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { app } from '@tauri-apps/api';
|
||||
import { useEffect } from 'react';
|
||||
import type { Appearance } from '../lib/theme/window';
|
||||
import {
|
||||
|
||||
@@ -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<void, unknown, Partial<HttpRequest>>({
|
||||
mutationFn: async (patch) => {
|
||||
if (request == null) {
|
||||
|
||||
Reference in New Issue
Block a user