Merge pull request #552 from mvanhorn/osc/129-feat-filter-issues-by-project

feat(ui): add project filter to issues list
This commit is contained in:
Dotta 2026-03-21 11:15:09 -05:00 committed by GitHub
commit e3e7a92c77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 37 additions and 1 deletions

View file

@ -40,6 +40,7 @@ export type IssueViewState = {
priorities: string[]; priorities: string[];
assignees: string[]; assignees: string[];
labels: string[]; labels: string[];
projects: string[];
sortField: "status" | "priority" | "title" | "created" | "updated"; sortField: "status" | "priority" | "title" | "created" | "updated";
sortDir: "asc" | "desc"; sortDir: "asc" | "desc";
groupBy: "status" | "priority" | "assignee" | "none"; groupBy: "status" | "priority" | "assignee" | "none";
@ -52,6 +53,7 @@ const defaultViewState: IssueViewState = {
priorities: [], priorities: [],
assignees: [], assignees: [],
labels: [], labels: [],
projects: [],
sortField: "updated", sortField: "updated",
sortDir: "desc", sortDir: "desc",
groupBy: "none", 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.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; return result;
} }
@ -135,6 +138,7 @@ function countActiveFilters(state: IssueViewState): number {
if (state.priorities.length > 0) count++; if (state.priorities.length > 0) count++;
if (state.assignees.length > 0) count++; if (state.assignees.length > 0) count++;
if (state.labels.length > 0) count++; if (state.labels.length > 0) count++;
if (state.projects.length > 0) count++;
return count; return count;
} }
@ -145,11 +149,17 @@ interface Agent {
name: string; name: string;
} }
interface ProjectOption {
id: string;
name: string;
}
interface IssuesListProps { interface IssuesListProps {
issues: Issue[]; issues: Issue[];
isLoading?: boolean; isLoading?: boolean;
error?: Error | null; error?: Error | null;
agents?: Agent[]; agents?: Agent[];
projects?: ProjectOption[];
liveIssueIds?: Set<string>; liveIssueIds?: Set<string>;
projectId?: string; projectId?: string;
viewStateKey: string; viewStateKey: string;
@ -165,6 +175,7 @@ export function IssuesList({
isLoading, isLoading,
error, error,
agents, agents,
projects,
liveIssueIds, liveIssueIds,
projectId, projectId,
viewStateKey, viewStateKey,
@ -362,7 +373,7 @@ export function IssuesList({
className="h-3 w-3 ml-1 hidden sm:block" className="h-3 w-3 ml-1 hidden sm:block"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
updateView({ statuses: [], priorities: [], assignees: [], labels: [] }); updateView({ statuses: [], priorities: [], assignees: [], labels: [], projects: [] });
}} }}
/> />
)} )}
@ -495,6 +506,23 @@ export function IssuesList({
</div> </div>
</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> </div>
</div> </div>

View file

@ -3,6 +3,7 @@ import { useLocation, useSearchParams } from "@/lib/router";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { issuesApi } from "../api/issues"; import { issuesApi } from "../api/issues";
import { agentsApi } from "../api/agents"; import { agentsApi } from "../api/agents";
import { projectsApi } from "../api/projects";
import { heartbeatsApi } from "../api/heartbeats"; import { heartbeatsApi } from "../api/heartbeats";
import { useCompany } from "../context/CompanyContext"; import { useCompany } from "../context/CompanyContext";
import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext";
@ -50,6 +51,12 @@ export function Issues() {
enabled: !!selectedCompanyId, enabled: !!selectedCompanyId,
}); });
const { data: projects } = useQuery({
queryKey: queryKeys.projects.list(selectedCompanyId!),
queryFn: () => projectsApi.list(selectedCompanyId!),
enabled: !!selectedCompanyId,
});
const { data: liveRuns } = useQuery({ const { data: liveRuns } = useQuery({
queryKey: queryKeys.liveRuns(selectedCompanyId!), queryKey: queryKeys.liveRuns(selectedCompanyId!),
queryFn: () => heartbeatsApi.liveRunsForCompany(selectedCompanyId!), queryFn: () => heartbeatsApi.liveRunsForCompany(selectedCompanyId!),
@ -102,6 +109,7 @@ export function Issues() {
isLoading={isLoading} isLoading={isLoading}
error={error as Error | null} error={error as Error | null}
agents={agents} agents={agents}
projects={projects}
liveIssueIds={liveIssueIds} liveIssueIds={liveIssueIds}
viewStateKey="paperclip:issues-view" viewStateKey="paperclip:issues-view"
issueLinkState={issueLinkState} issueLinkState={issueLinkState}