460 import event without organiser (#579)
Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -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,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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user