diff --git a/src/routes/Dashboard/DashboardComponentsV2View.test.tsx b/src/routes/Dashboard/DashboardComponentsV2View.test.tsx index 779f9d865..4e67b3e75 100644 --- a/src/routes/Dashboard/DashboardComponentsV2View.test.tsx +++ b/src/routes/Dashboard/DashboardComponentsV2View.test.tsx @@ -216,6 +216,7 @@ import { type SourceFilterOption, } from "./DashboardComponentsV2SourceFilter"; import { + buildComponentCollectionMatches, createRegisteredLibrariesFingerprint, DashboardComponentsV2View, } from "./DashboardComponentsV2View"; @@ -406,6 +407,53 @@ describe("SourceFilterBar", () => { }); }); +describe("buildComponentCollectionMatches", () => { + it("returns registered library collections matching the query", () => { + const result = buildComponentCollectionMatches( + [ + createIndexEntry("standard-component", { + kind: "standard", + label: "Standard", + id: "standard", + }), + createIndexEntry("load-csv", { + kind: "registered", + label: "Data tools", + id: "data-tools", + }), + createIndexEntry("clean-data", { + kind: "registered", + label: "Data tools", + id: "data-tools", + }), + ], + "data", + ); + + expect(result).toEqual([ + { + id: "data-tools", + label: "Data tools", + count: 2, + previewNames: ["load-csv", "clean-data"], + }, + ]); + }); + + it("returns no collections for empty or unmatched queries", () => { + const index = [ + createIndexEntry("load-csv", { + kind: "registered", + label: "Data tools", + id: "data-tools", + }), + ]; + + expect(buildComponentCollectionMatches(index, "")).toEqual([]); + expect(buildComponentCollectionMatches(index, "training")).toEqual([]); + }); +}); + describe("DashboardComponentsV2View", () => { beforeEach(() => { routeMocks.aiDescriptionsEnabled = false; @@ -452,6 +500,35 @@ describe("DashboardComponentsV2View", () => { }); }); + it("shows registered library collection results when the query matches", async () => { + render(); + + fireEvent.change(screen.getByLabelText("Search components"), { + target: { value: "github" }, + }); + + await waitFor(() => { + expect(screen.getByText("GitHub library")).toBeInTheDocument(); + }); + }); + + it("hides collection results from disabled sources", async () => { + render(); + + fireEvent.click( + screen.getByRole("button", { + name: "Registered libraries source (1 component)", + }), + ); + fireEvent.change(screen.getByLabelText("Search components"), { + target: { value: "github" }, + }); + + await waitFor(() => { + expect(screen.queryByText("GitHub library")).not.toBeInTheDocument(); + }); + }); + it("initializes search state from URL params", () => { routeMocks.search = { q: "registered", diff --git a/src/routes/Dashboard/DashboardComponentsV2View.tsx b/src/routes/Dashboard/DashboardComponentsV2View.tsx index c82f877e7..08c4f8af1 100644 --- a/src/routes/Dashboard/DashboardComponentsV2View.tsx +++ b/src/routes/Dashboard/DashboardComponentsV2View.tsx @@ -193,6 +193,45 @@ type ComponentLibraryFolder = Parameters[0]; type UserFolder = { components?: ComponentReference[] }; type RerankMode = "smart" | "deep"; +interface ComponentCollectionMatch { + id: string; + label: string; + count: number; + previewNames: string[]; +} + +export function buildComponentCollectionMatches( + index: IndexEntry[], + query: string, +): ComponentCollectionMatch[] { + const trimmedQuery = query.trim().toLowerCase(); + if (!trimmedQuery) return []; + + const bySourceId = new Map(); + for (const entry of index) { + if (entry.source.kind !== "registered") continue; + const current = bySourceId.get(entry.source.id); + if (current) { + current.count += 1; + if (current.previewNames.length < 3) + current.previewNames.push(entry.name); + } else { + bySourceId.set(entry.source.id, { + id: entry.source.id, + label: entry.source.label, + count: 1, + previewNames: [entry.name], + }); + } + } + + return Array.from(bySourceId.values()) + .filter((collection) => + collection.label.toLowerCase().includes(trimmedQuery), + ) + .sort((a, b) => a.label.localeCompare(b.label)); +} + interface ComponentCardProps { reference: ComponentReference; source?: ComponentSearchSource; @@ -316,6 +355,29 @@ const ComponentCard = ({ ); }; +interface CollectionCardProps { + collection: ComponentCollectionMatch; +} + +const CollectionCard = ({ collection }: CollectionCardProps) => ( + + + + + {collection.label} + + + {collection.count} component{collection.count === 1 ? "" : "s"} + + + {collection.previewNames.length > 0 && ( + + Includes {collection.previewNames.join(", ")} + + )} + +); + interface ComponentDescriptionPanelProps { prefilledDescription?: string; generatedDescription?: string; @@ -820,6 +882,11 @@ export const DashboardComponentsV2View = () => { 0, LEXICAL_RESULT_LIMIT, ); + const collectionMatches = buildComponentCollectionMatches( + filteredIndex, + deferredQuery, + ); + const aiCandidateMatches: LexicalMatch[] = (() => { if (trimmedQuery.length === 0) return []; return broadLexicalMatches; @@ -1147,7 +1214,11 @@ export const DashboardComponentsV2View = () => { ); } - if (lexicalMatches.length === 0 && !rerankActive) { + if ( + lexicalMatches.length === 0 && + collectionMatches.length === 0 && + !rerankActive + ) { return ( No components matched “{trimmedQuery}”. Try different terms or check @@ -1157,11 +1228,21 @@ export const DashboardComponentsV2View = () => { } return ( + {collectionMatches.length > 0 && ( + + + Collection{collectionMatches.length === 1 ? "" : "s"} + + {collectionMatches.map((collection) => ( + + ))} + + )} {rerankActive ? `AI-ranked ${displayedResults.length} result${displayedResults.length === 1 ? "" : "s"} for “${trimmedQuery}”` - : `${displayedResults.length} result${displayedResults.length === 1 ? "" : "s"} for “${trimmedQuery}”`} + : `${displayedResults.length} component result${displayedResults.length === 1 ? "" : "s"} for “${trimmedQuery}”`} {rerankActive && (