diff --git a/apps/desktop/src/components/layout/AppSidebar.connectionMultiSelect.spec.ts b/apps/desktop/src/components/layout/AppSidebar.connectionMultiSelect.spec.ts index ade7b1fb5b..4ae0a2ad0d 100644 --- a/apps/desktop/src/components/layout/AppSidebar.connectionMultiSelect.spec.ts +++ b/apps/desktop/src/components/layout/AppSidebar.connectionMultiSelect.spec.ts @@ -212,6 +212,44 @@ describe("AppSidebar connection multi-select moves", () => { expect(mocks.store.selectedTreeNodeIds).toEqual([]); }); + it("shows the selected count beside the connection title instead of as a toolbar item", async () => { + setSelection(["conn-visible", "conn-hidden"]); + const host = await mountSidebar(); + + const count = host.querySelector("[data-connection-selection-count]"); + expect(count?.textContent?.trim()).toBe("connectionGroup.selectedConnections"); + expect(count?.previousElementSibling?.textContent?.trim()).toBe("sidebar.connections"); + }); + + it("exits connection multi-select when Escape is pressed in the sidebar", async () => { + setSelection(["conn-visible", "conn-hidden"]); + const host = await mountSidebar(); + const event = new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }); + + host.querySelector(".app-sidebar-panel")!.dispatchEvent(event); + await nextTick(); + + expect(event.defaultPrevented).toBe(true); + expect(mocks.store.selectedTreeNodeIds).toEqual([]); + expect(mocks.store.selectedTreeNodeId).toBeNull(); + expect(mocks.store.treeSelectionAnchorId).toBeNull(); + expect(mocks.store.connectionMultiSelectActive).toBe(false); + }); + + it("does not exit connection multi-select when Escape comes from an input", async () => { + setSelection(["conn-visible", "conn-hidden"]); + const host = await mountSidebar(); + click(host.querySelector('[data-tooltip="connectionGroup.createGroup"] button')); + await nextTick(); + const input = host.querySelector("[data-dialog] input"); + + input!.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true })); + await nextTick(); + + expect(mocks.store.selectedTreeNodeIds).toEqual(["conn-visible", "conn-hidden"]); + expect(mocks.store.connectionMultiSelectActive).toBe(true); + }); + it("keeps selection on cancel and releases it after creating a new group", async () => { setSelection(["conn-visible", "conn-hidden"]); const host = await mountSidebar(); diff --git a/apps/desktop/src/components/layout/AppSidebar.vue b/apps/desktop/src/components/layout/AppSidebar.vue index a90e4e220b..0683c3c033 100644 --- a/apps/desktop/src/components/layout/AppSidebar.vue +++ b/apps/desktop/src/components/layout/AppSidebar.vue @@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input"; import LightDropdown from "@/components/ui/LightDropdown.vue"; import LightTooltip from "@/components/ui/LightTooltip.vue"; import ConnectionTree from "@/components/sidebar/ConnectionTree.vue"; -import { applyConnectionMultiSelection, emptyConnectionMultiSelection } from "@/lib/sidebar/sidebarConnectionMultiSelect"; +import { applyConnectionMultiSelection, emptyConnectionMultiSelection, isExitConnectionMultiSelectionShortcut } from "@/lib/sidebar/sidebarConnectionMultiSelect"; import { useConnectionStore } from "@/stores/connectionStore"; import { useToast } from "@/composables/useToast"; @@ -90,6 +90,18 @@ function clearConnectionMultiSelection() { applyConnectionMultiSelection(connectionStore, emptyConnectionMultiSelection()); } +function isEditableSidebarTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) return false; + return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target.isContentEditable || !!target.closest("[contenteditable='true'], [role='textbox']"); +} + +function onSidebarKeydown(event: KeyboardEvent) { + if (event.defaultPrevented || !connectionStore.connectionMultiSelectActive || !isExitConnectionMultiSelectionShortcut(event) || isEditableSidebarTarget(event.target)) return; + event.preventDefault(); + event.stopPropagation(); + clearConnectionMultiSelection(); +} + function toggleAllConnectionsSelected() { if (allConnectionsSelected.value) { clearConnectionMultiSelection(); @@ -153,13 +165,17 @@ defineExpose({ focusSearch });