"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { createList, deleteList, findList, findListById, ListError, listPath, mayManageLists, setInList, setLiked, updateList, type RepoList } from "@/lib/lists";
import { findRepoById, repoAccess } from "@/lib/repos";
import { getUserByUsername } from "@/lib/users";
import { ActionError, repoForAction, viewerOrThrow } from "./guard";
import type { FormState } from "./types";
// Anyone signed in likes the repositories they can read, and saves them to their own lists and to
// those of organizations they can push in.
async function attempt(fn: () => Promise<FormState | void>): Promise<FormState> {
try {
return (await fn()) ?? undefined;
} catch (e) {
if (e instanceof ListError || e instanceof ActionError) return { error: e.message };
throw e;
}
}
function listInput(formData: FormData) {
return {
name: String(formData.get("name") ?? ""),
description: String(formData.get("description") ?? ""),
visibility: formData.get("visibility") === "private" ? ("private" as const) : ("public" as const),
};
}
/** A list the viewer may change. */
async function listForAction(list: RepoList | null) {
const viewer = await viewerOrThrow();
if (!list || !(await mayManageLists(list.owner, viewer.id))) throw new ActionError("List not found.");
return { list, viewer };
}
/** A repository the viewer can read, by id. */
async function readableRepo(repoId: string, viewerId: string) {
const repo = await findRepoById(String(repoId));
if (!repo || !(await repoAccess(repo, viewerId))) throw new ActionError("Repository not found.");
return repo;
}
const refreshList = (list: RepoList) => {
revalidatePath(listPath(list));
revalidatePath(`/${list.owner.username}`);
};
export async function setLikedAction(owner: string, name: string, liked: boolean): Promise<FormState> {
return attempt(async () => {
const { repo, viewer } = await repoForAction(owner, name, "read");
await setLiked(repo, viewer, liked === true);
revalidatePath(`/${repo.owner}/${repo.name}`, "layout");
revalidatePath(`/${viewer.username}`);
});
}
/** Puts a repository in one of the viewer's lists (or an organization's), or takes it out. */
export async function setSavedAction(listId: string, repoId: string, saved: boolean): Promise<FormState> {
return attempt(async () => {
const { list, viewer } = await listForAction(await findListById(String(listId)));
const repo = await readableRepo(repoId, viewer.id);
await setInList(list, repo.id, saved === true);
refreshList(list);
revalidatePath(`/${repo.owner}/${repo.name}`, "layout");
return { success: saved ? `Saved to ${list.name}.` : `Removed from ${list.name}.` };
});
}
/**
* Creates a list. From a repository's Save menu (with `repo`, a repository id) the repository goes
* in it at once and the menu stays open; otherwise the new list's page opens.
*/
export async function createListAction(_prev: FormState, formData: FormData): Promise<FormState> {
let path = "";
const repoId = String(formData.get("repo") ?? "");
const result = await attempt(async () => {
const viewer = await viewerOrThrow();
const owner = await getUserByUsername(String(formData.get("owner") ?? viewer.username));
if (!owner || !(await mayManageLists(owner, viewer.id))) throw new ActionError("You can’t make lists for that account.");
const repo = repoId ? await readableRepo(repoId, viewer.id) : null;
const list = await createList(owner, listInput(formData));
if (repo) {
await setInList(list, repo.id, true);
revalidatePath(`/${repo.owner}/${repo.name}`, "layout");
}
refreshList(list);
path = listPath(list);
return repo ? { success: `Saved to ${list.name}.` } : undefined;
});
if (result?.error || repoId) return result;
redirect(path);
}
export async function updateListAction(owner: string, slug: string, _prev: FormState, formData: FormData): Promise<FormState> {
let path = "";
const result = await attempt(async () => {
const { list } = await listForAction(await findList(owner, slug));
const updated = await updateList(list, listInput(formData));
refreshList(list);
refreshList(updated);
path = listPath(updated);
});
if (result?.error) return result;
redirect(path);
}
export async function deleteListAction(owner: string, slug: string, _prev: FormState, formData: FormData): Promise<FormState> {
let path = "";
const result = await attempt(async () => {
const { list } = await listForAction(await findList(owner, slug));
if (formData.get("confirm") !== list.name) return { error: `Type ${list.name} to confirm.` };
await deleteList(list);
refreshList(list);
path = `/${list.owner.username}?tab=lists`;
});
if (result?.error) return result;
redirect(path);
}