mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-07 12:17:50 +00:00
feat(US-702): Restore from archive
- Added restoreTask() to TaskService - Added POST /api/tasks/:id/restore endpoint - Added useRestoreTask() hook - Added restore button (hover) to ArchiveSidebar items - Tasks restore to Done column with all data preserved
This commit is contained in:
parent
db802ed56d
commit
cf725c5eff
7 changed files with 151 additions and 5 deletions
|
|
@ -12,7 +12,7 @@
|
|||
| ID | Title | Status | Dependencies | Notes |
|
||||
|----|-------|--------|--------------|-------|
|
||||
| US-701 | Archive sidebar | ✅ Done | None | Slide-out viewer with search/filter for archived tasks |
|
||||
| US-702 | Restore from archive | ⏳ Todo | US-701 | Unarchive tasks back to board |
|
||||
| US-702 | Restore from archive | ✅ Done | US-701 | Unarchive tasks back to board |
|
||||
| US-703 | Subtasks | ⏳ Todo | None | Nested tasks with parent completion logic |
|
||||
| US-704 | Task dependencies | ⏳ Todo | US-703 | Block tasks until dependencies complete |
|
||||
| US-705 | Multiple task attempts | ⏳ Todo | None | Retry with different agent, preserve history |
|
||||
|
|
|
|||
|
|
@ -1,4 +1,45 @@
|
|||
[
|
||||
{
|
||||
"id": "activity_1769427982787_zlmx7wl1j",
|
||||
"type": "status_changed",
|
||||
"taskId": "task_20260126_UBz41o",
|
||||
"taskTitle": "US-702: Restore from archive",
|
||||
"details": {
|
||||
"from": "in-progress",
|
||||
"status": "done"
|
||||
},
|
||||
"timestamp": "2026-01-26T11:46:22.788Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769427885455_irs3lpfay",
|
||||
"type": "status_changed",
|
||||
"taskId": "task_20260126_UBz41o",
|
||||
"taskTitle": "US-702: Restore from archive",
|
||||
"details": {
|
||||
"from": "todo",
|
||||
"status": "in-progress"
|
||||
},
|
||||
"timestamp": "2026-01-26T11:44:45.455Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769427882031_qsa7fjdlb",
|
||||
"type": "task_created",
|
||||
"taskId": "task_20260126_UBz41o",
|
||||
"taskTitle": "US-702: Restore from archive",
|
||||
"details": {
|
||||
"type": "code",
|
||||
"priority": "medium",
|
||||
"project": "veritas-kanban"
|
||||
},
|
||||
"timestamp": "2026-01-26T11:44:42.031Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769427809793_r57vixvwa",
|
||||
"type": "task_updated",
|
||||
"taskId": "task_20260126_wzj1d0",
|
||||
"taskTitle": "US-701: Archive sidebar",
|
||||
"timestamp": "2026-01-26T11:43:29.793Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769427062484_c9s4q6kiq",
|
||||
"type": "task_created",
|
||||
|
|
|
|||
|
|
@ -201,6 +201,27 @@ router.post('/:id/archive', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// POST /api/tasks/:id/restore - Restore task from archive
|
||||
router.post('/:id/restore', async (req, res) => {
|
||||
try {
|
||||
const task = await taskService.restoreTask(req.params.id);
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Archived task not found' });
|
||||
}
|
||||
|
||||
// Log activity
|
||||
await activityService.logActivity('status_changed', task.id, task.title, {
|
||||
from: 'archived',
|
||||
status: 'done',
|
||||
});
|
||||
|
||||
res.json(task);
|
||||
} catch (error) {
|
||||
console.error('Error restoring task:', error);
|
||||
res.status(500).json({ error: 'Failed to restore task' });
|
||||
}
|
||||
});
|
||||
|
||||
// === Worktree Routes ===
|
||||
|
||||
// POST /api/tasks/:id/worktree - Create worktree
|
||||
|
|
|
|||
|
|
@ -228,4 +228,28 @@ export class TaskService {
|
|||
const tasks = await this.listArchivedTasks();
|
||||
return tasks.find(t => t.id === id) || null;
|
||||
}
|
||||
|
||||
async restoreTask(id: string): Promise<Task | null> {
|
||||
const task = await this.getArchivedTask(id);
|
||||
if (!task) return null;
|
||||
|
||||
const filename = this.taskToFilename(task);
|
||||
const sourcePath = path.join(this.archiveDir, filename);
|
||||
const destPath = path.join(this.tasksDir, filename);
|
||||
|
||||
// Move back to active and set status to done
|
||||
await fs.rename(sourcePath, destPath);
|
||||
|
||||
// Update status to done
|
||||
const restoredTask: Task = {
|
||||
...task,
|
||||
status: 'done',
|
||||
updated: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const content = this.taskToMarkdown(restoredTask);
|
||||
await fs.writeFile(destPath, content, 'utf-8');
|
||||
|
||||
return restoredTask;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useMemo } from 'react';
|
||||
import { Archive, RefreshCw, Search, Calendar, FolderOpen } from 'lucide-react';
|
||||
import { Archive, RefreshCw, Search, Calendar, FolderOpen, RotateCcw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
|
@ -17,7 +17,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useArchivedTasks } from '@/hooks/useTasks';
|
||||
import { useArchivedTasks, useRestoreTask } from '@/hooks/useTasks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Task, TaskType } from '@veritas-kanban/shared';
|
||||
|
||||
|
|
@ -50,11 +50,21 @@ function formatDate(dateString: string): string {
|
|||
});
|
||||
}
|
||||
|
||||
function ArchivedTaskItem({ task, onClick }: { task: Task; onClick?: () => void }) {
|
||||
function ArchivedTaskItem({
|
||||
task,
|
||||
onClick,
|
||||
onRestore,
|
||||
isRestoring,
|
||||
}: {
|
||||
task: Task;
|
||||
onClick?: () => void;
|
||||
onRestore?: () => void;
|
||||
isRestoring?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-start gap-3 py-3 px-2 rounded-md transition-colors",
|
||||
"flex items-start gap-3 py-3 px-2 rounded-md transition-colors group",
|
||||
onClick ? "hover:bg-muted/50 cursor-pointer" : ""
|
||||
)}
|
||||
onClick={onClick}
|
||||
|
|
@ -93,6 +103,21 @@ function ArchivedTaskItem({ task, onClick }: { task: Task; onClick?: () => void
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{onRestore && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRestore();
|
||||
}}
|
||||
disabled={isRestoring}
|
||||
title="Restore to board"
|
||||
>
|
||||
<RotateCcw className={cn("h-4 w-4", isRestoring && "animate-spin")} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -101,8 +126,21 @@ export function ArchiveSidebar({ open, onOpenChange, onTaskClick }: ArchiveSideb
|
|||
const [search, setSearch] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||
const [projectFilter, setProjectFilter] = useState<string>('all');
|
||||
const [restoringId, setRestoringId] = useState<string | null>(null);
|
||||
|
||||
const { data: archivedTasks, isLoading, refetch, isRefetching } = useArchivedTasks();
|
||||
const restoreTask = useRestoreTask();
|
||||
|
||||
const handleRestore = async (task: Task) => {
|
||||
setRestoringId(task.id);
|
||||
try {
|
||||
await restoreTask.mutateAsync(task.id);
|
||||
} catch (error) {
|
||||
console.error('Failed to restore task:', error);
|
||||
} finally {
|
||||
setRestoringId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Get unique projects for filter dropdown
|
||||
const projects = useMemo(() => {
|
||||
|
|
@ -224,6 +262,8 @@ export function ArchiveSidebar({ open, onOpenChange, onTaskClick }: ArchiveSideb
|
|||
key={task.id}
|
||||
task={task}
|
||||
onClick={onTaskClick ? () => onTaskClick(task) : undefined}
|
||||
onRestore={() => handleRestore(task)}
|
||||
isRestoring={restoringId === task.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,19 @@ export function useArchiveTask() {
|
|||
mutationFn: (id: string) => api.tasks.archive(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks', 'archived'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRestoreTask() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.tasks.restore(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks', 'archived'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,13 @@ export const api = {
|
|||
});
|
||||
return handleResponse<void>(response);
|
||||
},
|
||||
|
||||
restore: async (id: string): Promise<Task> => {
|
||||
const response = await fetch(`${API_BASE}/tasks/${id}/restore`, {
|
||||
method: 'POST',
|
||||
});
|
||||
return handleResponse<Task>(response);
|
||||
},
|
||||
},
|
||||
|
||||
config: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue