diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx index b8ec5a4..6e3a999 100644 --- a/__test__/features/Events/EventDisplay.test.tsx +++ b/__test__/features/Events/EventDisplay.test.tsx @@ -14,6 +14,8 @@ import { stringToColor, stringAvatar, } from "../../../src/components/Event/utils/eventUtils"; +import * as appHooks from "../../../src/app/hooks"; +import { ThunkDispatch } from "@reduxjs/toolkit"; describe("Event Preview Display", () => { const mockOnClose = jest.fn(); @@ -561,6 +563,200 @@ describe("Event Preview Display", () => { expect(mockOpen).toHaveBeenCalledWith(expectedUrl); }); + + describe("Owner Email Permissions", () => { + const today = new Date(); + const start = new Date(today); + start.setHours(10, 0, 0, 0); + const end = new Date(today); + end.setHours(11, 0, 0, 0); + + beforeEach(() => { + jest.clearAllMocks(); + const dispatch = jest.fn() as ThunkDispatch; + jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch); + }); + + const createPreloadedState = ( + userEmail: string, + ownerEmails: string[], + organizerEmail: string + ) => ({ + user: { + userData: { + sub: "test", + email: userEmail, + sid: "mockSid", + openpaasId: "667037022b752d0026472254", + }, + tokens: { + accessToken: "token", + }, + }, + calendars: { + list: { + "667037022b752d0026472254/cal1": { + name: "Calendar 1", + id: "667037022b752d0026472254/cal1", + color: "#FF0000", + ownerEmails: ownerEmails, + events: { + event1: { + id: "event1", + calId: "667037022b752d0026472254/cal1", + uid: "event1", + title: "Test Event", + start: start.toISOString(), + end: end.toISOString(), + partstat: "ACCEPTED", + organizer: { + cn: "Organizer", + cal_address: organizerEmail, + }, + attendee: [ + { + cn: "Organizer", + partstat: "ACCEPTED", + rsvp: "TRUE", + role: "REQ-PARTICIPANT", + cutype: "INDIVIDUAL", + cal_address: organizerEmail, + }, + ], + }, + }, + }, + }, + pending: false, + }, + }); + + it("should show edit button when user email matches organizer and is in ownerEmails", async () => { + const preloadedState = createPreloadedState( + "owner@test.com", + ["owner@test.com"], + "owner@test.com" + ); + + const mockOnClose = jest.fn(); + renderWithProviders( + , + preloadedState + ); + + await waitFor(() => { + const editButton = screen.queryByTestId("EditIcon"); + expect(editButton).toBeInTheDocument(); + }); + }); + + it("should NOT show edit button when user is organizer but not in owner of calendar", async () => { + const preloadedState = createPreloadedState( + "organizer@test.com", + ["other@test.com"], // organizer not in list + "organizer@test.com" + ); + + const mockOnClose = jest.fn(); + renderWithProviders( + , + preloadedState + ); + + await waitFor(() => { + const editButton = screen.queryByTestId("EditIcon"); + expect(editButton).not.toBeInTheDocument(); + }); + }); + + it("should NOT show edit button when user is in ownerEmails but not organizer", async () => { + const preloadedState = createPreloadedState( + "owner@test.com", + ["owner@test.com"], + "other@test.com" // different organizer + ); + + const mockOnClose = jest.fn(); + renderWithProviders( + , + preloadedState + ); + + await waitFor(() => { + const editButton = screen.queryByTestId("EditIcon"); + expect(editButton).not.toBeInTheDocument(); + }); + }); + + it("should handle calendars without ownerEmails property gracefully", async () => { + const preloadedStateWithoutOwnerEmails = { + user: { + userData: { + sub: "test", + email: "test@test.com", + sid: "mockSid", + openpaasId: "667037022b752d0026472254", + }, + tokens: { + accessToken: "token", + }, + }, + calendars: { + list: { + "667037022b752d0026472254/cal1": { + name: "Calendar 1", + id: "667037022b752d0026472254/cal1", + color: "#FF0000", + // ownerEmails missing + events: { + event1: { + id: "event1", + calId: "667037022b752d0026472254/cal1", + uid: "event1", + title: "Test Event", + start: start.toISOString(), + end: end.toISOString(), + organizer: { + cn: "Test", + cal_address: "test@test.com", + }, + }, + }, + }, + }, + pending: false, + }, + }; + + const mockOnClose = jest.fn(); + expect(() => { + renderWithProviders( + , + preloadedStateWithoutOwnerEmails + ); + }).not.toThrow(); + }); + }); }); describe("Event Full Display", () => { diff --git a/__test__/features/Events/TempUpdate.test.tsx b/__test__/features/Events/TempUpdate.test.tsx new file mode 100644 index 0000000..c355233 --- /dev/null +++ b/__test__/features/Events/TempUpdate.test.tsx @@ -0,0 +1,826 @@ +import { jest } from "@jest/globals"; +import { ThunkDispatch } from "@reduxjs/toolkit"; +import "@testing-library/jest-dom"; +import { screen, waitFor, fireEvent } from "@testing-library/react"; +import * as appHooks from "../../../src/app/hooks"; +import * as eventUtils from "../../../src/components/Event/utils/eventUtils"; +import * as userApi from "../../../src/features/User/userAPI"; +import * as calendarUtils from "../../../src/components/Calendar/utils/calendarUtils"; +import * as eventThunks from "../../../src/features/Calendars/CalendarSlice"; +import EventPreviewModal from "../../../src/features/Events/EventDisplayPreview"; +import EventPopover from "../../../src/features/Events/EventModal"; +import EventUpdateModal from "../../../src/features/Events/EventUpdateModal"; +import CalendarLayout from "../../../src/components/Calendar/CalendarLayout"; +import { renderWithProviders } from "../../utils/Renderwithproviders"; +import { SpiedFunction } from "jest-mock"; +import { Calendars } from "../../../src/features/Calendars/CalendarTypes"; +import { CalendarEvent } from "../../../src/features/Events/EventsTypes"; +import { DateSelectArg } from "@fullcalendar/core"; + +describe("Update tempcalendars called with correct params", () => { + const today = new Date(); + const start = new Date(today); + start.setHours(10, 0, 0, 0); + const end = new Date(today); + end.setHours(11, 0, 0, 0); + + let refreshCalendarsSpy: SpiedFunction< + ( + dispatch: ThunkDispatch, + calendars: Calendars[], + calendarRange: { start: Date; end: Date }, + calType?: "temp" + ) => Promise + >; + let refreshSingularCalendarSpy: SpiedFunction< + ( + dispatch: ThunkDispatch, + calendar: Calendars, + calendarRange: { start: Date; end: Date }, + calType?: "temp" + ) => Promise + >; + let updateTempCalendarSpy: SpiedFunction< + ( + tempcalendars: Record, + event: CalendarEvent, + dispatch: ThunkDispatch, + calendarRange: { start: Date; end: Date } + ) => Promise + >; + + beforeEach(() => { + jest.clearAllMocks(); + const dispatch = jest.fn() as ThunkDispatch; + jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch); + refreshCalendarsSpy = jest.spyOn(eventUtils, "refreshCalendars"); + updateTempCalendarSpy = jest.spyOn(calendarUtils, "updateTempCalendar"); + refreshSingularCalendarSpy = jest.spyOn( + eventUtils, + "refreshSingularCalendar" + ); + }); + + afterEach(() => { + refreshCalendarsSpy.mockRestore(); + updateTempCalendarSpy.mockRestore(); + refreshSingularCalendarSpy.mockRestore(); + }); + + const createPreloadedState = (withTempList = true) => ({ + user: { + userData: { + sub: "test", + email: "owner@test.com", + sid: "mockSid", + openpaasId: "667037022b752d0026472254", + }, + organiserData: { + cn: "Owner", + cal_address: "owner@test.com", + }, + tokens: { + accessToken: "token", + }, + }, + calendars: { + list: { + "667037022b752d0026472254/cal1": { + name: "Calendar 1", + id: "667037022b752d0026472254/cal1", + color: "#FF0000", + ownerEmails: ["owner@test.com"], + events: { + event1: { + id: "event1", + calId: "667037022b752d0026472254/cal1", + uid: "event1", + title: "Test Event", + start: start.toISOString(), + end: end.toISOString(), + attendee: [{ cal_address: "attendee@test.com" }], + organizer: { + cn: "Owner", + cal_address: "owner@test.com", + }, + }, + }, + }, + }, + templist: withTempList + ? { + temp1: { + id: "temp1", + name: "Temp Calendar", + color: "#00FF00", + events: {}, + ownerEmails: ["attendee@test.com"], + }, + } + : undefined, + pending: false, + }, + }); + + it("should call updateTempCalendar with correct params after event deletion", async () => { + const preloadedState = createPreloadedState(true); + + const mockOnClose = jest.fn(); + renderWithProviders( + , + preloadedState + ); + + await waitFor(() => { + fireEvent.click(screen.getByTestId("MoreVertIcon")); + expect(screen.getByText("Delete event")).toBeInTheDocument(); + }); + + const deleteMenuItem = screen.getByText("Delete event"); + fireEvent.click(deleteMenuItem); + + await waitFor(() => + expect(updateTempCalendarSpy).toHaveBeenCalledWith( + expect.objectContaining({ + temp1: expect.objectContaining({ id: "temp1" }), + }), + expect.objectContaining({ + attendee: [ + expect.objectContaining({ cal_address: "attendee@test.com" }), + ], + }), + expect.any(Function), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }) + ) + ); + + await waitFor(() => + expect(refreshSingularCalendarSpy).toHaveBeenCalledWith( + expect.any(Function), + expect.objectContaining({ id: "temp1" }), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }), + "temp" + ) + ); + }); + + it("should NOT call updateTempCalendar when templist is undefined", async () => { + const preloadedState = createPreloadedState(false); + + const mockOnClose = jest.fn(); + renderWithProviders( + , + preloadedState + ); + + await waitFor(() => { + fireEvent.click(screen.getByTestId("MoreVertIcon")); + expect(screen.getByText("Delete event")).toBeInTheDocument(); + }); + + const deleteMenuItem = screen.getByText("Delete event"); + fireEvent.click(deleteMenuItem); + + await waitFor(() => { + expect(updateTempCalendarSpy).not.toHaveBeenCalled(); + expect(refreshSingularCalendarSpy).not.toHaveBeenCalled(); + }); + }); + + it("should call updateTempCalendar with correct params after event creation", async () => { + const preloadedState = createPreloadedState(true); + const defaultSelectedRange = { + startStr: "2025-07-18T09:00Z", + endStr: "2025-07-18T10:00Z", + start: new Date("2025-07-18T09:00Z"), + end: new Date("2025-07-18T10:00Z"), + allDay: false, + resource: undefined, + } as unknown as DateSelectArg; + jest.spyOn(userApi, "searchUsers"); + renderWithProviders( + , + preloadedState + ); + + await waitFor(() => { + expect(screen.getByRole("dialog")).toBeInTheDocument(); + }); + + const titleInput = screen.getByLabelText(/title/i); + fireEvent.change(titleInput, { target: { value: "New Event" } }); + + const attendeeInput = screen.getByLabelText( + /Start typing a name or email/i + ); + fireEvent.change(attendeeInput, { target: { value: "attendee@test.com" } }); + fireEvent.keyDown(attendeeInput, { key: "Enter", code: "Enter" }); + + const saveButton = screen.getByText(/save/i); + fireEvent.click(saveButton); + + await waitFor(() => + expect(updateTempCalendarSpy).toHaveBeenCalledWith( + expect.objectContaining({ + temp1: expect.objectContaining({ id: "temp1" }), + }), + expect.objectContaining({ + title: "New Event", + }), + expect.any(Function), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }) + ) + ); + + await waitFor(() => + expect(refreshSingularCalendarSpy).toHaveBeenCalledWith( + expect.any(Function), + expect.objectContaining({ id: "temp1" }), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }), + "temp" + ) + ); + }); + + it("should call updateTempCalendar with correct params after event update", async () => { + const preloadedState = createPreloadedState(true); + + const mockOnClose = jest.fn(); + renderWithProviders( + , + preloadedState + ); + + await waitFor(() => { + expect(screen.getByRole("dialog")).toBeInTheDocument(); + }); + + const saveButton = screen.getByText(/save/i); + fireEvent.click(saveButton); + + await waitFor(() => + expect(updateTempCalendarSpy).toHaveBeenCalledWith( + expect.objectContaining({ + temp1: expect.objectContaining({ id: "temp1" }), + }), + expect.objectContaining({ + attendee: [ + expect.objectContaining({ cal_address: "attendee@test.com" }), + ], + }), + expect.any(Function), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }) + ) + ); + + await waitFor(() => + expect(refreshSingularCalendarSpy).toHaveBeenCalledWith( + expect.any(Function), + expect.objectContaining({ id: "temp1" }), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }), + "temp" + ) + ); + }); + + it("should call updateTempCalendar with correct params after recurring event instance update", async () => { + const preloadedStateWithRecurring = { + ...createPreloadedState(true), + calendars: { + ...createPreloadedState(true).calendars, + list: { + "667037022b752d0026472254/cal1": { + name: "Calendar 1", + id: "667037022b752d0026472254/cal1", + color: "#FF0000", + ownerEmails: ["owner@test.com"], + events: { + event1: { + id: "event1", + calId: "667037022b752d0026472254/cal1", + uid: "event1", + title: "Recurring Event", + start: start.toISOString(), + end: end.toISOString(), + attendee: [{ cal_address: "attendee@test.com" }], + rrule: { + freq: "WEEKLY", + }, + organizer: { + cn: "Owner", + cal_address: "owner@test.com", + }, + }, + }, + }, + }, + }, + }; + + const mockOnClose = jest.fn(); + renderWithProviders( + , + preloadedStateWithRecurring + ); + + await waitFor(() => { + expect(screen.getByRole("dialog")).toBeInTheDocument(); + }); + + const saveButton = screen.getByText(/save/i); + fireEvent.click(saveButton); + + await waitFor(() => + expect(updateTempCalendarSpy).toHaveBeenCalledWith( + expect.objectContaining({ + temp1: expect.objectContaining({ id: "temp1" }), + }), + expect.objectContaining({ + attendee: [ + expect.objectContaining({ cal_address: "attendee@test.com" }), + ], + }), + expect.any(Function), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }) + ) + ); + + await waitFor(() => + expect(refreshSingularCalendarSpy).toHaveBeenCalledWith( + expect.any(Function), + expect.objectContaining({ id: "temp1" }), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }), + "temp" + ) + ); + }); + + it("should call updateTempCalendar with correct params after recurring series update", async () => { + const preloadedStateWithRecurring = { + ...createPreloadedState(true), + calendars: { + ...createPreloadedState(true).calendars, + list: { + "667037022b752d0026472254/cal1": { + name: "Calendar 1", + id: "667037022b752d0026472254/cal1", + color: "#FF0000", + ownerEmails: ["owner@test.com"], + events: { + event1: { + id: "event1", + calId: "667037022b752d0026472254/cal1", + uid: "event1", + title: "Recurring Event", + start: start.toISOString(), + end: end.toISOString(), + attendee: [{ cal_address: "attendee@test.com" }], + rrule: { + freq: "DAILY", + }, + organizer: { + cn: "Owner", + cal_address: "owner@test.com", + }, + }, + }, + }, + }, + }, + }; + + const mockOnClose = jest.fn(); + renderWithProviders( + , + preloadedStateWithRecurring + ); + + await waitFor(() => { + expect(screen.getByRole("dialog")).toBeInTheDocument(); + }); + + const saveButton = screen.getByText(/save/i); + fireEvent.click(saveButton); + + await waitFor(() => + expect(updateTempCalendarSpy).toHaveBeenCalledWith( + expect.objectContaining({ + temp1: expect.objectContaining({ id: "temp1" }), + }), + expect.objectContaining({ + attendee: [ + expect.objectContaining({ cal_address: "attendee@test.com" }), + ], + }), + expect.any(Function), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }) + ) + ); + + await waitFor(() => + expect(refreshSingularCalendarSpy).toHaveBeenCalledWith( + expect.any(Function), + expect.objectContaining({ id: "temp1" }), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }), + "temp" + ) + ); + }); + + it("should call refreshCalendars with 'temp' param in CalendarLayout handleRefresh", async () => { + const preloadedState = createPreloadedState(true); + + renderWithProviders(, preloadedState); + + fireEvent.click(screen.getByTestId("RefreshIcon")); + + await waitFor(() => { + // Should call refreshCalendars for regular calendars + expect(refreshCalendarsSpy).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Array), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }) + ); + + // Should also call refreshCalendars with 'temp' for templist + expect(refreshCalendarsSpy).toHaveBeenCalledWith( + expect.any(Function), + expect.arrayContaining([expect.objectContaining({ id: "temp1" })]), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }), + "temp" + ); + }); + }); + + it("should only refresh temp calendars where attendees match ownerEmails", async () => { + const preloadedStateMultipleTempCals = { + ...createPreloadedState(true), + calendars: { + ...createPreloadedState(true).calendars, + templist: { + temp1: { + id: "temp1", + name: "Temp Calendar 1", + color: "#00FF00", + events: {}, + ownerEmails: ["attendee@test.com"], // matches event attendee + }, + temp2: { + id: "temp2", + name: "Temp Calendar 2", + color: "#0000FF", + events: {}, + ownerEmails: ["other@test.com"], // does NOT match event attendee + }, + }, + }, + }; + + const mockOnClose = jest.fn(); + renderWithProviders( + , + preloadedStateMultipleTempCals + ); + + await waitFor(() => { + fireEvent.click(screen.getByTestId("MoreVertIcon")); + expect(screen.getByText("Delete event")).toBeInTheDocument(); + }); + + const deleteMenuItem = screen.getByText("Delete event"); + fireEvent.click(deleteMenuItem); + + await waitFor(() => { + // Should only refresh temp1, not temp2 + const temp1Calls = refreshSingularCalendarSpy.mock.calls.filter( + (call) => call[1]?.id === "temp1" + ); + const temp2Calls = refreshSingularCalendarSpy.mock.calls.filter( + (call) => call[1]?.id === "temp2" + ); + + expect(temp1Calls.length).toBeGreaterThan(0); + expect(temp2Calls.length).toBe(0); + }); + }); + + it("should call emptyEventsCal with correct params before refreshing", async () => { + const emptyEventsCalSpy = jest.spyOn(eventThunks, "emptyEventsCal"); + + const preloadedState = createPreloadedState(true); + + const mockOnClose = jest.fn(); + renderWithProviders( + , + preloadedState + ); + + await waitFor(() => { + fireEvent.click(screen.getByTestId("MoreVertIcon")); + expect(screen.getByText("Delete event")).toBeInTheDocument(); + }); + + const deleteMenuItem = screen.getByText("Delete event"); + fireEvent.click(deleteMenuItem); + + await waitFor(() => { + // emptyEventsCal should be called with calType: "temp" for singular calendar refresh + expect(emptyEventsCalSpy).toHaveBeenCalledWith( + expect.objectContaining({ + calId: "temp1", + calType: "temp", + }) + ); + }); + + emptyEventsCalSpy.mockRestore(); + }); + + it("should handle temp calendar with multiple ownerEmails where one matches", async () => { + const preloadedStateMultipleOwners = { + ...createPreloadedState(true), + calendars: { + ...createPreloadedState(true).calendars, + list: { + "667037022b752d0026472254/cal1": { + name: "Calendar 1", + id: "667037022b752d0026472254/cal1", + color: "#FF0000", + ownerEmails: ["owner@test.com"], + events: { + event1: { + id: "event1", + calId: "667037022b752d0026472254/cal1", + uid: "event1", + title: "Test Event", + start: start.toISOString(), + end: end.toISOString(), + attendee: [{ cal_address: "attendee@test.com" }], + organizer: { + cn: "Owner", + cal_address: "owner@test.com", + }, + }, + }, + }, + }, + templist: { + temp1: { + id: "temp1", + name: "Shared Calendar", + color: "#00FF00", + events: {}, + ownerEmails: [ + "someoneelse@test.com", + "attendee@test.com", // matches + "anotherperson@test.com", + ], + }, + }, + }, + }; + + const mockOnClose = jest.fn(); + renderWithProviders( + , + preloadedStateMultipleOwners + ); + + await waitFor(() => { + fireEvent.click(screen.getByTestId("MoreVertIcon")); + expect(screen.getByText("Delete event")).toBeInTheDocument(); + }); + + const deleteMenuItem = screen.getByText("Delete event"); + fireEvent.click(deleteMenuItem); + + await waitFor(() => { + // Should still refresh temp1 because one ownerEmail matches + const temp1Calls = refreshSingularCalendarSpy.mock.calls.filter( + (call) => call[1]?.id === "temp1" + ); + + expect(temp1Calls.length).toBeGreaterThan(0); + expect(temp1Calls[0][3]).toBe("temp"); + }); + }); + it("should update multiple temp calendars when event has multiple attendees matching different ownerEmails", async () => { + const preloadedStateMultipleAttendees = { + ...createPreloadedState(true), + calendars: { + ...createPreloadedState(true).calendars, + list: { + "667037022b752d0026472254/cal1": { + name: "Calendar 1", + id: "667037022b752d0026472254/cal1", + color: "#FF0000", + ownerEmails: ["owner@test.com"], + events: { + event1: { + id: "event1", + calId: "667037022b752d0026472254/cal1", + uid: "event1", + title: "Test Event", + start: start.toISOString(), + end: end.toISOString(), + attendee: [ + { cal_address: "attendee1@test.com" }, + { cal_address: "attendee2@test.com" }, + { cal_address: "attendee3@test.com" }, + ], + organizer: { + cn: "Owner", + cal_address: "owner@test.com", + }, + }, + }, + }, + }, + templist: { + temp1: { + id: "temp1", + name: "Temp Calendar 1", + color: "#00FF00", + events: {}, + ownerEmails: ["attendee1@test.com"], + }, + temp2: { + id: "temp2", + name: "Temp Calendar 2", + color: "#0000FF", + events: {}, + ownerEmails: ["attendee2@test.com"], + }, + temp3: { + id: "temp3", + name: "Temp Calendar 3", + color: "#FF00FF", + events: {}, + ownerEmails: ["attendee3@test.com"], + }, + temp4: { + id: "temp4", + name: "Temp Calendar 4", + color: "#FFFF00", + events: {}, + ownerEmails: ["nonmatching@test.com"], // should NOT be refreshed + }, + }, + }, + }; + + const mockOnClose = jest.fn(); + renderWithProviders( + , + preloadedStateMultipleAttendees + ); + + await waitFor(() => { + fireEvent.click(screen.getByTestId("MoreVertIcon")); + expect(screen.getByText("Delete event")).toBeInTheDocument(); + }); + + const deleteMenuItem = screen.getByText("Delete event"); + fireEvent.click(deleteMenuItem); + + await waitFor(() => { + // Should be called once with all templist + expect(updateTempCalendarSpy).toHaveBeenCalledWith( + expect.objectContaining({ + temp1: expect.objectContaining({ id: "temp1" }), + temp2: expect.objectContaining({ id: "temp2" }), + temp3: expect.objectContaining({ id: "temp3" }), + temp4: expect.objectContaining({ id: "temp4" }), + }), + expect.objectContaining({ + attendee: expect.arrayContaining([ + expect.objectContaining({ cal_address: "attendee1@test.com" }), + expect.objectContaining({ cal_address: "attendee2@test.com" }), + expect.objectContaining({ cal_address: "attendee3@test.com" }), + ]), + }), + expect.any(Function), + expect.objectContaining({ + start: expect.any(Date), + end: expect.any(Date), + }) + ); + }); + + await waitFor(() => { + // Should refresh temp1, temp2, and temp3 (matching attendees) + const temp1Calls = refreshSingularCalendarSpy.mock.calls.filter( + (call) => call[1]?.id === "temp1" + ); + const temp2Calls = refreshSingularCalendarSpy.mock.calls.filter( + (call) => call[1]?.id === "temp2" + ); + const temp3Calls = refreshSingularCalendarSpy.mock.calls.filter( + (call) => call[1]?.id === "temp3" + ); + const temp4Calls = refreshSingularCalendarSpy.mock.calls.filter( + (call) => call[1]?.id === "temp4" + ); + + expect(temp1Calls.length).toBeGreaterThan(0); + expect(temp2Calls.length).toBeGreaterThan(0); + expect(temp3Calls.length).toBeGreaterThan(0); + expect(temp4Calls.length).toBe(0); // Should NOT be refreshed + + // Verify all matching calendars were refreshed with 'temp' param + expect(temp1Calls[0][3]).toBe("temp"); + expect(temp2Calls[0][3]).toBe("temp"); + expect(temp3Calls[0][3]).toBe("temp"); + }); + }); +}); diff --git a/src/components/Calendar/handlers/eventHandlers.ts b/src/components/Calendar/handlers/eventHandlers.ts index b451a36..0b6c1d0 100644 --- a/src/components/Calendar/handlers/eventHandlers.ts +++ b/src/components/Calendar/handlers/eventHandlers.ts @@ -13,6 +13,7 @@ import { import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils"; import { getEvent } from "../../../features/Events/EventApi"; import { refreshCalendars } from "../../Event/utils/eventUtils"; +import { updateTempCalendar } from "../utils/calendarUtils"; export interface EventHandlersProps { setSelectedRange: (range: DateSelectArg | null) => void; @@ -159,14 +160,12 @@ export const createEventHandlers = (props: EventHandlersProps) => { Object.values(calendars), calendarRange ); - if (tempcalendars) { - await refreshCalendars( - dispatch, - Object.values(tempcalendars), - calendarRange, - "temp" - ); - } + await updateTempCalendar( + tempcalendars, + event, + dispatch, + calendarRange + ); } } ); @@ -176,14 +175,7 @@ export const createEventHandlers = (props: EventHandlersProps) => { putEventAsync({ cal: calendars[newEvent.calId], newEvent }) ); } - if (tempcalendars) { - await refreshCalendars( - dispatch, - Object.values(tempcalendars), - calendarRange, - "temp" - ); - } + await updateTempCalendar(tempcalendars, event, dispatch, calendarRange); }; const handleEventResize = async (arg: any) => { @@ -243,14 +235,12 @@ export const createEventHandlers = (props: EventHandlersProps) => { Object.values(calendars), calendarRange ); - if (tempcalendars) { - await refreshCalendars( - dispatch, - Object.values(tempcalendars), - calendarRange, - "temp" - ); - } + await updateTempCalendar( + tempcalendars, + event, + dispatch, + calendarRange + ); } } ); @@ -259,14 +249,7 @@ export const createEventHandlers = (props: EventHandlersProps) => { putEventAsync({ cal: calendars[newEvent.calId], newEvent }) ); } - if (tempcalendars) { - await refreshCalendars( - dispatch, - Object.values(tempcalendars), - calendarRange, - "temp" - ); - } + await updateTempCalendar(tempcalendars, event, dispatch, calendarRange); }; return { diff --git a/src/components/Calendar/utils/calendarUtils.ts b/src/components/Calendar/utils/calendarUtils.ts index 6992b8e..f20cf79 100644 --- a/src/components/Calendar/utils/calendarUtils.ts +++ b/src/components/Calendar/utils/calendarUtils.ts @@ -4,6 +4,8 @@ import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils"; import { getCalendarDetailAsync } from "../../../features/Calendars/CalendarSlice"; import { SlotLabelContentArg } from "@fullcalendar/core"; import moment from "moment-timezone"; +import { refreshSingularCalendar } from "../../Event/utils/eventUtils"; +import { ThunkDispatch } from "@reduxjs/toolkit"; export const updateSlotLabelVisibility = ( currentTime: Date, @@ -154,3 +156,35 @@ export function getCalendarVisibility(acl: AclEntry[]): "private" | "public" { if (hasRead) return "public"; return "private"; } + +export async function updateTempCalendar( + tempcalendars: Record, + event: CalendarEvent, + dispatch: ThunkDispatch, + calendarRange: { start: Date; end: Date } +) { + if (tempcalendars && event?.attendee) { + const attendeesEmails = event.attendee + .map((a) => a.cal_address) + .filter(Boolean); + + const tempCalendarValues = Object.values(tempcalendars); + + for (const tempCalendar of tempCalendarValues) { + const ownerEmails = tempCalendar.ownerEmails || []; + + // Check if any of the attendees are owners of this temp calendar + const isOwnerAttendee = ownerEmails.some((ownerEmail) => + attendeesEmails.includes(ownerEmail) + ); + if (isOwnerAttendee) { + await refreshSingularCalendar( + dispatch, + tempCalendar, + calendarRange, + "temp" + ); + } + } + } +} diff --git a/src/components/Event/utils/eventUtils.tsx b/src/components/Event/utils/eventUtils.tsx index 454ef75..ea5805b 100644 --- a/src/components/Event/utils/eventUtils.tsx +++ b/src/components/Event/utils/eventUtils.tsx @@ -6,7 +6,7 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { ThunkDispatch } from "@reduxjs/toolkit"; import { - emptyTempCal, + emptyEventsCal, getCalendarDetailAsync, getCalendarsListAsync, } from "../../../features/Calendars/CalendarSlice"; @@ -119,7 +119,7 @@ export async function refreshCalendars( calType?: "temp" ) { !calType && (await dispatch(getCalendarsListAsync())); - calType && dispatch(emptyTempCal()); + calType && dispatch(emptyEventsCal({ calType })); calendars.map( async (cal) => @@ -135,3 +135,23 @@ export async function refreshCalendars( ) ); } + +export async function refreshSingularCalendar( + dispatch: ThunkDispatch, + calendar: Calendars, + calendarRange: { start: Date; end: Date }, + calType?: "temp" +) { + dispatch(emptyEventsCal({ calId: calendar.id, calType })); + + await dispatch( + getCalendarDetailAsync({ + calId: calendar.id, + match: { + start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start), + end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end), + }, + calType, + }) + ); +} diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index bb66df4..7239444 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -422,10 +422,19 @@ const CalendarSlice = createSlice({ removeTempCal: (state, action: PayloadAction) => { delete state.templist[action.payload]; }, - emptyTempCal: (state) => { - Object.keys(state.templist).forEach( - (calId) => (state.templist[calId].events = {}) - ); + emptyEventsCal: ( + state, + action: PayloadAction<{ calId?: string; calType?: "temp" }> + ) => { + const cals = + action.payload.calType === "temp" ? state.templist : state.list; + if (action.payload.calId) { + cals[action.payload.calId].events = {}; + } else { + Object.keys(state.templist).forEach( + (calId) => (cals[calId].events = {}) + ); + } }, updateEventLocal: ( state, @@ -707,7 +716,7 @@ export const { createCalendar, updateEventLocal, removeTempCal, - emptyTempCal, + emptyEventsCal, setTimeZone, clearFetchCache, } = CalendarSlice.actions; diff --git a/src/features/Events/EventDisplayPreview.tsx b/src/features/Events/EventDisplayPreview.tsx index 0b0f474..4cf16c0 100644 --- a/src/features/Events/EventDisplayPreview.tsx +++ b/src/features/Events/EventDisplayPreview.tsx @@ -36,12 +36,10 @@ import { handleRSVP, } from "../../components/Event/eventHandlers/eventHandlers"; import { InfoRow } from "../../components/Event/InfoRow"; -import { - refreshCalendars, - renderAttendeeBadge, -} from "../../components/Event/utils/eventUtils"; +import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils"; import { getTimezoneOffset } from "../../components/Calendar/TimezoneSelector"; import { getCalendarRange } from "../../utils/dateUtils"; +import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils"; export default function EventPreviewModal({ eventId, calId, @@ -114,14 +112,14 @@ export default function EventPreviewModal({ (a) => a.cal_address === event.organizer?.cal_address ); - const updateTempList = () => { + const updateTempList = async () => { if (calendars.templist) { const calendarRange = getCalendarRange(new Date(event.start)); - refreshCalendars( + await updateTempCalendar( + calendars.templist, + event, dispatch, - Object.values(calendars.templist), - calendarRange, - "temp" + calendarRange ); } }; diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index ac385f4..54328d6 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -49,7 +49,7 @@ import { resolveTimezone, } from "../../components/Calendar/TimezoneSelector"; import { getCalendarRange } from "../../utils/dateUtils"; -import { refreshCalendars } from "../../components/Event/utils/eventUtils"; +import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils"; // Helper component for field with label const FieldWithLabel = React.memo( @@ -409,12 +409,7 @@ function EventPopover({ ); if (tempList) { const calendarRange = getCalendarRange(new Date(start)); - refreshCalendars( - dispatch, - Object.values(tempList), - calendarRange, - "temp" - ); + await updateTempCalendar(tempList, newEvent, dispatch, calendarRange); } }; diff --git a/src/features/Events/EventUpdateModal.tsx b/src/features/Events/EventUpdateModal.tsx index 20db6fb..cd06ee7 100644 --- a/src/features/Events/EventUpdateModal.tsx +++ b/src/features/Events/EventUpdateModal.tsx @@ -27,6 +27,7 @@ import { combineMasterDateWithFormTime, detectRecurringEventChanges, } from "./eventUtils"; +import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils"; const showErrorNotification = (message: string) => { console.error(`[ERROR] ${message}`); @@ -478,12 +479,7 @@ function EventUpdateModal({ } if (tempList) { const calendarRange = getCalendarRange(new Date(start)); - refreshCalendars( - dispatch, - Object.values(tempList), - calendarRange, - "temp" - ); + await updateTempCalendar(tempList, event, dispatch, calendarRange); } return; } @@ -658,12 +654,7 @@ function EventUpdateModal({ } if (tempList) { const calendarRange = getCalendarRange(new Date(start)); - refreshCalendars( - dispatch, - Object.values(tempList), - calendarRange, - "temp" - ); + await updateTempCalendar(tempList, event, dispatch, calendarRange); } };