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:
parent
3a003e11cc
commit
bc5b30eccf
2 changed files with 37 additions and 1 deletions
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue