The Cascade Delete
Right now, deleting a project only deletes the project document. Its tasks become orphaned. They still exist in the tasks table, but their projectId points to a document that no longer exists. Let’s fix this by deleting the tasks too.
Update the Remove Mutation
The strategy is simple. In the mutation, query all the project’s tasks, delete them one by one, then delete the project itself.
Update convex/projects.ts:
export const remove = mutation({
args: {
id: v.id("projects"),
},
handler: async (ctx, args) => {
// Delete all tasks belonging to this project
const tasks = await ctx.db
.query("tasks")
.withIndex("by_project", (q) => q.eq("projectId", args.id))
.collect();
for (const task of tasks) {
await ctx.db.delete(task._id);
}
// Delete the project itself
await ctx.db.delete(args.id);
},
});
Wire Up the Delete Button
Let’s add a delete button to the project card. First, add src/components/delete-project-dialog.tsx with the following content:
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import type { Id } from "../../convex/_generated/dataModel";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Trash } from "lucide-react";
type DeleteProjectDialogProps = {
projectId: Id<"projects">;
projectName: string;
};
function DeleteProjectDialog({
projectId,
projectName,
}: DeleteProjectDialogProps) {
const removeProject = useMutation(api.projects.remove);
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon-sm">
<Trash className="w-4 h-4 text-destructive" />
</Button>
</DialogTrigger>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete{" "}
<span className="font-medium">{projectName}</span> and all of its
tasks.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button
variant="destructive"
onClick={() => removeProject({ id: projectId })}
>
Delete
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export default DeleteProjectDialog;
Then, update src/components/project-card.tsx to import and use the dialog:
import { Link } from "@tanstack/react-router";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { Doc } from "../../convex/_generated/dataModel";
import DeleteProjectDialog from "@/components/delete-project-dialog";
type ProjectCardProps = {
project: Doc<"projects">;
};
function ProjectCard({ project }: ProjectCardProps) {
return (
<Card className="transition-shadow hover:shadow-md">
<CardHeader>
<div className="flex items-center justify-between">
<Link to="/projects/$projectId" params={{ projectId: project._id }}>
<CardTitle className="text-base">{project.name}</CardTitle>
</Link>
<DeleteProjectDialog
projectId={project._id}
projectName={project.name}
/>
</div>
</CardHeader>
{project.description && (
<Link to="/projects/$projectId" params={{ projectId: project._id }}>
<CardContent>
<p className="line-clamp-2 text-sm text-muted-foreground">
{project.description}
</p>
</CardContent>
</Link>
)}
</Card>
);
}
export default ProjectCard;
Now run the app and try deleting a project with tasks. You will see a confirmation dialog telling you that the project and all of its tasks will be deleted and that the action cannot be undone. If you confirm, the mutation runs and deletes the project and its tasks. Open the Convex dashboard and check the projects and tasks tables to see the results.
Checkpoint: Commit your progress.
git add .
git commit -m "planner-09: Add cascade delete and confirmation dialog"
git push