#669 display resource admin (#677)

Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
lethemanh
2026-03-20 18:01:04 +07:00
committed by GitHub
parent 2ba92ed194
commit 5f26bef4dc
7 changed files with 252 additions and 52 deletions
@@ -17,11 +17,12 @@ import {
} from "@linagora/twake-mui";
import HighlightOffIcon from "@mui/icons-material/HighlightOff";
import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useI18n } from "twake-i18n";
import { PeopleSearch, User } from "../Attendees/PeopleSearch";
import { FieldWithLabel } from "../Event/components/FieldWithLabel";
import { stringAvatar } from "../Event/utils/eventUtils";
import { ResourceAdmin } from "./ResourceAdmins";
export interface UserWithAccess extends User {
accessRight: AccessRight;
@@ -34,6 +35,11 @@ interface CalendarAccessRightsProps {
onInvitesLoaded: (users: UserWithAccess[]) => void;
}
interface UserInCalendar {
id: string;
access: AccessRight;
}
export function CalendarAccessRights({
calendar,
value: usersWithAccess,
@@ -57,6 +63,8 @@ export function CalendarAccessRights({
const [searchWidth, setSearchWidth] = useState<number | undefined>(undefined);
const [accessRight, setAccessRight] = useState<AccessRight>(2);
const [inviteLoading, setInvitesLoading] = useState(false);
const [resourceAdmins, setResourceAdmins] = useState<UserWithAccess[]>([]);
const [adminLoading, setAdminLoading] = useState(false);
const currentUsersRef = useRef<UserWithAccess[]>(usersWithAccess);
useEffect(() => {
@@ -74,6 +82,39 @@ export function CalendarAccessRights({
return () => observer.disconnect();
}, []);
const handleLoadUsers = useCallback(
async (usersInCal: UserInCalendar[], cancelled: boolean) => {
const results = await Promise.allSettled(
usersInCal.map(async (user) => {
const details = await getUserDetails(user.id);
const email = details?.preferredEmail ?? details?.emails?.[0] ?? "";
return {
openpaasId: user.id,
displayName:
[details?.firstname, details?.lastname]
.filter(Boolean)
.join(" ")
.trim() || email,
email,
accessRight: user.access as AccessRight,
} satisfies UserWithAccess;
})
);
if (cancelled) {
return [] as UserWithAccess[];
}
const loaded: UserWithAccess[] = results
.filter((r) => r.status === "fulfilled")
.map((r) => (r as PromiseFulfilledResult<UserWithAccess>).value)
.filter(Boolean) as UserWithAccess[];
return loaded;
},
[]
);
useEffect(() => {
if (!calendar.invite?.length) return;
@@ -82,33 +123,19 @@ export function CalendarAccessRights({
async function loadInvitedUsers() {
setInvitesLoading(true);
try {
const loaded: UserWithAccess[] = (
await Promise.all(
calendar.invite.map(async (invite) => {
const principalId = invite.principal.split("/").pop();
if (!principalId) return null;
try {
const details = await getUserDetails(principalId);
const email =
details?.preferredEmail ?? details?.emails?.[0] ?? "";
return {
openpaasId: principalId,
displayName:
[details?.firstname, details?.lastname]
.filter(Boolean)
.join(" ")
.trim() || email,
email,
accessRight: invite.access as AccessRight,
} satisfies UserWithAccess;
} catch {
return null;
}
})
)
).filter((u) => u !== null && !!u.email);
const usersInCal = (calendar.invite
?.map((invite) => {
const principalId = invite.principal.split("/").pop();
if (!principalId) return null;
if (cancelled) return;
return {
id: principalId,
access: invite.access,
};
})
?.filter((invite) => !!invite) || []) satisfies UserInCalendar[];
const loaded = await handleLoadUsers(usersInCal, cancelled);
const loadedIds = new Set(loaded.map((u) => normalizeEmail(u.email)));
const manuallyAdded = currentUsersRef.current.filter(
@@ -127,7 +154,42 @@ export function CalendarAccessRights({
return () => {
cancelled = true;
};
}, [calendar.invite, onChange, onInvitesLoaded]);
}, [calendar.invite, handleLoadUsers, onChange, onInvitesLoaded]);
useEffect(() => {
const isResource = calendar.owner.resource;
const resourceAdmins = calendar.owner.administrators || [];
if (!isResource || !resourceAdmins?.length) return;
let cancelled = false;
async function loadAdmins() {
try {
setAdminLoading(true);
const resourceAdminsWithoutOwner = resourceAdmins
.filter((admin) => admin.id !== calendar.owner._id)
.map((admin) => ({
id: admin.id,
access: 5, // ADMIN
})) satisfies UserInCalendar[];
const admins = await handleLoadUsers(
resourceAdminsWithoutOwner,
cancelled
);
setResourceAdmins(admins);
} catch (error) {
console.error(error);
} finally {
if (!cancelled) setAdminLoading(false);
}
}
loadAdmins();
return () => {
cancelled = true;
};
}, [calendar.owner, handleLoadUsers, setResourceAdmins]);
const handleUserSelect = (_event: unknown, users: User[]) => {
const updated: UserWithAccess[] = users.map((user) => {
@@ -309,6 +371,15 @@ export function CalendarAccessRights({
</Typography>
</Box>
</Box>
{adminLoading ? (
<Box mt={2} display="flex" justifyContent="center">
<CircularProgress size={24} />
</Box>
) : (
resourceAdmins.map((admin) => (
<ResourceAdmin key={admin.email} admin={admin} />
))
)}
{inviteLoading ? (
<Box mt={2} display="flex" justifyContent="center">
<CircularProgress size={24} />
@@ -0,0 +1,45 @@
import { Avatar, Box, Typography } from "@linagora/twake-mui";
import { useI18n } from "twake-i18n";
import { stringAvatar } from "../Event/utils/eventUtils";
import { UserWithAccess } from "./CalendarAccessRights";
interface ResourceAdminProps {
admin: UserWithAccess;
}
export function ResourceAdmin({ admin }: ResourceAdminProps) {
const { t } = useI18n();
return (
<Box
display="flex"
alignItems="center"
justifyContent="space-between"
px={1}
py={0.5}
sx={{
borderRadius: "8px",
"&:hover": { backgroundColor: "action.hover" },
}}
>
<Box display="flex" alignItems="center" gap={1.5} minWidth={0}>
<Avatar
{...stringAvatar(admin.displayName)}
sx={{ width: 28, height: 28, fontSize: "0.875rem" }}
/>
<Box minWidth={0} display="flex" flexDirection="column" gap={0}>
<Typography noWrap>{admin.displayName}</Typography>
<Typography variant="caption" color="text.secondary">
{admin.email}
</Typography>
</Box>
</Box>
<Box display="flex" alignItems="center" gap={0.5} flexShrink={0}>
<Typography variant="caption">
{t("calendarPopover.access.administrator")}
</Typography>
</Box>
</Box>
);
}