Files
ProjectE/apps/web/components/tasks/task-activity-feed.tsx
T
mbatchelder b6e2ab415e feat: Phase 2 - Tasks CRUD API, kanban/list UI, dialogs, activity feed, keyboard shortcuts
- Tasks REST API under /api/domains/[domainId]/tasks/ with full CRUD, filtering, pagination
- Complete/uncomplete endpoints
- Bulk update endpoint for drag-to-reorder
- Dependencies API with cycle detection
- Tags API for task tagging
- Activity feed API scoped to workspace
- Updated kanban board view with 4 columns (todo/in_progress/done/cancelled)
- Updated list view with status column and workspace-scoped API calls
- Task create dialog with title, description, status, priority, due date, estimate
- Task detail panel (sheet) with full edit capabilities
- Task activity feed widget
- Keyboard shortcuts: c t (new task), e (edit), d (delete), Space (open), Esc (close), 1-4 (filter)
- All routes follow AGENTS.md contract: Drizzle writes + activity feed + pg_notify
2026-07-29 06:12:35 -04:00

95 lines
2.8 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Badge } from '@/components/ui/badge';
import { Loader2 } from 'lucide-react';
interface ActivityEntry {
id: string;
actor: string;
action: string;
entityType: string;
entityId: string;
changes?: Record<string, unknown> | null;
createdAt: string;
}
const actionLabels: Record<string, string> = {
created: 'created',
updated: 'updated',
deleted: 'deleted',
completed: 'completed',
uncompleted: 'reverted',
bulk_updated: 'bulk updated',
dependency_added: 'added dependency to',
dependency_removed: 'removed dependency from',
tag_added: 'added tag to',
tag_removed: 'removed tag from',
};
export function TaskActivityFeed({ domainId }: { domainId: string }) {
const [activities, setActivities] = useState<ActivityEntry[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!domainId) return;
fetch(`/api/domains/${domainId}/activity?entity_type=task&limit=10`)
.then((res) => {
if (!res.ok) throw new Error('Failed to load activity');
return res.json();
})
.then((data) => {
setActivities(data.items || []);
})
.catch((err) => {
console.error('Failed to load activity feed:', err);
})
.finally(() => setLoading(false));
}, [domainId]);
if (loading) {
return (
<div className="flex justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
);
}
if (activities.length === 0) {
return (
<div className="py-8 text-center text-sm text-muted-foreground">
<p>No recent activity</p>
</div>
);
}
return (
<ScrollArea className="h-[400px]">
<div className="space-y-3">
{activities.map((entry) => (
<div key={entry.id} className="flex items-start gap-2 text-sm">
<Badge variant="outline" className="mt-0.5 shrink-0 text-xs capitalize">
{actionLabels[entry.action] || entry.action}
</Badge>
<div className="min-w-0 flex-1">
<p className="truncate text-muted-foreground">
<span className="font-medium text-foreground">{entry.actor}</span>
{' '}
{actionLabels[entry.action] || entry.action}
{' '}
{entry.changes && typeof entry.changes === 'object' && 'title' in entry.changes
? `"${entry.changes.title}"`
: entry.entityId.slice(0, 8)}
</p>
<p className="text-xs text-muted-foreground">
{new Date(entry.createdAt).toLocaleString()}
</p>
</div>
</div>
))}
</div>
</ScrollArea>
);
}