"use client";
import { useMemo, useState } from "react";
import {
ArrowDown,
ArrowUp,
Bot,
Check,
ChevronDown,
CircleDot,
Columns3,
Filter,
List,
Plus,
SignalHigh,
SlidersHorizontal,
User,
UserMinus,
UserPen,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { useIssueStore } from "@/features/issues/store";
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 { useModalStore } from "@/features/modals";
import {
ALL_STATUSES,
STATUS_CONFIG,
PRIORITY_ORDER,
PRIORITY_CONFIG,
} from "@/features/issues/config";
import { StatusIcon, PriorityIcon } from "@/features/issues/components";
import { useWorkspaceStore, useActorName } from "@/features/workspace";
import {
useIssueViewStore,
SORT_OPTIONS,
CARD_PROPERTY_OPTIONS,
type ActorFilterValue,
} from "@/features/issues/stores/view-store";
import { filterIssues } from "@/features/issues/utils/filter";
import type { Issue } from "@/shared/types";
// ---------------------------------------------------------------------------
// HoverCheck — shadcn official pattern (PR #6862)
// Uses data-selected attr instead of Checkbox component to avoid
// DropdownMenuCheckboxItem's focus:**:text-accent-foreground cascade.
// ---------------------------------------------------------------------------
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]);
}
// ---------------------------------------------------------------------------
// 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 { getActorInitials } = useActorName();
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}
>
{getActorInitials("member", m.user_id)}
{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() {
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 allIssues = useIssueStore((s) => s.issues);
const counts = useIssueCounts(allIssues);
const filteredCount = useMemo(
() => filterIssues(allIssues, { statusFilters, priorityFilters, assigneeFilters, includeNoAssignee, creatorFilters }).length,
[allIssues, statusFilters, priorityFilters, assigneeFilters, includeNoAssignee, creatorFilters],
);
const filterCount = getActiveFilterCount({
statusFilters,
priorityFilters,
assigneeFilters,
includeNoAssignee,
creatorFilters,
});
const sortLabel =
SORT_OPTIONS.find((o) => o.value === sortBy)?.label ?? "Manual";
const hasActiveFilters = filterCount > 0;
return (
{/* View toggle */}
{viewMode === "board" ? :
}
{viewMode === "board" ? "Board" : "List"}
}
/>
View
act.setViewMode("board")}>
Board
act.setViewMode("list")}>
List
{/* Filter — DropdownMenu with sub-menus */}
Filter
{hasActiveFilters && (
{filterCount}
)}
}
/>
{/* 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
}
/>
Ordering
{sortLabel}
}
/>
{SORT_OPTIONS.map((opt) => (
act.setSortBy(opt.value)}
>
{opt.label}
))}
Card properties
{CARD_PROPERTY_OPTIONS.map((opt) => (
))}
{filteredCount} {filteredCount === 1 ? "Issue" : "Issues"}
);
}