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