"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import type { ActionResult, IssueLabel, IssuePatch, NewIssueInput } from "@/lib/issue-meta";
import {
addComment,
createIssue,
createLabel,
deleteIssue,
deleteLabel,
getIssue,
IssueError,
LabelError,
updateIssue,
validateIssuePatch,
} from "@/lib/issues";
import { person } from "@/lib/actor";
import { canWrite, type RepoRecord } from "@/lib/repos";
import { ActionError, repoForAction } from "./guard";
import type { FormState } from "./types";
const MAX_BODY = 65_536;
function revalidateIssues(repo: RepoRecord) {
revalidatePath(`/${repo.owner}/${repo.name}`, "layout");
}
// Permissions: anyone who can read a repository may open issues and comment.
// Changing an issue in any way (title, description, status, triage, order) needs
// push access. Deleting is reserved for the issue's creator and the repository owner.
export async function createIssueAction(
owner: string,
name: string,
input: NewIssueInput,
): Promise<ActionResult & { number?: number }> {
try {
const { repo, viewer, access } = await repoForAction(owner, name, "read");
const { title, body, ...properties } = await validateIssuePatch(repo, { ...input, sortOrder: undefined });
if (!title) return { error: "Title can't be blank." };
const number = await createIssue(repo, person(viewer.id), {
title,
body: body ?? "",
...(canWrite(access) ? properties : {}),
});
revalidateIssues(repo);
return { number };
} catch (e) {
if (e instanceof ActionError || e instanceof IssueError) return { error: e.message };
throw e;
}
}
export async function updateIssueAction(owner: string, name: string, number: number, patch: IssuePatch): Promise<ActionResult> {
try {
const { repo, viewer, access } = await repoForAction(owner, name, "read");
const issue = await getIssue(repo.id, number);
if (!issue) return { error: "Issue not found." };
if (!canWrite(access)) return { error: "Only collaborators can change issues. You can still comment." };
const clean = await validateIssuePatch(repo, patch);
await updateIssue(repo, issue, clean, person(viewer.id));
revalidateIssues(repo);
return {};
} catch (e) {
if (e instanceof ActionError || e instanceof IssueError) return { error: e.message };
throw e;
}
}
export async function commentAction(owner: string, name: string, number: number, formData: FormData) {
const { repo, viewer } = await repoForAction(owner, name, "read");
const issue = await getIssue(repo.id, number);
if (!issue) throw new ActionError("Issue not found");
const body = String(formData.get("body") ?? "").trim().slice(0, MAX_BODY);
if (body) await addComment(repo, issue, person(viewer.id), body);
revalidateIssues(repo);
}
export async function deleteIssueAction(owner: string, name: string, number: number) {
const { repo, viewer, access } = await repoForAction(owner, name, "read");
const issue = await getIssue(repo.id, number);
if (!issue) throw new ActionError("Issue not found.");
if (issue.authorId !== viewer.id && access !== "admin") {
throw new ActionError("Only the issue's creator and the repository owner can delete it.");
}
await deleteIssue(repo, issue, person(viewer.id));
revalidateIssues(repo);
redirect(`/${repo.owner}/${repo.name}/issues`);
}
export async function createLabelAction(
owner: string,
name: string,
input: { name: string; color: string },
): Promise<ActionResult & { label?: IssueLabel }> {
try {
const { repo } = await repoForAction(owner, name, "write");
const label = await createLabel(repo.id, { name: String(input.name ?? ""), color: String(input.color ?? "") });
revalidateIssues(repo);
return { label };
} catch (e) {
if (e instanceof ActionError || e instanceof LabelError) return { error: e.message };
throw e;
}
}
/** For the labels settings form. */
export async function createLabelFormAction(owner: string, name: string, _prev: FormState, formData: FormData): Promise<FormState> {
const result = await createLabelAction(owner, name, {
name: String(formData.get("name") ?? ""),
color: String(formData.get("color") ?? ""),
});
return result.error ? { error: result.error } : { success: `Created label "${result.label!.name}".` };
}
export async function deleteLabelAction(owner: string, name: string, labelId: string) {
const { repo } = await repoForAction(owner, name, "write");
await deleteLabel(repo.id, String(labelId));
revalidateIssues(repo);
}