diff --git a/.changeset/bright-bears-run.md b/.changeset/bright-bears-run.md new file mode 100644 index 00000000..8eed28ac --- /dev/null +++ b/.changeset/bright-bears-run.md @@ -0,0 +1,5 @@ +--- +'@prisma/studio-core': patch +--- + +Keep the SQL editor content-sized until results are shown, then cap its height so long scripts scroll internally and the result grid can use the remaining space. \ No newline at end of file diff --git a/Architecture/sql-view.md b/Architecture/sql-view.md index 34b19510..dab33c7d 100644 --- a/Architecture/sql-view.md +++ b/Architecture/sql-view.md @@ -47,6 +47,7 @@ SQL result visualization is governed by: - SQL result grid MUST keep column pinning enabled and URL-backed using existing `pin` navigation state. - SQL execution MUST remain cancellable via `AbortController`. - SQL editor lines MUST soft-wrap within the available editor width instead of forcing page-level horizontal overflow while typing long queries. +- When SQL results are visible, the editor pane MUST stay content-sized up to a bounded height and then scroll internally, rather than splitting the viewport evenly with the result grid. ## Result Rendering Contract @@ -103,3 +104,4 @@ Changes to SQL view MUST include tests for: - read-only grid rendering (pin control present, sorting controls disabled) - absence of history UI - absence of pagination controls in SQL result grid +- bounded editor height when results are visible diff --git a/FEATURES.md b/FEATURES.md index a6a8c714..81f9b2fe 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -260,6 +260,7 @@ The same lint transport also validates saved table-level SQL filter pills in the Keyboard execution supports `Cmd/Ctrl+Enter`, and in multi-statement scripts it runs only the top-level statement at the current cursor. Large SQL result sets stay responsive while you keep editing the query because result-grid rendering is isolated from editor keystrokes unless the executed result itself changes. Long SQL lines wrap inside the editor instead of stretching the overall page wider, so writing large queries stays readable on narrow viewports. +When a query returns results, the editor keeps its content-sized height until it reaches a viewport cap, then scrolls internally so the result grid can use the remaining space. ## AI SQL Generation diff --git a/ui/studio/views/sql/SqlView.test.tsx b/ui/studio/views/sql/SqlView.test.tsx index 5355c841..9f5e6a09 100644 --- a/ui/studio/views/sql/SqlView.test.tsx +++ b/ui/studio/views/sql/SqlView.test.tsx @@ -18,6 +18,9 @@ const setPinnedColumnIdsMock = vi.fn(); let mockEditorCursorHead = 0; let mockEditorDocLength = 0; let mockEditorDocText = ""; +let mockCodeMirrorProps: + | { height?: string; maxHeight?: string; minHeight?: string } + | undefined; let mockCodeMirrorOnChange: ((value: string) => void) | undefined; let mockCodeMirrorExtensions: unknown[] = []; const mockEditorDispatch = vi.fn((transaction: unknown) => { @@ -107,7 +110,10 @@ vi.mock("../../../hooks/use-navigation", () => ({ vi.mock("@uiw/react-codemirror", () => ({ default: (props: { "aria-label"?: string; + height?: string; extensions?: unknown[]; + maxHeight?: string; + minHeight?: string; onCreateEditor?: (view: { dispatch: (transaction: unknown) => void; focus: () => void; @@ -119,6 +125,11 @@ vi.mock("@uiw/react-codemirror", () => ({ onChange?: (value: string) => void; value?: string; }) => { + mockCodeMirrorProps = { + height: props.height, + maxHeight: props.maxHeight, + minHeight: props.minHeight, + }; mockCodeMirrorOnChange = props.onChange; mockCodeMirrorExtensions = props.extensions ?? []; mockEditorDocLength = (props.value ?? "").length; @@ -415,6 +426,7 @@ afterEach(() => { mockEditorDocText = ""; mockCodeMirrorOnChange = undefined; mockCodeMirrorExtensions = []; + mockCodeMirrorProps = undefined; }); beforeEach(() => { @@ -1740,6 +1752,76 @@ describe("SqlView", () => { harness.cleanup(); }); + it("keeps the SQL editor in a bounded scroll region for long scripts", () => { + const { adapter } = createAdapterMock(); + const studio = createStudioMock(adapter); + useStudioMock.mockReturnValue(studio); + + const harness = renderSqlView(); + + const scrollRegion = harness.container.querySelector( + '[data-testid="sql-editor-scroll-container"]', + ); + expect(scrollRegion).toBeTruthy(); + expect(scrollRegion?.className).toContain("min-h-0"); + expect(scrollRegion?.className).toContain("overflow-hidden"); + + harness.cleanup(); + }); + + it("keeps the editor content-sized until results are shown, then caps it", async () => { + const { adapter } = createAdapterMock(); + const studio = createStudioMock(adapter); + useStudioMock.mockReturnValue(studio); + + const harness = renderSqlView(); + + expect( + harness.container + .querySelector('[data-testid="sql-editor-scroll-container"]') + ?.className, + ).toContain("flex-1"); + expect(mockCodeMirrorProps).toMatchObject({ + height: "100%", + maxHeight: undefined, + }); + + const runButton = [...harness.container.querySelectorAll("button")].find( + (button) => button.textContent?.includes("Run SQL"), + ); + + if (!runButton) { + throw new Error("SQL view run control not rendered"); + } + + act(() => { + runButton.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + await waitFor(() => { + return ( + harness.container.textContent?.includes("1 row(s) returned") ?? false + ); + }); + + expect( + harness.container + .querySelector('[data-testid="sql-editor-scroll-container"]') + ?.className, + ).toContain("flex-none"); + expect( + harness.container + .querySelector('[data-testid="sql-result-grid-container"]') + ?.className, + ).toContain("flex-1"); + expect(mockCodeMirrorProps).toMatchObject({ + height: undefined, + maxHeight: "40vh", + }); + + harness.cleanup(); + }); + it("supports cancelling a running query", async () => { const raw: Adapter["raw"] = async (_details, options) => { return await new Promise((resolve) => { diff --git a/ui/studio/views/sql/SqlView.tsx b/ui/studio/views/sql/SqlView.tsx index 219f92df..02afba82 100644 --- a/ui/studio/views/sql/SqlView.tsx +++ b/ui/studio/views/sql/SqlView.tsx @@ -29,6 +29,7 @@ import { TableHead, TableRow } from "../../../components/ui/table"; import { useColumnPinning } from "../../../hooks/use-column-pinning"; import { useIntrospection } from "../../../hooks/use-introspection"; import { useNavigation } from "../../../hooks/use-navigation"; +import { cn } from "../../../lib/utils"; import type { CellProps } from "../../cell/Cell"; import { Cell } from "../../cell/Cell"; import { getCell } from "../../cell/get-cell"; @@ -581,6 +582,8 @@ export function SqlView(_props: ViewProps) { resetKey: visualizationResetKey, rows: result?.rows ?? EMPTY_SQL_RESULT_ROWS, }); + const hasResult = result != null; + const editorMaxHeight = hasResult ? "40vh" : undefined; function applyAiSqlGenerationResult(args: { aiQueryRequest: string; @@ -1157,67 +1160,130 @@ export function SqlView(_props: ViewProps) { ) : null} {result ? (