diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index d73e40150..0a11b9266 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -188,6 +188,15 @@ async function scrollToSidebarNode(nodeId: string) { } } +function clearSidebarSelection() { + // Clicking the blank area of the tree clears the current selection. Row + // clicks call event.stopPropagation(), so this only fires for blank clicks + // (issue #681 — selection wasn't cleared in double-click activation mode). + store.selectedTreeNodeId = null; + store.selectedTreeNodeIds = []; + store.treeSelectionAnchorId = null; +} + async function createNewGroup() { const groupId = store.createConnectionGroup(t("connectionGroup.newGroupDefault")); pendingRenameGroupId.value = groupId; @@ -506,6 +515,7 @@ defineExpose({ focusSearch, createNewGroup }); ref="treeScrollerRef" class="sidebar-tree connection-tree-scroller min-h-0 flex-1 overflow-y-auto" :class="sidebarTreeOverflowClass" + @click="clearSidebarSelection" :items="flatNodes" :item-size="SIDEBAR_TREE_ROW_HEIGHT" :buffer="SIDEBAR_TREE_SCROLL_BUFFER" @@ -533,6 +543,7 @@ defineExpose({ focusSearch, createNewGroup }); ref="plainTreeScrollerRef" class="sidebar-tree min-h-0 flex-1 overflow-y-auto" :class="sidebarTreeOverflowClass" + @click="clearSidebarSelection" > finishRenameGroup again. + // The first call can rebuild the tree and recycle props.node onto a different + // group, so a second run would act on the wrong group and cascade across + // groups (issue #681). + if (!isRenamingGroup.value) return; isRenamingGroup.value = false; const trimmed = renameInput.value.trim(); - if (!trimmed) { - connectionStore.deleteConnectionGroup(props.node.id); - return; - } - if (trimmed !== props.node.label) { - connectionStore.renameConnectionGroup(props.node.id, trimmed); - } + // An empty name cancels the rename and keeps the group as-is — never delete + // here. Deleting a group is done explicitly via the context menu (issue #681). + if (!trimmed || trimmed === props.node.label) return; + connectionStore.renameConnectionGroup(props.node.id, trimmed); } function deleteConnectionGroup() { @@ -2725,7 +2731,13 @@ const { startDrag, updateTarget, clearTarget, -} = useDragSort((draggedId, targetId, position) => connectionStore.reorderSidebarEntry(draggedId, targetId, position)); +} = useDragSort((draggedId, targetId, position) => { + // If the grabbed row is part of a multi-selection, move all selected rows + // together; otherwise just the grabbed one (issue #681). + const selected = connectionStore.selectedTreeNodeIds; + const draggedIds = selected.length > 1 && selected.includes(draggedId) ? [...selected] : [draggedId]; + connectionStore.reorderSidebarEntries(draggedIds, targetId, position); +}); const isDraggable = computed(() => { if (props.dragDisabled) return false; diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 8d3759c88..0d2ab622e 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -2344,5 +2344,17 @@ export const useConnectionStore = defineStore("connection", () => { reorderSidebarEntry(draggedId: string, targetId: string, position: DropPosition) { updateLayoutAndRebuild(reorderEntryOp(sidebarLayout.value, draggedId, targetId, position)); }, + reorderSidebarEntries(draggedIds: string[], targetId: string, position: DropPosition) { + // Apply each dragged entry in turn so a multi-selection moves together, + // not just the single grabbed row (issue #681). + let layout = sidebarLayout.value; + let changed = false; + for (const id of draggedIds) { + if (id === targetId) continue; + layout = reorderEntryOp(layout, id, targetId, position); + changed = true; + } + if (changed) updateLayoutAndRebuild(layout); + }, }; });