feat(issues): rewrite issues page with component decomposition and persisted filters

- Decompose monolithic 472-line page.tsx into focused components:
  board-card, board-column, board-view, list-row, list-view,
  issues-header, issues-page
- Add view-store with Zustand persist for viewMode and multi-select
  status/priority filters
- Fix kanban DnD with pointerWithin + closestCenter collision detection
- Add workspace breadcrumb header and Linear-style filter dropdowns
  using DropdownMenu with CheckboxItem for multi-select
- Status filter hides kanban columns, priority filter hides cards
- Drop target highlight with bg-accent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-03-26 12:36:01 +08:00
parent 655aa40732
commit 586a4916d1
11 changed files with 650 additions and 467 deletions

View file

@ -0,0 +1,79 @@
"use client";
import Link from "next/link";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import type { Issue } from "@multica/types";
import { ActorAvatar } from "@/components/common/actor-avatar";
import { PriorityIcon } from "./priority-icon";
function formatDate(date: string): string {
return new Date(date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}
export function BoardCardContent({ issue }: { issue: Issue }) {
return (
<div className="rounded-lg border bg-background p-3">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<PriorityIcon priority={issue.priority} />
<span>{issue.id.slice(0, 8)}</span>
</div>
<p className="mt-1.5 text-sm leading-snug">{issue.title}</p>
<div className="mt-2.5 flex items-center justify-between">
<div className="flex items-center gap-2">
{issue.assignee_type && issue.assignee_id && (
<ActorAvatar
actorType={issue.assignee_type}
actorId={issue.assignee_id}
size={20}
/>
)}
</div>
{issue.due_date && (
<span className="text-xs text-muted-foreground">
{formatDate(issue.due_date)}
</span>
)}
</div>
</div>
);
}
export function DraggableBoardCard({ issue }: { issue: Issue }) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: issue.id,
data: { status: issue.status },
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={isDragging ? "opacity-30" : ""}
>
<Link
href={`/issues/${issue.id}`}
className={`block transition-colors hover:opacity-80 ${isDragging ? "pointer-events-none" : ""}`}
>
<BoardCardContent issue={issue} />
</Link>
</div>
);
}