+
setView("table")} />
setView("chart")} />
@@ -372,10 +418,21 @@ function Result({ tab }: { tab: ResultTab }) {
disabled={busy}
onClick={() => setEdits((e) => ({ ...e, inserts: [...e.inserts, new Map()] }))}
/>
-
>
)}
+ {columnsAt && (
+
{
+ setColumnsAt(null);
+ setRevealColumn({ col });
+ }}
+ onClose={() => setColumnsAt(null)}
+ />
+ )}
);
}
diff --git a/src/features/database/TableEditor.tsx b/src/features/database/TableEditor.tsx
index 03bc6cd6a..c3545059a 100644
--- a/src/features/database/TableEditor.tsx
+++ b/src/features/database/TableEditor.tsx
@@ -13,7 +13,7 @@ import {
type DbTablePage,
type RequestId,
} from "@/platform/ipc";
-import { Button, EmptyState, Icon, IconButton, Select, Spinner, T, showContextMenu } from "@/ui";
+import { Button, EmptyState, Icon, IconButton, Select, Spinner, T, showContextMenu, tooltipAttrs } from "@/ui";
import { notifications, type EditorInput } from "@/workbench/contributions";
import { SqlFilterInput } from "./SqlFilterInput";
import { ColumnsPopup } from "./ColumnsPopup";
@@ -241,10 +241,21 @@ export function TableEditor({ input }: { input: EditorInput }) {
title="Add Row"
onClick={() => setEdits((e) => ({ ...e, inserts: [...e.inserts, new Map()] }))}
/>
-
void submit()}>
+ void submit()}
+ {...tooltipAttrs("Submit", shortcutLabel("db.submitChanges"))}
+ >
Submit
- setEdits(emptyEdits())}>
+ setEdits(emptyEdits())}
+ {...tooltipAttrs("Revert", shortcutLabel("db.revertChanges"))}
+ >
Revert
>
diff --git a/src/features/database/results-view.test.tsx b/src/features/database/results-view.test.tsx
index b8d96c60f..82a133fd7 100644
--- a/src/features/database/results-view.test.tsx
+++ b/src/features/database/results-view.test.tsx
@@ -1,8 +1,10 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { DbDriver, DbQueryResult, DbTableDetails } from "@/platform/ipc";
+import { getContext } from "@/platform/context";
import { ResultsView } from "./ResultsView";
import { executeConsole, useDatabase, type ResultTab } from "./store";
+import { activeTableEditor, installTableEditorFocus } from "./tableEditorFocus";
const backend = vi.hoisted(() => ({
driver: "postgres" as DbDriver,
@@ -326,3 +328,36 @@ describe("read-only results with a reason", () => {
);
});
});
+
+describe("result grid keyboard Submit / Revert (#2332)", () => {
+ it("confirms the cell being edited, then submits; reverts pending edits", async () => {
+ const uninstall = installTableEditorFocus();
+ try {
+ show("SELECT * FROM users", [{ name: "id" }, { name: "name" }], [["1", "Ann"]]);
+ await act(async () => render());
+ const submit = await screen.findByRole("button", { name: "Submit" });
+ expect(submit.getAttribute("data-tooltip-shortcut")).toBeTruthy();
+ const grid = screen.getByRole("grid");
+ act(() => grid.focus());
+ expect(getContext()).toMatchObject({ dbTableEditorFocus: true, dbTableEditorChanges: false });
+
+ fireEvent.doubleClick(screen.getByRole("gridcell", { name: "Ann" }));
+ fireEvent.change(screen.getByRole("textbox", { name: "Edit name" }), { target: { value: "Bob" } });
+ // Still the grid while a cell is being edited: ⌘↩ confirms the cell, then submits.
+ expect(getContext()).toMatchObject({ dbTableEditorFocus: true, dbTableEditorChanges: true });
+ await act(async () => activeTableEditor()!.submit());
+ expect(backend.applyEdits).toHaveBeenCalledWith("pg", "public", "users", [
+ { kind: "update", key: [{ column: "id", value: "1" }], values: [{ column: "name", value: "Bob" }] },
+ ]);
+
+ fireEvent.doubleClick(await screen.findByRole("gridcell", { name: "Ann" }));
+ fireEvent.change(screen.getByRole("textbox", { name: "Edit name" }), { target: { value: "Cara" } });
+ act(() => activeTableEditor()!.revert());
+ expect(screen.queryByRole("textbox", { name: "Edit name" })).toBeNull();
+ expect(screen.getByRole("gridcell", { name: "Ann" })).toBeTruthy();
+ expect(getContext().dbTableEditorChanges).toBe(false);
+ } finally {
+ uninstall();
+ }
+ });
+});
diff --git a/src/features/database/table-editor.test.tsx b/src/features/database/table-editor.test.tsx
index 2980a62c5..ad923f695 100644
--- a/src/features/database/table-editor.test.tsx
+++ b/src/features/database/table-editor.test.tsx
@@ -104,8 +104,8 @@ describe("table editor capabilities", () => {
fireEvent.doubleClick(await screen.findByRole("gridcell", { name: "Ann" }));
const input = screen.getByRole("textbox", { name: "Edit name" });
- // A text field keeps ⌘↩ for its own edit.
- expect(getContext().dbTableEditorFocus).toBe(false);
+ // The inline cell editor still counts as the grid: ⌘↩ confirms the cell, then submits (#2332).
+ expect(getContext().dbTableEditorFocus).toBe(true);
fireEvent.change(input, { target: { value: "Bob" } });
fireEvent.keyDown(input, { key: "Enter" });
act(() => grid.focus());
diff --git a/src/features/database/tableEditorFocus.ts b/src/features/database/tableEditorFocus.ts
index 4453d2f92..8a145364b 100644
--- a/src/features/database/tableEditorFocus.ts
+++ b/src/features/database/tableEditorFocus.ts
@@ -1,7 +1,9 @@
/**
- * The table editor that has focus, for Submit / Revert (⌘↩ / ⌥⌘Z) and Go to Column (⌘F12). Not
- * while a text field has it: ⌘↩ there applies the field's own edit (the Value panel, the cell editor).
+ * The table editor or editable result grid that has focus, for Submit / Revert (⌘↩ / ⌥⌘Z) and Go to
+ * Column (⌘F12). Not while a text field has it: ⌘↩ there applies the field's own edit (the Value
+ * panel, the multi-line editor). The grid's inline cell editor counts as the grid: the cell is confirmed first.
*/
+import { flushSync } from "react-dom";
import { setContext } from "@/platform/context";
export interface TableEditorHandle {
@@ -13,12 +15,15 @@ export interface TableEditorHandle {
const handles = new WeakMap();
let active: TableEditorHandle | null = null;
+let editingCell = false;
-const isTextField = (el: Element) => !!el.closest("input, textarea, select, [contenteditable='true']");
+const isCellEditor = (el: Element) => el.matches("[data-cell-editor]");
+const isTextField = (el: Element) =>
+ !isCellEditor(el) && !!el.closest("input, textarea, select, [contenteditable='true']");
function update() {
setContext("dbTableEditorFocus", !!active);
- setContext("dbTableEditorChanges", !!active?.dirty);
+ setContext("dbTableEditorChanges", !!active && (active.dirty || editingCell));
}
function onFocusIn(event: FocusEvent) {
@@ -29,6 +34,7 @@ function onFocusIn(event: FocusEvent) {
target?.closest("[data-db-table-editor]") ??
(target?.hasAttribute("data-editor-focus") ? target.querySelector("[data-db-table-editor]") : null);
active = (root && !isTextField(target!) && handles.get(root)) || null;
+ editingCell = !!active && isCellEditor(target!);
update();
}
@@ -49,7 +55,10 @@ export function tableEditorChanged(handle: TableEditorHandle) {
if (active === handle) update();
}
+/** The active editor, after confirming the cell being edited so its value counts. */
export function activeTableEditor(): TableEditorHandle | null {
+ const el = document.activeElement;
+ if (active && el instanceof HTMLElement && isCellEditor(el)) flushSync(() => el.blur());
return active;
}
@@ -58,6 +67,7 @@ export function installTableEditorFocus(): () => void {
return () => {
document.removeEventListener("focusin", onFocusIn);
active = null;
+ editingCell = false;
update();
};
}