Vectrum
Sign In
124 lines · 4.6 KB · TypeScript
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import {
  addCollaborator,
  createRepo,
  deleteRepo,
  removeCollaborator,
  removeRepoImage,
  renameRepo,
  RepoError,
  setRepoImage,
  setWikiEnabled,
  updateRepo,
  type Visibility,
} from "@/lib/repos";
import { ActionError, repoForAction, viewerOrThrow } from "./guard";
import type { FormState } from "./types";

const visibilityOf = (v: FormDataEntryValue | null): Visibility => (v === "private" ? "private" : "public");

async function attempt(fn: () => Promise<FormState | void> | FormState | void): Promise<FormState> {
  try {
    return (await fn()) ?? undefined;
  } catch (e) {
    if (e instanceof RepoError || e instanceof ActionError) return { error: e.message };
    throw e;
  }
}

export async function createRepoAction(_prev: FormState, formData: FormData): Promise<FormState> {
  let path = "";
  const result = await attempt(async () => {
    const viewer = await viewerOrThrow();
    const repo = await createRepo(viewer, {
      name: String(formData.get("name") ?? ""),
      description: String(formData.get("description") ?? ""),
      visibility: visibilityOf(formData.get("visibility")),
      readme: formData.get("readme") === "on",
    });
    path = `/${repo.owner}/${repo.name}`;
  });
  if (result?.error) return result;
  redirect(path);
}

export async function updateRepoAction(owner: string, name: string, _prev: FormState, formData: FormData) {
  return attempt(async () => {
    const { repo } = await repoForAction(owner, name, "admin");
    await updateRepo(repo.id, {
      description: String(formData.get("description") ?? ""),
      visibility: visibilityOf(formData.get("visibility")),
    });
    revalidatePath(`/${repo.owner}/${repo.name}`, "layout");
    return { success: "Settings saved." };
  });
}

export async function updateFeaturesAction(owner: string, name: string, _prev: FormState, formData: FormData) {
  return attempt(async () => {
    const { repo } = await repoForAction(owner, name, "admin");
    await setWikiEnabled(repo.id, formData.get("wiki") === "on");
    revalidatePath(`/${repo.owner}/${repo.name}`, "layout");
    return { success: "Features saved." };
  });
}

export async function updateRepoImageAction(owner: string, name: string, _prev: FormState, formData: FormData) {
  return attempt(async () => {
    const { repo } = await repoForAction(owner, name, "admin");
    const file = formData.get("image");
    if (!(file instanceof File)) throw new RepoError("Choose an image to upload.");
    await setRepoImage(repo.id, new Uint8Array(await file.arrayBuffer()));
    // The image shows up in sidebars and lists everywhere, not just on this repository's pages.
    revalidatePath("/", "layout");
    return { success: "Repository image updated." };
  });
}

export async function removeRepoImageAction(owner: string, name: string) {
  const { repo } = await repoForAction(owner, name, "admin");
  await removeRepoImage(repo.id);
  revalidatePath("/", "layout");
}

export async function renameRepoAction(owner: string, name: string, _prev: FormState, formData: FormData) {
  let path = "";
  const result = await attempt(async () => {
    const { repo } = await repoForAction(owner, name, "admin");
    const renamed = await renameRepo(repo, String(formData.get("name") ?? ""));
    path = `/${renamed.owner}/${renamed.name}/settings`;
  });
  if (result?.error) return result;
  redirect(path);
}

export async function deleteRepoAction(owner: string, name: string, _prev: FormState, formData: FormData) {
  const result = await attempt(async () => {
    const { repo } = await repoForAction(owner, name, "admin");
    if (formData.get("confirm") !== `${repo.owner}/${repo.name}`) {
      return { error: `Type ${repo.owner}/${repo.name} to confirm.` };
    }
    await deleteRepo(repo);
  });
  if (result?.error) return result;
  redirect("/");
}

export async function addCollaboratorAction(owner: string, name: string, _prev: FormState, formData: FormData) {
  return attempt(async () => {
    const { repo } = await repoForAction(owner, name, "admin");
    const username = String(formData.get("username") ?? "").trim();
    await addCollaborator(repo, username);
    revalidatePath(`/${repo.owner}/${repo.name}/settings`);
    return { success: `${username} can now push to this repository.` };
  });
}

export async function removeCollaboratorAction(owner: string, name: string, userId: string) {
  const { repo } = await repoForAction(owner, name, "admin");
  await removeCollaborator(repo.id, String(userId));
  revalidatePath(`/${repo.owner}/${repo.name}/settings`);
}