From 5836ddba5d68f3f5210e6ff16c6136d1cc04ce6a Mon Sep 17 00:00:00 2001 From: mbeaulne Date: Tue, 23 Jun 2026 12:14:01 -0400 Subject: [PATCH] Decouple editor component search input --- .../ComponentSearchV2Content.test.tsx | 79 +++++++++++++++++++ .../components/ComponentSearchV2Content.tsx | 17 ++-- 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 src/routes/v2/pages/Editor/components/ComponentSearchV2Content.test.tsx diff --git a/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.test.tsx b/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.test.tsx new file mode 100644 index 000000000..7e7065fc6 --- /dev/null +++ b/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.test.tsx @@ -0,0 +1,79 @@ +import { act, fireEvent, render, screen } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { ComponentSearchV2Content } from "./ComponentSearchV2Content"; + +const mocks = vi.hoisted(() => ({ + useComponentSearchV2State: vi.fn(), + useFlagValue: vi.fn(), +})); + +vi.mock( + "@/components/shared/ReactFlow/FlowSidebar/components/ImportComponent", + () => ({ + default: ({ triggerComponent }: { triggerComponent: ReactNode }) => ( + <>{triggerComponent} + ), + }), +); + +vi.mock("@/components/shared/Settings/useFlags", () => ({ + useFlagValue: mocks.useFlagValue, +})); + +vi.mock("@/routes/v2/pages/Editor/hooks/useComponentSearchV2State", () => ({ + useComponentSearchV2State: mocks.useComponentSearchV2State, +})); + +vi.mock("./ComponentSearchResults", () => ({ + ComponentSearchResults: ({ query }: { query: string }) => ( +
{query}
+ ), +})); + +describe("ComponentSearchV2Content", () => { + beforeEach(() => { + vi.useFakeTimers(); + mocks.useFlagValue.mockReturnValue(false); + mocks.useComponentSearchV2State.mockImplementation(() => ({ + results: [], + browseFolders: [], + isLoading: false, + canRerank: false, + canDeepRerank: false, + isReranking: false, + isRerankActive: false, + rerank: vi.fn(), + deepRerank: vi.fn(), + clearRerank: vi.fn(), + })); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("keeps typing local while delaying result query updates", async () => { + render(); + + const input = screen.getByLabelText("Search components"); + + fireEvent.change(input, { target: { value: "csv" } }); + + expect(input).toHaveValue("csv"); + expect(screen.getByTestId("results-query")).toHaveTextContent(""); + + await act(async () => { + vi.advanceTimersByTime(499); + }); + + expect(screen.getByTestId("results-query")).toHaveTextContent(""); + + await act(async () => { + vi.advanceTimersByTime(1); + }); + + expect(screen.getByTestId("results-query")).toHaveTextContent("csv"); + }); +}); diff --git a/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.tsx b/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.tsx index b9c4c3f58..97c9c3bc5 100644 --- a/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.tsx +++ b/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useDeferredValue, useState, useTransition } from "react"; import ImportComponent from "@/components/shared/ReactFlow/FlowSidebar/components/ImportComponent"; import { Button } from "@/components/ui/button"; @@ -12,12 +12,17 @@ import { useComponentSearchV2State } from "@/routes/v2/pages/Editor/hooks/useCom import { ComponentSearchResults } from "./ComponentSearchResults"; +const EDITOR_SEARCH_RESULT_DEBOUNCE_MS = 500; + function DebouncedComponentSearchInput({ onCommit, }: { onCommit: (value: string) => void; }) { - const [localValue, setLocalValue] = useDebouncedSearchValue(onCommit); + const [localValue, setLocalValue] = useDebouncedSearchValue( + onCommit, + EDITOR_SEARCH_RESULT_DEBOUNCE_MS, + ); return ( { - setQuery(value); + startSearchTransition(() => setQuery(value)); }; return ( @@ -96,7 +103,7 @@ export function ComponentSearchV2Content() {