Added delete version
Deploy / deploy (push) Successful in 2m28s

This commit is contained in:
twotalesanimation
2026-05-20 21:53:18 +02:00
parent 05475a6c19
commit e4eb8fb3a9
3 changed files with 102 additions and 0 deletions
+42
View File
@@ -165,6 +165,48 @@ export async function shareVersionWithClient(versionId: string) {
return { success: true }; return { success: true };
} }
export async function deleteVersion(versionId: string) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
if (!["ADMIN", "PRODUCER", "SUPERVISOR"].includes(session.user.role)) {
throw new Error("Insufficient permissions");
}
const version = await db.version.findUnique({
where: { id: versionId },
select: {
id: true,
isLatest: true,
taskId: true,
task: { select: { projectId: true, shotId: true } },
},
});
if (!version) throw new Error("Version not found");
// If this was the latest version, promote the next most recent one
if (version.isLatest && version.taskId) {
const next = await db.version.findFirst({
where: { taskId: version.taskId, id: { not: versionId } },
orderBy: { versionNumber: "desc" },
select: { id: true },
});
if (next) {
await db.version.update({ where: { id: next.id }, data: { isLatest: true } });
}
}
await db.version.delete({ where: { id: versionId } });
const projectId = version.task?.projectId;
const shotId = version.task?.shotId;
if (shotId) await recalcShotStatus(shotId).catch(() => {});
if (projectId) revalidatePath(`/projects/${projectId}`);
if (version.taskId) revalidatePath(`/tasks/${version.taskId}`);
if (shotId && projectId) revalidatePath(`/projects/${projectId}/shots/${shotId}`);
return { success: true };
}
export async function getVersionById(versionId: string) { export async function getVersionById(versionId: string) {
const session = await auth(); const session = await auth();
if (!session?.user) throw new Error("Unauthorized"); if (!session?.user) throw new Error("Unauthorized");
@@ -41,9 +41,11 @@ import {
AlertCircle, AlertCircle,
User, User,
ExternalLink, ExternalLink,
Trash2,
} from "lucide-react"; } from "lucide-react";
import { TASK_STATUS_CONFIG, TASK_TYPE_LABELS } from "@/components/tasks/TaskCard"; import { TASK_STATUS_CONFIG, TASK_TYPE_LABELS } from "@/components/tasks/TaskCard";
import { VersionUpload } from "@/components/versions/VersionUpload"; import { VersionUpload } from "@/components/versions/VersionUpload";
import { deleteVersion } from "@/actions/versions";
import type { CommentWithReplies } from "@/types"; import type { CommentWithReplies } from "@/types";
import { CommentPanel } from "@/components/comments/CommentPanel"; import { CommentPanel } from "@/components/comments/CommentPanel";
@@ -155,6 +157,17 @@ export function TaskDetailClient({
} }
}; };
const handleDeleteVersion = async (versionId: string, label: string) => {
if (!confirm(`Delete ${label}? This will permanently remove the file and all comments.`)) return;
try {
await deleteVersion(versionId);
toast({ title: "Version deleted" });
router.refresh();
} catch (e) {
toast({ title: "Delete failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" });
}
};
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<div className="max-w-5xl mx-auto p-6 space-y-6"> <div className="max-w-5xl mx-auto p-6 space-y-6">
@@ -298,6 +311,21 @@ export function TaskDetailClient({
Review Review
</Button> </Button>
</Link> </Link>
{canManage && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground hover:text-red-500"
onClick={() =>
handleDeleteVersion(
v.id,
`v${String(v.versionNumber).padStart(3, "0")}`
)
}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
)}
</div> </div>
</div> </div>
))} ))}
+32
View File
@@ -32,9 +32,11 @@ import {
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
MessageSquare, MessageSquare,
Trash2,
} from "lucide-react"; } from "lucide-react";
import type { VersionWithDetails } from "@/types"; import type { VersionWithDetails } from "@/types";
import { submitApproval } from "@/actions/approvals"; import { submitApproval } from "@/actions/approvals";
import { deleteVersion } from "@/actions/versions";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
interface VersionListProps { interface VersionListProps {
@@ -43,6 +45,7 @@ interface VersionListProps {
currentVersionId?: string; currentVersionId?: string;
onVersionSelect?: (versionId: string) => void; onVersionSelect?: (versionId: string) => void;
canApprove?: boolean; canApprove?: boolean;
canManage?: boolean;
} }
const APPROVAL_STATUS_STYLES: Record<string, string> = { const APPROVAL_STATUS_STYLES: Record<string, string> = {
@@ -75,6 +78,7 @@ export function VersionList({
currentVersionId, currentVersionId,
onVersionSelect, onVersionSelect,
canApprove = false, canApprove = false,
canManage = false,
}: VersionListProps) { }: VersionListProps) {
const [expanded, setExpanded] = useState<string | null>(currentVersionId ?? null); const [expanded, setExpanded] = useState<string | null>(currentVersionId ?? null);
const { toast } = useToast(); const { toast } = useToast();
@@ -109,6 +113,17 @@ export function VersionList({
} }
}; };
const handleDelete = async (versionId: string, label: string) => {
if (!confirm(`Delete ${label}? This will permanently remove the file and all comments.`)) return;
try {
await deleteVersion(versionId);
toast({ title: "Version deleted" });
router.refresh();
} catch (e) {
toast({ title: "Delete failed", description: e instanceof Error ? e.message : undefined, variant: "destructive" });
}
};
return ( return (
<div className="space-y-2"> <div className="space-y-2">
{versions.map((version) => { {versions.map((version) => {
@@ -220,6 +235,23 @@ export function VersionList({
</DropdownMenuItem> </DropdownMenuItem>
</> </>
)} )}
{canManage && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-500 focus:text-red-500"
onClick={() =>
handleDelete(
version.id,
`v${String(version.versionNumber).padStart(3, "0")}`
)
}
>
<Trash2 className="h-3.5 w-3.5 mr-2" />
Delete version
</DropdownMenuItem>
</>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>