From cf59bcebe48e07d69a4093fb7931abc242d4e63d Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Thu, 31 Jul 2025 17:51:55 +0200 Subject: [PATCH 1/3] [#48] added drag and drop and resize changes to events --- .../components/MiniCalendarColor.test.tsx | 2 + src/components/Calendar/Calendar.css | 13 +--- src/components/Calendar/Calendar.tsx | 73 ++++++++++++++++++- src/features/Calendars/CalendarSlice.ts | 4 + src/utils/dateUtils.ts | 18 +++++ 5 files changed, 98 insertions(+), 12 deletions(-) diff --git a/__test__/components/MiniCalendarColor.test.tsx b/__test__/components/MiniCalendarColor.test.tsx index 941b3fd..c89f957 100644 --- a/__test__/components/MiniCalendarColor.test.tsx +++ b/__test__/components/MiniCalendarColor.test.tsx @@ -32,6 +32,7 @@ describe("MiniCalendar", () => { color: "#FF0000", events: { event1: { + calId: "667037022b752d0026472254/cal1", id: "event1", title: "Test Event", start: day.toISOString(), @@ -140,6 +141,7 @@ describe("Found Bugs", () => { color: "#FF0000", events: { event1: { + calId: "667037022b752d0026472254/cal1", id: "event1", title: "Test Event", start: day.toISOString(), diff --git a/src/components/Calendar/Calendar.css b/src/components/Calendar/Calendar.css index 094fddd..25b0eb7 100644 --- a/src/components/Calendar/Calendar.css +++ b/src/components/Calendar/Calendar.css @@ -27,16 +27,11 @@ main { position: relative; } -.fc-timegrid-slot::after { - content: ""; - position: absolute; - top: 50%; - left: 0; - right: 0; - border-top: 1px dotted #ccc; - pointer-events: none; - z-index: 1; +.fc .fc-timegrid-slot { + height: 30px !important; + min-height: 30px !important; } + .fc .fc-timegrid-slot-label { display: flex; align-items: flex-start; diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 1ab9ee2..28a4e6d 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -12,11 +12,15 @@ import EventPopover from "../../features/Events/EventModal"; import CalendarPopover from "../../features/Calendars/CalendarModal"; import { CalendarEvent } from "../../features/Events/EventsTypes"; import CalendarSelection from "./CalendarSelection"; -import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice"; +import { + getCalendarDetailAsync, + putEventAsync, +} from "../../features/Calendars/CalendarSlice"; import ImportAlert from "../../features/Events/ImportAlert"; import { formatDateToYYYYMMDDTHHMMSS, getCalendarRange, + getDeltaInMilliseconds, } from "../../utils/dateUtils"; import { Calendars } from "../../features/Calendars/CalendarTypes"; import { push } from "redux-first-history"; @@ -129,6 +133,11 @@ export default function CalendarApp() { new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() + 1) ); }; + + if (process.env.NODE_ENV === "test") { + (window as any).__calendarRef = calendarRef; + } + return (
@@ -233,6 +242,7 @@ export default function CalendarApp() { initialView="timeGridWeek" editable={true} selectable={true} + timeZone="local" height={"100%"} select={handleDateSelect} nowIndicator @@ -240,10 +250,16 @@ export default function CalendarApp() { timeGridWeek: { titleFormat: { month: "long", year: "numeric" } }, }} dayMaxEvents={true} - events={filteredEvents} + events={filteredEvents.map((e) => { + if (e.calId.split("/")[0] === userId) { + return { ...e, editable: true }; + } + return { ...e, editable: false }; + })} weekNumbers weekNumberFormat={{ week: "long" }} - slotDuration={"01:00:00"} + slotDuration={"00:30:00"} + slotLabelInterval={"01:00:00"} scrollTime={"08:00:00"} unselectAuto={false} allDayText="" @@ -294,6 +310,57 @@ export default function CalendarApp() { setEventDisplayedCalId(info.event.extendedProps.calId); } }} + eventDrop={(arg) => { + const event = + calendars[arg.event._def.extendedProps.calId].events[ + arg.event._def.extendedProps.uid + ]; + const totalDeltaMs = getDeltaInMilliseconds(arg.delta); + + const originalStart = new Date(event.start); + const computedNewStart = new Date( + originalStart.getTime() + totalDeltaMs + ); + const originalEnd = new Date(event.end ?? ""); + const computedNewEnd = new Date( + originalEnd.getTime() + totalDeltaMs + ); + const newEvent = { + ...event, + start: computedNewStart, + end: computedNewEnd, + } as CalendarEvent; + console.log(newEvent); + console.log(arg); + dispatch( + putEventAsync({ cal: calendars[newEvent.calId], newEvent }) + ); + }} + eventResize={(arg) => { + const event = + calendars[arg.event._def.extendedProps.calId].events[ + arg.event._def.extendedProps.uid + ]; + + const originalStart = new Date(event.start); + const computedNewStart = new Date( + originalStart.getTime() + getDeltaInMilliseconds(arg.startDelta) + ); + const originalEnd = new Date(event.end ?? ""); + const computedNewEnd = new Date( + originalEnd.getTime() + getDeltaInMilliseconds(arg.endDelta) + ); + const newEvent = { + ...event, + start: computedNewStart, + end: computedNewEnd, + } as CalendarEvent; + console.log(newEvent); + console.log(arg); + dispatch( + putEventAsync({ cal: calendars[newEvent.calId], newEvent }) + ); + }} headerToolbar={{ left: "title", center: "prev,today,next", diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index 5809f06..45d1fc1 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -147,6 +147,8 @@ const CalendarSlice = createSlice({ state.list[action.payload.calId].color; state.list[action.payload.calId].events[id].calId = action.payload.calId; + state.list[action.payload.calId].events[id].timezone = + Intl.DateTimeFormat().resolvedOptions().timeZone; }); } ) @@ -171,6 +173,8 @@ const CalendarSlice = createSlice({ state.list[action.payload.calId].color; state.list[action.payload.calId].events[id].calId = action.payload.calId; + state.list[action.payload.calId].events[id].timezone = + Intl.DateTimeFormat().resolvedOptions().timeZone; }); } ) diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts index ad84e15..c5a7d42 100644 --- a/src/utils/dateUtils.ts +++ b/src/utils/dateUtils.ts @@ -27,3 +27,21 @@ export function getCalendarRange(date = new Date()) { end: endDate, }; } + +export function getDeltaInMilliseconds(delta: { + years: number; + months: number; + days: number; + milliseconds: number; +}) { + const MS_PER_DAY = 24 * 60 * 60 * 1000; + const AVG_MS_PER_MONTH = 30.44 * MS_PER_DAY; // approx + const AVG_MS_PER_YEAR = 365.25 * MS_PER_DAY; // approx + + return ( + (delta.years || 0) * AVG_MS_PER_YEAR + + (delta.months || 0) * AVG_MS_PER_MONTH + + (delta.days || 0) * MS_PER_DAY + + (delta.milliseconds || 0) + ); +} From 86b57c233844200a2e4ce0afb055ce187bf8556b Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Mon, 18 Aug 2025 10:55:12 +0200 Subject: [PATCH 2/3] [48] added tests --- .../components/EventModifications.test.tsx | 87 +++++++++++++++++++ src/components/Calendar/Calendar.tsx | 3 +- 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 __test__/components/EventModifications.test.tsx diff --git a/__test__/components/EventModifications.test.tsx b/__test__/components/EventModifications.test.tsx new file mode 100644 index 0000000..9581bf3 --- /dev/null +++ b/__test__/components/EventModifications.test.tsx @@ -0,0 +1,87 @@ +import { CalendarApi } from "@fullcalendar/core"; +import { jest } from "@jest/globals"; +import { ThunkDispatch } from "@reduxjs/toolkit"; +import "@testing-library/jest-dom"; +import { screen } from "@testing-library/react"; +import * as appHooks from "../../src/app/hooks"; +import CalendarApp from "../../src/components/Calendar/Calendar"; +import { renderWithProviders } from "../utils/Renderwithproviders"; + +describe("CalendarApp integration", () => { + 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); + jest.useFakeTimers(); // optional + }); + + const renderCalendar = () => { + const preloadedState = { + user: { + userData: { + sub: "test", + email: "test@test.com", + sid: "mockSid", + openpaasId: "667037022b752d0026472254", + }, + tokens: { accessToken: "token" }, // required to avoid redirect + }, + calendars: { + list: { + "667037022b752d0026472254/cal1": { + name: "Calendar 1", + color: "#FF0000", + events: { + event1: { + id: "event1", + calId: "667037022b752d0026472254/cal1", + uid: "event1", + title: "Test Event", + start: start.toISOString(), + end: end.toISOString(), + }, + }, + }, + }, + pending: false, + }, + }; + + renderWithProviders(, preloadedState); + }; + + it("renders the event on the calendar and calendarRef works", async () => { + const dispatch = appHooks.useAppDispatch(); + + renderCalendar(); + const calendarRef: React.RefObject = (window as any) + .__calendarRef; + + const calendarApi = calendarRef.current; + + // Wait for the FullCalendar DOM to populate + const eventEl = await screen.findByText( + "Test Event", + {}, + { timeout: 3000 } + ); + expect(eventEl).toBeInTheDocument(); + + if (calendarApi) { + const fcEvent = calendarApi.getEventById("event1"); + expect(fcEvent?.title).toBe("Test Event"); + const oldEnd = new Date(today.getTime() + 3600000); // +1 hour + const newEnd = new Date(oldEnd.getTime() + 1800000); // +30 min + + fcEvent?.setEnd(newEnd); + + expect(dispatch).toHaveBeenCalled(); + } + }); +}); diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 28a4e6d..6e5bd19 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -355,8 +355,7 @@ export default function CalendarApp() { start: computedNewStart, end: computedNewEnd, } as CalendarEvent; - console.log(newEvent); - console.log(arg); + dispatch( putEventAsync({ cal: calendars[newEvent.calId], newEvent }) ); From 22ebf31f7767e8be8596b338cc86cbc19ed623d8 Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Tue, 19 Aug 2025 15:27:54 +0200 Subject: [PATCH 3/3] [#48] fixed drag and drop waiting for api call to be done to show updated event --- src/components/Calendar/Calendar.tsx | 6 ++++-- src/features/Calendars/CalendarSlice.ts | 10 +++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 6e5bd19..e647ac1 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -15,6 +15,7 @@ import CalendarSelection from "./CalendarSelection"; import { getCalendarDetailAsync, putEventAsync, + updateEventLocal, } from "../../features/Calendars/CalendarSlice"; import ImportAlert from "../../features/Events/ImportAlert"; import { @@ -330,8 +331,9 @@ export default function CalendarApp() { start: computedNewStart, end: computedNewEnd, } as CalendarEvent; - console.log(newEvent); - console.log(arg); + dispatch( + updateEventLocal({ calId: newEvent.calId, event: newEvent }) + ); dispatch( putEventAsync({ cal: calendars[newEvent.calId], newEvent }) ); diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index 45d1fc1..0046c20 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -114,6 +114,13 @@ const CalendarSlice = createSlice({ action.payload.eventUid ]; }, + updateEventLocal: ( + state, + action: PayloadAction<{ calId: string; event: CalendarEvent }> + ) => { + const { calId, event } = action.payload; + state.list[calId].events[event.uid] = event; + }, }, extraReducers: (builder) => { builder @@ -197,5 +204,6 @@ const CalendarSlice = createSlice({ }, }); -export const { addEvent, removeEvent, createCalendar } = CalendarSlice.actions; +export const { addEvent, removeEvent, createCalendar, updateEventLocal } = + CalendarSlice.actions; export default CalendarSlice.reducer;