559 delegation create event in another user calendar (#561)
* [#559] added delegated calendars in user selector * [#559] selector for calendar display delegated and personal separatly Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 && <Divider component={"li"} />}
|
||||
{CalendarItemList(delegatedCalendars)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<RepetitionObject>(
|
||||
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<userAttendee[]>(
|
||||
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 = (
|
||||
<Box display="flex" justifyContent="space-between" width="100%" px={2}>
|
||||
{!showMore && (
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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<string, Calendar>;
|
||||
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 };
|
||||
}
|
||||
@@ -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(" ");
|
||||
}
|
||||
Reference in New Issue
Block a user