feat: add PriorityBadge component and integrate into TaskRow
- Implemented PriorityBadge component to display task priority with icons. - Created TaskRow component to represent individual tasks with metadata. - Added breadcrumb navigation for task hierarchy. - Enhanced checkbox component to support indeterminate state. - Introduced InputGroup for better input handling in task search. - Updated tasks route to utilize new components and improve UI. - Added empty state representation for task list.
This commit is contained in:
153
src/renderer/components/tasks/TaskRow.tsx
Normal file
153
src/renderer/components/tasks/TaskRow.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Calendar, User, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbSeparator,
|
||||
} from '@/components/ui/breadcrumb';
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from '@/components/ui/context-menu';
|
||||
import { PriorityBadge } from './PriorityBadge';
|
||||
|
||||
export type TaskItem = {
|
||||
id: string;
|
||||
projectId: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
status: string | null;
|
||||
priority: string | null;
|
||||
assignee: string | null;
|
||||
dueDate: number | null;
|
||||
projectName: string | null;
|
||||
clientName: string | null;
|
||||
subClientName: string | null;
|
||||
};
|
||||
|
||||
export function parseAssignees(raw: string | null): string[] {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (Array.isArray(parsed)) return parsed.filter((n): n is string => typeof n === 'string');
|
||||
} catch { /* plain string fallback */ }
|
||||
return [raw];
|
||||
}
|
||||
|
||||
function formatDueDate(timestamp: number): string {
|
||||
const d = new Date(timestamp);
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
return `Due ${months[d.getMonth()]} ${d.getDate()}`;
|
||||
}
|
||||
|
||||
export function TaskRow({
|
||||
task,
|
||||
onToggle,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: {
|
||||
task: TaskItem;
|
||||
onToggle: (id: string, status: string | null) => void;
|
||||
onEdit?: (task: TaskItem) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
}) {
|
||||
const isDone = task.status === 'done';
|
||||
|
||||
const checkboxState: boolean | 'indeterminate' =
|
||||
task.status === 'done' ? true :
|
||||
task.status === 'in_progress' ? 'indeterminate' : false;
|
||||
|
||||
const breadcrumb: string[] = [];
|
||||
if (task.clientName) breadcrumb.push(task.clientName);
|
||||
if (task.subClientName) breadcrumb.push(task.subClientName);
|
||||
if (task.projectName) breadcrumb.push(task.projectName);
|
||||
|
||||
const hasMetadata =
|
||||
task.priority ||
|
||||
task.dueDate ||
|
||||
breadcrumb.length > 0 ||
|
||||
task.assignee;
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
className={`flex flex-col gap-1.5 px-4 py-3 rounded-md border cursor-default select-none ${
|
||||
isDone ? 'bg-green-50 border-green-200' : 'bg-white border-border'
|
||||
}`}
|
||||
>
|
||||
{/* Row 1: checkbox + title + description */}
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
checked={checkboxState}
|
||||
onCheckedChange={() => onToggle(task.id, task.status)}
|
||||
className="mt-0.5 shrink-0"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`text-sm font-semibold ${isDone ? 'line-through text-muted-foreground' : ''}`}>
|
||||
{task.title}
|
||||
</div>
|
||||
{task.description && (
|
||||
<div className="text-sm text-muted-foreground truncate">
|
||||
{task.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: metadata, indented to align with title text */}
|
||||
{hasMetadata && (
|
||||
<div className="flex flex-wrap items-center gap-2 pl-7">
|
||||
<PriorityBadge priority={task.priority} />
|
||||
|
||||
{task.dueDate && (
|
||||
<Badge variant="outline" className="text-xs gap-1 shrink-0">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{formatDueDate(task.dueDate)}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{breadcrumb.length > 0 && (
|
||||
<Breadcrumb className="shrink-0">
|
||||
<BreadcrumbList>
|
||||
{breadcrumb.map((part, i) => (
|
||||
<BreadcrumbItem key={i}>
|
||||
{i > 0 && <BreadcrumbSeparator />}
|
||||
<span className="text-xs">{part}</span>
|
||||
</BreadcrumbItem>
|
||||
))}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
|
||||
{task.assignee && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground shrink-0">
|
||||
<User className="h-3 w-3" />
|
||||
{parseAssignees(task.assignee).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem onSelect={() => onEdit?.(task)}>
|
||||
<Pencil className="h-4 w-4 mr-2" />
|
||||
Edit Task
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
onSelect={() => onDelete?.(task.id)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete Task
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user