import { THEME_CSS, escapeHtml, escapeAttr, tabNavHTML } from "../render/layout"; import { countProjectAdmins, listProjectMembersWithUsers, listUsers, type ProjectMemberWithUser, type ProjectRole, type ProjectRow, type UserRow, } from "../store"; import { projectSettingsTabsHTML } from "./projectUpstreams"; interface Flash { kind: "ok" | "err"; msg: string; } const ALL_ROLES: ProjectRole[] = ["reader", "writer", "admin"]; function roleBadge(role: ProjectRole): string { const cls = role === "admin" ? "admin" : role === "writer" ? "writer" : "reader"; return `${escapeHtml(role)}`; } export async function buildProjectMembersPage( viewer: UserRow, project: ProjectRow, flash: Flash | null, ): Promise { const members = await listProjectMembersWithUsers(project.id); const users = await listUsers(); const memberLogins = new Set(members.map((m) => m.user_login)); const candidates = users.filter((u) => u.is_active === 1 && !memberLogins.has(u.login)); const flashHtml = flash ? `${escapeHtml(flash.msg)}` : ""; const memberRows = await Promise.all(members.map((m) => memberRowHtml(m, viewer, project))); const rowsHtml = members.length ? ` 用户角色加入时间操作 ${memberRows.join("")} ` : `该项目还没有成员记录(site-admin 已自动获得 admin 权限,可在下方添加)。`; const candOptions = candidates .map((u) => `${escapeHtml(u.login)} (${escapeHtml(u.kind)})`) .join(""); const addFormHtml = candidates.length ? ` 添加成员 用户${candOptions} 角色${ALL_ROLES.map((r) => `${r}`).join("")} reader = 只读(预留);writer = 可创建 issue、评论、关闭、上传;admin = 可管理 webhook 和成员。 添加 ` : `添加成员没有可添加的候选用户。先在 用户管理 创建账户。`; return ` 成员 · ${escapeHtml(project.owner)}/${escapeHtml(project.name)} 👥 ${escapeHtml(project.owner)}/${escapeHtml(project.name)} · 成员 ${tabNavHTML("projects")} ${escapeHtml(project.owner)}/${escapeHtml(project.name)} · 成员 ${projectSettingsTabsHTML(project.owner, project.name, "members")} ${flashHtml} 可见性 项目可见性 🌐 公开 — 所有登录用户可见 🔒 私有 — 仅项目成员可见 公开 = 任何登录用户都能查看 issue 和评论;私有 = 需要项目成员(reader+)权限。Bot 用户已自动获得 writer 角色,不受影响。 保存 当前成员(${members.length}) ${rowsHtml} ${addFormHtml} `; } async function memberRowHtml(m: ProjectMemberWithUser, viewer: UserRow, project: ProjectRow): Promise { const isSelf = m.user_login === viewer.login; const isAdminCountOne = m.role === "admin" && (await countProjectAdmins(project.id)) <= 1; const kindBadge = m.user_kind === "bot" ? `bot` : m.user_kind === "system" ? `system` : ""; const inactiveBadge = m.user_is_active === 1 ? "" : `禁用`; const selfNote = isSelf ? `(你)` : ""; const disableRole = isSelf && isAdminCountOne; const disableRemove = isAdminCountOne; const roleSelect = ` ${ALL_ROLES.map((r) => `${r}`).join("")} ${disableRole ? `🔒` : ""} `; const removeBtn = ` 移除 `; return ` ${escapeHtml(m.user_login)} ${selfNote} ${kindBadge} ${inactiveBadge} ${roleBadge(m.role)} ${escapeHtml((m.created_at || "").slice(0, 10))} ${roleSelect} ${removeBtn} `; }