From 29a173a07e6412b6f974c535f8b78783f9300460 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Nov 2026 10:51:12 -0700 Subject: [PATCH] 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 --- apps/yaak-proxy/components/FlowBodyViewer.tsx | 27 +++++-------- apps/yaak-proxy/components/FlowCard.tsx | 27 ++++++++++--- packages/ui/src/components/ActionsBar.tsx | 17 +++++--- .../components/http/FormUrlencodedEditor.tsx | 6 ++- .../pairs/PairOrBulkEditor.test.tsx | 40 +++++++++++++++++++ .../src/components/pairs/PairOrBulkEditor.tsx | 38 ++++++++++++++---- 6 files changed, 121 insertions(+), 34 deletions(-) create mode 100644 packages/ui/src/components/pairs/PairOrBulkEditor.test.tsx diff --git a/apps/yaak-proxy/components/FlowBodyViewer.tsx b/apps/yaak-proxy/components/FlowBodyViewer.tsx index 2523880ac..8e687c3ab 100644 --- a/apps/yaak-proxy/components/FlowBodyViewer.tsx +++ b/apps/yaak-proxy/components/FlowBodyViewer.tsx @@ -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 = ; @@ -196,8 +196,9 @@ export function FlowBodyViewer({ flow, which, form, hideCutNote = false }: Props viewer = ; } else if (text == null) { viewer = ; - } 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 = ; } else if (prettyView === "preview" && pretty) { @@ -207,7 +208,7 @@ export function FlowBodyViewer({ flow, which, form, hideCutNote = false }: Props ); } else { - textViewer = true; + ownsBar = true; viewer = ( {viewer} - - {modeSwitch} - + {modeSwitch} ); } diff --git a/apps/yaak-proxy/components/FlowCard.tsx b/apps/yaak-proxy/components/FlowCard.tsx index 2fba503c7..16898f5e3 100644 --- a/apps/yaak-proxy/components/FlowCard.tsx +++ b/apps/yaak-proxy/components/FlowCard.tsx @@ -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 ( } - renderBody={(editor) => ( + renderBody={() => ( ( + + ) + : undefined + } hideCutNote /> } diff --git a/packages/ui/src/components/ActionsBar.tsx b/packages/ui/src/components/ActionsBar.tsx index 918a5476b..ff1315d61 100644 --- a/packages/ui/src/components/ActionsBar.tsx +++ b/packages/ui/src/components/ActionsBar.tsx @@ -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 (
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( () => @@ -52,6 +55,7 @@ export function FormUrlencodedEditor({ pairs={pairs} onChange={handleChange} readOnly={readOnly} + actions={actions} forceUpdateKey={forceUpdateKey} stateKey={`urlencoded.${requestId}`} /> diff --git a/packages/ui/src/components/pairs/PairOrBulkEditor.test.tsx b/packages/ui/src/components/pairs/PairOrBulkEditor.test.tsx new file mode 100644 index 000000000..17d6134da --- /dev/null +++ b/packages/ui/src/components/pairs/PairOrBulkEditor.test.tsx @@ -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
{defaultValue}
; +} + +function render(actions?: string) { + return renderToStaticMarkup( + + {}} + actions={actions == null ? undefined : {actions}} + /> + , + ); +} + +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"); + }); +}); diff --git a/packages/ui/src/components/pairs/PairOrBulkEditor.tsx b/packages/ui/src/components/pairs/PairOrBulkEditor.tsx index 22f4facc4..7ac8fbea1 100644 --- a/packages/ui/src/components/pairs/PairOrBulkEditor.tsx +++ b/packages/ui/src/components/pairs/PairOrBulkEditor.tsx @@ -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( ["bulk_edit", preferenceName], @@ -18,20 +25,37 @@ export function PairOrBulkEditor({ preferenceName, ...props }: Props) { "global", ); + const editor = useBulk ? : ; + 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 ( +
+ {editor} + + + {actions} + +
+ ); + } + return (
- {useBulk ? : } + {editor}
setUseBulk((b) => !b)} - icon={useBulk ? "table" : "file_code"} />