From e239004d885d71441720a768d85fa13e5c3f6271 Mon Sep 17 00:00:00 2001 From: mbeaulne Date: Mon, 29 Jun 2026 11:29:14 -0400 Subject: [PATCH] Add editor component search loading skeleton --- .../ComponentSearchResults.test.tsx | 15 ++++++++++ .../components/ComponentSearchResults.tsx | 28 ++++++++++++++++--- .../ComponentSearchV2Content.test.tsx | 15 ++++++++-- .../components/ComponentSearchV2Content.tsx | 12 +++++++- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/routes/v2/pages/Editor/components/ComponentSearchResults.test.tsx b/src/routes/v2/pages/Editor/components/ComponentSearchResults.test.tsx index eca59c545..aa6e3ac3f 100644 --- a/src/routes/v2/pages/Editor/components/ComponentSearchResults.test.tsx +++ b/src/routes/v2/pages/Editor/components/ComponentSearchResults.test.tsx @@ -42,12 +42,27 @@ const baseProps = { { label: "dataset", kind: "type" }, ] satisfies ComponentSearchSuggestion[], isLoading: false, + isSearching: false, isRerankActive: false, onClearRerank: vi.fn(), onSuggestedSearch: vi.fn(), }; describe("ComponentSearchResults", () => { + it("shows a skeleton while search is pending", () => { + render( + , + ); + + expect(screen.getByTestId("search-results-skeleton")).toBeInTheDocument(); + expect(screen.getByText("Searching")).toBeInTheDocument(); + }); + it("shows actionable no-results guidance with clickable suggestions", () => { const onSuggestedSearch = vi.fn(); render( diff --git a/src/routes/v2/pages/Editor/components/ComponentSearchResults.tsx b/src/routes/v2/pages/Editor/components/ComponentSearchResults.tsx index 2aa54d8df..1a9eaccab 100644 --- a/src/routes/v2/pages/Editor/components/ComponentSearchResults.tsx +++ b/src/routes/v2/pages/Editor/components/ComponentSearchResults.tsx @@ -8,6 +8,7 @@ import FolderItem from "@/components/shared/ReactFlow/FlowSidebar/components/Fol import { Button } from "@/components/ui/button"; import { BlockStack, InlineStack } from "@/components/ui/layout"; import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { Paragraph, Text } from "@/components/ui/typography"; import type { ComponentSearchSuggestion } from "@/services/componentSearchSuggestions"; @@ -21,6 +22,7 @@ interface ComponentSearchResultsProps { browseFolders: UIComponentFolder[]; searchSuggestions: ComponentSearchSuggestion[]; isLoading: boolean; + isSearching: boolean; isRerankActive: boolean; onClearRerank: () => void; onSuggestedSearch: (query: string) => void; @@ -32,17 +34,35 @@ export function ComponentSearchResults({ browseFolders, searchSuggestions, isLoading, + isSearching, isRerankActive, onClearRerank, onSuggestedSearch, }: ComponentSearchResultsProps) { - if (isLoading) { + if (isLoading || isSearching) { return ( - - - Search Results + + + + {isSearching ? "Searching" : "Search Results"} + + + {Array.from({ length: 5 }, (_, index) => ( + + + + + + + + ))} + ); } diff --git a/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.test.tsx b/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.test.tsx index 4bcbd4c91..d6996243a 100644 --- a/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.test.tsx +++ b/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.test.tsx @@ -27,8 +27,17 @@ vi.mock("@/routes/v2/pages/Editor/hooks/useComponentSearchV2State", () => ({ })); vi.mock("./ComponentSearchResults", () => ({ - ComponentSearchResults: ({ query }: { query: string }) => ( -
{query}
+ ComponentSearchResults: ({ + query, + isSearching, + }: { + query: string; + isSearching: boolean; + }) => ( +
+
{query}
+
{String(isSearching)}
+
), })); @@ -62,6 +71,7 @@ describe("ComponentSearchV2Content", () => { expect(input).toHaveValue("csv"); expect(screen.getByTestId("results-query")).toHaveTextContent(""); + expect(screen.getByTestId("results-searching")).toHaveTextContent("true"); await act(async () => { vi.advanceTimersByTime(499); @@ -74,6 +84,7 @@ describe("ComponentSearchV2Content", () => { }); expect(screen.getByTestId("results-query")).toHaveTextContent("csv"); + expect(screen.getByTestId("results-searching")).toHaveTextContent("false"); }); it("tracks editor component search completions without query text", async () => { diff --git a/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.tsx b/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.tsx index 5d246ed75..f9f30a3aa 100644 --- a/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.tsx +++ b/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.tsx @@ -19,9 +19,11 @@ const EDITOR_SEARCH_RESULT_DEBOUNCE_MS = 500; function DebouncedComponentSearchInput({ initialValue, onCommit, + onLocalChange, }: { initialValue: string; onCommit: (value: string) => void; + onLocalChange: (value: string) => void; }) { const [localValue, setLocalValue] = useDebouncedSearchValue( onCommit, @@ -36,7 +38,10 @@ function DebouncedComponentSearchInput({ placeholder="Search components..." className="w-full pl-8 text-sm h-8 focus-visible:ring-gray-400/50" value={localValue} - onChange={(event) => setLocalValue(event.target.value)} + onChange={(event) => { + setLocalValue(event.target.value); + onLocalChange(event.target.value); + }} aria-label="Search components" autoComplete="off" /> @@ -46,6 +51,7 @@ function DebouncedComponentSearchInput({ export function ComponentSearchV2Content() { const { track } = useAnalytics(); const [query, setQuery] = useState(""); + const [localQuery, setLocalQuery] = useState(""); const deferredQuery = useDeferredValue(query); const [, startSearchTransition] = useTransition(); const { @@ -65,10 +71,12 @@ export function ComponentSearchV2Content() { }; const handleSuggestedSearch = (value: string) => { + setLocalQuery(value); startSearchTransition(() => setQuery(value)); }; const trimmedDeferredQuery = deferredQuery.trim(); + const isSearching = localQuery.trim() !== trimmedDeferredQuery; useEffect(() => { if (isLoading || trimmedDeferredQuery.length === 0) return; @@ -121,6 +129,7 @@ export function ComponentSearchV2Content() {