diff --git a/apps/desktop/src/components/layout/AppSidebar.vue b/apps/desktop/src/components/layout/AppSidebar.vue index 2ff359f7e..829d5dcbf 100644 --- a/apps/desktop/src/components/layout/AppSidebar.vue +++ b/apps/desktop/src/components/layout/AppSidebar.vue @@ -2,8 +2,10 @@ import { computed, ref } from "vue"; import { useI18n } from "vue-i18n"; import { translateBackendError } from "@/i18n/backend-errors"; -import { Upload, Download, FolderPlus, RefreshCw, ChevronsLeft, ChevronsUp } from "@lucide/vue"; +import { Upload, Download, FolderPlus, 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"; import LightDropdown from "@/components/ui/LightDropdown.vue"; import LightTooltip from "@/components/ui/LightTooltip.vue"; import ConnectionTree from "@/components/sidebar/ConnectionTree.vue"; @@ -22,16 +24,41 @@ const emit = defineEmits<{ collapse: []; }>(); +type ImportSource = "dbx" | "navicat" | "dbeaver" | "datagrip"; + const { t } = useI18n(); const connectionStore = useConnectionStore(); const { toast } = useToast(); const connectionTreeRef = ref>(); +const showDeleteSelectedConfirm = ref(false); +const showCreateSelectedGroupDialog = ref(false); +const selectedGroupName = ref(""); +const UNGROUPED_GROUP_VALUE = "__ungrouped"; const importSourceItems = computed(() => [ { value: "dbx", label: t("sidebar.importDbx") }, { value: "navicat", label: t("sidebar.importNavicat") }, { value: "dbeaver", label: t("sidebar.importDbeaver") }, { value: "datagrip", label: t("sidebar.importDatagrip") }, ]); +const connectionIdSet = computed(() => new Set(connectionStore.connections.map((connection) => connection.id))); +const allConnectionIds = computed(() => connectionStore.connections.map((connection) => connection.id)); +const selectedConnectionIds = computed(() => (connectionStore.connectionMultiSelectActive ? connectionStore.selectedTreeNodeIds.filter((id) => connectionIdSet.value.has(id)) : [])); +const selectedConnectionCount = computed(() => selectedConnectionIds.value.length); +const showConnectionMultiSelectToolbar = computed(() => connectionStore.connectionMultiSelectActive && selectedConnectionCount.value > 0); +const allConnectionsSelected = computed(() => allConnectionIds.value.length > 0 && selectedConnectionCount.value === allConnectionIds.value.length); +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) => ({ + value: group.id, + label: group.name, + })), + { + value: UNGROUPED_GROUP_VALUE, + label: t("connectionGroup.ungrouped"), + separatorBefore: connectionStore.sidebarLayout.groups.length > 0, + }, +]); async function refreshTree() { try { @@ -45,6 +72,10 @@ function createNewGroup() { void connectionTreeRef.value?.createNewGroup(); } +function selectImportSource(source: string) { + emit("import", source as ImportSource); +} + function collapseAllTreeNodes() { connectionTreeRef.value?.collapseAllTreeNodes(); } @@ -53,6 +84,66 @@ function focusSearch(): boolean { return connectionTreeRef.value?.focusSearch() ?? false; } +function clearConnectionMultiSelection() { + connectionStore.connectionMultiSelectActive = false; + connectionStore.selectedTreeNodeIds = []; + connectionStore.selectedTreeNodeId = null; + connectionStore.treeSelectionAnchorId = null; +} + +function toggleAllConnectionsSelected() { + if (allConnectionsSelected.value) { + clearConnectionMultiSelection(); + return; + } + + const ids = allConnectionIds.value; + connectionStore.connectionMultiSelectActive = ids.length > 0; + connectionStore.selectedTreeNodeIds = ids; + connectionStore.selectedTreeNodeId = ids[0] ?? null; + connectionStore.treeSelectionAnchorId = ids[0] ?? null; +} + +async function confirmDeleteSelectedConnections() { + const ids = selectedConnectionIds.value; + if (ids.length === 0) return; + try { + await connectionStore.removeConnections(ids); + for (const connectionId of ids) { + connectionStore.disconnect(connectionId).catch((error) => { + console.warn("[DBX][connection:delete:disconnect-failed]", { connectionId, error }); + }); + } + clearConnectionMultiSelection(); + showDeleteSelectedConfirm.value = false; + toast(t("connection.deletedSelected", { count: ids.length }), 2000); + } catch (e: any) { + toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000); + } +} + +function moveSelectedConnectionsToGroup(value: string) { + const groupId = value === UNGROUPED_GROUP_VALUE ? null : value; + for (const connectionId of selectedConnectionIds.value) { + connectionStore.moveConnectionToGroup(connectionId, groupId); + } +} + +function openCreateSelectedGroupDialog() { + selectedGroupName.value = ""; + showCreateSelectedGroupDialog.value = true; +} + +function confirmCreateSelectedGroup() { + const name = selectedGroupName.value.trim(); + if (!name || selectedConnectionIds.value.length === 0) return; + const groupId = connectionStore.createConnectionGroup(name); + for (const connectionId of selectedConnectionIds.value) { + connectionStore.moveConnectionToGroup(connectionId, groupId); + } + showCreateSelectedGroupDialog.value = false; +} + defineExpose({ focusSearch }); @@ -62,55 +153,125 @@ defineExpose({ focusSearch });
{{ t("sidebar.connections") }} - - - - - - - - - - - - - - - - - - - - + +
+ + + + {{ t("contextMenu.confirmDeleteTitle") }} + +

+ {{ t("contextMenu.confirmDeleteSelectedMessage", { count: selectedConnectionCount }) }} +

+ + + + +
+
+ + + + {{ t("connectionGroup.createGroup") }} + + + + + + + +
diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 6348fb9ee..59c7f7ac6 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -440,6 +440,7 @@ 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.connectionMultiSelectActive = false; store.selectedTreeNodeId = null; store.selectedTreeNodeIds = []; store.treeSelectionAnchorId = null; diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 7f2c6f040..b87885f40 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -1,5 +1,5 @@