460 import event without organiser (#579)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2026-02-25 17:15:25 +01:00
committed by GitHub
parent 6874b9ee70
commit 82ec10cfad
4 changed files with 282 additions and 54 deletions
@@ -0,0 +1,213 @@
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { updateAttendeesAfterTimeChange } from "@/features/Events/updateEventHelpers/updateAttendeesAfterTimeChange";
import { userAttendee } from "@/features/User/models/attendee";
import { userOrganiser } from "@/features/User/userDataTypes";
describe("updateAttendeesAfterTimeChange", () => {
const mockAttendee: userAttendee = {
cal_address: "attendee@example.com",
partstat: "ACCEPTED",
rsvp: "FALSE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
cn: "attendee",
};
const mockOrganizer: userOrganiser = {
cn: "Organizer",
cal_address: "organizer@example.com",
};
const organizerAttendee: userAttendee = {
cal_address: "organizer@example.com",
partstat: "ACCEPTED",
rsvp: "FALSE",
role: "CHAIR",
cutype: "INDIVIDUAL",
cn: "organizer",
};
const baseEvent: CalendarEvent = {
uid: "test-event",
URL: "/calendar/667037022b752d0026472254/cal1/test-event.ics",
title: "Test Event",
calId: "667037022b752d0026472254/cal1",
start: "2025-01-15T07:00:00.000Z",
end: "2025-01-15T08:00:00.000Z",
allday: false,
organizer: mockOrganizer,
attendee: [mockAttendee, organizerAttendee],
timezone: "ETC/UTC",
};
describe("early returns", () => {
it("should return the event unchanged when attendee list is undefined", () => {
const event = { ...baseEvent, attendee: undefined };
const result = updateAttendeesAfterTimeChange(
event as unknown as CalendarEvent
);
expect(result).toBe(event);
});
it("should NOT bail out early when organizer is undefined", () => {
const event = { ...baseEvent, organizer: undefined };
const result = updateAttendeesAfterTimeChange(event);
expect(result.attendee).toBeDefined();
expect(result.attendee).toHaveLength(2);
});
});
describe("without provided attendees list", () => {
it("should return attendees unchanged when timeChanged is false", () => {
const result = updateAttendeesAfterTimeChange(baseEvent, false);
expect(result.attendee?.[0].partstat).toBe("ACCEPTED");
expect(result.attendee?.[0].rsvp).toBe("FALSE");
});
it("should mark non-organizer attendees as NEEDS-ACTION when timeChanged is true", () => {
const result = updateAttendeesAfterTimeChange(baseEvent, true);
const attendee = result.attendee?.find(
(a) => a.cal_address === "attendee@example.com"
);
expect(attendee?.partstat).toBe("NEEDS-ACTION");
expect(attendee?.rsvp).toBe("TRUE");
});
it("should NOT mark the organizer as NEEDS-ACTION when timeChanged is true", () => {
const result = updateAttendeesAfterTimeChange(baseEvent, true);
const organizer = result.attendee?.find(
(a) => a.cal_address === "organizer@example.com"
);
expect(organizer?.partstat).toBe("ACCEPTED");
expect(organizer?.rsvp).toBe("FALSE");
});
it("should return attendees unchanged when timeChanged is undefined", () => {
const result = updateAttendeesAfterTimeChange(baseEvent);
expect(result.attendee?.[0].partstat).toBe("ACCEPTED");
});
});
describe("with provided attendees list", () => {
const newAttendee: userAttendee = {
cal_address: "new@example.com",
partstat: "ACCEPTED",
rsvp: "FALSE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
cn: "new",
};
it("should use existing attendee data when the address already exists in the event", () => {
const result = updateAttendeesAfterTimeChange(baseEvent, false, [
mockAttendee,
]);
const attendee = result.attendee?.find(
(a) => a.cal_address === "attendee@example.com"
);
expect(attendee?.partstat).toBe("ACCEPTED");
});
it("should mark new attendee as NEEDS-ACTION when not found in existing list", () => {
const result = updateAttendeesAfterTimeChange(baseEvent, false, [
newAttendee,
]);
const attendee = result.attendee?.find(
(a) => a.cal_address === "new@example.com"
);
expect(attendee?.partstat).toBe("NEEDS-ACTION");
expect(attendee?.rsvp).toBe("TRUE");
});
it("should mark all attendees as NEEDS-ACTION when timeChanged is true", () => {
const result = updateAttendeesAfterTimeChange(baseEvent, true, [
mockAttendee,
newAttendee,
]);
const nonOrganizerAttendees = result.attendee?.filter(
(a) => a.cal_address !== "organizer@example.com"
);
nonOrganizerAttendees?.forEach((a) => {
expect(a.partstat).toBe("NEEDS-ACTION");
expect(a.rsvp).toBe("TRUE");
});
});
it("should append organizer entry at the end of the attendee list", () => {
const result = updateAttendeesAfterTimeChange(baseEvent, false, [
mockAttendee,
]);
const last = result.attendee?.[result.attendee.length - 1];
expect(last?.cal_address).toBe("organizer@example.com");
});
it("should use existing organizer attendee data when found in event attendees", () => {
const result = updateAttendeesAfterTimeChange(baseEvent, false, [
mockAttendee,
]);
const organizer = result.attendee?.find(
(a) => a.cal_address === "organizer@example.com"
);
expect(organizer?.partstat).toBe("ACCEPTED");
});
it("should fall back to organizer defaults when organizer is not in existing attendees", () => {
const eventWithoutOrganizerInAttendees: CalendarEvent = {
...baseEvent,
attendee: [mockAttendee], // organizer not in attendee list
};
const result = updateAttendeesAfterTimeChange(
eventWithoutOrganizerInAttendees,
false,
[mockAttendee]
);
const organizer = result.attendee?.find(
(a) => a.cal_address === "organizer@example.com"
);
expect(organizer?.role).toBe("CHAIR");
expect(organizer?.partstat).toBe("NEEDS-ACTION");
});
});
describe("organizer undefined", () => {
const eventNoOrganizer: CalendarEvent = {
...baseEvent,
organizer: undefined,
};
it("should return attendees without bailing out", () => {
const result = updateAttendeesAfterTimeChange(eventNoOrganizer);
expect(result.attendee).toHaveLength(2);
});
it("should mark attendees as NEEDS-ACTION when timeChanged is true", () => {
const result = updateAttendeesAfterTimeChange(eventNoOrganizer, true);
result.attendee?.forEach((a) => {
expect(a.partstat).toBe("NEEDS-ACTION");
expect(a.rsvp).toBe("TRUE");
});
});
it("should not mutate attendees when timeChanged is false", () => {
const result = updateAttendeesAfterTimeChange(eventNoOrganizer, false);
expect(result.attendee?.[0].partstat).toBe("ACCEPTED");
expect(result.attendee?.[0].rsvp).toBe("FALSE");
});
it("should not append an organizer entry when organizer is undefined and attendees list is provided", () => {
const newAttendee: userAttendee = {
cal_address: "new@example.com",
partstat: "ACCEPTED",
rsvp: "FALSE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
cn: "new",
};
const result = updateAttendeesAfterTimeChange(eventNoOrganizer, false, [
newAttendee,
]);
expect(result.attendee).toHaveLength(1);
expect(result.attendee?.[0].cal_address).toBe("new@example.com");
});
});
});
@@ -10,7 +10,7 @@ import {
} from "@/features/Calendars/services";
import { getEvent } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import { userAttendee } from "@/features/User/models/attendee";
import { updateAttendeesAfterTimeChange } from "@/features/Events/updateEventHelpers/updateAttendeesAfterTimeChange";
import { createAttendee } from "@/features/User/models/attendee.mapper";
import { getDeltaInMilliseconds } from "@/utils/dateUtils";
import {
@@ -258,55 +258,3 @@ export const createEventHandlers = (props: EventHandlersProps) => {
handleEventResize,
};
};
export const updateAttendeesAfterTimeChange = (
event: CalendarEvent,
timeChanged?: boolean,
attendees?: userAttendee[]
): CalendarEvent => {
const { attendee, organizer } = event;
if (!attendee || !organizer) return event;
const organizerAddr = organizer.cal_address;
const markNeedsAction = (att: userAttendee): userAttendee => ({
...att,
partstat: "NEEDS-ACTION",
rsvp: "TRUE",
});
const getExistingOrDefault = (addr: string, fallback: userAttendee) =>
attendee.find((a) => a?.cal_address === addr) ?? fallback;
if (attendees) {
const updatedAttendees = attendees.map((att) => {
const existing = getExistingOrDefault(
att.cal_address,
markNeedsAction(att)
);
return timeChanged ? markNeedsAction(existing) : existing;
});
const organizerEntry = getExistingOrDefault(organizerAddr, {
...organizer,
role: "CHAIR",
cutype: "INDIVIDUAL",
partstat: "NEEDS-ACTION",
rsvp: "TRUE",
});
return {
...event,
attendee: [...updatedAttendees, organizerEntry],
};
}
const updatedAttendees = attendee.map((att) => {
if (att.cal_address === organizerAddr) return att;
return timeChanged ? markNeedsAction(att) : att;
});
return {
...event,
attendee: updatedAttendees,
};
};
+1 -1
View File
@@ -1,5 +1,5 @@
import { useAppDispatch, useAppSelector } from "@/app/hooks";
import { updateAttendeesAfterTimeChange } from "@/components/Calendar/handlers/eventHandlers";
import { updateAttendeesAfterTimeChange } from "@/features/Events/updateEventHelpers/updateAttendeesAfterTimeChange";
import { ResponsiveDialog } from "@/components/Dialog";
import EventFormFields from "@/components/Event/EventFormFields";
import { addDays } from "@/components/Event/utils/dateRules";
@@ -0,0 +1,67 @@
import { userAttendee } from "@/features/User/models/attendee";
import { CalendarEvent } from "../EventsTypes";
export const updateAttendeesAfterTimeChange = (
event: CalendarEvent,
timeChanged?: boolean,
attendees?: userAttendee[]
): CalendarEvent => {
const { attendee, organizer } = event;
if (!attendee) return event;
const organizerAddr = organizer?.cal_address;
const markNeedsAction = (att: userAttendee): userAttendee => ({
...att,
partstat: "NEEDS-ACTION",
rsvp: "TRUE",
});
const getExistingOrDefault = (addr: string, fallback: userAttendee) =>
attendee.find((a) => a?.cal_address === addr) ?? fallback;
if (attendees) {
const updatedAttendees = attendees.map((att) => {
const existing = getExistingOrDefault(
att.cal_address,
markNeedsAction(att)
);
return timeChanged ? markNeedsAction(existing) : existing;
});
// Only append organizer entry if organizer exists
const organizerEntry =
organizer && organizerAddr
? getExistingOrDefault(organizerAddr, {
...organizer,
role: "CHAIR",
cutype: "INDIVIDUAL",
partstat: "NEEDS-ACTION",
rsvp: "TRUE",
})
: null;
const deduped = organizerEntry
? updatedAttendees.filter(
(attendee) => attendee.cal_address !== organizerAddr
)
: updatedAttendees;
return {
...event,
attendee: organizerEntry
? [...deduped, organizerEntry]
: updatedAttendees,
};
}
const updatedAttendees = attendee.map((att) => {
if (organizerAddr && att.cal_address === organizerAddr) return att;
return timeChanged ? markNeedsAction(att) : att;
});
return {
...event,
attendee: updatedAttendees,
};
};