Merge pull request #55 from linagora/48-drag-and-drop-to-update-event
drag and drop to update event
This commit is contained in:
@@ -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",
|
color: "#FF0000",
|
||||||
events: {
|
events: {
|
||||||
event1: {
|
event1: {
|
||||||
|
calId: "667037022b752d0026472254/cal1",
|
||||||
id: "event1",
|
id: "event1",
|
||||||
title: "Test Event",
|
title: "Test Event",
|
||||||
start: day.toISOString(),
|
start: day.toISOString(),
|
||||||
@@ -140,6 +141,7 @@ describe("Found Bugs", () => {
|
|||||||
color: "#FF0000",
|
color: "#FF0000",
|
||||||
events: {
|
events: {
|
||||||
event1: {
|
event1: {
|
||||||
|
calId: "667037022b752d0026472254/cal1",
|
||||||
id: "event1",
|
id: "event1",
|
||||||
title: "Test Event",
|
title: "Test Event",
|
||||||
start: day.toISOString(),
|
start: day.toISOString(),
|
||||||
|
|||||||
@@ -27,16 +27,11 @@ main {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-timegrid-slot::after {
|
.fc .fc-timegrid-slot {
|
||||||
content: "";
|
height: 30px !important;
|
||||||
position: absolute;
|
min-height: 30px !important;
|
||||||
top: 50%;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
border-top: 1px dotted #ccc;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-timegrid-slot-label {
|
.fc .fc-timegrid-slot-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -12,11 +12,16 @@ import EventPopover from "../../features/Events/EventModal";
|
|||||||
import CalendarPopover from "../../features/Calendars/CalendarModal";
|
import CalendarPopover from "../../features/Calendars/CalendarModal";
|
||||||
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
||||||
import CalendarSelection from "./CalendarSelection";
|
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 ImportAlert from "../../features/Events/ImportAlert";
|
||||||
import {
|
import {
|
||||||
formatDateToYYYYMMDDTHHMMSS,
|
formatDateToYYYYMMDDTHHMMSS,
|
||||||
getCalendarRange,
|
getCalendarRange,
|
||||||
|
getDeltaInMilliseconds,
|
||||||
} from "../../utils/dateUtils";
|
} from "../../utils/dateUtils";
|
||||||
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
||||||
import { push } from "redux-first-history";
|
import { push } from "redux-first-history";
|
||||||
@@ -129,6 +134,11 @@ export default function CalendarApp() {
|
|||||||
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() + 1)
|
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() + 1)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "test") {
|
||||||
|
(window as any).__calendarRef = calendarRef;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
@@ -233,6 +243,7 @@ export default function CalendarApp() {
|
|||||||
initialView="timeGridWeek"
|
initialView="timeGridWeek"
|
||||||
editable={true}
|
editable={true}
|
||||||
selectable={true}
|
selectable={true}
|
||||||
|
timeZone="local"
|
||||||
height={"100%"}
|
height={"100%"}
|
||||||
select={handleDateSelect}
|
select={handleDateSelect}
|
||||||
nowIndicator
|
nowIndicator
|
||||||
@@ -240,10 +251,16 @@ export default function CalendarApp() {
|
|||||||
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
|
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
|
||||||
}}
|
}}
|
||||||
dayMaxEvents={true}
|
dayMaxEvents={true}
|
||||||
events={filteredEvents}
|
events={filteredEvents.map((e) => {
|
||||||
|
if (e.calId.split("/")[0] === userId) {
|
||||||
|
return { ...e, editable: true };
|
||||||
|
}
|
||||||
|
return { ...e, editable: false };
|
||||||
|
})}
|
||||||
weekNumbers
|
weekNumbers
|
||||||
weekNumberFormat={{ week: "long" }}
|
weekNumberFormat={{ week: "long" }}
|
||||||
slotDuration={"01:00:00"}
|
slotDuration={"00:30:00"}
|
||||||
|
slotLabelInterval={"01:00:00"}
|
||||||
scrollTime={"08:00:00"}
|
scrollTime={"08:00:00"}
|
||||||
unselectAuto={false}
|
unselectAuto={false}
|
||||||
allDayText=""
|
allDayText=""
|
||||||
@@ -294,6 +311,57 @@ export default function CalendarApp() {
|
|||||||
setEventDisplayedCalId(info.event.extendedProps.calId);
|
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={{
|
headerToolbar={{
|
||||||
left: "title",
|
left: "title",
|
||||||
center: "prev,today,next",
|
center: "prev,today,next",
|
||||||
|
|||||||
@@ -114,6 +114,13 @@ const CalendarSlice = createSlice({
|
|||||||
action.payload.eventUid
|
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) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
@@ -147,6 +154,8 @@ const CalendarSlice = createSlice({
|
|||||||
state.list[action.payload.calId].color;
|
state.list[action.payload.calId].color;
|
||||||
state.list[action.payload.calId].events[id].calId =
|
state.list[action.payload.calId].events[id].calId =
|
||||||
action.payload.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].color;
|
||||||
state.list[action.payload.calId].events[id].calId =
|
state.list[action.payload.calId].events[id].calId =
|
||||||
action.payload.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;
|
export default CalendarSlice.reducer;
|
||||||
|
|||||||
@@ -27,3 +27,21 @@ export function getCalendarRange(date = new Date()) {
|
|||||||
end: endDate,
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user