From dd8c1ca3b2e615298ccc8afa018119990f7a641a Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 26 Mar 2026 11:01:09 -0500 Subject: [PATCH] Speed up issues page search responsiveness Co-Authored-By: Paperclip --- ui/src/components/IssuesList.tsx | 71 +++++++++++++++++++++----------- ui/src/pages/Issues.tsx | 32 ++++++-------- 2 files changed, 60 insertions(+), 43 deletions(-) diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 1eb95e22..1876492c 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState, useCallback, useRef } from "react"; +import { startTransition, useEffect, useMemo, useState, useCallback, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { pickTextColorForPillBg } from "@/lib/color-contrast"; import { useDialog } from "../context/DialogContext"; @@ -68,6 +68,7 @@ const quickFilterPresets = [ { label: "Backlog", statuses: ["backlog"] }, { label: "Done", statuses: ["done", "cancelled"] }, ]; +const ISSUE_SEARCH_COMMIT_DELAY_MS = 150; function getViewState(key: string): IssueViewState { try { @@ -174,6 +175,39 @@ interface IssuesListProps { onUpdateIssue: (id: string, data: Record) => void; } +interface IssuesSearchInputProps { + initialValue: string; + onValueCommitted: (value: string) => void; +} + +function IssuesSearchInput({ initialValue, onValueCommitted }: IssuesSearchInputProps) { + const [value, setValue] = useState(initialValue); + + useEffect(() => { + setValue(initialValue); + }, [initialValue]); + + useEffect(() => { + const timeoutId = window.setTimeout(() => { + onValueCommitted(value); + }, ISSUE_SEARCH_COMMIT_DELAY_MS); + return () => window.clearTimeout(timeoutId); + }, [value, onValueCommitted]); + + return ( +
+ + setValue(e.target.value)} + placeholder="Search issues..." + className="pl-7 text-xs sm:text-sm" + aria-label="Search issues" + /> +
+ ); +} + export function IssuesList({ issues, isLoading, @@ -210,20 +244,12 @@ export function IssuesList({ const [assigneePickerIssueId, setAssigneePickerIssueId] = useState(null); const [assigneeSearch, setAssigneeSearch] = useState(""); const [issueSearch, setIssueSearch] = useState(initialSearch ?? ""); - const [debouncedIssueSearch, setDebouncedIssueSearch] = useState(issueSearch); - const normalizedIssueSearch = debouncedIssueSearch.trim(); + const normalizedIssueSearch = issueSearch.trim(); useEffect(() => { setIssueSearch(initialSearch ?? ""); }, [initialSearch]); - useEffect(() => { - const timeoutId = window.setTimeout(() => { - setDebouncedIssueSearch(issueSearch); - }, 300); - return () => window.clearTimeout(timeoutId); - }, [issueSearch]); - // Reload view state from localStorage when company changes (scopedKey changes). const prevScopedKey = useRef(scopedKey); useEffect(() => { @@ -235,6 +261,13 @@ export function IssuesList({ } }, [scopedKey, initialAssignees]); + const handleIssueSearchCommit = useCallback((nextSearch: string) => { + startTransition(() => { + setIssueSearch(nextSearch); + }); + onSearchChange?.(nextSearch); + }, [onSearchChange]); + const updateView = useCallback((patch: Partial) => { setViewState((prev) => { const next = { ...prev, ...patch }; @@ -250,6 +283,7 @@ export function IssuesList({ ], queryFn: () => issuesApi.list(selectedCompanyId!, { q: normalizedIssueSearch, projectId, ...searchFilters }), enabled: !!selectedCompanyId && normalizedIssueSearch.length > 0, + placeholderData: (previousData) => previousData, }); const agentName = useCallback((id: string | null) => { @@ -333,19 +367,10 @@ export function IssuesList({ New Issue -
- - { - setIssueSearch(e.target.value); - onSearchChange?.(e.target.value); - }} - placeholder="Search issues..." - className="pl-7 text-xs sm:text-sm" - aria-label="Search issues" - /> -
+
diff --git a/ui/src/pages/Issues.tsx b/ui/src/pages/Issues.tsx index ee3d64b0..2b6e48b0 100644 --- a/ui/src/pages/Issues.tsx +++ b/ui/src/pages/Issues.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useCallback, useRef } from "react"; +import { useEffect, useMemo, useCallback } from "react"; import { useLocation, useSearchParams } from "@/lib/router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { issuesApi } from "../api/issues"; @@ -22,28 +22,20 @@ export function Issues() { const initialSearch = searchParams.get("q") ?? ""; const participantAgentId = searchParams.get("participantAgentId") ?? undefined; - const debounceRef = useRef>(undefined); const handleSearchChange = useCallback((search: string) => { - clearTimeout(debounceRef.current); - debounceRef.current = setTimeout(() => { - const trimmedSearch = search.trim(); - const currentSearch = new URLSearchParams(window.location.search).get("q") ?? ""; - if (currentSearch === trimmedSearch) return; + const trimmedSearch = search.trim(); + const currentSearch = new URLSearchParams(window.location.search).get("q") ?? ""; + if (currentSearch === trimmedSearch) return; - const url = new URL(window.location.href); - if (trimmedSearch) { - url.searchParams.set("q", trimmedSearch); - } else { - url.searchParams.delete("q"); - } + const url = new URL(window.location.href); + if (trimmedSearch) { + url.searchParams.set("q", trimmedSearch); + } else { + url.searchParams.delete("q"); + } - const nextUrl = `${url.pathname}${url.search}${url.hash}`; - window.history.replaceState(window.history.state, "", nextUrl); - }, 300); - }, []); - - useEffect(() => { - return () => clearTimeout(debounceRef.current); + const nextUrl = `${url.pathname}${url.search}${url.hash}`; + window.history.replaceState(window.history.state, "", nextUrl); }, []); const { data: agents } = useQuery({