mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 18:38:33 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d1057538b | ||
|
|
df67920512 | ||
|
|
2780259998 |
No files matched your search
@@ -3,7 +3,7 @@
|
||||
* a WHERE filter, and pending cell edits / added / deleted rows submitted
|
||||
* together in one transaction. Tables without a primary key are read-only.
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
call,
|
||||
commands,
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { openDialog, sourceById, withConnection } from "./store";
|
||||
import type { TableRef } from "./tableInput";
|
||||
import styles from "./Database.module.css";
|
||||
import { registerTableEditor, tableEditorChanged, type TableEditorHandle } from "./tableEditorFocus";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { shortcutLabel } from "@/platform/keymap";
|
||||
|
||||
@@ -179,9 +180,19 @@ export function TableEditor({ input }: { input: EditorInput }) {
|
||||
});
|
||||
|
||||
const dirty = hasEdits(edits);
|
||||
// Submit / Revert actions (⌘↩ / ⌥⌘Z) reach the latest state through the handle.
|
||||
const viewRef = useRef<HTMLDivElement>(null);
|
||||
const handle = useRef<TableEditorHandle>({ dirty: false, submit: () => {}, revert: () => {} }).current;
|
||||
handle.dirty = editable && dirty;
|
||||
handle.submit = () => {
|
||||
if (editable && dirty) void submit();
|
||||
};
|
||||
handle.revert = () => setEdits(emptyEdits());
|
||||
useEffect(() => registerTableEditor(viewRef.current!, handle), [handle]);
|
||||
useEffect(() => tableEditorChanged(handle), [handle, dirty, editable]);
|
||||
const orderInvalid = orderDraft !== null && !parseOrderBy(orderDraft, names);
|
||||
return (
|
||||
<div className={styles.view}>
|
||||
<div className={styles.view} ref={viewRef} data-db-table-editor="">
|
||||
<div className={styles.toolbar}>
|
||||
<IconButton icon="refresh" title="Reload Page" onClick={reload} disabled={loading} />
|
||||
<div className={styles.pager}>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { lazyComponent } from "@/ui";
|
||||
import { activeEditor, registerEditorContribution } from "@/editor/api";
|
||||
import { registerActions } from "@/platform/actions";
|
||||
import { activeTableEditor, installTableEditorFocus } from "./tableEditorFocus";
|
||||
import { useProject } from "@/platform/project";
|
||||
import {
|
||||
registerEditorKind,
|
||||
@@ -94,6 +95,7 @@ export function activate(): () => void {
|
||||
}),
|
||||
() => setFileDecorations("database", new Map()),
|
||||
watchIdleConnections(),
|
||||
installTableEditorFocus(),
|
||||
registerDatabaseTreeActions(),
|
||||
registerActions([
|
||||
{
|
||||
@@ -152,6 +154,22 @@ export function activate(): () => void {
|
||||
await executeInView(e.view, e.ctx.path);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "db.submitChanges",
|
||||
title: "Submit",
|
||||
category: "Tools",
|
||||
keywords: ["table editor", "apply changes", "commit"],
|
||||
when: "dbTableEditorFocus && dbTableEditorChanges",
|
||||
run: () => activeTableEditor()?.submit(),
|
||||
},
|
||||
{
|
||||
id: "db.revertChanges",
|
||||
title: "Revert",
|
||||
category: "Tools",
|
||||
keywords: ["table editor", "discard changes", "undo"],
|
||||
when: "dbTableEditorFocus && dbTableEditorChanges",
|
||||
run: () => activeTableEditor()?.revert(),
|
||||
},
|
||||
{
|
||||
id: "db.explainPlan",
|
||||
title: "Explain Plan",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { DbDriver, DbTablePage } from "@/platform/ipc";
|
||||
import { getContext } from "@/platform/context";
|
||||
import { TableEditor } from "./TableEditor";
|
||||
import { activeTableEditor, installTableEditorFocus } from "./tableEditorFocus";
|
||||
import { tableInput } from "./tableInput";
|
||||
|
||||
const backend = vi.hoisted(() => ({
|
||||
@@ -87,4 +89,40 @@ describe("table editor capabilities", () => {
|
||||
expect(screen.getByRole("button", { name: "Add Row" })).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Submit" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("offers Submit and Revert to the keyboard while the grid has focus and edits are pending (#1549)", async () => {
|
||||
backend.driver = "postgres";
|
||||
backend.applyEdits.mockResolvedValue({ status: "ok", data: 1 });
|
||||
const uninstall = installTableEditorFocus();
|
||||
try {
|
||||
await act(async () => {
|
||||
render(<TableEditor input={tableInput("source", "public", "users")} />);
|
||||
});
|
||||
const grid = screen.getByRole("grid");
|
||||
act(() => grid.focus());
|
||||
expect(getContext()).toMatchObject({ dbTableEditorFocus: true, dbTableEditorChanges: false });
|
||||
|
||||
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);
|
||||
fireEvent.change(input, { target: { value: "Bob" } });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
act(() => grid.focus());
|
||||
expect(getContext()).toMatchObject({ dbTableEditorFocus: true, dbTableEditorChanges: true });
|
||||
|
||||
act(() => activeTableEditor()!.revert());
|
||||
expect(screen.getByRole("gridcell", { name: "Ann" })).toBeTruthy();
|
||||
expect(getContext().dbTableEditorChanges).toBe(false);
|
||||
|
||||
fireEvent.doubleClick(screen.getByRole("gridcell", { name: "Ann" }));
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "Edit name" }), { target: { value: "Bob" } });
|
||||
fireEvent.keyDown(screen.getByRole("textbox", { name: "Edit name" }), { key: "Enter" });
|
||||
act(() => grid.focus());
|
||||
await act(async () => activeTableEditor()!.submit());
|
||||
expect(backend.applyEdits).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
uninstall();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* The table editor that has focus, for Submit / Revert (⌘↩ / ⌥⌘Z). Not while a text field has
|
||||
* it: ⌘↩ there applies the field's own edit (the Value panel, the cell editor).
|
||||
*/
|
||||
import { setContext } from "@/platform/context";
|
||||
|
||||
export interface TableEditorHandle {
|
||||
dirty: boolean;
|
||||
submit(): void;
|
||||
revert(): void;
|
||||
}
|
||||
|
||||
const handles = new WeakMap<Element, TableEditorHandle>();
|
||||
let active: TableEditorHandle | null = null;
|
||||
|
||||
const isTextField = (el: Element) => !!el.closest("input, textarea, select, [contenteditable='true']");
|
||||
|
||||
function update() {
|
||||
setContext("dbTableEditorFocus", !!active);
|
||||
setContext("dbTableEditorChanges", !!active?.dirty);
|
||||
}
|
||||
|
||||
function onFocusIn(event: FocusEvent) {
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
const root = target?.closest("[data-db-table-editor]");
|
||||
active = (root && !isTextField(target!) && handles.get(root)) || null;
|
||||
update();
|
||||
}
|
||||
|
||||
/** Registers the editor rooted at `root`; returns the function that unregisters it. */
|
||||
export function registerTableEditor(root: Element, handle: TableEditorHandle): () => void {
|
||||
handles.set(root, handle);
|
||||
if (root.contains(document.activeElement) && !isTextField(document.activeElement!)) active = handle;
|
||||
update();
|
||||
return () => {
|
||||
handles.delete(root);
|
||||
if (active === handle) active = null;
|
||||
update();
|
||||
};
|
||||
}
|
||||
|
||||
/** The handle's state changed (pending edits). */
|
||||
export function tableEditorChanged(handle: TableEditorHandle) {
|
||||
if (active === handle) update();
|
||||
}
|
||||
|
||||
export function activeTableEditor(): TableEditorHandle | null {
|
||||
return active;
|
||||
}
|
||||
|
||||
export function installTableEditorFocus(): () => void {
|
||||
document.addEventListener("focusin", onFocusIn);
|
||||
return () => {
|
||||
document.removeEventListener("focusin", onFocusIn);
|
||||
active = null;
|
||||
update();
|
||||
};
|
||||
}
|
||||
@@ -123,6 +123,8 @@ export const eclipseMacos: Record<string, string[]> = {
|
||||
"debug.stepOver": ["F6"],
|
||||
"debug.forceStepOver": ["Meta+Ctrl+F6"],
|
||||
"code.surroundWith": ["Meta+Alt+Z"],
|
||||
// ⌥⌘Z is Surround With here, not the table editor's Revert.
|
||||
"db.revertChanges": [],
|
||||
"file.reloadFromDisk": [],
|
||||
"project.reloadFromDisk": [],
|
||||
"navigate.toggleBookmark": [],
|
||||
|
||||
@@ -352,6 +352,9 @@ export const intellijMacos: Record<string, string[]> = {
|
||||
"graphql.runQuery": ["Meta+Enter"],
|
||||
"http.runAll": [],
|
||||
"db.execute": ["Meta+Enter"],
|
||||
// Table editor with pending changes (after `vcs.rollback`, so ⌥⌘Z reverts the table's edits there).
|
||||
"db.submitChanges": ["Meta+Enter"],
|
||||
"db.revertChanges": ["Meta+Alt+Z"],
|
||||
"db.showToolWindow": [],
|
||||
"agents.newTask": ["Meta+Shift+I"],
|
||||
"agents.showToolWindow": [],
|
||||
|
||||
@@ -350,6 +350,9 @@ export const intellijWindows: Record<string, string[]> = {
|
||||
"graphql.runQuery": ["Ctrl+Enter"],
|
||||
"http.runAll": [],
|
||||
"db.execute": ["Ctrl+Enter"],
|
||||
// Table editor with pending changes (after `vcs.rollback`, so Ctrl+Alt+Z reverts the table's edits there).
|
||||
"db.submitChanges": ["Ctrl+Enter"],
|
||||
"db.revertChanges": ["Ctrl+Alt+Z"],
|
||||
"db.showToolWindow": [],
|
||||
"agents.newTask": ["Ctrl+Alt+Shift+I"],
|
||||
"agents.showToolWindow": [],
|
||||
|
||||
Reference in new issue
Block a user