feat(admin): streamline group management
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
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<string, string>,
|
||||
group: { id: string; isDefault: boolean },
|
||||
) {
|
||||
return group.isDefault ? !assignmentByUser[userId] : assignmentByUser[userId] === group.id;
|
||||
}
|
||||
|
||||
export function groupSlug(name: string, existingSlugs: Set<string>) {
|
||||
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");
|
||||
}
|
||||
Reference in New Issue
Block a user