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 <noreply@anthropic.com>
This commit is contained in:
Matt Van Horn
2026-03-10 16:57:01 -07:00
parent 3a003e11cc
commit bc5b30eccf
2 changed files with 37 additions and 1 deletions

View File

@@ -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<string>;
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({
</div>
</div>
)}
{projects && projects.length > 0 && (
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Project</span>
<div className="space-y-0.5 max-h-32 overflow-y-auto">
{projects.map((project) => (
<label key={project.id} className="flex items-center gap-2 px-2 py-1 rounded-sm hover:bg-accent/50 cursor-pointer">
<Checkbox
checked={viewState.projects.includes(project.id)}
onCheckedChange={() => updateView({ projects: toggleInArray(viewState.projects, project.id) })}
/>
<span className="text-sm">{project.name}</span>
</label>
))}
</div>
</div>
)}
</div>
</div>
</div>

View File

@@ -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}