diff --git a/apps/desktop/src/components/transfer/DataTransferDialog.vue b/apps/desktop/src/components/transfer/DataTransferDialog.vue
index 1a9dc9bb5a..4f8cdd346e 100644
--- a/apps/desktop/src/components/transfer/DataTransferDialog.vue
+++ b/apps/desktop/src/components/transfer/DataTransferDialog.vue
@@ -10,10 +10,8 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import SearchableSelect from "@/components/ui/searchable-select/SearchableSelect.vue";
-import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
+import ConnectionTreeSelect from "@/components/connection/ConnectionTreeSelect.vue";
import { useConnectionStore } from "@/stores/connectionStore";
-import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
-import { connectionIconType } from "@/lib/connection/connectionPresentation";
import * as api from "@/lib/backend/api";
import type { TransferContent, TransferMode, TransferObjectKind, TransferTableNameCase } from "@/lib/backend/api";
import { crossFamilyTransferableKinds, isSameTransferFamily, transferObjectKindsForDatabase } from "@/lib/database/transferObjectKinds";
@@ -946,25 +944,16 @@ async function saveConfigTask() {
-
-
-
-
-
- {{ label }}
-
-
-
+ trigger-class="h-8 w-full max-w-none justify-between gap-1.5 border border-input rounded-md bg-transparent px-2.5 text-xs shadow-none hover:bg-muted/40 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30 dark:hover:bg-input/50"
+ list-class="w-[var(--reka-popover-trigger-width)]"
+ />
@@ -1025,25 +1014,16 @@ async function saveConfigTask() {
-
-
-
-
-
- {{ label }}
-
-
-
+ trigger-class="h-8 w-full max-w-none justify-between gap-1.5 border border-input rounded-md bg-transparent px-2.5 text-xs shadow-none hover:bg-muted/40 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30 dark:hover:bg-input/50"
+ list-class="w-[var(--reka-popover-trigger-width)]"
+ />
diff --git a/apps/desktop/src/lib/__tests__/connection/connectionPickerTree.spec.ts b/apps/desktop/src/lib/__tests__/connection/connectionPickerTree.spec.ts
index c8500f2562..99cdcc53f4 100644
--- a/apps/desktop/src/lib/__tests__/connection/connectionPickerTree.spec.ts
+++ b/apps/desktop/src/lib/__tests__/connection/connectionPickerTree.spec.ts
@@ -81,6 +81,17 @@ describe("buildConnectionPickerRows", () => {
expect(buildConnectionPickerRows(layout(), connections, new Set(), "不存在")).toEqual([]);
});
+ it("prunes groups that hold none of the given connections", () => {
+ const onlyC3 = connections.filter((connection) => connection.id === "c3");
+ expect(kinds(buildConnectionPickerRows(layout(), onlyC3, new Set(), ""))).toEqual(["connection:c3@0"]);
+ });
+
+ it("prunes nested groups without usable descendants even when collapsed", () => {
+ const onlyC2 = connections.filter((connection) => connection.id === "c2");
+ const rows = buildConnectionPickerRows(layout(), onlyC2, new Set(["g2"]), "");
+ expect(kinds(rows)).toEqual(["group:g1@0", "connection:c2@1"]);
+ });
+
it("falls back to the connection id when the name is empty", () => {
const rows = buildConnectionPickerRows({ groups: [], order: [] }, [{ id: "c9", name: "" }], new Set(), "");
expect(rows[0]?.label).toBe("c9");
diff --git a/apps/desktop/src/lib/common/__tests__/searchableSelectLayout.spec.ts b/apps/desktop/src/lib/common/__tests__/searchableSelectLayout.spec.ts
index c3fd0816fe..0b8786e13f 100644
--- a/apps/desktop/src/lib/common/__tests__/searchableSelectLayout.spec.ts
+++ b/apps/desktop/src/lib/common/__tests__/searchableSelectLayout.spec.ts
@@ -18,12 +18,12 @@ describe("SearchableSelect layout", () => {
expect(searchableSelectSource).toContain("props.trimCustom ? searchText.value.trim() : searchText.value");
});
- it("truncates long data transfer connection labels without shrinking their icons", () => {
- const truncatedConnectionLabels = dataTransferDialogSource.match(/\{\{ label \}\}<\/span>/g) ?? [];
- const fixedConnectionIcons = dataTransferDialogSource.match(/class="h-3\.5 w-3\.5 shrink-0"/g) ?? [];
+ it("renders data transfer connection pickers as sidebar-like trees", () => {
+ const treeSelects = dataTransferDialogSource.match(/ {
diff --git a/apps/desktop/src/lib/connection/connectionPickerTree.ts b/apps/desktop/src/lib/connection/connectionPickerTree.ts
index 1b73eadfad..3b04c4e0c7 100644
--- a/apps/desktop/src/lib/connection/connectionPickerTree.ts
+++ b/apps/desktop/src/lib/connection/connectionPickerTree.ts
@@ -86,6 +86,17 @@ export function buildConnectionPickerRows(layout: SidebarLayout, connections: re
}
const rows: ConnectionPickerRow[] = [];
+ const subtreeHasConnection = (entries: SidebarOrderEntry[]): boolean => {
+ for (const entry of entries) {
+ if (entry.type === "connection") {
+ if (connectionById.has(entry.id)) return true;
+ continue;
+ }
+ if (!groupById.has(entry.id)) continue;
+ if (subtreeHasConnection(groupEntryChildren(entry))) return true;
+ }
+ return false;
+ };
const visitTree = (entries: SidebarOrderEntry[], depth: number) => {
for (const entry of entries) {
if (entry.type === "connection") {
@@ -96,6 +107,9 @@ export function buildConnectionPickerRows(layout: SidebarLayout, connections: re
}
const group = groupById.get(entry.id);
if (!group) continue;
+ // Skip groups that hold none of the picker's connections (e.g. when the
+ // caller passes a filtered list such as transfer-capable connections).
+ if (!subtreeHasConnection(groupEntryChildren(entry))) continue;
const collapsed = collapsedGroupIds.has(group.id);
rows.push({ key: `group:${group.id}`, kind: "group", id: group.id, label: group.name, depth, collapsed });
if (!collapsed) visitTree(groupEntryChildren(entry), depth + 1);