Files
kandev/apps/web/components/task/task-unarchive-button.tsx
T
João Salavisa 7bcb10bfef feat: unarchive tasks with worktree branch recovery (#1687)
* feat: unarchive tasks with worktree branch recovery

Archived tasks can now be restored. Unarchive works for manual/legacy
archives (empty cascade id, root-only) and cascade archives. Archive
deletes the local branch, so worktree recreate now falls back to
fetching the branch from origin, and unarchive writes the old branch
back as checkout_branch so a fresh session resumes the prior work.
Unrecoverable branches (never pushed) surface a warning. WS archive now
routes through the handoff cascade so it stays stamped and reversible.
UI: unarchive action on /tasks archived rows and the task top bar.

* fix: address PR review feedback on unarchive recovery

Forward PRNumber to the recreate fetch so fork-PR heads (only reachable
via pull/<N>/head) are recoverable, publish task.updated per task in the
cascade unarchive loop so the UI restores without a reload, and
re-publish task.updated after a checkout_branch restore since the
unarchive event goes out with the pre-restore repository rows.

* fix: harden unarchive recovery per review feedback

Propagate branch-probe errors instead of misreporting recoverable work
as gone, and reserve ErrBranchUnrecoverable for a confirmed-missing
remote ref (transient fetch failures keep their own error). Validate
the archive root exists so WS/HTTP archive no longer reports success
for unknown task IDs, CAS-guard manual unarchive on an empty cascade
stamp, detach branch recovery from the request context, and skip
checkout_branch restore for multi-branch repos. UI: in-flight guard on
the list unarchive button, pluralized unrecoverable-branch toast.

* docs: document CAS guard on UnarchiveTask interface contract

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-14 19:15:51 +01:00

52 lines
1.5 KiB
TypeScript

"use client";
import { useState } from "react";
import { IconArchiveOff, IconLoader } from "@tabler/icons-react";
import { Button } from "@kandev/ui/button";
import { unarchiveTask } from "@/lib/api";
import { unarchiveToastPayload } from "@/lib/tasks/unarchive-feedback";
import { useToast } from "@/components/toast-provider";
// Shown in the task top bar when the task is archived. On success the
// backend publishes task.updated with archived_at=null, which restores the
// task in the kanban/store — no manual refetch needed here.
export function TaskUnarchiveButton({ taskId }: { taskId?: string | null }) {
const { toast } = useToast();
const [isPending, setIsPending] = useState(false);
if (!taskId) return null;
const handleClick = async () => {
setIsPending(true);
try {
const result = await unarchiveTask(taskId);
toast(unarchiveToastPayload(result));
} catch (err) {
toast({
title: "Failed to unarchive task",
description: err instanceof Error ? err.message : "Unknown error",
variant: "error",
});
} finally {
setIsPending(false);
}
};
return (
<Button
size="sm"
variant="outline"
className="h-7 cursor-pointer px-2"
disabled={isPending}
onClick={handleClick}
data-testid="task-unarchive-button"
>
{isPending ? (
<IconLoader className="h-3.5 w-3.5 animate-spin" />
) : (
<IconArchiveOff className="h-3.5 w-3.5" />
)}
Unarchive
</Button>
);
}