diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue
index fd18588a2c..498b84bc90 100644
--- a/apps/desktop/src/components/sidebar/TreeItem.vue
+++ b/apps/desktop/src/components/sidebar/TreeItem.vue
@@ -131,6 +131,7 @@ import {
treeSelectionRangeIdsByIndex,
treeSelectionRangeIds,
} from "@/lib/sidebarTreeSelection";
+import { selectedConnectionDeleteTargets } from "@/lib/sidebarConnectionSelection";
import { sidebarTreeContextKey } from "@/lib/sidebarTreeContext";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
import ProcedureExecutionDialog from "@/components/objects/ProcedureExecutionDialog.vue";
@@ -904,20 +905,41 @@ async function refresh() {
const showDeleteConfirm = ref(false);
+function connectionDeleteTargets() {
+ return selectedConnectionDeleteTargets(props.node, selectedTreeNodesInVisibleOrder());
+}
+
+function connectionDeleteMenuLabel(): string {
+ const count = connectionDeleteTargets().length;
+ return count > 1 ? t("contextMenu.deleteSelectedConnections", { count }) : t("contextMenu.deleteConnection");
+}
+
+function connectionDeleteConfirmMessage(): string {
+ const targets = connectionDeleteTargets();
+ return targets.length > 1
+ ? t("contextMenu.confirmDeleteSelectedMessage", { count: targets.length })
+ : t("contextMenu.confirmDeleteMessage", { name: props.node.label });
+}
+
function deleteConnection() {
+ if (!connectionDeleteTargets().length) return;
showDeleteConfirm.value = true;
}
async function confirmDelete() {
- const node = props.node;
- if (node.connectionId) {
- try {
- await connectionStore.disconnect(node.connectionId);
- await connectionStore.removeConnection(node.connectionId);
- toast(t("connection.deleted"), 2000);
- } catch (e: any) {
- toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000);
+ const targets = connectionDeleteTargets();
+ if (!targets.length) return;
+ try {
+ for (const target of targets) {
+ await connectionStore.disconnect(target.connectionId);
}
+ await connectionStore.removeConnections(targets.map((target) => target.connectionId));
+ toast(
+ targets.length > 1 ? t("connection.deletedSelected", { count: targets.length }) : t("connection.deleted"),
+ 2000,
+ );
+ } catch (e: any) {
+ toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000);
}
}
@@ -2913,7 +2935,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
items.push({ label: t("contextMenu.duplicateConnection"), action: duplicateConnection, icon: CopyPlus });
items.push({ label: "", separator: true });
items.push({
- label: t("contextMenu.deleteConnection"),
+ label: connectionDeleteMenuLabel(),
action: deleteConnection,
icon: Trash2,
shortcut: shortcutDelete,
@@ -3383,7 +3405,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
{{ t("contextMenu.confirmDeleteTitle") }}
- {{ t("contextMenu.confirmDeleteMessage", { name: node.label }) }}
+ {{ connectionDeleteConfirmMessage() }}
@@ -3393,7 +3415,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
showDeleteConfirm = false;
confirmDelete();
"
- >{{ t("contextMenu.deleteConnection") }}{{ connectionDeleteMenuLabel() }}
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index e3483b53a2..fef4ce32a8 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -223,6 +223,7 @@ export default {
clearError: "Clear connection error",
saveFailed: "Failed to save connection: {message}",
deleted: "Connection deleted",
+ deletedSelected: "{count} connections deleted",
copied: "Copied",
disconnected: "Disconnected",
databaseConnectionClosed: "Closed database connection: {name}",
@@ -939,8 +940,11 @@ export default {
closeConnection: "Close Connection",
closeDatabaseConnection: "Close Database Connection",
deleteConnection: "Delete Connection",
+ deleteSelectedConnections: "Delete Selected {count} Connections",
confirmDeleteTitle: "Delete Connection",
confirmDeleteMessage: 'Are you sure you want to delete "{name}"? This cannot be undone.',
+ confirmDeleteSelectedMessage:
+ "Are you sure you want to delete the selected {count} connections? This cannot be undone.",
editConnection: "Edit Connection",
selectVisibleDatabases: "Select Visible Databases",
duplicateConnection: "Duplicate Connection",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 8f07b5d9cb..8353208fd5 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -215,6 +215,7 @@ export default {
clearError: "Limpiar error de conexión",
saveFailed: "Error al guardar la conexión: {message}",
deleted: "Conexión eliminada",
+ deletedSelected: "{count} conexiones eliminadas",
copied: "Copiado",
disconnected: "Desconectado",
databaseConnectionClosed: "Conexión de base de datos cerrada: {name}",
@@ -834,8 +835,10 @@ export default {
closeConnection: "Cerrar conexión",
closeDatabaseConnection: "Cerrar conexión de base de datos",
deleteConnection: "Eliminar conexión",
+ deleteSelectedConnections: "Eliminar {count} conexiones seleccionadas",
confirmDeleteTitle: "Eliminar conexión",
confirmDeleteMessage: '¿Estás seguro de que deseas eliminar "{name}"? Esta acción no se puede deshacer.',
+ confirmDeleteSelectedMessage: "¿Eliminar las {count} conexiones seleccionadas? Esta acción no se puede deshacer.",
editConnection: "Editar conexión",
selectVisibleDatabases: "Seleccionar bases visibles",
duplicateConnection: "Duplicar conexión",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index befa75e25d..8da5646f38 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -226,6 +226,7 @@ export default {
clearError: "Cancella errore di connessione",
saveFailed: "Impossibile salvare la connessione: {message}",
deleted: "Connessione eliminata",
+ deletedSelected: "{count} connessioni eliminate",
copied: "Copiato",
disconnected: "Disconnesso",
databaseConnectionClosed: "Connessione al database chiusa: {name}",
@@ -956,8 +957,11 @@ export default {
closeConnection: "Chiudi Connessione",
closeDatabaseConnection: "Chiudi Connessione Database",
deleteConnection: "Elimina Connessione",
+ deleteSelectedConnections: "Elimina {count} connessioni selezionate",
confirmDeleteTitle: "Elimina Connessione",
confirmDeleteMessage: 'Sei sicuro di voler eliminare "{name}"? Questa azione non può essere annullata.',
+ confirmDeleteSelectedMessage:
+ "Eliminare le {count} connessioni selezionate? Questa azione non può essere annullata.",
editConnection: "Modifica Connessione",
selectVisibleDatabases: "Seleziona Database Visibili",
duplicateConnection: "Duplica Connessione",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 611fa0ee92..257cc063b7 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -225,6 +225,7 @@ export default {
clearError: "Limpar erro de conexão",
saveFailed: "Falha ao salvar a conexão: {message}",
deleted: "Conexão excluída",
+ deletedSelected: "{count} conexões excluídas",
copied: "Copiado",
disconnected: "Desconectado",
databaseConnectionClosed: "Conexão de banco de dados fechada: {name}",
@@ -952,8 +953,10 @@ export default {
closeConnection: "Fechar Conexão",
closeDatabaseConnection: "Fechar Conexão do Banco de Dados",
deleteConnection: "Excluir Conexão",
+ deleteSelectedConnections: "Excluir {count} conexões selecionadas",
confirmDeleteTitle: "Excluir Conexão",
confirmDeleteMessage: 'Tem certeza de que deseja excluir "{name}"? Esta ação não pode ser desfeita.',
+ confirmDeleteSelectedMessage: "Excluir as {count} conexões selecionadas? Esta ação não pode ser desfeita.",
editConnection: "Editar Conexão",
selectVisibleDatabases: "Selecionar Bancos de Dados Visíveis",
duplicateConnection: "Duplicar Conexão",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 14a9474ff8..c68783ef3d 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -219,6 +219,7 @@ export default {
clearError: "清除连接错误",
saveFailed: "保存连接失败:{message}",
deleted: "连接已删除",
+ deletedSelected: "已删除 {count} 个连接",
copied: "已复制",
disconnected: "已断开连接",
databaseConnectionClosed: "已关闭数据库连接:{name}",
@@ -919,8 +920,10 @@ export default {
closeConnection: "关闭连接",
closeDatabaseConnection: "关闭数据库连接",
deleteConnection: "删除连接",
+ deleteSelectedConnections: "删除选中的 {count} 个连接",
confirmDeleteTitle: "删除连接",
confirmDeleteMessage: "确定要删除「{name}」吗?此操作不可撤销。",
+ confirmDeleteSelectedMessage: "确定要删除选中的{count}个连接吗?",
editConnection: "编辑连接",
selectVisibleDatabases: "选择显示数据库",
duplicateConnection: "复制连接",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index 3e55f0ee16..65b797cbb1 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -219,6 +219,7 @@ export default {
clearError: "清除連線錯誤",
saveFailed: "儲存連線失敗:{message}",
deleted: "連線已刪除",
+ deletedSelected: "已刪除 {count} 個連線",
copied: "已複製",
disconnected: "已斷開連線",
databaseConnectionClosed: "已關閉資料庫連線:{name}",
@@ -897,8 +898,10 @@ export default {
closeConnection: "關閉連線",
closeDatabaseConnection: "關閉資料庫連線",
deleteConnection: "刪除連線",
+ deleteSelectedConnections: "刪除選取的 {count} 個連線",
confirmDeleteTitle: "刪除連線",
confirmDeleteMessage: "確定要刪除「{name}」嗎?此操作無法復原。",
+ confirmDeleteSelectedMessage: "確定要刪除選取的{count}個連線嗎?",
editConnection: "編輯連線",
selectVisibleDatabases: "選擇顯示資料庫",
duplicateConnection: "複製連線",
diff --git a/apps/desktop/src/lib/sidebarConnectionSelection.ts b/apps/desktop/src/lib/sidebarConnectionSelection.ts
new file mode 100644
index 0000000000..a0cdb5b1a0
--- /dev/null
+++ b/apps/desktop/src/lib/sidebarConnectionSelection.ts
@@ -0,0 +1,19 @@
+import type { TreeNode } from "@/types/database";
+
+type ConnectionTreeNode = TreeNode & { connectionId: string };
+
+function isConnectionNode(node: TreeNode): node is ConnectionTreeNode {
+ return node.type === "connection" && !!node.connectionId;
+}
+
+export function selectedConnectionDeleteTargets(
+ currentNode: TreeNode,
+ selectedNodes: TreeNode[],
+): ConnectionTreeNode[] {
+ if (!isConnectionNode(currentNode)) return [];
+ const selectedContainsCurrent = selectedNodes.some((node) => node.id === currentNode.id);
+ if (selectedNodes.length > 1 && selectedContainsCurrent && selectedNodes.every(isConnectionNode)) {
+ return selectedNodes;
+ }
+ return [currentNode];
+}
diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts
index 8342fe7fed..b21ea9fe60 100644
--- a/apps/desktop/src/stores/connectionStore.ts
+++ b/apps/desktop/src/stores/connectionStore.ts
@@ -584,21 +584,39 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
- async function removeConnection(id: string) {
- const nextConnections = connections.value.filter((c) => c.id !== id);
+ async function removeConnections(ids: Iterable) {
+ const connectionIds = [...new Set(ids)].filter((id) => connections.value.some((c) => c.id === id));
+ if (!connectionIds.length) return;
+
+ const removedIds = new Set(connectionIds);
+ const nextConnections = connections.value.filter((c) => !removedIds.has(c.id));
await persistConnections(nextConnections);
connections.value = nextConnections;
- pinnedTreeNodeIds.value = prunePinnedTreeNodeIdsForConnection(pinnedTreeNodeIds.value, id);
+ for (const id of removedIds) {
+ pinnedTreeNodeIds.value = prunePinnedTreeNodeIdsForConnection(pinnedTreeNodeIds.value, id);
+ }
persistPinnedTreeNodeIds();
- clearConnectionError(id);
- sidebarLayout.value = removeConnectionFromSidebarLayout(sidebarLayout.value, id);
+ for (const id of removedIds) {
+ clearConnectionError(id);
+ connectedIds.value.delete(id);
+ sidebarLayout.value = removeConnectionFromSidebarLayout(sidebarLayout.value, id);
+ }
rebuildTreeNodes();
persistSidebarLayoutDebounced();
- if (activeConnectionId.value === id) {
+ if (activeConnectionId.value && removedIds.has(activeConnectionId.value)) {
activeConnectionId.value = null;
}
- invalidateCompletionCache(id);
- clearLoadedChildrenCache(id);
+ selectedTreeNodeIds.value = selectedTreeNodeIds.value.filter((id) => !removedIds.has(id));
+ if (selectedTreeNodeId.value && removedIds.has(selectedTreeNodeId.value)) selectedTreeNodeId.value = null;
+ if (treeSelectionAnchorId.value && removedIds.has(treeSelectionAnchorId.value)) treeSelectionAnchorId.value = null;
+ for (const id of removedIds) {
+ invalidateCompletionCache(id);
+ clearLoadedChildrenCache(id);
+ }
+ }
+
+ async function removeConnection(id: string) {
+ await removeConnections([id]);
}
async function updateConnection(config: ConnectionConfig) {
@@ -2126,6 +2144,7 @@ export const useConnectionStore = defineStore("connection", () => {
setVisibleDatabases,
clearVisibleDatabases,
removeConnection,
+ removeConnections,
editingConnectionId,
newConnectionGroupId,
startEditing,
diff --git a/packages/app-tests/sidebarConnectionSelection.test.ts b/packages/app-tests/sidebarConnectionSelection.test.ts
new file mode 100644
index 0000000000..223bd1c3d4
--- /dev/null
+++ b/packages/app-tests/sidebarConnectionSelection.test.ts
@@ -0,0 +1,38 @@
+import { strict as assert } from "node:assert";
+import { test } from "node:test";
+import { selectedConnectionDeleteTargets } from "../../apps/desktop/src/lib/sidebarConnectionSelection.ts";
+import type { TreeNode } from "../../apps/desktop/src/types/database.ts";
+
+function node(id: string, type: TreeNode["type"] = "connection"): TreeNode {
+ return { id, label: id, type, connectionId: type === "connection" ? id : "conn-1" };
+}
+
+test("deletes all selected connections when the context-clicked connection is selected", () => {
+ const first = node("conn-1");
+ const second = node("conn-2");
+
+ assert.deepEqual(
+ selectedConnectionDeleteTargets(first, [first, second]).map((target) => target.connectionId),
+ ["conn-1", "conn-2"],
+ );
+});
+
+test("falls back to only the current connection for mixed selections", () => {
+ const connection = node("conn-1");
+ const table = node("table-1", "table");
+
+ assert.deepEqual(
+ selectedConnectionDeleteTargets(connection, [connection, table]).map((target) => target.connectionId),
+ ["conn-1"],
+ );
+});
+
+test("falls back to only the current connection when right-clicking outside the selection", () => {
+ const current = node("conn-1");
+ const selected = node("conn-2");
+
+ assert.deepEqual(
+ selectedConnectionDeleteTargets(current, [selected]).map((target) => target.connectionId),
+ ["conn-1"],
+ );
+});