fix(sidebar): preserve existing table data tabs
This commit is contained in:
parent
61f442f9b9
commit
678033c82e
|
|
@ -89,7 +89,7 @@ import {
|
||||||
import { copyNameForTreeNode, isDocumentBrowserTreeNode, objectSourceKindForTreeNode, shouldRunTreeNodeRowAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "@/lib/sidebar/treeNodeClick";
|
import { copyNameForTreeNode, isDocumentBrowserTreeNode, objectSourceKindForTreeNode, shouldRunTreeNodeRowAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "@/lib/sidebar/treeNodeClick";
|
||||||
import { dataTabOpenModeFromTreeClick, type DataTabOpenMode } from "@/lib/sidebar/dataTabOpenPolicy";
|
import { dataTabOpenModeFromTreeClick, type DataTabOpenMode } from "@/lib/sidebar/dataTabOpenPolicy";
|
||||||
import { isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts";
|
import { isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||||
import { canRefreshDataTableFromSingleActivationDoubleClick, dataTableDoubleClickAction } from "@/lib/tabs/dataTabActivation";
|
import { dataTableDoubleClickAction } from "@/lib/tabs/dataTabActivation";
|
||||||
import { attachedDatabaseNameFromPath, buildCreateDatabaseSql, buildDuckDbAttachDatabaseSql, buildSqliteAttachDatabaseSql, supportsCreateDatabaseCharset, uniqueAttachedDatabaseName } from "@/lib/database/createDatabaseSql";
|
import { attachedDatabaseNameFromPath, buildCreateDatabaseSql, buildDuckDbAttachDatabaseSql, buildSqliteAttachDatabaseSql, supportsCreateDatabaseCharset, uniqueAttachedDatabaseName } from "@/lib/database/createDatabaseSql";
|
||||||
import { appendCreateDatabaseErrorHint } from "@/lib/database/createDatabaseErrorHints";
|
import { appendCreateDatabaseErrorHint } from "@/lib/database/createDatabaseErrorHints";
|
||||||
import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection";
|
import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection";
|
||||||
|
|
@ -671,9 +671,6 @@ function runRowClickAction(clickDetail: number) {
|
||||||
const action = treeNodeRowAction(node.type, canExpand.value, settingsStore.editorSettings.sidebarActivation);
|
const action = treeNodeRowAction(node.type, canExpand.value, settingsStore.editorSettings.sidebarActivation);
|
||||||
if (!shouldRunTreeNodeRowAction(action, clickDetail)) return;
|
if (!shouldRunTreeNodeRowAction(action, clickDetail)) return;
|
||||||
if (action === "open-data") {
|
if (action === "open-data") {
|
||||||
if (node.type === "table") {
|
|
||||||
singleActivationDoubleClickRefreshAllowed = canRefreshDataTableFromSingleActivationDoubleClick(findExistingSameTableDataTab());
|
|
||||||
}
|
|
||||||
scheduleOpenData(node);
|
scheduleOpenData(node);
|
||||||
} else if (isDocumentBrowserTreeNode(node.type)) {
|
} else if (isDocumentBrowserTreeNode(node.type)) {
|
||||||
openMongoTreeData(node);
|
openMongoTreeData(node);
|
||||||
|
|
@ -684,8 +681,6 @@ function runRowClickAction(clickDetail: number) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let singleActivationDoubleClickRefreshAllowed = false;
|
|
||||||
|
|
||||||
function refreshActiveKvBrowserAfterOpen(mode: "etcd" | "zookeeper", connectionId: string) {
|
function refreshActiveKvBrowserAfterOpen(mode: "etcd" | "zookeeper", connectionId: string) {
|
||||||
void nextTick(() => {
|
void nextTick(() => {
|
||||||
window.dispatchEvent(new CustomEvent("dbx-refresh-active-kv-browser", { detail: { mode, connectionId } }));
|
window.dispatchEvent(new CustomEvent("dbx-refresh-active-kv-browser", { detail: { mode, connectionId } }));
|
||||||
|
|
@ -919,8 +914,8 @@ function onDoubleClick(event: MouseEvent) {
|
||||||
if (!activeNode.value.isExpanded) void toggle();
|
if (!activeNode.value.isExpanded) void toggle();
|
||||||
} else if (action === "open-data") {
|
} else if (action === "open-data") {
|
||||||
openDataImmediately(activeNode.value);
|
openDataImmediately(activeNode.value);
|
||||||
} else if (action === "refresh-data") {
|
} else if (action === "activate-data") {
|
||||||
void refreshData();
|
activateDataTableFromDoubleClick();
|
||||||
} else if (action === "open-source") {
|
} else if (action === "open-source") {
|
||||||
openObjectSourceDialog(false);
|
openObjectSourceDialog(false);
|
||||||
} else if (action === "open-saved-sql") {
|
} else if (action === "open-saved-sql") {
|
||||||
|
|
@ -932,24 +927,21 @@ function onDoubleClick(event: MouseEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshData() {
|
function activateDataTableFromDoubleClick() {
|
||||||
const node = activeNode.value;
|
const node = activeNode.value;
|
||||||
if (node.type !== "table" || !hasNodeDatabaseContext(node)) return;
|
if (node.type !== "table" || !hasNodeDatabaseContext(node)) return;
|
||||||
const singleActivationRefreshAllowed = singleActivationDoubleClickRefreshAllowed;
|
|
||||||
singleActivationDoubleClickRefreshAllowed = false;
|
|
||||||
const activation = settingsStore.editorSettings.sidebarActivation;
|
const activation = settingsStore.editorSettings.sidebarActivation;
|
||||||
if (activation === "single" && !singleActivationRefreshAllowed) return;
|
|
||||||
const existingSameTableTab = findExistingSameTableDataTab();
|
const existingSameTableTab = findExistingSameTableDataTab();
|
||||||
const action = dataTableDoubleClickAction(existingSameTableTab, activation, singleActivationRefreshAllowed);
|
const action = dataTableDoubleClickAction(existingSameTableTab, activation);
|
||||||
if (action === "none") return;
|
if (action === "none") return;
|
||||||
if (action === "open") {
|
if (action === "open") {
|
||||||
openDataImmediately(node);
|
openDataImmediately(node);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!existingSameTableTab) return;
|
if (!existingSameTableTab) return;
|
||||||
|
// Reopening an available table follows DBeaver's editor reuse model: only
|
||||||
|
// activate the existing tab so filters, result rows, and in-flight work stay intact.
|
||||||
queryStore.switchTab(existingSameTableTab.id);
|
queryStore.switchTab(existingSameTableTab.id);
|
||||||
if (action === "activate") return;
|
|
||||||
await queryStore.refreshDataTab(existingSameTableTab.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findExistingSameTableDataTab() {
|
function findExistingSameTableDataTab() {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import type { ObjectSourceKind, TreeNode, TreeNodeType } from "@/types/database"
|
||||||
import { matchesShortcut, type ShortcutLikeEvent } from "@/lib/editor/keyboardShortcuts";
|
import { matchesShortcut, type ShortcutLikeEvent } from "@/lib/editor/keyboardShortcuts";
|
||||||
|
|
||||||
export type TreeNodeRowAction = "open-data" | "toggle" | "none";
|
export type TreeNodeRowAction = "open-data" | "toggle" | "none";
|
||||||
export type TreeNodeRowDoubleClickAction = "open-data" | "refresh-data" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-saved-sql" | "toggle" | "none";
|
export type TreeNodeRowDoubleClickAction = "open-data" | "activate-data" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-saved-sql" | "toggle" | "none";
|
||||||
export type SidebarSelectionCopyAction = "copy-name" | "none";
|
export type SidebarSelectionCopyAction = "copy-name" | "none";
|
||||||
export type SidebarActivation = "single" | "double";
|
export type SidebarActivation = "single" | "double";
|
||||||
|
|
||||||
|
|
@ -48,7 +48,9 @@ export function shouldRunTreeNodeRowAction(action: TreeNodeRowAction, clickDetai
|
||||||
}
|
}
|
||||||
|
|
||||||
export function treeNodeRowDoubleClickAction(type: TreeNodeType, canOpenObjectBrowser: boolean, activation: SidebarActivation = "single", canExpand = false): TreeNodeRowDoubleClickAction {
|
export function treeNodeRowDoubleClickAction(type: TreeNodeType, canOpenObjectBrowser: boolean, activation: SidebarActivation = "single", canExpand = false): TreeNodeRowDoubleClickAction {
|
||||||
if (type === "table") return "refresh-data";
|
// Single-click activation already handles the first click in a dblclick
|
||||||
|
// sequence. Only double-click activation needs a second-stage table action.
|
||||||
|
if (type === "table") return activation === "double" ? "activate-data" : "none";
|
||||||
if (activation === "double") {
|
if (activation === "double") {
|
||||||
if (dataNodeTypes.has(type)) return "open-data";
|
if (dataNodeTypes.has(type)) return "open-data";
|
||||||
if (sourceNodeTypes.has(type)) return "open-source";
|
if (sourceNodeTypes.has(type)) return "open-source";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { QueryResult, QueryTab } from "@/types/database";
|
import type { QueryResult, QueryTab } from "@/types/database";
|
||||||
|
|
||||||
export type DataTableDoubleClickAction = "activate" | "open" | "refresh" | "none";
|
export type DataTableDoubleClickAction = "activate" | "open" | "none";
|
||||||
|
|
||||||
function isErrorResult(result: QueryResult | undefined): boolean {
|
function isErrorResult(result: QueryResult | undefined): boolean {
|
||||||
return result?.columns.length === 1 && result.columns[0] === "Error";
|
return result?.columns.length === 1 && result.columns[0] === "Error";
|
||||||
|
|
@ -12,13 +12,9 @@ export function canActivateExistingDataTableTab(tab: QueryTab, options: { activa
|
||||||
return !!tab.result || !!tab.results?.length;
|
return !!tab.result || !!tab.results?.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canRefreshDataTableFromSingleActivationDoubleClick(tab: QueryTab | undefined): boolean {
|
export function dataTableDoubleClickAction(tab: QueryTab | undefined, activation: "single" | "double"): DataTableDoubleClickAction {
|
||||||
return !!tab && !tab.isExecuting && canActivateExistingDataTableTab(tab);
|
if (activation === "single") return "none";
|
||||||
}
|
|
||||||
|
|
||||||
export function dataTableDoubleClickAction(tab: QueryTab | undefined, activation: "single" | "double", singleActivationRefreshAllowed = false): DataTableDoubleClickAction {
|
|
||||||
if (activation === "single" && !singleActivationRefreshAllowed) return "none";
|
|
||||||
if (!tab) return activation === "double" ? "open" : "none";
|
if (!tab) return activation === "double" ? "open" : "none";
|
||||||
if (!canActivateExistingDataTableTab(tab)) return "open";
|
if (!canActivateExistingDataTableTab(tab)) return "open";
|
||||||
return tab.isExecuting ? "activate" : "refresh";
|
return "activate";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { strict as assert } from "node:assert";
|
import { strict as assert } from "node:assert";
|
||||||
import { test } from "vitest";
|
import { test } from "vitest";
|
||||||
import { canActivateExistingDataTableTab, canRefreshDataTableFromSingleActivationDoubleClick, dataTableDoubleClickAction } from "../../apps/desktop/src/lib/tabs/dataTabActivation.ts";
|
import { canActivateExistingDataTableTab, dataTableDoubleClickAction } from "../../apps/desktop/src/lib/tabs/dataTabActivation.ts";
|
||||||
import type { QueryTab } from "../../apps/desktop/src/types/database.ts";
|
import type { QueryTab } from "../../apps/desktop/src/types/database.ts";
|
||||||
|
|
||||||
function dataTab(overrides: Partial<QueryTab> = {}): QueryTab {
|
function dataTab(overrides: Partial<QueryTab> = {}): QueryTab {
|
||||||
|
|
@ -62,25 +62,11 @@ test("reloads existing data table tabs showing an error result", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("single activation snapshots only successful idle tabs as refreshable", () => {
|
test("single activation leaves double click handling to the first click", () => {
|
||||||
assert.equal(canRefreshDataTableFromSingleActivationDoubleClick(undefined), false);
|
assert.equal(dataTableDoubleClickAction(undefined, "single"), "none");
|
||||||
assert.equal(canRefreshDataTableFromSingleActivationDoubleClick(dataTab()), false);
|
assert.equal(dataTableDoubleClickAction(dataTab({ isExecuting: true }), "single"), "none");
|
||||||
assert.equal(canRefreshDataTableFromSingleActivationDoubleClick(dataTab({ isExecuting: true })), false);
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
canRefreshDataTableFromSingleActivationDoubleClick(
|
dataTableDoubleClickAction(
|
||||||
dataTab({
|
|
||||||
result: {
|
|
||||||
columns: ["Error"],
|
|
||||||
rows: [["connection failed"]],
|
|
||||||
affected_rows: 0,
|
|
||||||
execution_time_ms: 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
canRefreshDataTableFromSingleActivationDoubleClick(
|
|
||||||
dataTab({
|
dataTab({
|
||||||
result: {
|
result: {
|
||||||
columns: ["id"],
|
columns: ["id"],
|
||||||
|
|
@ -89,41 +75,17 @@ test("single activation snapshots only successful idle tabs as refreshable", ()
|
||||||
execution_time_ms: 1,
|
execution_time_ms: 1,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
"single",
|
||||||
),
|
),
|
||||||
true,
|
"none",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("single activation uses missing, restored, error, and busy first-click snapshots even if the tab succeeds before dblclick", () => {
|
|
||||||
const successfulAtDoubleClick = dataTab({
|
|
||||||
result: {
|
|
||||||
columns: ["id"],
|
|
||||||
rows: [[1]],
|
|
||||||
affected_rows: 0,
|
|
||||||
execution_time_ms: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const errorAtFirstClick = dataTab({
|
|
||||||
result: {
|
|
||||||
columns: ["Error"],
|
|
||||||
rows: [["connection failed"]],
|
|
||||||
affected_rows: 0,
|
|
||||||
execution_time_ms: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
for (const initialTab of [undefined, dataTab(), errorAtFirstClick, dataTab({ isExecuting: true })]) {
|
|
||||||
const refreshAllowed = canRefreshDataTableFromSingleActivationDoubleClick(initialTab);
|
|
||||||
assert.equal(dataTableDoubleClickAction(successfulAtDoubleClick, "single", refreshAllowed), "none");
|
|
||||||
}
|
|
||||||
const refreshAllowed = canRefreshDataTableFromSingleActivationDoubleClick(successfulAtDoubleClick);
|
|
||||||
assert.equal(dataTableDoubleClickAction(successfulAtDoubleClick, "single", refreshAllowed), "refresh");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("double activation opens a missing table without a first-click snapshot", () => {
|
test("double activation opens a missing table without a first-click snapshot", () => {
|
||||||
assert.equal(dataTableDoubleClickAction(undefined, "double"), "open");
|
assert.equal(dataTableDoubleClickAction(undefined, "double"), "open");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("double activation decisions preserve loading, refresh, and recovery behavior", () => {
|
test("double activation reuses loading and successful tabs without refreshing", () => {
|
||||||
assert.equal(dataTableDoubleClickAction(dataTab({ isExecuting: true }), "double"), "activate");
|
assert.equal(dataTableDoubleClickAction(dataTab({ isExecuting: true }), "double"), "activate");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
dataTableDoubleClickAction(
|
dataTableDoubleClickAction(
|
||||||
|
|
@ -137,8 +99,11 @@ test("double activation decisions preserve loading, refresh, and recovery behavi
|
||||||
}),
|
}),
|
||||||
"double",
|
"double",
|
||||||
),
|
),
|
||||||
"refresh",
|
"activate",
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("double activation preserves restored and error recovery behavior", () => {
|
||||||
assert.equal(dataTableDoubleClickAction(dataTab(), "double"), "open");
|
assert.equal(dataTableDoubleClickAction(dataTab(), "double"), "open");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
dataTableDoubleClickAction(
|
dataTableDoubleClickAction(
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,12 @@ test("double click navigation mode selects rows on single click", () => {
|
||||||
assert.equal(treeNodeRowAction("saved-sql-file", false, "double"), "none");
|
assert.equal(treeNodeRowAction("saved-sql-file", false, "double"), "none");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("table rows refresh on double click in both navigation modes", () => {
|
test("table double click avoids a second action in single activation mode", () => {
|
||||||
assert.equal(treeNodeRowDoubleClickAction("table", true, "single"), "refresh-data");
|
assert.equal(treeNodeRowDoubleClickAction("table", true, "single"), "none");
|
||||||
assert.equal(treeNodeRowDoubleClickAction("table", true, "double"), "refresh-data");
|
});
|
||||||
|
|
||||||
|
test("table double click activates data in double activation mode", () => {
|
||||||
|
assert.equal(treeNodeRowDoubleClickAction("table", true, "double"), "activate-data");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("double click navigation mode opens other actionable rows on double click", () => {
|
test("double click navigation mode opens other actionable rows on double click", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue