multica/apps/web/features/issues/components/issues-header.tsx
Naiyuan Qing 2cf088ddf6 feat: resizable sidebar, issue detail rewrite, package consolidation
- Add drag-to-resize sidebar with localStorage persistence
- Rewrite issue detail page with Tiptap rich text editor, due date picker, acceptance criteria
- Redesign create-issue modal with pill-based property toolbar and expand/collapse
- Consolidate @multica/sdk and @multica/types into apps/web/shared/
- Simplify auth: remove verification codes, PATs, email service (dev-only login)
- Add 401 unauthorized handler to redirect expired sessions to login
- Fix due date format to send full RFC3339 timestamps
- Increase description editor debounce to 1500ms
- Remove arbitrary Tailwind values in create-issue modal
- Renumber migrations (inbox_actor 012→009), remove unused migrations
- UI polish across agents, settings, inbox, knowledge-base pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:47:04 +08:00

161 lines
5.4 KiB
TypeScript

"use client";
import { ChevronDown, Columns3, List, Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
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 { useIssueViewStore } from "@/features/issues/stores/view-store";
function formatFilterLabel(
prefix: string,
selected: string[],
configMap: Record<string, { label: string }>
) {
if (selected.length === 0) return `${prefix}: All`;
if (selected.length === 1) {
const key = selected[0];
if (key) return `${prefix}: ${configMap[key]?.label ?? key}`;
}
return `${prefix}: ${selected.length} selected`;
}
export function IssuesHeader() {
const viewMode = useIssueViewStore((s) => s.viewMode);
const statusFilters = useIssueViewStore((s) => s.statusFilters);
const priorityFilters = useIssueViewStore((s) => s.priorityFilters);
const setViewMode = useIssueViewStore((s) => s.setViewMode);
const toggleStatusFilter = useIssueViewStore((s) => s.toggleStatusFilter);
const togglePriorityFilter = useIssueViewStore((s) => s.togglePriorityFilter);
return (
<div className="flex h-12 shrink-0 items-center justify-between px-4">
<div className="flex items-center gap-2">
{/* Status filter */}
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="outline" size="sm" className="whitespace-nowrap text-xs">
{formatFilterLabel("Status", statusFilters, STATUS_CONFIG)}
<ChevronDown className="text-muted-foreground" />
</Button>
}
/>
<DropdownMenuContent align="start" className="w-auto">
<DropdownMenuGroup>
<DropdownMenuLabel>Status</DropdownMenuLabel>
<DropdownMenuItem
onClick={() =>
useIssueViewStore.setState({ statusFilters: [] })
}
>
All
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
{ALL_STATUSES.map((s) => (
<DropdownMenuCheckboxItem
key={s}
checked={statusFilters.length === 0 || statusFilters.includes(s)}
onCheckedChange={() => toggleStatusFilter(s)}
>
<StatusIcon status={s} className="h-3.5 w-3.5" />
{STATUS_CONFIG[s].label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
{/* Priority filter */}
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="outline" size="sm" className="whitespace-nowrap text-xs">
{formatFilterLabel("Priority", priorityFilters, PRIORITY_CONFIG)}
<ChevronDown className="text-muted-foreground" />
</Button>
}
/>
<DropdownMenuContent align="start" className="w-auto">
<DropdownMenuGroup>
<DropdownMenuLabel>Priority</DropdownMenuLabel>
<DropdownMenuItem
onClick={() =>
useIssueViewStore.setState({ priorityFilters: [] })
}
>
All
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
{PRIORITY_ORDER.map((p) => (
<DropdownMenuCheckboxItem
key={p}
checked={priorityFilters.length === 0 || priorityFilters.includes(p)}
onCheckedChange={() => togglePriorityFilter(p)}
>
<PriorityIcon priority={p} />
{PRIORITY_CONFIG[p].label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center gap-2">
{/* View toggle */}
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="outline" size="sm">
{viewMode === "board" ? <Columns3 /> : <List />}
</Button>
}
/>
<DropdownMenuContent align="end" className="w-auto">
<DropdownMenuGroup>
<DropdownMenuLabel>View</DropdownMenuLabel>
<DropdownMenuItem onClick={() => setViewMode("board")}>
<Columns3 />
Board
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setViewMode("list")}>
<List />
List
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
{/* New issue */}
<Button
variant="outline"
size="sm"
onClick={() => useModalStore.getState().open("create-issue")}
>
<Plus />
New Issue
</Button>
</div>
</div>
);
}