mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
fix(grid): allow edit table structure shortcut without clicking grid
This commit is contained in:
@@ -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() {
|
||||
|
||||
+10
-2
@@ -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]")) {
|
||||
|
||||
+16
-3
@@ -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 };
|
||||
|
||||
+11
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user