7bcb10bfef
* 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>
52 lines
1.5 KiB
TypeScript
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>
|
|
);
|
|
}
|