"use client"; import { useMemo, useState } from "react"; import { ArrowDown, ArrowUp, Check, ChevronDown, CircleDot, Columns3, Filter, List, SignalHigh, SlidersHorizontal, User, UserMinus, UserPen, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, } from "@/components/ui/dropdown-menu"; import { Popover, PopoverTrigger, PopoverContent, } from "@/components/ui/popover"; import { Switch } from "@/components/ui/switch"; import { ALL_STATUSES, STATUS_CONFIG, PRIORITY_ORDER, PRIORITY_CONFIG, } from "@/features/issues/config"; import { StatusIcon, PriorityIcon } from "@/features/issues/components"; import { useWorkspaceStore } from "@/features/workspace"; import { ActorAvatar } from "@/components/common/actor-avatar"; import { useIssueViewStore, SORT_OPTIONS, CARD_PROPERTY_OPTIONS, type ActorFilterValue, } from "@/features/issues/stores/view-store"; import { useIssuesScopeStore, type IssuesScope, } from "@/features/issues/stores/issues-scope-store"; import { filterIssues } from "@/features/issues/utils/filter"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import type { Issue } from "@/shared/types"; // --------------------------------------------------------------------------- // HoverCheck — shadcn official pattern (PR #6862) // --------------------------------------------------------------------------- const FILTER_ITEM_CLASS = "group/fitem pr-1.5! [&>[data-slot=dropdown-menu-checkbox-item-indicator]]:hidden"; function HoverCheck({ checked }: { checked: boolean }) { return (
); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function getActiveFilterCount(state: { statusFilters: string[]; priorityFilters: string[]; assigneeFilters: ActorFilterValue[]; includeNoAssignee: boolean; creatorFilters: ActorFilterValue[]; }) { let count = 0; if (state.statusFilters.length > 0) count++; if (state.priorityFilters.length > 0) count++; if (state.assigneeFilters.length > 0 || state.includeNoAssignee) count++; if (state.creatorFilters.length > 0) count++; return count; } function useIssueCounts(allIssues: Issue[]) { return useMemo(() => { const status = new Map(); const priority = new Map(); const assignee = new Map(); const creator = new Map(); let noAssignee = 0; for (const issue of allIssues) { status.set(issue.status, (status.get(issue.status) ?? 0) + 1); priority.set(issue.priority, (priority.get(issue.priority) ?? 0) + 1); if (!issue.assignee_id) { noAssignee++; } else { const aKey = `${issue.assignee_type}:${issue.assignee_id}`; assignee.set(aKey, (assignee.get(aKey) ?? 0) + 1); } const cKey = `${issue.creator_type}:${issue.creator_id}`; creator.set(cKey, (creator.get(cKey) ?? 0) + 1); } return { status, priority, assignee, creator, noAssignee }; }, [allIssues]); } // --------------------------------------------------------------------------- // Scope config // --------------------------------------------------------------------------- const SCOPES: { value: IssuesScope; label: string; description: string }[] = [ { value: "all", label: "All", description: "All issues in this workspace" }, { value: "members", label: "Members", description: "Issues assigned to team members" }, { value: "agents", label: "Agents", description: "Issues assigned to AI agents" }, ]; // --------------------------------------------------------------------------- // Actor sub-menu content (shared between Assignee and Creator) // --------------------------------------------------------------------------- function ActorSubContent({ counts, selected, onToggle, showNoAssignee, includeNoAssignee, onToggleNoAssignee, noAssigneeCount, }: { counts: Map; selected: ActorFilterValue[]; onToggle: (value: ActorFilterValue) => void; showNoAssignee?: boolean; includeNoAssignee?: boolean; onToggleNoAssignee?: () => void; noAssigneeCount?: number; }) { const [search, setSearch] = useState(""); const members = useWorkspaceStore((s) => s.members); const agents = useWorkspaceStore((s) => s.agents); const query = search.toLowerCase(); const filteredMembers = members.filter((m) => m.name.toLowerCase().includes(query), ); const filteredAgents = agents.filter((a) => a.name.toLowerCase().includes(query), ); const isSelected = (type: "member" | "agent", id: string) => selected.some((f) => f.type === type && f.id === id); return ( <>
setSearch(e.target.value)} placeholder="Filter..." className="w-full bg-transparent text-sm placeholder:text-muted-foreground outline-none" autoFocus />
{showNoAssignee && (!query || "no assignee".includes(query) || "unassigned".includes(query)) && ( onToggleNoAssignee?.()} className={FILTER_ITEM_CLASS} > No assignee {(noAssigneeCount ?? 0) > 0 && ( {noAssigneeCount} )} )} {filteredMembers.length > 0 && ( Members {filteredMembers.map((m) => { const checked = isSelected("member", m.user_id); const count = counts.get(`member:${m.user_id}`) ?? 0; return ( onToggle({ type: "member", id: m.user_id }) } className={FILTER_ITEM_CLASS} > {m.name} {count > 0 && ( {count} )} ); })} )} {filteredAgents.length > 0 && ( Agents {filteredAgents.map((a) => { const checked = isSelected("agent", a.id); const count = counts.get(`agent:${a.id}`) ?? 0; return ( onToggle({ type: "agent", id: a.id }) } className={FILTER_ITEM_CLASS} > {a.name} {count > 0 && ( {count} )} ); })} )} {filteredMembers.length === 0 && filteredAgents.length === 0 && search && (
No results
)}
); } // --------------------------------------------------------------------------- // IssuesHeader // --------------------------------------------------------------------------- export function IssuesHeader({ scopedIssues }: { scopedIssues: Issue[] }) { const scope = useIssuesScopeStore((s) => s.scope); const setScope = useIssuesScopeStore((s) => s.setScope); const viewMode = useIssueViewStore((s) => s.viewMode); const statusFilters = useIssueViewStore((s) => s.statusFilters); const priorityFilters = useIssueViewStore((s) => s.priorityFilters); const assigneeFilters = useIssueViewStore((s) => s.assigneeFilters); const includeNoAssignee = useIssueViewStore((s) => s.includeNoAssignee); const creatorFilters = useIssueViewStore((s) => s.creatorFilters); const sortBy = useIssueViewStore((s) => s.sortBy); const sortDirection = useIssueViewStore((s) => s.sortDirection); const cardProperties = useIssueViewStore((s) => s.cardProperties); const act = useIssueViewStore.getState(); const counts = useIssueCounts(scopedIssues); const hasActiveFilters = getActiveFilterCount({ statusFilters, priorityFilters, assigneeFilters, includeNoAssignee, creatorFilters, }) > 0; const sortLabel = SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? "Manual"; return (
{/* Left: scope buttons */}
{SCOPES.map((s) => ( setScope(s.value)} > {s.label} } /> {s.description} ))}
{/* Right: filter + display + view toggle */}
{/* Filter */} {hasActiveFilters && ( )} } /> } /> Filter {/* Status */} Status {statusFilters.length > 0 && ( {statusFilters.length} )} {ALL_STATUSES.map((s) => { const checked = statusFilters.includes(s); const count = counts.status.get(s) ?? 0; return ( act.toggleStatusFilter(s)} className={FILTER_ITEM_CLASS} > {STATUS_CONFIG[s].label} {count > 0 && ( {count} {count === 1 ? "issue" : "issues"} )} ); })} {/* Priority */} Priority {priorityFilters.length > 0 && ( {priorityFilters.length} )} {PRIORITY_ORDER.map((p) => { const checked = priorityFilters.includes(p); const count = counts.priority.get(p) ?? 0; return ( act.togglePriorityFilter(p)} className={FILTER_ITEM_CLASS} > {PRIORITY_CONFIG[p].label} {count > 0 && ( {count} {count === 1 ? "issue" : "issues"} )} ); })} {/* Assignee */} Assignee {(assigneeFilters.length > 0 || includeNoAssignee) && ( {assigneeFilters.length + (includeNoAssignee ? 1 : 0)} )} {/* Creator */} Creator {creatorFilters.length > 0 && ( {creatorFilters.length} )} {/* Reset */} {hasActiveFilters && ( <> Reset all filters )} {/* Display settings */} } /> } /> Display settings
Ordering
{sortLabel} } /> {SORT_OPTIONS.map((opt) => ( act.setSortBy(opt.value)} > {opt.label} ))}
Card properties
{CARD_PROPERTY_OPTIONS.map((opt) => ( ))}
{/* View toggle */} {viewMode === "board" ? ( ) : ( )} } /> } /> {viewMode === "board" ? "Board view" : "List view"} View act.setViewMode("board")}> Board act.setViewMode("list")}> List
); }