feat(ui): sync issues search with URL query parameter
Debounces search input (300ms) and syncs it to a ?q= URL parameter so searches persist across navigation and can be shared via URL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,8 @@ interface IssuesListProps {
|
||||
projectId?: string;
|
||||
viewStateKey: string;
|
||||
initialAssignees?: string[];
|
||||
initialSearch?: string;
|
||||
onSearchChange?: (search: string) => void;
|
||||
onUpdateIssue: (id: string, data: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
@@ -154,6 +156,8 @@ export function IssuesList({
|
||||
projectId,
|
||||
viewStateKey,
|
||||
initialAssignees,
|
||||
initialSearch,
|
||||
onSearchChange,
|
||||
onUpdateIssue,
|
||||
}: IssuesListProps) {
|
||||
const { selectedCompanyId } = useCompany();
|
||||
@@ -170,7 +174,7 @@ export function IssuesList({
|
||||
});
|
||||
const [assigneePickerIssueId, setAssigneePickerIssueId] = useState<string | null>(null);
|
||||
const [assigneeSearch, setAssigneeSearch] = useState("");
|
||||
const [issueSearch, setIssueSearch] = useState("");
|
||||
const [issueSearch, setIssueSearch] = useState(initialSearch ?? "");
|
||||
const deferredIssueSearch = useDeferredValue(issueSearch);
|
||||
const normalizedIssueSearch = deferredIssueSearch.trim();
|
||||
|
||||
@@ -291,7 +295,10 @@ export function IssuesList({
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={issueSearch}
|
||||
onChange={(e) => setIssueSearch(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setIssueSearch(e.target.value);
|
||||
onSearchChange?.(e.target.value);
|
||||
}}
|
||||
placeholder="Search issues..."
|
||||
className="pl-7 text-xs sm:text-sm"
|
||||
aria-label="Search issues"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useEffect, useMemo, useCallback, useRef } from "react";
|
||||
import { useSearchParams } from "@/lib/router";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { issuesApi } from "../api/issues";
|
||||
@@ -14,9 +14,30 @@ import { CircleDot } from "lucide-react";
|
||||
export function Issues() {
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const initialSearch = searchParams.get("q") ?? "";
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
const handleSearchChange = useCallback((search: string) => {
|
||||
clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setSearchParams((prev) => {
|
||||
const next = new URLSearchParams(prev);
|
||||
if (search.trim()) {
|
||||
next.set("q", search.trim());
|
||||
} else {
|
||||
next.delete("q");
|
||||
}
|
||||
return next;
|
||||
}, { replace: true });
|
||||
}, 300);
|
||||
}, [setSearchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => clearTimeout(debounceRef.current);
|
||||
}, []);
|
||||
|
||||
const { data: agents } = useQuery({
|
||||
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
||||
queryFn: () => agentsApi.list(selectedCompanyId!),
|
||||
@@ -69,6 +90,8 @@ export function Issues() {
|
||||
liveIssueIds={liveIssueIds}
|
||||
viewStateKey="paperclip:issues-view"
|
||||
initialAssignees={searchParams.get("assignee") ? [searchParams.get("assignee")!] : undefined}
|
||||
initialSearch={initialSearch}
|
||||
onSearchChange={handleSearchChange}
|
||||
onUpdateIssue={(id, data) => updateIssue.mutate({ id, data })}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user