mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-07 16:41:06 +08:00
feat: add SpecDialog component for editing spec frontmatter
- Implement SpecDialog for managing spec details including title, read mode, priority, and keywords. - Add validation and keyword management functionality. - Integrate SpecDialog into SpecsSettingsPage for editing specs. feat: create index file for specs components - Export SpecCard, SpecDialog, and related types from a new index file for better organization. feat: implement SpecsSettingsPage for managing specs and hooks - Create main settings page with tabs for Project Specs, Personal Specs, Hooks, Injection, and Settings. - Integrate SpecDialog and HookDialog for editing specs and hooks. - Add search functionality and mock data for specs and hooks. feat: add spec management API routes - Implement API endpoints for listing specs, getting spec details, updating frontmatter, rebuilding indices, and initializing the spec system. - Handle errors and responses appropriately for each endpoint.
This commit is contained in:
@@ -25,11 +25,12 @@ type ToolName = 'claude' | 'codex' | 'gemini';
|
||||
type ResumeStrategy = 'nativeResume' | 'promptConcat';
|
||||
|
||||
const BOARD_COLUMNS: Array<{ id: IssueBoardStatus; titleKey: string }> = [
|
||||
{ id: 'open', titleKey: 'issues.status.open' },
|
||||
{ id: 'in_progress', titleKey: 'issues.status.inProgress' },
|
||||
{ id: 'resolved', titleKey: 'issues.status.resolved' },
|
||||
{ id: 'registered', titleKey: 'issues.status.registered' },
|
||||
{ id: 'planning', titleKey: 'issues.status.planning' },
|
||||
{ id: 'planned', titleKey: 'issues.status.planned' },
|
||||
{ id: 'executing', titleKey: 'issues.status.executing' },
|
||||
{ id: 'completed', titleKey: 'issues.status.completed' },
|
||||
{ id: 'closed', titleKey: 'issues.status.closed' },
|
||||
{ id: 'failed', titleKey: 'issues.status.failed' },
|
||||
];
|
||||
|
||||
type BoardOrder = Partial<Record<IssueBoardStatus, string[]>>;
|
||||
@@ -294,8 +295,8 @@ export function IssueBoardPanel() {
|
||||
try {
|
||||
await updateIssue(issueId, { status: destStatus });
|
||||
|
||||
// Auto action: drag to in_progress opens the drawer on terminal tab.
|
||||
if (destStatus === 'in_progress' && sourceStatus !== 'in_progress') {
|
||||
// Auto action: drag to executing opens the drawer on terminal tab.
|
||||
if (destStatus === 'executing' && sourceStatus !== 'executing') {
|
||||
setDrawerInitialTab('terminal');
|
||||
setSelectedIssue({ ...issue, status: destStatus });
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/Button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// Keep in sync with IssueHubHeader/IssueHubPage
|
||||
export type IssueTab = 'issues' | 'board' | 'queue' | 'discovery';
|
||||
export type IssueTab = 'issues' | 'queue' | 'discovery';
|
||||
|
||||
interface IssueHubTabsProps {
|
||||
currentTab: IssueTab;
|
||||
@@ -20,7 +20,6 @@ export function IssueHubTabs({ currentTab, onTabChange }: IssueHubTabsProps) {
|
||||
|
||||
const tabs: Array<{ value: IssueTab; label: string }> = [
|
||||
{ value: 'issues', label: formatMessage({ id: 'issues.hub.tabs.issues' }) },
|
||||
{ value: 'board', label: formatMessage({ id: 'issues.hub.tabs.board' }) },
|
||||
{ value: 'queue', label: formatMessage({ id: 'issues.hub.tabs.queue' }) },
|
||||
{ value: 'discovery', label: formatMessage({ id: 'issues.hub.tabs.discovery' }) },
|
||||
];
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// ========================================
|
||||
// Issues Panel
|
||||
// ========================================
|
||||
// Issue list panel for IssueHub
|
||||
// Unified issue list panel with list/board view toggle
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
Search,
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
AlertCircle,
|
||||
LayoutGrid,
|
||||
List,
|
||||
} from 'lucide-react';
|
||||
import type { DropResult } from '@hello-pangea/dnd';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
@@ -19,11 +22,24 @@ import { Button } from '@/components/ui/Button';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/Select';
|
||||
import { IssueCard } from '@/components/shared/IssueCard';
|
||||
import { IssueDrawer } from '@/components/issue/hub/IssueDrawer';
|
||||
import { KanbanBoard, type KanbanColumn, type KanbanItem } from '@/components/shared/KanbanBoard';
|
||||
import { useIssues, useIssueMutations } from '@/hooks';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Issue } from '@/lib/api';
|
||||
|
||||
type StatusFilter = 'all' | Issue['status'];
|
||||
type PriorityFilter = 'all' | Issue['priority'];
|
||||
type ViewMode = 'list' | 'board';
|
||||
|
||||
// Board columns matching backend status with Chinese defaults
|
||||
const BOARD_COLUMNS: Array<{ id: Issue['status']; titleKey: string; defaultLabel: string }> = [
|
||||
{ id: 'registered', titleKey: 'issues.status.registered', defaultLabel: '新注册' },
|
||||
{ id: 'planning', titleKey: 'issues.status.planning', defaultLabel: '规划中' },
|
||||
{ id: 'planned', titleKey: 'issues.status.planned', defaultLabel: '已规划' },
|
||||
{ id: 'executing', titleKey: 'issues.status.executing', defaultLabel: '执行中' },
|
||||
{ id: 'completed', titleKey: 'issues.status.completed', defaultLabel: '已完成' },
|
||||
];
|
||||
|
||||
interface IssuesPanelProps {
|
||||
onCreateIssue?: () => void;
|
||||
@@ -77,14 +93,114 @@ function IssueList({ issues, isLoading, onIssueClick, onIssueEdit, onIssueDelete
|
||||
);
|
||||
}
|
||||
|
||||
// Local storage key for board order
|
||||
function storageKey(projectPath: string | null | undefined): string {
|
||||
const base = projectPath ? encodeURIComponent(projectPath) : 'global';
|
||||
return `ccw.issues.board.order:${base}`;
|
||||
}
|
||||
|
||||
type BoardOrder = Partial<Record<Issue['status'], string[]>>;
|
||||
|
||||
function safeParseOrder(value: string | null): BoardOrder {
|
||||
if (!value) return {};
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown;
|
||||
if (!parsed || typeof parsed !== 'object') return {};
|
||||
return parsed as BoardOrder;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function buildColumns(
|
||||
issues: Issue[],
|
||||
order: BoardOrder,
|
||||
formatTitle: (statusId: Issue['status']) => string
|
||||
): KanbanColumn<Issue & KanbanItem>[] {
|
||||
const byId = new Map(issues.map((i) => [i.id, i]));
|
||||
const columns: KanbanColumn<Issue & KanbanItem>[] = [];
|
||||
|
||||
for (const col of BOARD_COLUMNS) {
|
||||
const desired = (order[col.id] ?? []).map((id) => byId.get(id)).filter(Boolean) as Issue[];
|
||||
const desiredIds = new Set(desired.map((i) => i.id));
|
||||
|
||||
const remaining = issues
|
||||
.filter((i) => i.status === col.id && !desiredIds.has(i.id))
|
||||
.sort((a, b) => {
|
||||
const at = a.updatedAt || a.createdAt || '';
|
||||
const bt = b.updatedAt || b.createdAt || '';
|
||||
return bt.localeCompare(at);
|
||||
});
|
||||
|
||||
const items = [...desired, ...remaining].map((issue) => ({
|
||||
...issue,
|
||||
id: issue.id,
|
||||
title: issue.title,
|
||||
status: issue.status,
|
||||
}));
|
||||
|
||||
columns.push({
|
||||
id: col.id,
|
||||
title: formatTitle(col.id),
|
||||
items,
|
||||
icon: <LayoutGrid className="w-4 h-4" />,
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
function syncOrderWithIssues(prev: BoardOrder, issues: Issue[]): BoardOrder {
|
||||
const statusById = new Map(issues.map((i) => [i.id, i.status]));
|
||||
const next: BoardOrder = {};
|
||||
|
||||
for (const { id: status } of BOARD_COLUMNS) {
|
||||
const existing = prev[status] ?? [];
|
||||
const filtered = existing.filter((id) => statusById.get(id) === status);
|
||||
const present = new Set(filtered);
|
||||
|
||||
const missing = issues
|
||||
.filter((i) => i.status === status && !present.has(i.id))
|
||||
.map((i) => i.id);
|
||||
|
||||
next[status] = [...filtered, ...missing];
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
function reorderIds(list: string[], from: number, to: number): string[] {
|
||||
const next = [...list];
|
||||
const [moved] = next.splice(from, 1);
|
||||
if (moved === undefined) return list;
|
||||
next.splice(to, 0, moved);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function IssuesPanel({ onCreateIssue: _onCreateIssue }: IssuesPanelProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
// View mode state
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
|
||||
// Filter state
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
||||
const [priorityFilter, setPriorityFilter] = useState<PriorityFilter>('all');
|
||||
const [selectedIssue, setSelectedIssue] = useState<Issue | null>(null);
|
||||
|
||||
const { issues, issuesByStatus, openCount, criticalCount, isLoading } = useIssues({
|
||||
// Board order state
|
||||
const [order, setOrder] = useState<BoardOrder>({});
|
||||
|
||||
// Load board order when project changes
|
||||
useEffect(() => {
|
||||
const key = storageKey(projectPath);
|
||||
const loaded = safeParseOrder(localStorage.getItem(key));
|
||||
setOrder(loaded);
|
||||
}, [projectPath]);
|
||||
|
||||
const { issues, criticalCount, isLoading } = useIssues({
|
||||
filter: {
|
||||
search: searchQuery || undefined,
|
||||
status: statusFilter !== 'all' ? [statusFilter] : undefined,
|
||||
@@ -94,14 +210,50 @@ export function IssuesPanel({ onCreateIssue: _onCreateIssue }: IssuesPanelProps)
|
||||
|
||||
const { updateIssue, deleteIssue } = useIssueMutations();
|
||||
|
||||
// Keep order consistent with current issues
|
||||
useEffect(() => {
|
||||
setOrder((prev) => syncOrderWithIssues(prev, issues));
|
||||
}, [issues]);
|
||||
|
||||
// Persist order
|
||||
useEffect(() => {
|
||||
const key = storageKey(projectPath);
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(order));
|
||||
} catch {
|
||||
// ignore quota errors
|
||||
}
|
||||
}, [order, projectPath]);
|
||||
|
||||
// Status counts using backend statuses
|
||||
const statusCounts = useMemo(() => ({
|
||||
all: issues.length,
|
||||
open: issuesByStatus.open?.length || 0,
|
||||
in_progress: issuesByStatus.in_progress?.length || 0,
|
||||
resolved: issuesByStatus.resolved?.length || 0,
|
||||
closed: issuesByStatus.closed?.length || 0,
|
||||
completed: issuesByStatus.completed?.length || 0,
|
||||
}), [issues, issuesByStatus]);
|
||||
registered: issues.filter(i => i.status === 'registered').length,
|
||||
planning: issues.filter(i => i.status === 'planning').length,
|
||||
planned: issues.filter(i => i.status === 'planned').length,
|
||||
executing: issues.filter(i => i.status === 'executing').length,
|
||||
completed: issues.filter(i => i.status === 'completed').length,
|
||||
failed: issues.filter(i => i.status === 'failed').length,
|
||||
}), [issues]);
|
||||
|
||||
// Build kanban columns
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
buildColumns(issues, order, (statusId) => {
|
||||
const col = BOARD_COLUMNS.find((c) => c.id === statusId);
|
||||
if (!col) return statusId;
|
||||
return formatMessage({ id: col.titleKey, defaultMessage: col.defaultLabel });
|
||||
}),
|
||||
[issues, order, formatMessage]
|
||||
);
|
||||
|
||||
const idsByStatus = useMemo(() => {
|
||||
const map: Record<string, string[]> = {};
|
||||
for (const col of columns) {
|
||||
map[col.id] = col.items.map((i) => i.id);
|
||||
}
|
||||
return map;
|
||||
}, [columns]);
|
||||
|
||||
const handleEditIssue = (_issue: Issue) => {};
|
||||
|
||||
@@ -123,22 +275,66 @@ export function IssuesPanel({ onCreateIssue: _onCreateIssue }: IssuesPanelProps)
|
||||
setSelectedIssue(null);
|
||||
};
|
||||
|
||||
// Board drag handler
|
||||
const handleDragEnd = useCallback(
|
||||
async (result: DropResult, sourceColumn: string, destColumn: string) => {
|
||||
const issueId = result.draggableId;
|
||||
const issue = issues.find((i) => i.id === issueId);
|
||||
if (!issue) return;
|
||||
|
||||
const sourceStatus = sourceColumn as Issue['status'];
|
||||
const destStatus = destColumn as Issue['status'];
|
||||
const sourceIds = idsByStatus[sourceStatus] ?? [];
|
||||
const destIds = idsByStatus[destStatus] ?? [];
|
||||
|
||||
// Update local order first (optimistic)
|
||||
setOrder((prev) => {
|
||||
const next = { ...prev };
|
||||
if (sourceStatus === destStatus) {
|
||||
next[sourceStatus] = reorderIds(sourceIds, result.source.index, result.destination!.index);
|
||||
return next;
|
||||
}
|
||||
|
||||
const nextSource = [...sourceIds];
|
||||
nextSource.splice(result.source.index, 1);
|
||||
|
||||
const nextDest = [...destIds];
|
||||
nextDest.splice(result.destination!.index, 0, issueId);
|
||||
|
||||
next[sourceStatus] = nextSource;
|
||||
next[destStatus] = nextDest;
|
||||
return next;
|
||||
});
|
||||
|
||||
// Status update
|
||||
if (sourceStatus !== destStatus) {
|
||||
try {
|
||||
await updateIssue(issueId, { status: destStatus });
|
||||
} catch (e) {
|
||||
console.error('Failed to update issue status:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
[issues, idsByStatus, updateIssue]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-info" />
|
||||
<span className="text-2xl font-bold">{openCount}</span>
|
||||
<span className="text-2xl font-bold">{statusCounts.registered}</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'common.status.openIssues' })}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'issues.status.registered', defaultMessage: '新注册' })}</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-warning" />
|
||||
<span className="text-2xl font-bold">{issuesByStatus.in_progress?.length || 0}</span>
|
||||
<span className="text-2xl font-bold">{statusCounts.executing}</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'issues.status.inProgress' })}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'issues.status.executing', defaultMessage: '执行中' })}</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -150,12 +346,13 @@ export function IssuesPanel({ onCreateIssue: _onCreateIssue }: IssuesPanelProps)
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5 text-success" />
|
||||
<span className="text-2xl font-bold">{issuesByStatus.resolved?.length || 0}</span>
|
||||
<span className="text-2xl font-bold">{statusCounts.completed}</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'issues.status.resolved' })}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'issues.status.completed', defaultMessage: '已完成' })}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
@@ -166,6 +363,29 @@ export function IssuesPanel({ onCreateIssue: _onCreateIssue }: IssuesPanelProps)
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex border border-border rounded-md overflow-hidden">
|
||||
<Button
|
||||
variant={viewMode === 'list' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className="rounded-none"
|
||||
onClick={() => setViewMode('list')}
|
||||
>
|
||||
<List className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'common.view.list', defaultMessage: '列表' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'board' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className="rounded-none"
|
||||
onClick={() => setViewMode('board')}
|
||||
>
|
||||
<LayoutGrid className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'common.view.board', defaultMessage: '看板' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as StatusFilter)}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
@@ -173,10 +393,12 @@ export function IssuesPanel({ onCreateIssue: _onCreateIssue }: IssuesPanelProps)
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{formatMessage({ id: 'issues.filters.all' })}</SelectItem>
|
||||
<SelectItem value="open">{formatMessage({ id: 'issues.status.open' })}</SelectItem>
|
||||
<SelectItem value="in_progress">{formatMessage({ id: 'issues.status.inProgress' })}</SelectItem>
|
||||
<SelectItem value="resolved">{formatMessage({ id: 'issues.status.resolved' })}</SelectItem>
|
||||
<SelectItem value="closed">{formatMessage({ id: 'issues.status.closed' })}</SelectItem>
|
||||
<SelectItem value="registered">{formatMessage({ id: 'issues.status.registered', defaultMessage: '新注册' })}</SelectItem>
|
||||
<SelectItem value="planning">{formatMessage({ id: 'issues.status.planning', defaultMessage: '规划中' })}</SelectItem>
|
||||
<SelectItem value="planned">{formatMessage({ id: 'issues.status.planned', defaultMessage: '已规划' })}</SelectItem>
|
||||
<SelectItem value="executing">{formatMessage({ id: 'issues.status.executing', defaultMessage: '执行中' })}</SelectItem>
|
||||
<SelectItem value="completed">{formatMessage({ id: 'issues.status.completed', defaultMessage: '已完成' })}</SelectItem>
|
||||
<SelectItem value="failed">{formatMessage({ id: 'issues.status.failed', defaultMessage: '失败' })}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={priorityFilter} onValueChange={(v) => setPriorityFilter(v as PriorityFilter)}>
|
||||
@@ -194,32 +416,59 @@ export function IssuesPanel({ onCreateIssue: _onCreateIssue }: IssuesPanelProps)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant={statusFilter === 'all' ? 'default' : 'outline'} size="sm" onClick={() => setStatusFilter('all')}>
|
||||
{formatMessage({ id: 'issues.filters.all' })} ({statusCounts.all})
|
||||
</Button>
|
||||
<Button variant={statusFilter === 'open' ? 'default' : 'outline'} size="sm" onClick={() => setStatusFilter('open')}>
|
||||
<Badge variant="info" className="mr-2">{statusCounts.open}</Badge>
|
||||
{formatMessage({ id: 'issues.status.open' })}
|
||||
</Button>
|
||||
<Button variant={statusFilter === 'in_progress' ? 'default' : 'outline'} size="sm" onClick={() => setStatusFilter('in_progress')}>
|
||||
<Badge variant="warning" className="mr-2">{statusCounts.in_progress}</Badge>
|
||||
{formatMessage({ id: 'issues.status.inProgress' })}
|
||||
</Button>
|
||||
<Button variant={priorityFilter === 'critical' ? 'destructive' : 'outline'} size="sm" onClick={() => { setPriorityFilter(priorityFilter === 'critical' ? 'all' : 'critical'); setStatusFilter('all'); }}>
|
||||
<Badge variant="destructive" className="mr-2">{criticalCount}</Badge>
|
||||
{formatMessage({ id: 'issues.priority.critical' })}
|
||||
</Button>
|
||||
</div>
|
||||
{/* Status Filter Pills - only in list mode */}
|
||||
{viewMode === 'list' && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant={statusFilter === 'all' ? 'default' : 'outline'} size="sm" onClick={() => setStatusFilter('all')}>
|
||||
{formatMessage({ id: 'issues.filters.all' })} ({statusCounts.all})
|
||||
</Button>
|
||||
<Button variant={statusFilter === 'registered' ? 'default' : 'outline'} size="sm" onClick={() => setStatusFilter('registered')}>
|
||||
<Badge variant="info" className="mr-2">{statusCounts.registered}</Badge>
|
||||
{formatMessage({ id: 'issues.status.registered', defaultMessage: '新注册' })}
|
||||
</Button>
|
||||
<Button variant={statusFilter === 'executing' ? 'default' : 'outline'} size="sm" onClick={() => setStatusFilter('executing')}>
|
||||
<Badge variant="warning" className="mr-2">{statusCounts.executing}</Badge>
|
||||
{formatMessage({ id: 'issues.status.executing', defaultMessage: '执行中' })}
|
||||
</Button>
|
||||
<Button variant={priorityFilter === 'critical' ? 'destructive' : 'outline'} size="sm" onClick={() => { setPriorityFilter(priorityFilter === 'critical' ? 'all' : 'critical'); setStatusFilter('all'); }}>
|
||||
<Badge variant="destructive" className="mr-2">{criticalCount}</Badge>
|
||||
{formatMessage({ id: 'issues.priority.critical' })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IssueList
|
||||
issues={issues}
|
||||
isLoading={isLoading}
|
||||
onIssueClick={handleIssueClick}
|
||||
onIssueEdit={handleEditIssue}
|
||||
onIssueDelete={handleDeleteIssue}
|
||||
onStatusChange={handleStatusChange}
|
||||
/>
|
||||
{/* Content Area */}
|
||||
{viewMode === 'list' ? (
|
||||
<IssueList
|
||||
issues={issues}
|
||||
isLoading={isLoading}
|
||||
onIssueClick={handleIssueClick}
|
||||
onIssueEdit={handleEditIssue}
|
||||
onIssueDelete={handleDeleteIssue}
|
||||
onStatusChange={handleStatusChange}
|
||||
/>
|
||||
) : (
|
||||
<KanbanBoard<Issue & KanbanItem>
|
||||
columns={columns}
|
||||
onDragEnd={handleDragEnd}
|
||||
onItemClick={(item) => handleIssueClick(item as unknown as Issue)}
|
||||
isLoading={isLoading}
|
||||
emptyColumnMessage={formatMessage({ id: 'issues.emptyState.message' })}
|
||||
className={cn('gap-4', 'grid')}
|
||||
renderItem={(item, provided) => (
|
||||
<IssueCard
|
||||
issue={item as unknown as Issue}
|
||||
compact
|
||||
showActions={false}
|
||||
onClick={(i) => handleIssueClick(i)}
|
||||
innerRef={provided.innerRef}
|
||||
draggableProps={provided.draggableProps}
|
||||
dragHandleProps={provided.dragHandleProps}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Issue Detail Drawer */}
|
||||
<IssueDrawer
|
||||
|
||||
Reference in New Issue
Block a user