diff --git a/__test__/utils/buildDelegatedEvent.test.ts b/__test__/utils/buildDelegatedEvent.test.ts
index 6bca6b4..f1249f7 100644
--- a/__test__/utils/buildDelegatedEvent.test.ts
+++ b/__test__/utils/buildDelegatedEvent.test.ts
@@ -1,6 +1,5 @@
-import { buildDelegatedEventURL } from "@/features/Events/eventUtils";
import { Calendar } from "@/features/Calendars/CalendarTypes";
-import { CalendarEvent } from "@/features/Events/EventsTypes";
+import { buildDelegatedEventURL } from "@/features/Events/eventUtils";
const makeCalendar = (link: string): Calendar =>
({
@@ -10,45 +9,42 @@ const makeCalendar = (link: string): Calendar =>
owner: { emails: ["owner@example.com"] },
}) as Calendar;
-const makeEvent = (url: string): CalendarEvent =>
- ({ URL: url, calId: "user2/cal1" }) as CalendarEvent;
-
describe("buildDelegatedEventURL", () => {
it("rebases event filename onto calendar link base path", () => {
const calendar = makeCalendar("/calendars/user2/cal1.json");
- const event = makeEvent("/calendars/someother/path/event-abc.ics");
- expect(buildDelegatedEventURL(calendar, event)).toBe(
+ const eventURL = "/calendars/someother/path/event-abc.ics";
+ expect(buildDelegatedEventURL(calendar, eventURL)).toBe(
"/calendars/user2/cal1/event-abc.ics"
);
});
it("strips .json suffix from calendar link", () => {
const calendar = makeCalendar("/calendars/user2/cal1.json");
- const event = makeEvent("/calendars/user2/cal1/event-abc.ics");
- const result = buildDelegatedEventURL(calendar, event);
+ const eventURL = "/calendars/user2/cal1/event-abc.ics";
+ const result = buildDelegatedEventURL(calendar, eventURL);
expect(result).not.toContain(".json");
});
it("preserves the exact filename from the event URL", () => {
const calendar = makeCalendar("/calendars/user2/cal1.json");
- const event = makeEvent("/calendars/user2/cal1/some-uid-with-dashes.ics");
- expect(buildDelegatedEventURL(calendar, event)).toBe(
+ const eventURL = "/calendars/user2/cal1/some-uid-with-dashes.ics";
+ expect(buildDelegatedEventURL(calendar, eventURL)).toBe(
"/calendars/user2/cal1/some-uid-with-dashes.ics"
);
});
it("throws when event URL has no filename", () => {
const calendar = makeCalendar("/calendars/user2/cal1.json");
- const event = makeEvent("");
- expect(() => buildDelegatedEventURL(calendar, event)).toThrow(
+ const eventURL = "";
+ expect(() => buildDelegatedEventURL(calendar, eventURL)).toThrow(
/Cannot extract filename from event URL/
);
});
it("works with nested calendar link paths", () => {
const calendar = makeCalendar("/dav/calendars/users/user2/cal1.json");
- const event = makeEvent("/other/path/event-xyz.ics");
- expect(buildDelegatedEventURL(calendar, event)).toBe(
+ const eventURL = "/other/path/event-xyz.ics";
+ expect(buildDelegatedEventURL(calendar, eventURL)).toBe(
"/dav/calendars/users/user2/cal1/event-xyz.ics"
);
});
diff --git a/src/components/Event/EventFormFields.tsx b/src/components/Event/EventFormFields.tsx
index cc6bb46..e60b010 100644
--- a/src/components/Event/EventFormFields.tsx
+++ b/src/components/Event/EventFormFields.tsx
@@ -11,6 +11,7 @@ import {
Box,
Button,
Checkbox,
+ Divider,
FormControl,
FormControlLabel,
IconButton,
@@ -183,6 +184,12 @@ export default function EventFormFields({
// Track if user has clicked on calendar section in normal mode
const [hasClickedCalendarSection, setHasClickedCalendarSection] =
React.useState(false);
+ const delegatedCalendars = userPersonalCalendars.filter(
+ (cal) => cal.delegated
+ );
+ const personalCalendars = userPersonalCalendars.filter(
+ (cal) => !cal.delegated
+ );
// Reset hasEndDateChanged and hasClickedDateTimeSection when modal closes
React.useEffect(() => {
@@ -790,7 +797,10 @@ export default function EventFormFields({
handleCalendarChange(e.target.value)
}
>
- {CalendarItemList(userPersonalCalendars)}
+ {CalendarItemList(personalCalendars)}
+ {delegatedCalendars.length > 0 &&
+ personalCalendars.length > 0 && }
+ {CalendarItemList(delegatedCalendars)}
)}
diff --git a/src/features/Calendars/services/getTempCalendarsListAsync.ts b/src/features/Calendars/services/getTempCalendarsListAsync.ts
index 8b9dc3b..8e4fdc7 100644
--- a/src/features/Calendars/services/getTempCalendarsListAsync.ts
+++ b/src/features/Calendars/services/getTempCalendarsListAsync.ts
@@ -55,8 +55,7 @@ export const getTempCalendarsListAsync = createAsyncThunk<
id,
name,
link,
- owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${ownerData.lastname}`,
- ownerEmails: ownerData.emails,
+ owner: ownerData,
description,
delegated,
color: {
diff --git a/src/features/Calendars/services/refreshCalendar.ts b/src/features/Calendars/services/refreshCalendar.ts
index 65766d1..a1f06b6 100644
--- a/src/features/Calendars/services/refreshCalendar.ts
+++ b/src/features/Calendars/services/refreshCalendar.ts
@@ -65,7 +65,7 @@ export const refreshCalendarWithSyncToken = createAsyncThunk<
calId: calendar.id,
deletedEvents: toDelete.map((eventURL) =>
calendar.delegated
- ? buildDelegatedEventURL(calendar, { URL: eventURL })
+ ? buildDelegatedEventURL(calendar, eventURL)
: eventURL
),
createdOrUpdatedEvents: createdOrUpdatedEvents
diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx
index 48def8a..8b8c46c 100644
--- a/src/features/Events/EventModal.tsx
+++ b/src/features/Events/EventModal.tsx
@@ -1,5 +1,4 @@
import { useAppDispatch, useAppSelector } from "@/app/hooks";
-import { RootState } from "@/app/store";
import { ResponsiveDialog } from "@/components/Dialog";
import EventFormFields from "@/components/Event/EventFormFields";
import { addDays } from "@/components/Event/utils/dateRules";
@@ -27,7 +26,6 @@ import { addVideoConferenceToDescription } from "@/utils/videoConferenceUtils";
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import { Box, Button } from "@linagora/twake-mui";
import AddIcon from "@mui/icons-material/Add";
-import { createSelector } from "@reduxjs/toolkit";
import React, {
startTransition,
useCallback,
@@ -42,6 +40,8 @@ import { putEventAsync } from "../Calendars/services";
import { AsyncThunkResult } from "../Calendars/types/AsyncThunkResult";
import { userAttendee } from "../User/models/attendee";
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
+import { buildDelegatedEventURL } from "./eventUtils";
+import { useEventOrganizer } from "./useEventOrganizer";
function EventPopover({
open,
@@ -62,24 +62,13 @@ function EventPopover({
const dispatch = useAppDispatch();
const { t } = useI18n();
- const organizer = useAppSelector((state) => state.user.organiserData);
+ const userOrganizer = useAppSelector((state) => state.user.organiserData);
const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const calList = useAppSelector((state) => state.calendars.list);
- const selectPersonalCalendars = createSelector(
- (state: RootState) => state.calendars,
- (calendars: RootState["calendars"]) =>
- Object.keys(calendars.list || {})
- .map((id) => {
- if (id.split("/")[0] === userId) {
- return calendars.list?.[id];
- }
- return {} as Calendar;
- })
- .filter((calendar) => calendar.id)
- );
- const userPersonalCalendars: Calendar[] = useAppSelector(
- selectPersonalCalendars
+ const userPersonalCalendars: Calendar[] = Object.values(calList || {}).filter(
+ (cal) =>
+ cal.id?.split("/")[0] === userId || (cal.delegated && cal.access?.write)
);
const timezoneList = useMemo(() => {
@@ -121,6 +110,16 @@ function EventPopover({
const [repetition, setRepetition] = useState(
event?.repetition ?? ({} as RepetitionObject)
);
+
+ // Derive the effective organizer based on the selected calendar.
+ // When a delegated calendar is selected, the organizer must be the
+ // calendar owner, not the logged-in user.
+ const { organizer } = useEventOrganizer({
+ calendarid,
+ calList,
+ userOrganizer,
+ });
+
const [attendees, setAttendees] = useState(
event?.attendee
? event.attendee.filter((a) => a.cal_address !== organizer?.cal_address)
@@ -728,11 +727,13 @@ function EventPopover({
console.error("No target calendar available to save event");
return;
}
-
+ const newEventURL = `/calendars/${targetCalendar.id}/${newEventUID}.ics`;
const newEvent: CalendarEvent = {
calId: targetCalendar.id,
title,
- URL: `/calendars/${targetCalendar.id}/${newEventUID}.ics`,
+ URL: targetCalendar.delegated
+ ? buildDelegatedEventURL(targetCalendar, newEventURL)
+ : newEventURL,
start: "",
allday,
uid: newEventUID,
@@ -865,6 +866,7 @@ function EventPopover({
);
}
};
+
const dialogActions = (
{!showMore && (
diff --git a/src/features/Events/EventUpdateModal.tsx b/src/features/Events/EventUpdateModal.tsx
index 0ba0a05..1ea5cca 100644
--- a/src/features/Events/EventUpdateModal.tsx
+++ b/src/features/Events/EventUpdateModal.tsx
@@ -76,14 +76,17 @@ function EventUpdateModal({
const user = useAppSelector((state) => state.user);
- const calendarsList = useAppSelector((state) => state.calendars.list);
-
+ // if the event's calendar is delegated then it shall be the only calendar accessible from the event update modal
const userPersonalCalendars: Calendar[] = useMemo(() => {
- const allCalendars = Object.values(calendarsList) as Calendar[];
+ const allCalendars = Object.values(calList) as Calendar[];
+ if (calList[calId]?.delegated) {
+ return [calList[calId]];
+ }
return allCalendars.filter(
- (c: Calendar) => c.id?.split("/")[0] === user.userData?.openpaasId
+ (calendar: Calendar) =>
+ calendar.id?.split("/")[0] === user.userData?.openpaasId
);
- }, [calendarsList, user.userData?.openpaasId]);
+ }, [calList, calId, user.userData?.openpaasId]);
const timezoneList = useMemo(() => {
const zones = Object.keys(TIMEZONES.zones).sort();
@@ -275,7 +278,7 @@ function EventUpdateModal({
// Handle repetition properly - check both current event and base event
const baseEventId = extractEventBaseUuid(eventToDisplay.uid);
- const baseEvent = calendarsList[calId]?.events[baseEventId];
+ const baseEvent = calList[calId]?.events[baseEventId];
const repetitionSource =
eventToDisplay.repetition || baseEvent?.repetition;
@@ -343,7 +346,7 @@ function EventUpdateModal({
event,
calId,
userPersonalCalendars,
- calendarsList,
+ calList,
masterEvent,
isLoadingMasterEvent,
]);
diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts
index c302e83..e719b20 100644
--- a/src/features/Events/eventUtils.ts
+++ b/src/features/Events/eventUtils.ts
@@ -183,10 +183,7 @@ export function parseCalendarEvent(
}
event.calId = calendar.id;
event.URL = calendar.delegated
- ? buildDelegatedEventURL(calendar, {
- ...event,
- URL: eventURL,
- } as CalendarEvent)
+ ? buildDelegatedEventURL(calendar, eventURL)
: eventURL;
if (!event.uid || !event.start) {
console.error(
@@ -565,12 +562,12 @@ export function detectRecurringEventChanges(
export function buildDelegatedEventURL(
calendar: Calendar,
- event: CalendarEvent
+ eventURL: string
): string {
const calendarBasePath = calendar.link.replace(/\.json$/, "");
- const eventFilename = event.URL.split("/").pop();
+ const eventFilename = eventURL.split("/").pop();
if (!eventFilename) {
- throw new Error(`Cannot extract filename from event URL: ${event.URL}`);
+ throw new Error(`Cannot extract filename from event URL: ${eventURL}`);
}
return `${calendarBasePath}/${eventFilename}`;
}
diff --git a/src/features/Events/useEventOrganizer.ts b/src/features/Events/useEventOrganizer.ts
new file mode 100644
index 0000000..59f07fe
--- /dev/null
+++ b/src/features/Events/useEventOrganizer.ts
@@ -0,0 +1,34 @@
+import { makeDisplayName } from "@/utils/makeDisplayName";
+import { useMemo } from "react";
+import { Calendar } from "../Calendars/CalendarTypes";
+
+// Update event organizer accordingly to selected calendar's delegated status
+export function useEventOrganizer({
+ calendarid,
+ calList,
+ userOrganizer,
+}: {
+ calendarid: string;
+ calList: Record;
+ userOrganizer?: { cn: string; cal_address: string };
+}) {
+ const selectedCalendar = useMemo(
+ () => calList?.[calendarid],
+ [calList, calendarid]
+ );
+
+ const organizer = useMemo(() => {
+ if (selectedCalendar?.delegated && selectedCalendar?.owner) {
+ return {
+ cn:
+ makeDisplayName(selectedCalendar) ??
+ selectedCalendar.owner.emails?.[0] ??
+ "",
+ cal_address: selectedCalendar.owner.emails?.[0] ?? "",
+ };
+ }
+ return userOrganizer;
+ }, [selectedCalendar, userOrganizer]);
+
+ return { organizer, selectedCalendar };
+}
diff --git a/src/utils/makeDisplayName.tsx b/src/utils/makeDisplayName.tsx
new file mode 100644
index 0000000..8db7eba
--- /dev/null
+++ b/src/utils/makeDisplayName.tsx
@@ -0,0 +1,14 @@
+import { Calendar } from "../features/Calendars/CalendarTypes";
+
+export function makeDisplayName(
+ selectedCalendar: Calendar
+): string | undefined {
+ if (
+ !selectedCalendar ||
+ (!selectedCalendar.owner?.lastname && !selectedCalendar.owner?.firstname)
+ )
+ return;
+ return [selectedCalendar.owner?.firstname, selectedCalendar.owner?.lastname]
+ .filter(Boolean)
+ .join(" ");
+}