Three independent connection-list bugs: 1. Multi-select drag only moved one row. The drag system tracks a single draggedId and ignored selectedTreeNodeIds. The drop callback now expands to the full selection when the grabbed row is part of it, and the store gains reorderSidebarEntries() to move them together. 2. Clicking the blank tree area didn't clear the selection (notably in double-click activation mode). Row clicks now stopPropagation and the tree containers clear the selection on click, so only blank clicks reset. 3. Creating a group then submitting an empty name dissolved ALL groups. Enter (@keydown.enter) and the following @blur both fired finishRenameGroup; the first call rebuilt the tree and recycled props.node onto another group, so the second deleted the wrong one, cascading. Guard against double invocation and treat an empty name as a cancel (never delete here — deleting a group stays in the context menu). Verified end-to-end on the web build: multi-select drag nests both connections under the group; blank-click clears selection; empty-name Enter keeps all groups intact. pnpm check is green. Co-authored-by: vrustx <vrustx@vrustxdeMac-mini.local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c54e43e4b9
commit
14fa15531f
|
|
@ -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"
|
||||
>
|
||||
<TreeItem
|
||||
v-for="item in flatNodes"
|
||||
|
|
|
|||
|
|
@ -530,6 +530,9 @@ function onClick(event: MouseEvent) {
|
|||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
// Row clicks must not bubble to the tree container, whose click handler
|
||||
// clears the selection when the blank area is clicked (issue #681).
|
||||
event.stopPropagation();
|
||||
if (event.shiftKey) {
|
||||
selectTreeNodeRange(props.node);
|
||||
rowRef.value?.focus({ preventScroll: true });
|
||||
|
|
@ -2591,15 +2594,18 @@ watch(
|
|||
);
|
||||
|
||||
function finishRenameGroup() {
|
||||
// Guard against double invocation: pressing Enter sets isRenamingGroup=false
|
||||
// and unmounts the input, which then fires @blur -> 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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue