feat(editor): handle tabs after connection disconnect
This commit is contained in:
1 parent
3cb1faf1c6
commit
28cc1b9768
31 files changed
+1137
-60
No files matched your search
@@ -2820,6 +2820,30 @@ watch(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 删除连接时若开启了「记住连接名与数据库」,新建同名**同类型**连接会自动选中记住的数据库。
|
||||||
|
// 只在数据库字段为空、或仍是上一次自动回填的值时才覆盖,避免抢走用户手输的内容。
|
||||||
|
const lastRememberedDatabaseAutofill = ref("");
|
||||||
|
watch(
|
||||||
|
() => [open.value, editingId.value, form.value.name, form.value.db_type] as const,
|
||||||
|
([isOpen, editing, rawName, dbType]) => {
|
||||||
|
if (!isOpen || editing) {
|
||||||
|
lastRememberedDatabaseAutofill.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = (rawName ?? "").trim();
|
||||||
|
const remembered = name ? settingsStore.rememberedDatabaseForConnection(name, dbType) : "";
|
||||||
|
const current = (form.value.database ?? "").trim();
|
||||||
|
if (current === remembered) {
|
||||||
|
lastRememberedDatabaseAutofill.value = remembered;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (current && current !== lastRememberedDatabaseAutofill.value) return;
|
||||||
|
form.value.database = remembered || undefined;
|
||||||
|
lastRememberedDatabaseAutofill.value = remembered;
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
const databaseLabel = computed(() => {
|
const databaseLabel = computed(() => {
|
||||||
if (form.value.db_type === "oracle" && form.value.oracle_connection_type === "tns") return t("connection.oracleTnsAlias");
|
if (form.value.db_type === "oracle" && form.value.oracle_connection_type === "tns") return t("connection.oracleTnsAlias");
|
||||||
if (form.value.db_type === "oracle") return t("connection.serviceName");
|
if (form.value.db_type === "oracle") return t("connection.serviceName");
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ import {
|
|||||||
type DesktopIconTheme,
|
type DesktopIconTheme,
|
||||||
type InterfaceLayout,
|
type InterfaceLayout,
|
||||||
type DisconnectTabHandlingMode,
|
type DisconnectTabHandlingMode,
|
||||||
|
type DeleteConnectionTabHandlingMode,
|
||||||
type DataTabReuseMode,
|
type DataTabReuseMode,
|
||||||
type DataGridFilterEditorView,
|
type DataGridFilterEditorView,
|
||||||
type MultiStatementDefaultView,
|
type MultiStatementDefaultView,
|
||||||
@@ -723,6 +724,15 @@ const editAutoSelectActiveSidebarNode = ref(settingsStore.editorSettings.autoSel
|
|||||||
const editSidebarBrowseObjectsOnDatabaseActivation = ref(settingsStore.editorSettings.sidebarBrowseObjectsOnDatabaseActivation);
|
const editSidebarBrowseObjectsOnDatabaseActivation = ref(settingsStore.editorSettings.sidebarBrowseObjectsOnDatabaseActivation);
|
||||||
const editOpenTabsRestoreMode = ref<OpenTabsRestoreMode>(settingsStore.editorSettings.openTabsRestoreMode);
|
const editOpenTabsRestoreMode = ref<OpenTabsRestoreMode>(settingsStore.editorSettings.openTabsRestoreMode);
|
||||||
const editDisconnectTabHandlingMode = ref<DisconnectTabHandlingMode>(settingsStore.editorSettings.disconnectTabHandlingMode);
|
const editDisconnectTabHandlingMode = ref<DisconnectTabHandlingMode>(settingsStore.editorSettings.disconnectTabHandlingMode);
|
||||||
|
const editDeleteConnectionTabHandlingMode = ref<DeleteConnectionTabHandlingMode>(settingsStore.editorSettings.deleteConnectionTabHandlingMode);
|
||||||
|
const editRememberConnectionDatabaseOnDelete = ref(settingsStore.editorSettings.rememberConnectionDatabaseOnDelete);
|
||||||
|
// 「记住的连接名 → 数据库」是累积数据而非偏好设置,不进弹窗草稿;这里直接读写 store,
|
||||||
|
// 让清除操作立即生效,不受弹窗的保存/取消影响。
|
||||||
|
const rememberedConnectionDatabaseCount = computed(() => Object.keys(settingsStore.editorSettings.rememberedConnectionDatabases).length);
|
||||||
|
|
||||||
|
function clearRememberedConnectionDatabases() {
|
||||||
|
settingsStore.clearRememberedConnectionDatabases();
|
||||||
|
}
|
||||||
const editDataTabReuseMode = ref<DataTabReuseMode>(settingsStore.editorSettings.dataTabReuseMode);
|
const editDataTabReuseMode = ref<DataTabReuseMode>(settingsStore.editorSettings.dataTabReuseMode);
|
||||||
const editOpenDataTabsNextToActive = ref(settingsStore.editorSettings.openDataTabsNextToActive);
|
const editOpenDataTabsNextToActive = ref(settingsStore.editorSettings.openDataTabsNextToActive);
|
||||||
const editPrefillNewQueryWithSelect = ref(settingsStore.editorSettings.prefillNewQueryWithSelect);
|
const editPrefillNewQueryWithSelect = ref(settingsStore.editorSettings.prefillNewQueryWithSelect);
|
||||||
@@ -798,6 +808,20 @@ const disconnectTabHandlingModeDescriptionKey = computed(() => {
|
|||||||
|
|
||||||
return "disconnectTabHandlingModeCloseTabsDescription";
|
return "disconnectTabHandlingModeCloseTabsDescription";
|
||||||
});
|
});
|
||||||
|
const deleteConnectionTabHandlingModeDescriptionKey = computed(() => {
|
||||||
|
switch (editDeleteConnectionTabHandlingMode.value) {
|
||||||
|
case "close-tabs":
|
||||||
|
return "deleteConnectionTabHandlingModeCloseTabsDescription";
|
||||||
|
case "keep-sql-tabs":
|
||||||
|
return "deleteConnectionTabHandlingModeKeepSqlTabsDescription";
|
||||||
|
case "keep-pinned-sql-tabs":
|
||||||
|
return "deleteConnectionTabHandlingModeKeepPinnedSqlTabsDescription";
|
||||||
|
case "keep-all-tabs":
|
||||||
|
return "deleteConnectionTabHandlingModeKeepAllTabsDescription";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "deleteConnectionTabHandlingModeCloseTabsDescription";
|
||||||
|
});
|
||||||
const normalizedEditTableColumnTemplateFields = computed(() => tableColumnTemplateRowsToSettings(editTableColumnTemplateRows.value));
|
const normalizedEditTableColumnTemplateFields = computed(() => tableColumnTemplateRowsToSettings(editTableColumnTemplateRows.value));
|
||||||
const visibleTableColumnTemplateRows = computed(() =>
|
const visibleTableColumnTemplateRows = computed(() =>
|
||||||
editTableColumnTemplateRows.value.filter((row) => {
|
editTableColumnTemplateRows.value.filter((row) => {
|
||||||
@@ -969,6 +993,8 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
|||||||
sidebarBrowseObjectsOnDatabaseActivation: editSidebarBrowseObjectsOnDatabaseActivation.value,
|
sidebarBrowseObjectsOnDatabaseActivation: editSidebarBrowseObjectsOnDatabaseActivation.value,
|
||||||
openTabsRestoreMode: editOpenTabsRestoreMode.value,
|
openTabsRestoreMode: editOpenTabsRestoreMode.value,
|
||||||
disconnectTabHandlingMode: editDisconnectTabHandlingMode.value,
|
disconnectTabHandlingMode: editDisconnectTabHandlingMode.value,
|
||||||
|
deleteConnectionTabHandlingMode: editDeleteConnectionTabHandlingMode.value,
|
||||||
|
rememberConnectionDatabaseOnDelete: editRememberConnectionDatabaseOnDelete.value,
|
||||||
dataTabReuseMode: editDataTabReuseMode.value,
|
dataTabReuseMode: editDataTabReuseMode.value,
|
||||||
openDataTabsNextToActive: editOpenDataTabsNextToActive.value,
|
openDataTabsNextToActive: editOpenDataTabsNextToActive.value,
|
||||||
prefillNewQueryWithSelect: editPrefillNewQueryWithSelect.value,
|
prefillNewQueryWithSelect: editPrefillNewQueryWithSelect.value,
|
||||||
@@ -1610,6 +1636,8 @@ function syncEditorSettingsDraftFromStore() {
|
|||||||
editSidebarBrowseObjectsOnDatabaseActivation.value = settingsStore.editorSettings.sidebarBrowseObjectsOnDatabaseActivation;
|
editSidebarBrowseObjectsOnDatabaseActivation.value = settingsStore.editorSettings.sidebarBrowseObjectsOnDatabaseActivation;
|
||||||
editOpenTabsRestoreMode.value = settingsStore.editorSettings.openTabsRestoreMode;
|
editOpenTabsRestoreMode.value = settingsStore.editorSettings.openTabsRestoreMode;
|
||||||
editDisconnectTabHandlingMode.value = settingsStore.editorSettings.disconnectTabHandlingMode;
|
editDisconnectTabHandlingMode.value = settingsStore.editorSettings.disconnectTabHandlingMode;
|
||||||
|
editDeleteConnectionTabHandlingMode.value = settingsStore.editorSettings.deleteConnectionTabHandlingMode;
|
||||||
|
editRememberConnectionDatabaseOnDelete.value = settingsStore.editorSettings.rememberConnectionDatabaseOnDelete;
|
||||||
editDataTabReuseMode.value = settingsStore.editorSettings.dataTabReuseMode;
|
editDataTabReuseMode.value = settingsStore.editorSettings.dataTabReuseMode;
|
||||||
editOpenDataTabsNextToActive.value = settingsStore.editorSettings.openDataTabsNextToActive;
|
editOpenDataTabsNextToActive.value = settingsStore.editorSettings.openDataTabsNextToActive;
|
||||||
editPrefillNewQueryWithSelect.value = settingsStore.editorSettings.prefillNewQueryWithSelect;
|
editPrefillNewQueryWithSelect.value = settingsStore.editorSettings.prefillNewQueryWithSelect;
|
||||||
@@ -1737,6 +1765,8 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
|
|||||||
sidebarBrowseObjectsOnDatabaseActivation: editSidebarBrowseObjectsOnDatabaseActivation,
|
sidebarBrowseObjectsOnDatabaseActivation: editSidebarBrowseObjectsOnDatabaseActivation,
|
||||||
openTabsRestoreMode: editOpenTabsRestoreMode,
|
openTabsRestoreMode: editOpenTabsRestoreMode,
|
||||||
disconnectTabHandlingMode: editDisconnectTabHandlingMode,
|
disconnectTabHandlingMode: editDisconnectTabHandlingMode,
|
||||||
|
deleteConnectionTabHandlingMode: editDeleteConnectionTabHandlingMode,
|
||||||
|
rememberConnectionDatabaseOnDelete: editRememberConnectionDatabaseOnDelete,
|
||||||
dataTabReuseMode: editDataTabReuseMode,
|
dataTabReuseMode: editDataTabReuseMode,
|
||||||
openDataTabsNextToActive: editOpenDataTabsNextToActive,
|
openDataTabsNextToActive: editOpenDataTabsNextToActive,
|
||||||
prefillNewQueryWithSelect: editPrefillNewQueryWithSelect,
|
prefillNewQueryWithSelect: editPrefillNewQueryWithSelect,
|
||||||
@@ -2177,6 +2207,8 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
|||||||
editSidebarBrowseObjectsOnDatabaseActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarBrowseObjectsOnDatabaseActivation;
|
editSidebarBrowseObjectsOnDatabaseActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarBrowseObjectsOnDatabaseActivation;
|
||||||
editOpenTabsRestoreMode.value = DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode;
|
editOpenTabsRestoreMode.value = DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode;
|
||||||
editDisconnectTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode;
|
editDisconnectTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode;
|
||||||
|
editDeleteConnectionTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.deleteConnectionTabHandlingMode;
|
||||||
|
editRememberConnectionDatabaseOnDelete.value = DEFAULT_EDITOR_SETTINGS.rememberConnectionDatabaseOnDelete;
|
||||||
editDataTabReuseMode.value = DEFAULT_EDITOR_SETTINGS.dataTabReuseMode;
|
editDataTabReuseMode.value = DEFAULT_EDITOR_SETTINGS.dataTabReuseMode;
|
||||||
editOpenDataTabsNextToActive.value = DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive;
|
editOpenDataTabsNextToActive.value = DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive;
|
||||||
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
|
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
|
||||||
@@ -2339,6 +2371,8 @@ function resetAllDefaults() {
|
|||||||
editSidebarBrowseObjectsOnDatabaseActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarBrowseObjectsOnDatabaseActivation;
|
editSidebarBrowseObjectsOnDatabaseActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarBrowseObjectsOnDatabaseActivation;
|
||||||
editOpenTabsRestoreMode.value = DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode;
|
editOpenTabsRestoreMode.value = DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode;
|
||||||
editDisconnectTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode;
|
editDisconnectTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode;
|
||||||
|
editDeleteConnectionTabHandlingMode.value = DEFAULT_EDITOR_SETTINGS.deleteConnectionTabHandlingMode;
|
||||||
|
editRememberConnectionDatabaseOnDelete.value = DEFAULT_EDITOR_SETTINGS.rememberConnectionDatabaseOnDelete;
|
||||||
editDataTabReuseMode.value = DEFAULT_EDITOR_SETTINGS.dataTabReuseMode;
|
editDataTabReuseMode.value = DEFAULT_EDITOR_SETTINGS.dataTabReuseMode;
|
||||||
editOpenDataTabsNextToActive.value = DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive;
|
editOpenDataTabsNextToActive.value = DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive;
|
||||||
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
|
editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect;
|
||||||
@@ -2595,6 +2629,12 @@ function onDisconnectTabHandlingModeChange(v: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDeleteConnectionTabHandlingModeChange(v: any) {
|
||||||
|
if (v === "close-tabs" || v === "keep-sql-tabs" || v === "keep-pinned-sql-tabs" || v === "keep-all-tabs") {
|
||||||
|
editDeleteConnectionTabHandlingMode.value = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onLocaleChange(v: any) {
|
function onLocaleChange(v: any) {
|
||||||
if (typeof v === "string") void setLocale(v as Locale);
|
if (typeof v === "string") void setLocale(v as Locale);
|
||||||
}
|
}
|
||||||
@@ -7122,6 +7162,52 @@ onUnmounted(() => {
|
|||||||
{{ t(`settings.${disconnectTabHandlingModeDescriptionKey}`) }}
|
{{ t(`settings.${disconnectTabHandlingModeDescriptionKey}`) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-item space-y-2 rounded-md border bg-muted/20 px-3 py-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Label for="delete-connection-tab-handling-mode">{{ t("settings.deleteConnectionTabHandlingMode") }}</Label>
|
||||||
|
<HelpTooltip :label="t('settings.deleteConnectionTabHandlingMode')">
|
||||||
|
{{ t("settings.deleteConnectionTabHandlingModeDescription") }}
|
||||||
|
</HelpTooltip>
|
||||||
|
</div>
|
||||||
|
<Select :model-value="editDeleteConnectionTabHandlingMode" @update:model-value="onDeleteConnectionTabHandlingModeChange">
|
||||||
|
<SelectTrigger id="delete-connection-tab-handling-mode" class="w-full">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="close-tabs">{{ t("settings.deleteConnectionTabHandlingModeCloseTabs") }}</SelectItem>
|
||||||
|
<SelectItem value="keep-sql-tabs">{{ t("settings.deleteConnectionTabHandlingModeKeepSqlTabs") }}</SelectItem>
|
||||||
|
<SelectItem value="keep-pinned-sql-tabs">
|
||||||
|
{{ t("settings.deleteConnectionTabHandlingModeKeepPinnedSqlTabs") }}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="keep-all-tabs">{{ t("settings.deleteConnectionTabHandlingModeKeepAllTabs") }}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
{{ t(`settings.${deleteConnectionTabHandlingModeDescriptionKey}`) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item flex items-center justify-between gap-3 rounded-md border bg-muted/20 px-3 py-2">
|
||||||
|
<div class="min-w-0 space-y-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Label for="remember-connection-database-on-delete">{{ t("settings.rememberConnectionDatabaseOnDelete") }}</Label>
|
||||||
|
<HelpTooltip :label="t('settings.rememberConnectionDatabaseOnDelete')">
|
||||||
|
{{ t("settings.rememberConnectionDatabaseOnDeleteDescription") }}
|
||||||
|
</HelpTooltip>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
{{ t("settings.rememberConnectionDatabaseOnDeleteHint") }}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="text-xs text-muted-foreground">
|
||||||
|
{{ t("settings.rememberedConnectionDatabaseCount", { count: rememberedConnectionDatabaseCount }) }}
|
||||||
|
</span>
|
||||||
|
<Button variant="outline" size="sm" :disabled="rememberedConnectionDatabaseCount === 0" @click="clearRememberedConnectionDatabases">
|
||||||
|
{{ t("settings.clearRememberedConnectionDatabases") }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch id="remember-connection-database-on-delete" v-model="editRememberConnectionDatabaseOnDelete" />
|
||||||
|
</div>
|
||||||
<div class="settings-item space-y-2 rounded-md border bg-muted/20 px-3 py-2">
|
<div class="settings-item space-y-2 rounded-md border bg-muted/20 px-3 py-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Label for="sidebar-object-info-mode">{{ t("settings.sidebarObjectInfoMode") }}</Label>
|
<Label for="sidebar-object-info-mode">{{ t("settings.sidebarObjectInfoMode") }}</Label>
|
||||||
|
|||||||
@@ -153,7 +153,8 @@ async function confirmDeleteSelectedConnections() {
|
|||||||
try {
|
try {
|
||||||
await connectionStore.removeConnections(ids);
|
await connectionStore.removeConnections(ids);
|
||||||
for (const connectionId of ids) {
|
for (const connectionId of ids) {
|
||||||
connectionStore.disconnect(connectionId).catch((error) => {
|
// 页签已由 removeConnections 按「删除连接」策略处理,这里只清会话。
|
||||||
|
connectionStore.disconnect(connectionId, { skipTabHandling: true }).catch((error) => {
|
||||||
console.warn("[DBX][connection:delete:disconnect-failed]", { connectionId, error });
|
console.warn("[DBX][connection:delete:disconnect-failed]", { connectionId, error });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -416,13 +416,14 @@ function tabTitleText(tab: QueryTab) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tabConnectionLabel(tab: QueryTab) {
|
function tabConnectionLabel(tab: QueryTab) {
|
||||||
return connectionStore.getConfig(tab.connectionId)?.name || tab.connectionId;
|
// 连接已删除但页签被保留时,回退到原连接名,避免显示成裸 uuid。
|
||||||
|
return connectionStore.getConfig(tab.connectionId)?.name || tab.detachedConnectionName || tab.connectionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function tabConnectionTargetLabel(tab: QueryTab) {
|
function tabConnectionTargetLabel(tab: QueryTab) {
|
||||||
const connection = connectionStore.getConfig(tab.connectionId);
|
const connection = connectionStore.getConfig(tab.connectionId);
|
||||||
const host = connection?.host.trim();
|
const host = connection?.host.trim();
|
||||||
return connection && host ? `${host}:${connection.port}` : tab.connectionId;
|
return connection && host ? `${host}:${connection.port}` : tab.detachedConnectionName || tab.connectionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function databaseTabGroupBaseLabel(tab: QueryTab) {
|
function databaseTabGroupBaseLabel(tab: QueryTab) {
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { createPinia, setActivePinia } from "pinia";
|
||||||
|
import { shallowRef } from "vue";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { ConnectionConfig, TreeNode } from "@/types/database";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({ toast: vi.fn() }));
|
||||||
|
|
||||||
|
vi.mock("vue-i18n", async (importOriginal) => ({
|
||||||
|
...(await importOriginal<typeof import("vue-i18n")>()),
|
||||||
|
useI18n: () => ({ t: (key: string, params?: Record<string, unknown>) => (params ? `${key}:${JSON.stringify(params)}` : key) }),
|
||||||
|
}));
|
||||||
|
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: mocks.toast }) }));
|
||||||
|
|
||||||
|
function installLocalStorage() {
|
||||||
|
const data = new Map<string, string>();
|
||||||
|
vi.stubGlobal("localStorage", {
|
||||||
|
getItem: vi.fn((key: string) => data.get(key) ?? null),
|
||||||
|
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
|
||||||
|
removeItem: vi.fn((key: string) => data.delete(key)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function installApiMocks() {
|
||||||
|
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||||
|
vi.doMock("@/lib/backend/api", () => ({
|
||||||
|
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||||
|
connectDb: vi.fn().mockResolvedValue("preview-1"),
|
||||||
|
disconnectDb: vi.fn().mockResolvedValue(undefined),
|
||||||
|
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||||
|
loadConnections: vi.fn().mockResolvedValue([]),
|
||||||
|
loadEditorSettings: vi.fn().mockResolvedValue(null),
|
||||||
|
loadPinnedTreeNodeIds: vi.fn().mockResolvedValue([]),
|
||||||
|
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||||
|
loadTunnelProfiles: vi.fn().mockResolvedValue([]),
|
||||||
|
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||||
|
saveEditorSettings: vi.fn().mockResolvedValue(undefined),
|
||||||
|
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||||
|
loadSidebarLayout: vi.fn().mockResolvedValue(null),
|
||||||
|
loadTableVGroups: vi.fn().mockResolvedValue({}),
|
||||||
|
saveTableVGroups: vi.fn().mockResolvedValue(undefined),
|
||||||
|
deleteTableVGroupsForConnection: vi.fn().mockResolvedValue(undefined),
|
||||||
|
saveOpenTabsState: vi.fn().mockResolvedValue(undefined),
|
||||||
|
loadOpenTabsState: vi.fn().mockResolvedValue(null),
|
||||||
|
closeQuerySession: vi.fn().mockResolvedValue(undefined),
|
||||||
|
closeClientConnectionSession: vi.fn().mockResolvedValue(undefined),
|
||||||
|
connectionDatabaseInfo: vi.fn().mockResolvedValue(undefined),
|
||||||
|
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
||||||
|
sessionCredentialStatus: vi.fn().mockResolvedValue(false),
|
||||||
|
forgetSessionCredential: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
vi.doMock("@/lib/tabs/tabResultCache", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@/lib/tabs/tabResultCache")>();
|
||||||
|
return { ...actual, deleteTabResultSnapshotsForOwner: vi.fn().mockResolvedValue(undefined) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pgConnection(id: string, name: string, database?: string): ConnectionConfig {
|
||||||
|
return { id, name, db_type: "postgres", host: "127.0.0.1", port: 5432, username: "postgres", password: "", database, read_only: false } as ConnectionConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectionNode(connectionId: string): TreeNode {
|
||||||
|
return { id: connectionId, label: connectionId, type: "connection", connectionId, isExpanded: false, children: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("删除连接的真实侧边栏流程", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
installLocalStorage();
|
||||||
|
installApiMocks();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keep-all-tabs:走 confirmDelete 后 SQL 页签仍在", async () => {
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||||
|
const { useSidebarConnectionMutationRuntime } = await import("@/composables/useSidebarConnectionMutationRuntime");
|
||||||
|
const { showDeleteConfirm } = await import("@/components/sidebar/sidebarTreeDialogState");
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
settingsStore.updateEditorSettings({ deleteConnectionTabHandlingMode: "keep-all-tabs", rememberConnectionDatabaseOnDelete: true });
|
||||||
|
|
||||||
|
const connectionStore = useConnectionStore();
|
||||||
|
connectionStore.connections = [pgConnection("conn-a", "prod", "app")];
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
const sqlId = queryStore.createTab("conn-a", "app", "draft.sql", "query");
|
||||||
|
queryStore.updateSql(sqlId, "select 1;");
|
||||||
|
|
||||||
|
const node = connectionNode("conn-a");
|
||||||
|
const runtime = useSidebarConnectionMutationRuntime({
|
||||||
|
activeNode: shallowRef(node),
|
||||||
|
releaseActiveNodeReference: vi.fn(),
|
||||||
|
selectedTreeNodesInVisibleOrder: () => [node],
|
||||||
|
connectionStore,
|
||||||
|
queryStore,
|
||||||
|
requestGroupRename: vi.fn(),
|
||||||
|
openVisibleDatabases: vi.fn(),
|
||||||
|
openVisibleSchemas: vi.fn(),
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
runtime.deleteConnection();
|
||||||
|
expect(showDeleteConfirm.value).toBe(true);
|
||||||
|
await runtime.confirmDelete();
|
||||||
|
|
||||||
|
expect(connectionStore.connections.some((c) => c.id === "conn-a")).toBe(false);
|
||||||
|
expect(queryStore.tabs.some((tab) => tab.id === sqlId)).toBe(true);
|
||||||
|
expect(settingsStore.editorSettings.rememberedConnectionDatabases.prod).toEqual({ database: "app", dbType: "postgres" });
|
||||||
|
});
|
||||||
|
});
|
||||||
Vendored
+35
-2
@@ -1,6 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { shallowRef } from "vue";
|
import { shallowRef } from "vue";
|
||||||
import { connectionGroupDeleteTargetSnapshot, deleteConnectionsWithGroup, showDeleteGroupConfirm, sidebarFormTarget } from "@/components/sidebar/sidebarTreeDialogState";
|
import { connectionDeleteTargetSnapshot, connectionGroupDeleteTargetSnapshot, deleteConnectionsWithGroup, showDeleteConfirm, showDeleteGroupConfirm, sidebarFormTarget } from "@/components/sidebar/sidebarTreeDialogState";
|
||||||
import type { TreeNode } from "@/types/database";
|
import type { TreeNode } from "@/types/database";
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
@@ -174,6 +174,35 @@ describe("sidebar connection move selection", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("sidebar connection deletion selection", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
sidebarFormTarget.value = null;
|
||||||
|
connectionDeleteTargetSnapshot.value = [];
|
||||||
|
showDeleteConfirm.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes the selected connections and leaves tab handling to the delete policy", async () => {
|
||||||
|
const nodes = [connectionNode("conn-1"), connectionNode("conn-2")];
|
||||||
|
const store = connectionStore(nodes.map((node) => node.id));
|
||||||
|
const { deleteConnection, confirmDelete } = runtime(nodes[0], store, nodes);
|
||||||
|
|
||||||
|
deleteConnection();
|
||||||
|
expect(showDeleteConfirm.value).toBe(true);
|
||||||
|
|
||||||
|
await confirmDelete();
|
||||||
|
|
||||||
|
expect(store.removeConnections).toHaveBeenCalledWith(["conn-1", "conn-2"]);
|
||||||
|
// removeConnections 已按「删除连接」策略处理页签,disconnect 必须跳过断开策略,
|
||||||
|
// 否则会把刚保留下来的 SQL 页签又关掉。
|
||||||
|
expect(store.disconnect.mock.calls).toEqual([
|
||||||
|
["conn-1", { skipTabHandling: true }],
|
||||||
|
["conn-2", { skipTabHandling: true }],
|
||||||
|
]);
|
||||||
|
expect(mocks.toast).toHaveBeenCalledWith('connection.deletedSelected:{"count":2}', 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("sidebar connection group deletion selection", () => {
|
describe("sidebar connection group deletion selection", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -221,7 +250,11 @@ describe("sidebar connection group deletion selection", () => {
|
|||||||
|
|
||||||
expect(store.removeConnections).not.toHaveBeenCalled();
|
expect(store.removeConnections).not.toHaveBeenCalled();
|
||||||
expect(store.deleteConnectionGroups).toHaveBeenCalledWith(["group-1", "group-2"], true);
|
expect(store.deleteConnectionGroups).toHaveBeenCalledWith(["group-1", "group-2"], true);
|
||||||
expect(store.disconnect.mock.calls).toEqual([["conn-1"], ["conn-2"]]);
|
// 页签已由 deleteConnectionGroups 按「删除连接」策略处理,disconnect 只清会话。
|
||||||
|
expect(store.disconnect.mock.calls).toEqual([
|
||||||
|
["conn-1", { skipTabHandling: true }],
|
||||||
|
["conn-2", { skipTabHandling: true }],
|
||||||
|
]);
|
||||||
expect(showDeleteGroupConfirm.value).toBe(false);
|
expect(showDeleteGroupConfirm.value).toBe(false);
|
||||||
expect(connectionGroupDeleteTargetSnapshot.value).toEqual([]);
|
expect(connectionGroupDeleteTargetSnapshot.value).toEqual([]);
|
||||||
expect(deleteConnectionsWithGroup.value).toBe(false);
|
expect(deleteConnectionsWithGroup.value).toBe(false);
|
||||||
|
|||||||
@@ -116,7 +116,9 @@ export function useSidebarConnectionMutationRuntime(options: SidebarConnectionMu
|
|||||||
await connectionStore.removeConnections(connectionIds);
|
await connectionStore.removeConnections(connectionIds);
|
||||||
options.releaseActiveNodeReference(targets.map((target) => target.id));
|
options.releaseActiveNodeReference(targets.map((target) => target.id));
|
||||||
for (const connectionId of connectionIds) {
|
for (const connectionId of connectionIds) {
|
||||||
connectionStore.disconnect(connectionId).catch((error) => {
|
// 页签已由 removeConnections 按「删除连接」策略处理,这里只清会话,
|
||||||
|
// 避免再套用「断开连接」策略把刚保留的 SQL 页签关掉。
|
||||||
|
connectionStore.disconnect(connectionId, { skipTabHandling: true }).catch((error) => {
|
||||||
// Removal has already succeeded; disconnect cleanup must not turn it into a failed delete.
|
// Removal has already succeeded; disconnect cleanup must not turn it into a failed delete.
|
||||||
console.warn("[DBX][connection:delete:disconnect-failed]", { connectionId, error });
|
console.warn("[DBX][connection:delete:disconnect-failed]", { connectionId, error });
|
||||||
});
|
});
|
||||||
@@ -412,7 +414,8 @@ export function useSidebarConnectionMutationRuntime(options: SidebarConnectionMu
|
|||||||
const connectionIds = await connectionStore.deleteConnectionGroups(groupIds, deleteConnectionsWithGroup.value);
|
const connectionIds = await connectionStore.deleteConnectionGroups(groupIds, deleteConnectionsWithGroup.value);
|
||||||
options.releaseActiveNodeReference(groupIds);
|
options.releaseActiveNodeReference(groupIds);
|
||||||
for (const connectionId of connectionIds) {
|
for (const connectionId of connectionIds) {
|
||||||
connectionStore.disconnect(connectionId).catch((error) => {
|
// 页签已由 deleteConnectionGroups 按「删除连接」策略处理,这里只清会话。
|
||||||
|
connectionStore.disconnect(connectionId, { skipTabHandling: true }).catch((error) => {
|
||||||
console.warn("[DBX][connection-group:delete:disconnect-failed]", { connectionId, error });
|
console.warn("[DBX][connection-group:delete:disconnect-failed]", { connectionId, error });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7470,6 +7470,21 @@ export default {
|
|||||||
disconnectTabHandlingModeKeepTabsClearResultsDescription: "Close table data, object browser, and structure tabs while keeping SQL query tabs and SQL text, and also clear execution state and result sessions.",
|
disconnectTabHandlingModeKeepTabsClearResultsDescription: "Close table data, object browser, and structure tabs while keeping SQL query tabs and SQL text, and also clear execution state and result sessions.",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResults: "Do not close related tabs",
|
disconnectTabHandlingModeKeepTabsKeepResults: "Do not close related tabs",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Keep related tabs, SQL text, and current results without extra cleanup.",
|
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Keep related tabs, SQL text, and current results without extra cleanup.",
|
||||||
|
deleteConnectionTabHandlingMode: "Tabs after connection deletion",
|
||||||
|
deleteConnectionTabHandlingModeDescription: "Choose how related tabs are handled after a connection is deleted. A deleted connection cannot be reconnected, so kept SQL tabs are scratch workspaces only.",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabs: "Close all tabs",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabsDescription: "Close every tab that belongs to the deleted connection.",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabs: "Keep all SQL tabs",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabsDescription: "Close table, object browser, and structure tabs while keeping every SQL tab of this connection (pinned or not) and its SQL text, and clear execution state and results.",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabs: "Keep pinned SQL tabs",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabsDescription: "Keep only pinned SQL tabs; every other tab, including unpinned SQL tabs, is closed.",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabs: "Keep all tabs",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabsDescription: "Close nothing and keep SQL text and current results. Open transactions are rolled back because the connection is gone.",
|
||||||
|
rememberConnectionDatabaseOnDelete: "Remember connection and database names",
|
||||||
|
rememberConnectionDatabaseOnDeleteDescription: "Remember a connection's name and database when it is deleted, and preselect them when a connection with the same name is created again.",
|
||||||
|
rememberConnectionDatabaseOnDeleteHint: "When enabled, creating a connection with the same name and database type prefills that database, and kept SQL tabs are rebound to the new connection automatically.",
|
||||||
|
rememberedConnectionDatabaseCount: "Remembered entries: {count}",
|
||||||
|
clearRememberedConnectionDatabases: "Clear remembered entries",
|
||||||
reuseDataTab: "Reuse data tab",
|
reuseDataTab: "Reuse data tab",
|
||||||
reuseDataTabDescription: "Choose how opening a table uses data tabs.",
|
reuseDataTabDescription: "Choose how opening a table uses data tabs.",
|
||||||
openDataTabsNextToActive: "Open new data tabs next to the active tab",
|
openDataTabsNextToActive: "Open new data tabs next to the active tab",
|
||||||
|
|||||||
@@ -7015,6 +7015,21 @@ export default withEnglishFallback({
|
|||||||
disconnectTabHandlingModeKeepTabsClearResultsDescription: "Cierra pestañas de datos, explorador de objetos y estructura, conserva las pestañas SQL y limpia el estado de ejecución y las sesiones.",
|
disconnectTabHandlingModeKeepTabsClearResultsDescription: "Cierra pestañas de datos, explorador de objetos y estructura, conserva las pestañas SQL y limpia el estado de ejecución y las sesiones.",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResults: "No cerrar pestañas relacionadas",
|
disconnectTabHandlingModeKeepTabsKeepResults: "No cerrar pestañas relacionadas",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Conserva las pestañas relacionadas, el texto SQL y los resultados actuales sin limpieza adicional.",
|
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Conserva las pestañas relacionadas, el texto SQL y los resultados actuales sin limpieza adicional.",
|
||||||
|
deleteConnectionTabHandlingMode: "Pestañas tras eliminar la conexión",
|
||||||
|
deleteConnectionTabHandlingModeDescription: "Elige cómo se tratan las pestañas relacionadas al eliminar una conexión. Una conexión eliminada no se puede reconectar, así que las pestañas SQL conservadas son solo un espacio de borrador.",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabs: "Cerrar todas las pestañas",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabsDescription: "Cierra todas las pestañas que pertenecen a la conexión eliminada.",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabs: "Conservar todas las pestañas SQL",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabsDescription: "Cierra las pestañas de tabla, explorador de objetos y estructura, conserva todas las pestañas SQL de esta conexión (fijadas o no) y su texto SQL, y limpia el estado de ejecución y los resultados.",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabs: "Conservar pestañas SQL fijadas",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabsDescription: "Conserva solo las pestañas SQL fijadas; el resto, incluidas las pestañas SQL sin fijar, se cierran.",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabs: "Conservar todas las pestañas",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabsDescription: "No cierra ninguna pestaña y conserva el texto SQL y los resultados actuales. Las transacciones abiertas se revierten porque la conexión ya no existe.",
|
||||||
|
rememberConnectionDatabaseOnDelete: "Recordar nombres de conexión y base de datos",
|
||||||
|
rememberConnectionDatabaseOnDeleteDescription: "Recuerda el nombre y la base de datos de una conexión al eliminarla, y los preselecciona al crear otra con el mismo nombre.",
|
||||||
|
rememberConnectionDatabaseOnDeleteHint: "Si se activa, al crear una conexión con el mismo nombre y tipo de base de datos se rellena esa base de datos y las pestañas SQL conservadas se vuelven a vincular automáticamente a la nueva conexión.",
|
||||||
|
rememberedConnectionDatabaseCount: "Entradas recordadas: {count}",
|
||||||
|
clearRememberedConnectionDatabases: "Borrar las entradas recordadas",
|
||||||
reuseDataTab: "Reutilizar pestaña de datos",
|
reuseDataTab: "Reutilizar pestaña de datos",
|
||||||
reuseDataTabDescription: "Elige cómo se usan las pestañas de datos al abrir una tabla.",
|
reuseDataTabDescription: "Elige cómo se usan las pestañas de datos al abrir una tabla.",
|
||||||
openDataTabsNextToActive: "Abrir nuevas pestañas de datos junto a la activa",
|
openDataTabsNextToActive: "Abrir nuevas pestañas de datos junto a la activa",
|
||||||
|
|||||||
@@ -7015,6 +7015,21 @@ export default withEnglishFallback({
|
|||||||
disconnectTabHandlingModeKeepTabsClearResultsDescription: "Chiudi i dati delle tabelle, l'esplorazione degli oggetti e le schede della struttura mantenendo le schede delle query SQL e il testo SQL, e cancella anche lo stato di esecuzione e le sessioni dei risultati.",
|
disconnectTabHandlingModeKeepTabsClearResultsDescription: "Chiudi i dati delle tabelle, l'esplorazione degli oggetti e le schede della struttura mantenendo le schede delle query SQL e il testo SQL, e cancella anche lo stato di esecuzione e le sessioni dei risultati.",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResults: "Non chiudere le schede correlate",
|
disconnectTabHandlingModeKeepTabsKeepResults: "Non chiudere le schede correlate",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Mantieni le schede correlate, il testo SQL e i risultati correnti senza ulteriore pulizia.",
|
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Mantieni le schede correlate, il testo SQL e i risultati correnti senza ulteriore pulizia.",
|
||||||
|
deleteConnectionTabHandlingMode: "Schede dopo l'eliminazione della connessione",
|
||||||
|
deleteConnectionTabHandlingModeDescription: "Scegli come gestire le schede correlate quando una connessione viene eliminata. Una connessione eliminata non può essere riconnessa, quindi le schede SQL mantenute sono solo uno spazio di lavoro provvisorio.",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabs: "Chiudi tutte le schede",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabsDescription: "Chiude tutte le schede che appartengono alla connessione eliminata.",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabs: "Mantieni tutte le schede SQL",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabsDescription: "Chiude le schede di tabella, esplorazione oggetti e struttura, mantiene tutte le schede SQL di questa connessione (fissate o no) e il relativo testo SQL e azzera stato di esecuzione e risultati.",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabs: "Mantieni le schede SQL fissate",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabsDescription: "Mantiene solo le schede SQL fissate; tutte le altre, comprese le schede SQL non fissate, vengono chiuse.",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabs: "Mantieni tutte le schede",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabsDescription: "Non chiude nessuna scheda e mantiene il testo SQL e i risultati correnti. Le transazioni aperte vengono annullate perché la connessione non esiste più.",
|
||||||
|
rememberConnectionDatabaseOnDelete: "Ricorda i nomi di connessione e database",
|
||||||
|
rememberConnectionDatabaseOnDeleteDescription: "Ricorda il nome e il database di una connessione quando viene eliminata e li preseleziona quando ne viene creata una con lo stesso nome.",
|
||||||
|
rememberConnectionDatabaseOnDeleteHint: "Se attivo, creando una connessione con lo stesso nome e tipo di database il database viene precompilato e le schede SQL mantenute vengono ricollegate automaticamente alla nuova connessione.",
|
||||||
|
rememberedConnectionDatabaseCount: "Voci ricordate: {count}",
|
||||||
|
clearRememberedConnectionDatabases: "Cancella le voci ricordate",
|
||||||
reuseDataTab: "Riusa scheda dati",
|
reuseDataTab: "Riusa scheda dati",
|
||||||
reuseDataTabDescription: "Scegli come usare le schede dati quando apri una tabella.",
|
reuseDataTabDescription: "Scegli come usare le schede dati quando apri una tabella.",
|
||||||
openDataTabsNextToActive: "Apri le nuove schede dati accanto a quella attiva",
|
openDataTabsNextToActive: "Apri le nuove schede dati accanto a quella attiva",
|
||||||
|
|||||||
@@ -7039,6 +7039,21 @@ export default withEnglishFallback({
|
|||||||
disconnectTabHandlingModeKeepTabsClearResultsDescription: "テーブルデータ、オブジェクトブラウザ、構造タブを閉じ、SQLクエリタブとSQLテキストを保持し、実状態と結果セッションもクリアします。",
|
disconnectTabHandlingModeKeepTabsClearResultsDescription: "テーブルデータ、オブジェクトブラウザ、構造タブを閉じ、SQLクエリタブとSQLテキストを保持し、実状態と結果セッションもクリアします。",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResults: "関連タブを閉じない",
|
disconnectTabHandlingModeKeepTabsKeepResults: "関連タブを閉じない",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "関連タブ、SQLテキスト、現在の結果を追加のクリーンアップなしで保持します。",
|
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "関連タブ、SQLテキスト、現在の結果を追加のクリーンアップなしで保持します。",
|
||||||
|
deleteConnectionTabHandlingMode: "接続削除後のタブ処理",
|
||||||
|
deleteConnectionTabHandlingModeDescription: "接続を削除した後に関連タブをどう扱うかを選択します。削除した接続は再接続できないため、保持された SQL タブは下書き用の作業領域にすぎません。",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabs: "すべてのタブを閉じる",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabsDescription: "削除した接続に属するすべてのタブを閉じます。",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabs: "すべての SQL タブを保持",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabsDescription: "テーブル、オブジェクトブラウザー、構造などのタブを閉じ、この接続のすべての SQL タブ(固定・未固定を問わず)とその SQL テキストは保持しつつ、実行状態と結果をクリアします。",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabs: "固定した SQL タブを保持",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabsDescription: "固定した SQL タブのみを保持し、それ以外(固定していない SQL タブを含む)はすべて閉じます。",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabs: "すべてのタブを保持",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabsDescription: "どのタブも閉じず、SQL テキストと現在の結果を保持します。接続が削除済みのため、未完了のトランザクションはロールバックされます。",
|
||||||
|
rememberConnectionDatabaseOnDelete: "接続名とデータベース名を記憶",
|
||||||
|
rememberConnectionDatabaseOnDeleteDescription: "接続を削除するときに名前とデータベースを記憶し、同名の接続を再度作成するときに自動で選択します。",
|
||||||
|
rememberConnectionDatabaseOnDeleteHint: "有効にすると、同名・同タイプの接続を作成したときにそのデータベースが自動入力され、保持された SQL タブも新しい接続に自動で再バインドされます。",
|
||||||
|
rememberedConnectionDatabaseCount: "記憶済みの項目: {count}",
|
||||||
|
clearRememberedConnectionDatabases: "記憶した項目を消去",
|
||||||
reuseDataTab: "データタブを再利用",
|
reuseDataTab: "データタブを再利用",
|
||||||
reuseDataTabDescription: "テーブルを開くときのデータタブの使い方を選択します。",
|
reuseDataTabDescription: "テーブルを開くときのデータタブの使い方を選択します。",
|
||||||
openDataTabsNextToActive: "新しいデータタブをアクティブタブの隣に開く",
|
openDataTabsNextToActive: "新しいデータタブをアクティブタブの隣に開く",
|
||||||
|
|||||||
@@ -6771,6 +6771,21 @@ export default withEnglishFallback({
|
|||||||
disconnectTabHandlingModeKeepTabsClearResultsDescription: "테이블 데이터, 객체 브라우저, 구조 탭을 닫고 SQL 쿼리 탭과 SQL 텍스트를 유지하며 실행 상태와 결과 세션도 지웁니다.",
|
disconnectTabHandlingModeKeepTabsClearResultsDescription: "테이블 데이터, 객체 브라우저, 구조 탭을 닫고 SQL 쿼리 탭과 SQL 텍스트를 유지하며 실행 상태와 결과 세션도 지웁니다.",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResults: "관련 탭 닫지 않기",
|
disconnectTabHandlingModeKeepTabsKeepResults: "관련 탭 닫지 않기",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "관련 탭, SQL 텍스트, 현재 결과를 추가 정리 없이 유지합니다.",
|
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "관련 탭, SQL 텍스트, 현재 결과를 추가 정리 없이 유지합니다.",
|
||||||
|
deleteConnectionTabHandlingMode: "연결 삭제 후 탭 처리",
|
||||||
|
deleteConnectionTabHandlingModeDescription: "연결을 삭제한 뒤 관련 탭을 어떻게 처리할지 선택합니다. 삭제한 연결은 다시 연결할 수 없으므로 유지된 SQL 탭은 임시 작업 공간일 뿐입니다.",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabs: "모든 탭 닫기",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabsDescription: "삭제한 연결에 속한 모든 탭을 닫습니다.",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabs: "모든 SQL 탭 유지",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabsDescription: "테이블, 개체 브라우저, 구조 탭은 닫고 이 연결의 모든 SQL 탭(고정 여부와 무관)과 그 SQL 텍스트는 유지하며 실행 상태와 결과는 지웁니다.",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabs: "고정된 SQL 탭 유지",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabsDescription: "고정된 SQL 탭만 유지하고 나머지 탭(고정되지 않은 SQL 탭 포함)은 모두 닫습니다.",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabs: "모든 탭 유지",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabsDescription: "어떤 탭도 닫지 않고 SQL 텍스트와 현재 결과를 유지합니다. 연결이 삭제되었으므로 끝나지 않은 트랜잭션은 롤백됩니다.",
|
||||||
|
rememberConnectionDatabaseOnDelete: "연결 이름과 데이터베이스 이름 기억",
|
||||||
|
rememberConnectionDatabaseOnDeleteDescription: "연결을 삭제할 때 이름과 데이터베이스를 기억하고, 같은 이름의 연결을 다시 만들 때 자동으로 선택합니다.",
|
||||||
|
rememberConnectionDatabaseOnDeleteHint: "활성화하면 같은 이름과 같은 유형의 연결을 만들 때 해당 데이터베이스가 자동으로 채워지고, 유지된 SQL 탭도 새 연결에 자동으로 다시 바인딩됩니다.",
|
||||||
|
rememberedConnectionDatabaseCount: "기억된 항목: {count}",
|
||||||
|
clearRememberedConnectionDatabases: "기억된 항목 지우기",
|
||||||
reuseDataTab: "데이터 탭 재사용",
|
reuseDataTab: "데이터 탭 재사용",
|
||||||
reuseDataTabDescription: "테이블을 열 때 데이터 탭을 사용하는 방식을 선택합니다.",
|
reuseDataTabDescription: "테이블을 열 때 데이터 탭을 사용하는 방식을 선택합니다.",
|
||||||
openDataTabsNextToActive: "새 데이터 탭을 활성 탭 옆에 열기",
|
openDataTabsNextToActive: "새 데이터 탭을 활성 탭 옆에 열기",
|
||||||
|
|||||||
@@ -7017,6 +7017,21 @@ export default withEnglishFallback({
|
|||||||
disconnectTabHandlingModeKeepTabsClearResultsDescription: "Fechar abas de dados de tabela, navegador de objetos e estrutura, mantendo as abas de consultas SQL e o texto SQL, e também limpar o estado de execução e as sessões de resultados.",
|
disconnectTabHandlingModeKeepTabsClearResultsDescription: "Fechar abas de dados de tabela, navegador de objetos e estrutura, mantendo as abas de consultas SQL e o texto SQL, e também limpar o estado de execução e as sessões de resultados.",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResults: "Não fechar abas relacionadas",
|
disconnectTabHandlingModeKeepTabsKeepResults: "Não fechar abas relacionadas",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Manter abas relacionadas, texto SQL e resultados atuais sem limpeza adicional.",
|
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "Manter abas relacionadas, texto SQL e resultados atuais sem limpeza adicional.",
|
||||||
|
deleteConnectionTabHandlingMode: "Abas após excluir a conexão",
|
||||||
|
deleteConnectionTabHandlingModeDescription: "Escolha como as abas relacionadas são tratadas após excluir uma conexão. Uma conexão excluída não pode ser reconectada, então as abas SQL mantidas são apenas uma área de rascunho.",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabs: "Fechar todas as abas",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabsDescription: "Fecha todas as abas pertencentes à conexão excluída.",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabs: "Manter todas as abas SQL",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabsDescription: "Fecha as abas de tabela, navegador de objetos e estrutura, mantém todas as abas SQL desta conexão (fixadas ou não) e seu texto SQL e limpa o estado de execução e os resultados.",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabs: "Manter abas SQL fixadas",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabsDescription: "Mantém apenas as abas SQL fixadas; todas as outras, incluindo as abas SQL não fixadas, são fechadas.",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabs: "Manter todas as abas",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabsDescription: "Não fecha nenhuma aba e mantém o texto SQL e os resultados atuais. Transações abertas são revertidas porque a conexão não existe mais.",
|
||||||
|
rememberConnectionDatabaseOnDelete: "Lembrar nomes de conexão e banco de dados",
|
||||||
|
rememberConnectionDatabaseOnDeleteDescription: "Lembra o nome e o banco de dados de uma conexão ao excluí-la e os pré-seleciona ao criar outra com o mesmo nome.",
|
||||||
|
rememberConnectionDatabaseOnDeleteHint: "Quando ativado, criar uma conexão com o mesmo nome e tipo de banco de dados preenche esse banco de dados e as abas SQL mantidas são religadas automaticamente à nova conexão.",
|
||||||
|
rememberedConnectionDatabaseCount: "Entradas lembradas: {count}",
|
||||||
|
clearRememberedConnectionDatabases: "Limpar entradas lembradas",
|
||||||
reuseDataTab: "Reutilizar aba de dados",
|
reuseDataTab: "Reutilizar aba de dados",
|
||||||
reuseDataTabDescription: "Escolha como as abas de dados são usadas ao abrir uma tabela.",
|
reuseDataTabDescription: "Escolha como as abas de dados são usadas ao abrir uma tabela.",
|
||||||
openDataTabsNextToActive: "Abrir novas abas de dados ao lado da aba ativa",
|
openDataTabsNextToActive: "Abrir novas abas de dados ao lado da aba ativa",
|
||||||
|
|||||||
@@ -7118,6 +7118,21 @@ export default withEnglishFallback({
|
|||||||
disconnectTabHandlingModeKeepTabsClearResultsDescription: "Tablo verisi, nesne gezgini ve yapı sekmelerini kapatırken SQL sorgu sekmelerini ve SQL metnini koru; ayrıca yürütme durumunu ve sonuç oturumlarını temizle.",
|
disconnectTabHandlingModeKeepTabsClearResultsDescription: "Tablo verisi, nesne gezgini ve yapı sekmelerini kapatırken SQL sorgu sekmelerini ve SQL metnini koru; ayrıca yürütme durumunu ve sonuç oturumlarını temizle.",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResults: "İlgili sekmeleri kapatma",
|
disconnectTabHandlingModeKeepTabsKeepResults: "İlgili sekmeleri kapatma",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "İlgili sekmeleri, SQL metnini ve geçerli sonuçları ek bir temizlik yapmadan koru.",
|
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "İlgili sekmeleri, SQL metnini ve geçerli sonuçları ek bir temizlik yapmadan koru.",
|
||||||
|
deleteConnectionTabHandlingMode: "Bağlantı silindikten sonra sekmeler",
|
||||||
|
deleteConnectionTabHandlingModeDescription: "Bir bağlantı silindikten sonra ilgili sekmelerin nasıl ele alınacağını seçin. Silinen bir bağlantıya yeniden bağlanılamaz, bu nedenle korunan SQL sekmeleri yalnızca taslak çalışma alanıdır.",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabs: "Tüm sekmeleri kapat",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabsDescription: "Silinen bağlantıya ait tüm sekmeleri kapatır.",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabs: "Tüm SQL sekmelerini koru",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabsDescription: "Tablo, nesne tarayıcısı ve yapı sekmelerini kapatır; bu bağlantının tüm SQL sekmelerini (sabitlenmiş olsun olmasın) ve SQL metnini korur, yürütme durumunu ve sonuçları temizler.",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabs: "Sabitlenmiş SQL sekmelerini koru",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabsDescription: "Yalnızca sabitlenmiş SQL sekmelerini korur; sabitlenmemiş SQL sekmeleri dahil diğer tüm sekmeler kapatılır.",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabs: "Tüm sekmeleri koru",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabsDescription: "Hiçbir sekmeyi kapatmaz, SQL metnini ve geçerli sonuçları korur. Bağlantı silindiği için açık işlemler geri alınır.",
|
||||||
|
rememberConnectionDatabaseOnDelete: "Bağlantı ve veritabanı adlarını hatırla",
|
||||||
|
rememberConnectionDatabaseOnDeleteDescription: "Bir bağlantı silindiğinde adını ve veritabanını hatırlar ve aynı adla yeni bir bağlantı oluşturulduğunda bunları önceden seçer.",
|
||||||
|
rememberConnectionDatabaseOnDeleteHint: "Etkinleştirildiğinde, aynı ad ve aynı veritabanı türüyle bir bağlantı oluşturmak o veritabanını önceden doldurur ve korunan SQL sekmeleri yeni bağlantıya otomatik olarak yeniden bağlanır.",
|
||||||
|
rememberedConnectionDatabaseCount: "Hatırlanan kayıt: {count}",
|
||||||
|
clearRememberedConnectionDatabases: "Hatırlanan kayıtları temizle",
|
||||||
reuseDataTab: "Veri sekmesini yeniden kullan",
|
reuseDataTab: "Veri sekmesini yeniden kullan",
|
||||||
reuseDataTabDescription: "Bir tablo açıldığında veri sekmelerinin nasıl kullanılacağını seçin.",
|
reuseDataTabDescription: "Bir tablo açıldığında veri sekmelerinin nasıl kullanılacağını seçin.",
|
||||||
openDataTabsNextToActive: "Yeni veri sekmelerini etkin sekmenin yanında aç",
|
openDataTabsNextToActive: "Yeni veri sekmelerini etkin sekmenin yanında aç",
|
||||||
|
|||||||
@@ -7443,6 +7443,21 @@ export default withEnglishFallback({
|
|||||||
disconnectTabHandlingModeKeepTabsClearResultsDescription: "关闭表数据、对象浏览和结构等页签,保留 SQL 查询页签和 SQL 文本,同时清理执行状态与结果会话。",
|
disconnectTabHandlingModeKeepTabsClearResultsDescription: "关闭表数据、对象浏览和结构等页签,保留 SQL 查询页签和 SQL 文本,同时清理执行状态与结果会话。",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResults: "不关闭相关页签",
|
disconnectTabHandlingModeKeepTabsKeepResults: "不关闭相关页签",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "保留相关页签、SQL 文本和当前结果,不做额外处理。",
|
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "保留相关页签、SQL 文本和当前结果,不做额外处理。",
|
||||||
|
deleteConnectionTabHandlingMode: "删除连接后的标签页处理",
|
||||||
|
deleteConnectionTabHandlingModeDescription: "删除连接后相关标签页的保留策略。删除后连接无法重连,保留的 SQL 页签只是草稿工作区。",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabs: "关闭全部页签",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabsDescription: "删除连接时直接关闭该连接下的全部页签。",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabs: "保留所有 SQL 页签",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabsDescription: "关闭表格、对象浏览和结构等页签,保留该连接下的全部 SQL 页签(含未固定的)及其 SQL 文本,并清空执行状态与结果。",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabs: "保留 SQL 固定页签",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabsDescription: "只保留已固定的 SQL 页签,其余页签(含未固定的 SQL 页签)全部关闭。",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabs: "保留全部页签",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabsDescription: "不关闭任何页签,保留 SQL 文本与当前结果;因连接已删除,未结束的事务会被回滚。",
|
||||||
|
rememberConnectionDatabaseOnDelete: "记住连接名与数据库名",
|
||||||
|
rememberConnectionDatabaseOnDeleteDescription: "删除连接时记住它的名称与数据库,再次新建同名连接时自动选中。",
|
||||||
|
rememberConnectionDatabaseOnDeleteHint: "开启后,新建同名同类型连接会自动回填该数据库,保留下来的 SQL 页签也会自动重新绑定到新连接。",
|
||||||
|
rememberedConnectionDatabaseCount: "已记住 {count} 条记录",
|
||||||
|
clearRememberedConnectionDatabases: "清除已记住的记录",
|
||||||
reuseDataTab: "复用数据标签页",
|
reuseDataTab: "复用数据标签页",
|
||||||
reuseDataTabDescription: "选择打开表时如何使用数据标签页。",
|
reuseDataTabDescription: "选择打开表时如何使用数据标签页。",
|
||||||
openDataTabsNextToActive: "在当前标签页旁打开新数据标签页",
|
openDataTabsNextToActive: "在当前标签页旁打开新数据标签页",
|
||||||
|
|||||||
@@ -6341,6 +6341,21 @@ export default withEnglishFallback({
|
|||||||
disconnectTabHandlingModeKeepTabsClearResultsDescription: "關閉資料表資料、物件瀏覽和結構等分頁,保留 SQL 查詢分頁與 SQL 文字,同時清理執行狀態與結果工作階段。",
|
disconnectTabHandlingModeKeepTabsClearResultsDescription: "關閉資料表資料、物件瀏覽和結構等分頁,保留 SQL 查詢分頁與 SQL 文字,同時清理執行狀態與結果工作階段。",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResults: "不關閉相關分頁",
|
disconnectTabHandlingModeKeepTabsKeepResults: "不關閉相關分頁",
|
||||||
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "保留相關分頁、SQL 文字與目前結果,不另外做清理。",
|
disconnectTabHandlingModeKeepTabsKeepResultsDescription: "保留相關分頁、SQL 文字與目前結果,不另外做清理。",
|
||||||
|
deleteConnectionTabHandlingMode: "刪除連線後的分頁處理",
|
||||||
|
deleteConnectionTabHandlingModeDescription: "刪除連線後相關分頁的保留策略。刪除後連線無法重新連線,保留的 SQL 分頁只是草稿工作區。",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabs: "關閉全部分頁",
|
||||||
|
deleteConnectionTabHandlingModeCloseTabsDescription: "刪除連線時直接關閉該連線下的全部分頁。",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabs: "保留所有 SQL 分頁",
|
||||||
|
deleteConnectionTabHandlingModeKeepSqlTabsDescription: "關閉表格、物件瀏覽與結構等分頁,保留該連線下的全部 SQL 分頁(含未固定的)及其 SQL 文字,並清空執行狀態與結果。",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabs: "保留 SQL 固定分頁",
|
||||||
|
deleteConnectionTabHandlingModeKeepPinnedSqlTabsDescription: "只保留已固定的 SQL 分頁,其餘分頁(含未固定的 SQL 分頁)全部關閉。",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabs: "保留全部分頁",
|
||||||
|
deleteConnectionTabHandlingModeKeepAllTabsDescription: "不關閉任何分頁,保留 SQL 文字與目前結果;由於連線已刪除,未結束的交易會被回滾。",
|
||||||
|
rememberConnectionDatabaseOnDelete: "記住連線名稱與資料庫名稱",
|
||||||
|
rememberConnectionDatabaseOnDeleteDescription: "刪除連線時記住它的名稱與資料庫,再次建立同名連線時自動選取。",
|
||||||
|
rememberConnectionDatabaseOnDeleteHint: "開啟後,建立同名同類型連線會自動填入該資料庫,保留下來的 SQL 分頁也會自動重新綁定到新連線。",
|
||||||
|
rememberedConnectionDatabaseCount: "已記住 {count} 筆記錄",
|
||||||
|
clearRememberedConnectionDatabases: "清除已記住的記錄",
|
||||||
reuseDataTab: "重複使用資料分頁",
|
reuseDataTab: "重複使用資料分頁",
|
||||||
reuseDataTabDescription: "選擇開啟資料表時如何使用資料分頁。",
|
reuseDataTabDescription: "選擇開啟資料表時如何使用資料分頁。",
|
||||||
openDataTabsNextToActive: "在目前分頁旁開啟新的資料分頁",
|
openDataTabsNextToActive: "在目前分頁旁開啟新的資料分頁",
|
||||||
|
|||||||
@@ -250,3 +250,28 @@ describe("openTabsPersistence originalSql round-trip", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("openTabsPersistence detached connection tabs", () => {
|
||||||
|
it("preserves the original connection name of a kept SQL tab across a round-trip", () => {
|
||||||
|
const [restored] = roundTrip([queryTab({ connectionId: "deleted-conn", detachedConnectionName: "prod" })]);
|
||||||
|
|
||||||
|
expect(restored.connectionId).toBe("deleted-conn");
|
||||||
|
expect(restored.detachedConnectionName).toBe("prod");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a detached SQL tab even though its connection no longer exists", () => {
|
||||||
|
const saved = serializeOpenTabs([queryTab({ id: "orphan", connectionId: "deleted-conn", detachedConnectionName: "prod" })]);
|
||||||
|
|
||||||
|
const { tabs } = restoreOpenTabsPayload({ tabs: saved, activeTabId: "orphan" }, { validConnectionIds: ["other-conn"] });
|
||||||
|
|
||||||
|
// SQL 页签不受 validConnectionIds 过滤,删除连接后保留下来的草稿页签才能跨重启存活。
|
||||||
|
expect(tabs.map((tab) => tab.id)).toEqual(["orphan"]);
|
||||||
|
expect(tabs[0].detachedConnectionName).toBe("prod");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits the detached connection name when the tab is bound to a live connection", () => {
|
||||||
|
const [saved] = serializeOpenTabs([queryTab({})]);
|
||||||
|
|
||||||
|
expect(saved.detachedConnectionName).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,6 +23,8 @@ export interface SavedOpenTab {
|
|||||||
customTitle?: boolean;
|
customTitle?: boolean;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
database: string;
|
database: string;
|
||||||
|
/** 原连接已被删除但页签被保留时记录的原连接名,用于新建同名连接后重新绑定。 */
|
||||||
|
detachedConnectionName?: string;
|
||||||
catalog?: string;
|
catalog?: string;
|
||||||
schema?: string;
|
schema?: string;
|
||||||
sql: string;
|
sql: string;
|
||||||
@@ -163,6 +165,7 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
|
|||||||
...(tab.customTitle ? { customTitle: true } : {}),
|
...(tab.customTitle ? { customTitle: true } : {}),
|
||||||
connectionId: tab.connectionId,
|
connectionId: tab.connectionId,
|
||||||
database: tab.database,
|
database: tab.database,
|
||||||
|
...(tab.detachedConnectionName ? { detachedConnectionName: tab.detachedConnectionName } : {}),
|
||||||
...(tab.catalog !== undefined ? { catalog: tab.catalog } : {}),
|
...(tab.catalog !== undefined ? { catalog: tab.catalog } : {}),
|
||||||
schema: tab.schema,
|
schema: tab.schema,
|
||||||
sql: shouldPersistTabSql(tab) ? tab.sql : "",
|
sql: shouldPersistTabSql(tab) ? tab.sql : "",
|
||||||
|
|||||||
@@ -300,3 +300,8 @@ const DRAFT_VISIBLE_SCHEMAS_PREFIX = "__visible_schema_draft_";
|
|||||||
export function buildDraftVisibleSchemasConnectionId(seed: string): string {
|
export function buildDraftVisibleSchemasConnectionId(seed: string): string {
|
||||||
return `${DRAFT_VISIBLE_SCHEMAS_PREFIX}${seed}`;
|
return `${DRAFT_VISIBLE_SCHEMAS_PREFIX}${seed}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 可见 schema 选择器用的临时连接:只存在于弹窗交互期间,不是用户保存的连接。 */
|
||||||
|
export function isDraftVisibleSchemasConnectionId(connectionId: string): boolean {
|
||||||
|
return connectionId.startsWith(DRAFT_VISIBLE_SCHEMAS_PREFIX);
|
||||||
|
}
|
||||||
@@ -84,6 +84,8 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
|||||||
"sidebarBrowseObjectsOnDatabaseActivation",
|
"sidebarBrowseObjectsOnDatabaseActivation",
|
||||||
"openTabsRestoreMode",
|
"openTabsRestoreMode",
|
||||||
"disconnectTabHandlingMode",
|
"disconnectTabHandlingMode",
|
||||||
|
"deleteConnectionTabHandlingMode",
|
||||||
|
"rememberConnectionDatabaseOnDelete",
|
||||||
"dataTabReuseMode",
|
"dataTabReuseMode",
|
||||||
"openDataTabsNextToActive",
|
"openDataTabsNextToActive",
|
||||||
"prefillNewQueryWithSelect",
|
"prefillNewQueryWithSelect",
|
||||||
|
|||||||
@@ -198,6 +198,8 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
|
|||||||
{ id: "navigation-hidden-tables", category: "navigation", titleKey: "settings.sidebarHiddenTablePrefixes", descriptionKey: "settings.sidebarHiddenTablePrefixesDescription", targetId: "navigation" },
|
{ id: "navigation-hidden-tables", category: "navigation", titleKey: "settings.sidebarHiddenTablePrefixes", descriptionKey: "settings.sidebarHiddenTablePrefixesDescription", targetId: "navigation" },
|
||||||
{ id: "navigation-table-page-size", category: "navigation", titleKey: "settings.sidebarTablePageSize", descriptionKey: "settings.sidebarTablePageSizeDescription", targetId: "navigation" },
|
{ id: "navigation-table-page-size", category: "navigation", titleKey: "settings.sidebarTablePageSize", descriptionKey: "settings.sidebarTablePageSizeDescription", targetId: "navigation" },
|
||||||
{ id: "navigation-disconnect-tabs", category: "navigation", titleKey: "settings.disconnectTabHandlingMode", descriptionKey: "settings.disconnectTabHandlingModeDescription", targetId: "navigation" },
|
{ id: "navigation-disconnect-tabs", category: "navigation", titleKey: "settings.disconnectTabHandlingMode", descriptionKey: "settings.disconnectTabHandlingModeDescription", targetId: "navigation" },
|
||||||
|
{ id: "navigation-delete-connection-tabs", category: "navigation", titleKey: "settings.deleteConnectionTabHandlingMode", descriptionKey: "settings.deleteConnectionTabHandlingModeDescription", targetId: "navigation" },
|
||||||
|
{ id: "navigation-remember-connection-database", category: "navigation", titleKey: "settings.rememberConnectionDatabaseOnDelete", descriptionKey: "settings.rememberConnectionDatabaseOnDeleteDescription", targetId: "navigation" },
|
||||||
{ id: "query-page-size", category: "data", titleKey: "settings.queryPageSize", descriptionKey: "settings.queryPageSizeDescription", targetId: "data" },
|
{ id: "query-page-size", category: "data", titleKey: "settings.queryPageSize", descriptionKey: "settings.queryPageSizeDescription", targetId: "data" },
|
||||||
{ id: "data-page-size", category: "data", titleKey: "settings.tableOpenPageSize", descriptionKey: "settings.tableOpenPageSizeDescription", targetId: "data" },
|
{ id: "data-page-size", category: "data", titleKey: "settings.tableOpenPageSize", descriptionKey: "settings.tableOpenPageSizeDescription", targetId: "data" },
|
||||||
{ id: "data-table-open-sort-mode", category: "data", titleKey: "settings.tableOpenSortMode", descriptionKey: "settings.tableOpenSortDescription", targetId: "data" },
|
{ id: "data-table-open-sort-mode", category: "data", titleKey: "settings.tableOpenSortMode", descriptionKey: "settings.tableOpenSortDescription", targetId: "data" },
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
|
|||||||
"sidebarBrowseObjectsOnDatabaseActivation",
|
"sidebarBrowseObjectsOnDatabaseActivation",
|
||||||
"openTabsRestoreMode",
|
"openTabsRestoreMode",
|
||||||
"disconnectTabHandlingMode",
|
"disconnectTabHandlingMode",
|
||||||
|
"deleteConnectionTabHandlingMode",
|
||||||
|
"rememberConnectionDatabaseOnDelete",
|
||||||
"dataTabReuseMode",
|
"dataTabReuseMode",
|
||||||
"openDataTabsNextToActive",
|
"openDataTabsNextToActive",
|
||||||
"clickTableNavigationTarget",
|
"clickTableNavigationTarget",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { deletedConnectionTabKeepMode } from "@/lib/tabs/deletedConnectionTabs";
|
||||||
|
|
||||||
|
describe("deletedConnectionTabKeepMode", () => {
|
||||||
|
it("maps each delete-time tab handling mode to its keep scope", () => {
|
||||||
|
expect(deletedConnectionTabKeepMode("close-tabs")).toBe("none");
|
||||||
|
expect(deletedConnectionTabKeepMode("keep-sql-tabs")).toBe("sql");
|
||||||
|
expect(deletedConnectionTabKeepMode("keep-pinned-sql-tabs")).toBe("pinned-sql");
|
||||||
|
expect(deletedConnectionTabKeepMode("keep-all-tabs")).toBe("all");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { DeleteConnectionTabHandlingMode } from "@/stores/settingsStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除连接时对已打开页签的保留范围。
|
||||||
|
*
|
||||||
|
* 与「断开连接」不同,删除后连接配置已从磁盘移除、无法再重连,因此被保留下来的
|
||||||
|
* SQL 页签只是草稿工作区;它们会记录原连接名,等新建同名连接时再重新绑定。
|
||||||
|
*
|
||||||
|
* - `none`:关闭全部页签
|
||||||
|
* - `sql`:保留全部 SQL 页签(清空结果),其余关闭
|
||||||
|
* - `pinned-sql`:只保留固定的 SQL 页签(清空结果),其余关闭
|
||||||
|
* - `all`:不关闭任何页签,保留 SQL 文本与当前结果,只回滚事务
|
||||||
|
*/
|
||||||
|
export type DeletedConnectionTabKeepMode = "none" | "sql" | "pinned-sql" | "all";
|
||||||
|
|
||||||
|
export function deletedConnectionTabKeepMode(mode: DeleteConnectionTabHandlingMode): DeletedConnectionTabKeepMode {
|
||||||
|
if (mode === "keep-sql-tabs") return "sql";
|
||||||
|
if (mode === "keep-pinned-sql-tabs") return "pinned-sql";
|
||||||
|
if (mode === "keep-all-tabs") return "all";
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
import { createPinia, setActivePinia } from "pinia";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { ConnectionConfig } from "@/types/database";
|
||||||
|
|
||||||
|
function installLocalStorage() {
|
||||||
|
const data = new Map<string, string>();
|
||||||
|
vi.stubGlobal("localStorage", {
|
||||||
|
getItem: vi.fn((key: string) => data.get(key) ?? null),
|
||||||
|
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
|
||||||
|
removeItem: vi.fn((key: string) => data.delete(key)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function installApiMocks() {
|
||||||
|
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||||
|
vi.doMock("@/lib/backend/api", () => ({
|
||||||
|
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||||
|
connectDb: vi.fn().mockResolvedValue("preview-1"),
|
||||||
|
disconnectDb: vi.fn().mockResolvedValue(undefined),
|
||||||
|
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||||
|
loadConnections: vi.fn().mockResolvedValue([]),
|
||||||
|
loadEditorSettings: vi.fn().mockResolvedValue(null),
|
||||||
|
loadPinnedTreeNodeIds: vi.fn().mockResolvedValue([]),
|
||||||
|
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||||
|
loadTunnelProfiles: vi.fn().mockResolvedValue([]),
|
||||||
|
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||||
|
saveEditorSettings: vi.fn().mockResolvedValue(undefined),
|
||||||
|
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||||
|
loadSidebarLayout: vi.fn().mockResolvedValue(null),
|
||||||
|
loadTableVGroups: vi.fn().mockResolvedValue({}),
|
||||||
|
saveTableVGroups: vi.fn().mockResolvedValue(undefined),
|
||||||
|
deleteTableVGroupsForConnection: vi.fn().mockResolvedValue(undefined),
|
||||||
|
saveOpenTabsState: vi.fn().mockResolvedValue(undefined),
|
||||||
|
loadOpenTabsState: vi.fn().mockResolvedValue(null),
|
||||||
|
// 关闭页签会顺带回收结果/客户端会话;缺失的导出会让清理路径报错刷屏。
|
||||||
|
closeQuerySession: vi.fn().mockResolvedValue(undefined),
|
||||||
|
closeClientConnectionSession: vi.fn().mockResolvedValue(undefined),
|
||||||
|
connectionDatabaseInfo: vi.fn().mockResolvedValue(undefined),
|
||||||
|
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
||||||
|
sessionCredentialStatus: vi.fn().mockResolvedValue(false),
|
||||||
|
forgetSessionCredential: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
// Result-snapshot cleanup hits a relative URL, which node's fetch rejects outright.
|
||||||
|
vi.doMock("@/lib/tabs/tabResultCache", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@/lib/tabs/tabResultCache")>();
|
||||||
|
return { ...actual, deleteTabResultSnapshotsForOwner: vi.fn().mockResolvedValue(undefined) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pgConnection(id: string, name: string, database?: string): ConnectionConfig {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
db_type: "postgres",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 5432,
|
||||||
|
username: "postgres",
|
||||||
|
password: "",
|
||||||
|
database,
|
||||||
|
read_only: false,
|
||||||
|
} as ConnectionConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("connectionStore delete-time tab handling", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetModules();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
installLocalStorage();
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keep-sql-tabs keeps SQL tabs, closes object tabs, and remembers the database", async () => {
|
||||||
|
installApiMocks();
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
settingsStore.editorSettings.deleteConnectionTabHandlingMode = "keep-sql-tabs";
|
||||||
|
settingsStore.editorSettings.rememberConnectionDatabaseOnDelete = true;
|
||||||
|
|
||||||
|
const store = useConnectionStore();
|
||||||
|
store.connections = [pgConnection("conn-a", "prod", "app")];
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
const sqlId = queryStore.createTab("conn-a", "app", "draft.sql", "query");
|
||||||
|
const dataId = queryStore.createTab("conn-a", "app", "users", "data", "public", undefined, undefined, { forceNew: true });
|
||||||
|
|
||||||
|
await store.removeConnection("conn-a");
|
||||||
|
|
||||||
|
expect(queryStore.tabs.some((tab) => tab.id === sqlId)).toBe(true);
|
||||||
|
expect(queryStore.tabs.some((tab) => tab.id === dataId)).toBe(false);
|
||||||
|
const kept = queryStore.tabs.find((tab) => tab.id === sqlId);
|
||||||
|
// 页签仍指向已删除的连接,但记录了原连接名,供新建同名连接重绑。
|
||||||
|
expect(kept?.connectionId).toBe("conn-a");
|
||||||
|
expect(kept?.detachedConnectionName).toBe("prod");
|
||||||
|
expect(settingsStore.editorSettings.rememberedConnectionDatabases.prod).toEqual({ database: "app", dbType: "postgres" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keep-pinned-sql-tabs keeps pinned SQL tabs only", async () => {
|
||||||
|
installApiMocks();
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||||
|
useSettingsStore().editorSettings.deleteConnectionTabHandlingMode = "keep-pinned-sql-tabs";
|
||||||
|
|
||||||
|
const store = useConnectionStore();
|
||||||
|
store.connections = [pgConnection("conn-a", "prod", "app")];
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
const pinnedId = queryStore.createTab("conn-a", "app", "pinned.sql", "query");
|
||||||
|
const plainId = queryStore.createTab("conn-a", "app", "plain.sql", "query");
|
||||||
|
const dataId = queryStore.createTab("conn-a", "app", "users", "data", "public", undefined, undefined, { forceNew: true });
|
||||||
|
queryStore.togglePinnedTab(pinnedId);
|
||||||
|
|
||||||
|
await store.removeConnection("conn-a");
|
||||||
|
|
||||||
|
expect(queryStore.tabs.map((tab) => tab.id)).toEqual([pinnedId]);
|
||||||
|
expect(queryStore.tabs.some((tab) => tab.id === plainId)).toBe(false);
|
||||||
|
expect(queryStore.tabs.some((tab) => tab.id === dataId)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not remember or detach tabs when the memory switch is off", async () => {
|
||||||
|
installApiMocks();
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
settingsStore.editorSettings.deleteConnectionTabHandlingMode = "keep-sql-tabs";
|
||||||
|
settingsStore.editorSettings.rememberConnectionDatabaseOnDelete = false;
|
||||||
|
|
||||||
|
const store = useConnectionStore();
|
||||||
|
store.connections = [pgConnection("conn-a", "prod", "app")];
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
const sqlId = queryStore.createTab("conn-a", "app", "draft.sql", "query");
|
||||||
|
|
||||||
|
await store.removeConnection("conn-a");
|
||||||
|
|
||||||
|
const kept = queryStore.tabs.find((tab) => tab.id === sqlId);
|
||||||
|
expect(kept?.detachedConnectionName).toBeUndefined();
|
||||||
|
expect(settingsStore.editorSettings.rememberedConnectionDatabases).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rebinds kept SQL tabs and reuses the remembered database when a same-named connection is created", async () => {
|
||||||
|
installApiMocks();
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
settingsStore.editorSettings.deleteConnectionTabHandlingMode = "keep-sql-tabs";
|
||||||
|
settingsStore.editorSettings.rememberConnectionDatabaseOnDelete = true;
|
||||||
|
|
||||||
|
const store = useConnectionStore();
|
||||||
|
store.connections = [pgConnection("conn-a", "prod", "app")];
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
const defaultDbTab = queryStore.createTab("conn-a", "app", "draft.sql", "query");
|
||||||
|
// 页签可能指向该连接下的另一个库,重绑不应改写它的执行上下文。
|
||||||
|
const otherDbTab = queryStore.createTab("conn-a", "reporting", "report.sql", "query");
|
||||||
|
const noDbTab = queryStore.createTab("conn-a", "", "scratch.sql", "query");
|
||||||
|
|
||||||
|
await store.removeConnection("conn-a");
|
||||||
|
expect(queryStore.tabs.some((tab) => tab.id === defaultDbTab)).toBe(true);
|
||||||
|
|
||||||
|
// 新建同名连接(表单未填数据库)时,回填记住的数据库并把脱离页签重绑过来。
|
||||||
|
await store.addConnection(pgConnection("conn-b", "prod"));
|
||||||
|
|
||||||
|
expect(store.getConfig("conn-b")?.database).toBe("app");
|
||||||
|
const rebound = queryStore.tabs.find((tab) => tab.id === defaultDbTab);
|
||||||
|
expect(rebound?.connectionId).toBe("conn-b");
|
||||||
|
expect(rebound?.database).toBe("app");
|
||||||
|
expect(rebound?.detachedConnectionName).toBeUndefined();
|
||||||
|
expect(queryStore.tabs.find((tab) => tab.id === otherDbTab)).toMatchObject({ connectionId: "conn-b", database: "reporting" });
|
||||||
|
// 页签没有库时才回落到新连接的默认库。
|
||||||
|
expect(queryStore.tabs.find((tab) => tab.id === noDbTab)).toMatchObject({ connectionId: "conn-b", database: "app" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still asks before discarding an unsaved SQL draft when the policy closes tabs", async () => {
|
||||||
|
installApiMocks();
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
settingsStore.editorSettings.deleteConnectionTabHandlingMode = "close-tabs";
|
||||||
|
|
||||||
|
const store = useConnectionStore();
|
||||||
|
store.connections = [pgConnection("conn-a", "prod", "app")];
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
const dirtyId = queryStore.createTab("conn-a", "app", "draft.sql", "query");
|
||||||
|
queryStore.updateSql(dirtyId, "select 1;");
|
||||||
|
|
||||||
|
await store.removeConnection("conn-a");
|
||||||
|
|
||||||
|
// 删除连接不能静默丢掉未保存的草稿:先弹保存/放弃确认,而不是直接关掉。
|
||||||
|
expect(queryStore.showCloseConfirm).toBe(true);
|
||||||
|
expect(queryStore.pendingCloseTabId).toBe(dirtyId);
|
||||||
|
|
||||||
|
// 用户在确认框里选择取消时页签会留下,此时它仍带着原连接名,可被新建的同名连接接管。
|
||||||
|
expect(queryStore.tabs.find((tab) => tab.id === dirtyId)?.detachedConnectionName).toBe("prod");
|
||||||
|
|
||||||
|
queryStore.forceCloseAllPendingTabs();
|
||||||
|
expect(queryStore.tabs.some((tab) => tab.id === dirtyId)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not reuse the remembered database for a same-named connection of another type", async () => {
|
||||||
|
installApiMocks();
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
settingsStore.editorSettings.deleteConnectionTabHandlingMode = "keep-sql-tabs";
|
||||||
|
settingsStore.editorSettings.rememberConnectionDatabaseOnDelete = true;
|
||||||
|
|
||||||
|
const store = useConnectionStore();
|
||||||
|
store.connections = [pgConnection("conn-a", "test", "app")];
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
const sqlId = queryStore.createTab("conn-a", "app", "draft.sql", "query");
|
||||||
|
await store.removeConnection("conn-a");
|
||||||
|
|
||||||
|
// "test" 这类名字常被不同数据库类型复用;跨类型回填会把无意义的库名写进新连接配置。
|
||||||
|
await store.addConnection({ ...pgConnection("conn-b", "test"), db_type: "sqlite", driver_profile: "sqlite" } as ConnectionConfig);
|
||||||
|
|
||||||
|
expect(store.getConfig("conn-b")?.database).toBeUndefined();
|
||||||
|
// 页签重绑只看连接名,与类型无关。
|
||||||
|
expect(queryStore.tabs.find((tab) => tab.id === sqlId)?.connectionId).toBe("conn-b");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("asks before discarding an unsaved structure draft when only SQL tabs are kept", async () => {
|
||||||
|
installApiMocks();
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||||
|
useSettingsStore().editorSettings.deleteConnectionTabHandlingMode = "keep-sql-tabs";
|
||||||
|
|
||||||
|
const store = useConnectionStore();
|
||||||
|
store.connections = [pgConnection("conn-a", "prod", "app")];
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
const sqlId = queryStore.createTab("conn-a", "app", "kept.sql", "query");
|
||||||
|
const structureId = queryStore.createTab("conn-a", "app", "users", "structure", "public", undefined, undefined, { forceNew: true });
|
||||||
|
const structureTab = queryStore.tabs.find((tab) => tab.id === structureId);
|
||||||
|
if (!structureTab) throw new Error("structure tab was not created");
|
||||||
|
structureTab.structureDraft = { dirty: true } as never;
|
||||||
|
|
||||||
|
await store.removeConnection("conn-a");
|
||||||
|
|
||||||
|
// SQL 页签按策略保留;未保存的表结构草稿仍要先确认,不能静默丢弃。
|
||||||
|
expect(queryStore.tabs.some((tab) => tab.id === sqlId)).toBe(true);
|
||||||
|
expect(queryStore.showCloseConfirm).toBe(true);
|
||||||
|
expect(queryStore.pendingCloseTabId).toBe(structureId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keep-all-tabs closes nothing and keeps the current results", async () => {
|
||||||
|
installApiMocks();
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||||
|
useSettingsStore().editorSettings.deleteConnectionTabHandlingMode = "keep-all-tabs";
|
||||||
|
|
||||||
|
const store = useConnectionStore();
|
||||||
|
store.connections = [pgConnection("conn-a", "prod", "app")];
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
const sqlId = queryStore.createTab("conn-a", "app", "kept.sql", "query");
|
||||||
|
const dataId = queryStore.createTab("conn-a", "app", "users", "data", "public", undefined, undefined, { forceNew: true });
|
||||||
|
const sqlTab = queryStore.tabs.find((tab) => tab.id === sqlId);
|
||||||
|
if (!sqlTab) throw new Error("query tab was not created");
|
||||||
|
sqlTab.result = { columns: [], rows: [] } as never;
|
||||||
|
|
||||||
|
await store.removeConnection("conn-a");
|
||||||
|
|
||||||
|
// 连表格页签也保留,且结果不被清空(对应断开设置里的「不关闭相关页签」)。
|
||||||
|
expect(queryStore.tabs.map((tab) => tab.id)).toEqual([sqlId, dataId]);
|
||||||
|
expect(queryStore.tabs.find((tab) => tab.id === sqlId)?.result).toBeDefined();
|
||||||
|
expect(queryStore.tabs.find((tab) => tab.id === sqlId)?.detachedConnectionName).toBe("prod");
|
||||||
|
expect(queryStore.showCloseConfirm).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reproduces the reported flow: keep-all-tabs set through the real settings write path", async () => {
|
||||||
|
installApiMocks();
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
// 走设置面板真实使用的写入路径(而不是直接改字段),确保草稿/patch/归一化链路都覆盖。
|
||||||
|
settingsStore.updateEditorSettings({ deleteConnectionTabHandlingMode: "keep-all-tabs", rememberConnectionDatabaseOnDelete: true });
|
||||||
|
expect(settingsStore.editorSettings.deleteConnectionTabHandlingMode).toBe("keep-all-tabs");
|
||||||
|
|
||||||
|
const store = useConnectionStore();
|
||||||
|
store.connections = [pgConnection("conn-a", "prod", "app")];
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
const sqlId = queryStore.createTab("conn-a", "app", "draft.sql", "query");
|
||||||
|
queryStore.updateSql(sqlId, "select 1;");
|
||||||
|
|
||||||
|
// 真实删除流程:removeConnections 应用删除策略,随后 disconnect 只清会话。
|
||||||
|
await store.removeConnections(["conn-a"]);
|
||||||
|
await store.disconnect("conn-a", { skipTabHandling: true });
|
||||||
|
|
||||||
|
expect(queryStore.tabs.some((tab) => tab.id === sqlId)).toBe(true);
|
||||||
|
expect(queryStore.tabs.find((tab) => tab.id === sqlId)?.connectionId).toBe("conn-a");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps tabs untouched when a new connection has a different name", async () => {
|
||||||
|
installApiMocks();
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||||
|
useSettingsStore().editorSettings.deleteConnectionTabHandlingMode = "keep-sql-tabs";
|
||||||
|
|
||||||
|
const store = useConnectionStore();
|
||||||
|
store.connections = [pgConnection("conn-a", "prod", "app")];
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
const sqlId = queryStore.createTab("conn-a", "app", "draft.sql", "query");
|
||||||
|
await store.removeConnection("conn-a");
|
||||||
|
|
||||||
|
await store.addConnection(pgConnection("conn-b", "staging"));
|
||||||
|
|
||||||
|
const orphan = queryStore.tabs.find((tab) => tab.id === sqlId);
|
||||||
|
expect(orphan?.connectionId).toBe("conn-a");
|
||||||
|
expect(orphan?.detachedConnectionName).toBe("prod");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -30,6 +30,9 @@ function installApiMocks() {
|
|||||||
loadTableVGroups: vi.fn().mockResolvedValue({}),
|
loadTableVGroups: vi.fn().mockResolvedValue({}),
|
||||||
saveTableVGroups: vi.fn().mockResolvedValue(undefined),
|
saveTableVGroups: vi.fn().mockResolvedValue(undefined),
|
||||||
deleteTableVGroupsForConnection: vi.fn().mockResolvedValue(undefined),
|
deleteTableVGroupsForConnection: vi.fn().mockResolvedValue(undefined),
|
||||||
|
// 关闭页签会顺带回收结果/客户端会话;缺失的导出会让清理路径报错刷屏。
|
||||||
|
closeQuerySession: vi.fn().mockResolvedValue(undefined),
|
||||||
|
closeClientConnectionSession: vi.fn().mockResolvedValue(undefined),
|
||||||
connectionDatabaseInfo: vi.fn().mockResolvedValue(undefined),
|
connectionDatabaseInfo: vi.fn().mockResolvedValue(undefined),
|
||||||
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
||||||
sessionCredentialStatus: vi.fn().mockResolvedValue(false),
|
sessionCredentialStatus: vi.fn().mockResolvedValue(false),
|
||||||
@@ -126,9 +129,10 @@ describe("connectionStore one_time runtime cleanup", () => {
|
|||||||
expect(disconnectDb).not.toHaveBeenCalled();
|
expect(disconnectDb).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
// The connection is gone for good, so its tabs can never reconnect: executing in
|
// 删除连接是终态,页签一律由「删除连接」策略决定,one_time 连接也不例外——它的页签虽然
|
||||||
// one would fail with "Connection config not found".
|
// 无法再执行,但里面的 SQL 文本是用户的工作。默认 close-tabs 仍会关闭页签,只是未保存的
|
||||||
it("removeConnection closes the tabs of a one_time connection", async () => {
|
// 草稿必须先经保存/放弃确认,不能像以前那样被静默丢弃。
|
||||||
|
it("removeConnection asks before discarding an unsaved SQL draft of a one_time connection", async () => {
|
||||||
installApiMocks();
|
installApiMocks();
|
||||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
const { useQueryStore } = await import("@/stores/queryStore");
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
@@ -141,7 +145,28 @@ describe("connectionStore one_time runtime cleanup", () => {
|
|||||||
|
|
||||||
await store.removeConnection("preview-1");
|
await store.removeConnection("preview-1");
|
||||||
|
|
||||||
|
expect(queryStore.showCloseConfirm).toBe(true);
|
||||||
|
expect(queryStore.pendingCloseTabId).toBe(queryId);
|
||||||
|
|
||||||
|
queryStore.forceCloseAllPendingTabs();
|
||||||
expect(queryStore.tabs.some((tab) => tab.connectionId === "preview-1")).toBe(false);
|
expect(queryStore.tabs.some((tab) => tab.connectionId === "preview-1")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removeConnection keeps the SQL tabs of a one_time connection when the policy says so", async () => {
|
||||||
|
installApiMocks();
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||||
|
useSettingsStore().updateEditorSettings({ deleteConnectionTabHandlingMode: "keep-all-tabs" });
|
||||||
|
const store = useConnectionStore();
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
store.connections = [previewConnection()];
|
||||||
|
const queryId = queryStore.createTab("preview-1", "", "sales.parquet", "query");
|
||||||
|
queryStore.updateSql(queryId, "select 1;");
|
||||||
|
|
||||||
|
await store.removeConnection("preview-1");
|
||||||
|
|
||||||
|
expect(queryStore.tabs.some((tab) => tab.id === queryId)).toBe(true);
|
||||||
expect(queryStore.showCloseConfirm).toBe(false);
|
expect(queryStore.showCloseConfirm).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,7 +191,9 @@ describe("connectionStore one_time runtime cleanup", () => {
|
|||||||
expect(queryStore.tabs.some((tab) => tab.connectionId === "preview-1")).toBe(false);
|
expect(queryStore.tabs.some((tab) => tab.connectionId === "preview-1")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removeConnection keeps the tabs of a saved connection for the disconnect handling mode", async () => {
|
// 删除连接是终态(配置已从磁盘移除、无法重连),因此页签由「删除连接」策略决定,
|
||||||
|
// 不再复用「断开连接」策略。默认 close-tabs:删除即关闭该连接的页签。
|
||||||
|
it("removeConnection applies the delete tab handling mode to a saved connection", async () => {
|
||||||
installApiMocks();
|
installApiMocks();
|
||||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
const { useQueryStore } = await import("@/stores/queryStore");
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
@@ -177,6 +204,6 @@ describe("connectionStore one_time runtime cleanup", () => {
|
|||||||
|
|
||||||
await store.removeConnection("saved-1");
|
await store.removeConnection("saved-1");
|
||||||
|
|
||||||
expect(queryStore.tabs.some((tab) => tab.connectionId === "saved-1")).toBe(true);
|
expect(queryStore.tabs.some((tab) => tab.connectionId === "saved-1")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -313,6 +313,41 @@ describe("normalizeEditorSettings", () => {
|
|||||||
expect(normalizeEditorSettings({ restoreOpenTabsOnLaunch: true } as any).openTabsRestoreMode).toBe("all");
|
expect(normalizeEditorSettings({ restoreOpenTabsOnLaunch: true } as any).openTabsRestoreMode).toBe("all");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("defaults the delete-time tab handling to closing tabs and preserves explicit modes", () => {
|
||||||
|
expect(normalizeEditorSettings({}).deleteConnectionTabHandlingMode).toBe("close-tabs");
|
||||||
|
expect(normalizeEditorSettings({ deleteConnectionTabHandlingMode: "keep-sql-tabs" }).deleteConnectionTabHandlingMode).toBe("keep-sql-tabs");
|
||||||
|
expect(normalizeEditorSettings({ deleteConnectionTabHandlingMode: "keep-pinned-sql-tabs" }).deleteConnectionTabHandlingMode).toBe("keep-pinned-sql-tabs");
|
||||||
|
expect(normalizeEditorSettings({ deleteConnectionTabHandlingMode: "keep-all-tabs" }).deleteConnectionTabHandlingMode).toBe("keep-all-tabs");
|
||||||
|
expect(normalizeEditorSettings({ deleteConnectionTabHandlingMode: "invalid" as any }).deleteConnectionTabHandlingMode).toBe("close-tabs");
|
||||||
|
// 早期试验值收敛到最接近的正式取值。
|
||||||
|
expect(normalizeEditorSettings({ deleteConnectionTabHandlingMode: "keep-tabs" } as any).deleteConnectionTabHandlingMode).toBe("keep-sql-tabs");
|
||||||
|
expect(normalizeEditorSettings({ deleteConnectionTabHandlingMode: "keep-pinned" } as any).deleteConnectionTabHandlingMode).toBe("keep-pinned-sql-tabs");
|
||||||
|
expect(normalizeEditorSettings({ deleteConnectionTabHandlingMode: "keep-all" } as any).deleteConnectionTabHandlingMode).toBe("keep-all-tabs");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remembers connection databases by default and sanitizes the stored map", () => {
|
||||||
|
expect(normalizeEditorSettings({}).rememberConnectionDatabaseOnDelete).toBe(true);
|
||||||
|
expect(normalizeEditorSettings({ rememberConnectionDatabaseOnDelete: false }).rememberConnectionDatabaseOnDelete).toBe(false);
|
||||||
|
expect(normalizeEditorSettings({ rememberConnectionDatabaseOnDelete: "true" } as any).rememberConnectionDatabaseOnDelete).toBe(true);
|
||||||
|
expect(normalizeEditorSettings({ rememberedConnectionDatabases: { prod: { database: "app", dbType: "postgres" } } }).rememberedConnectionDatabases).toEqual({ prod: { database: "app", dbType: "postgres" } });
|
||||||
|
// 空名、缺库名/类型、以及非对象条目全部丢弃。
|
||||||
|
expect(
|
||||||
|
normalizeEditorSettings({
|
||||||
|
rememberedConnectionDatabases: {
|
||||||
|
" ": { database: "app", dbType: "postgres" },
|
||||||
|
dev: { database: "shop", dbType: "mysql" },
|
||||||
|
noDb: { database: " ", dbType: "mysql" },
|
||||||
|
noType: { database: "shop" },
|
||||||
|
legacyString: "shop",
|
||||||
|
bad: 3,
|
||||||
|
other: null,
|
||||||
|
},
|
||||||
|
} as any).rememberedConnectionDatabases,
|
||||||
|
).toEqual({ dev: { database: "shop", dbType: "mysql" } });
|
||||||
|
expect(normalizeEditorSettings({ rememberedConnectionDatabases: [] } as any).rememberedConnectionDatabases).toEqual({});
|
||||||
|
expect(normalizeEditorSettings({ rememberedConnectionDatabases: "prod" } as any).rememberedConnectionDatabases).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps unsaved SQL drafts on quit by default and preserves explicit modes", () => {
|
it("keeps unsaved SQL drafts on quit by default and preserves explicit modes", () => {
|
||||||
expect(normalizeEditorSettings({}).appCloseUnsavedTabsMode).toBe("keep-drafts");
|
expect(normalizeEditorSettings({}).appCloseUnsavedTabsMode).toBe("keep-drafts");
|
||||||
expect(normalizeEditorSettings({ appCloseUnsavedTabsMode: "prompt" }).appCloseUnsavedTabsMode).toBe("prompt");
|
expect(normalizeEditorSettings({ appCloseUnsavedTabsMode: "prompt" }).appCloseUnsavedTabsMode).toBe("prompt");
|
||||||
|
|||||||
@@ -132,8 +132,9 @@ import { loadTimeoutInheritanceBackup, saveTimeoutInheritanceBackup } from "@/li
|
|||||||
import { migrateSqlServerLegacyCompatibilityConfig, requiresSqlServerLegacyCompatibilityComponent, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
|
import { migrateSqlServerLegacyCompatibilityConfig, requiresSqlServerLegacyCompatibilityComponent, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
|
||||||
import { gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp";
|
import { gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp";
|
||||||
import { deleteTabResultSnapshotsForOwner } from "@/lib/tabs/tabResultCache";
|
import { deleteTabResultSnapshotsForOwner } from "@/lib/tabs/tabResultCache";
|
||||||
|
import { deletedConnectionTabKeepMode } from "@/lib/tabs/deletedConnectionTabs";
|
||||||
import { disposeSqlServerActivityTracesForConnection, hasSqlServerActivityTraceForConnection } from "@/lib/sqlserver/sqlServerActivityTraceRuntime";
|
import { disposeSqlServerActivityTracesForConnection, hasSqlServerActivityTraceForConnection } from "@/lib/sqlserver/sqlServerActivityTraceRuntime";
|
||||||
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterSchemaNamesForConnection, filterVisibleDatabaseNames, normalizeVisibleDatabaseSelection, visibleDatabasePatternsAreEnabled } from "@/lib/database/visibleDatabases";
|
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterSchemaNamesForConnection, filterVisibleDatabaseNames, isDraftVisibleSchemasConnectionId, normalizeVisibleDatabaseSelection, visibleDatabasePatternsAreEnabled } from "@/lib/database/visibleDatabases";
|
||||||
import {
|
import {
|
||||||
buildObjectGroupPlaceholderNodes,
|
buildObjectGroupPlaceholderNodes,
|
||||||
buildGroupedObjectTreeNodes,
|
buildGroupedObjectTreeNodes,
|
||||||
@@ -1118,27 +1119,43 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A one-time connection is deleted outright rather than left behind for a
|
* A one-time connection is deleted outright rather than left behind for a
|
||||||
* reconnect, so its tabs have nothing to point at once it is gone: executing in
|
* reconnect, so its runtime record has nothing left to point at.
|
||||||
* one would fail with "Connection config not found". Close them regardless of
|
*
|
||||||
* `disconnectTabHandlingMode`, which only makes sense for connections that can
|
* Tab handling is deliberately NOT done here: `applyDeletedConnectionTabHandling`
|
||||||
* still be reconnected.
|
* already applied `deleteConnectionTabHandlingMode` to these connections, so
|
||||||
|
* force-closing here would silently override the user's policy (and discard
|
||||||
|
* unsaved SQL drafts) — exactly what that setting exists to prevent.
|
||||||
*/
|
*/
|
||||||
async function closeOneTimeConnectionTabs(connectionIds: string[]) {
|
async function cleanupRemovedOneTimeConnections(connectionIds: string[]) {
|
||||||
if (!connectionIds.length) return;
|
releaseOneTimeRuntimeConnections(connectionIds);
|
||||||
const { useQueryStore } = await import("@/stores/queryStore");
|
|
||||||
const queryStore = useQueryStore();
|
|
||||||
for (const connectionId of connectionIds) {
|
|
||||||
queryStore.closeConnectionTabs(connectionId, { force: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupRemovedOneTimeConnections(connectionIds: string[]) {
|
/**
|
||||||
try {
|
* 删除连接后的页签处理:按 `deleteConnectionTabHandlingMode` 决定保留哪些 SQL 页签,
|
||||||
await closeOneTimeConnectionTabs(connectionIds);
|
* 并在开启「记住连接名与数据库」时记录 连接名 → { 数据库名, 类型 },供新建同名连接回填与重绑。
|
||||||
} catch (error) {
|
*
|
||||||
console.warn("[DBX][connection:delete:one-time-tab-cleanup-failed]", { connectionIds, error });
|
* `one_time` 临时连接同样走这套策略:它的页签虽然无法再执行,但里面的 SQL 文本是用户的工作,
|
||||||
|
* 不该被静默丢弃。
|
||||||
|
*
|
||||||
|
* 只排除可见 schema 选择器的临时草稿连接——它们只活在弹窗交互期间,不会出现在页签里。
|
||||||
|
* 空名连接无法按名字重绑,只跳过「记住连接名」;页签本身仍要按策略关闭/保留,
|
||||||
|
* 否则会留下指向已删除连接的孤儿页签。
|
||||||
|
*/
|
||||||
|
async function applyDeletedConnectionTabHandling(configs: readonly ConnectionConfig[]) {
|
||||||
|
const targets = configs.filter((config) => !isDraftVisibleSchemasConnectionId(config.id));
|
||||||
|
if (!targets.length) return;
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
const keep = deletedConnectionTabKeepMode(settingsStore.editorSettings.deleteConnectionTabHandlingMode);
|
||||||
|
const remember = settingsStore.editorSettings.rememberConnectionDatabaseOnDelete;
|
||||||
|
const namedTargets = targets.filter((config) => config.name.trim() !== "");
|
||||||
|
if (remember && namedTargets.length) settingsStore.rememberConnectionDatabases(namedTargets.map((config) => [config.name, config.database, config.db_type] as const));
|
||||||
|
for (const config of targets) {
|
||||||
|
queryStore.detachConnectionTabsForDelete(config.id, { keep, connectionName: remember && config.name.trim() !== "" ? config.name : undefined });
|
||||||
}
|
}
|
||||||
releaseOneTimeRuntimeConnections(connectionIds);
|
// 页签处理是同步的内存操作,但落盘是防抖的。这里立即冲刷,避免随后重启恢复出
|
||||||
|
// 已被策略关闭的页签。
|
||||||
|
await queryStore.flushPendingPersist().catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelDisconnectKey(connectionId: string, attempt: number): string {
|
function cancelDisconnectKey(connectionId: string, attempt: number): string {
|
||||||
@@ -3454,6 +3471,12 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
async function addConnection(config: ConnectionConfig, targetGroupId?: string | null) {
|
async function addConnection(config: ConnectionConfig, targetGroupId?: string | null) {
|
||||||
const normalized = normalizeConnection(config);
|
const normalized = normalizeConnection(config);
|
||||||
if (normalized.save_password === false) normalized.password = "";
|
if (normalized.save_password === false) normalized.password = "";
|
||||||
|
const isNewConnection = !connections.value.some((c) => c.id === normalized.id);
|
||||||
|
// 新建同名同类型连接时回填「记住的数据库」,让删除连接后重建的流程少一步手选。
|
||||||
|
if (isNewConnection && !normalized.database?.trim()) {
|
||||||
|
const remembered = settingsStore.rememberedDatabaseForConnection(normalized.name, normalized.db_type);
|
||||||
|
if (remembered) normalized.database = remembered;
|
||||||
|
}
|
||||||
await persistTimeoutInheritance(normalized.id, normalized.connect_timeout_inherit === true, normalized.query_timeout_inherit === true);
|
await persistTimeoutInheritance(normalized.id, normalized.connect_timeout_inherit === true, normalized.query_timeout_inherit === true);
|
||||||
const existing = connections.value.findIndex((c) => c.id === normalized.id);
|
const existing = connections.value.findIndex((c) => c.id === normalized.id);
|
||||||
const nextConnections = [...connections.value];
|
const nextConnections = [...connections.value];
|
||||||
@@ -3470,6 +3493,14 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
rebuildTreeNodes();
|
rebuildTreeNodes();
|
||||||
persistSidebarLayoutDebounced();
|
persistSidebarLayoutDebounced();
|
||||||
stopCreatingConnectionInGroup();
|
stopCreatingConnectionInGroup();
|
||||||
|
// 删除连接时保留下来的 SQL 页签按连接名重新绑定到新连接上(含数据库回填)。
|
||||||
|
if (isNewConnection) {
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const queryStore = useQueryStore();
|
||||||
|
if (queryStore.rebindDetachedTabs(normalized.name, normalized.id, normalized.database) > 0) {
|
||||||
|
await queryStore.flushPendingPersist().catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyConnectionsToTreeClipboard(connectionIds: Iterable<string>): number {
|
function copyConnectionsToTreeClipboard(connectionIds: Iterable<string>): number {
|
||||||
@@ -3590,12 +3621,20 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
|
|
||||||
const removedIds = new Set(connectionIds);
|
const removedIds = new Set(connectionIds);
|
||||||
const oneTimeIds = connectionIds.filter((id) => getConfig(id)?.one_time === true);
|
const oneTimeIds = connectionIds.filter((id) => getConfig(id)?.one_time === true);
|
||||||
|
// 连接配置在 applyConnectionRemoval 后就读不到了,先抓取快照供删除策略使用。
|
||||||
|
const removedConfigs = connectionIds.map((id) => getConfig(id)).filter((config): config is ConnectionConfig => !!config);
|
||||||
const nextConnections = connections.value.filter((c) => !removedIds.has(c.id));
|
const nextConnections = connections.value.filter((c) => !removedIds.has(c.id));
|
||||||
let nextLayout = sidebarLayout.value;
|
let nextLayout = sidebarLayout.value;
|
||||||
for (const id of removedIds) nextLayout = removeConnectionFromSidebarLayout(nextLayout, id);
|
for (const id of removedIds) nextLayout = removeConnectionFromSidebarLayout(nextLayout, id);
|
||||||
await persistConnectionDeletion(nextConnections, nextLayout);
|
await persistConnectionDeletion(nextConnections, nextLayout);
|
||||||
applyConnectionRemoval(removedIds, nextConnections, nextLayout);
|
applyConnectionRemoval(removedIds, nextConnections, nextLayout);
|
||||||
purgeTableVGroupsForConnections(removedIds);
|
purgeTableVGroupsForConnections(removedIds);
|
||||||
|
// 删除已经落盘完成;页签处理失败只告警,不能让已成功的删除以异常收场。
|
||||||
|
try {
|
||||||
|
await applyDeletedConnectionTabHandling(removedConfigs);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[DBX][connection:delete:tab-handling-failed]", { connectionIds: [...removedIds], error });
|
||||||
|
}
|
||||||
await cleanupRemovedOneTimeConnections(oneTimeIds);
|
await cleanupRemovedOneTimeConnections(oneTimeIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4274,7 +4313,14 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
clearConnectionHealthCheck(connectionId);
|
clearConnectionHealthCheck(connectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function disconnect(connectionId: string) {
|
/**
|
||||||
|
* 断开连接。
|
||||||
|
*
|
||||||
|
* `options.skipTabHandling` 用于「删除连接」流程:删除时页签已按
|
||||||
|
* `deleteConnectionTabHandlingMode` 处理过,这里只做会话清理,不能再套用
|
||||||
|
* `disconnectTabHandlingMode`,否则会把刚保留下来的 SQL 页签又关掉。
|
||||||
|
*/
|
||||||
|
async function disconnect(connectionId: string, options: { skipTabHandling?: boolean } = {}) {
|
||||||
const stateRevision = bumpConnectionStateRevision(connectionId);
|
const stateRevision = bumpConnectionStateRevision(connectionId);
|
||||||
const shouldRemoveOneTimeConnection = getConfig(connectionId)?.one_time === true;
|
const shouldRemoveOneTimeConnection = getConfig(connectionId)?.one_time === true;
|
||||||
if (hasSqlServerActivityTraceForConnection(connectionId)) await disposeSqlServerActivityTracesForConnection(connectionId);
|
if (hasSqlServerActivityTraceForConnection(connectionId)) await disposeSqlServerActivityTracesForConnection(connectionId);
|
||||||
@@ -4314,16 +4360,18 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
// 同表必须重新拉取结构)。completion/object-browser 缓存上面已清;
|
// 同表必须重新拉取结构)。completion/object-browser 缓存上面已清;
|
||||||
// 这里再走统一 helper,bump 连接代次并清 data-tab freshness。
|
// 这里再走统一 helper,bump 连接代次并清 data-tab freshness。
|
||||||
invalidateConnectionMetadataLifetime(connectionId);
|
invalidateConnectionMetadataLifetime(connectionId);
|
||||||
switch (settingsStore.editorSettings.disconnectTabHandlingMode) {
|
if (!options.skipTabHandling) {
|
||||||
case "close-tabs":
|
switch (settingsStore.editorSettings.disconnectTabHandlingMode) {
|
||||||
queryStore.closeConnectionTabs(connectionId);
|
case "close-tabs":
|
||||||
break;
|
queryStore.closeConnectionTabs(connectionId);
|
||||||
case "keep-tabs-clear-results":
|
break;
|
||||||
queryStore.releaseConnectionTabs(connectionId);
|
case "keep-tabs-clear-results":
|
||||||
break;
|
queryStore.releaseConnectionTabs(connectionId);
|
||||||
case "keep-tabs-keep-results":
|
break;
|
||||||
queryStore.rollbackConnectionTransactions(connectionId);
|
case "keep-tabs-keep-results":
|
||||||
break;
|
queryStore.rollbackConnectionTransactions(connectionId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Tab handling is synchronous in memory, but persistence is debounced. Flush
|
// Tab handling is synchronous in memory, but persistence is debounced. Flush
|
||||||
// the scoped tab state before disconnect returns so a quick reconnect/restart
|
// the scoped tab state before disconnect returns so a quick reconnect/restart
|
||||||
@@ -8944,6 +8992,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
const connectionIds = deleteConnections ? connectionIdsInGroupsOp(sidebarLayout.value, uniqueGroupIds).filter((id) => connections.value.some((connection) => connection.id === id)) : [];
|
const connectionIds = deleteConnections ? connectionIdsInGroupsOp(sidebarLayout.value, uniqueGroupIds).filter((id) => connections.value.some((connection) => connection.id === id)) : [];
|
||||||
const oneTimeIds = connectionIds.filter((id) => getConfig(id)?.one_time === true);
|
const oneTimeIds = connectionIds.filter((id) => getConfig(id)?.one_time === true);
|
||||||
const removedConnectionIds = new Set(connectionIds);
|
const removedConnectionIds = new Set(connectionIds);
|
||||||
|
// 删除策略需要连接配置,applyConnectionRemoval 之后就读不到了,先抓快照。
|
||||||
|
const removedConnectionConfigs = connectionIds.map((id) => getConfig(id)).filter((config): config is ConnectionConfig => !!config);
|
||||||
const nextConnections = removedConnectionIds.size ? connections.value.filter((connection) => !removedConnectionIds.has(connection.id)) : connections.value;
|
const nextConnections = removedConnectionIds.size ? connections.value.filter((connection) => !removedConnectionIds.has(connection.id)) : connections.value;
|
||||||
let layoutAfterConnectionRemoval = previousLayout;
|
let layoutAfterConnectionRemoval = previousLayout;
|
||||||
for (const id of removedConnectionIds) layoutAfterConnectionRemoval = removeConnectionFromSidebarLayout(layoutAfterConnectionRemoval, id);
|
for (const id of removedConnectionIds) layoutAfterConnectionRemoval = removeConnectionFromSidebarLayout(layoutAfterConnectionRemoval, id);
|
||||||
@@ -8954,6 +9004,12 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
if (removedConnectionIds.size) {
|
if (removedConnectionIds.size) {
|
||||||
applyConnectionRemoval(removedConnectionIds, nextConnections, nextLayout);
|
applyConnectionRemoval(removedConnectionIds, nextConnections, nextLayout);
|
||||||
purgeTableVGroupsForConnections(removedConnectionIds);
|
purgeTableVGroupsForConnections(removedConnectionIds);
|
||||||
|
// 删除已经落盘完成;页签处理失败只告警,不能让已成功的删除以异常收场。
|
||||||
|
try {
|
||||||
|
await applyDeletedConnectionTabHandling(removedConnectionConfigs);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[DBX][connection:delete:tab-handling-failed]", { connectionIds: [...removedConnectionIds], error });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
sidebarLayout.value = nextLayout;
|
sidebarLayout.value = nextLayout;
|
||||||
rebuildTreeNodes();
|
rebuildTreeNodes();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useI18n } from "vue-i18n";
|
|||||||
import { useToast } from "@/composables/useToast";
|
import { useToast } from "@/composables/useToast";
|
||||||
import { savedSqlErrorMessage } from "@/lib/savedSql/savedSqlErrors";
|
import { savedSqlErrorMessage } from "@/lib/savedSql/savedSqlErrors";
|
||||||
import { sanitizeTabPageUiState } from "@/lib/tabs/tabUiState";
|
import { sanitizeTabPageUiState } from "@/lib/tabs/tabUiState";
|
||||||
|
import type { DeletedConnectionTabKeepMode } from "@/lib/tabs/deletedConnectionTabs";
|
||||||
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, IndexInfo, NacosConfigEditorViewport, ObjectBrowserFilter, ObjectBrowserViewport, ObjectSource, ObjectSourceKind, QueryResult, QueryResultSourceColumnRef, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
|
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, IndexInfo, NacosConfigEditorViewport, ObjectBrowserFilter, ObjectBrowserViewport, ObjectSource, ObjectSourceKind, QueryResult, QueryResultSourceColumnRef, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
|
||||||
import { orderPinnedFirst } from "@/lib/app/pinnedItems";
|
import { orderPinnedFirst } from "@/lib/app/pinnedItems";
|
||||||
import { canCancelQueryExecution } from "@/lib/sql/queryExecutionState";
|
import { canCancelQueryExecution } from "@/lib/sql/queryExecutionState";
|
||||||
@@ -2423,6 +2424,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
whereInput: t.whereInput,
|
whereInput: t.whereInput,
|
||||||
pinned: t.pinned,
|
pinned: t.pinned,
|
||||||
mode: t.mode,
|
mode: t.mode,
|
||||||
|
detachedConnectionName: t.detachedConnectionName,
|
||||||
autoCommit: t.autoCommit,
|
autoCommit: t.autoCommit,
|
||||||
resultAutoSave: t.resultAutoSave,
|
resultAutoSave: t.resultAutoSave,
|
||||||
uiState: t.uiState,
|
uiState: t.uiState,
|
||||||
@@ -4578,33 +4580,94 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
return refreshed;
|
return refreshed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 释放单个页签的运行期状态:回滚事务、清空结果与执行态,但保留页签本身。 */
|
||||||
|
function releaseTabRuntimeState(tab: QueryTab) {
|
||||||
|
rollbackTabTransaction(tab, { resetAutoCommit: true });
|
||||||
|
clearDataGridPendingSnapshotsForTab(tab.id);
|
||||||
|
beginClosingDataGridViewSnapshotsForTab(tab.id);
|
||||||
|
beginClosingBrowserState(tab.id);
|
||||||
|
clearDataGridStructuredFilterStatesForTab(tab.id);
|
||||||
|
clearDataGridSearchStatesForTab(tab.id);
|
||||||
|
if (tab.isExecuting) void cancelTabExecution(tab.id);
|
||||||
|
if (tab.isExplaining) void cancelTabExplain(tab.id);
|
||||||
|
void closeResultSession(tab);
|
||||||
|
void closeClientConnectionSession(tab);
|
||||||
|
clearResultRunSnapshots(tab);
|
||||||
|
void deleteTabResultSnapshot(tabResultCacheKey(tab.id));
|
||||||
|
releaseTabResultObjectPayloads(tab);
|
||||||
|
clearResultRuns(tab);
|
||||||
|
clearResultPayload(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放匹配的页签:关闭非 SQL 页签,并清空 SQL 页签的运行期状态(保留页签本身)。
|
||||||
|
*/
|
||||||
function releaseTabsWhere(predicate: (tab: QueryTab) => boolean) {
|
function releaseTabsWhere(predicate: (tab: QueryTab) => boolean) {
|
||||||
closeTabsWhere((tab) => predicate(tab) && tab.mode !== "query");
|
closeTabsWhere((tab) => predicate(tab) && tab.mode !== "query");
|
||||||
tabs.value
|
tabs.value.filter((tab) => predicate(tab)).forEach(releaseTabRuntimeState);
|
||||||
.filter((tab) => predicate(tab))
|
|
||||||
.forEach((tab) => {
|
|
||||||
rollbackTabTransaction(tab, { resetAutoCommit: true });
|
|
||||||
clearDataGridPendingSnapshotsForTab(tab.id);
|
|
||||||
beginClosingDataGridViewSnapshotsForTab(tab.id);
|
|
||||||
beginClosingBrowserState(tab.id);
|
|
||||||
clearDataGridStructuredFilterStatesForTab(tab.id);
|
|
||||||
clearDataGridSearchStatesForTab(tab.id);
|
|
||||||
if (tab.isExecuting) void cancelTabExecution(tab.id);
|
|
||||||
if (tab.isExplaining) void cancelTabExplain(tab.id);
|
|
||||||
void closeResultSession(tab);
|
|
||||||
void closeClientConnectionSession(tab);
|
|
||||||
clearResultRunSnapshots(tab);
|
|
||||||
void deleteTabResultSnapshot(tabResultCacheKey(tab.id));
|
|
||||||
releaseTabResultObjectPayloads(tab);
|
|
||||||
clearResultRuns(tab);
|
|
||||||
clearResultPayload(tab);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function releaseConnectionTabs(connectionId: string) {
|
function releaseConnectionTabs(connectionId: string) {
|
||||||
releaseTabsWhere((tab) => tab.connectionId === connectionId);
|
releaseTabsWhere((tab) => tab.connectionId === connectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除连接时的页签处理:连接配置已从磁盘移除、无法再重连,因此策略与断开连接分开。
|
||||||
|
* - `none`:关闭该连接的全部页签(默认)
|
||||||
|
* - `sql`:保留全部 SQL 页签(`mode === "query"`),其余关闭
|
||||||
|
* - `pinned-sql`:只保留固定的 SQL 页签,其余关闭
|
||||||
|
* - `all`:不关闭任何页签,保留 SQL 文本与当前结果(与断开设置的「不关闭相关页签」一致)
|
||||||
|
*
|
||||||
|
* 关闭一律走非 force 路径(`closeScopedTabsWhere` → `beginBatchClose`):干净页签立即关闭,
|
||||||
|
* 未保存的 SQL 草稿与表结构草稿都要经保存/放弃确认,删除连接不能静默丢改动。
|
||||||
|
*
|
||||||
|
* 该连接下的 SQL 页签都会记录原连接名(`detachedConnectionName`),便于新建同名连接后
|
||||||
|
* 重新绑定;包括因用户在确认框中选择取消而残留的页签。
|
||||||
|
*/
|
||||||
|
function detachConnectionTabsForDelete(connectionId: string, options: { keep?: DeletedConnectionTabKeepMode; connectionName?: string } = {}) {
|
||||||
|
const keepMode = options.keep ?? "none";
|
||||||
|
const keep = keepMode === "all" ? () => true : keepMode === "pinned-sql" ? (tab: QueryTab) => tab.mode === "query" && tab.pinned === true : keepMode === "sql" ? (tab: QueryTab) => tab.mode === "query" : () => false;
|
||||||
|
const connectionName = options.connectionName?.trim();
|
||||||
|
// 先给该连接下的 SQL 页签打上脱离标记。关闭流程可能因为「未保存」确认被用户取消,
|
||||||
|
// 取消后残留的页签同样需要能按连接名重绑到新建的同名连接。
|
||||||
|
if (connectionName) {
|
||||||
|
for (const tab of tabs.value) {
|
||||||
|
if (tab.connectionId !== connectionId || tab.mode !== "query") continue;
|
||||||
|
tab.detachedConnectionName = connectionName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (keepMode === "all") {
|
||||||
|
// 连接已消失,任何未结束的事务都不可能再提交,但仍保留页签与已加载的结果。
|
||||||
|
rollbackConnectionTransactions(connectionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeScopedTabsWhere((tab) => tab.connectionId === connectionId && !keep(tab));
|
||||||
|
tabs.value.filter((tab) => tab.connectionId === connectionId && keep(tab)).forEach(releaseTabRuntimeState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建同名连接后,把因删除连接而保留下来的 SQL 页签重新绑定到新连接。
|
||||||
|
* 返回重新绑定的页签数量;同名页签已指向该连接时只清除脱离标记。
|
||||||
|
*
|
||||||
|
* 页签自身记录的库优先——它可能指向该连接下的另一个库;只有页签没有库时才回落到
|
||||||
|
* 新连接的默认库,避免重绑把页签的执行上下文改掉。
|
||||||
|
*/
|
||||||
|
function rebindDetachedTabs(connectionName: string, connectionId: string, database?: string): number {
|
||||||
|
const name = connectionName.trim();
|
||||||
|
if (!name) return 0;
|
||||||
|
const fallbackDatabase = database?.trim();
|
||||||
|
let rebound = 0;
|
||||||
|
for (const tab of tabs.value) {
|
||||||
|
if (tab.detachedConnectionName !== name) continue;
|
||||||
|
tab.detachedConnectionName = undefined;
|
||||||
|
if (tab.connectionId === connectionId) continue;
|
||||||
|
tab.connectionId = connectionId;
|
||||||
|
if (fallbackDatabase && !tab.database?.trim()) tab.database = fallbackDatabase;
|
||||||
|
rebound += 1;
|
||||||
|
}
|
||||||
|
return rebound;
|
||||||
|
}
|
||||||
|
|
||||||
function releaseDatabaseTabs(connectionId: string, database: string) {
|
function releaseDatabaseTabs(connectionId: string, database: string) {
|
||||||
releaseTabsWhere((tab) => tab.connectionId === connectionId && tab.database === database);
|
releaseTabsWhere((tab) => tab.connectionId === connectionId && tab.database === database);
|
||||||
}
|
}
|
||||||
@@ -8830,6 +8893,8 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
refreshDataTabsForTable,
|
refreshDataTabsForTable,
|
||||||
releaseConnectionTabs,
|
releaseConnectionTabs,
|
||||||
releaseDatabaseTabs,
|
releaseDatabaseTabs,
|
||||||
|
detachConnectionTabsForDelete,
|
||||||
|
rebindDetachedTabs,
|
||||||
staleConnectionDataTabMetadata,
|
staleConnectionDataTabMetadata,
|
||||||
isDatabaseOpen,
|
isDatabaseOpen,
|
||||||
openDatabaseKeys,
|
openDatabaseKeys,
|
||||||
|
|||||||
@@ -673,6 +673,13 @@ export const SIDEBAR_INDENT_DEFAULT = 16;
|
|||||||
const DISCONNECT_TAB_HANDLING_MODES = ["close-tabs", "keep-tabs-clear-results", "keep-tabs-keep-results"] as const;
|
const DISCONNECT_TAB_HANDLING_MODES = ["close-tabs", "keep-tabs-clear-results", "keep-tabs-keep-results"] as const;
|
||||||
export type DisconnectTabHandlingMode = (typeof DISCONNECT_TAB_HANDLING_MODES)[number];
|
export type DisconnectTabHandlingMode = (typeof DISCONNECT_TAB_HANDLING_MODES)[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除连接(连接配置已从磁盘移除,无法再重连)时,对该连接已打开页签的处理策略。
|
||||||
|
* 与 {@link DisconnectTabHandlingMode} 分开:断开连接仍可重连,删除则不会。
|
||||||
|
*/
|
||||||
|
const DELETE_CONNECTION_TAB_HANDLING_MODES = ["close-tabs", "keep-sql-tabs", "keep-pinned-sql-tabs", "keep-all-tabs"] as const;
|
||||||
|
export type DeleteConnectionTabHandlingMode = (typeof DELETE_CONNECTION_TAB_HANDLING_MODES)[number];
|
||||||
|
|
||||||
const CLICK_TABLE_NAVIGATION_TARGETS = ["data", "ddl"] as const;
|
const CLICK_TABLE_NAVIGATION_TARGETS = ["data", "ddl"] as const;
|
||||||
export type ClickTableNavigationTarget = (typeof CLICK_TABLE_NAVIGATION_TARGETS)[number];
|
export type ClickTableNavigationTarget = (typeof CLICK_TABLE_NAVIGATION_TARGETS)[number];
|
||||||
|
|
||||||
@@ -744,6 +751,16 @@ export const DEFAULT_CUSTOM_THEMES: CustomTheme[] = [
|
|||||||
|
|
||||||
export type SidebarObjectInfoMode = "comment-inline" | "comment-aligned" | "comment-right" | "size" | "hidden";
|
export type SidebarObjectInfoMode = "comment-inline" | "comment-aligned" | "comment-right" | "size" | "hidden";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除连接时记住的「连接名 → 数据库」。带上 `dbType` 是为了只在新建同名**同类型**
|
||||||
|
* 连接时回填——像 "test"/"local" 这类名字常被不同数据库类型复用,跨类型回填会把
|
||||||
|
* 无意义的库名写进新连接配置。
|
||||||
|
*/
|
||||||
|
export interface RememberedConnectionDatabase {
|
||||||
|
database: string;
|
||||||
|
dbType: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EditorSettings {
|
export interface EditorSettings {
|
||||||
fontFamily: string;
|
fontFamily: string;
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
@@ -863,6 +880,11 @@ export interface EditorSettings {
|
|||||||
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: number;
|
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: number;
|
||||||
openTabsRestoreMode: OpenTabsRestoreMode;
|
openTabsRestoreMode: OpenTabsRestoreMode;
|
||||||
disconnectTabHandlingMode: DisconnectTabHandlingMode;
|
disconnectTabHandlingMode: DisconnectTabHandlingMode;
|
||||||
|
deleteConnectionTabHandlingMode: DeleteConnectionTabHandlingMode;
|
||||||
|
/** 删除连接时记住「连接名 → 数据库名」,新建同名同类型连接时自动回填并重绑保留的 SQL 页签。 */
|
||||||
|
rememberConnectionDatabaseOnDelete: boolean;
|
||||||
|
/** 已记住的「连接名 → { 数据库名, 数据库类型 }」映射,用于新建同名连接时自动选中数据库。 */
|
||||||
|
rememberedConnectionDatabases: Record<string, RememberedConnectionDatabase>;
|
||||||
dataTabReuseMode: DataTabReuseMode;
|
dataTabReuseMode: DataTabReuseMode;
|
||||||
openDataTabsNextToActive: boolean;
|
openDataTabsNextToActive: boolean;
|
||||||
prefillNewQueryWithSelect: boolean;
|
prefillNewQueryWithSelect: boolean;
|
||||||
@@ -1121,6 +1143,9 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
|||||||
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION,
|
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION,
|
||||||
openTabsRestoreMode: "all",
|
openTabsRestoreMode: "all",
|
||||||
disconnectTabHandlingMode: "close-tabs",
|
disconnectTabHandlingMode: "close-tabs",
|
||||||
|
deleteConnectionTabHandlingMode: "close-tabs",
|
||||||
|
rememberConnectionDatabaseOnDelete: true,
|
||||||
|
rememberedConnectionDatabases: {},
|
||||||
dataTabReuseMode: DEFAULT_DATA_TAB_REUSE_MODE,
|
dataTabReuseMode: DEFAULT_DATA_TAB_REUSE_MODE,
|
||||||
openDataTabsNextToActive: false,
|
openDataTabsNextToActive: false,
|
||||||
prefillNewQueryWithSelect: true,
|
prefillNewQueryWithSelect: true,
|
||||||
@@ -1310,6 +1335,35 @@ function normalizeDisconnectTabHandlingMode(value: unknown, legacyCloseTabsOnDis
|
|||||||
return DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode;
|
return DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDeleteConnectionTabHandlingMode(value: unknown): DeleteConnectionTabHandlingMode {
|
||||||
|
if (DELETE_CONNECTION_TAB_HANDLING_MODES.includes(value as DeleteConnectionTabHandlingMode)) {
|
||||||
|
return value as DeleteConnectionTabHandlingMode;
|
||||||
|
}
|
||||||
|
// 兼容早期试验值:把「保留页签」统一收敛到最接近的正式取值。
|
||||||
|
if (value === "keep-tabs" || value === "keep-sql") return "keep-sql-tabs";
|
||||||
|
if (value === "keep-pinned" || value === "keep-fixed-tabs") return "keep-pinned-sql-tabs";
|
||||||
|
if (value === "keep-all") return "keep-all-tabs";
|
||||||
|
return DEFAULT_EDITOR_SETTINGS.deleteConnectionTabHandlingMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 记住的连接名 → 数据库映射:只保留合法条目,并限制条目数量避免无限增长。 */
|
||||||
|
const REMEMBERED_CONNECTION_DATABASES_LIMIT = 200;
|
||||||
|
|
||||||
|
function normalizeRememberedConnectionDatabases(value: unknown): Record<string, RememberedConnectionDatabase> {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
||||||
|
const entries: [string, RememberedConnectionDatabase][] = [];
|
||||||
|
for (const [name, entry] of Object.entries(value as Record<string, unknown>)) {
|
||||||
|
const key = name.trim();
|
||||||
|
if (!key || !entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
||||||
|
const { database, dbType } = entry as { database?: unknown; dbType?: unknown };
|
||||||
|
if (typeof database !== "string" || !database.trim() || typeof dbType !== "string") continue;
|
||||||
|
entries.push([key, { database, dbType }]);
|
||||||
|
}
|
||||||
|
if (entries.length <= REMEMBERED_CONNECTION_DATABASES_LIMIT) return Object.fromEntries(entries);
|
||||||
|
// 超出上限时保留最后写入的部分(对象插入顺序即写入顺序)。
|
||||||
|
return Object.fromEntries(entries.slice(entries.length - REMEMBERED_CONNECTION_DATABASES_LIMIT));
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeClickTableNavigationTarget(value: unknown): ClickTableNavigationTarget {
|
function normalizeClickTableNavigationTarget(value: unknown): ClickTableNavigationTarget {
|
||||||
return value === "data" || value === "ddl" ? value : DEFAULT_EDITOR_SETTINGS.clickTableNavigationTarget;
|
return value === "data" || value === "ddl" ? value : DEFAULT_EDITOR_SETTINGS.clickTableNavigationTarget;
|
||||||
}
|
}
|
||||||
@@ -1650,6 +1704,9 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
|||||||
}
|
}
|
||||||
).closeQueryTabsOnDisconnect,
|
).closeQueryTabsOnDisconnect,
|
||||||
),
|
),
|
||||||
|
deleteConnectionTabHandlingMode: normalizeDeleteConnectionTabHandlingMode(settings.deleteConnectionTabHandlingMode),
|
||||||
|
rememberConnectionDatabaseOnDelete: typeof settings.rememberConnectionDatabaseOnDelete === "boolean" ? settings.rememberConnectionDatabaseOnDelete : DEFAULT_EDITOR_SETTINGS.rememberConnectionDatabaseOnDelete,
|
||||||
|
rememberedConnectionDatabases: normalizeRememberedConnectionDatabases(settings.rememberedConnectionDatabases),
|
||||||
dataTabReuseMode: normalizeDataTabReuseMode(
|
dataTabReuseMode: normalizeDataTabReuseMode(
|
||||||
settings.dataTabReuseMode,
|
settings.dataTabReuseMode,
|
||||||
(
|
(
|
||||||
@@ -2414,6 +2471,9 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||||||
if (partial.sidebarBrowseObjectsOnDatabaseActivation !== undefined) editorSettings.value.sidebarBrowseObjectsOnDatabaseActivation = partial.sidebarBrowseObjectsOnDatabaseActivation === true;
|
if (partial.sidebarBrowseObjectsOnDatabaseActivation !== undefined) editorSettings.value.sidebarBrowseObjectsOnDatabaseActivation = partial.sidebarBrowseObjectsOnDatabaseActivation === true;
|
||||||
if (partial.openTabsRestoreMode !== undefined) editorSettings.value.openTabsRestoreMode = normalizeOpenTabsRestoreMode(partial.openTabsRestoreMode);
|
if (partial.openTabsRestoreMode !== undefined) editorSettings.value.openTabsRestoreMode = normalizeOpenTabsRestoreMode(partial.openTabsRestoreMode);
|
||||||
if (partial.disconnectTabHandlingMode !== undefined) editorSettings.value.disconnectTabHandlingMode = normalizeDisconnectTabHandlingMode(partial.disconnectTabHandlingMode);
|
if (partial.disconnectTabHandlingMode !== undefined) editorSettings.value.disconnectTabHandlingMode = normalizeDisconnectTabHandlingMode(partial.disconnectTabHandlingMode);
|
||||||
|
if (partial.deleteConnectionTabHandlingMode !== undefined) editorSettings.value.deleteConnectionTabHandlingMode = normalizeDeleteConnectionTabHandlingMode(partial.deleteConnectionTabHandlingMode);
|
||||||
|
if (partial.rememberConnectionDatabaseOnDelete !== undefined) editorSettings.value.rememberConnectionDatabaseOnDelete = partial.rememberConnectionDatabaseOnDelete === true;
|
||||||
|
if (partial.rememberedConnectionDatabases !== undefined) editorSettings.value.rememberedConnectionDatabases = normalizeRememberedConnectionDatabases(partial.rememberedConnectionDatabases);
|
||||||
if (partial.dataTabReuseMode !== undefined) editorSettings.value.dataTabReuseMode = normalizeDataTabReuseMode(partial.dataTabReuseMode);
|
if (partial.dataTabReuseMode !== undefined) editorSettings.value.dataTabReuseMode = normalizeDataTabReuseMode(partial.dataTabReuseMode);
|
||||||
if (partial.openDataTabsNextToActive !== undefined) editorSettings.value.openDataTabsNextToActive = partial.openDataTabsNextToActive === true;
|
if (partial.openDataTabsNextToActive !== undefined) editorSettings.value.openDataTabsNextToActive = partial.openDataTabsNextToActive === true;
|
||||||
if (partial.prefillNewQueryWithSelect !== undefined) editorSettings.value.prefillNewQueryWithSelect = partial.prefillNewQueryWithSelect;
|
if (partial.prefillNewQueryWithSelect !== undefined) editorSettings.value.prefillNewQueryWithSelect = partial.prefillNewQueryWithSelect;
|
||||||
@@ -2526,6 +2586,46 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量记住「连接名 → 数据库名 + 类型」;数据库名或类型为空表示清除该条目。
|
||||||
|
* 删除连接时写入,新建同名同类型连接时回填。整体只触发一次设置落盘。
|
||||||
|
*/
|
||||||
|
function rememberConnectionDatabases(entries: Iterable<readonly [string, string | undefined, string | undefined]>) {
|
||||||
|
const next = { ...editorSettings.value.rememberedConnectionDatabases };
|
||||||
|
let changed = false;
|
||||||
|
for (const [connectionName, database, dbType] of entries) {
|
||||||
|
const name = connectionName.trim();
|
||||||
|
const value = database?.trim();
|
||||||
|
if (!name || !value || !dbType) {
|
||||||
|
if (name && name in next) {
|
||||||
|
delete next[name];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const previous = next[name];
|
||||||
|
if (previous?.database === value && previous.dbType === dbType) continue;
|
||||||
|
// 重新插入以刷新写入顺序,让上限裁剪优先淘汰最旧的条目。
|
||||||
|
delete next[name];
|
||||||
|
next[name] = { database: value, dbType };
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!changed) return;
|
||||||
|
updateEditorSettings({ rememberedConnectionDatabases: next });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 只在连接名与数据库类型都匹配时返回记住的数据库;类型不同视为无关记录。 */
|
||||||
|
function rememberedDatabaseForConnection(connectionName: string, dbType: string): string {
|
||||||
|
const name = connectionName.trim();
|
||||||
|
if (!name || !dbType) return "";
|
||||||
|
const entry = editorSettings.value.rememberedConnectionDatabases[name];
|
||||||
|
return entry && entry.dbType === dbType ? entry.database : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearRememberedConnectionDatabases() {
|
||||||
|
updateEditorSettings({ rememberedConnectionDatabases: {} });
|
||||||
|
}
|
||||||
|
|
||||||
function updateColumnFormatter(key: string, formatter: ColumnFormatterConfig | undefined) {
|
function updateColumnFormatter(key: string, formatter: ColumnFormatterConfig | undefined) {
|
||||||
const columnFormatters = { ...editorSettings.value.columnFormatters };
|
const columnFormatters = { ...editorSettings.value.columnFormatters };
|
||||||
const normalized = normalizeColumnFormatter(formatter);
|
const normalized = normalizeColumnFormatter(formatter);
|
||||||
@@ -2677,6 +2777,9 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||||||
initMcpGlobalPolicy,
|
initMcpGlobalPolicy,
|
||||||
updateMcpGlobalPolicy,
|
updateMcpGlobalPolicy,
|
||||||
updateColumnFormatter,
|
updateColumnFormatter,
|
||||||
|
rememberConnectionDatabases,
|
||||||
|
rememberedDatabaseForConnection,
|
||||||
|
clearRememberedConnectionDatabases,
|
||||||
customColumnFormatterDeleteVersion,
|
customColumnFormatterDeleteVersion,
|
||||||
upsertCustomColumnFormatter,
|
upsertCustomColumnFormatter,
|
||||||
deleteCustomColumnFormatter,
|
deleteCustomColumnFormatter,
|
||||||
|
|||||||
@@ -1599,6 +1599,11 @@ export interface QueryTab {
|
|||||||
forceWordWrap?: boolean;
|
forceWordWrap?: boolean;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
database: string;
|
database: string;
|
||||||
|
/**
|
||||||
|
* 所属连接被删除后,被保留下来的 SQL 页签会记录原连接名。新建同名连接时按此
|
||||||
|
* 字段把页签重新绑定到新连接上;绑定完成后清空。
|
||||||
|
*/
|
||||||
|
detachedConnectionName?: string;
|
||||||
/** Optional branch context for a driver-profile database workspace. */
|
/** Optional branch context for a driver-profile database workspace. */
|
||||||
workspaceBranch?: string;
|
workspaceBranch?: string;
|
||||||
schema?: string;
|
schema?: string;
|
||||||
|
|||||||
Reference in new issue
Block a user