Merge pull request #55 from linagora/48-drag-and-drop-to-update-event

drag and drop to update event
This commit is contained in:
Camille Moussu
2025-08-19 17:38:27 +02:00
committed by GitHub
6 changed files with 195 additions and 13 deletions
@@ -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<any, any, any>;
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(<CalendarApp />, preloadedState);
};
it("renders the event on the calendar and calendarRef works", async () => {
const dispatch = appHooks.useAppDispatch();
renderCalendar();
const calendarRef: React.RefObject<CalendarApi | null> = (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();
}
});
});
@@ -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(),
+4 -9
View File
@@ -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;
+71 -3
View File
@@ -12,11 +12,16 @@ 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,
updateEventLocal,
} 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 +134,11 @@ export default function CalendarApp() {
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() + 1)
);
};
if (process.env.NODE_ENV === "test") {
(window as any).__calendarRef = calendarRef;
}
return (
<main>
<div className="sidebar">
@@ -233,6 +243,7 @@ export default function CalendarApp() {
initialView="timeGridWeek"
editable={true}
selectable={true}
timeZone="local"
height={"100%"}
select={handleDateSelect}
nowIndicator
@@ -240,10 +251,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 +311,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;
dispatch(
updateEventLocal({ calId: newEvent.calId, event: newEvent })
);
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;
dispatch(
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
);
}}
headerToolbar={{
left: "title",
center: "prev,today,next",
+13 -1
View File
@@ -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
@@ -147,6 +154,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 +180,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;
});
}
)
@@ -193,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;
+18
View File
@@ -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)
);
}