diff --git a/apps/desktop/src/components/layout/AppSidebar.connectionMultiSelect.spec.ts b/apps/desktop/src/components/layout/AppSidebar.connectionMultiSelect.spec.ts index 4ae0a2ad0d..0aa9e84703 100644 --- a/apps/desktop/src/components/layout/AppSidebar.connectionMultiSelect.spec.ts +++ b/apps/desktop/src/components/layout/AppSidebar.connectionMultiSelect.spec.ts @@ -135,7 +135,10 @@ const mountedApps: Array<{ unmount: () => void; host: HTMLElement }> = []; function createStore() { return reactive({ connections: [{ id: "conn-visible" }, { id: "conn-hidden" }, { id: "conn-next" }], - sidebarLayout: { groups: [{ id: "group-a", name: "Group A" }] }, + sidebarLayout: { + groups: [{ id: "group-a", name: "Group A" }], + order: [{ type: "group", id: "group-a", children: [] }], + }, selectedTreeNodeIds: [] as string[], selectedTreeNodeId: null as string | null, treeSelectionAnchorId: null as string | null, diff --git a/apps/desktop/src/components/layout/AppSidebar.vue b/apps/desktop/src/components/layout/AppSidebar.vue index 3c006d1a59..54c3197b10 100644 --- a/apps/desktop/src/components/layout/AppSidebar.vue +++ b/apps/desktop/src/components/layout/AppSidebar.vue @@ -2,7 +2,7 @@ import { computed, ref } from "vue"; import { useI18n } from "vue-i18n"; import { translateBackendError } from "@/i18n/backend-errors"; -import { Upload, Download, FolderPlus, RefreshCw, ChevronsLeft, ChevronsUp, Trash2, FolderInput, Check, Minus, Square, X } from "@lucide/vue"; +import { Upload, Download, FolderPlus, FolderOpen, RefreshCw, ChevronsLeft, ChevronsUp, Trash2, FolderInput, Check, Minus, Square, X } from "@lucide/vue"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; @@ -10,6 +10,7 @@ import LightDropdown from "@/components/ui/LightDropdown.vue"; import LightTooltip from "@/components/ui/LightTooltip.vue"; import ConnectionTree from "@/components/sidebar/ConnectionTree.vue"; import { applyConnectionMultiSelection, emptyConnectionMultiSelection, isExitConnectionMultiSelectionShortcut } from "@/lib/sidebar/sidebarConnectionMultiSelect"; +import { connectionGroupDestinationRows } from "@/lib/sidebar/sidebarLayout"; import { useConnectionStore } from "@/stores/connectionStore"; import { useToast } from "@/composables/useToast"; import type { QueryTab, TreeNode } from "@/types/database"; @@ -53,9 +54,12 @@ const allConnectionsSelected = computed(() => allConnectionIds.value.length > 0 const selectAllIcon = computed(() => (allConnectionsSelected.value ? Check : selectedConnectionCount.value > 0 ? Minus : Square)); const selectAllLabel = computed(() => (allConnectionsSelected.value ? t("connectionGroup.deselectAllConnections") : t("connectionGroup.selectAllConnections"))); const moveGroupItems = computed(() => [ - ...connectionStore.sidebarLayout.groups.map((group) => ({ + ...connectionGroupDestinationRows(connectionStore.sidebarLayout).map((group) => ({ value: group.id, label: group.name, + title: group.path.join(" / "), + icon: FolderOpen, + indentLevel: group.depth, })), { value: UNGROUPED_GROUP_VALUE, diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index f83910e60e..089c7a6c3b 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -74,6 +74,7 @@ import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase"; import { connectionUsesVisibleSchemaFilter } from "@/lib/database/visibleDatabases"; import { canTreeNodePin, canTreeNodeShowExpander } from "@/lib/sidebar/sidebarTreeItemLayout"; import { sidebarConnectionVisibleFilterMenu } from "@/lib/sidebar/sidebarVisibleFilterMenu"; +import { connectionGroupDestinationRows } from "@/lib/sidebar/sidebarLayout"; import { objectTypesForGroupNode } from "@/lib/table/tableTree"; import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting"; import { isXuguTypeMemberContainer } from "@/lib/sidebar/xuguTypeMembers"; @@ -148,7 +149,7 @@ import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connec import { hasTreeNodeDatabaseContext } from "@/lib/sidebar/treeNodeContext"; import { defaultPasteTableMode, pasteTableModeCopiesData, supportsWholeRowTableDataCopy, tableClipboardMatchesTarget, tableClipboardMenuState, tableClipboardSourceContext, tableDataCopyColumnOptions, type TableClipboardContext, type TableClipboardTableContext } from "@/lib/table/tableClipboard"; import { selectedSidebarBatchTargets, selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes } from "@/lib/sidebar/sidebarTreeSelection"; -import { connectionPasteTargetGroupId, selectedConnectionClipboardTargets, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection"; +import { connectionPasteTargetGroupId, selectedConnectionClipboardTargets, selectedConnectionEditTarget, selectedConnectionMoveTargets } from "@/lib/sidebar/sidebarConnectionSelection"; import { connectionSupportsDatabaseUserAdmin, resolveDatabaseUserAdminProviderForConnection, type DatabaseUserIdentity } from "@/lib/database/databaseUserAdmin"; import { authorizationPlanSql, authorizationPlanStatus, buildCreateDatabaseAuthorizationPlan, executeAuthorizationPlan, type AuthorizationPlan, type AuthorizationStepResult } from "@/lib/database/databaseAuthorizationPlan"; import { connectionSupportsProcessList } from "@/lib/database/processListDrivers"; @@ -4539,23 +4540,7 @@ function getTreeItemDialogController(): Record { return treeItemDialogController; } -const availableGroups = computed(() => connectionStore.sidebarLayout.groups); - -const currentGroupId = computed(() => { - if (activeNode.value.type !== "connection" || !activeNode.value.connectionId) return null; - const find = (entries: typeof connectionStore.sidebarLayout.order): string | null => { - for (const entry of entries) { - if (entry.type !== "group") continue; - if ((entry.children ?? entry.connectionIds?.map((id) => ({ type: "connection" as const, id })) ?? []).some((child) => child.type === "connection" && child.id === activeNode.value.connectionId)) { - return entry.id; - } - const found = find(entry.children ?? []); - if (found) return found; - } - return null; - }; - return find(connectionStore.sidebarLayout.order); -}); +const availableGroups = computed(() => connectionGroupDestinationRows(connectionStore.sidebarLayout)); onBeforeUnmount(() => { stopDangerDialogRouting?.(); @@ -4766,14 +4751,18 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean }); } items.push({ label: "", separator: true }); - if (availableGroups.value.length > 0 || currentGroupId.value) { - const groupChildren: ContextMenuItem[] = availableGroups.value.map((group: { id: string; name: string }) => ({ + const moveTargets = selectedConnectionMoveTargets(node, selectedTreeNodesInVisibleOrder()); + const allMoveTargetsInGroup = (groupId: string | null) => moveTargets.length > 0 && moveTargets.every((target) => connectionStore.groupIdForConnection(target.connectionId) === groupId); + if (availableGroups.value.length > 0 || !allMoveTargetsInGroup(null)) { + const groupChildren: ContextMenuItem[] = availableGroups.value.map((group) => ({ label: group.name, + title: group.path.join(" / "), action: () => moveToGroup(group.id), icon: FolderOpen, - disabled: group.id === currentGroupId.value, + indentLevel: group.depth, + disabled: allMoveTargetsInGroup(group.id), })); - if (currentGroupId.value) { + if (!allMoveTargetsInGroup(null)) { groupChildren.push({ label: "", separator: true }); groupChildren.push({ label: t("connectionGroup.ungrouped"), action: () => moveToGroup(null) }); } diff --git a/apps/desktop/src/components/ui/CustomContextMenu.vue b/apps/desktop/src/components/ui/CustomContextMenu.vue index bb35ca87b3..2e6514ea30 100644 --- a/apps/desktop/src/components/ui/CustomContextMenu.vue +++ b/apps/desktop/src/components/ui/CustomContextMenu.vue @@ -282,7 +282,7 @@ onBeforeUnmount(() => {
- -