Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -87,6 +87,8 @@ const baseCalendar: Calendar = {
|
|||||||
events: {},
|
events: {},
|
||||||
invite: [],
|
invite: [],
|
||||||
owner: { emails: ["user1@example.com"] },
|
owner: { emails: ["user1@example.com"] },
|
||||||
|
access: { write: true },
|
||||||
|
delegated: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("CalendarAccessRights", () => {
|
describe("CalendarAccessRights", () => {
|
||||||
@@ -102,7 +104,8 @@ describe("CalendarAccessRights", () => {
|
|||||||
value={[]}
|
value={[]}
|
||||||
onChange={mockOnChange}
|
onChange={mockOnChange}
|
||||||
onInvitesLoaded={mockOnInvitesLoaded}
|
onInvitesLoaded={mockOnInvitesLoaded}
|
||||||
/>
|
/>,
|
||||||
|
{ ...userState }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -113,7 +116,7 @@ describe("CalendarAccessRights", () => {
|
|||||||
it("shows a list of users already passed in via value prop", () => {
|
it("shows a list of users already passed in via value prop", () => {
|
||||||
const users: UserWithAccess[] = [
|
const users: UserWithAccess[] = [
|
||||||
{
|
{
|
||||||
openpaasId: "abc",
|
openpaasId: "user1",
|
||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
email: "alice@example.com",
|
email: "alice@example.com",
|
||||||
accessRight: 2,
|
accessRight: 2,
|
||||||
@@ -136,10 +139,10 @@ describe("CalendarAccessRights", () => {
|
|||||||
it("calls onChange with user removed when remove button is clicked", () => {
|
it("calls onChange with user removed when remove button is clicked", () => {
|
||||||
const users: UserWithAccess[] = [
|
const users: UserWithAccess[] = [
|
||||||
{
|
{
|
||||||
openpaasId: "abc",
|
openpaasId: "user1",
|
||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
email: "alice@example.com",
|
email: "alice@example.com",
|
||||||
accessRight: 2,
|
accessRight: 5,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -149,7 +152,8 @@ describe("CalendarAccessRights", () => {
|
|||||||
value={users}
|
value={users}
|
||||||
onChange={mockOnChange}
|
onChange={mockOnChange}
|
||||||
onInvitesLoaded={mockOnInvitesLoaded}
|
onInvitesLoaded={mockOnInvitesLoaded}
|
||||||
/>
|
/>,
|
||||||
|
{ ...userState }
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByLabelText(/remove/i));
|
fireEvent.click(screen.getByLabelText(/remove/i));
|
||||||
@@ -182,7 +186,8 @@ describe("CalendarAccessRights", () => {
|
|||||||
value={[]}
|
value={[]}
|
||||||
onChange={mockOnChange}
|
onChange={mockOnChange}
|
||||||
onInvitesLoaded={mockOnInvitesLoaded}
|
onInvitesLoaded={mockOnInvitesLoaded}
|
||||||
/>
|
/>,
|
||||||
|
{ ...userState }
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => expect(getUserDetails).toHaveBeenCalledWith("bob123"));
|
await waitFor(() => expect(getUserDetails).toHaveBeenCalledWith("bob123"));
|
||||||
@@ -219,7 +224,8 @@ describe("CalendarAccessRights", () => {
|
|||||||
value={[]}
|
value={[]}
|
||||||
onChange={mockOnChange}
|
onChange={mockOnChange}
|
||||||
onInvitesLoaded={mockOnInvitesLoaded}
|
onInvitesLoaded={mockOnInvitesLoaded}
|
||||||
/>
|
/>,
|
||||||
|
{ ...userState }
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => expect(mockOnInvitesLoaded).toHaveBeenCalledWith([]));
|
await waitFor(() => expect(mockOnInvitesLoaded).toHaveBeenCalledWith([]));
|
||||||
@@ -249,7 +255,8 @@ describe("CalendarAccessRights", () => {
|
|||||||
value={[]}
|
value={[]}
|
||||||
onChange={mockOnChange}
|
onChange={mockOnChange}
|
||||||
onInvitesLoaded={mockOnInvitesLoaded}
|
onInvitesLoaded={mockOnInvitesLoaded}
|
||||||
/>
|
/>,
|
||||||
|
{ ...userState }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(document.querySelector('[role="progressbar"]')).toBeInTheDocument();
|
expect(document.querySelector('[role="progressbar"]')).toBeInTheDocument();
|
||||||
@@ -286,7 +293,7 @@ describe("AccessTab – conditional rendering of CalendarAccessRights", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides CalendarAccessRights for a non-owner without admin delegation", () => {
|
it("hides CalendarAccessRights textfield for a non-owner without admin delegation", () => {
|
||||||
const foreignCalendar: Calendar = {
|
const foreignCalendar: Calendar = {
|
||||||
...baseCalendar,
|
...baseCalendar,
|
||||||
id: "otherUser/cal1",
|
id: "otherUser/cal1",
|
||||||
@@ -306,9 +313,10 @@ describe("AccessTab – conditional rendering of CalendarAccessRights", () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText("peopleSearch.label")).not.toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText("calendarPopover.access.grantAccessRights")
|
screen.getByText("calendarPopover.access.accessRights")
|
||||||
).not.toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows CalendarAccessRights when the user has access=5 (admin) delegation", () => {
|
it("shows CalendarAccessRights when the user has access=5 (admin) delegation", () => {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useAppSelector } from "@/app/hooks";
|
|
||||||
import {
|
import {
|
||||||
exportCalendar,
|
exportCalendar,
|
||||||
getSecretLink,
|
getSecretLink,
|
||||||
@@ -36,11 +35,6 @@ export function AccessTab({
|
|||||||
onInvitesLoaded,
|
onInvitesLoaded,
|
||||||
}: AccessTabProps) {
|
}: AccessTabProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const userData = useAppSelector((state) => state.user.userData);
|
|
||||||
const isPersonalCalendar = userData.openpaasId === calendar.id.split("/")[0];
|
|
||||||
const isDelegatedWithAdministration = calendar.invite?.find(
|
|
||||||
(invite) => invite.href.includes(userData.email) && invite.access === 5
|
|
||||||
);
|
|
||||||
|
|
||||||
const calDAVLink = `${window.DAV_BASE_URL}${calendar.link.replace(".json", "")}`;
|
const calDAVLink = `${window.DAV_BASE_URL}${calendar.link.replace(".json", "")}`;
|
||||||
|
|
||||||
@@ -98,14 +92,12 @@ export function AccessTab({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(isPersonalCalendar || isDelegatedWithAdministration) && (
|
<CalendarAccessRights
|
||||||
<CalendarAccessRights
|
calendar={calendar}
|
||||||
calendar={calendar}
|
value={usersWithAccess}
|
||||||
value={usersWithAccess}
|
onChange={onUsersWithAccessChange}
|
||||||
onChange={onUsersWithAccessChange}
|
onInvitesLoaded={onInvitesLoaded}
|
||||||
onInvitesLoaded={onInvitesLoaded}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!!window.DAV_BASE_URL && (
|
{!!window.DAV_BASE_URL && (
|
||||||
<FieldWithLabel label={t("calendar.caldav_access")} isExpanded={false}>
|
<FieldWithLabel label={t("calendar.caldav_access")} isExpanded={false}>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { useAppSelector } from "@/app/hooks";
|
||||||
import { AccessRight, Calendar } from "@/features/Calendars/CalendarTypes";
|
import { AccessRight, Calendar } from "@/features/Calendars/CalendarTypes";
|
||||||
import { getUserDetails } from "@/features/User/userAPI";
|
import { getUserDetails } from "@/features/User/userAPI";
|
||||||
|
import { makeDisplayName } from "@/utils/makeDisplayName";
|
||||||
|
import { normalizeEmail } from "@/utils/normalizeEmail";
|
||||||
import {
|
import {
|
||||||
AutocompleteRenderInputParams,
|
AutocompleteRenderInputParams,
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -38,6 +41,17 @@ export function CalendarAccessRights({
|
|||||||
onInvitesLoaded,
|
onInvitesLoaded,
|
||||||
}: CalendarAccessRightsProps) {
|
}: CalendarAccessRightsProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const userData = useAppSelector((state) => state.user.userData);
|
||||||
|
const isPersonalCalendar = userData?.openpaasId === calendar.id.split("/")[0];
|
||||||
|
const currentUserEmail = normalizeEmail(userData?.email);
|
||||||
|
const isDelegatedWithAdministration = !!calendar.invite?.some((invite) => {
|
||||||
|
const invitedEmail = normalizeEmail(invite.href.replace(/^mailto:/i, ""));
|
||||||
|
return invitedEmail === currentUserEmail && invite.access === 5;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ownerEmail =
|
||||||
|
calendar.owner?.preferredEmail ?? calendar.owner?.emails?.[0] ?? "";
|
||||||
|
const ownerName = makeDisplayName(calendar) ?? ownerEmail;
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [searchWidth, setSearchWidth] = useState<number | undefined>(undefined);
|
const [searchWidth, setSearchWidth] = useState<number | undefined>(undefined);
|
||||||
@@ -115,8 +129,6 @@ export function CalendarAccessRights({
|
|||||||
};
|
};
|
||||||
}, [calendar.invite, onChange, onInvitesLoaded]);
|
}, [calendar.invite, onChange, onInvitesLoaded]);
|
||||||
|
|
||||||
const normalizeEmail = (email?: string) => email?.trim().toLowerCase() ?? "";
|
|
||||||
|
|
||||||
const handleUserSelect = (_event: unknown, users: User[]) => {
|
const handleUserSelect = (_event: unknown, users: User[]) => {
|
||||||
const updated: UserWithAccess[] = users.map((user) => {
|
const updated: UserWithAccess[] = users.map((user) => {
|
||||||
const existing = usersWithAccess.find(
|
const existing = usersWithAccess.find(
|
||||||
@@ -153,188 +165,217 @@ export function CalendarAccessRights({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldWithLabel
|
<FieldWithLabel
|
||||||
label={t("calendarPopover.access.grantAccessRights")}
|
label={
|
||||||
|
isPersonalCalendar || isDelegatedWithAdministration
|
||||||
|
? t("calendarPopover.access.grantAccessRights")
|
||||||
|
: t("calendarPopover.access.accessRights")
|
||||||
|
}
|
||||||
isExpanded={false}
|
isExpanded={false}
|
||||||
>
|
>
|
||||||
<Box ref={containerRef}>
|
{(isPersonalCalendar || isDelegatedWithAdministration) && (
|
||||||
<PeopleSearch
|
<Box ref={containerRef}>
|
||||||
selectedUsers={usersWithAccess}
|
<PeopleSearch
|
||||||
onChange={handleUserSelect}
|
selectedUsers={usersWithAccess}
|
||||||
objectTypes={["user"]}
|
onChange={handleUserSelect}
|
||||||
onToggleEventPreview={() => {}}
|
objectTypes={["user"]}
|
||||||
customSlotProps={{
|
onToggleEventPreview={() => {}}
|
||||||
popper: {
|
customSlotProps={{
|
||||||
anchorEl: containerRef.current,
|
popper: {
|
||||||
placement: "bottom-start",
|
anchorEl: containerRef.current,
|
||||||
sx: {
|
placement: "bottom-start",
|
||||||
minWidth: searchWidth,
|
sx: {
|
||||||
"& .MuiPaper-root": {
|
minWidth: searchWidth,
|
||||||
width: "100%",
|
"& .MuiPaper-root": {
|
||||||
},
|
width: "100%",
|
||||||
},
|
|
||||||
modifiers: [
|
|
||||||
{
|
|
||||||
name: "offset",
|
|
||||||
options: {
|
|
||||||
offset: [0, 8],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
modifiers: [
|
||||||
},
|
{
|
||||||
}}
|
name: "offset",
|
||||||
customRenderInput={(
|
options: {
|
||||||
params: AutocompleteRenderInputParams,
|
offset: [0, 8],
|
||||||
query: string,
|
},
|
||||||
setQuery: (value: string) => void
|
},
|
||||||
) => (
|
],
|
||||||
<TextField
|
},
|
||||||
{...params}
|
}}
|
||||||
fullWidth
|
customRenderInput={(
|
||||||
autoFocus
|
params: AutocompleteRenderInputParams,
|
||||||
placeholder={t("peopleSearch.label")}
|
query: string,
|
||||||
value={query}
|
setQuery: (value: string) => void
|
||||||
inputRef={(el) => {
|
) => (
|
||||||
const ref = params.InputProps.ref;
|
<TextField
|
||||||
if (typeof ref === "function") {
|
{...params}
|
||||||
ref(el);
|
fullWidth
|
||||||
} else if (ref && "current" in ref) {
|
autoFocus
|
||||||
(
|
placeholder={t("peopleSearch.label")}
|
||||||
ref as React.MutableRefObject<HTMLInputElement | null>
|
value={query}
|
||||||
).current = el;
|
inputRef={(el) => {
|
||||||
}
|
const ref = params.InputProps.ref;
|
||||||
}}
|
if (typeof ref === "function") {
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
ref(el);
|
||||||
variant="outlined"
|
} else if (ref && "current" in ref) {
|
||||||
inputProps={{
|
(
|
||||||
...params.inputProps,
|
ref as React.MutableRefObject<HTMLInputElement | null>
|
||||||
sx: {
|
).current = el;
|
||||||
fontSize: "14px",
|
}
|
||||||
"&::placeholder": { fontSize: "14px" },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
InputProps={{
|
|
||||||
...params.InputProps,
|
|
||||||
startAdornment: (
|
|
||||||
<InputAdornment position="start">
|
|
||||||
<PeopleOutlineOutlinedIcon
|
|
||||||
sx={{ color: "text.secondary" }}
|
|
||||||
/>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<Select
|
|
||||||
value={accessRight}
|
|
||||||
onChange={(e) =>
|
|
||||||
setAccessRight(e.target.value as AccessRight)
|
|
||||||
}
|
|
||||||
variant="standard"
|
|
||||||
disableUnderline
|
|
||||||
sx={{
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "text.secondary",
|
|
||||||
"& .MuiSelect-select": {
|
|
||||||
paddingRight: "24px !important",
|
|
||||||
paddingY: 0,
|
|
||||||
},
|
|
||||||
"& .MuiSelect-icon": { fontSize: "1rem" },
|
|
||||||
"&:before, &:after": { display: "none" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{accessRightOptions.map((opt) => (
|
|
||||||
<MenuItem
|
|
||||||
key={opt.value}
|
|
||||||
value={opt.value}
|
|
||||||
sx={{ color: "text.secondary" }}
|
|
||||||
>
|
|
||||||
{opt.label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{inviteLoading ? (
|
|
||||||
<Box mt={2} display="flex" justifyContent="center">
|
|
||||||
<CircularProgress size={24} />
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
usersWithAccess.length > 0 && (
|
|
||||||
<Box mt={2} display="flex" flexDirection="column" gap={1}>
|
|
||||||
{usersWithAccess.map((user) => (
|
|
||||||
<Box
|
|
||||||
key={user.email}
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="space-between"
|
|
||||||
px={1}
|
|
||||||
py={0.5}
|
|
||||||
sx={{
|
|
||||||
borderRadius: "8px",
|
|
||||||
"&:hover": { backgroundColor: "action.hover" },
|
|
||||||
}}
|
}}
|
||||||
>
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
<Box display="flex" alignItems="center" gap={1.5} minWidth={0}>
|
variant="outlined"
|
||||||
<Avatar
|
inputProps={{
|
||||||
{...stringAvatar(user.displayName)}
|
...params.inputProps,
|
||||||
sx={{ width: 28, height: 28, fontSize: "0.875rem" }}
|
sx: {
|
||||||
>
|
fontSize: "14px",
|
||||||
{user.displayName?.[0]?.toUpperCase()}
|
"&::placeholder": { fontSize: "14px" },
|
||||||
</Avatar>
|
},
|
||||||
<Box
|
}}
|
||||||
minWidth={0}
|
InputProps={{
|
||||||
display="flex"
|
...params.InputProps,
|
||||||
flexDirection="column"
|
startAdornment: (
|
||||||
gap={0}
|
<InputAdornment position="start">
|
||||||
>
|
<PeopleOutlineOutlinedIcon
|
||||||
<Typography noWrap>{user.displayName}</Typography>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
{user.email}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
gap={0.5}
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
value={user.accessRight}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleChangeUserRight(
|
|
||||||
user.email,
|
|
||||||
e.target.value as AccessRight
|
|
||||||
)
|
|
||||||
}
|
|
||||||
variant="standard"
|
|
||||||
disableUnderline
|
|
||||||
sx={{
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "text.secondary",
|
|
||||||
"& .MuiSelect-select": {
|
|
||||||
paddingRight: "24px !important",
|
|
||||||
paddingY: 0,
|
|
||||||
},
|
|
||||||
"& .MuiSelect-icon": { fontSize: "1rem" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{accessRightOptions.map((opt) => (
|
|
||||||
<MenuItem
|
|
||||||
key={opt.value}
|
|
||||||
value={opt.value}
|
|
||||||
sx={{ color: "text.secondary" }}
|
sx={{ color: "text.secondary" }}
|
||||||
|
/>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<Select
|
||||||
|
value={accessRight}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAccessRight(e.target.value as AccessRight)
|
||||||
|
}
|
||||||
|
variant="standard"
|
||||||
|
disableUnderline
|
||||||
|
sx={{
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
color: "text.secondary",
|
||||||
|
"& .MuiSelect-select": {
|
||||||
|
paddingRight: "24px !important",
|
||||||
|
paddingY: 0,
|
||||||
|
},
|
||||||
|
"& .MuiSelect-icon": { fontSize: "1rem" },
|
||||||
|
"&:before, &:after": { display: "none" },
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{accessRightOptions.map((opt) => (
|
||||||
</MenuItem>
|
<MenuItem
|
||||||
))}
|
key={opt.value}
|
||||||
</Select>
|
value={opt.value}
|
||||||
|
sx={{ color: "text.secondary" }}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box mt={2} display="flex" flexDirection="column" gap={1}>
|
||||||
|
<Box
|
||||||
|
key={ownerEmail}
|
||||||
|
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(ownerName)}
|
||||||
|
sx={{ width: 28, height: 28, fontSize: "0.875rem" }}
|
||||||
|
/>
|
||||||
|
<Box minWidth={0} display="flex" flexDirection="column" gap={0}>
|
||||||
|
<Typography noWrap>{ownerName}</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{ownerEmail}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" alignItems="center" gap={0.5} flexShrink={0}>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{t("calendarPopover.access.owner")}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{inviteLoading ? (
|
||||||
|
<Box mt={2} display="flex" justifyContent="center">
|
||||||
|
<CircularProgress size={24} />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
usersWithAccess.length > 0 &&
|
||||||
|
usersWithAccess.map((user) => (
|
||||||
|
<Box
|
||||||
|
key={user.email}
|
||||||
|
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(user.displayName)}
|
||||||
|
sx={{ width: 28, height: 28, fontSize: "0.875rem" }}
|
||||||
|
/>
|
||||||
|
<Box minWidth={0} display="flex" flexDirection="column" gap={0}>
|
||||||
|
<Typography noWrap>{user.displayName}</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{user.email}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" alignItems="center" gap={0.5} flexShrink={0}>
|
||||||
|
<Select
|
||||||
|
value={user.accessRight}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChangeUserRight(
|
||||||
|
user.email,
|
||||||
|
e.target.value as AccessRight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant="standard"
|
||||||
|
disableUnderline
|
||||||
|
disabled={
|
||||||
|
!(isPersonalCalendar || isDelegatedWithAdministration)
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
color: "text.secondary",
|
||||||
|
"& .MuiSelect-select": {
|
||||||
|
paddingRight: "24px !important",
|
||||||
|
paddingY: 0,
|
||||||
|
},
|
||||||
|
"& .MuiSelect-icon": { fontSize: "1rem" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{accessRightOptions.map((opt) => (
|
||||||
|
<MenuItem
|
||||||
|
key={opt.value}
|
||||||
|
value={opt.value}
|
||||||
|
sx={{ color: "text.secondary" }}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{(isPersonalCalendar || isDelegatedWithAdministration) && (
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
aria-label={t("actions.remove")}
|
aria-label={t("actions.remove")}
|
||||||
@@ -343,12 +384,12 @@ export function CalendarAccessRights({
|
|||||||
>
|
>
|
||||||
<HighlightOffIcon fontSize="small" />
|
<HighlightOffIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
</Box>
|
||||||
</Box>
|
))
|
||||||
)
|
)}
|
||||||
)}
|
</Box>
|
||||||
</FieldWithLabel>
|
</FieldWithLabel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,16 @@ function CalendarPopover({
|
|||||||
const isOwn = calendar?.id
|
const isOwn = calendar?.id
|
||||||
? extractEventBaseUuid(calendar.id) === userData.openpaasId
|
? extractEventBaseUuid(calendar.id) === userData.openpaasId
|
||||||
: true;
|
: true;
|
||||||
|
const canManageInvites =
|
||||||
|
isOwn ||
|
||||||
|
!!calendar?.invite?.some((invite) => {
|
||||||
|
const inviteEmail = invite.href
|
||||||
|
.replace(/^mailto:/i, "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
const currentEmail = userData.email?.trim().toLowerCase();
|
||||||
|
return inviteEmail === currentEmail && invite.access === 5;
|
||||||
|
});
|
||||||
|
|
||||||
// existing calendar params
|
// existing calendar params
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
@@ -146,7 +156,7 @@ function CalendarPopover({
|
|||||||
.filter((u) => !!normaliseEmail(u) && !currentMap.has(normaliseEmail(u)))
|
.filter((u) => !!normaliseEmail(u) && !currentMap.has(normaliseEmail(u)))
|
||||||
.map((u) => ({ "dav:href": `mailto:${normaliseEmail(u)}` }));
|
.map((u) => ({ "dav:href": `mailto:${normaliseEmail(u)}` }));
|
||||||
|
|
||||||
if (set.length === 0 && remove.length === 0) return;
|
if ((set.length === 0 && remove.length === 0) || !canManageInvites) return;
|
||||||
|
|
||||||
await dispatch(
|
await dispatch(
|
||||||
updateDelegationCalendarAsync({
|
updateDelegationCalendarAsync({
|
||||||
|
|||||||
@@ -84,8 +84,10 @@
|
|||||||
},
|
},
|
||||||
"access": {
|
"access": {
|
||||||
"grantAccessRights": "Grant Access rights",
|
"grantAccessRights": "Grant Access rights",
|
||||||
|
"accessRights": "Access rights",
|
||||||
"viewAllEvents": "View all events",
|
"viewAllEvents": "View all events",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
|
"owner": "Owner",
|
||||||
"administrator": "Administrator"
|
"administrator": "Administrator"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
},
|
},
|
||||||
"access": {
|
"access": {
|
||||||
"grantAccessRights": "Accorder des droits d'accès",
|
"grantAccessRights": "Accorder des droits d'accès",
|
||||||
|
"accessRights": "Droits d'accès",
|
||||||
"viewAllEvents": "Voir tous les événements",
|
"viewAllEvents": "Voir tous les événements",
|
||||||
"editor": "Éditeur",
|
"editor": "Éditeur",
|
||||||
"administrator": "Administrateur"
|
"administrator": "Administrateur"
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
},
|
},
|
||||||
"access": {
|
"access": {
|
||||||
"grantAccessRights": "Предоставить права доступа",
|
"grantAccessRights": "Предоставить права доступа",
|
||||||
|
"accessRights": "Права доступа",
|
||||||
"viewAllEvents": "Просмотр всех событий",
|
"viewAllEvents": "Просмотр всех событий",
|
||||||
"editor": "Редактор",
|
"editor": "Редактор",
|
||||||
"administrator": "Администратор"
|
"administrator": "Администратор"
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
},
|
},
|
||||||
"access": {
|
"access": {
|
||||||
"grantAccessRights": "Cấp quyền truy cập",
|
"grantAccessRights": "Cấp quyền truy cập",
|
||||||
|
"accessRights": "Quyền truy cập",
|
||||||
"viewAllEvents": "Xem tất cả sự kiện",
|
"viewAllEvents": "Xem tất cả sự kiện",
|
||||||
"editor": "Biên tập viên",
|
"editor": "Biên tập viên",
|
||||||
"administrator": "Quản trị viên"
|
"administrator": "Quản trị viên"
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function normalizeEmail(email?: string) {
|
||||||
|
return email?.trim().toLowerCase() ?? "";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user