mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 17:48:30 +02:00
Optimized a few components
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
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 { 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(() => {
|
||||||
|
|||||||
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 { 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));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
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 { 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user