[#83] Change minicalendar lib to MUI (#211)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-10-16 10:36:59 +02:00
committed by GitHub
parent b9905cbb7a
commit 0f82711d7d
9 changed files with 285 additions and 224 deletions
+3 -3
View File
@@ -360,7 +360,7 @@ describe("calendar Availability search", () => {
calendarApi.changeView("dayGridMonth");
await waitFor(() => {
expect(screen.queryAllByRole("columnheader").length).toBe(7);
expect(screen.queryAllByRole("columnheader").length).toBe(14);
});
const dayNumbers = screen.getAllByRole("columnheader");
@@ -394,8 +394,8 @@ describe("calendar Availability search", () => {
calendarApi.changeView("dayGridMonth");
fireEvent.click(screen.getByTestId("ChevronRightIcon"));
expect(spy).toHaveBeenCalledTimes(2);
const callArgs = spy.mock.calls[1][0];
expect(spy).toHaveBeenCalledTimes(4);
const callArgs = spy.mock.calls[3][0];
expect(callArgs.calId).toBe("user1/cal1");
const startDate = new Date(
@@ -57,6 +57,6 @@ describe("MiniCalendar", () => {
const dateTestId = `date-${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
const todayTile = screen.getByTestId(dateTestId);
expect(todayTile?.parentElement).toHaveClass("today");
expect(todayTile).toHaveClass("today");
});
});
+116 -80
View File
@@ -19,6 +19,7 @@
"@fullcalendar/timegrid": "^6.1.17",
"@mui/icons-material": "^7.1.2",
"@mui/material": "^7.1.2",
"@mui/x-date-pickers": "^8.14.0",
"@reduxjs/toolkit": "^2.8.2",
"@types/node": "^16.18.126",
"@types/react": "^19.1.8",
@@ -30,7 +31,6 @@
"moment-timezone": "^0.5.48",
"openid-client": "^6.5.3",
"react": "^19.1.0",
"react-calendar": "^6.0.0",
"react-dom": "^19.1.0",
"react-i18next": "^13.5.0",
"react-redux": "^9.2.0",
@@ -1908,7 +1908,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.27.6",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -3261,10 +3263,12 @@
}
},
"node_modules/@mui/types": {
"version": "7.4.4",
"version": "7.4.7",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.7.tgz",
"integrity": "sha512-8vVje9rdEr1rY8oIkYgP+Su5Kwl6ik7O3jQ0wl78JGSmiZhRHV+vkjooGdKD8pbtZbutXFVTWQYshu2b3sG9zw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6"
"@babel/runtime": "^7.28.4"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -3276,15 +3280,17 @@
}
},
"node_modules/@mui/utils": {
"version": "7.2.0",
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.3.tgz",
"integrity": "sha512-kwNAUh7bLZ7mRz9JZ+6qfRnnxbE4Zuc+RzXnhSpRSxjTlSTj7b4JxRLXpG+MVtPVtqks5k/XC8No1Vs3x4Z2gg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/types": "^7.4.4",
"@babel/runtime": "^7.28.4",
"@mui/types": "^7.4.7",
"@types/prop-types": "^15.7.15",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.1.0"
"react-is": "^19.1.1"
},
"engines": {
"node": ">=14.0.0"
@@ -3303,6 +3309,94 @@
}
}
},
"node_modules/@mui/x-date-pickers": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.14.0.tgz",
"integrity": "sha512-fz8z1hoi1tbG1QUcZAkQdiO3GsCOpUeRfyANXDDzDN88L4VqwNEyrv0wmzmCfIX2sgur4gWwWJzMuCvauMgXRw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/utils": "^7.3.3",
"@mui/x-internals": "8.14.0",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
"@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
"date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
"date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
"dayjs": "^1.10.7",
"luxon": "^3.0.2",
"moment": "^2.29.4",
"moment-hijri": "^2.1.2 || ^3.0.0",
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"date-fns": {
"optional": true
},
"date-fns-jalali": {
"optional": true
},
"dayjs": {
"optional": true
},
"luxon": {
"optional": true
},
"moment": {
"optional": true
},
"moment-hijri": {
"optional": true
},
"moment-jalaali": {
"optional": true
}
}
},
"node_modules/@mui/x-internals": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.14.0.tgz",
"integrity": "sha512-esYyl61nuuFXiN631TWuPh2tqdoyTdBI/4UXgwH3rytF8jiWvy6prPBPRHEH1nvW3fgw9FoBI48FlOO+yEI8xg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/utils": "^7.3.3",
"reselect": "^5.1.1",
"use-sync-external-store": "^1.6.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz",
@@ -5115,13 +5209,6 @@
"linux"
]
},
"node_modules/@wojtekmaj/date-utils": {
"version": "2.0.2",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/date-utils?sponsor=1"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"license": "MIT",
@@ -6408,6 +6495,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/dayjs": {
"version": "1.11.18",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/debug": {
"version": "4.4.1",
"license": "MIT",
@@ -8203,16 +8298,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-user-locale": {
"version": "3.0.0",
"license": "MIT",
"dependencies": {
"memoize": "^10.0.0"
},
"funding": {
"url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1"
}
},
"node_modules/glob": {
"version": "7.2.3",
"dev": true,
@@ -10752,19 +10837,6 @@
"dev": true,
"license": "CC0-1.0"
},
"node_modules/memoize": {
"version": "10.1.0",
"license": "MIT",
"dependencies": {
"mimic-function": "^5.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sindresorhus/memoize?sponsor=1"
}
},
"node_modules/merge": {
"version": "2.1.1",
"dev": true,
@@ -10803,16 +10875,6 @@
"node": ">=6"
}
},
"node_modules/mimic-function": {
"version": "5.0.1",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/min-indent": {
"version": "1.0.1",
"dev": true,
@@ -11733,29 +11795,6 @@
"node": ">=0.10.0"
}
},
"node_modules/react-calendar": {
"version": "6.0.0",
"license": "MIT",
"dependencies": {
"@wojtekmaj/date-utils": "^2.0.2",
"clsx": "^2.0.0",
"get-user-locale": "^3.0.0",
"warning": "^4.0.0"
},
"funding": {
"url": "https://github.com/wojtekmaj/react-calendar?sponsor=1"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-dom": {
"version": "19.1.0",
"license": "MIT",
@@ -11787,7 +11826,9 @@
}
},
"node_modules/react-is": {
"version": "19.1.0",
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz",
"integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==",
"license": "MIT"
},
"node_modules/react-redux": {
@@ -13485,7 +13526,9 @@
}
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -13547,13 +13590,6 @@
"makeerror": "1.0.12"
}
},
"node_modules/warning": {
"version": "4.0.3",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/web-vitals": {
"version": "2.1.4",
"license": "Apache-2.0"
+1 -2
View File
@@ -14,6 +14,7 @@
"@fullcalendar/timegrid": "^6.1.17",
"@mui/icons-material": "^7.1.2",
"@mui/material": "^7.1.2",
"@mui/x-date-pickers": "^8.14.0",
"@reduxjs/toolkit": "^2.8.2",
"@types/node": "^16.18.126",
"@types/react": "^19.1.8",
@@ -23,10 +24,8 @@
"ky": "^1.8.1",
"moment": "^2.30.1",
"moment-timezone": "^0.5.48",
"moment": "^2.30.1",
"openid-client": "^6.5.3",
"react": "^19.1.0",
"react-calendar": "^6.0.0",
"react-dom": "^19.1.0",
"react-i18next": "^13.5.0",
"react-redux": "^9.2.0",
+5 -28
View File
@@ -23,8 +23,9 @@
.sidebar
border-right 2px solid #f3f4f6
width 330px
padding 16px 25px
width 350px
padding-top 16px
padding-left 5px
height 90vh
flex-direction column
overflow auto
@@ -71,32 +72,8 @@
font-family "Segoe UI", Tahoma, Geneva, Verdana, sans-serif
/* mini calendar style */
.react-calendar
border none
font-size 14px
width 100%
user-select none
/* selected day hover */
.react-calendar__tile
transition background-color 0.3s ease, color 0.3s ease
border none
background-color white
width 12px
height 37.52px
.react-calendar__tile--active
background-color rgb(255, 255, 255)
.today
background orange !important
color white
.selectedWeek
background-color lightgrey
.react-calendar__month-view__days__day--neighboringMonth
color gray
.MuiDayCalendar-weekContainer:has(.selectedWeek)
background-color lightgray !important
/* sidebar header */
.sidebar-calendar h2
+13 -88
View File
@@ -41,6 +41,13 @@ import { EventErrorHandler } from "../Error/EventErrorHandler";
import { EventErrorSnackbar } from "../Error/ErrorSnackbar";
import momentTimezonePlugin from "@fullcalendar/moment-timezone";
import { TimezoneSelector } from "./TimezoneSelector";
import { DateCalendar } from "@mui/x-date-pickers/DateCalendar";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
import moment from "moment";
import { DayCalendarSkeleton } from "@mui/x-date-pickers/DayCalendarSkeleton";
import { PickersDay } from "@mui/x-date-pickers/PickersDay";
import { MiniCalendar } from "./MiniCalendar";
interface CalendarAppProps {
calendarRef: React.RefObject<CalendarApi | null>;
@@ -242,14 +249,6 @@ export default function CalendarApp({
errorHandler: errorHandler.current,
});
const handleMonthUp = () => {
eventHandlers.handleMonthUp(selectedMiniDate, setSelectedMiniDate);
};
const handleMonthDown = () => {
eventHandlers.handleMonthDown(selectedMiniDate, setSelectedMiniDate);
};
if (process.env.NODE_ENV === "test") {
(window as any).__calendarRef = calendarRef;
}
@@ -265,87 +264,13 @@ export default function CalendarApp({
>
<AddIcon /> <p>Create Event</p>
</Button>
<div className="calendar-label">
<div className="calendar-label">
<span title="mini calendar month">
{selectedMiniDate.toLocaleDateString("en-us", {
month: "long",
year: "numeric",
})}
</span>
</div>
<button onClick={handleMonthUp}>&lt;</button>
<button onClick={handleMonthDown}>&gt;</button>
</div>
<ReactCalendar
key={selectedMiniDate.toDateString()}
calendarType="iso8601"
formatShortWeekday={(locale, date) =>
date.toLocaleDateString(locale, { weekday: "narrow" })
}
value={selectedMiniDate}
onClickDay={(date) => {
setSelectedDate(date);
setSelectedMiniDate(date);
calendarRef.current?.gotoDate(date);
}}
prevLabel={null}
showNeighboringMonth={true}
nextLabel={null}
showNavigation={false}
tileClassName={({ date }) => {
const classNames: string[] = [];
const today = new Date().setHours(0, 0, 0, 0);
if (date.getTime() === today) {
classNames.push("today");
}
const selected = new Date(selectedDate);
selected.setHours(0, 0, 0, 0);
if (
calendarRef.current?.view.type === "timeGridWeek" ||
calendarRef.current?.view.type === undefined
) {
const startOfWeek = computeStartOfTheWeek(selected);
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 6); // Sunday
endOfWeek.setHours(23, 59, 59, 999);
if (date <= endOfWeek && date >= startOfWeek) {
classNames.push("selectedWeek");
}
}
if (
calendarRef.current?.view.type === "timeGridDay" &&
date.getTime() === selected.getTime()
) {
classNames.push("selectedWeek");
}
return classNames;
}}
tileContent={({ date }) => {
const classNames: string[] = [];
const hasEvents = dottedEvents.some((event) => {
const eventDate = new Date(event.start);
return (
eventDate.getFullYear() === date.getFullYear() &&
eventDate.getMonth() === date.getMonth() &&
eventDate.getDate() === date.getDate() &&
event.status !== "CANCELLED"
);
});
if (hasEvents) {
classNames.push("event-dot");
}
return (
<div
className={classNames.join(" ")}
data-testid={`date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`}
></div>
);
}}
<MiniCalendar
calendarRef={calendarRef}
selectedDate={selectedMiniDate}
setSelectedDate={setSelectedDate}
setSelectedMiniDate={setSelectedMiniDate}
dottedEvents={dottedEvents}
/>
<TempCalendarsInput
setAnchorEl={setAnchorEl}
+146
View File
@@ -0,0 +1,146 @@
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
import { DateCalendar, PickersDay } from "@mui/x-date-pickers";
import { DayCalendarSkeleton } from "@mui/x-date-pickers/DayCalendarSkeleton";
import moment from "moment";
import {
computeStartOfTheWeek,
formatDateToYYYYMMDDTHHMMSS,
getCalendarRange,
} from "../../utils/dateUtils";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { refreshCalendars } from "../Event/utils/eventUtils";
import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice";
import { useEffect, useState } from "react";
export function MiniCalendar({
calendarRef,
selectedDate,
setSelectedDate,
setSelectedMiniDate,
dottedEvents,
}: {
calendarRef: any;
selectedDate: Date;
setSelectedDate: (d: Date) => void;
setSelectedMiniDate: (d: Date) => void;
dottedEvents: any[];
}) {
const dispatch = useAppDispatch();
const calendars = useAppSelector((state) => state.calendars);
const [visibleDate, setVisibleDate] = useState(selectedDate);
useEffect(() => setVisibleDate(selectedDate), [selectedDate]);
return (
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="en-gb">
<DateCalendar
value={moment(visibleDate)}
onChange={(dateMoment) => {
if (!dateMoment) return;
const date = dateMoment.toDate();
setSelectedDate(date);
setSelectedMiniDate(date);
calendarRef.current?.gotoDate(date);
}}
showDaysOutsideCurrentMonth
onMonthChange={(month) => {
const calendarRange = getCalendarRange(month.toDate());
setVisibleDate(month.toDate());
Object.values(calendars.list).forEach((cal) =>
dispatch(
getCalendarDetailAsync({
calId: cal.id,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
})
)
);
}}
views={["month", "day"]}
slotProps={{
day: (ownerState) => {
const date = ownerState.day.toDate();
const today = new Date();
today.setHours(0, 0, 0, 0);
const selected = new Date(selectedDate);
selected.setHours(0, 0, 0, 0);
const isToday = date.getTime() === today.getTime();
const isSelectedDay =
calendarRef.current?.view.type === "timeGridDay" &&
date.getTime() === selected.getTime();
const isInSelectedWeek =
calendarRef.current?.view.type === "timeGridWeek" ||
calendarRef.current?.view.type === undefined
? (() => {
const startOfWeek = computeStartOfTheWeek(selected);
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 6);
endOfWeek.setHours(23, 59, 59, 999);
return date >= startOfWeek && date <= endOfWeek;
})()
: false;
const hasEvents = dottedEvents.some((event) => {
const eventDate = new Date(event.start);
return (
eventDate.getFullYear() === date.getFullYear() &&
eventDate.getMonth() === date.getMonth() &&
eventDate.getDate() === date.getDate() &&
event.status !== "CANCELLED"
);
});
const classNames = [
isToday ? "today" : "",
isSelectedDay ? "selectedDay" : "",
isInSelectedWeek ? "selectedWeek" : "",
hasEvents ? "event-dot" : "",
].join(" ");
return {
className: classNames,
selected: classNames.includes("selectedWeek"),
outsideCurrentMonth: ownerState.isDayOutsideMonth,
disableMargin: false,
onClick: () => {
setSelectedDate(date);
setSelectedMiniDate(date);
calendarRef.current?.gotoDate(date);
},
style: {
backgroundColor: "transparent",
position: "relative",
flexDirection: "column",
border: 0,
},
sx: {
"&.Mui-selected": {
color: "inherit !important",
fontWeight: "inherit !important",
},
"&.selectedDay": {
backgroundColor: "lightgray !important",
},
"&.today": {
background: "orange !important",
color: "white !important",
},
},
"data-testid": `date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`,
children: (
<>
{ownerState.day.date()}
{hasEvents && <div className="event-dot" />}
</>
),
};
},
}}
/>
</LocalizationProvider>
);
}
@@ -89,24 +89,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
setOpenEventDisplay(false);
};
const handleMonthUp = (
selectedMiniDate: Date,
setSelectedMiniDate: (date: Date) => void
) => {
setSelectedMiniDate(
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() - 1)
);
};
const handleMonthDown = (
selectedMiniDate: Date,
setSelectedMiniDate: (date: Date) => void
) => {
setSelectedMiniDate(
new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() + 1)
);
};
const handleEventClick = (info: any) => {
info.jsEvent.preventDefault();
@@ -255,8 +237,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
handleDateSelect,
handleClosePopover,
handleCloseEventDisplay,
handleMonthUp,
handleMonthDown,
handleEventClick,
handleEventAllow,
handleEventDrop,
@@ -25,8 +25,6 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => {
eventHandlers.handleCloseEventDisplay,
[props.setOpenEventDisplay]
),
handleMonthUp: useCallback(eventHandlers.handleMonthUp, []),
handleMonthDown: useCallback(eventHandlers.handleMonthDown, []),
handleEventClick: useCallback(eventHandlers.handleEventClick, [
props.setOpenEventDisplay,
props.setEventDisplayedId,