mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-13 12:13:42 +01:00
Give the actions bar one inset in both of Yaak Proxy's panes
The request half's tab content ran to the card's bottom edge while the response half's stopped above it, so the Pretty/Raw bar sat 7px from the bottom on one side and 19px on the other. The request half is now padded below as the response is. The bar's inset is one constant that viewers can no longer override, and the form view draws the switch in one bar with its bulk-view toggle instead of shifting the bar past it, so every viewer's bar sits 11px from the pane's right and 19px from its bottom. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
700003e9a2
commit
29a173a07e
6 files changed
+121
-34
No files matched your search
@@ -43,8 +43,8 @@ import { useSaveFlowBody } from "../lib/flowActions";
|
||||
interface Props {
|
||||
flow: Flow;
|
||||
which: FlowBody;
|
||||
/** A URL-encoded body's fields, shown as its formatted view */
|
||||
form?: ReactNode;
|
||||
/** A URL-encoded body's fields, its formatted view, with the switch in the fields' own bar */
|
||||
form?: (actions: ReactNode) => ReactNode;
|
||||
/** Leaves saying the body was cut short to the tab, which says it its own way */
|
||||
hideCutNote?: boolean;
|
||||
}
|
||||
@@ -178,7 +178,7 @@ export function FlowBodyViewer({ flow, which, form, hideCutNote = false }: Props
|
||||
);
|
||||
|
||||
let viewer: ReactNode;
|
||||
let textViewer = false;
|
||||
let ownsBar = false;
|
||||
if (decodeError != null) {
|
||||
// What failed to decode is as it was on the wire, compressed, so never text.
|
||||
viewer = <BinaryBody flow={flow} which={which} bytes={bytes} />;
|
||||
@@ -196,8 +196,9 @@ export function FlowBodyViewer({ flow, which, form, hideCutNote = false }: Props
|
||||
viewer = <ImageBody bytes={bytes} mime={imageType} />;
|
||||
} else if (text == null) {
|
||||
viewer = <BinaryBody flow={flow} which={which} bytes={bytes} />;
|
||||
} else if (prettyView === "form" && pretty) {
|
||||
viewer = form;
|
||||
} else if (prettyView === "form" && pretty && form != null) {
|
||||
ownsBar = true;
|
||||
viewer = form(modeSwitch);
|
||||
} else if (prettyView === "table" && pretty) {
|
||||
viewer = <CsvViewer text={text} className="pb-2" />;
|
||||
} else if (prettyView === "preview" && pretty) {
|
||||
@@ -207,7 +208,7 @@ export function FlowBodyViewer({ flow, which, form, hideCutNote = false }: Props
|
||||
<WebPageViewer html={text} baseUrl={flow.url} />
|
||||
);
|
||||
} else {
|
||||
textViewer = true;
|
||||
ownsBar = true;
|
||||
viewer = (
|
||||
<FlowTextViewer
|
||||
flow={flow}
|
||||
@@ -220,19 +221,13 @@ export function FlowBodyViewer({ flow, which, form, hideCutNote = false }: Props
|
||||
);
|
||||
}
|
||||
|
||||
// The text viewer draws the switch in its own bar, beside its filter; the rest get a bar here
|
||||
if (modeSwitch != null && !textViewer) {
|
||||
// The text viewer and the form draw the switch in their own bar, beside their own actions; the
|
||||
// rest get a bar here. Each fills the tab content, so every bar has the same inset.
|
||||
if (modeSwitch != null && !ownsBar) {
|
||||
viewer = (
|
||||
<div className={classNames(ACTIONS_BAR_CONTAINER, "h-full w-full")}>
|
||||
{viewer}
|
||||
<ActionsBar
|
||||
className={classNames(
|
||||
// Beside the form's own bulk-view toggle in the corner, until that joins the bar
|
||||
prettyView === "form" && pretty && "bottom-0! right-[calc(var(--height-sm)+0.25rem)]!",
|
||||
)}
|
||||
>
|
||||
{modeSwitch}
|
||||
</ActionsBar>
|
||||
<ActionsBar>{modeSwitch}</ActionsBar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Dropdown,
|
||||
EmptyStateText,
|
||||
type ExtraPaneTab,
|
||||
FormUrlencodedEditor,
|
||||
type HttpBodyResult,
|
||||
HttpRequestPaneView,
|
||||
HttpResponsePaneView,
|
||||
@@ -148,28 +149,44 @@ function FlowRequestHalf({ flow }: { flow: Flow }) {
|
||||
];
|
||||
const { defaultTab, onTabChange } = useShapeTab("request", flow, available);
|
||||
|
||||
const forceUpdateKey = `${flow.updatedAt}.${body.data == null ? "" : "loaded"}`;
|
||||
|
||||
return (
|
||||
<HttpRequestPaneView
|
||||
className="px-3"
|
||||
// Padded below as the response half's tabs are, so their content, and the actions bar
|
||||
// over it, sit the same distance from the card's edges
|
||||
className="px-3 pb-3"
|
||||
request={request}
|
||||
fullHeight
|
||||
forceUpdateKey={`${flow.updatedAt}.${body.data == null ? "" : "loaded"}`}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
tabsStorageKey="proxy_request_tabs"
|
||||
bodyLoader={loader}
|
||||
extraTabs={extraTabs}
|
||||
defaultTab={defaultTab}
|
||||
onTabChange={onTabChange}
|
||||
urlLine={<UrlLine flow={flow} />}
|
||||
renderBody={(editor) => (
|
||||
renderBody={() => (
|
||||
<FlowRequestBody
|
||||
body={body}
|
||||
// The response's viewer choice, with the form's fields as a URL-encoded body's
|
||||
// formatted view
|
||||
// formatted view, the switch in one bar with their bulk-view toggle
|
||||
viewer={
|
||||
<FlowBodyViewer
|
||||
flow={flow}
|
||||
which="request"
|
||||
form={request.bodyType === BODY_TYPE_FORM_URLENCODED ? editor : undefined}
|
||||
form={
|
||||
request.bodyType === BODY_TYPE_FORM_URLENCODED
|
||||
? (actions) => (
|
||||
<FormUrlencodedEditor
|
||||
readOnly
|
||||
requestId={request.id}
|
||||
form={request.body.form}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
actions={actions}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
hideCutNote
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -7,9 +7,15 @@ export const ACTIONS_BAR_CONTAINER = "relative group/actions";
|
||||
/** Set on an action's outermost element to keep it in view, as a control that shows state does */
|
||||
export const PINNED_ACTION = { "data-pinned": true } as const;
|
||||
|
||||
/**
|
||||
* Where every bar sits in its container, the same for every viewer: flush with the content's
|
||||
* right edge, which a text viewer's scrollbar sits just past, and lifted off the bottom edge
|
||||
* above its horizontal scrollbar. Not overridable, so bars line up from pane to pane.
|
||||
*/
|
||||
const ACTIONS_BAR_INSET = "bottom-2 right-0 left-0";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,14 +23,15 @@ interface Props {
|
||||
* small bordered buttons on the surface colour, so they read over text, each held to the height
|
||||
* and radius of an `sm` icon button and centred on the row. Each fades in while the
|
||||
* container is hovered or the action has focus, unless it carries `PINNED_ACTION`. The row lets
|
||||
* clicks through to the content between its actions.
|
||||
* clicks through to the content between its actions. Place the container over the whole tab
|
||||
* content, not a viewer's inner box, so the bar's inset is the pane's.
|
||||
*/
|
||||
export function ActionsBar({ children, className }: Props) {
|
||||
export function ActionsBar({ children }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
"absolute bottom-2 left-0 right-0 z-10 flex items-center justify-end gap-1",
|
||||
"absolute z-10 flex items-center justify-end gap-1",
|
||||
ACTIONS_BAR_INSET,
|
||||
"pointer-events-none",
|
||||
"*:pointer-events-auto *:bg-surface *:shadow *:transition-opacity *:transform-gpu",
|
||||
// Every action as tall and as round as a small icon button, so they line up
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { type ReactNode, useCallback, useMemo } from "react";
|
||||
import type { Pair, PairEditorProps } from "../pairs/PairEditor";
|
||||
import { PairOrBulkEditor } from "../pairs/PairOrBulkEditor";
|
||||
import type { HttpFormFieldView, HttpRequestBodyView } from "./types";
|
||||
@@ -11,6 +11,8 @@ type Props = {
|
||||
onChange?: (body: HttpRequestBodyView) => void;
|
||||
/** Show the fields without a way to change them */
|
||||
readOnly?: boolean;
|
||||
/** Drawn in one bar with the bulk-edit toggle, like a view-mode switch */
|
||||
actions?: ReactNode;
|
||||
};
|
||||
|
||||
export function FormUrlencodedEditor({
|
||||
@@ -19,6 +21,7 @@ export function FormUrlencodedEditor({
|
||||
forceUpdateKey,
|
||||
onChange,
|
||||
readOnly,
|
||||
actions,
|
||||
}: Props) {
|
||||
const pairs = useMemo<Pair[]>(
|
||||
() =>
|
||||
@@ -52,6 +55,7 @@ export function FormUrlencodedEditor({
|
||||
pairs={pairs}
|
||||
onChange={handleChange}
|
||||
readOnly={readOnly}
|
||||
actions={actions}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
stateKey={`urlencoded.${requestId}`}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import type { HostInputProps } from "../EditorHost";
|
||||
import { EditorHostProvider } from "../EditorHost";
|
||||
import { PairOrBulkEditor } from "./PairOrBulkEditor";
|
||||
|
||||
function StubInput({ name, defaultValue }: HostInputProps) {
|
||||
return <div data-input={name}>{defaultValue}</div>;
|
||||
}
|
||||
|
||||
function render(actions?: string) {
|
||||
return renderToStaticMarkup(
|
||||
<EditorHostProvider value={{ Input: StubInput }}>
|
||||
<PairOrBulkEditor
|
||||
readOnly
|
||||
preferenceName="test"
|
||||
stateKey="test"
|
||||
forceUpdateKey=""
|
||||
pairs={[{ id: "p1", enabled: true, name: "a", value: "1" }]}
|
||||
onChange={() => {}}
|
||||
actions={actions == null ? undefined : <span data-action>{actions}</span>}
|
||||
/>
|
||||
</EditorHostProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("PairOrBulkEditor", () => {
|
||||
test("with actions, its bulk toggle shares one actions bar with them", () => {
|
||||
const markup = render("SWITCH");
|
||||
expect(markup).toContain("group/actions");
|
||||
// The toggle, then the given actions, inside the bar
|
||||
expect(markup).toMatch(/group-hover\/actions[^>]*>.*Enable bulk edit.*SWITCH/);
|
||||
});
|
||||
|
||||
test("without actions, its toggle keeps its own corner", () => {
|
||||
const markup = render();
|
||||
expect(markup).not.toContain("group/actions");
|
||||
expect(markup).toContain("Enable bulk edit");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
import { ACTIONS_BAR_CONTAINER, ActionsBar } from "../ActionsBar";
|
||||
import { useEditorHost } from "../EditorHost";
|
||||
import { IconButton } from "../IconButton";
|
||||
import { BulkPairEditor } from "./BulkPairEditor";
|
||||
@@ -8,9 +10,14 @@ import { PairEditor } from "./PairEditor";
|
||||
interface Props extends PairEditorProps {
|
||||
preferenceName: string;
|
||||
forcedEnvironmentId?: string;
|
||||
/**
|
||||
* Drawn after the bulk-edit toggle in one `ActionsBar`, like a view-mode switch. Giving it puts
|
||||
* the toggle in that bar rather than its own corner; once every site has moved, it always is
|
||||
*/
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
|
||||
export function PairOrBulkEditor({ preferenceName, actions, ...props }: Props) {
|
||||
const { useStoredValue } = useEditorHost();
|
||||
const [useBulk, setUseBulk] = useStoredValue<boolean>(
|
||||
["bulk_edit", preferenceName],
|
||||
@@ -18,20 +25,37 @@ export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
|
||||
"global",
|
||||
);
|
||||
|
||||
const editor = useBulk ? <BulkPairEditor {...props} /> : <PairEditor {...props} />;
|
||||
const toggleProps = {
|
||||
size: "sm",
|
||||
variant: "border",
|
||||
title: useBulk ? "Enable form edit" : "Enable bulk edit",
|
||||
onClick: () => setUseBulk((b) => !b),
|
||||
icon: useBulk ? "table" : "file_code",
|
||||
} as const;
|
||||
|
||||
if (actions != null) {
|
||||
return (
|
||||
<div className={classNames(ACTIONS_BAR_CONTAINER, "h-full w-full")}>
|
||||
{editor}
|
||||
<ActionsBar>
|
||||
<IconButton {...toggleProps} />
|
||||
{actions}
|
||||
</ActionsBar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full group/wrapper">
|
||||
{useBulk ? <BulkPairEditor {...props} /> : <PairEditor {...props} />}
|
||||
{editor}
|
||||
<div className="absolute right-0 bottom-0">
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="border"
|
||||
title={useBulk ? "Enable form edit" : "Enable bulk edit"}
|
||||
{...toggleProps}
|
||||
className={classNames(
|
||||
"transition-opacity opacity-0 group-hover:opacity-80 hover:opacity-100! shadow",
|
||||
"bg-surface hover:text group-hover/wrapper:opacity-100",
|
||||
)}
|
||||
onClick={() => setUseBulk((b) => !b)}
|
||||
icon={useBulk ? "table" : "file_code"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user