Add a "Project" filter section to the issues filter popover, following the same pattern as the existing Assignee and Labels filters. Issues can now be filtered by one or more projects from the filter dropdown. Closes #129 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
111 lines
3.7 KiB
TypeScript
111 lines
3.7 KiB
TypeScript
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";
|
|
import { agentsApi } from "../api/agents";
|
|
import { projectsApi } from "../api/projects";
|
|
import { heartbeatsApi } from "../api/heartbeats";
|
|
import { useCompany } from "../context/CompanyContext";
|
|
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
import { EmptyState } from "../components/EmptyState";
|
|
import { IssuesList } from "../components/IssuesList";
|
|
import { CircleDot } from "lucide-react";
|
|
|
|
export function Issues() {
|
|
const { selectedCompanyId } = useCompany();
|
|
const { setBreadcrumbs } = useBreadcrumbs();
|
|
const [searchParams] = 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(() => {
|
|
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 nextUrl = `${url.pathname}${url.search}${url.hash}`;
|
|
window.history.replaceState(window.history.state, "", nextUrl);
|
|
}, 300);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
return () => clearTimeout(debounceRef.current);
|
|
}, []);
|
|
|
|
const { data: agents } = useQuery({
|
|
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
|
queryFn: () => agentsApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId,
|
|
});
|
|
|
|
const { data: projects } = useQuery({
|
|
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
|
queryFn: () => projectsApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId,
|
|
});
|
|
|
|
const { data: liveRuns } = useQuery({
|
|
queryKey: queryKeys.liveRuns(selectedCompanyId!),
|
|
queryFn: () => heartbeatsApi.liveRunsForCompany(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId,
|
|
refetchInterval: 5000,
|
|
});
|
|
|
|
const liveIssueIds = useMemo(() => {
|
|
const ids = new Set<string>();
|
|
for (const run of liveRuns ?? []) {
|
|
if (run.issueId) ids.add(run.issueId);
|
|
}
|
|
return ids;
|
|
}, [liveRuns]);
|
|
|
|
useEffect(() => {
|
|
setBreadcrumbs([{ label: "Issues" }]);
|
|
}, [setBreadcrumbs]);
|
|
|
|
const { data: issues, isLoading, error } = useQuery({
|
|
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
|
queryFn: () => issuesApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId,
|
|
});
|
|
|
|
const updateIssue = useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: Record<string, unknown> }) =>
|
|
issuesApi.update(id, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId!) });
|
|
},
|
|
});
|
|
|
|
if (!selectedCompanyId) {
|
|
return <EmptyState icon={CircleDot} message="Select a company to view issues." />;
|
|
}
|
|
|
|
return (
|
|
<IssuesList
|
|
issues={issues ?? []}
|
|
isLoading={isLoading}
|
|
error={error as Error | null}
|
|
agents={agents}
|
|
projects={projects}
|
|
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 })}
|
|
/>
|
|
);
|
|
}
|