"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import {
answerInvitation,
cancelInvitation,
createOrg,
deleteOrg,
inviteMember,
isOrgRole,
OrgError,
removeMember,
removeOrgAvatar,
ROLE_INFO,
setMemberRole,
setOrgAvatar,
updateOrgProfile,
} from "@/lib/orgs";
import { ActionError, orgForAction, viewerOrThrow } from "./guard";
import type { FormState } from "./types";
async function attempt(fn: () => Promise<FormState | void>): Promise<FormState> {
try {
return (await fn()) ?? undefined;
} catch (e) {
if (e instanceof OrgError || e instanceof ActionError) return { error: e.message };
throw e;
}
}
function roleOf(formData: FormData) {
const role = formData.get("role");
if (!isOrgRole(role)) throw new ActionError("Choose a role.");
return role;
}
// Memberships change what's in everyone's sidebar and which repositories they see.
const refresh = () => revalidatePath("/", "layout");
export async function createOrgAction(_prev: FormState, formData: FormData): Promise<FormState> {
let path = "";
const result = await attempt(async () => {
const viewer = await viewerOrThrow();
const org = await createOrg(viewer, {
name: String(formData.get("name") ?? ""),
displayName: String(formData.get("displayName") ?? ""),
description: String(formData.get("description") ?? ""),
});
path = `/organizations/${org.username}/settings/members`;
});
if (result?.error) return result;
refresh();
redirect(path);
}
export async function updateOrgAction(name: string, _prev: FormState, formData: FormData) {
return attempt(async () => {
const { org } = await orgForAction(name, "owner");
await updateOrgProfile(org.id, {
displayName: String(formData.get("displayName") ?? ""),
description: String(formData.get("description") ?? ""),
});
revalidatePath(`/${org.username}`);
return { success: "Profile updated." };
});
}
export async function updateOrgAvatarAction(name: string, _prev: FormState, formData: FormData) {
return attempt(async () => {
const { org } = await orgForAction(name, "owner");
const file = formData.get("image");
if (!(file instanceof File)) throw new OrgError("Choose an image to upload.");
await setOrgAvatar(org.id, new Uint8Array(await file.arrayBuffer()));
// The avatar shows up in sidebars and lists everywhere, beside the organization's repositories too.
refresh();
return { success: "Avatar updated." };
});
}
export async function removeOrgAvatarAction(name: string) {
const { org } = await orgForAction(name, "owner");
await removeOrgAvatar(org.id);
refresh();
}
export async function deleteOrgAction(name: string, _prev: FormState, formData: FormData) {
const result = await attempt(async () => {
const { org } = await orgForAction(name, "owner");
if (formData.get("confirm") !== org.username) return { error: `Type ${org.username} to confirm.` };
await deleteOrg(org);
});
if (result?.error) return result;
refresh();
redirect("/settings/organizations");
}
export async function inviteMemberAction(name: string, _prev: FormState, formData: FormData) {
return attempt(async () => {
const { org, viewer } = await orgForAction(name, "owner");
const role = roleOf(formData);
const user = await inviteMember(org, viewer, String(formData.get("username") ?? ""), role);
revalidatePath(`/organizations/${org.username}/settings/members`);
return { success: `Invited ${user.username} as ${ROLE_INFO[role].title.toLowerCase()}. They’ll find the invitation on their dashboard.` };
});
}
export async function cancelInvitationAction(name: string, invitationId: string) {
const { org } = await orgForAction(name, "owner");
await cancelInvitation(org.id, String(invitationId));
revalidatePath(`/organizations/${org.username}/settings/members`);
}
export async function setMemberRoleAction(name: string, userId: string, _prev: FormState, formData: FormData) {
return attempt(async () => {
const { org } = await orgForAction(name, "owner");
const role = roleOf(formData);
await setMemberRole(org.id, String(userId), role);
refresh();
return { success: `Role changed to ${ROLE_INFO[role].title.toLowerCase()}.` };
});
}
export async function removeMemberAction(name: string, userId: string): Promise<FormState> {
let left = false;
const result = await attempt(async () => {
const { org, viewer } = await orgForAction(name, "owner");
await removeMember(org.id, String(userId));
left = userId === viewer.id;
});
if (result?.error) return result;
refresh();
if (left) redirect("/settings/organizations");
return { success: "Member removed." };
}
/** Any member can leave, unless they're its last owner. */
export async function leaveOrgAction(name: string): Promise<FormState> {
const result = await attempt(async () => {
const { org, viewer } = await orgForAction(name, "read");
await removeMember(org.id, viewer.id);
});
if (result?.error) return result;
refresh();
return { success: `You left ${name}.` };
}
export async function answerInvitationAction(invitationId: string, accept: boolean): Promise<FormState> {
accept = accept === true;
let path = "";
const result = await attempt(async () => {
const viewer = await viewerOrThrow();
const invitation = await answerInvitation(viewer.id, String(invitationId), accept);
if (accept) path = `/${invitation.org.username}`;
});
if (result?.error) return result;
refresh();
if (path) redirect(path);
return { success: "Invitation declined." };
}