fix(grid): allow edit table structure shortcut without clicking grid

This commit is contained in:
lxk955
2026-10-04 16:35:33 +08:00
committed by GitHub
parent f951983914
commit 6606a98af5
10 changed files with 66 additions and 15 deletions
+10 -1
View File
@@ -109,6 +109,7 @@ import {
isBrowserTaskManagerShortcut,
isCloseOtherTabsShortcut,
isCloseTabShortcut,
isEditTableStructureShortcut,
isExecuteSqlInNewResultTabShortcut,
isExecuteSqlShortcut,
isFocusSearchShortcut,
@@ -152,7 +153,7 @@ import { countAvailableAgentDriverUpdates } from "@/lib/connection/agentDriverUp
import type { DriverStoreFocus, DriverStoreTab } from "@/lib/connection/agentDriverInstallHint";
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
import { webPath } from "@/lib/common/webPath";
import { shouldBlockAppNativeSelectAll } from "@/lib/common/clipboard";
import { eventTargetAllowsNativeClipboard, shouldBlockAppNativeSelectAll } from "@/lib/common/clipboard";
import { APP_FONT_SANS_CSS_VAR, DATA_GRID_FONT_FAMILY_CSS_VAR, DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_MONO_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, FONT_MONO_CSS_VAR } from "@/lib/app/appFonts";
import { DATA_GRID_TYPE_COLOR_KEYS, dataGridTypeColorCssVar, resolveActiveDataGridTypeColors } from "@/lib/dataGrid/dataGridTypeColorScheme";
import { rankSavedSqlHistory } from "@/lib/savedSql/savedSqlHistory";
@@ -3833,6 +3834,14 @@ async function handleKeydown(e: KeyboardEvent) {
return;
}
}
if (isEditTableStructureShortcut(e, shortcuts) && !showSettingsPage.value && !showPluginCenter.value && !showDriverStore.value) {
const target = e.target instanceof Element ? e.target : null;
if (!target?.closest('[role="dialog"], [role="alertdialog"]') && !eventTargetAllowsNativeClipboard(e) && contentAreaRef.value?.openTableStructureEditor?.()) {
e.preventDefault();
e.stopPropagation();
return;
}
}
// Grid-scoped shortcuts normally win inside DataGrid. Keep that precedence
// for a data tab even when focus is in a sibling input, where the grid's
// local listener intentionally leaves native editing untouched.
@@ -9786,10 +9786,9 @@ async function onGridKeydown(event: KeyboardEvent) {
}
const targetAllowsNativeClipboard = eventTargetAllowsNativeClipboard(event);
if (!targetAllowsNativeClipboard && props.context === "table-data" && canOpenTableStructureEditor.value && isEditTableStructureShortcut(event, settingsStore.editorSettings.shortcuts)) {
if (!targetAllowsNativeClipboard && props.context === "table-data" && isEditTableStructureShortcut(event, settingsStore.editorSettings.shortcuts) && openTableStructureEditor("columns")) {
event.preventDefault();
event.stopPropagation();
openTableStructureEditor("columns");
return;
}
if (!targetAllowsNativeClipboard && isGoToColumnShortcut(event, settingsStore.editorSettings.shortcuts) && openGoToColumn()) {
@@ -11898,9 +11897,10 @@ function copyDdl() {
copyText(ddlContent.value);
}
function openTableStructureEditor(initialTab: TableInfoTab) {
if (!props.connectionId || !props.database || !props.tableMeta?.tableName || !canOpenTableStructureEditor.value) return;
function openTableStructureEditor(initialTab: TableInfoTab = "columns"): boolean {
if (!props.connectionId || !props.database || !props.tableMeta?.tableName || !canOpenTableStructureEditor.value) return false;
queryStore.openTableStructure(props.connectionId, props.database, props.tableMeta.schema, props.tableMeta.tableName, initialTab, undefined, props.tableMeta.catalog, (props.tableMeta.tableType || "").toUpperCase() === "VIEW" ? "view" : "table");
return true;
}
function toggleDdlWrap() {
@@ -95,7 +95,7 @@ function mountGrid(
};
const host = document.createElement("div");
const grid = ref<{ canOpenTableStructureEditor: boolean; openTableStructureEditor: (tab: TableInfoTab) => void }>();
const grid = ref<{ canOpenTableStructureEditor: boolean; openTableStructureEditor: (tab?: TableInfoTab) => boolean }>();
document.body.append(host);
const Root = defineComponent({
setup() {
@@ -160,10 +160,18 @@ describe("DataGrid edit-table-structure shortcut", () => {
await settle();
expect(host.querySelector("[data-edit-table-structure]")).toBeNull();
expect(grid.value?.canOpenTableStructureEditor).toBe(true);
grid.value!.openTableStructureEditor("columns");
expect(grid.value!.openTableStructureEditor()).toBe(true);
expect(openTableStructure).toHaveBeenCalledExactlyOnceWith("connection-1", "app", "public", "users", "columns", undefined, "warehouse", "table");
});
it("returns false from openTableStructureEditor when table structure editing is not available", async () => {
const { grid, openTableStructure } = mountGrid({ databaseType: "redis" });
await settle();
expect(grid.value?.canOpenTableStructureEditor).toBe(false);
expect(grid.value!.openTableStructureEditor()).toBe(false);
expect(openTableStructure).not.toHaveBeenCalled();
});
it("opens the existing structure editor route for an eligible table-data grid", async () => {
const { host, openTableStructure } = mountGrid();
await settle();
@@ -222,7 +222,7 @@ type DataGridHandle = DataGridColumnLayoutHandle & {
showDdl: boolean;
toggleDdl: (tab?: TableInfoTab) => void;
canOpenTableStructureEditor: boolean;
openTableStructureEditor: (tab: TableInfoTab) => void;
openTableStructureEditor: (tab?: TableInfoTab) => boolean;
multiRowTranspose: boolean;
setMultiRowTranspose: (value: boolean) => void;
exportCsv: () => Promise<void>;
@@ -1302,6 +1302,11 @@ function openGoToColumn(): boolean {
return dataGridRef.value?.openGoToColumn() ?? false;
}
function openTableStructureEditor(initialTab: TableInfoTab = "columns"): boolean {
if (props.activeTab.mode !== "data") return false;
return dataGridRef.value?.openTableStructureEditor?.(initialTab) ?? false;
}
function refreshQueryEditorCompletionCache(): boolean {
if (props.activeTab.mode !== "query" || !queryEditorRef.value) return false;
queryEditorRef.value.refreshCompletionCache();
@@ -1786,6 +1791,7 @@ defineExpose({
focusSearch,
focusWhere,
openGoToColumn,
openTableStructureEditor,
refreshData,
toggleResultsPane,
refreshQueryEditorCompletionCache,
@@ -16,7 +16,7 @@ import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { usesProvenReadOnlyStickyTransactionState } from "@/lib/database/databaseFeatureSupport";
import { GROUP_TAB_BAR_PORTAL } from "./groupTabBarPortal";
import type { ContentAreaSurfaceEmits, ContentAreaSurfaceProps, QueryEditorSurfaceHandle, StatementRange } from "./querySurfaces";
import type { QueryTab } from "@/types/database";
import type { QueryTab, TableInfoTab } from "@/types/database";
import type { AiConversationBinding } from "@/lib/ai/aiConversationBinding";
defineOptions({ inheritAttrs: false });
@@ -73,6 +73,7 @@ defineExpose({
focusSearch: (target: Element | null = null) => activeSurfaceRef.value?.focusSearch(target) ?? false,
focusWhere: () => activeSurfaceRef.value?.focusWhere() ?? false,
openGoToColumn: () => activeSurfaceRef.value?.openGoToColumn() ?? false,
openTableStructureEditor: (initialTab?: TableInfoTab) => activeSurfaceRef.value?.openTableStructureEditor?.(initialTab) ?? false,
refreshData: () => activeSurfaceRef.value?.refreshData() ?? false,
toggleResultsPane: () => activeSurfaceRef.value?.toggleResultsPane() ?? false,
refreshQueryEditorCompletionCache: () => activeSurfaceRef.value?.refreshQueryEditorCompletionCache() ?? false,
@@ -4,6 +4,7 @@ import ContentArea from "./ContentArea.vue";
import { createContentSurfaceEventForwarders } from "@/lib/tabs/contentSurfaceEvents";
import type { ContentAreaSurfaceEmits, ContentAreaSurfaceProps, QueryEditorSurfaceHandle, StatementRange } from "./querySurfaces";
import type { AiConversationBinding } from "@/lib/ai/aiConversationBinding";
import type { TableInfoTab } from "@/types/database";
const props = defineProps<ContentAreaSurfaceProps & { autoFocus?: boolean }>();
const emit = defineEmits<ContentAreaSurfaceEmits>();
@@ -26,6 +27,7 @@ defineExpose<QueryEditorSurfaceHandle>({
focusSearch: (target: Element | null = null) => contentAreaRef.value?.focusSearch(target) ?? false,
focusWhere: () => contentAreaRef.value?.focusWhere() ?? false,
openGoToColumn: () => contentAreaRef.value?.openGoToColumn() ?? false,
openTableStructureEditor: (initialTab?: TableInfoTab) => contentAreaRef.value?.openTableStructureEditor?.(initialTab) ?? false,
refreshData: () => contentAreaRef.value?.refreshData() ?? false,
toggleResultsPane: () => contentAreaRef.value?.toggleResultsPane() ?? false,
refreshQueryEditorCompletionCache: () => contentAreaRef.value?.refreshQueryEditorCompletionCache() ?? false,
@@ -15,7 +15,7 @@ import EditorGroup from "./EditorGroup.vue";
import QueryResultSurface from "./QueryResultSurface.vue";
import { createContentSurfaceEventForwarders } from "@/lib/tabs/contentSurfaceEvents";
import type { ContentAreaSurfaceEmits, ContentAreaSurfaceProps, StatementRange } from "./querySurfaces";
import type { QueryTab } from "@/types/database";
import type { QueryTab, TableInfoTab } from "@/types/database";
import type { AiConversationBinding } from "@/lib/ai/aiConversationBinding";
defineOptions({ inheritAttrs: false });
@@ -74,6 +74,7 @@ defineExpose({
},
focusWhere: () => activeEditorGroup()?.focusWhere() ?? false,
openGoToColumn: () => activeEditorGroup()?.openGoToColumn() ?? false,
openTableStructureEditor: (initialTab?: TableInfoTab) => activeEditorGroup()?.openTableStructureEditor?.(initialTab) ?? false,
refreshData: (target: Element | null = null) => {
const element = commandTargetElement(target);
if (element?.closest("[data-shared-result-surface]")) {
@@ -1,13 +1,13 @@
// @vitest-environment happy-dom
import { createApp, h, nextTick, reactive, type App } from "vue";
import { createApp, h, nextTick, reactive, ref, type App } from "vue";
import { createPinia, setActivePinia } from "pinia";
import { createI18n } from "vue-i18n";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ConnectionConfig, QueryTab } from "@/types/database";
vi.mock("@/components/editor/QueryEditor.vue", () => ({ default: { render: () => null } }));
const mocks = vi.hoisted(() => ({ openTableStructureEditor: vi.fn() }));
const mocks = vi.hoisted(() => ({ openTableStructureEditor: vi.fn(() => true) }));
vi.mock("@/components/grid/DataGrid.vue", () => ({
__esModule: true,
default: {
@@ -62,9 +62,11 @@ async function mountDataTab() {
store.switchTab(tab.id);
const host = document.createElement("div");
document.body.appendChild(host);
const contentAreaRef = ref<InstanceType<typeof ContentArea> | null>(null);
const app = createApp({
setup: () => () =>
h(ContentArea, {
ref: contentAreaRef,
activeTab: tab,
activeConnection: connection,
activeOutputView: "result",
@@ -81,10 +83,21 @@ async function mountDataTab() {
app.mount(host);
mounted.push({ app, host });
await nextTick();
return { host, store, tab };
return { host, store, tab, contentAreaRef };
}
describe("table data header navigation", () => {
it("delegates openTableStructureEditor to the data grid for active table data tabs", async () => {
const { contentAreaRef, tab } = await mountDataTab();
tab.result = { columns: [], rows: [], affected_rows: 0, execution_time_ms: 0 };
await vi.waitFor(() => expect(contentAreaRef.value?.openTableStructureEditor()).toBe(true));
expect(mocks.openTableStructureEditor).toHaveBeenCalledWith("columns");
tab.mode = "query";
await nextTick();
expect(contentAreaRef.value?.openTableStructureEditor()).toBe(false);
});
it("opens the columns editor from the header beside the table controls", async () => {
const { host, tab } = await mountDataTab();
tab.result = { columns: [], rows: [], affected_rows: 0, execution_time_ms: 0 };
@@ -19,6 +19,7 @@ const editorPreviewCalls = vi.hoisted(() => [] as Array<{ tabId: string; range:
const editorFocusCalls = vi.hoisted(() => [] as Array<{ tabId: string; range: unknown }>);
const groupHandleModRCalls = vi.hoisted(() => [] as Element[]);
const groupFocusWhereCalls = vi.hoisted(() => [] as string[]);
const groupOpenTableStructureEditorCalls = vi.hoisted(() => [] as string[]);
const groupFocusSearchCalls = vi.hoisted(() => [] as Array<Element | null>);
const resultHandleModRCalls = vi.hoisted(() => [] as Element[]);
const resultFocusSearchCalls = vi.hoisted(() => [] as boolean[]);
@@ -46,6 +47,10 @@ vi.mock("@/components/layout/EditorGroup.vue", () => ({
groupFocusWhereCalls.push(this.groupId);
return true;
},
openTableStructureEditor(this: { groupId: string }) {
groupOpenTableStructureEditorCalls.push(this.groupId);
return true;
},
focusSearch(target?: Element | null) {
groupFocusSearchCalls.push(target ?? null);
return true;
@@ -163,6 +168,7 @@ describe("SqlEditorWorkspace mount contract", () => {
groupHandleModRCalls.length = 0;
groupFocusSearchCalls.length = 0;
groupFocusWhereCalls.length = 0;
groupOpenTableStructureEditorCalls.length = 0;
resultHandleModRCalls.length = 0;
resultFocusSearchCalls.length = 0;
groupExecutionCalls.capture.length = 0;
@@ -744,16 +750,20 @@ describe("SqlEditorWorkspace mount contract", () => {
});
app.use(pinia);
app.use(i18n);
const vm = app.mount(host) as unknown as { focusWhere: () => boolean };
const vm = app.mount(host) as unknown as { focusWhere: () => boolean; openTableStructureEditor: () => boolean };
try {
await nextTick();
expect(vm.focusWhere()).toBe(true);
expect(groupFocusWhereCalls).toEqual(["g1"]);
expect(vm.openTableStructureEditor()).toBe(true);
expect(groupOpenTableStructureEditorCalls).toEqual(["g1"]);
store.focusedGroupId = "g2";
store.activeTabId = "data-b";
await nextTick();
expect(vm.focusWhere()).toBe(true);
expect(groupFocusWhereCalls).toEqual(["g1", "g2"]);
expect(vm.openTableStructureEditor()).toBe(true);
expect(groupOpenTableStructureEditorCalls).toEqual(["g1", "g2"]);
} finally {
app.unmount();
host.remove();
@@ -1,4 +1,4 @@
import type { ConnectionConfig, ObjectBrowserFilter, ObjectBrowserViewport, QueryTab, TabOutputView } from "@/types/database";
import type { ConnectionConfig, ObjectBrowserFilter, ObjectBrowserViewport, QueryTab, TabOutputView, TableInfoTab } from "@/types/database";
import type { NavigationTarget } from "@/composables/useNavigationTargets";
import type { DataGridReloadIntent } from "@/lib/dataGrid/dataGridToolbar";
import type { DataGridSortMode } from "@/lib/dataGrid/dataGridSort";
@@ -15,6 +15,7 @@ export interface QueryEditorSurfaceHandle {
focusSearch(target?: Element | null): boolean;
focusWhere(): boolean;
openGoToColumn(): boolean;
openTableStructureEditor?(initialTab?: TableInfoTab): boolean;
refreshData(target?: Element | null): boolean;
toggleResultsPane(): boolean;
refreshQueryEditorCompletionCache(): boolean;