diff --git a/__test__/features/Events/eventUtils.test.ts b/__test__/features/Events/eventUtils.test.ts index 74812fd..822d4e1 100644 --- a/__test__/features/Events/eventUtils.test.ts +++ b/__test__/features/Events/eventUtils.test.ts @@ -76,6 +76,80 @@ describe("parseCalendarEvent", () => { ]); }); + it("dedupes attendees addresses correctly", () => { + const rawData = [ + ["UID", {}, "text", "event-1"], + ["DTSTART", {}, "date-time", "2025-07-18T09:00:00Z"], + ["DTEND", {}, "date-time", "2025-07-18T10:00:00Z"], + ["SUMMARY", {}, "text", "Team Meeting"], + ["DESCRIPTION", {}, "text", "Discuss roadmap"], + ["LOCATION", {}, "text", "Zoom"], + ["ORGANIZER", { cn: "Alice" }, "cal-address", "alice@example.com"], + [ + "ATTENDEE", + { cn: "Bob", partstat: "ACCEPTED" }, + "cal-address", + "bob@example.com", + ], + [ + "ATTENDEE", + { cn: "Bob", partstat: "ACCEPTED" }, + "cal-address", + "bob@example.com", + ], + [ + "ATTENDEE", + { cn: "Bob", partstat: "ACCEPTED" }, + "cal-address", + "bob@example.com", + ], + ["X-OPENPAAS-VIDEOCONFERENCE", {}, "text", "https://meet.link"], + ["STATUS", {}, "text", "CONFIRMED"], + ["SEQUENCE", {}, "integer", "2"], + ["TRANSP", {}, "text", "OPAQUE"], + ["CLASS", {}, "text", "PUBLIC"], + ["DTSTAMP", {}, "date-time", "2025-07-18T08:00:00Z"], + ] as VObjectProperty[]; + + const result = parseCalendarEvent( + rawData, + baseColor, + calendar, + "/calendars/test.ics" + ); + + expect(result.uid).toBe("event-1"); + expect(result.title).toBe("Team Meeting"); + expect(result.description).toBe("Discuss roadmap"); + expect(result.location).toBe("Zoom"); + expect(result.start).toBe("2025-07-18T09:00:00Z"); + expect(result.end).toBe("2025-07-18T10:00:00Z"); + expect(result.stamp).toBe("2025-07-18T08:00:00Z"); + expect(result.sequence).toBe(2); + expect(result.color).toBe(baseColor); + expect(result.status).toBe("CONFIRMED"); + expect(result.transp).toBe("OPAQUE"); + expect(result.class).toBe("PUBLIC"); + expect(result.x_openpass_videoconference).toBe("https://meet.link"); + expect(result.timezone).not.toBeDefined(); + + expect(result.organizer).toEqual({ + cn: "Alice", + cal_address: "alice@example.com", + }); + + expect(result.attendee).toEqual([ + { + cn: "Bob", + cal_address: "bob@example.com", + partstat: "ACCEPTED", + cutype: "INDIVIDUAL", + role: "REQ-PARTICIPANT", + rsvp: "FALSE", + }, + ]); + }); + it("parses a full event correctly", () => { const rawData = [ ["UID", {}, "text", "event-1"], diff --git a/src/components/Attendees/PeopleSearch.tsx b/src/components/Attendees/PeopleSearch.tsx index 93d017f..759288e 100644 --- a/src/components/Attendees/PeopleSearch.tsx +++ b/src/components/Attendees/PeopleSearch.tsx @@ -297,11 +297,16 @@ export function PeopleSearch({ return; } setInputError(null); - const mapped = value.map((v: string | User) => - typeof v === "string" - ? { email: v.trim(), displayName: v.trim() } - : v - ); + const mapped = value + .map((v: string | User) => + typeof v === "string" + ? { email: v.trim(), displayName: v.trim() } + : v + ) + .filter( + (user, index, self) => + self.findIndex((u) => u.email === user.email) === index + ); onChange(event, mapped); }} slotProps={{ diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts index a8b89f1..112f5fe 100644 --- a/src/features/Events/eventUtils.ts +++ b/src/features/Events/eventUtils.ts @@ -110,16 +110,23 @@ export function parseCalendarEvent( } case "attendee": { const paramsObj = params as Record; - (event.attendee as userAttendee[]).push( - createAttendee({ - cn: paramsObj?.cn, - cal_address: String(value).replace(/^mailto:/i, ""), - partstat: paramsObj?.partstat as userAttendee["partstat"], - rsvp: paramsObj?.rsvp as userAttendee["rsvp"], - role: paramsObj?.role as userAttendee["role"], - cutype: paramsObj?.cutype as userAttendee["cutype"], - }) - ); + if ( + !event.attendee?.find( + (attendee) => + attendee.cal_address === String(value).replace(/^mailto:/i, "") + ) + ) { + (event.attendee as userAttendee[]).push( + createAttendee({ + cn: paramsObj?.cn, + cal_address: String(value).replace(/^mailto:/i, ""), + partstat: paramsObj?.partstat as userAttendee["partstat"], + rsvp: paramsObj?.rsvp as userAttendee["rsvp"], + role: paramsObj?.role as userAttendee["role"], + cutype: paramsObj?.cutype as userAttendee["cutype"], + }) + ); + } break; } case "dtstamp":