97 lines
No EOL
2.7 KiB
TypeScript
97 lines
No EOL
2.7 KiB
TypeScript
"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));
|
|
} |