diff --git a/__test__/features/Calendars/CalendarAccessRights.test.tsx b/__test__/features/Calendars/CalendarAccessRights.test.tsx
index dcee911..6e86a45 100644
--- a/__test__/features/Calendars/CalendarAccessRights.test.tsx
+++ b/__test__/features/Calendars/CalendarAccessRights.test.tsx
@@ -87,6 +87,8 @@ const baseCalendar: Calendar = {
events: {},
invite: [],
owner: { emails: ["user1@example.com"] },
+ access: { write: true },
+ delegated: true,
};
describe("CalendarAccessRights", () => {
@@ -102,7 +104,8 @@ describe("CalendarAccessRights", () => {
value={[]}
onChange={mockOnChange}
onInvitesLoaded={mockOnInvitesLoaded}
- />
+ />,
+ { ...userState }
);
expect(
@@ -113,7 +116,7 @@ describe("CalendarAccessRights", () => {
it("shows a list of users already passed in via value prop", () => {
const users: UserWithAccess[] = [
{
- openpaasId: "abc",
+ openpaasId: "user1",
displayName: "Alice",
email: "alice@example.com",
accessRight: 2,
@@ -136,10 +139,10 @@ describe("CalendarAccessRights", () => {
it("calls onChange with user removed when remove button is clicked", () => {
const users: UserWithAccess[] = [
{
- openpaasId: "abc",
+ openpaasId: "user1",
displayName: "Alice",
email: "alice@example.com",
- accessRight: 2,
+ accessRight: 5,
},
];
@@ -149,7 +152,8 @@ describe("CalendarAccessRights", () => {
value={users}
onChange={mockOnChange}
onInvitesLoaded={mockOnInvitesLoaded}
- />
+ />,
+ { ...userState }
);
fireEvent.click(screen.getByLabelText(/remove/i));
@@ -182,7 +186,8 @@ describe("CalendarAccessRights", () => {
value={[]}
onChange={mockOnChange}
onInvitesLoaded={mockOnInvitesLoaded}
- />
+ />,
+ { ...userState }
);
await waitFor(() => expect(getUserDetails).toHaveBeenCalledWith("bob123"));
@@ -219,7 +224,8 @@ describe("CalendarAccessRights", () => {
value={[]}
onChange={mockOnChange}
onInvitesLoaded={mockOnInvitesLoaded}
- />
+ />,
+ { ...userState }
);
await waitFor(() => expect(mockOnInvitesLoaded).toHaveBeenCalledWith([]));
@@ -249,7 +255,8 @@ describe("CalendarAccessRights", () => {
value={[]}
onChange={mockOnChange}
onInvitesLoaded={mockOnInvitesLoaded}
- />
+ />,
+ { ...userState }
);
expect(document.querySelector('[role="progressbar"]')).toBeInTheDocument();
@@ -286,7 +293,7 @@ describe("AccessTab – conditional rendering of CalendarAccessRights", () => {
).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 = {
...baseCalendar,
id: "otherUser/cal1",
@@ -306,9 +313,10 @@ describe("AccessTab – conditional rendering of CalendarAccessRights", () => {
}
);
+ expect(screen.queryByText("peopleSearch.label")).not.toBeInTheDocument();
expect(
- screen.queryByText("calendarPopover.access.grantAccessRights")
- ).not.toBeInTheDocument();
+ screen.getByText("calendarPopover.access.accessRights")
+ ).toBeInTheDocument();
});
it("shows CalendarAccessRights when the user has access=5 (admin) delegation", () => {
diff --git a/src/components/Calendar/AccessTab.tsx b/src/components/Calendar/AccessTab.tsx
index 7f35cb1..3c6cbe4 100644
--- a/src/components/Calendar/AccessTab.tsx
+++ b/src/components/Calendar/AccessTab.tsx
@@ -1,4 +1,3 @@
-import { useAppSelector } from "@/app/hooks";
import {
exportCalendar,
getSecretLink,
@@ -36,11 +35,6 @@ export function AccessTab({
onInvitesLoaded,
}: AccessTabProps) {
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", "")}`;
@@ -98,14 +92,12 @@ export function AccessTab({
return (
<>
- {(isPersonalCalendar || isDelegatedWithAdministration) && (
-
- )}
+
{!!window.DAV_BASE_URL && (
diff --git a/src/components/Calendar/CalendarAccessRights.tsx b/src/components/Calendar/CalendarAccessRights.tsx
index ccf6897..d37fbe2 100644
--- a/src/components/Calendar/CalendarAccessRights.tsx
+++ b/src/components/Calendar/CalendarAccessRights.tsx
@@ -1,5 +1,8 @@
+import { useAppSelector } from "@/app/hooks";
import { AccessRight, Calendar } from "@/features/Calendars/CalendarTypes";
import { getUserDetails } from "@/features/User/userAPI";
+import { makeDisplayName } from "@/utils/makeDisplayName";
+import { normalizeEmail } from "@/utils/normalizeEmail";
import {
AutocompleteRenderInputParams,
Avatar,
@@ -38,6 +41,17 @@ export function CalendarAccessRights({
onInvitesLoaded,
}: CalendarAccessRightsProps) {
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(null);
const [searchWidth, setSearchWidth] = useState(undefined);
@@ -115,8 +129,6 @@ export function CalendarAccessRights({
};
}, [calendar.invite, onChange, onInvitesLoaded]);
- const normalizeEmail = (email?: string) => email?.trim().toLowerCase() ?? "";
-
const handleUserSelect = (_event: unknown, users: User[]) => {
const updated: UserWithAccess[] = users.map((user) => {
const existing = usersWithAccess.find(
@@ -153,188 +165,217 @@ export function CalendarAccessRights({
return (
-
- {}}
- customSlotProps={{
- popper: {
- anchorEl: containerRef.current,
- placement: "bottom-start",
- sx: {
- minWidth: searchWidth,
- "& .MuiPaper-root": {
- width: "100%",
- },
- },
- modifiers: [
- {
- name: "offset",
- options: {
- offset: [0, 8],
+ {(isPersonalCalendar || isDelegatedWithAdministration) && (
+
+ {}}
+ customSlotProps={{
+ popper: {
+ anchorEl: containerRef.current,
+ placement: "bottom-start",
+ sx: {
+ minWidth: searchWidth,
+ "& .MuiPaper-root": {
+ width: "100%",
},
},
- ],
- },
- }}
- customRenderInput={(
- params: AutocompleteRenderInputParams,
- query: string,
- setQuery: (value: string) => void
- ) => (
- {
- const ref = params.InputProps.ref;
- if (typeof ref === "function") {
- ref(el);
- } else if (ref && "current" in ref) {
- (
- ref as React.MutableRefObject
- ).current = el;
- }
- }}
- onChange={(e) => setQuery(e.target.value)}
- variant="outlined"
- inputProps={{
- ...params.inputProps,
- sx: {
- fontSize: "14px",
- "&::placeholder": { fontSize: "14px" },
- },
- }}
- InputProps={{
- ...params.InputProps,
- startAdornment: (
-
-
-
- ),
- endAdornment: (
-
-
-
- ),
- }}
- />
- )}
- />
-
-
- {inviteLoading ? (
-
-
-
- ) : (
- usersWithAccess.length > 0 && (
-
- {usersWithAccess.map((user) => (
- void
+ ) => (
+ {
+ const ref = params.InputProps.ref;
+ if (typeof ref === "function") {
+ ref(el);
+ } else if (ref && "current" in ref) {
+ (
+ ref as React.MutableRefObject
+ ).current = el;
+ }
}}
- >
-
-
- {user.displayName?.[0]?.toUpperCase()}
-
-
- {user.displayName}
-
- {user.email}
-
-
-
-
-
-
+ {accessRightOptions.map((opt) => (
+
+ ))}
+
+
+ ),
+ }}
+ />
+ )}
+ />
+
+ )}
+
+
+
+
+
+
+ {ownerName}
+
+ {ownerEmail}
+
+
+
+
+
+
+ {t("calendarPopover.access.owner")}
+
+
+
+ {inviteLoading ? (
+
+
+
+ ) : (
+ usersWithAccess.length > 0 &&
+ usersWithAccess.map((user) => (
+
+
+
+
+ {user.displayName}
+
+ {user.email}
+
+
+
+
+
+
+ {(isPersonalCalendar || isDelegatedWithAdministration) && (
-
+ )}
- ))}
-
- )
- )}
+
+ ))
+ )}
+
);
}
diff --git a/src/components/Calendar/CalendarModal.tsx b/src/components/Calendar/CalendarModal.tsx
index cb3b201..79d271b 100644
--- a/src/components/Calendar/CalendarModal.tsx
+++ b/src/components/Calendar/CalendarModal.tsx
@@ -39,6 +39,16 @@ function CalendarPopover({
const isOwn = calendar?.id
? extractEventBaseUuid(calendar.id) === userData.openpaasId
: 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
const [name, setName] = useState("");
@@ -146,7 +156,7 @@ function CalendarPopover({
.filter((u) => !!normaliseEmail(u) && !currentMap.has(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(
updateDelegationCalendarAsync({
diff --git a/src/locales/en.json b/src/locales/en.json
index 2b21b2a..6e4da30 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -84,8 +84,10 @@
},
"access": {
"grantAccessRights": "Grant Access rights",
+ "accessRights": "Access rights",
"viewAllEvents": "View all events",
"editor": "Editor",
+ "owner": "Owner",
"administrator": "Administrator"
}
},
diff --git a/src/locales/fr.json b/src/locales/fr.json
index 15c5968..57772a7 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -86,6 +86,7 @@
},
"access": {
"grantAccessRights": "Accorder des droits d'accès",
+ "accessRights": "Droits d'accès",
"viewAllEvents": "Voir tous les événements",
"editor": "Éditeur",
"administrator": "Administrateur"
diff --git a/src/locales/ru.json b/src/locales/ru.json
index 3c9a335..5e68d79 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -86,6 +86,7 @@
},
"access": {
"grantAccessRights": "Предоставить права доступа",
+ "accessRights": "Права доступа",
"viewAllEvents": "Просмотр всех событий",
"editor": "Редактор",
"administrator": "Администратор"
diff --git a/src/locales/vi.json b/src/locales/vi.json
index c1b07e5..5592656 100644
--- a/src/locales/vi.json
+++ b/src/locales/vi.json
@@ -84,6 +84,7 @@
},
"access": {
"grantAccessRights": "Cấp quyền truy cập",
+ "accessRights": "Quyền truy cập",
"viewAllEvents": "Xem tất cả sự kiện",
"editor": "Biên tập viên",
"administrator": "Quản trị viên"
diff --git a/src/utils/normalizeEmail.tsx b/src/utils/normalizeEmail.tsx
new file mode 100644
index 0000000..18f35f6
--- /dev/null
+++ b/src/utils/normalizeEmail.tsx
@@ -0,0 +1,3 @@
+export function normalizeEmail(email?: string) {
+ return email?.trim().toLowerCase() ?? "";
+}