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:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user