Compare commits

...

2 Commits

Author SHA1 Message Date
Dotta
e3e7a92c77 Merge pull request #552 from mvanhorn/osc/129-feat-filter-issues-by-project
feat(ui): add project filter to issues list
2026-03-21 11:15:09 -05:00
Matt Van Horn
bc5b30eccf 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>
2026-03-10 16:57:01 -07:00
2 changed files with 37 additions and 1 deletions

View File

@@ -40,6 +40,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";
@@ -52,6 +53,7 @@ const defaultViewState: IssueViewState = {
priorities: [],
assignees: [],
labels: [],
projects: [],
sortField: "updated",
sortDir: "desc",
groupBy: "none",
@@ -104,6 +106,7 @@ function applyFilters(issues: Issue[], state: IssueViewState, currentUserId?: st
});
}
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;
}
@@ -135,6 +138,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;
}
@@ -145,11 +149,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;
@@ -165,6 +175,7 @@ export function IssuesList({
isLoading,
error,
agents,
projects,
liveIssueIds,
projectId,
viewStateKey,
@@ -362,7 +373,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: [] });
}}
/>
)}
@@ -495,6 +506,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 { useLocation, 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";
@@ -50,6 +51,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!),
@@ -102,6 +109,7 @@ export function Issues() {
isLoading={isLoading}
error={error as Error | null}
agents={agents}
projects={projects}
liveIssueIds={liveIssueIds}
viewStateKey="paperclip:issues-view"
issueLinkState={issueLinkState}