From bc5b30eccfd5d6366787d69ace2304ea2813e416 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:57:01 -0700 Subject: [PATCH] feat(ui): add project filter to issues list 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 --- ui/src/components/IssuesList.tsx | 30 +++++++++++++++++++++++++++++- ui/src/pages/Issues.tsx | 8 ++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 10d0709b..a7e4023d 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -38,6 +38,7 @@ export type IssueViewState = { priorities: string[]; assignees: string[]; labels: string[]; + projects: string[]; sortField: "status" | "priority" | "title" | "created" | "updated"; sortDir: "asc" | "desc"; groupBy: "status" | "priority" | "assignee" | "none"; @@ -50,6 +51,7 @@ const defaultViewState: IssueViewState = { priorities: [], assignees: [], labels: [], + projects: [], sortField: "updated", sortDir: "desc", groupBy: "none", @@ -93,6 +95,7 @@ function applyFilters(issues: Issue[], state: IssueViewState): Issue[] { if (state.priorities.length > 0) result = result.filter((i) => state.priorities.includes(i.priority)); if (state.assignees.length > 0) result = result.filter((i) => i.assigneeAgentId != null && state.assignees.includes(i.assigneeAgentId)); if (state.labels.length > 0) result = result.filter((i) => (i.labelIds ?? []).some((id) => state.labels.includes(id))); + if (state.projects.length > 0) result = result.filter((i) => i.projectId != null && state.projects.includes(i.projectId)); return result; } @@ -124,6 +127,7 @@ function countActiveFilters(state: IssueViewState): number { if (state.priorities.length > 0) count++; if (state.assignees.length > 0) count++; if (state.labels.length > 0) count++; + if (state.projects.length > 0) count++; return count; } @@ -134,11 +138,17 @@ interface Agent { name: string; } +interface ProjectOption { + id: string; + name: string; +} + interface IssuesListProps { issues: Issue[]; isLoading?: boolean; error?: Error | null; agents?: Agent[]; + projects?: ProjectOption[]; liveIssueIds?: Set; projectId?: string; viewStateKey: string; @@ -153,6 +163,7 @@ export function IssuesList({ isLoading, error, agents, + projects, liveIssueIds, projectId, viewStateKey, @@ -333,7 +344,7 @@ export function IssuesList({ className="h-3 w-3 ml-1 hidden sm:block" onClick={(e) => { e.stopPropagation(); - updateView({ statuses: [], priorities: [], assignees: [], labels: [] }); + updateView({ statuses: [], priorities: [], assignees: [], labels: [], projects: [] }); }} /> )} @@ -451,6 +462,23 @@ export function IssuesList({ )} + + {projects && projects.length > 0 && ( +
+ Project +
+ {projects.map((project) => ( + + ))} +
+
+ )} diff --git a/ui/src/pages/Issues.tsx b/ui/src/pages/Issues.tsx index fce74c7a..6dd23bf0 100644 --- a/ui/src/pages/Issues.tsx +++ b/ui/src/pages/Issues.tsx @@ -3,6 +3,7 @@ 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"; @@ -48,6 +49,12 @@ export function Issues() { 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!), @@ -91,6 +98,7 @@ export function 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}