feat(actions): subtask and owner comment mutations

This commit is contained in:
Dave 2026-09-16 10:59:37 +02:00
parent 32927ed350
commit 1f1f9854ff
2 changed files with 146 additions and 0 deletions

View file

@ -0,0 +1,49 @@
"use server";
import { revalidatePath } from "next/cache";
import { getServerPb } from "@/lib/pb/server";
import { requireSession } from "@/lib/auth/session";
import { logActivity } from "@/app/actions/activity";
import type { CardRecord } from "@/lib/pb/types";
/**
* The OWNER's comment path only.
*
* A client manager comments through POST /api/client/cards/:id/comments, built
* in Plan 1a. Do not route them here: that route verifies the card belongs to
* their client and runs under a superuser with an explicit whitelist, which is
* the boundary this action deliberately does not have.
*/
export async function addOwnerComment(input: {
cardId: string;
body: string;
clientSlug: string;
projectSlug: string;
}): Promise<void> {
const session = await requireSession();
if (session.role !== "owner") throw new Error("Not found");
const body = input.body.trim().slice(0, 20000);
if (!body) return;
const pb = await getServerPb();
const card = await pb.collection("cards").getOne<CardRecord>(input.cardId);
await pb.collection("comments").create({
card: input.cardId,
author: session.userId,
body,
});
await logActivity({
project: card.project,
card: card.id,
actor: session.userId,
action: "comment_added",
});
revalidatePath(
`/p/${input.clientSlug}/${input.projectSlug}/c/${input.cardId}`
);
}

View file

@ -0,0 +1,97 @@
"use server";
import { revalidatePath } from "next/cache";
import { getServerPb } from "@/lib/pb/server";
import { requireSession } from "@/lib/auth/session";
import { logActivity } from "@/app/actions/activity";
import { positionBetween } from "@/lib/ordering/fractional";
import type { CardRecord, SubtaskRecord } from "@/lib/pb/types";
/** See the security note at the top of src/app/actions/cards.ts. */
async function requireOwner() {
const session = await requireSession();
if (session.role !== "owner") throw new Error("Not found");
return session;
}
function cardPath(clientSlug: string, projectSlug: string, cardId: string) {
return `/p/${clientSlug}/${projectSlug}/c/${cardId}`;
}
export async function addSubtask(input: {
cardId: string;
title: string;
clientSlug: string;
projectSlug: string;
}): Promise<void> {
await requireOwner();
const pb = await getServerPb();
const title = input.title.trim().slice(0, 200);
if (!title) return;
const last = await pb
.collection("subtasks")
.getFirstListItem<SubtaskRecord>(`card = "${input.cardId}"`, {
sort: "-position",
})
.catch(() => null);
await pb.collection("subtasks").create({
card: input.cardId,
title,
done: false,
position: positionBetween(last ? last.position : null, null),
});
revalidatePath(cardPath(input.clientSlug, input.projectSlug, input.cardId));
}
export async function toggleSubtask(input: {
subtaskId: string;
done: boolean;
clientSlug: string;
projectSlug: string;
}): Promise<void> {
const session = await requireOwner();
const pb = await getServerPb();
const subtask = await pb
.collection("subtasks")
.getOne<SubtaskRecord>(input.subtaskId);
await pb.collection("subtasks").update(input.subtaskId, { done: input.done });
const card = await pb.collection("cards").getOne<CardRecord>(subtask.card);
await logActivity({
project: card.project,
card: card.id,
actor: session.userId,
action: "subtask_toggled",
// `field` carries the subtask's title so the timeline can name it without
// a second lookup, and still reads correctly after the subtask is deleted.
field: subtask.title,
newValue: String(input.done),
});
revalidatePath(cardPath(input.clientSlug, input.projectSlug, subtask.card));
}
export async function deleteSubtask(input: {
subtaskId: string;
clientSlug: string;
projectSlug: string;
}): Promise<void> {
await requireOwner();
const pb = await getServerPb();
const subtask = await pb
.collection("subtasks")
.getOne<SubtaskRecord>(input.subtaskId);
await pb.collection("subtasks").delete(input.subtaskId);
revalidatePath(cardPath(input.clientSlug, input.projectSlug, subtask.card));
}