diff --git a/apps/web/app/(dashboard)/issues/page.tsx b/apps/web/app/(dashboard)/issues/page.tsx
index 3a6a9016..546ca001 100644
--- a/apps/web/app/(dashboard)/issues/page.tsx
+++ b/apps/web/app/(dashboard)/issues/page.tsx
@@ -1,471 +1,7 @@
"use client";
-import { useState, useCallback, useMemo } from "react";
-import { useIssueStore } from "@/features/issues";
-import { useModalStore } from "@/features/modals";
-import { toast } from "sonner";
-import Link from "next/link";
-import {
- Columns3,
- List,
- Plus,
-} from "lucide-react";
-import { Skeleton } from "@/components/ui/skeleton";
-import {
- DndContext,
- DragOverlay,
- PointerSensor,
- useSensor,
- useSensors,
- useDroppable,
- closestCorners,
- type DragStartEvent,
- type DragEndEvent,
-} from "@dnd-kit/core";
-import { useSortable } from "@dnd-kit/sortable";
-import { CSS } from "@dnd-kit/utilities";
-import type { Issue, IssueStatus, IssuePriority } from "@multica/types";
-import { STATUS_CONFIG, PRIORITY_CONFIG, ALL_STATUSES, PRIORITY_ORDER, STATUS_ORDER } from "@/features/issues/config";
-import { Button } from "@/components/ui/button";
-import {
- Select,
- SelectTrigger,
- SelectValue,
- SelectContent,
- SelectItem,
- SelectGroup,
-} from "@/components/ui/select";
-import { ActorAvatar } from "@/components/common/actor-avatar";
-import { StatusIcon, PriorityIcon } from "@/features/issues/components";
-import { api } from "@/shared/api";
-import { useActorName } from "@/features/workspace";
+import { IssuesPage } from "@/features/issues/components/issues-page";
-function formatDate(date: string): string {
- return new Date(date).toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- });
-}
-
-const BOARD_STATUSES: IssueStatus[] = [
- "backlog",
- "todo",
- "in_progress",
- "in_review",
- "done",
- "blocked",
-];
-
-// ---------------------------------------------------------------------------
-// Board View — Card
-// ---------------------------------------------------------------------------
-
-function BoardCardContent({ issue }: { issue: Issue }) {
- const { getActorName, getActorInitials } = useActorName();
- return (
-
-
-
-
{issue.id.slice(0, 8)}
-
-
{issue.title}
-
-
- {issue.assignee_type && issue.assignee_id && (
-
- )}
-
- {issue.due_date && (
-
- {formatDate(issue.due_date)}
-
- )}
-
-
- );
-}
-
-// ---------------------------------------------------------------------------
-// Draggable card wrapper
-// ---------------------------------------------------------------------------
-
-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 (
-
-
-
-
-
- );
-}
-
-// ---------------------------------------------------------------------------
-// Droppable column
-// ---------------------------------------------------------------------------
-
-function DroppableColumn({
- status,
- issues,
-}: {
- status: IssueStatus;
- issues: Issue[];
-}) {
- const cfg = STATUS_CONFIG[status];
- const { setNodeRef, isOver } = useDroppable({ id: status });
-
- return (
-
-
-
- {cfg.label}
- {issues.length}
-
-
- {issues.map((issue) => (
-
- ))}
- {issues.length === 0 && (
-
No issues
- )}
-
-
- );
-}
-
-// ---------------------------------------------------------------------------
-// Board View (with DnD)
-// ---------------------------------------------------------------------------
-
-function BoardView({
- issues,
- onMoveIssue,
-}: {
- issues: Issue[];
- onMoveIssue: (issueId: string, newStatus: IssueStatus) => void;
-}) {
- const [activeIssue, setActiveIssue] = useState(null);
-
- const sensors = useSensors(
- useSensor(PointerSensor, {
- activationConstraint: { distance: 5 },
- })
- );
-
- const visibleStatuses = BOARD_STATUSES;
-
- const handleDragStart = useCallback(
- (event: DragStartEvent) => {
- const issue = issues.find((i) => i.id === event.active.id);
- if (issue) setActiveIssue(issue);
- },
- [issues]
- );
-
- const handleDragEnd = useCallback(
- (event: DragEndEvent) => {
- setActiveIssue(null);
- const { active, over } = event;
- if (!over) return;
-
- const issueId = active.id as string;
- let targetStatus: IssueStatus | undefined;
-
- if (visibleStatuses.includes(over.id as IssueStatus)) {
- targetStatus = over.id as IssueStatus;
- } else {
- const targetIssue = issues.find((i) => i.id === over.id);
- if (targetIssue) targetStatus = targetIssue.status;
- }
-
- if (targetStatus) {
- const currentIssue = issues.find((i) => i.id === issueId);
- if (currentIssue && currentIssue.status !== targetStatus) {
- onMoveIssue(issueId, targetStatus);
- }
- }
- },
- [issues, onMoveIssue, visibleStatuses]
- );
-
- return (
-
-
- {visibleStatuses.map((status) => (
- i.status === status)}
- />
- ))}
-
-
-
- {activeIssue ? (
-
-
-
- ) : null}
-
-
- );
-}
-
-// ---------------------------------------------------------------------------
-// List View
-// ---------------------------------------------------------------------------
-
-function ListRow({ issue }: { issue: Issue }) {
- const { getActorName, getActorInitials } = useActorName();
- return (
-
-
-
- {issue.id.slice(0, 8)}
-
-
- {issue.title}
- {issue.due_date && (
-
- {formatDate(issue.due_date)}
-
- )}
- {issue.assignee_type && issue.assignee_id && (
-
- )}
-
- );
-}
-
-function ListView({ issues }: { issues: Issue[] }) {
- const groupOrder = STATUS_ORDER.filter((s) => s !== "cancelled");
-
- return (
-
- {groupOrder.map((status) => {
- const cfg = STATUS_CONFIG[status];
- const filtered = issues.filter((i) => i.status === status);
- if (filtered.length === 0) return null;
- return (
-
-
-
- {cfg.label}
- {filtered.length}
-
- {filtered.map((issue) => (
-
- ))}
-
- );
- })}
-
- );
-}
-
-// ---------------------------------------------------------------------------
-// Create Issue Dialog
-// ---------------------------------------------------------------------------
-
-// ---------------------------------------------------------------------------
-// Page
-// ---------------------------------------------------------------------------
-
-type ViewMode = "board" | "list";
-
-export default function IssuesPage() {
- const [view, setView] = useState("board");
- const [filterStatus, setFilterStatus] = useState("");
- const [filterPriority, setFilterPriority] = useState("");
-
- // Read from global store (populated by workspace hydrate + useRealtimeSync)
- const allIssues = useIssueStore((s) => s.issues);
- const loading = useIssueStore((s) => s.loading);
-
- // Apply local filters
- const issues = useMemo(() => {
- return allIssues.filter((issue) => {
- if (filterStatus && issue.status !== filterStatus) return false;
- if (filterPriority && issue.priority !== filterPriority) return false;
- return true;
- });
- }, [allIssues, filterStatus, filterPriority]);
-
- const handleMoveIssue = useCallback(
- (issueId: string, newStatus: IssueStatus) => {
- // Optimistic update in store
- useIssueStore.getState().updateIssue(issueId, { status: newStatus });
-
- // Persist to API
- api.updateIssue(issueId, { status: newStatus }).catch((err) => {
- toast.error("Failed to move issue");
- // Revert on error by refetching
- api.listIssues({ limit: 200 }).then((res) => {
- useIssueStore.getState().setIssues(res.issues);
- });
- });
- },
- []
- );
-
- if (loading) {
- return (
-
-
-
-
-
-
- {Array.from({ length: 5 }).map((_, i) => (
-
-
-
-
-
- ))}
-
-
- );
- }
-
- return (
-
- {/* Toolbar */}
-
-
-
All Issues
-
-
-
-
-
-
-
-
-
-
-
-
-
- {issues.length === 0 && !loading ? (
-
-
No matching issues
- {(filterStatus || filterPriority) && (
-
- )}
-
- ) : view === "board" ? (
-
- ) : (
-
- )}
-
-
- );
+export default function Page() {
+ return ;
}
diff --git a/apps/web/features/issues/components/board-card.tsx b/apps/web/features/issues/components/board-card.tsx
new file mode 100644
index 00000000..bde5eb7c
--- /dev/null
+++ b/apps/web/features/issues/components/board-card.tsx
@@ -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 (
+
+
+
+
{issue.id.slice(0, 8)}
+
+
{issue.title}
+
+
+ {issue.assignee_type && issue.assignee_id && (
+
+ )}
+
+ {issue.due_date && (
+
+ {formatDate(issue.due_date)}
+
+ )}
+
+
+ );
+}
+
+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 (
+
+
+
+
+
+ );
+}
diff --git a/apps/web/features/issues/components/board-column.tsx b/apps/web/features/issues/components/board-column.tsx
new file mode 100644
index 00000000..29bd1a87
--- /dev/null
+++ b/apps/web/features/issues/components/board-column.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import { useDroppable } from "@dnd-kit/core";
+import type { Issue, IssueStatus } from "@multica/types";
+import { STATUS_CONFIG } from "@/features/issues/config";
+import { StatusIcon } from "./status-icon";
+import { DraggableBoardCard } from "./board-card";
+
+export function BoardColumn({
+ status,
+ issues,
+}: {
+ status: IssueStatus;
+ issues: Issue[];
+}) {
+ const cfg = STATUS_CONFIG[status];
+ const { setNodeRef, isOver } = useDroppable({ id: status });
+
+ return (
+
+
+
+ {cfg.label}
+ {issues.length}
+
+
+ {issues.map((issue) => (
+
+ ))}
+ {issues.length === 0 && (
+
+ No issues
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/features/issues/components/board-view.tsx b/apps/web/features/issues/components/board-view.tsx
new file mode 100644
index 00000000..1410ed38
--- /dev/null
+++ b/apps/web/features/issues/components/board-view.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import { useState, useCallback } from "react";
+import {
+ DndContext,
+ DragOverlay,
+ PointerSensor,
+ useSensor,
+ useSensors,
+ pointerWithin,
+ closestCenter,
+ type CollisionDetection,
+ type DragStartEvent,
+ type DragEndEvent,
+} from "@dnd-kit/core";
+import type { Issue, IssueStatus } from "@multica/types";
+import { BoardColumn } from "./board-column";
+import { BoardCardContent } from "./board-card";
+
+const kanbanCollision: CollisionDetection = (args) => {
+ const pointer = pointerWithin(args);
+ if (pointer.length > 0) return pointer;
+ return closestCenter(args);
+};
+
+export function BoardView({
+ issues,
+ visibleStatuses,
+ onMoveIssue,
+}: {
+ issues: Issue[];
+ visibleStatuses: IssueStatus[];
+ onMoveIssue: (issueId: string, newStatus: IssueStatus) => void;
+}) {
+ const [activeIssue, setActiveIssue] = useState(null);
+
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: { distance: 5 },
+ })
+ );
+
+ const handleDragStart = useCallback(
+ (event: DragStartEvent) => {
+ const issue = issues.find((i) => i.id === event.active.id);
+ if (issue) setActiveIssue(issue);
+ },
+ [issues]
+ );
+
+ const handleDragEnd = useCallback(
+ (event: DragEndEvent) => {
+ setActiveIssue(null);
+ const { active, over } = event;
+ if (!over) return;
+
+ const issueId = active.id as string;
+ let targetStatus: IssueStatus | undefined;
+
+ if (visibleStatuses.includes(over.id as IssueStatus)) {
+ targetStatus = over.id as IssueStatus;
+ } else {
+ const targetIssue = issues.find((i) => i.id === over.id);
+ if (targetIssue) targetStatus = targetIssue.status;
+ }
+
+ if (targetStatus) {
+ const currentIssue = issues.find((i) => i.id === issueId);
+ if (currentIssue && currentIssue.status !== targetStatus) {
+ onMoveIssue(issueId, targetStatus);
+ }
+ }
+ },
+ [issues, onMoveIssue, visibleStatuses]
+ );
+
+ return (
+
+
+ {visibleStatuses.map((status) => (
+ i.status === status)}
+ />
+ ))}
+
+
+
+ {activeIssue ? (
+
+
+
+ ) : null}
+
+
+ );
+}
diff --git a/apps/web/features/issues/components/index.ts b/apps/web/features/issues/components/index.ts
index ef5b8364..934aab28 100644
--- a/apps/web/features/issues/components/index.ts
+++ b/apps/web/features/issues/components/index.ts
@@ -2,3 +2,4 @@ export { StatusIcon } from "./status-icon";
export { PriorityIcon } from "./priority-icon";
export { StatusPicker, PriorityPicker, AssigneePicker } from "./pickers";
export { IssueDetail } from "./issue-detail";
+export { IssuesPage } from "./issues-page";
diff --git a/apps/web/features/issues/components/issues-header.tsx b/apps/web/features/issues/components/issues-header.tsx
new file mode 100644
index 00000000..3e84f1a8
--- /dev/null
+++ b/apps/web/features/issues/components/issues-header.tsx
@@ -0,0 +1,161 @@
+"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
+) {
+ 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 (
+
+
+ {/* Status filter */}
+
+
+ {formatFilterLabel("Status", statusFilters, STATUS_CONFIG)}
+
+
+ }
+ />
+
+
+ Status
+
+ useIssueViewStore.setState({ statusFilters: [] })
+ }
+ >
+ All
+
+
+
+
+ {ALL_STATUSES.map((s) => (
+ toggleStatusFilter(s)}
+ >
+
+ {STATUS_CONFIG[s].label}
+
+ ))}
+
+
+
+
+ {/* Priority filter */}
+
+
+ {formatFilterLabel("Priority", priorityFilters, PRIORITY_CONFIG)}
+
+
+ }
+ />
+
+
+ Priority
+
+ useIssueViewStore.setState({ priorityFilters: [] })
+ }
+ >
+ All
+
+
+
+
+ {PRIORITY_ORDER.map((p) => (
+ togglePriorityFilter(p)}
+ >
+
+ {PRIORITY_CONFIG[p].label}
+
+ ))}
+
+
+
+
+
+
+ {/* View toggle */}
+
+
+ {viewMode === "board" ? :
}
+
+ }
+ />
+
+
+ View
+ setViewMode("board")}>
+
+ Board
+
+ setViewMode("list")}>
+
+ List
+
+
+
+
+
+ {/* New issue */}
+
+
+
+ );
+}
diff --git a/apps/web/features/issues/components/issues-page.tsx b/apps/web/features/issues/components/issues-page.tsx
new file mode 100644
index 00000000..78dba128
--- /dev/null
+++ b/apps/web/features/issues/components/issues-page.tsx
@@ -0,0 +1,133 @@
+"use client";
+
+import { useCallback, useMemo } from "react";
+import { toast } from "sonner";
+import { ChevronRight } from "lucide-react";
+import type { IssueStatus } from "@multica/types";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useIssueStore } from "@/features/issues/store";
+import { useIssueViewStore } from "@/features/issues/stores/view-store";
+import { useWorkspaceStore } from "@/features/workspace";
+import { WorkspaceAvatar } from "@/features/workspace";
+import { api } from "@/shared/api";
+import { IssuesHeader } from "./issues-header";
+import { BoardView } from "./board-view";
+import { ListView } from "./list-view";
+
+const BOARD_STATUSES: IssueStatus[] = [
+ "backlog",
+ "todo",
+ "in_progress",
+ "in_review",
+ "done",
+ "blocked",
+];
+
+export function IssuesPage() {
+ const allIssues = useIssueStore((s) => s.issues);
+ const loading = useIssueStore((s) => s.loading);
+ const workspace = useWorkspaceStore((s) => s.workspace);
+ const viewMode = useIssueViewStore((s) => s.viewMode);
+ const statusFilters = useIssueViewStore((s) => s.statusFilters);
+ const priorityFilters = useIssueViewStore((s) => s.priorityFilters);
+ const clearFilters = useIssueViewStore((s) => s.clearFilters);
+
+ const issues = useMemo(() => {
+ return allIssues.filter((issue) => {
+ if (statusFilters.length > 0 && !statusFilters.includes(issue.status))
+ return false;
+ if (
+ priorityFilters.length > 0 &&
+ !priorityFilters.includes(issue.priority)
+ )
+ return false;
+ return true;
+ });
+ }, [allIssues, statusFilters, priorityFilters]);
+
+ const visibleStatuses = useMemo(() => {
+ if (statusFilters.length > 0)
+ return BOARD_STATUSES.filter((s) => statusFilters.includes(s));
+ return BOARD_STATUSES;
+ }, [statusFilters]);
+
+ const handleMoveIssue = useCallback(
+ (issueId: string, newStatus: IssueStatus) => {
+ useIssueStore.getState().updateIssue(issueId, { status: newStatus });
+
+ api.updateIssue(issueId, { status: newStatus }).catch(() => {
+ toast.error("Failed to move issue");
+ api.listIssues({ limit: 200 }).then((res) => {
+ useIssueStore.getState().setIssues(res.issues);
+ });
+ });
+ },
+ []
+ );
+
+ if (loading) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+
+
+
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+ {/* Header 1: Workspace breadcrumb */}
+
+
+
+ {workspace?.name ?? "Workspace"}
+
+
+ Issues
+
+
+ {/* Header 2: View toggle + filters */}
+
+
+ {/* Content: scrollable */}
+
+ {issues.length === 0 ? (
+
+
No matching issues
+ {(statusFilters.length > 0 || priorityFilters.length > 0) && (
+
+ )}
+
+ ) : viewMode === "board" ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/features/issues/components/list-row.tsx b/apps/web/features/issues/components/list-row.tsx
new file mode 100644
index 00000000..0cbcfeef
--- /dev/null
+++ b/apps/web/features/issues/components/list-row.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import Link from "next/link";
+import type { Issue } from "@multica/types";
+import { ActorAvatar } from "@/components/common/actor-avatar";
+import { StatusIcon } from "./status-icon";
+import { PriorityIcon } from "./priority-icon";
+
+function formatDate(date: string): string {
+ return new Date(date).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ });
+}
+
+export function ListRow({ issue }: { issue: Issue }) {
+ return (
+
+
+
+ {issue.id.slice(0, 8)}
+
+
+ {issue.title}
+ {issue.due_date && (
+
+ {formatDate(issue.due_date)}
+
+ )}
+ {issue.assignee_type && issue.assignee_id && (
+
+ )}
+
+ );
+}
diff --git a/apps/web/features/issues/components/list-view.tsx b/apps/web/features/issues/components/list-view.tsx
new file mode 100644
index 00000000..e4a1842b
--- /dev/null
+++ b/apps/web/features/issues/components/list-view.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import type { Issue } from "@multica/types";
+import { STATUS_ORDER, STATUS_CONFIG } from "@/features/issues/config";
+import { StatusIcon } from "./status-icon";
+import { ListRow } from "./list-row";
+
+export function ListView({ issues }: { issues: Issue[] }) {
+ const groupOrder = STATUS_ORDER.filter((s) => s !== "cancelled");
+
+ return (
+
+ {groupOrder.map((status) => {
+ const cfg = STATUS_CONFIG[status];
+ const filtered = issues.filter((i) => i.status === status);
+ if (filtered.length === 0) return null;
+ return (
+
+
+
+ {cfg.label}
+
+ {filtered.length}
+
+
+ {filtered.map((issue) => (
+
+ ))}
+
+ );
+ })}
+
+ );
+}
diff --git a/apps/web/features/issues/index.ts b/apps/web/features/issues/index.ts
index 7dbb612d..6de13853 100644
--- a/apps/web/features/issues/index.ts
+++ b/apps/web/features/issues/index.ts
@@ -1,3 +1,4 @@
export { useIssueStore } from "./store";
+export { useIssueViewStore } from "./stores/view-store";
export { StatusIcon, PriorityIcon, StatusPicker, PriorityPicker, AssigneePicker } from "./components";
export * from "./config";
diff --git a/apps/web/features/issues/stores/view-store.ts b/apps/web/features/issues/stores/view-store.ts
new file mode 100644
index 00000000..67540b43
--- /dev/null
+++ b/apps/web/features/issues/stores/view-store.ts
@@ -0,0 +1,50 @@
+"use client";
+
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+import type { IssueStatus, IssuePriority } from "@multica/types";
+
+export type ViewMode = "board" | "list";
+
+interface IssueViewState {
+ viewMode: ViewMode;
+ statusFilters: IssueStatus[];
+ priorityFilters: IssuePriority[];
+ setViewMode: (mode: ViewMode) => void;
+ toggleStatusFilter: (status: IssueStatus) => void;
+ togglePriorityFilter: (priority: IssuePriority) => void;
+ clearFilters: () => void;
+}
+
+export const useIssueViewStore = create()(
+ persist(
+ (set) => ({
+ viewMode: "board",
+ statusFilters: [],
+ priorityFilters: [],
+
+ setViewMode: (mode) => set({ viewMode: mode }),
+ toggleStatusFilter: (status) =>
+ set((state) => ({
+ statusFilters: state.statusFilters.includes(status)
+ ? state.statusFilters.filter((s) => s !== status)
+ : [...state.statusFilters, status],
+ })),
+ togglePriorityFilter: (priority) =>
+ set((state) => ({
+ priorityFilters: state.priorityFilters.includes(priority)
+ ? state.priorityFilters.filter((p) => p !== priority)
+ : [...state.priorityFilters, priority],
+ })),
+ clearFilters: () => set({ statusFilters: [], priorityFilters: [] }),
+ }),
+ {
+ name: "multica_issues_view",
+ partialize: (state) => ({
+ viewMode: state.viewMode,
+ statusFilters: state.statusFilters,
+ priorityFilters: state.priorityFilters,
+ }),
+ }
+ )
+);