feat(tabs): add configurable data tab reuse modes
This commit is contained in:
26 files changed
+393
-98
No files matched your search
@@ -37,6 +37,7 @@ import {
|
||||
type DesktopIconTheme,
|
||||
type InterfaceLayout,
|
||||
type DisconnectTabHandlingMode,
|
||||
type DataTabReuseMode,
|
||||
type OpenTabsRestoreMode,
|
||||
type SidebarObjectInfoMode,
|
||||
type SqlSemanticDiagnosticsMode,
|
||||
@@ -383,12 +384,13 @@ const editingShortcutId = ref<ShortcutActionId | null>(null);
|
||||
const editSidebarActivation = ref(settingsStore.editorSettings.sidebarActivation);
|
||||
const editSidebarObjectDisplay = ref(settingsStore.editorSettings.sidebarObjectDisplay);
|
||||
const sidebarObjectDisplayHelp = ref<"grouped" | "simple" | null>(null);
|
||||
const dataTabReuseModeHelp = ref<DataTabReuseMode | null>(null);
|
||||
const editRoutineSourceOpenMode = ref(settingsStore.editorSettings.routineSourceOpenMode);
|
||||
const editSidebarTableSearchEnabled = ref(settingsStore.editorSettings.sidebarTableSearchEnabled);
|
||||
const editAutoSelectActiveSidebarNode = ref(settingsStore.editorSettings.autoSelectActiveSidebarNode);
|
||||
const editOpenTabsRestoreMode = ref<OpenTabsRestoreMode>(settingsStore.editorSettings.openTabsRestoreMode);
|
||||
const editDisconnectTabHandlingMode = ref<DisconnectTabHandlingMode>(settingsStore.editorSettings.disconnectTabHandlingMode);
|
||||
const editReuseDataTab = ref(settingsStore.editorSettings.reuseDataTab);
|
||||
const editDataTabReuseMode = ref<DataTabReuseMode>(settingsStore.editorSettings.dataTabReuseMode);
|
||||
const editPrefillNewQueryWithSelect = ref(settingsStore.editorSettings.prefillNewQueryWithSelect);
|
||||
const editClickTableNavigationTarget = ref<ClickTableNavigationTarget>(settingsStore.editorSettings.clickTableNavigationTarget);
|
||||
const editUpdateNotificationsEnabled = ref(settingsStore.editorSettings.updateNotificationsEnabled);
|
||||
@@ -499,7 +501,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
||||
autoSelectActiveSidebarNode: editAutoSelectActiveSidebarNode.value,
|
||||
openTabsRestoreMode: editOpenTabsRestoreMode.value,
|
||||
disconnectTabHandlingMode: editDisconnectTabHandlingMode.value,
|
||||
reuseDataTab: editReuseDataTab.value,
|
||||
dataTabReuseMode: editDataTabReuseMode.value,
|
||||
prefillNewQueryWithSelect: editPrefillNewQueryWithSelect.value,
|
||||
updateNotificationsEnabled: editUpdateNotificationsEnabled.value,
|
||||
sidebarObjectInfoMode: editSidebarObjectInfoMode.value,
|
||||
@@ -767,7 +769,7 @@ function syncEditorSettingsDraftFromStore() {
|
||||
editAutoSelectActiveSidebarNode.value = settingsStore.editorSettings.autoSelectActiveSidebarNode;
|
||||
editOpenTabsRestoreMode.value = settingsStore.editorSettings.openTabsRestoreMode;
|
||||
editDisconnectTabHandlingMode.value = settingsStore.editorSettings.disconnectTabHandlingMode;
|
||||
editReuseDataTab.value = settingsStore.editorSettings.reuseDataTab;
|
||||
editDataTabReuseMode.value = settingsStore.editorSettings.dataTabReuseMode;
|
||||
editPrefillNewQueryWithSelect.value = settingsStore.editorSettings.prefillNewQueryWithSelect;
|
||||
editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget;
|
||||
editUpdateNotificationsEnabled.value = settingsStore.editorSettings.updateNotificationsEnabled;
|
||||
@@ -974,7 +976,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
editAutoSelectActiveSidebarNode.value = DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode;
|
||||
editOpenTabsRestoreMode.value = DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode;
|
||||
editDisconnectTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode;
|
||||
editReuseDataTab.value = DEFAULT_EDITOR_SETTINGS.reuseDataTab;
|
||||
editDataTabReuseMode.value = DEFAULT_EDITOR_SETTINGS.dataTabReuseMode;
|
||||
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
|
||||
editClickTableNavigationTarget.value = DEFAULT_EDITOR_SETTINGS.clickTableNavigationTarget;
|
||||
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
|
||||
@@ -1067,7 +1069,7 @@ function resetAllDefaults() {
|
||||
editAutoSelectActiveSidebarNode.value = DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode;
|
||||
editOpenTabsRestoreMode.value = DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode;
|
||||
editDisconnectTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode;
|
||||
editReuseDataTab.value = DEFAULT_EDITOR_SETTINGS.reuseDataTab;
|
||||
editDataTabReuseMode.value = DEFAULT_EDITOR_SETTINGS.dataTabReuseMode;
|
||||
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
|
||||
editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled;
|
||||
editSidebarObjectInfoMode.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectInfoMode;
|
||||
@@ -4434,14 +4436,66 @@ onUnmounted(() => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Label for="reuse-data-tab">{{ t("settings.reuseDataTab") }}</Label>
|
||||
<Label>{{ t("settings.reuseDataTab") }}</Label>
|
||||
<HelpTooltip :label="t('settings.reuseDataTab')">
|
||||
{{ t("settings.reuseDataTabDescription") }}
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<Switch id="reuse-data-tab" v-model="editReuseDataTab" />
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||
<Button type="button" variant="outline" class="h-auto items-start justify-start border p-3" :class="editDataTabReuseMode === 'always-new' ? 'dbx-choice-selected' : ''" @click="editDataTabReuseMode = 'always-new'">
|
||||
<div class="text-left">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-sm font-medium">{{ t("settings.dataTabReuseAlwaysNew") }}</div>
|
||||
<Tooltip :open="dataTabReuseModeHelp === 'always-new'">
|
||||
<TooltipTrigger as-child>
|
||||
<span class="inline-flex shrink-0 cursor-help text-muted-foreground hover:text-foreground" @click.stop @pointerdown.stop @mouseenter="dataTabReuseModeHelp = 'always-new'" @mouseleave="dataTabReuseModeHelp = null">
|
||||
<CircleHelp class="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent class="max-w-[320px] text-xs leading-relaxed" side="top" align="center" :side-offset="8">
|
||||
{{ t("settings.dataTabReuseAlwaysNewDescription") }}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
<Button type="button" variant="outline" class="h-auto items-start justify-start border p-3" :class="editDataTabReuseMode === 'same-table' ? 'dbx-choice-selected' : ''" @click="editDataTabReuseMode = 'same-table'">
|
||||
<div class="text-left">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-sm font-medium">{{ t("settings.dataTabReuseSameTable") }}</div>
|
||||
<Tooltip :open="dataTabReuseModeHelp === 'same-table'">
|
||||
<TooltipTrigger as-child>
|
||||
<span class="inline-flex shrink-0 cursor-help text-muted-foreground hover:text-foreground" @click.stop @pointerdown.stop @mouseenter="dataTabReuseModeHelp = 'same-table'" @mouseleave="dataTabReuseModeHelp = null">
|
||||
<CircleHelp class="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent class="max-w-[320px] text-xs leading-relaxed" side="top" align="center" :side-offset="8">
|
||||
{{ t("settings.dataTabReuseSameTableDescription") }}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
<Button type="button" variant="outline" class="h-auto items-start justify-start border p-3" :class="editDataTabReuseMode === 'active-tab' ? 'dbx-choice-selected' : ''" @click="editDataTabReuseMode = 'active-tab'">
|
||||
<div class="text-left">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-sm font-medium">{{ t("settings.dataTabReuseActiveTab") }}</div>
|
||||
<Tooltip :open="dataTabReuseModeHelp === 'active-tab'">
|
||||
<TooltipTrigger as-child>
|
||||
<span class="inline-flex shrink-0 cursor-help text-muted-foreground hover:text-foreground" @click.stop @pointerdown.stop @mouseenter="dataTabReuseModeHelp = 'active-tab'" @mouseleave="dataTabReuseModeHelp = null">
|
||||
<CircleHelp class="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent class="max-w-[320px] text-xs leading-relaxed" side="top" align="center" :side-offset="8">
|
||||
{{ t("settings.dataTabReuseActiveTabDescription") }}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.sidebarObjectDisplay") }}</Label>
|
||||
|
||||
@@ -3054,6 +3054,16 @@ watch([pendingChangeCount, pendingChangesVersion], () => {
|
||||
schedulePreviewRefresh();
|
||||
});
|
||||
|
||||
watch(
|
||||
pendingChangeCount,
|
||||
(count) => {
|
||||
if (props.context !== "table-data" || !props.cacheKey) return;
|
||||
const tab = queryStore.tabs.find((item) => item.id === props.cacheKey);
|
||||
if (tab?.mode === "data") tab.pendingDataChangeCount = count || undefined;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const saveActionMode = computed(() =>
|
||||
dataGridSaveActionMode({
|
||||
pendingChangeCount: pendingChangeCount.value,
|
||||
|
||||
@@ -1075,7 +1075,7 @@ function activateDataTableFromDoubleClick() {
|
||||
if (node.type !== "table" || !hasNodeDatabaseContext(node)) return;
|
||||
const activation = settingsStore.editorSettings.sidebarActivation;
|
||||
const existingSameTableTab = findExistingSameTableDataTab();
|
||||
const action = dataTableDoubleClickAction(existingSameTableTab, activation, settingsStore.editorSettings.reuseDataTab);
|
||||
const action = dataTableDoubleClickAction(existingSameTableTab, activation, settingsStore.editorSettings.dataTabReuseMode);
|
||||
if (action === "none") return;
|
||||
if (action === "open") {
|
||||
openDataImmediately(node);
|
||||
|
||||
@@ -79,7 +79,7 @@ vi.mock("@/stores/queryStore", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/settingsStore", () => ({
|
||||
useSettingsStore: () => ({ editorSettings: { reuseDataTab: true } }),
|
||||
useSettingsStore: () => ({ editorSettings: { dataTabReuseMode: "same-table" } }),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/table/tableSelectSql", () => ({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { DataTabReuseMode } from "@/lib/tabs/dataTabReuseMode";
|
||||
import type { QueryResult } from "@/types/database";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -17,7 +18,7 @@ const mocks = vi.hoisted(() => ({
|
||||
continueOnErrorOnBatch: false,
|
||||
openTabsRestoreMode: "all",
|
||||
pageSize: 100,
|
||||
reuseDataTab: true,
|
||||
dataTabReuseMode: "same-table" as DataTabReuseMode,
|
||||
tableOpenPageSize: 100,
|
||||
},
|
||||
},
|
||||
@@ -100,7 +101,7 @@ describe("useNavigationTargets with the real query store", () => {
|
||||
installLocalStorage();
|
||||
mocks.connectionStore.activeConnectionId = "";
|
||||
mocks.connectionStore.getConfig.mockImplementation((connectionId: string) => ({ id: connectionId, db_type: "postgres" }));
|
||||
mocks.settingsStore.editorSettings.reuseDataTab = true;
|
||||
mocks.settingsStore.editorSettings.dataTabReuseMode = "same-table";
|
||||
mocks.ensureConnected?.mockResolvedValue?.(undefined);
|
||||
mocks.connectionStore.ensureConnected.mockResolvedValue(undefined);
|
||||
mocks.loadOpenTabsState.mockResolvedValue(null);
|
||||
@@ -148,8 +149,8 @@ describe("useNavigationTargets with the real query store", () => {
|
||||
expect(mocks.connectionStore.activeConnectionId).toBe("connection-1");
|
||||
});
|
||||
|
||||
it("keeps object-browser tabs independent when data-tab reuse is disabled", async () => {
|
||||
mocks.settingsStore.editorSettings.reuseDataTab = false;
|
||||
it("keeps object-browser tabs independent in always-new mode", async () => {
|
||||
mocks.settingsStore.editorSettings.dataTabReuseMode = "always-new";
|
||||
const { navigation, queryStore } = await setupNavigation();
|
||||
const target = { connectionId: "connection-1", database: "app", schema: "public", tableName: "users", tableType: "TABLE" };
|
||||
|
||||
@@ -159,8 +160,8 @@ describe("useNavigationTargets with the real query store", () => {
|
||||
expect(queryStore.tabs).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps repeated sidebar opens independent when data-tab reuse is disabled", async () => {
|
||||
mocks.settingsStore.editorSettings.reuseDataTab = false;
|
||||
it("keeps repeated sidebar opens independent in always-new mode", async () => {
|
||||
mocks.settingsStore.editorSettings.dataTabReuseMode = "always-new";
|
||||
const { queryStore } = await setupNavigation();
|
||||
const { useSidebarDataOpenRuntime } = await import("@/composables/useSidebarDataOpenRuntime");
|
||||
const runtime = useSidebarDataOpenRuntime();
|
||||
@@ -173,7 +174,7 @@ describe("useNavigationTargets with the real query store", () => {
|
||||
expect(new Set(queryStore.tabs.map((tab) => tab.id))).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps different sidebar tables independent when data-tab reuse is enabled", async () => {
|
||||
it("keeps different sidebar tables independent in same-table mode", async () => {
|
||||
const { queryStore } = await setupNavigation();
|
||||
const { useSidebarDataOpenRuntime } = await import("@/composables/useSidebarDataOpenRuntime");
|
||||
const runtime = useSidebarDataOpenRuntime();
|
||||
@@ -186,6 +187,21 @@ describe("useNavigationTargets with the real query store", () => {
|
||||
expect(queryStore.tabs.map((tab) => tab.tableMeta?.tableName)).toEqual(["users", "orders"]);
|
||||
});
|
||||
|
||||
it("reuses the active data tab for different object-browser tables in active-tab mode", async () => {
|
||||
mocks.settingsStore.editorSettings.dataTabReuseMode = "active-tab";
|
||||
const { navigation, queryStore } = await setupNavigation();
|
||||
const target = { connectionId: "connection-1", database: "app", schema: "public", tableName: "users", tableType: "TABLE" };
|
||||
|
||||
await navigation.openObjectBrowserTableTarget(target);
|
||||
const originalTabId = queryStore.activeTabId;
|
||||
await navigation.openObjectBrowserTableTarget({ ...target, tableName: "orders" });
|
||||
|
||||
expect(queryStore.tabs).toHaveLength(1);
|
||||
expect(queryStore.activeTabId).toBe(originalTabId);
|
||||
expect(queryStore.tabs[0]?.tableMeta?.tableName).toBe("orders");
|
||||
expect(queryStore.tabs[0]?.sql).toBe("SELECT * FROM orders");
|
||||
});
|
||||
|
||||
it("reuses a sidebar table when the same table is opened from the object browser", async () => {
|
||||
mocks.connectionStore.getConfig.mockImplementation((connectionId: string) => ({ id: connectionId, db_type: "mysql" }));
|
||||
const { navigation, queryStore } = await setupNavigation();
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useSidebarDataOpenRuntime } from "@/composables/useSidebarDataOpenRuntime";
|
||||
import type { DataTabReuseMode } from "@/lib/tabs/dataTabReuseMode";
|
||||
import type { QueryTab, TreeNode } from "@/types/database";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
databaseType: "oceanbase" as string,
|
||||
callOrder: [] as string[],
|
||||
tabs: [] as QueryTab[],
|
||||
activeTabId: null as string | null,
|
||||
cachedMetadata: undefined as unknown,
|
||||
reuseDataTab: false,
|
||||
dataTabReuseMode: "same-table" as DataTabReuseMode,
|
||||
ensureConnected: vi.fn(),
|
||||
executeTabSql: vi.fn(),
|
||||
loadTableMetadata: vi.fn(),
|
||||
@@ -27,10 +29,16 @@ vi.mock("@/stores/connectionStore", () => ({
|
||||
vi.mock("@/stores/queryStore", () => ({
|
||||
useQueryStore: () => ({
|
||||
tabs: mocks.tabs,
|
||||
get activeTabId() {
|
||||
return mocks.activeTabId;
|
||||
},
|
||||
createTab: (connectionId: string, database: string, title: string, mode: QueryTab["mode"], schema?: string, _initialSql?: string, catalog?: string, options: { forceNew?: boolean } = {}) => {
|
||||
if (!options.forceNew) {
|
||||
const existing = mocks.tabs.find((tab) => tab.connectionId === connectionId && tab.database === database && tab.title === title && tab.mode === mode && (tab.schema || "") === (schema || "") && (tab.catalog || "") === (catalog || ""));
|
||||
if (existing) return existing.id;
|
||||
if (existing) {
|
||||
mocks.activeTabId = existing.id;
|
||||
return existing.id;
|
||||
}
|
||||
}
|
||||
const tab = {
|
||||
id: `tab-${mocks.tabs.length + 1}`,
|
||||
@@ -47,9 +55,12 @@ vi.mock("@/stores/queryStore", () => ({
|
||||
isExplaining: false,
|
||||
} as QueryTab;
|
||||
mocks.tabs.push(tab);
|
||||
mocks.activeTabId = tab.id;
|
||||
return tab.id;
|
||||
},
|
||||
switchTab: vi.fn(),
|
||||
switchTab: vi.fn((id: string) => {
|
||||
mocks.activeTabId = id;
|
||||
}),
|
||||
cancelTabExecution: mocks.cancelTabExecution,
|
||||
setExecutingWithId: (id: string, executionId: string) => {
|
||||
const tab = mocks.tabs.find((item) => item.id === id);
|
||||
@@ -76,7 +87,7 @@ vi.mock("@/stores/queryStore", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/settingsStore", () => ({
|
||||
useSettingsStore: () => ({ editorSettings: { reuseDataTab: mocks.reuseDataTab, pageSize: 100 } }),
|
||||
useSettingsStore: () => ({ editorSettings: { dataTabReuseMode: mocks.dataTabReuseMode, pageSize: 100 } }),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/database/jdbcDialect", () => ({
|
||||
@@ -127,8 +138,9 @@ describe("useSidebarDataOpenRuntime", () => {
|
||||
mocks.databaseType = "oceanbase";
|
||||
mocks.callOrder.length = 0;
|
||||
mocks.tabs.length = 0;
|
||||
mocks.activeTabId = null;
|
||||
mocks.cachedMetadata = undefined;
|
||||
mocks.reuseDataTab = false;
|
||||
mocks.dataTabReuseMode = "same-table";
|
||||
mocks.ensureConnected.mockResolvedValue(undefined);
|
||||
mocks.buildTableSelectSql.mockResolvedValue("SELECT * FROM users");
|
||||
mocks.executeTabSql.mockImplementation(async () => {
|
||||
@@ -153,24 +165,23 @@ describe("useSidebarDataOpenRuntime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a new sidebar tab for the same table when reuse is disabled", async () => {
|
||||
it("creates a new sidebar tab for the same table in always-new mode", async () => {
|
||||
mocks.dataTabReuseMode = "always-new";
|
||||
|
||||
await useSidebarDataOpenRuntime().openData(tableNode);
|
||||
await useSidebarDataOpenRuntime().openData(tableNode);
|
||||
|
||||
expect(mocks.tabs).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("reuses a sidebar tab for the same table when reuse is enabled", async () => {
|
||||
mocks.reuseDataTab = true;
|
||||
|
||||
it("reuses a sidebar tab for the same table in same-table mode", async () => {
|
||||
await useSidebarDataOpenRuntime().openData(tableNode);
|
||||
await useSidebarDataOpenRuntime().openData(tableNode);
|
||||
|
||||
expect(mocks.tabs).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps different sidebar tables independent when reuse is enabled", async () => {
|
||||
mocks.reuseDataTab = true;
|
||||
it("keeps different sidebar tables independent in same-table mode", async () => {
|
||||
const ordersNode = { ...tableNode, id: "table-orders", label: "orders" };
|
||||
|
||||
await useSidebarDataOpenRuntime().openData(tableNode);
|
||||
@@ -180,8 +191,68 @@ describe("useSidebarDataOpenRuntime", () => {
|
||||
expect(mocks.tabs.map((tab) => tab.title)).toEqual(["users", "orders"]);
|
||||
});
|
||||
|
||||
it("creates a new HBase tab for the same table when reuse is disabled", async () => {
|
||||
it("reuses the active safe data tab for a different table in active-tab mode", async () => {
|
||||
mocks.dataTabReuseMode = "active-tab";
|
||||
const ordersNode = { ...tableNode, id: "table-orders", label: "orders" };
|
||||
mocks.loadTableMetadata.mockImplementation(async (request: { database: string; schema?: string; tableName: string; tableType?: string }) => ({
|
||||
metadata: {
|
||||
schema: request.schema,
|
||||
tableName: request.tableName,
|
||||
tableType: request.tableType ?? "TABLE",
|
||||
database: request.database,
|
||||
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
|
||||
indexes: [],
|
||||
primaryKeys: ["id"],
|
||||
cachedAt: Date.now(),
|
||||
},
|
||||
cacheStatus: "miss",
|
||||
ageMs: 0,
|
||||
}));
|
||||
|
||||
await useSidebarDataOpenRuntime().openData(tableNode);
|
||||
mocks.tabs[0]!.isExecuting = false;
|
||||
mocks.tabs[0]!.executionId = undefined;
|
||||
await useSidebarDataOpenRuntime().openData(ordersNode);
|
||||
|
||||
expect(mocks.tabs).toHaveLength(1);
|
||||
expect(mocks.tabs[0]?.title).toBe("orders");
|
||||
expect(mocks.tabs[0]?.tableMeta?.tableName).toBe("orders");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["pinned", { pinned: true }],
|
||||
["executing", { isExecuting: true, executionId: "running" }],
|
||||
["pending edits", { pendingDataChangeCount: 1 }],
|
||||
])("opens a new tab instead of replacing an active %s data tab", async (_label, patch) => {
|
||||
mocks.dataTabReuseMode = "active-tab";
|
||||
const activeTab = {
|
||||
id: "active-users",
|
||||
connectionId: "connection-1",
|
||||
database: "app",
|
||||
title: "users",
|
||||
mode: "data",
|
||||
schema: "public",
|
||||
sql: "SELECT * FROM users",
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
tableMeta: { schema: "public", tableName: "users", tableType: "TABLE", columns: [], primaryKeys: [] },
|
||||
...patch,
|
||||
} as QueryTab;
|
||||
mocks.tabs.push(activeTab);
|
||||
mocks.activeTabId = activeTab.id;
|
||||
|
||||
await useSidebarDataOpenRuntime().openData({ ...tableNode, id: "table-orders", label: "orders" });
|
||||
|
||||
expect(mocks.tabs).toHaveLength(2);
|
||||
expect(mocks.tabs[0]?.title).toBe("users");
|
||||
expect(mocks.tabs[1]?.title).toBe("orders");
|
||||
expect(mocks.cancelTabExecution).not.toHaveBeenCalledWith(activeTab.id);
|
||||
});
|
||||
|
||||
it("creates a new HBase tab for the same table in always-new mode", async () => {
|
||||
mocks.databaseType = "hbase";
|
||||
mocks.dataTabReuseMode = "always-new";
|
||||
|
||||
await useSidebarDataOpenRuntime().openData(tableNode);
|
||||
await useSidebarDataOpenRuntime().openData(tableNode);
|
||||
@@ -189,9 +260,8 @@ describe("useSidebarDataOpenRuntime", () => {
|
||||
expect(mocks.tabs).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps different HBase tables independent when reuse is enabled", async () => {
|
||||
it("keeps different HBase tables independent in same-table mode", async () => {
|
||||
mocks.databaseType = "hbase";
|
||||
mocks.reuseDataTab = true;
|
||||
|
||||
await useSidebarDataOpenRuntime().openData(tableNode);
|
||||
await useSidebarDataOpenRuntime().openData({ ...tableNode, id: "table-orders", label: "orders" });
|
||||
@@ -352,11 +422,7 @@ describe("useSidebarDataOpenRuntime", () => {
|
||||
expect(mocks.tabs[0]?.tableMetaPending).toBe(true);
|
||||
});
|
||||
|
||||
it("aborts when a newer navigation takes over the tab during the cancel wait", async () => {
|
||||
mocks.reuseDataTab = true;
|
||||
const { beginDataTabNavigation } = await import("@/lib/tabs/dataTabNavigationGeneration");
|
||||
// 已存在同表 data tab 且有在途执行:真实 findExistingDataTabCandidate 会
|
||||
// 选中它(same-table 复用分支),openData 需先等待取消
|
||||
it("activates an already-running same-table tab without cancelling it", async () => {
|
||||
mocks.tabs.push({
|
||||
id: "existing-tab",
|
||||
connectionId: "connection-1",
|
||||
@@ -370,29 +436,22 @@ describe("useSidebarDataOpenRuntime", () => {
|
||||
executionId: "stale-execution",
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
tableMeta: { schema: "public", tableName: "users", tableType: "TABLE", columns: [], primaryKeys: [] },
|
||||
tableMeta: {
|
||||
schema: "public",
|
||||
tableName: "users",
|
||||
tableType: "TABLE",
|
||||
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
|
||||
primaryKeys: ["id"],
|
||||
},
|
||||
tableMetaUpdatedAt: Date.now(),
|
||||
} as QueryTab);
|
||||
let releaseCancel: () => void = () => {};
|
||||
mocks.cancelTabExecution.mockReturnValueOnce(
|
||||
new Promise<void>((resolve) => {
|
||||
releaseCancel = resolve;
|
||||
}),
|
||||
);
|
||||
const open = useSidebarDataOpenRuntime().openData(tableNode);
|
||||
await vi.waitFor(() => expect(mocks.cancelTabExecution).toHaveBeenCalledWith("existing-tab"));
|
||||
// 复用既有 tab,而非新建
|
||||
mocks.activeTabId = null;
|
||||
|
||||
await useSidebarDataOpenRuntime().openData(tableNode);
|
||||
|
||||
expect(mocks.tabs).toHaveLength(1);
|
||||
|
||||
// 取消等待期间,更晚的导航(如 openTableTarget)接管该 tab
|
||||
beginDataTabNavigation("existing-tab");
|
||||
const takeoverMeta = { schema: "public", tableName: "other_table", tableType: "TABLE", columns: [], primaryKeys: [] };
|
||||
mocks.tabs[0]!.tableMeta = takeoverMeta;
|
||||
|
||||
releaseCancel();
|
||||
await open;
|
||||
|
||||
// 旧 openData 必须让位:不得覆盖新导航写入的身份,不得启动查询
|
||||
expect(mocks.tabs[0]?.tableMeta).toBe(takeoverMeta);
|
||||
expect(mocks.activeTabId).toBe("existing-tab");
|
||||
expect(mocks.cancelTabExecution).not.toHaveBeenCalled();
|
||||
expect(mocks.executeTabSql).not.toHaveBeenCalled();
|
||||
expect(mocks.loadTableMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -258,7 +258,7 @@ export function useNavigationTargets(dialogs: { showFieldLineageDialog: { value:
|
||||
const { openData } = useSidebarDataOpenRuntime();
|
||||
|
||||
async function openObjectBrowserTableTarget(target: NavigationTarget) {
|
||||
if (!settingsStore.editorSettings.reuseDataTab) {
|
||||
if (settingsStore.editorSettings.dataTabReuseMode === "always-new") {
|
||||
await openTableTarget(target);
|
||||
return;
|
||||
}
|
||||
@@ -278,7 +278,7 @@ export function useNavigationTargets(dialogs: { showFieldLineageDialog: { value:
|
||||
},
|
||||
undefined,
|
||||
"default",
|
||||
{ reuseScope: "same-table" },
|
||||
{ reuseMode: settingsStore.editorSettings.dataTabReuseMode },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { uuid } from "@/lib/common/utils";
|
||||
import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog";
|
||||
import { effectiveDatabaseTypeForConnection, connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema } from "@/lib/database/jdbcDialect";
|
||||
import { getCachedTableMetadata, loadTableMetadata, TABLE_METADATA_CACHE_TTL_MS, tableMetadataToDataTabMeta } from "@/lib/metadata/tableMetadataCache";
|
||||
import { canApplyDataTabMetadata, dataTabMetadataNeedsRefresh, findExistingDataTabCandidate, type DataTabOpenMode, type DataTabReuseScope } from "@/lib/sidebar/dataTabOpenPolicy";
|
||||
import { canApplyDataTabMetadata, dataTabMetadataNeedsRefresh, findExistingDataTabCandidate, type DataTabOpenMode, type DataTabReuseMode } from "@/lib/sidebar/dataTabOpenPolicy";
|
||||
import type { SidebarDataOpenRequest } from "@/lib/sidebar/sidebarDataOpenCoordinator";
|
||||
import { hasTreeNodeDatabaseContext } from "@/lib/sidebar/treeNodeContext";
|
||||
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
|
||||
@@ -27,13 +27,13 @@ export function useSidebarDataOpenRuntime() {
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMode: DataTabOpenMode = "default", options: { reuseScope?: DataTabReuseScope } = {}) {
|
||||
async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMode: DataTabOpenMode = "default", options: { reuseMode?: DataTabReuseMode } = {}) {
|
||||
if (!(node.type === "table" || node.type === "view" || node.type === "materialized_view") || !hasNodeDatabaseContext(node)) return;
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
const reuseScope = options.reuseScope ?? (settingsStore.editorSettings.reuseDataTab ? "same-table" : "none");
|
||||
const reuseMode = options.reuseMode ?? settingsStore.editorSettings.dataTabReuseMode;
|
||||
if (config?.db_type === "hbase") {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const tabId = queryStore.createTab(node.connectionId, node.database, node.label, "hbase", undefined, node.label, undefined, { forceNew: openMode === "new-tab" || reuseScope === "none" });
|
||||
const tabId = queryStore.createTab(node.connectionId, node.database, node.label, "hbase", undefined, node.label, undefined, { forceNew: openMode === "new-tab" || reuseMode === "always-new" });
|
||||
queryStore.updateSql(tabId, node.label);
|
||||
return;
|
||||
}
|
||||
@@ -60,6 +60,7 @@ export function useSidebarDataOpenRuntime() {
|
||||
type: node.type,
|
||||
dbType: config?.db_type,
|
||||
openMode,
|
||||
reuseMode,
|
||||
});
|
||||
const tableSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema);
|
||||
const tableType = node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : (node.tableType ?? "TABLE");
|
||||
@@ -126,7 +127,7 @@ export function useSidebarDataOpenRuntime() {
|
||||
openDataLog("warn", "metadata:error", { traceId, tabId: targetTabId, elapsed: elapsed(), error });
|
||||
}
|
||||
};
|
||||
const existingDataTabCandidate = findExistingDataTabCandidate(queryStore.tabs, dataTabTarget, { openMode, reuseScope });
|
||||
const existingDataTabCandidate = findExistingDataTabCandidate(queryStore.tabs, dataTabTarget, { openMode, reuseMode, activeTabId: queryStore.activeTabId });
|
||||
const existingSameTableTab = existingDataTabCandidate?.match === "same-table" ? existingDataTabCandidate.tab : undefined;
|
||||
const resetReusedDataTabState = (tab: (typeof queryStore.tabs)[number]) => {
|
||||
tab.title = node.label;
|
||||
@@ -152,7 +153,7 @@ export function useSidebarDataOpenRuntime() {
|
||||
tab.queryEditabilityReason = undefined;
|
||||
};
|
||||
|
||||
if (existingSameTableTab && canActivateExistingDataTableTab(existingSameTableTab, { activateExecuting: false })) {
|
||||
if (existingSameTableTab && (existingSameTableTab.isExecuting || canActivateExistingDataTableTab(existingSameTableTab, { activateExecuting: false }))) {
|
||||
queryStore.switchTab(existingSameTableTab.id);
|
||||
logPhase("existing-tab-activated", { table: node.label });
|
||||
if (dataTabMetadataNeedsRefresh(existingSameTableTab, DATA_TAB_METADATA_TTL_MS)) {
|
||||
@@ -171,7 +172,7 @@ export function useSidebarDataOpenRuntime() {
|
||||
resetReusedDataTabState(existingDataTabCandidate.tab);
|
||||
return existingDataTabCandidate.tab.id;
|
||||
}
|
||||
return queryStore.createTab(node.connectionId, node.database, node.label, "data", tableSchema, undefined, node.catalog, { forceNew: reuseScope === "none" || openMode === "new-tab" });
|
||||
return queryStore.createTab(node.connectionId, node.database, node.label, "data", tableSchema, undefined, node.catalog, { forceNew: true });
|
||||
})();
|
||||
openDataLog("info", "tab-created", { traceId, tabId, elapsed: elapsed() });
|
||||
logPhase("tab-created", { tabId });
|
||||
|
||||
@@ -5053,7 +5053,13 @@ export default {
|
||||
disconnectTabHandlingModeKeepTabsKeepResults: "Do not close related tabs",
|
||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Keep related tabs, SQL text, and current results without extra cleanup.",
|
||||
reuseDataTab: "Reuse data tab",
|
||||
reuseDataTabDescription: "Switch to the existing data tab when reopening the same table; different tables always use separate tabs.",
|
||||
reuseDataTabDescription: "Choose how opening a table uses data tabs.",
|
||||
dataTabReuseAlwaysNew: "Always open a new tab",
|
||||
dataTabReuseAlwaysNewDescription: "Create a new data tab every time, even when the table is already open.",
|
||||
dataTabReuseSameTable: "Reuse the same table",
|
||||
dataTabReuseSameTableDescription: "Switch to an existing tab for the same table and open different tables separately.",
|
||||
dataTabReuseActiveTab: "Reuse the active data tab",
|
||||
dataTabReuseActiveTabDescription: "Reuse the active safe data tab for a different table; protected tabs open separately.",
|
||||
sidebarHiddenTablePrefixes: "Hidden table name prefixes",
|
||||
sidebarHiddenTablePrefixesDescription: "One prefix per line. Only sidebar table, view, and collection labels are shortened; tooltips and actions still use the full name.",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "Example:\nODS_\nT8Y2_LONG_",
|
||||
|
||||
@@ -4826,7 +4826,13 @@ export default withEnglishFallback({
|
||||
disconnectTabHandlingModeKeepTabsKeepResults: "No cerrar pestañas relacionadas",
|
||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Conserva las pestañas relacionadas, el texto SQL y los resultados actuales sin limpieza adicional.",
|
||||
reuseDataTab: "Reutilizar pestaña de datos",
|
||||
reuseDataTabDescription: "Al volver a abrir la misma tabla, cambia a su pestaña de datos existente; las tablas diferentes siempre usan pestañas separadas.",
|
||||
reuseDataTabDescription: "Elige cómo se usan las pestañas de datos al abrir una tabla.",
|
||||
dataTabReuseAlwaysNew: "Abrir siempre una pestaña nueva",
|
||||
dataTabReuseAlwaysNewDescription: "Crea una pestaña de datos nueva cada vez, aunque la tabla ya esté abierta.",
|
||||
dataTabReuseSameTable: "Reutilizar solo la misma tabla",
|
||||
dataTabReuseSameTableDescription: "Cambia a la pestaña existente de la misma tabla y abre las demás por separado.",
|
||||
dataTabReuseActiveTab: "Reutilizar la pestaña activa",
|
||||
dataTabReuseActiveTabDescription: "Reutiliza la pestaña de datos activa cuando sea seguro; las pestañas protegidas se abren por separado.",
|
||||
sidebarHiddenTablePrefixes: "Prefijos ocultos de tablas",
|
||||
sidebarHiddenTablePrefixesDescription: "Un prefijo por linea. Solo acorta etiquetas de tablas, vistas y colecciones en la barra lateral; las acciones y ayudas usan el nombre completo.",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "Ejemplo:\nODS_\nT8Y2_LONG_",
|
||||
|
||||
@@ -4826,7 +4826,13 @@ export default withEnglishFallback({
|
||||
disconnectTabHandlingModeKeepTabsKeepResults: "Non chiudere le schede correlate",
|
||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Mantieni le schede correlate, il testo SQL e i risultati correnti senza ulteriore pulizia.",
|
||||
reuseDataTab: "Riusa scheda dati",
|
||||
reuseDataTabDescription: "Quando riapri la stessa tabella, passa alla scheda dati esistente; tabelle diverse usano sempre schede separate.",
|
||||
reuseDataTabDescription: "Scegli come usare le schede dati quando apri una tabella.",
|
||||
dataTabReuseAlwaysNew: "Apri sempre una nuova scheda",
|
||||
dataTabReuseAlwaysNewDescription: "Crea ogni volta una nuova scheda dati, anche se la tabella è già aperta.",
|
||||
dataTabReuseSameTable: "Riusa solo la stessa tabella",
|
||||
dataTabReuseSameTableDescription: "Passa alla scheda esistente per la stessa tabella e apre separatamente le altre.",
|
||||
dataTabReuseActiveTab: "Riusa la scheda dati attiva",
|
||||
dataTabReuseActiveTabDescription: "Riusa la scheda dati attiva quando è sicuro; le schede protette vengono aperte separatamente.",
|
||||
sidebarHiddenTablePrefixes: "Prefissi dei nomi delle tabelle nascosti",
|
||||
sidebarHiddenTablePrefixesDescription: "Un prefisso per riga. Solo le etichette di tabelle, viste e collezioni della barra laterale vengono abbreviate; i suggerimenti e le azioni utilizzano ancora il nome completo.",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "Esempio:\nODS_\nT8Y2_LONG_",
|
||||
|
||||
@@ -4855,7 +4855,13 @@ export default withEnglishFallback({
|
||||
disconnectTabHandlingModeKeepTabsKeepResults: "関連タブを閉じない",
|
||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "関連タブ、SQLテキスト、現在の結果を追加のクリーンアップなしで保持します。",
|
||||
reuseDataTab: "データタブを再利用",
|
||||
reuseDataTabDescription: "同じテーブルを再度開くと既存のデータタブに切り替え、異なるテーブルは常に別のタブで開きます。",
|
||||
reuseDataTabDescription: "テーブルを開くときのデータタブの使い方を選択します。",
|
||||
dataTabReuseAlwaysNew: "常に新しいタブを開く",
|
||||
dataTabReuseAlwaysNewDescription: "同じテーブルが開いていても、毎回新しいデータタブを作成します。",
|
||||
dataTabReuseSameTable: "同じテーブルだけ再利用",
|
||||
dataTabReuseSameTableDescription: "同じテーブルは既存のタブに切り替え、異なるテーブルは別々に開きます。",
|
||||
dataTabReuseActiveTab: "アクティブなデータタブを再利用",
|
||||
dataTabReuseActiveTabDescription: "安全な場合は異なるテーブルにもアクティブなデータタブを再利用し、保護されたタブは別に開きます。",
|
||||
sidebarHiddenTablePrefixes: "非表示テーブル名プレフィックス",
|
||||
sidebarHiddenTablePrefixesDescription: "1行に1つのプレフィックス。サイドバーのテーブル、ビュー、コレクションラベルのみ短縮されます。ツールチップと操作は完全な名前を使用します。",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "Example:\nODS_\nT8Y2_LONG_",
|
||||
|
||||
@@ -4588,7 +4588,13 @@ export default withEnglishFallback({
|
||||
disconnectTabHandlingModeKeepTabsKeepResults: "관련 탭 닫지 않기",
|
||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "관련 탭, SQL 텍스트, 현재 결과를 추가 정리 없이 유지합니다.",
|
||||
reuseDataTab: "데이터 탭 재사용",
|
||||
reuseDataTabDescription: "같은 테이블을 다시 열면 기존 데이터 탭으로 전환하고, 다른 테이블은 항상 별도 탭에서 엽니다.",
|
||||
reuseDataTabDescription: "테이블을 열 때 데이터 탭을 사용하는 방식을 선택합니다.",
|
||||
dataTabReuseAlwaysNew: "항상 새 탭 열기",
|
||||
dataTabReuseAlwaysNewDescription: "테이블이 이미 열려 있어도 매번 새 데이터 탭을 만듭니다.",
|
||||
dataTabReuseSameTable: "같은 테이블만 재사용",
|
||||
dataTabReuseSameTableDescription: "같은 테이블은 기존 탭으로 전환하고 다른 테이블은 별도로 엽니다.",
|
||||
dataTabReuseActiveTab: "활성 데이터 탭 재사용",
|
||||
dataTabReuseActiveTabDescription: "안전한 경우 다른 테이블에 활성 데이터 탭을 재사용하며 보호된 탭은 별도로 엽니다.",
|
||||
sidebarHiddenTablePrefixes: "숨겨진 테이블 이름 접두사",
|
||||
sidebarHiddenTablePrefixesDescription: "한 줄에 하나의 접두사. 사이드바의 테이블, 뷰, 컬렉션 라벨만 줄이며 툴팁과 작업은 전체 이름을 계속 사용합니다.",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "예:\nODS_\nT8Y2_LONG_",
|
||||
|
||||
@@ -4828,7 +4828,13 @@ export default withEnglishFallback({
|
||||
disconnectTabHandlingModeKeepTabsKeepResults: "Não fechar abas relacionadas",
|
||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Manter abas relacionadas, texto SQL e resultados atuais sem limpeza adicional.",
|
||||
reuseDataTab: "Reutilizar aba de dados",
|
||||
reuseDataTabDescription: "Ao reabrir a mesma tabela, alterna para a aba de dados existente; tabelas diferentes sempre usam abas separadas.",
|
||||
reuseDataTabDescription: "Escolha como as abas de dados são usadas ao abrir uma tabela.",
|
||||
dataTabReuseAlwaysNew: "Sempre abrir uma nova aba",
|
||||
dataTabReuseAlwaysNewDescription: "Cria uma nova aba de dados todas as vezes, mesmo que a tabela já esteja aberta.",
|
||||
dataTabReuseSameTable: "Reutilizar apenas a mesma tabela",
|
||||
dataTabReuseSameTableDescription: "Alterna para a aba existente da mesma tabela e abre tabelas diferentes separadamente.",
|
||||
dataTabReuseActiveTab: "Reutilizar a aba de dados ativa",
|
||||
dataTabReuseActiveTabDescription: "Reutiliza a aba de dados ativa quando for seguro; abas protegidas são abertas separadamente.",
|
||||
sidebarHiddenTablePrefixes: "Prefixos de nome de tabela ocultos",
|
||||
sidebarHiddenTablePrefixesDescription: "Um prefixo por linha. Apenas os rótulos de tabela, view e coleção da barra lateral são encurtados; tooltips e ações ainda usam o nome completo.",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "Exemplo:\nODS_\nT8Y2_LONG_",
|
||||
|
||||
@@ -5051,7 +5051,13 @@ export default withEnglishFallback({
|
||||
disconnectTabHandlingModeKeepTabsKeepResults: "不关闭相关页签",
|
||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "保留相关页签、SQL 文本和当前结果,不做额外处理。",
|
||||
reuseDataTab: "复用数据标签页",
|
||||
reuseDataTabDescription: "重复打开同一张表时切换到已有数据标签页,不同表始终使用独立标签页。",
|
||||
reuseDataTabDescription: "选择打开表时如何使用数据标签页。",
|
||||
dataTabReuseAlwaysNew: "始终新建标签页",
|
||||
dataTabReuseAlwaysNewDescription: "每次打开表都新建标签页,即使该表已经打开。",
|
||||
dataTabReuseSameTable: "仅复用同一张表",
|
||||
dataTabReuseSameTableDescription: "同表跳转到已有标签页,不同表分别打开。",
|
||||
dataTabReuseActiveTab: "复用当前数据标签页",
|
||||
dataTabReuseActiveTabDescription: "不同表复用当前安全的数据标签页;受保护的标签页仍会新建。",
|
||||
sidebarHiddenTablePrefixes: "隐藏表名前缀",
|
||||
sidebarHiddenTablePrefixesDescription: "每行一个前缀,仅影响侧边栏表、视图和集合的显示名称,悬浮提示和实际操作仍使用完整名称。",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "例如:\nODS_\nT8Y2_LONG_",
|
||||
|
||||
@@ -4289,7 +4289,13 @@ export default withEnglishFallback({
|
||||
disconnectTabHandlingModeKeepTabsKeepResults: "不關閉相關分頁",
|
||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "保留相關分頁、SQL 文字與目前結果,不另外做清理。",
|
||||
reuseDataTab: "重複使用資料分頁",
|
||||
reuseDataTabDescription: "重複開啟同一資料表時切換到現有資料分頁,不同資料表一律使用獨立分頁。",
|
||||
reuseDataTabDescription: "選擇開啟資料表時如何使用資料分頁。",
|
||||
dataTabReuseAlwaysNew: "一律開啟新分頁",
|
||||
dataTabReuseAlwaysNewDescription: "每次開啟資料表都建立新分頁,即使該資料表已經開啟。",
|
||||
dataTabReuseSameTable: "僅重複使用同一資料表",
|
||||
dataTabReuseSameTableDescription: "同一資料表切換到現有分頁,不同資料表分別開啟。",
|
||||
dataTabReuseActiveTab: "重複使用目前資料分頁",
|
||||
dataTabReuseActiveTabDescription: "不同資料表重複使用目前安全的資料分頁;受保護的分頁仍會另行開啟。",
|
||||
sidebarHiddenTablePrefixes: "隱藏資料表名稱字首",
|
||||
sidebarHiddenTablePrefixesDescription: "每行一個字首。只縮短側邊欄中的資料表、檢視和集合標籤;工具提示和實際操作仍使用完整名稱。",
|
||||
sidebarHiddenTablePrefixesPlaceholder: "範例:\nODS_\nT8Y2_LONG_",
|
||||
|
||||
@@ -52,8 +52,8 @@ describe("dataTabOpenPolicy", () => {
|
||||
const existing = dataTab("users", "users");
|
||||
existing.tableMeta = { schema: "public", tableName: "users", columns: [], primaryKeys: [] };
|
||||
|
||||
expect(findExistingDataTabCandidate([existing], usersTarget, { openMode: "new-tab", reuseScope: "same-table" })).toBeUndefined();
|
||||
expect(findExistingDataTabCandidate([existing], usersTarget, { openMode: "new-tab", reuseScope: "none" })).toBeUndefined();
|
||||
expect(findExistingDataTabCandidate([existing], usersTarget, { openMode: "new-tab", reuseMode: "same-table" })).toBeUndefined();
|
||||
expect(findExistingDataTabCandidate([existing], usersTarget, { openMode: "new-tab", reuseMode: "active-tab", activeTabId: existing.id })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("only reuses the same table when reuse is enabled", () => {
|
||||
@@ -61,16 +61,45 @@ describe("dataTabOpenPolicy", () => {
|
||||
sameTable.tableMeta = { schema: "public", tableName: "users", columns: [], primaryKeys: [] };
|
||||
const otherTable = dataTab("orders", "orders");
|
||||
|
||||
expect(findExistingDataTabCandidate([sameTable], usersTarget, { openMode: "default", reuseScope: "none" })).toBeUndefined();
|
||||
expect(findExistingDataTabCandidate([sameTable], usersTarget, { openMode: "default", reuseScope: "same-table" })).toEqual({ tab: sameTable, match: "same-table" });
|
||||
expect(findExistingDataTabCandidate([otherTable], usersTarget, { openMode: "default", reuseScope: "same-table" })).toBeUndefined();
|
||||
expect(findExistingDataTabCandidate([sameTable], usersTarget, { openMode: "default", reuseMode: "always-new" })).toBeUndefined();
|
||||
expect(findExistingDataTabCandidate([sameTable], usersTarget, { openMode: "default", reuseMode: "same-table" })).toEqual({ tab: sameTable, match: "same-table" });
|
||||
expect(findExistingDataTabCandidate([otherTable], usersTarget, { openMode: "default", reuseMode: "same-table" })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reuses the active safe data tab for a different table", () => {
|
||||
const active = dataTab("orders", "orders");
|
||||
|
||||
expect(findExistingDataTabCandidate([active], usersTarget, { openMode: "default", reuseMode: "active-tab", activeTabId: active.id })).toEqual({ tab: active, match: "active-tab" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["pinned", { pinned: true }],
|
||||
["executing", { isExecuting: true }],
|
||||
["cancelling", { isCancelling: true }],
|
||||
["explaining", { isExplaining: true }],
|
||||
["manual transaction", { txnSessionId: "txn-1" }],
|
||||
["pending edits", { pendingDataChangeCount: 1 }],
|
||||
])("does not reuse the active tab when it is %s", (_label, patch) => {
|
||||
const active = Object.assign(dataTab("orders", "orders"), patch);
|
||||
|
||||
expect(findExistingDataTabCandidate([active], usersTarget, { openMode: "default", reuseMode: "active-tab", activeTabId: active.id })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not reuse an active tab from another database or catalog", () => {
|
||||
const otherDatabase = dataTab("orders", "orders");
|
||||
otherDatabase.database = "archive";
|
||||
const otherCatalog = dataTab("catalog-orders", "orders");
|
||||
otherCatalog.catalog = "analytics";
|
||||
|
||||
expect(findExistingDataTabCandidate([otherDatabase], usersTarget, { openMode: "default", reuseMode: "active-tab", activeTabId: otherDatabase.id })).toBeUndefined();
|
||||
expect(findExistingDataTabCandidate([otherCatalog], usersTarget, { openMode: "default", reuseMode: "active-tab", activeTabId: otherCatalog.id })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not reuse a same-name table from another schema", () => {
|
||||
const archiveUsers = dataTab("archive-users", "users", "archive");
|
||||
archiveUsers.tableMeta = { schema: "archive", tableName: "users", columns: [], primaryKeys: [] };
|
||||
|
||||
expect(findExistingDataTabCandidate([archiveUsers], usersTarget, { openMode: "default", reuseScope: "same-table" })).toBeUndefined();
|
||||
expect(findExistingDataTabCandidate([archiveUsers], usersTarget, { openMode: "default", reuseMode: "same-table" })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("allows metadata to update a tab that still points to the requested table", () => {
|
||||
@@ -86,7 +115,7 @@ describe("dataTabOpenPolicy", () => {
|
||||
tab.tableMeta = { schema: "public", tableName: "users", columns: [], primaryKeys: [] };
|
||||
|
||||
expect(canApplyDataTabMetadata(tab, usersTarget, new AbortController().signal)).toBe(true);
|
||||
expect(findExistingDataTabCandidate([tab], usersTarget, { openMode: "default", reuseScope: "same-table" })).toEqual({ tab, match: "same-table" });
|
||||
expect(findExistingDataTabCandidate([tab], usersTarget, { openMode: "default", reuseMode: "same-table" })).toEqual({ tab, match: "same-table" });
|
||||
});
|
||||
|
||||
it("ignores metadata query schemas for database-scoped tables", () => {
|
||||
@@ -96,7 +125,7 @@ describe("dataTabOpenPolicy", () => {
|
||||
const mysqlTarget = { connectionId: "conn", database: "app", tableName: "users" };
|
||||
|
||||
expect(canApplyDataTabMetadata(tab, mysqlTarget, new AbortController().signal)).toBe(true);
|
||||
expect(findExistingDataTabCandidate([tab], mysqlTarget, { openMode: "default", reuseScope: "same-table" })).toEqual({ tab, match: "same-table" });
|
||||
expect(findExistingDataTabCandidate([tab], mysqlTarget, { openMode: "default", reuseMode: "same-table" })).toEqual({ tab, match: "same-table" });
|
||||
});
|
||||
|
||||
it("rejects metadata after its request is cancelled", () => {
|
||||
|
||||
@@ -45,6 +45,10 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => {
|
||||
it("includes the regular expression match limit", () => {
|
||||
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("regexMaxMatchCount");
|
||||
});
|
||||
|
||||
it("includes the data-tab reuse mode", () => {
|
||||
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("dataTabReuseMode");
|
||||
});
|
||||
});
|
||||
|
||||
describe("editorSettingsDraftFromSettings", () => {
|
||||
@@ -115,6 +119,14 @@ describe("editorSettingsDraftChanged", () => {
|
||||
draft.savedSqlOpenTargetMode = "current";
|
||||
expect(editorSettingsDraftChanged(draft, base)).toBe(true);
|
||||
});
|
||||
|
||||
it("detects a data-tab reuse mode change", () => {
|
||||
const settings = makeSettings({ dataTabReuseMode: "same-table" });
|
||||
const draft = editorSettingsDraftFromSettings(settings);
|
||||
const base = editorSettingsDraftFromSettings(settings);
|
||||
draft.dataTabReuseMode = "active-tab";
|
||||
expect(editorSettingsDraftChanged(draft, base)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("editorSettingsPatchFromDraft", () => {
|
||||
@@ -150,6 +162,14 @@ describe("editorSettingsPatchFromDraft", () => {
|
||||
draft.savedSqlOpenTargetMode = "current";
|
||||
expect(editorSettingsPatchFromDraft(draft, base).savedSqlOpenTargetMode).toBe("current");
|
||||
});
|
||||
|
||||
it("includes the data-tab reuse mode when changed", () => {
|
||||
const settings = makeSettings({ dataTabReuseMode: "same-table" });
|
||||
const draft = editorSettingsDraftFromSettings(settings);
|
||||
const base = editorSettingsDraftFromSettings(settings);
|
||||
draft.dataTabReuseMode = "always-new";
|
||||
expect(editorSettingsPatchFromDraft(draft, base).dataTabReuseMode).toBe("always-new");
|
||||
});
|
||||
});
|
||||
|
||||
describe("EDITOR_SETTINGS_DRAFT_KEYS - tabLayout", () => {
|
||||
|
||||
@@ -46,7 +46,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"autoSelectActiveSidebarNode",
|
||||
"openTabsRestoreMode",
|
||||
"disconnectTabHandlingMode",
|
||||
"reuseDataTab",
|
||||
"dataTabReuseMode",
|
||||
"prefillNewQueryWithSelect",
|
||||
"updateNotificationsEnabled",
|
||||
"sidebarObjectInfoMode",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { matchesModifierOnlyShortcut, type ShortcutLikeEvent } from "@/lib/editor/keyboardShortcuts";
|
||||
import type { DataTabReuseMode } from "@/lib/tabs/dataTabReuseMode";
|
||||
import type { QueryTab, TreeNodeType } from "@/types/database";
|
||||
|
||||
export type DataTabOpenMode = "default" | "new-tab";
|
||||
export type DataTabReuseScope = "none" | "same-table";
|
||||
export type { DataTabReuseMode };
|
||||
|
||||
type DataTabLike = Pick<QueryTab, "id" | "mode" | "connectionId" | "database" | "schema" | "title" | "tableMeta" | "tableMetaUpdatedAt">;
|
||||
type DataTabLike = Pick<QueryTab, "id" | "mode" | "connectionId" | "database" | "catalog" | "schema" | "title" | "tableMeta" | "tableMetaUpdatedAt" | "pinned" | "isExecuting" | "isCancelling" | "isExplaining" | "txnSessionId" | "pendingDataChangeCount">;
|
||||
|
||||
export interface DataTabTarget {
|
||||
connectionId: string;
|
||||
@@ -16,7 +17,7 @@ export interface DataTabTarget {
|
||||
|
||||
export type ExistingDataTabCandidate<T extends DataTabLike> = {
|
||||
tab: T;
|
||||
match: "same-table";
|
||||
match: "same-table" | "active-tab";
|
||||
};
|
||||
|
||||
const dataNodeTypes = new Set<TreeNodeType>(["table", "view", "materialized_view"]);
|
||||
@@ -34,9 +35,27 @@ function isSameDatabase(tab: DataTabLike, target: Pick<DataTabTarget, "connectio
|
||||
return tab.mode === "data" && tab.connectionId === target.connectionId && tab.database === target.database;
|
||||
}
|
||||
|
||||
function dataTabCatalog(tab: DataTabLike): string {
|
||||
return tab.tableMeta?.catalog || tab.catalog || "";
|
||||
}
|
||||
|
||||
function isSameTable(tab: DataTabLike, target: DataTabTarget): boolean {
|
||||
const tabSchema = tab.schema || (target.schema ? tab.tableMeta?.schema : undefined) || "";
|
||||
return isSameDatabase(tab, target) && (tab.tableMeta?.catalog || "") === (target.catalog || "") && tabSchema === (target.schema || "") && (tab.tableMeta?.tableName || tab.title) === target.tableName;
|
||||
return isSameDatabase(tab, target) && dataTabCatalog(tab) === (target.catalog || "") && tabSchema === (target.schema || "") && (tab.tableMeta?.tableName || tab.title) === target.tableName;
|
||||
}
|
||||
|
||||
export function canReuseActiveDataTab(tab: DataTabLike | undefined, target: DataTabTarget): boolean {
|
||||
return (
|
||||
tab !== undefined &&
|
||||
isSameDatabase(tab, target) &&
|
||||
dataTabCatalog(tab) === (target.catalog || "") &&
|
||||
!tab.pinned &&
|
||||
!tab.isExecuting &&
|
||||
!tab.isCancelling &&
|
||||
!tab.isExplaining &&
|
||||
!tab.txnSessionId &&
|
||||
!tab.pendingDataChangeCount
|
||||
);
|
||||
}
|
||||
|
||||
export function canApplyDataTabMetadata(tab: DataTabLike | undefined, target: DataTabTarget, signal?: AbortSignal): boolean {
|
||||
@@ -48,10 +67,15 @@ export function dataTabMetadataNeedsRefresh(tab: DataTabLike, maxAgeMs: number,
|
||||
return now - tab.tableMetaUpdatedAt >= maxAgeMs;
|
||||
}
|
||||
|
||||
export function findExistingDataTabCandidate<T extends DataTabLike>(tabs: T[], target: DataTabTarget, options: { openMode: DataTabOpenMode; reuseScope: DataTabReuseScope }): ExistingDataTabCandidate<T> | undefined {
|
||||
if (options.openMode === "new-tab" || options.reuseScope === "none") return undefined;
|
||||
export function findExistingDataTabCandidate<T extends DataTabLike>(tabs: T[], target: DataTabTarget, options: { openMode: DataTabOpenMode; reuseMode: DataTabReuseMode; activeTabId?: string | null }): ExistingDataTabCandidate<T> | undefined {
|
||||
if (options.openMode === "new-tab" || options.reuseMode === "always-new") return undefined;
|
||||
|
||||
const sameTable = tabs.find((tab) => isSameTable(tab, target));
|
||||
if (sameTable) return { tab: sameTable, match: "same-table" };
|
||||
|
||||
if (options.reuseMode === "active-tab") {
|
||||
const activeTab = tabs.find((tab) => tab.id === options.activeTabId);
|
||||
if (activeTab && canReuseActiveDataTab(activeTab, target)) return { tab: activeTab, match: "active-tab" };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { QueryTab } from "@/types/database";
|
||||
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import type { DataTabReuseMode } from "@/lib/tabs/dataTabReuseMode";
|
||||
|
||||
export type DataTableDoubleClickAction = "activate" | "open" | "none";
|
||||
|
||||
@@ -9,9 +10,9 @@ export function canActivateExistingDataTableTab(tab: QueryTab, options: { activa
|
||||
return !!tab.result || !!tab.results?.length;
|
||||
}
|
||||
|
||||
export function dataTableDoubleClickAction(tab: QueryTab | undefined, activation: "single" | "double", reuseDataTab = true): DataTableDoubleClickAction {
|
||||
export function dataTableDoubleClickAction(tab: QueryTab | undefined, activation: "single" | "double", reuseMode: DataTabReuseMode = "same-table"): DataTableDoubleClickAction {
|
||||
if (activation === "single") return "none";
|
||||
if (!reuseDataTab) return "open";
|
||||
if (reuseMode === "always-new") return "open";
|
||||
if (!tab) return activation === "double" ? "open" : "none";
|
||||
if (!canActivateExistingDataTableTab(tab)) return "open";
|
||||
return "activate";
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const DATA_TAB_REUSE_MODES = ["always-new", "same-table", "active-tab"] as const;
|
||||
|
||||
export type DataTabReuseMode = (typeof DATA_TAB_REUSE_MODES)[number];
|
||||
|
||||
export const DEFAULT_DATA_TAB_REUSE_MODE: DataTabReuseMode = "same-table";
|
||||
|
||||
const dataTabReuseModes = new Set<DataTabReuseMode>(DATA_TAB_REUSE_MODES);
|
||||
|
||||
export function normalizeDataTabReuseMode(value: unknown, legacyReuseDataTab?: unknown): DataTabReuseMode {
|
||||
if (typeof value === "string" && dataTabReuseModes.has(value as DataTabReuseMode)) return value as DataTabReuseMode;
|
||||
if (legacyReuseDataTab === true) return "same-table";
|
||||
if (legacyReuseDataTab === false) return "always-new";
|
||||
return DEFAULT_DATA_TAB_REUSE_MODE;
|
||||
}
|
||||
@@ -5,6 +5,15 @@ import { isProxy } from "vue";
|
||||
import type { AiConfigItem } from "@/types/ai";
|
||||
|
||||
describe("normalizeEditorSettings", () => {
|
||||
it("defaults and migrates the data-tab reuse mode", () => {
|
||||
expect(normalizeEditorSettings({}).dataTabReuseMode).toBe("same-table");
|
||||
expect(normalizeEditorSettings({ dataTabReuseMode: "always-new" }).dataTabReuseMode).toBe("always-new");
|
||||
expect(normalizeEditorSettings({ dataTabReuseMode: "active-tab" }).dataTabReuseMode).toBe("active-tab");
|
||||
expect(normalizeEditorSettings({ dataTabReuseMode: "invalid" } as any).dataTabReuseMode).toBe("same-table");
|
||||
expect(normalizeEditorSettings({ reuseDataTab: false } as any).dataTabReuseMode).toBe("always-new");
|
||||
expect(normalizeEditorSettings({ reuseDataTab: true } as any).dataTabReuseMode).toBe("same-table");
|
||||
});
|
||||
|
||||
it("defaults and bounds the regular expression match limit", () => {
|
||||
expect(normalizeEditorSettings({}).regexMaxMatchCount).toBe(1000);
|
||||
expect(normalizeEditorSettings({ regexMaxMatchCount: 2500 }).regexMaxMatchCount).toBe(2500);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type Sql
|
||||
import { normalizeSqlVariableSyntaxOverrides, type SqlVariableSyntaxOverrides } from "@/lib/sql/sqlVariableSyntax";
|
||||
import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
|
||||
import type { SidebarActivation } from "@/lib/sidebar/treeNodeClick";
|
||||
import { DEFAULT_DATA_TAB_REUSE_MODE, normalizeDataTabReuseMode, type DataTabReuseMode } from "@/lib/tabs/dataTabReuseMode";
|
||||
import type { SqlSnippet, TableInfoTab } from "@/types/database";
|
||||
import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion";
|
||||
import { setDebugLoggingEnabled } from "@/lib/backend/debugLog";
|
||||
@@ -22,6 +23,7 @@ import type { AiProvider, AiApiStyle, AiAuthMethod, AiEffortLevel, AiReasoningLe
|
||||
|
||||
export type { AiProvider, AiApiStyle, AiAuthMethod, AiEffortLevel, AiReasoningLevel, AiConfiguredModel, AiConfig, AiTestConnectionResult, AiConfigItem, AiChatSelectionState, AiEffortSelection };
|
||||
export type { SavedSqlOpenTargetMode };
|
||||
export type { DataTabReuseMode };
|
||||
|
||||
export interface DesktopSettings {
|
||||
show_tray_icon: boolean;
|
||||
@@ -500,7 +502,7 @@ export interface EditorSettings {
|
||||
autoSelectActiveSidebarNode: boolean;
|
||||
openTabsRestoreMode: OpenTabsRestoreMode;
|
||||
disconnectTabHandlingMode: DisconnectTabHandlingMode;
|
||||
reuseDataTab: boolean;
|
||||
dataTabReuseMode: DataTabReuseMode;
|
||||
prefillNewQueryWithSelect: boolean;
|
||||
updateNotificationsEnabled: boolean;
|
||||
sidebarHiddenTablePrefixes: string[];
|
||||
@@ -678,7 +680,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
autoSelectActiveSidebarNode: false,
|
||||
openTabsRestoreMode: "all",
|
||||
disconnectTabHandlingMode: "close-tabs",
|
||||
reuseDataTab: false,
|
||||
dataTabReuseMode: DEFAULT_DATA_TAB_REUSE_MODE,
|
||||
prefillNewQueryWithSelect: true,
|
||||
updateNotificationsEnabled: true,
|
||||
sidebarHiddenTablePrefixes: [],
|
||||
@@ -1001,7 +1003,14 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
}
|
||||
).closeQueryTabsOnDisconnect,
|
||||
),
|
||||
reuseDataTab: settings.reuseDataTab ?? DEFAULT_EDITOR_SETTINGS.reuseDataTab,
|
||||
dataTabReuseMode: normalizeDataTabReuseMode(
|
||||
settings.dataTabReuseMode,
|
||||
(
|
||||
settings as Partial<EditorSettings> & {
|
||||
reuseDataTab?: boolean;
|
||||
}
|
||||
).reuseDataTab,
|
||||
),
|
||||
prefillNewQueryWithSelect: typeof settings.prefillNewQueryWithSelect === "boolean" ? settings.prefillNewQueryWithSelect : DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect,
|
||||
updateNotificationsEnabled: settings.updateNotificationsEnabled ?? DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled,
|
||||
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
|
||||
@@ -1473,7 +1482,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.autoSelectActiveSidebarNode !== undefined) editorSettings.value.autoSelectActiveSidebarNode = partial.autoSelectActiveSidebarNode;
|
||||
if (partial.openTabsRestoreMode !== undefined) editorSettings.value.openTabsRestoreMode = normalizeOpenTabsRestoreMode(partial.openTabsRestoreMode);
|
||||
if (partial.disconnectTabHandlingMode !== undefined) editorSettings.value.disconnectTabHandlingMode = normalizeDisconnectTabHandlingMode(partial.disconnectTabHandlingMode);
|
||||
if (partial.reuseDataTab !== undefined) editorSettings.value.reuseDataTab = partial.reuseDataTab;
|
||||
if (partial.dataTabReuseMode !== undefined) editorSettings.value.dataTabReuseMode = normalizeDataTabReuseMode(partial.dataTabReuseMode);
|
||||
if (partial.prefillNewQueryWithSelect !== undefined) editorSettings.value.prefillNewQueryWithSelect = partial.prefillNewQueryWithSelect;
|
||||
if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled;
|
||||
if (partial.sidebarHiddenTablePrefixes !== undefined) editorSettings.value.sidebarHiddenTablePrefixes = normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes);
|
||||
|
||||
@@ -1058,6 +1058,7 @@ export interface QueryTab {
|
||||
primaryKeys: string[];
|
||||
};
|
||||
tableMetaUpdatedAt?: number;
|
||||
pendingDataChangeCount?: number;
|
||||
/** 冷缓存打开表数据时元数据仍在途:行标识未知,编辑/保存必须等待其落地 */
|
||||
tableMetaPending?: boolean;
|
||||
/** 取消请求单调计数:isCancelling 是瞬态的(取消失败/查询先完成会被清),
|
||||
|
||||
Reference in new issue
Block a user