const NAME_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/; const TEXT_CONTROL_CHARACTERS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/; export function validateGroupDetails(nameValue: unknown, descriptionValue: unknown) { const name = String(nameValue ?? "").trim(); const description = String(descriptionValue ?? "").trim(); if ( name.length < 1 || name.length > 50 || NAME_CONTROL_CHARACTERS.test(name) || description.length > 500 || TEXT_CONTROL_CHARACTERS.test(description) ) return null; return { name, description }; } export function adminGroupReturnPath( value: unknown, result: "saved=group" | "error=invalid-group-assignment", ) { const requested = String(value ?? ""); let pathname = "/admin/users"; const parameters = new URLSearchParams(); if (requested.startsWith("/")) { const url = new URL(requested, "http://internal"); if (url.pathname === "/admin/users") { const search = url.searchParams.get("q")?.trim().slice(0, 100); if (search) parameters.set("q", search); } else if (/^\/admin\/groups\/[0-9a-f-]{36}$/i.test(url.pathname)) { pathname = url.pathname; } } const [key, resultValue] = result.split("=", 2) as ["saved" | "error", string]; parameters.set(key, resultValue); return `${pathname}?${parameters.toString()}`; } export function editableGroupName(currentName: string, isDefault: boolean, requestedName: string) { return isDefault ? currentName : requestedName; } export function effectiveGroupMemberCount( totalUsers: number, assignedGroupIds: string[], group: { id: string; isDefault: boolean }, ) { return group.isDefault ? Math.max(0, totalUsers - assignedGroupIds.length) : assignedGroupIds.filter((groupId) => groupId === group.id).length; } export function isEffectiveGroupMember( userId: string, assignmentByUser: Record, group: { id: string; isDefault: boolean }, ) { return group.isDefault ? !assignmentByUser[userId] : assignmentByUser[userId] === group.id; } export function groupSlug(name: string, existingSlugs: Set) { const normalized = name .normalize("NFKD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "group"; const base = normalized.slice(0, 50).replace(/-+$/g, "") || "group"; if (!existingSlugs.has(base)) return base; for (let suffix = 2; suffix < 10_000; suffix += 1) { const suffixText = `-${suffix}`; const candidate = `${base.slice(0, 50 - suffixText.length).replace(/-+$/g, "")}${suffixText}`; if (!existingSlugs.has(candidate)) return candidate; } throw new Error("Could not generate a unique group slug"); }