From 6a5afa1f21860515be0c97555a00df8f16870317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20N=C3=B6sberger?= Date: Fri, 29 Nov 2024 13:44:50 +0100 Subject: [PATCH 1/6] Replace MUI DatePicker with react-datepicker for search filters Fixes several issues with date picking. react-datepicker allows date-range-picking and makes it therefore easier to handle date picking. --- src/components/shared/TableFilters.tsx | 172 +++++------------- src/styles/components/_components-config.scss | 1 + src/styles/components/_datepicker-custom.scss | 33 ++++ 3 files changed, 82 insertions(+), 124 deletions(-) create mode 100644 src/styles/components/_datepicker-custom.scss diff --git a/src/components/shared/TableFilters.tsx b/src/components/shared/TableFilters.tsx index 449a395d55..ba436639c7 100644 --- a/src/components/shared/TableFilters.tsx +++ b/src/components/shared/TableFilters.tsx @@ -1,6 +1,6 @@ -import React, { useRef, useState, useEffect } from "react"; +import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import DatePicker from "react-datepicker"; import { getFilters, getSecondFilter, @@ -26,6 +26,7 @@ import { useHotkeys } from "react-hotkeys-hook"; import moment from "moment"; import { AppThunk, useAppDispatch, useAppSelector } from "../../store"; import { renderValidDate } from "../../utils/dateUtils"; +import { getCurrentLanguageInformation } from "../../utils/utils"; import { Tooltip } from "./Tooltip"; import DropDown from "./DropDown"; import { AsyncThunk } from "@reduxjs/toolkit"; @@ -147,77 +148,34 @@ const TableFilters = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [itemValue]); - // Set the sate of startDate and endDate picked with datepicker - const handleDatepickerChange = async (date: Date | null, isStart = false) => { - if (date === null) { - return; - } - - if (isStart) { - date.setHours(0); - date.setMinutes(0); - date.setSeconds(0); - setStartDate(date); - } else { - date.setHours(23); - date.setMinutes(59); - date.setSeconds(59); - setEndDate(date); - } - }; - - // If both dates are set, set the value for the startDate filter - // If the just changed, it can be passed here so we don't have wait a render - // cycle for the useState state to update - const handleDatepickerConfirm = async (date?: Date | null, isStart = false) => { - if (date === null) { - return; - } - - let myStartDate = startDate; - let myEndDate = endDate; - if (date && isStart) { - myStartDate = date; - myStartDate.setHours(0); - myStartDate.setMinutes(0); - myStartDate.setSeconds(0); - } - if (date && !isStart) { - myEndDate = date; - myEndDate.setHours(23); - myEndDate.setMinutes(59); - myEndDate.setSeconds(59); - } - - if (myStartDate && myEndDate && moment(myStartDate).isValid() && moment(myEndDate).isValid()) { - let filter = filterMap.find(({ name }) => name === selectedFilter); - if (filter) { - dispatch(editFilterValue({ - filterName: filter.name, - value: myStartDate.toISOString() + "/" + myEndDate.toISOString() - })); - setFilterSelector(false); - dispatch(removeSelectedFilter()); - // Reload of resource after going to very first page. - dispatch(goToPage(0)) - await dispatch(loadResource()); - dispatch(loadResourceIntoTable()); + const handleDatepicker = async (dates?: [Date | undefined | null, Date | undefined | null]) => { + if (dates != null) { + let [start, end] = dates; + + start?.setHours(0); + start?.setMinutes(0); + start?.setSeconds(0) + end?.setHours(23); + end?.setMinutes(59); + end?.setSeconds(59); + + if (start && end && moment(start).isValid() && moment(end).isValid()) { + let filter = filterMap.find(({ name }) => name === selectedFilter); + if (filter) { + dispatch(editFilterValue({ + filterName: filter.name, + value: start.toISOString() + "/" + end.toISOString() + })); + setFilterSelector(false); + dispatch(removeSelectedFilter()); + // Reload of resource after going to very first page. + dispatch(goToPage(0)) + await dispatch(loadResource()); + dispatch(loadResourceIntoTable()); + } } - } - - if (myStartDate && isStart && !endDate) { - let tmp = new Date(myStartDate.getTime()); - tmp.setHours(23); - tmp.setMinutes(59); - tmp.setSeconds(59); - setEndDate(tmp); - } - if (myEndDate && !isStart && !startDate) { - let tmp = new Date(myEndDate.getTime()); - tmp.setHours(0); - tmp.setMinutes(0); - tmp.setSeconds(0); - setStartDate(tmp); + if (start) setStartDate(start); + if (end) setEndDate(end); } } @@ -316,8 +274,7 @@ const TableFilters = ({ secondFilter={secondFilter} startDate={startDate} endDate={endDate} - handleDate={handleDatepickerChange} - handleDateConfirm={handleDatepickerConfirm} + handleDate={handleDatepicker} handleChange={handleChange} openSecondFilterMenu={openSecondFilterMenu} setOpenSecondFilterMenu={setOpenSecondFilterMenu} @@ -399,7 +356,6 @@ const FilterSwitch = ({ startDate, endDate, handleDate, - handleDateConfirm, secondFilter, openSecondFilterMenu, setOpenSecondFilterMenu, @@ -408,17 +364,13 @@ const FilterSwitch = ({ handleChange: (name: string, value: string) => void, startDate: Date | undefined, endDate: Date | undefined, - handleDate: (date: Date | null, isStart?: boolean) => void, - handleDateConfirm: (date: Date | undefined | null, isStart?: boolean) => void, + handleDate: (dates: [Date | undefined | null, Date | undefined | null]) => void, secondFilter: string, openSecondFilterMenu: boolean, setOpenSecondFilterMenu: (open: boolean) => void, }) => { const { t } = useTranslation(); - const startDateRef = useRef(null); - const endDateRef = useRef(null); - if (!filter) { return null; } @@ -472,51 +424,23 @@ const FilterSwitch = ({ case "period": return (
- {/* Show datepicker for start date */} - handleDate(date as Date | null, true)} - // FixMe: onAccept does not trigger if the already set value is the same as the selected value - // This prevents us from confirming from confirming our filter, if someone wants to selected the same - // day for both start and end date (since we automatically set one to the other) - onAccept={(e) => {handleDateConfirm(e as Date | null, true)}} - slotProps={{ - textField: { - onKeyDown: (event) => { - if (event.key === "Enter") { - handleDateConfirm(undefined, true) - if (endDateRef.current && startDate && moment(startDate).isValid()) { - endDateRef.current.focus(); - } - } - }, - }, - }} - /> handleDate(date as Date | null)} - // FixMe: See above - onAccept={(e) => handleDateConfirm(e as Date | null, false)} - slotProps={{ - textField: { - onKeyDown: (event) => { - if (event.key === "Enter") { - handleDateConfirm(undefined, false) - if (startDateRef.current && endDate && moment(endDate).isValid()) { - startDateRef.current.focus(); - } - } - }, - }, - }} + startOpen + showIcon + icon="fa fa-calendar" + selected={startDate} + onChange={(dates) => handleDate(dates)} + startDate={startDate} + endDate={endDate} + selectsRange + swapRange + allowSameDay + dateFormat="P" + popperPlacement="bottom" + popperClassName="datepicker-custom" + className="datepicker-custom-input" + locale={getCurrentLanguageInformation()?.dateLocale} + />
); diff --git a/src/styles/components/_components-config.scss b/src/styles/components/_components-config.scss index 17c8b27cf4..0860459220 100644 --- a/src/styles/components/_components-config.scss +++ b/src/styles/components/_components-config.scss @@ -35,6 +35,7 @@ @import "footer"; @import "tables"; @import "date-picker"; +@import "datepicker-custom"; @import "alerts"; @import "inputs"; @import "stats"; diff --git a/src/styles/components/_datepicker-custom.scss b/src/styles/components/_datepicker-custom.scss new file mode 100644 index 0000000000..4f9fc2cea8 --- /dev/null +++ b/src/styles/components/_datepicker-custom.scss @@ -0,0 +1,33 @@ +/** + * Licensed to The Apereo Foundation under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * + * The Apereo Foundation licenses this file to you under the Educational + * Community License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License + * at: + * + * http://opensource.org/licenses/ecl2.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + */ + + + .datepicker-custom { + z-index: 10000 !important; + + &-wrapper { + width: 100%; + } + + &-input { + height: 25px !important; + } + } From 7c85e8e24a2b9b1cd85f38fcd8355152de99cfc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20N=C3=B6sberger?= Date: Fri, 29 Nov 2024 13:51:29 +0100 Subject: [PATCH 2/6] Replace MUI DatePicker with react-datepicker for event creation and editing Fixes several issues with date picking. react-datepicker allows date-range-picking and makes it therefore easier to handle date picking. --- .../EventDetailsSchedulingTab.tsx | 14 ++-- .../ModalTabsAndPages/NewSourcePage.tsx | 16 ++++- src/components/shared/wizard/RenderField.tsx | 64 +++++++++---------- 3 files changed, 51 insertions(+), 43 deletions(-) diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx index c992d69bdb..e8052c2ad6 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import cn from "classnames"; import _ from "lodash"; -import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import DatePicker from "react-datepicker"; import { Formik, FormikErrors, FormikProps } from "formik"; import { Field } from "../../../shared/Field"; import Notifications from "../../../shared/Notifications"; @@ -195,12 +195,12 @@ const EventDetailsSchedulingTab = ({ : []; return { - scheduleStartDate: startDate.setHours(0, 0, 0).toString(), + scheduleStartDate: startDate.toString(), scheduleStartHour: source.start.hour != null ? makeTwoDigits(source.start.hour) : "", scheduleStartMinute: source.start.minute != null ? makeTwoDigits(source.start.minute) : "", scheduleDurationHours: source.duration.hour != null ? makeTwoDigits(source.duration.hour) : "", scheduleDurationMinutes: source.duration.minute != null ? makeTwoDigits(source.duration.minute): "", - scheduleEndDate: endDate.setHours(0, 0, 0).toString(), + scheduleEndDate: endDate.toString(), scheduleEndHour: source.end.hour != null ? makeTwoDigits(source.end.hour): "", scheduleEndMinute: source.end.minute != null ? makeTwoDigits(source.end.minute): "", captureAgent: source.device.name, @@ -286,8 +286,7 @@ const EventDetailsSchedulingTab = ({ /* date picker for start date */ value && changeStartDate( value, @@ -297,6 +296,11 @@ const EventDetailsSchedulingTab = ({ checkConflictsWrapper ) } + dateFormat="P" + popperClassName="datepicker-custom" + className="datepicker-custom-input" + portalId="root" + locale={currentLanguage?.dateLocale} /> ) : ( <> diff --git a/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx b/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx index 6a1e3aa5c3..b6d8f9e3e8 100644 --- a/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import cn from "classnames"; import Notifications from "../../../shared/Notifications"; -import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import DatePicker from "react-datepicker"; import { getCurrentLanguageInformation, getTimezoneOffset, @@ -444,7 +444,7 @@ const Schedule = { if (formik.values.sourceMode === "SCHEDULE_MULTIPLE") { value && changeStartDateMultiple( @@ -460,6 +460,11 @@ const Schedule = @@ -474,7 +479,7 @@ const Schedule = value && changeEndDateMultiple( value, @@ -482,6 +487,11 @@ const Schedule = diff --git a/src/components/shared/wizard/RenderField.tsx b/src/components/shared/wizard/RenderField.tsx index 1a72718c97..84af7296e1 100644 --- a/src/components/shared/wizard/RenderField.tsx +++ b/src/components/shared/wizard/RenderField.tsx @@ -1,9 +1,10 @@ import React from "react"; import { useTranslation } from "react-i18next"; -import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; +import DatePicker from "react-datepicker"; import cn from "classnames"; import { useClickOutsideField } from "../../../hooks/wizardHooks"; import { getMetadataCollectionFieldName } from "../../../utils/resourceUtils"; +import { getCurrentLanguageInformation } from "../../../utils/utils"; import DropDown, { DropDownType } from "../DropDown"; import RenderDate from "../RenderDate"; import { parseISO } from "date-fns"; @@ -179,24 +180,20 @@ const EditableDateValue = ({ handleKeyDown: (event: React.KeyboardEvent, type: string) => void }) => editMode ? (
- setFieldValue(field.name, value)} - onClose={() => setEditMode(false)} - slotProps={{ - textField: { - fullWidth: true, - onKeyDown: (event) => { - if (event.key === "Enter") { - handleKeyDown(event, "date") - } - }, - onBlur: (event) => { - setEditMode(false) - } - } - }} + onClickOutside={() => setEditMode(false)} + onBlur={() => setEditMode(false)} + showTimeInput + dateFormat="Pp" + startOpen + popperPlacement="bottom" + popperClassName="datepicker-custom" + className="datepicker-custom-input" + wrapperClassName="datepicker-custom-wrapper" + locale={getCurrentLanguageInformation()?.dateLocale} />
) : ( @@ -388,24 +385,21 @@ const EditableSingleValueTime = ({ return editMode ? (
- setFieldValue(field.name, value)} - onClose={() => setEditMode(false)} - slotProps={{ - textField: { - fullWidth: true, - onKeyDown: (event) => { - if (event.key === "Enter") { - handleKeyDown(event, "date") - } - }, - onBlur: () => { - setEditMode(false) - } - } - }} + onClickOutside={() => setEditMode(false)} + onBlur={() => setEditMode(false)} + showTimeSelect + showTimeSelectOnly + dateFormat="p" + startOpen + popperPlacement="bottom" + popperClassName="datepicker-custom" + className="datepicker-custom-input" + wrapperClassName="datepicker-custom-wrapper" + locale={getCurrentLanguageInformation()?.dateLocale} />
) : ( From 27ff91fc8f15a65098ca9acb4959290049e64cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20N=C3=B6sberger?= Date: Fri, 29 Nov 2024 13:53:38 +0100 Subject: [PATCH 3/6] Fix datepicker localization issue with languages codes like "de-CH" --- src/utils/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 8e6857d36b..0344429ec2 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -23,7 +23,11 @@ export const getCurrentLanguageInformation = () => { // Get code, flag, name and date locale of the current language let currentLang = languages.find(({ code }) => code === i18n.language); if (typeof currentLang === "undefined") { - currentLang = languages.find(({ code }) => code === "en-US"); + // If detected language code, like "de-CH", isn't part of translations try 2-digit language code + currentLang = languages.find(({ code }) => code === i18n.language.split("-")[0]); + if (typeof currentLang === "undefined") { + currentLang = languages.find(({ code }) => code === "en-US"); + } } return currentLang; From d01b7cc7cb4c3dbe3ce6e088d9f94f69632c3c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20N=C3=B6sberger?= Date: Fri, 6 Dec 2024 09:15:06 +0100 Subject: [PATCH 4/6] Add auto focus on react-datepicker input field --- src/components/shared/TableFilters.tsx | 3 +-- src/components/shared/wizard/RenderField.tsx | 12 ++++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/shared/TableFilters.tsx b/src/components/shared/TableFilters.tsx index ba436639c7..809443f860 100644 --- a/src/components/shared/TableFilters.tsx +++ b/src/components/shared/TableFilters.tsx @@ -426,8 +426,7 @@ const FilterSwitch = ({
handleDate(dates)} startDate={startDate} diff --git a/src/components/shared/wizard/RenderField.tsx b/src/components/shared/wizard/RenderField.tsx index 84af7296e1..e6b9599bc3 100644 --- a/src/components/shared/wizard/RenderField.tsx +++ b/src/components/shared/wizard/RenderField.tsx @@ -181,15 +181,13 @@ const EditableDateValue = ({ }) => editMode ? (
setFieldValue(field.name, value)} onClickOutside={() => setEditMode(false)} - onBlur={() => setEditMode(false)} showTimeInput dateFormat="Pp" - startOpen - popperPlacement="bottom" + popperPlacement="bottom-start" popperClassName="datepicker-custom" className="datepicker-custom-input" wrapperClassName="datepicker-custom-wrapper" @@ -386,16 +384,14 @@ const EditableSingleValueTime = ({ return editMode ? (
setFieldValue(field.name, value)} onClickOutside={() => setEditMode(false)} - onBlur={() => setEditMode(false)} showTimeSelect showTimeSelectOnly dateFormat="p" - startOpen - popperPlacement="bottom" + popperPlacement="bottom-start" popperClassName="datepicker-custom" className="datepicker-custom-input" wrapperClassName="datepicker-custom-wrapper" From ae8d344f91e635d78cfad497906790d44e488441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20N=C3=B6sberger?= Date: Fri, 6 Dec 2024 15:31:48 +0100 Subject: [PATCH 5/6] Add year and month dropdown for react-datepicker --- .../ModalTabsAndPages/EventDetailsSchedulingTab.tsx | 3 +++ .../events/partials/ModalTabsAndPages/NewSourcePage.tsx | 6 ++++++ src/components/shared/TableFilters.tsx | 3 +++ src/components/shared/wizard/RenderField.tsx | 3 +++ 4 files changed, 15 insertions(+) diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx index e8052c2ad6..bbec1c0c49 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx @@ -296,6 +296,9 @@ const EventDetailsSchedulingTab = ({ checkConflictsWrapper ) } + showYearDropdown + showMonthDropdown + yearDropdownItemNumber={2} dateFormat="P" popperClassName="datepicker-custom" className="datepicker-custom-input" diff --git a/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx b/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx index b6d8f9e3e8..d85537eec2 100644 --- a/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx @@ -460,6 +460,9 @@ const Schedule = setFieldValue(field.name, value)} onClickOutside={() => setEditMode(false)} showTimeInput + showYearDropdown + showMonthDropdown + yearDropdownItemNumber={2} dateFormat="Pp" popperPlacement="bottom-start" popperClassName="datepicker-custom" From 55db4a10d50d75d0faec1f515fafd2ff51783c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20N=C3=B6sberger?= Date: Fri, 13 Dec 2024 17:03:53 +0100 Subject: [PATCH 6/6] Add styling to up/down buttons in react-datepicker years dropdown --- src/styles/components/_datepicker-custom.scss | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/styles/components/_datepicker-custom.scss b/src/styles/components/_datepicker-custom.scss index 4f9fc2cea8..b0816d9ffe 100644 --- a/src/styles/components/_datepicker-custom.scss +++ b/src/styles/components/_datepicker-custom.scss @@ -18,7 +18,7 @@ * the License. * */ - + @use "sass:color"; .datepicker-custom { z-index: 10000 !important; @@ -30,4 +30,32 @@ &-input { height: 25px !important; } - } + } + + .react-datepicker__navigation--years { + &::before { + border-color: #ccc; + border-style: solid; + border-width: 3px 3px 0 0; + content: ''; + display: block; + height: 9px; + left: 11px; + position: absolute; + width: 9px; + } + + &-upcoming::before { + top: 17px; + transform: rotate(315deg); + } + + &-previous::before { + top: 6px; + transform: rotate(135deg); + } + + &:hover::before { + border-color: color.adjust(#ccc, $lightness: -15%); + } + }