feat(actions): subtask and owner comment mutations
This commit is contained in:
parent
32927ed350
commit
1f1f9854ff
2 changed files with 146 additions and 0 deletions
49
src/app/actions/comments.ts
Normal file
49
src/app/actions/comments.ts
Normal 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}`
|
||||
);
|
||||
}
|
||||
97
src/app/actions/subtasks.ts
Normal file
97
src/app/actions/subtasks.ts
Normal 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));
|
||||
}
|
||||
Loading…
Reference in a new issue