feat(sidebar): refresh data tables on double click
This commit is contained in:
parent
020d9869ac
commit
f9bf039aef
|
|
@ -107,7 +107,7 @@ import { isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPast
|
|||
import { formatSqlInsert } from "@/lib/export/exportFormats";
|
||||
import { joinExportedDdls } from "@/lib/export/ddlExport";
|
||||
import { fetchTableDataForExport } from "@/lib/table/tableDataExport";
|
||||
import { canActivateExistingDataTableTab } from "@/lib/tabs/dataTabActivation";
|
||||
import { canActivateExistingDataTableTab, canRefreshDataTableFromSingleActivationDoubleClick, dataTableDoubleClickAction } from "@/lib/tabs/dataTabActivation";
|
||||
import { buildCreateDatabaseSql, buildDuckDbAttachDatabaseSql, duckDbAttachedDatabaseNameFromPath, supportsCreateDatabaseCharset, uniqueDuckDbAttachedDatabaseName } from "@/lib/database/createDatabaseSql";
|
||||
import {
|
||||
buildCreateSchemaSql,
|
||||
|
|
@ -838,6 +838,9 @@ function runRowClickAction(clickDetail: number) {
|
|||
const action = treeNodeRowAction(node.type, canExpand.value, settingsStore.editorSettings.sidebarActivation);
|
||||
if (!shouldRunTreeNodeRowAction(action, clickDetail)) return;
|
||||
if (action === "open-data") {
|
||||
if (node.type === "table") {
|
||||
singleActivationDoubleClickRefreshAllowed = canRefreshDataTableFromSingleActivationDoubleClick(findExistingSameTableDataTab());
|
||||
}
|
||||
scheduleOpenData(node);
|
||||
} else if (isDocumentBrowserTreeNode(node.type)) {
|
||||
openMongoTreeData(node);
|
||||
|
|
@ -848,6 +851,8 @@ function runRowClickAction(clickDetail: number) {
|
|||
}
|
||||
}
|
||||
|
||||
let singleActivationDoubleClickRefreshAllowed = false;
|
||||
|
||||
function refreshActiveKvBrowserAfterOpen(mode: "etcd" | "zookeeper", connectionId: string) {
|
||||
void nextTick(() => {
|
||||
window.dispatchEvent(new CustomEvent("dbx-refresh-active-kv-browser", { detail: { mode, connectionId } }));
|
||||
|
|
@ -972,6 +977,7 @@ function toggleConnectionMultiSelection(event: MouseEvent) {
|
|||
}
|
||||
|
||||
function onClick(event: MouseEvent) {
|
||||
if (props.node.type === "table" && event.detail <= 1) singleActivationDoubleClickRefreshAllowed = false;
|
||||
if (suppressNextTableReferenceClick) {
|
||||
suppressNextTableReferenceClick = false;
|
||||
event.preventDefault();
|
||||
|
|
@ -1193,6 +1199,8 @@ function onDoubleClick() {
|
|||
if (!props.node.isExpanded) void toggle();
|
||||
} else if (action === "open-data") {
|
||||
openDataImmediately(props.node);
|
||||
} else if (action === "refresh-data") {
|
||||
void refreshData();
|
||||
} else if (action === "open-source") {
|
||||
openObjectSourceDialog(false);
|
||||
} else if (action === "open-saved-sql") {
|
||||
|
|
@ -1204,6 +1212,34 @@ function onDoubleClick() {
|
|||
}
|
||||
}
|
||||
|
||||
async function refreshData() {
|
||||
const node = props.node;
|
||||
if (node.type !== "table" || !hasNodeDatabaseContext(node)) return;
|
||||
const singleActivationRefreshAllowed = singleActivationDoubleClickRefreshAllowed;
|
||||
singleActivationDoubleClickRefreshAllowed = false;
|
||||
const activation = settingsStore.editorSettings.sidebarActivation;
|
||||
if (activation === "single" && !singleActivationRefreshAllowed) return;
|
||||
const existingSameTableTab = findExistingSameTableDataTab();
|
||||
const action = dataTableDoubleClickAction(existingSameTableTab, activation, singleActivationRefreshAllowed);
|
||||
if (action === "none") return;
|
||||
if (action === "open") {
|
||||
openDataImmediately(node);
|
||||
return;
|
||||
}
|
||||
if (!existingSameTableTab) return;
|
||||
queryStore.switchTab(existingSameTableTab.id);
|
||||
if (action === "activate") return;
|
||||
await queryStore.refreshDataTab(existingSameTableTab.id);
|
||||
}
|
||||
|
||||
function findExistingSameTableDataTab() {
|
||||
const node = props.node;
|
||||
if (node.type !== "table" || !hasNodeDatabaseContext(node)) return undefined;
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
const tableSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema);
|
||||
return queryStore.tabs.find((tab) => tab.mode === "data" && tab.connectionId === node.connectionId && tab.database === node.database && (tab.tableMeta?.catalog || "") === (node.catalog || "") && (tab.schema || "") === (tableSchema || "") && (tab.tableMeta?.tableName || tab.title) === node.label);
|
||||
}
|
||||
|
||||
function openMongoTreeData(node: TreeNode) {
|
||||
if (!node.connectionId || !node.database) return;
|
||||
if (node.type === "mongo-gridfs") {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { ObjectSourceKind, TreeNode, TreeNodeType } from "@/types/database"
|
|||
import { matchesShortcut, type ShortcutLikeEvent } from "@/lib/editor/keyboardShortcuts";
|
||||
|
||||
export type TreeNodeRowAction = "open-data" | "toggle" | "none";
|
||||
export type TreeNodeRowDoubleClickAction = "open-data" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-saved-sql" | "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 SidebarSelectionCopyAction = "copy-name" | "none";
|
||||
export type SidebarActivation = "single" | "double";
|
||||
|
||||
|
|
@ -45,6 +45,7 @@ export function shouldRunTreeNodeRowAction(action: TreeNodeRowAction, clickDetai
|
|||
}
|
||||
|
||||
export function treeNodeRowDoubleClickAction(type: TreeNodeType, canOpenObjectBrowser: boolean, activation: SidebarActivation = "single", canExpand = false): TreeNodeRowDoubleClickAction {
|
||||
if (type === "table") return "refresh-data";
|
||||
if (activation === "double") {
|
||||
if (dataNodeTypes.has(type)) return "open-data";
|
||||
if (sourceNodeTypes.has(type)) return "open-source";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import type { QueryResult, QueryTab } from "@/types/database";
|
||||
|
||||
export type DataTableDoubleClickAction = "activate" | "open" | "refresh" | "none";
|
||||
|
||||
function isErrorResult(result: QueryResult | undefined): boolean {
|
||||
return result?.columns.length === 1 && result.columns[0] === "Error";
|
||||
}
|
||||
|
|
@ -9,3 +11,14 @@ export function canActivateExistingDataTableTab(tab: QueryTab, options: { activa
|
|||
if (isErrorResult(tab.result)) return false;
|
||||
return !!tab.result || !!tab.results?.length;
|
||||
}
|
||||
|
||||
export function canRefreshDataTableFromSingleActivationDoubleClick(tab: QueryTab | undefined): boolean {
|
||||
return !!tab && !tab.isExecuting && canActivateExistingDataTableTab(tab);
|
||||
}
|
||||
|
||||
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 (!canActivateExistingDataTableTab(tab)) return "open";
|
||||
return tab.isExecuting ? "activate" : "refresh";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,4 +124,147 @@ describe("queryStore table data refresh", () => {
|
|||
expect(store.tabs.find((tab) => tab.id === publicTabId)?.result?.rows).toEqual([]);
|
||||
expect(store.tabs.find((tab) => tab.id === archiveTabId)?.result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("refreshes one targeted tab while preserving its query context", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const firstTabId = store.createTab("pg-1", "app", "users", "data", "public");
|
||||
const secondTabId = store.createTab("pg-1", "app", "users-copy", "data", "public");
|
||||
for (const tabId of [firstTabId, secondTabId]) {
|
||||
store.setTableMeta(tabId, {
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
tableType: "TABLE",
|
||||
columns: [{ name: "id", data_type: "integer", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
|
||||
primaryKeys: ["id"],
|
||||
});
|
||||
}
|
||||
const firstTab = store.tabs.find((tab) => tab.id === firstTabId)!;
|
||||
firstTab.whereInput = "status = 'ACTIVE'";
|
||||
firstTab.resultSortColumn = "created_at";
|
||||
firstTab.resultSortDirection = "desc";
|
||||
firstTab.resultPageLimit = 25;
|
||||
firstTab.resultPageOffset = 50;
|
||||
|
||||
const refreshed = await store.refreshDataTab(firstTabId);
|
||||
|
||||
expect(refreshed).toBe(true);
|
||||
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
whereInput: "status = 'ACTIVE'",
|
||||
orderBy: '"created_at" DESC',
|
||||
limit: 25,
|
||||
offset: 50,
|
||||
}),
|
||||
);
|
||||
expect(mocks.executeMulti).toHaveBeenCalledTimes(1);
|
||||
expect(store.tabs.find((tab) => tab.id === firstTabId)?.result?.rows).toEqual([]);
|
||||
expect(store.tabs.find((tab) => tab.id === secondTabId)?.result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects a repeated refresh while SQL construction is in progress", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("pg-1", "app", "users", "data", "public");
|
||||
store.setTableMeta(tabId, {
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
tableType: "TABLE",
|
||||
columns: [{ name: "id", data_type: "integer", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
|
||||
primaryKeys: ["id"],
|
||||
});
|
||||
let resolveSql!: (sql: string) => void;
|
||||
mocks.buildTableSelectSql.mockReturnValueOnce(new Promise((resolve) => (resolveSql = resolve)));
|
||||
|
||||
const firstRefresh = store.refreshDataTab(tabId);
|
||||
expect(store.tabs.find((tab) => tab.id === tabId)?.isExecuting).toBe(true);
|
||||
await expect(store.refreshDataTab(tabId)).resolves.toBe(false);
|
||||
expect(mocks.buildTableSelectSql).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.executeMulti).not.toHaveBeenCalled();
|
||||
|
||||
resolveSql("SELECT id FROM public.users LIMIT 100 OFFSET 0");
|
||||
await expect(firstRefresh).resolves.toBe(true);
|
||||
expect(mocks.executeMulti).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns false for SQL build failures, stores an error result, and clears the busy state", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("pg-1", "app", "users", "data", "public");
|
||||
store.setTableMeta(tabId, {
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
tableType: "TABLE",
|
||||
columns: [],
|
||||
primaryKeys: [],
|
||||
});
|
||||
mocks.buildTableSelectSql.mockRejectedValueOnce(new Error("failed to build refresh SQL"));
|
||||
|
||||
await expect(store.refreshDataTab(tabId)).resolves.toBe(false);
|
||||
|
||||
const tab = store.tabs.find((candidate) => candidate.id === tabId)!;
|
||||
expect(tab.isExecuting).toBe(false);
|
||||
expect(tab.executionId).toBeUndefined();
|
||||
expect(tab.result?.execution_error).toBe(true);
|
||||
expect(tab.result?.rows).toEqual([["failed to build refresh SQL"]]);
|
||||
expect(mocks.executeMulti).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the bulk refresh supersede and count behavior for busy matching tabs", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("pg-1", "app", "users", "data", "public");
|
||||
store.setTableMeta(tabId, {
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
tableType: "TABLE",
|
||||
columns: [{ name: "id", data_type: "integer", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
|
||||
primaryKeys: ["id"],
|
||||
});
|
||||
const tab = store.tabs.find((candidate) => candidate.id === tabId)!;
|
||||
tab.isExecuting = true;
|
||||
tab.executionId = "previous-execution";
|
||||
|
||||
await expect(
|
||||
store.refreshDataTabsForTable({
|
||||
connectionId: "pg-1",
|
||||
database: "app",
|
||||
schema: "public",
|
||||
name: "users",
|
||||
}),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(mocks.buildTableSelectSql).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.executeMulti).toHaveBeenCalledTimes(1);
|
||||
expect(tab.isExecuting).toBe(false);
|
||||
expect(tab.executionId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps bulk SQL build failures observable to callers", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("pg-1", "app", "users", "data", "public");
|
||||
store.setTableMeta(tabId, {
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
tableType: "TABLE",
|
||||
columns: [],
|
||||
primaryKeys: [],
|
||||
});
|
||||
mocks.buildTableSelectSql.mockRejectedValueOnce(new Error("bulk refresh SQL failed"));
|
||||
|
||||
await expect(
|
||||
store.refreshDataTabsForTable({
|
||||
connectionId: "pg-1",
|
||||
database: "app",
|
||||
schema: "public",
|
||||
name: "users",
|
||||
}),
|
||||
).rejects.toThrow("bulk refresh SQL failed");
|
||||
|
||||
const tab = store.tabs.find((candidate) => candidate.id === tabId)!;
|
||||
expect(tab.isExecuting).toBe(false);
|
||||
expect(tab.result?.execution_error).toBe(true);
|
||||
expect(mocks.executeMulti).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1754,25 +1754,28 @@ export const useQueryStore = defineStore("query", () => {
|
|||
closeTabsWhere((tab) => tabMatchesDroppedTableObject(tab, target));
|
||||
}
|
||||
|
||||
async function refreshDataTabsForTable(target: TableDataRefreshTarget): Promise<number> {
|
||||
const matchingTabs = tabs.value.filter((tab) => tabMatchesTableDataRefreshTarget(tab, target));
|
||||
if (matchingTabs.length === 0) return 0;
|
||||
async function refreshDataTabInternal(id: string, options?: { supersedeBusy?: boolean; propagateBuildError?: boolean }): Promise<boolean> {
|
||||
const tab = tabs.value.find((candidate) => candidate.id === id);
|
||||
if (!tab || tab.mode !== "data" || (tab.isExecuting && !options?.supersedeBusy)) return false;
|
||||
const tableMeta = tableMetaForDataTab(tab);
|
||||
if (!tableMeta?.tableName) return false;
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
let refreshed = 0;
|
||||
const connStore = useConnectionStore();
|
||||
const conn = connStore.getConfig(tab.connectionId);
|
||||
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
|
||||
const identifierQuote = connStore.connectionIdentifierQuote?.(tab.connectionId);
|
||||
const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : tableMeta.primaryKeys;
|
||||
const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableDataIdentifier(effectiveDbType, tab.resultSortColumn, identifierQuote)} ${tab.resultSortDirection.toUpperCase()}` : undefined;
|
||||
const orderBy = tab.orderByInput?.trim() || sortOrder;
|
||||
const limit = tab.resultPageLimit ?? settingsStore.editorSettings.pageSize ?? tableOpenPageLimit();
|
||||
const offset = tab.resultPageOffset ?? 0;
|
||||
const refreshPreparationId = uuid();
|
||||
|
||||
for (const tab of matchingTabs) {
|
||||
const tableMeta = tableMetaForDataTab(tab);
|
||||
if (!tableMeta?.tableName) continue;
|
||||
const connStore = useConnectionStore();
|
||||
const conn = connStore.getConfig(tab.connectionId);
|
||||
const effectiveDbType = effectiveDatabaseTypeForConnection(conn);
|
||||
const identifierQuote = connStore.connectionIdentifierQuote?.(tab.connectionId);
|
||||
const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : tableMeta.primaryKeys;
|
||||
const sortOrder = tab.resultSortColumn && tab.resultSortDirection ? `${quoteTableDataIdentifier(effectiveDbType, tab.resultSortColumn, identifierQuote)} ${tab.resultSortDirection.toUpperCase()}` : undefined;
|
||||
const orderBy = tab.orderByInput?.trim() || sortOrder;
|
||||
const limit = tab.resultPageLimit ?? settingsStore.editorSettings.pageSize ?? tableOpenPageLimit();
|
||||
const offset = tab.resultPageOffset ?? 0;
|
||||
// Reserve the tab synchronously before SQL construction yields so repeated
|
||||
// refresh requests cannot build and execute duplicate queries.
|
||||
setExecutingWithId(tab.id, refreshPreparationId);
|
||||
try {
|
||||
const sql = await buildTableSelectSql({
|
||||
databaseType: effectiveDbType,
|
||||
identifierQuote,
|
||||
|
|
@ -1789,12 +1792,34 @@ export const useQueryStore = defineStore("query", () => {
|
|||
limit,
|
||||
offset,
|
||||
});
|
||||
if (!sql.trim()) throw new Error("Failed to build table refresh SQL");
|
||||
const current = tabs.value.find((candidate) => candidate.id === id);
|
||||
if (!current || current.executionId !== refreshPreparationId) return false;
|
||||
updateSql(tab.id, sql);
|
||||
await executeTabSql(tab.id, sql, {
|
||||
pagination: { limit, offset },
|
||||
preserveResultDuringExecution: true,
|
||||
});
|
||||
refreshed += 1;
|
||||
return true;
|
||||
} catch (error) {
|
||||
const current = tabs.value.find((candidate) => candidate.id === id);
|
||||
if (current?.executionId === refreshPreparationId) setErrorResult(id, error);
|
||||
if (options?.propagateBuildError) throw error;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDataTab(id: string): Promise<boolean> {
|
||||
return refreshDataTabInternal(id);
|
||||
}
|
||||
|
||||
async function refreshDataTabsForTable(target: TableDataRefreshTarget): Promise<number> {
|
||||
const matchingTabs = tabs.value.filter((tab) => tabMatchesTableDataRefreshTarget(tab, target));
|
||||
if (matchingTabs.length === 0) return 0;
|
||||
|
||||
let refreshed = 0;
|
||||
for (const tab of matchingTabs) {
|
||||
if (await refreshDataTabInternal(tab.id, { supersedeBusy: true, propagateBuildError: true })) refreshed += 1;
|
||||
}
|
||||
|
||||
return refreshed;
|
||||
|
|
@ -4036,6 +4061,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
closeConnectionTabs,
|
||||
closeDatabaseTabs,
|
||||
closeDroppedTableObjectTabs,
|
||||
refreshDataTab,
|
||||
refreshDataTabsForTable,
|
||||
releaseConnectionTabs,
|
||||
releaseDatabaseTabs,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { canActivateExistingDataTableTab } from "../../apps/desktop/src/lib/tabs/dataTabActivation.ts";
|
||||
import { canActivateExistingDataTableTab, canRefreshDataTableFromSingleActivationDoubleClick, dataTableDoubleClickAction } from "../../apps/desktop/src/lib/tabs/dataTabActivation.ts";
|
||||
import type { QueryTab } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
function dataTab(overrides: Partial<QueryTab> = {}): QueryTab {
|
||||
|
|
@ -61,3 +61,97 @@ test("reloads existing data table tabs showing an error result", () => {
|
|||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("single activation snapshots only successful idle tabs as refreshable", () => {
|
||||
assert.equal(canRefreshDataTableFromSingleActivationDoubleClick(undefined), false);
|
||||
assert.equal(canRefreshDataTableFromSingleActivationDoubleClick(dataTab()), false);
|
||||
assert.equal(canRefreshDataTableFromSingleActivationDoubleClick(dataTab({ isExecuting: true })), false);
|
||||
assert.equal(
|
||||
canRefreshDataTableFromSingleActivationDoubleClick(
|
||||
dataTab({
|
||||
result: {
|
||||
columns: ["Error"],
|
||||
rows: [["connection failed"]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
},
|
||||
}),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
canRefreshDataTableFromSingleActivationDoubleClick(
|
||||
dataTab({
|
||||
result: {
|
||||
columns: ["id"],
|
||||
rows: [[1]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
},
|
||||
}),
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
assert.equal(dataTableDoubleClickAction(undefined, "double"), "open");
|
||||
});
|
||||
|
||||
test("double activation decisions preserve loading, refresh, and recovery behavior", () => {
|
||||
assert.equal(dataTableDoubleClickAction(dataTab({ isExecuting: true }), "double"), "activate");
|
||||
assert.equal(
|
||||
dataTableDoubleClickAction(
|
||||
dataTab({
|
||||
result: {
|
||||
columns: ["id"],
|
||||
rows: [[1]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
},
|
||||
}),
|
||||
"double",
|
||||
),
|
||||
"refresh",
|
||||
);
|
||||
assert.equal(dataTableDoubleClickAction(dataTab(), "double"), "open");
|
||||
assert.equal(
|
||||
dataTableDoubleClickAction(
|
||||
dataTab({
|
||||
result: {
|
||||
columns: ["Error"],
|
||||
rows: [["connection failed"]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
},
|
||||
}),
|
||||
"double",
|
||||
),
|
||||
"open",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,9 +14,14 @@ test("double click navigation mode selects rows on single click", () => {
|
|||
assert.equal(treeNodeRowAction("saved-sql-file", false, "double"), "none");
|
||||
});
|
||||
|
||||
test("double click navigation mode opens actionable rows on double click", () => {
|
||||
assert.equal(treeNodeRowDoubleClickAction("table", true, "double"), "open-data");
|
||||
test("table rows refresh on double click in both navigation modes", () => {
|
||||
assert.equal(treeNodeRowDoubleClickAction("table", true, "single"), "refresh-data");
|
||||
assert.equal(treeNodeRowDoubleClickAction("table", true, "double"), "refresh-data");
|
||||
});
|
||||
|
||||
test("double click navigation mode opens other actionable rows on double click", () => {
|
||||
assert.equal(treeNodeRowDoubleClickAction("view", true, "double"), "open-data");
|
||||
assert.equal(treeNodeRowDoubleClickAction("materialized_view", true, "double"), "open-data");
|
||||
assert.equal(treeNodeRowDoubleClickAction("procedure", false, "double"), "open-source");
|
||||
assert.equal(treeNodeRowDoubleClickAction("saved-sql-file", false, "double"), "open-saved-sql");
|
||||
});
|
||||
|
|
@ -96,7 +101,8 @@ test("double click navigation mode opens object browser and expands expandable d
|
|||
|
||||
test("double click does not open object browser for non-browsable rows", () => {
|
||||
assert.equal(treeNodeRowDoubleClickAction("database", false), "none");
|
||||
assert.equal(treeNodeRowDoubleClickAction("table", true), "none");
|
||||
assert.equal(treeNodeRowDoubleClickAction("view", true), "none");
|
||||
assert.equal(treeNodeRowDoubleClickAction("materialized_view", true), "none");
|
||||
assert.equal(treeNodeRowDoubleClickAction("column", true), "none");
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue