diff --git a/apps/desktop/src/components/transfer/ObjectSelectionTree.vue b/apps/desktop/src/components/transfer/ObjectSelectionTree.vue index 5f79bbddff..00d341680f 100644 --- a/apps/desktop/src/components/transfer/ObjectSelectionTree.vue +++ b/apps/desktop/src/components/transfer/ObjectSelectionTree.vue @@ -114,10 +114,19 @@ const filteredGroups = computed(() => { if (!query) { return props.groups; } - return props.groups.map((g) => ({ - ...g, - items: g.items.filter((name) => name.toLowerCase().includes(query)), - })); + return props.groups.map((group) => { + const prefixMatches: string[] = []; + const substringMatches: string[] = []; + for (const name of group.items) { + const normalizedName = name.toLowerCase(); + if (normalizedName.startsWith(query)) { + prefixMatches.push(name); + } else if (normalizedName.includes(query)) { + substringMatches.push(name); + } + } + return { ...group, items: [...prefixMatches, ...substringMatches] }; + }); }); function selectAllEnabled() { diff --git a/apps/desktop/src/components/transfer/__tests__/ObjectSelectionTree.spec.ts b/apps/desktop/src/components/transfer/__tests__/ObjectSelectionTree.spec.ts index 1ce83562ae..01bfc7ef13 100644 --- a/apps/desktop/src/components/transfer/__tests__/ObjectSelectionTree.spec.ts +++ b/apps/desktop/src/components/transfer/__tests__/ObjectSelectionTree.spec.ts @@ -145,6 +145,18 @@ describe("ObjectSelectionTree interaction", () => { expect(container.querySelector('[data-test="group-FUNCTION"]')?.textContent).toContain("无匹配"); }); + it("search ranks exact-prefix matches before mid-name matches", async () => { + // "abc_users" contains "user" but doesn't start with it; "users_archive" + // and "user_log" are both true prefix matches and should be sorted + // ahead of it, keeping their own relative order (stable sort). + const SEARCHABLE: TreeGroup = { kind: "TABLE", label: "Tables", items: ["abc_users", "users_archive", "xuser_log", "user_log"] }; + const { container, app } = mountTree({ groups: [SEARCHABLE] }); + cleanup = () => app.unmount(); + await typeSearch(container, "user"); + const order = Array.from(container.querySelectorAll('label[data-test^="item-TABLE-"]')).map((el) => el.getAttribute("data-test")!.replace("item-TABLE-", "")); + expect(order).toEqual(["users_archive", "user_log", "abc_users", "xuser_log"]); + }); + it("deselect all only clears visible enabled selections, keeping hidden ones", async () => { const { state, container, app } = mountTree({ selection: { VIEW: ["v1", "v2", "v3"], FUNCTION: ["f1", "f2"] },