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() {
- - - + 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() {
- - - + 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);