diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 94b12487..7e6b3de3 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -5,7 +5,7 @@ import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { Editor } from './core/Editor'; import { TabContent, Tabs } from './core/Tabs/Tabs'; import { GraphQLEditor } from './editors/GraphQLEditor'; -import { HeaderEditor } from './HeaderEditor'; +import { PairEditor } from './core/PairEditor'; import { UrlBar } from './UrlBar'; interface Props { @@ -55,7 +55,11 @@ export function RequestPane({ fullHeight, className }: Props) { label="Request body" > - + updateRequest.mutate({ headers })} + /> {activeRequest.bodyType === 'json' ? ( diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 145f4272..c653e1d5 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -71,6 +71,7 @@ export const ResponsePane = memo(function ResponsePane({ className }: Props) { - + diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 62da5092..8f0a4d80 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -31,6 +31,7 @@ export function Sidebar({ className }: Props) { > { @@ -49,8 +50,12 @@ export function Sidebar({ className }: Props) { alignItems="center" justifyContent="end" > - deleteRequest.mutate()} /> - + deleteRequest.mutate()} /> + diff --git a/src-web/components/UrlBar.tsx b/src-web/components/UrlBar.tsx index cbff485e..e9d5d084 100644 --- a/src-web/components/UrlBar.tsx +++ b/src-web/components/UrlBar.tsx @@ -54,13 +54,13 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan } rightSlot={ } /> diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx index e4d66a2f..f852ddc3 100644 --- a/src-web/components/core/Button.tsx +++ b/src-web/components/core/Button.tsx @@ -22,6 +22,7 @@ export type ButtonProps = HTMLAttributes & { type?: 'button' | 'submit'; forDropdown?: boolean; disabled?: boolean; + title?: string; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src-web/components/core/Dialog.tsx b/src-web/components/core/Dialog.tsx index f40ac197..81394f9a 100644 --- a/src-web/components/core/Dialog.tsx +++ b/src-web/components/core/Dialog.tsx @@ -40,7 +40,7 @@ export function Dialog({ )} > - + diff --git a/src-web/components/core/IconButton.tsx b/src-web/components/core/IconButton.tsx index 354213de..e2654cbd 100644 --- a/src-web/components/core/IconButton.tsx +++ b/src-web/components/core/IconButton.tsx @@ -5,7 +5,8 @@ import { Button } from './Button'; import type { IconProps } from './Icon'; import { Icon } from './Icon'; -type Props = IconProps & ButtonProps & { iconClassName?: string; iconSize?: IconProps['size'] }; +type Props = IconProps & + ButtonProps & { iconClassName?: string; iconSize?: IconProps['size']; title: string }; export const IconButton = forwardRef(function IconButton( { icon, spin, className, iconClassName, size = 'md', iconSize, ...props }: Props, diff --git a/src-web/components/HeaderEditor.tsx b/src-web/components/core/PairEditor.tsx similarity index 51% rename from src-web/components/HeaderEditor.tsx rename to src-web/components/core/PairEditor.tsx index ac4291a3..03b5f3ea 100644 --- a/src-web/components/HeaderEditor.tsx +++ b/src-web/components/core/PairEditor.tsx @@ -1,57 +1,57 @@ import classnames from 'classnames'; import { useEffect, useState } from 'react'; -import { useUpdateRequest } from '../hooks/useUpdateRequest'; -import type { HttpHeader, HttpRequest } from '../lib/models'; -import { IconButton } from './core/IconButton'; -import { Input } from './core/Input'; -import { VStack } from './core/Stacks'; +import { IconButton } from './IconButton'; +import { Input } from './Input'; +import { VStack } from './Stacks'; interface Props { - request: HttpRequest; + pairs: Pair[]; + onChange: (pairs: Pair[]) => void; className?: string; } -type PairWithId = { header: Partial; id: string }; +interface Pair { + name: string; + value: string; +} -export function HeaderEditor({ request, className }: Props) { - const updateRequest = useUpdateRequest(request); +interface PairContainer { + pair: Pair; + id: string; +} - const newPair = () => { - return { header: { name: '', value: '' }, id: Math.random().toString() }; +export function PairEditor({ pairs: originalPairs, className, onChange }: Props) { + const newPairContainer = (): PairContainer => { + return { pair: { name: '', value: '' }, id: Math.random().toString() }; }; - const [pairs, setPairs] = useState(() => { + const [pairs, setPairs] = useState(() => { // Remove empty headers on initial render - const nonEmpty = request.headers.filter((h) => !(h.name === '' && h.value === '')); - const pairs = nonEmpty.map((h) => ({ header: h, id: Math.random().toString() })); - return [...pairs, newPair()]; + const nonEmpty = originalPairs.filter((h) => !(h.name === '' && h.value === '')); + const pairs = nonEmpty.map((h) => ({ pair: h, id: Math.random().toString() })); + return [...pairs, newPairContainer()]; }); - const setPairsAndSave = (fn: (pairs: PairWithId[]) => PairWithId[]) => { + const setPairsAndSave = (fn: (pairs: PairContainer[]) => PairContainer[]) => { setPairs((oldPairs) => { - const newPairs = fn(oldPairs); - const headers = newPairs.map((p) => ({ name: '', value: '', ...p.header })); - updateRequest.mutate({ headers }); - return newPairs; + const pairs = fn(oldPairs).map((p) => p.pair); + onChange(pairs); + return fn(oldPairs); }); }; - const handleChangeHeader = (pair: PairWithId) => { - setPairsAndSave((pairs) => - pairs.map((p) => - pair.id !== p.id ? p : { id: p.id, header: { ...p.header, ...pair.header } }, - ), - ); + const handleChangeHeader = (pair: PairContainer) => { + setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))); }; // Ensure there's always at least one pair useEffect(() => { if (pairs.length === 0) { - setPairs((pairs) => [...pairs, newPair()]); + setPairs((pairs) => [...pairs, newPairContainer()]); } }, [pairs]); - const handleDelete = (pair: PairWithId) => { + const handleDelete = (pair: PairContainer) => { setPairsAndSave((oldPairs) => oldPairs.filter((p) => p.id !== pair.id)); }; @@ -63,12 +63,12 @@ export function HeaderEditor({ request, className }: Props) { return ( { if (isLast) { - setPairs((pairs) => [...pairs, newPair()]); + setPairs((pairs) => [...pairs, newPairContainer()]); } }} onDelete={isLast ? undefined : handleDelete} @@ -81,27 +81,28 @@ export function HeaderEditor({ request, className }: Props) { } function FormRow({ - pair, + pairContainer, onChange, onDelete, onFocus, isLast, }: { - pair: PairWithId; - onChange: (pair: PairWithId) => void; - onDelete?: (pair: PairWithId) => void; + pairContainer: PairContainer; + onChange: (pair: PairContainer) => void; + onDelete?: (pair: PairContainer) => void; onFocus?: () => void; isLast?: boolean; }) { + const { id } = pairContainer; return (
onChange({ id: pair.id, header: { name } })} + onChange={(name) => onChange({ id, pair: { name, value: pairContainer.pair.value } })} onFocus={onFocus} placeholder={isLast ? 'new name' : 'name'} useEditor={{ useTemplating: true }} @@ -109,10 +110,10 @@ function FormRow({ onChange({ id: pair.id, header: { value } })} + onChange={(value) => onChange({ id, pair: { name: pairContainer.pair.name, value } })} onFocus={onFocus} placeholder={isLast ? 'new value' : 'value'} useEditor={{ useTemplating: true }} @@ -120,7 +121,8 @@ function FormRow({ {onDelete && ( onDelete(pair)} + title="Delete header" + onClick={() => onDelete(pairContainer)} tabIndex={-1} className={classnames('opacity-0 group-hover:opacity-100')} />