feat(editor): add table structure peek and dblclick insert

This commit is contained in:
Mo-Moore authored and GitHub committed 2026-09-23 14:43:24 +08:00
1 parent da95bd435f
commit 12ed7a49b3
19 files changed
+1021 -8

No files matched your search

@@ -0,0 +1,104 @@
/**
* @vitest-environment happy-dom
*/
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { afterEach, describe, expect, it } from "vitest";
import {
STRUCTURE_PEEK_PANEL_SELECTOR,
clearRememberedFocusedQueryEditorView,
focusedQueryEditorView,
queryEditorInsertContext,
registerQueryEditorInsertContext,
rememberFocusedQueryEditorView,
shouldPreserveEditorFocusOnPeekPointerDown,
unregisterQueryEditorInsertContext,
} from "@/lib/editor/focusedQueryEditorView";
function mountEditor(): { root: HTMLElement; view: EditorView } {
const root = document.createElement("div");
root.setAttribute("data-query-editor-root", "");
document.body.appendChild(root);
const view = new EditorView({
state: EditorState.create({ doc: "select 1" }),
parent: root,
});
return { root, view };
}
describe("shouldPreserveEditorFocusOnPeekPointerDown", () => {
it("preserves focus for table / chrome clicks", () => {
const td = document.createElement("td");
expect(shouldPreserveEditorFocusOnPeekPointerDown(td)).toBe(true);
});
it("allows native focus for editable controls", () => {
const input = document.createElement("input");
expect(shouldPreserveEditorFocusOnPeekPointerDown(input)).toBe(false);
const button = document.createElement("button");
expect(shouldPreserveEditorFocusOnPeekPointerDown(button)).toBe(false);
});
});
describe("focusedQueryEditorView", () => {
afterEach(() => {
clearRememberedFocusedQueryEditorView();
document.body.replaceChildren();
});
it("returns the focused query editor under activeElement", () => {
const { root, view } = mountEditor();
view.focus();
expect(focusedQueryEditorView(view.contentDOM)).toBe(view);
expect(focusedQueryEditorView(root)).toBe(view);
});
it("falls back to the last focused editor when activeElement is inside a peek panel", () => {
const { view } = mountEditor();
rememberFocusedQueryEditorView(view);
const peek = document.createElement("div");
peek.setAttribute("data-structure-peek-panel", "");
const input = document.createElement("input");
peek.appendChild(input);
document.body.appendChild(peek);
expect(input.closest(STRUCTURE_PEEK_PANEL_SELECTOR)).toBe(peek);
expect(focusedQueryEditorView(input)).toBe(view);
});
it("returns null when focus is outside editors and peek panels", () => {
const { view } = mountEditor();
rememberFocusedQueryEditorView(view);
const sidebar = document.createElement("button");
document.body.appendChild(sidebar);
expect(focusedQueryEditorView(sidebar)).toBeNull();
});
it("ignores a remembered editor that was disconnected", () => {
const { root, view } = mountEditor();
rememberFocusedQueryEditorView(view);
root.remove();
const peek = document.createElement("div");
peek.setAttribute("data-structure-peek-panel", "");
document.body.appendChild(peek);
expect(focusedQueryEditorView(peek)).toBeNull();
});
});
describe("queryEditorInsertContext", () => {
afterEach(() => {
document.body.replaceChildren();
});
it("stores and clears per-view insert context", () => {
const { view } = mountEditor();
registerQueryEditorInsertContext(view, { connectionId: "c1", database: "db", databaseType: "postgres" });
expect(queryEditorInsertContext(view)).toEqual({ connectionId: "c1", database: "db", databaseType: "postgres" });
unregisterQueryEditorInsertContext(view);
expect(queryEditorInsertContext(view)).toBeUndefined();
});
});
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { STRUCTURE_PEEK_MIN_HEIGHT, STRUCTURE_PEEK_MIN_WIDTH, clampStructurePeekRect, resizeStructurePeekRect, structurePeekPanelId } from "@/lib/editor/structurePeekPanel";
describe("structurePeekPanel", () => {
it("builds a stable case-insensitive identity including catalog", () => {
expect(structurePeekPanelId("Conn", "Db", "Public", "Orders", "Cat")).toBe(structurePeekPanelId("conn", "db", "public", "orders", "cat"));
expect(structurePeekPanelId("c", "db", "a", "t")).not.toBe(structurePeekPanelId("c", "db", "b", "t"));
expect(structurePeekPanelId("c", "db", "s", "t", "cat-a")).not.toBe(structurePeekPanelId("c", "db", "s", "t", "cat-b"));
});
it("clamps rects inside the viewport with a margin", () => {
expect(clampStructurePeekRect({ left: -40, top: -20, width: 500, height: 400 }, 800, 600)).toEqual({
left: 8,
top: 8,
width: 500,
height: 400,
});
expect(clampStructurePeekRect({ left: 700, top: 500, width: 500, height: 400 }, 800, 600)).toEqual({
left: 800 - 8 - 500,
top: 600 - 8 - 400,
width: 500,
height: 400,
});
});
it("enforces minimum size while resizing from the west/north edges", () => {
const origin = { left: 100, top: 100, width: 400, height: 300 };
const shrunkWest = resizeStructurePeekRect(origin, "w", 200, 0, 1200, 800);
expect(shrunkWest.width).toBe(STRUCTURE_PEEK_MIN_WIDTH);
expect(shrunkWest.left).toBe(100 + 400 - STRUCTURE_PEEK_MIN_WIDTH);
const shrunkNorth = resizeStructurePeekRect(origin, "n", 0, 200, 1200, 800);
expect(shrunkNorth.height).toBe(STRUCTURE_PEEK_MIN_HEIGHT);
expect(shrunkNorth.top).toBe(100 + 300 - STRUCTURE_PEEK_MIN_HEIGHT);
});
it("resizes northeast corners without treating n as containing e", () => {
const origin = { left: 100, top: 100, width: 400, height: 300 };
const grown = resizeStructurePeekRect(origin, "ne", 50, -40, 1200, 800);
expect(grown.width).toBe(450);
expect(grown.height).toBe(340);
expect(grown.left).toBe(100);
expect(grown.top).toBe(60);
});
});