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";
|
} from "@/features/Calendars/services";
|
||||||
import { getEvent } from "@/features/Events/EventApi";
|
import { getEvent } from "@/features/Events/EventApi";
|
||||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
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 { createAttendee } from "@/features/User/models/attendee.mapper";
|
||||||
import { getDeltaInMilliseconds } from "@/utils/dateUtils";
|
import { getDeltaInMilliseconds } from "@/utils/dateUtils";
|
||||||
import {
|
import {
|
||||||
@@ -258,55 +258,3 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
|||||||
handleEventResize,
|
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 { 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 { ResponsiveDialog } from "@/components/Dialog";
|
||||||
import EventFormFields from "@/components/Event/EventFormFields";
|
import EventFormFields from "@/components/Event/EventFormFields";
|
||||||
import { addDays } from "@/components/Event/utils/dateRules";
|
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