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;
|
projectId?: string;
|
||||||
viewStateKey: string;
|
viewStateKey: string;
|
||||||
initialAssignees?: string[];
|
initialAssignees?: string[];
|
||||||
|
initialSearch?: string;
|
||||||
|
onSearchChange?: (search: string) => void;
|
||||||
onUpdateIssue: (id: string, data: Record<string, unknown>) => void;
|
onUpdateIssue: (id: string, data: Record<string, unknown>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +156,8 @@ export function IssuesList({
|
|||||||
projectId,
|
projectId,
|
||||||
viewStateKey,
|
viewStateKey,
|
||||||
initialAssignees,
|
initialAssignees,
|
||||||
|
initialSearch,
|
||||||
|
onSearchChange,
|
||||||
onUpdateIssue,
|
onUpdateIssue,
|
||||||
}: IssuesListProps) {
|
}: IssuesListProps) {
|
||||||
const { selectedCompanyId } = useCompany();
|
const { selectedCompanyId } = useCompany();
|
||||||
@@ -170,7 +174,7 @@ export function IssuesList({
|
|||||||
});
|
});
|
||||||
const [assigneePickerIssueId, setAssigneePickerIssueId] = useState<string | null>(null);
|
const [assigneePickerIssueId, setAssigneePickerIssueId] = useState<string | null>(null);
|
||||||
const [assigneeSearch, setAssigneeSearch] = useState("");
|
const [assigneeSearch, setAssigneeSearch] = useState("");
|
||||||
const [issueSearch, setIssueSearch] = useState("");
|
const [issueSearch, setIssueSearch] = useState(initialSearch ?? "");
|
||||||
const deferredIssueSearch = useDeferredValue(issueSearch);
|
const deferredIssueSearch = useDeferredValue(issueSearch);
|
||||||
const normalizedIssueSearch = deferredIssueSearch.trim();
|
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" />
|
<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
|
<Input
|
||||||
value={issueSearch}
|
value={issueSearch}
|
||||||
onChange={(e) => setIssueSearch(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setIssueSearch(e.target.value);
|
||||||
|
onSearchChange?.(e.target.value);
|
||||||
|
}}
|
||||||
placeholder="Search issues..."
|
placeholder="Search issues..."
|
||||||
className="pl-7 text-xs sm:text-sm"
|
className="pl-7 text-xs sm:text-sm"
|
||||||
aria-label="Search issues"
|
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 { useSearchParams } from "@/lib/router";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { issuesApi } from "../api/issues";
|
import { issuesApi } from "../api/issues";
|
||||||
@@ -14,9 +14,30 @@ import { CircleDot } from "lucide-react";
|
|||||||
export function Issues() {
|
export function Issues() {
|
||||||
const { selectedCompanyId } = useCompany();
|
const { selectedCompanyId } = useCompany();
|
||||||
const { setBreadcrumbs } = useBreadcrumbs();
|
const { setBreadcrumbs } = useBreadcrumbs();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const queryClient = useQueryClient();
|
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({
|
const { data: agents } = useQuery({
|
||||||
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
||||||
queryFn: () => agentsApi.list(selectedCompanyId!),
|
queryFn: () => agentsApi.list(selectedCompanyId!),
|
||||||
@@ -69,6 +90,8 @@ export function Issues() {
|
|||||||
liveIssueIds={liveIssueIds}
|
liveIssueIds={liveIssueIds}
|
||||||
viewStateKey="paperclip:issues-view"
|
viewStateKey="paperclip:issues-view"
|
||||||
initialAssignees={searchParams.get("assignee") ? [searchParams.get("assignee")!] : undefined}
|
initialAssignees={searchParams.get("assignee") ? [searchParams.get("assignee")!] : undefined}
|
||||||
|
initialSearch={initialSearch}
|
||||||
|
onSearchChange={handleSearchChange}
|
||||||
onUpdateIssue={(id, data) => updateIssue.mutate({ id, data })}
|
onUpdateIssue={(id, data) => updateIssue.mutate({ id, data })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user