Enable safe multi-connection cleanup from the sidebar (#816)
The sidebar already supports multi-select, so deletion now resolves the context-clicked selected connection set before confirming. Connection removal gained a batched path to persist once while pruning sidebar layout, pins, active state, connection errors, and stale selection for every removed connection. Constraint: No new dependencies and existing single-connection delete behavior must remain available.\nRejected: Delete every selected tree item regardless of type | mixed tree selections could unexpectedly remove connections while table/object deletion has separate SQL confirmation behavior.\nConfidence: high\nScope-risk: narrow\nTested: pnpm fmt; pnpm test; pnpm typecheck; pnpm lint; pnpm build\nNot-tested: Manual desktop UI interaction against a running Tauri window Co-authored-by: caisin <caisin@caisins-Mac-mini.local>
This commit is contained in:
1 parent
74e19ec6a3
commit
13363b9094
10 files changed
+137
-19
No files matched your search
@@ -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[] {
|
||||
<DialogTitle>{{ t("contextMenu.confirmDeleteTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t("contextMenu.confirmDeleteMessage", { name: node.label }) }}
|
||||
{{ connectionDeleteConfirmMessage() }}
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showDeleteConfirm = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
@@ -3393,7 +3415,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
|
||||
showDeleteConfirm = false;
|
||||
confirmDelete();
|
||||
"
|
||||
>{{ t("contextMenu.deleteConnection") }}</Button
|
||||
>{{ connectionDeleteMenuLabel() }}</Button
|
||||
>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "复制连接",
|
||||
|
||||
@@ -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: "複製連線",
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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<string>) {
|
||||
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,
|
||||
|
||||
Reference in new issue
Block a user