diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx
index 5127fec..742f94b 100644
--- a/__test__/features/Events/EventDisplay.test.tsx
+++ b/__test__/features/Events/EventDisplay.test.tsx
@@ -819,6 +819,616 @@ describe("Event Preview Display", () => {
expect(screen.getByText("Calendar")).toBeInTheDocument();
});
+
+ describe("Attendee Preview Display", () => {
+ const mockOnClose = jest.fn();
+ const day = new Date("2025-01-15T10:00:00.000Z");
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (window as any).MAIL_SPA_URL = null;
+ });
+
+ const createStateWithAttendees = (attendees: any[]) => ({
+ user: {
+ userData: {
+ sub: "test",
+ email: "test@test.com",
+ sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
+ openpaasId: "667037022b752d0026472254",
+ },
+ organiserData: {
+ cn: "test",
+ cal_address: "test@test.com",
+ },
+ },
+ calendars: {
+ list: {
+ "667037022b752d0026472254/cal1": {
+ id: "667037022b752d0026472254/cal1",
+ name: "Calendar",
+ color: "#FF0000",
+ events: {
+ event1: {
+ uid: "event1",
+ title: "Test Event",
+ calId: "667037022b752d0026472254/cal1",
+ start: day.toISOString(),
+ end: day.toISOString(),
+ organizer: {
+ cn: "organizer",
+ cal_address: "organizer@test.com",
+ },
+ attendee: attendees,
+ },
+ },
+ ownerEmails: ["test@test.com"],
+ },
+ },
+ pending: false,
+ },
+ });
+
+ describe("Guest count display", () => {
+ it("displays correct guest count including organizer", () => {
+ const attendees = [
+ {
+ cn: "organizer",
+ cal_address: "organizer@test.com",
+ partstat: "ACCEPTED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 1",
+ cal_address: "guest1@test.com",
+ partstat: "NEEDS-ACTION",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 2",
+ cal_address: "guest2@test.com",
+ partstat: "NEEDS-ACTION",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ ];
+
+ const state = createStateWithAttendees(attendees);
+ renderWithProviders(
+ ,
+ state
+ );
+
+ // Should display "3 guests" (organizer + 2 guests)
+ expect(screen.getByText(/3/)).toBeInTheDocument();
+ });
+ });
+
+ describe("All attendees with single status", () => {
+ it("displays only yes count when all attendees accepted", () => {
+ const attendees = [
+ {
+ cn: "organizer",
+ cal_address: "organizer@test.com",
+ partstat: "ACCEPTED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 1",
+ cal_address: "guest1@test.com",
+ partstat: "ACCEPTED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 2",
+ cal_address: "guest2@test.com",
+ partstat: "ACCEPTED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ ];
+
+ const state = createStateWithAttendees(attendees);
+ renderWithProviders(
+ ,
+ state
+ );
+
+ expect(
+ screen.getByText("eventPreview.yesCount(count=3)")
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.maybeCount/i)
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.needActionCount/i)
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.noCount\b/i)
+ ).not.toBeInTheDocument();
+ });
+
+ it("displays only maybe count when all attendees tentative", () => {
+ const attendees = [
+ {
+ cn: "organizer",
+ cal_address: "organizer@test.com",
+ partstat: "TENTATIVE",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 1",
+ cal_address: "guest1@test.com",
+ partstat: "TENTATIVE",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ ];
+
+ const state = createStateWithAttendees(attendees);
+ renderWithProviders(
+ ,
+ state
+ );
+
+ expect(
+ screen.getByText("eventPreview.maybeCount(count=2)")
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.yesCount/i)
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.needActionCount/i)
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.noCount\b/i)
+ ).not.toBeInTheDocument();
+ });
+
+ it("displays only no count when all attendees declined", () => {
+ const attendees = [
+ {
+ cn: "organizer",
+ cal_address: "organizer@test.com",
+ partstat: "DECLINED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 1",
+ cal_address: "guest1@test.com",
+ partstat: "DECLINED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 2",
+ cal_address: "guest2@test.com",
+ partstat: "DECLINED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 3",
+ cal_address: "guest3@test.com",
+ partstat: "DECLINED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ ];
+
+ const state = createStateWithAttendees(attendees);
+ renderWithProviders(
+ ,
+ state
+ );
+
+ expect(
+ screen.getByText("eventPreview.noCount(count=4)")
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.yesCount/i)
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.maybeCount/i)
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.needActionCount/i)
+ ).not.toBeInTheDocument();
+ });
+
+ it("displays only waiting count when all attendees need action", () => {
+ const attendees = [
+ {
+ cn: "organizer",
+ cal_address: "organizer@test.com",
+ partstat: "NEEDS-ACTION",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 1",
+ cal_address: "guest1@test.com",
+ partstat: "NEEDS-ACTION",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ ];
+
+ const state = createStateWithAttendees(attendees);
+ renderWithProviders(
+ ,
+ state
+ );
+
+ expect(
+ screen.getByText("eventPreview.needActionCount(count=2)")
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.yesCount/i)
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.maybeCount/i)
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.noCount/i)
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe("No display when count is 0", () => {
+ it("does not display attendee preview when no attendees", () => {
+ const state = createStateWithAttendees([]);
+ renderWithProviders(
+ ,
+ state
+ );
+
+ expect(
+ screen.queryByTestId("PeopleAltOutlinedIcon")
+ ).not.toBeInTheDocument();
+ expect(screen.queryByText(/guests/i)).not.toBeInTheDocument();
+ });
+ });
+
+ describe("Regression tests: mixed statuses with correct counts", () => {
+ it("displays all statuses when attendees have mixed responses", () => {
+ const attendees = [
+ {
+ cn: "organizer",
+ cal_address: "organizer@test.com",
+ partstat: "ACCEPTED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 1",
+ cal_address: "guest1@test.com",
+ partstat: "ACCEPTED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 2",
+ cal_address: "guest2@test.com",
+ partstat: "TENTATIVE",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 3",
+ cal_address: "guest3@test.com",
+ partstat: "NEEDS-ACTION",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 4",
+ cal_address: "guest4@test.com",
+ partstat: "DECLINED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ ];
+
+ const state = createStateWithAttendees(attendees);
+ renderWithProviders(
+ ,
+ state
+ );
+
+ expect(
+ screen.getByText(/eventPreview.yesCount\(count\=2\)/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/eventPreview.maybeCount\(count\=1\)/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/eventPreview.needActionCount\(count\=1\)/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/eventPreview.noCount\(count\=1\)/i)
+ ).toBeInTheDocument();
+ });
+
+ it("displays correct counts with multiple yes and maybe", () => {
+ const attendees = [
+ {
+ cn: "Guest 1",
+ cal_address: "guest1@test.com",
+ partstat: "ACCEPTED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 2",
+ cal_address: "guest2@test.com",
+ partstat: "ACCEPTED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 3",
+ cal_address: "guest3@test.com",
+ partstat: "ACCEPTED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 4",
+ cal_address: "guest4@test.com",
+ partstat: "TENTATIVE",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 5",
+ cal_address: "guest5@test.com",
+ partstat: "TENTATIVE",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ ];
+
+ const state = createStateWithAttendees(attendees);
+ renderWithProviders(
+ ,
+ state
+ );
+
+ expect(
+ screen.getByText(/eventPreview\.yesCount\(count\=3\)/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/eventPreview\.maybeCount\(count\=2\)/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.noCount/i)
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.needActionCount/i)
+ ).not.toBeInTheDocument();
+ });
+
+ it("displays correct counts with waiting and declined", () => {
+ const attendees = [
+ {
+ cn: "Guest 1",
+ cal_address: "guest1@test.com",
+ partstat: "NEEDS-ACTION",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 2",
+ cal_address: "guest2@test.com",
+ partstat: "NEEDS-ACTION",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 3",
+ cal_address: "guest3@test.com",
+ partstat: "NEEDS-ACTION",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 4",
+ cal_address: "guest4@test.com",
+ partstat: "DECLINED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ ];
+
+ const state = createStateWithAttendees(attendees);
+ renderWithProviders(
+ ,
+ state
+ );
+
+ expect(
+ screen.getByText(/eventPreview.needActionCount\(count\=3\)/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/eventPreview.noCount\(count\=1\)/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.yesCount/i)
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.maybeCount/i)
+ ).not.toBeInTheDocument();
+ });
+
+ it("does not display status categories with zero count", () => {
+ const attendees = [
+ {
+ cn: "Guest 1",
+ cal_address: "guest1@test.com",
+ partstat: "ACCEPTED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "Guest 2",
+ cal_address: "guest2@test.com",
+ partstat: "DECLINED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ ];
+
+ const state = createStateWithAttendees(attendees);
+ renderWithProviders(
+ ,
+ state
+ );
+
+ expect(
+ screen.getByText(/eventPreview.yesCount\(count\=1\)/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/eventPreview.noCount\(count\=1\)/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.maybeCount/i)
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/eventPreview.needActionCount/i)
+ ).not.toBeInTheDocument();
+ });
+
+ it("handles large number of attendees correctly", () => {
+ const attendees = Array.from({ length: 15 }, (_, i) => ({
+ cn: `Guest ${i}`,
+ cal_address: `guest${i}@test.com`,
+ partstat:
+ i < 8
+ ? "ACCEPTED"
+ : i < 11
+ ? "TENTATIVE"
+ : i < 13
+ ? "NEEDS-ACTION"
+ : "DECLINED",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ }));
+
+ const state = createStateWithAttendees(attendees);
+ renderWithProviders(
+ ,
+ state
+ );
+
+ expect(
+ screen.getByText(/eventPreview.yesCount\(count\=8\)/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/eventPreview.maybeCount\(count\=3\)/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/eventPreview.needActionCount\(count\=2\)/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/eventPreview.noCount\(count\=2\)/i)
+ ).toBeInTheDocument();
+ });
+ });
+ });
+
describe("BUGFIX", () => {
it("doesnt render anything next to date of all day preview", () => {
const allDayState = {
diff --git a/src/features/Events/EventDisplayPreview.tsx b/src/features/Events/EventDisplayPreview.tsx
index 07d5a79..4c1a4f9 100644
--- a/src/features/Events/EventDisplayPreview.tsx
+++ b/src/features/Events/EventDisplayPreview.tsx
@@ -48,6 +48,8 @@ import { dlEvent } from "./EventApi";
import { CalendarEvent } from "./EventsTypes";
import EventUpdateModal from "./EventUpdateModal";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
+import { userAttendee } from "../User/userDataTypes";
+
export default function EventPreviewModal({
eventId,
calId,
@@ -89,6 +91,7 @@ export default function EventPreviewModal({
undefined
);
const [afterChoiceFunc, setAfterChoiceFunc] = useState();
+ const attendeePreview = makeAttendeePreview(event.attendee, t);
const [toggleActionMenu, setToggleActionMenu] = useState(
null
@@ -494,23 +497,13 @@ export default function EventPreviewModal({
{t("eventPreview.guests", {
- count: attendees.length,
+ count: event.attendee.length,
})}
- {t("eventPreview.yesCount", {
- count: attendees.filter(
- (a) => a.partstat === "ACCEPTED"
- ).length,
- })}
- ,{" "}
- {t("eventPreview.noCount", {
- count: attendees.filter(
- (a) => a.partstat === "DECLINED"
- ).length,
- })}
+ {attendeePreview}
{!showAllAttendees && (
@@ -843,3 +836,30 @@ function formatEnd(
});
}
}
+
+export function makeAttendeePreview(attendees: userAttendee[], t: Function) {
+ const attendeePreview = [];
+ const yesCount = attendees?.filter((a) => a.partstat === "ACCEPTED").length;
+ const noCount = attendees?.filter((a) => a.partstat === "DECLINED").length;
+ const maybeCount = attendees?.filter(
+ (a) => a.partstat === "TENTATIVE"
+ ).length;
+ const needActionCount = attendees?.filter(
+ (a) => a.partstat === "NEEDS-ACTION"
+ ).length;
+ if (yesCount) {
+ attendeePreview.push(t("eventPreview.yesCount", { count: yesCount }));
+ }
+ if (maybeCount) {
+ attendeePreview.push(t("eventPreview.maybeCount", { count: maybeCount }));
+ }
+ if (needActionCount) {
+ attendeePreview.push(
+ t("eventPreview.needActionCount", { count: needActionCount })
+ );
+ }
+ if (noCount) {
+ attendeePreview.push(t("eventPreview.noCount", { count: noCount }));
+ }
+ return attendeePreview.join(", ");
+}
diff --git a/src/locales/en.json b/src/locales/en.json
index 16cd30f..2c35f38 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -207,6 +207,8 @@
"joinVideo": "Join the video conference",
"guests": "%{count} guests",
"yesCount": "%{count} yes",
+ "maybeCount": "%{count} maybe",
+ "needActionCount": "%{count} waiting",
"noCount": "%{count} no",
"recurrentEvent": "Recurrent Event",
"freq": {
diff --git a/src/locales/fr.json b/src/locales/fr.json
index 69b7f10..ae3dba9 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -205,8 +205,10 @@
"showMore": "Afficher plus",
"showLess": "Afficher moins",
"joinVideo": "Rejoindre la visioconférence",
- "guests": "%{count} invités",
+ "guests": "%{count} participants",
"yesCount": "%{count} oui",
+ "maybeCount": "%{count} peut-être",
+ "needActionCount": "%{count} en attente",
"noCount": "%{count} non",
"recurrentEvent": "Événement récurrent",
"freq": {
diff --git a/src/locales/ru.json b/src/locales/ru.json
index 784eaaf..6b5e016 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -207,6 +207,8 @@
"joinVideo": "Присоединиться к видеоконференции",
"guests": "%{count} гостей",
"yesCount": "%{count} да",
+ "maybeCount": "%{count} может быть",
+ "needActionCount": "%{count} в ожидании",
"noCount": "%{count} нет",
"recurrentEvent": "Повторяющееся событие",
"freq": {
diff --git a/src/locales/vi.json b/src/locales/vi.json
index 4133ac6..8e14336 100644
--- a/src/locales/vi.json
+++ b/src/locales/vi.json
@@ -207,6 +207,8 @@
"joinVideo": "Tham gia cuộc họp video",
"guests": "%{count} khách",
"yesCount": "%{count} đồng ý",
+ "maybeCount": "%{count} có thể",
+ "needActionCount": "%{count} đang chờ",
"noCount": "%{count} từ chối",
"recurrentEvent": "Sự kiện lặp lại",
"freq": {