diff --git a/src/shell/components/FieldTypeDate/index.tsx b/src/shell/components/FieldTypeDate/index.tsx index d7fec6415f..bbd8610a17 100644 --- a/src/shell/components/FieldTypeDate/index.tsx +++ b/src/shell/components/FieldTypeDate/index.tsx @@ -4,7 +4,14 @@ import { DatePickerProps, } from "@mui/x-date-pickers-pro"; import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFns"; -import { memo, useEffect, useRef, useState } from "react"; +import { + memo, + useEffect, + useRef, + useState, + useImperativeHandle, + forwardRef, +} from "react"; import Button from "@mui/material/Button"; import { Typography, Stack, Box, TextField } from "@mui/material"; import format from "date-fns/format"; @@ -43,6 +50,8 @@ const parseDateInput = (input: string): Date | null => { const currentYear = new Date().getFullYear(); let [monthInput, dayInput, yearInput] = dateParts; + yearInput = yearInput.slice(0, 4); + dayInput = dayInput.slice(0, 2); let month = months[monthInput.toLowerCase().slice(0, 3)]; if (isNaN(month)) { month = currentMonth; @@ -58,161 +67,183 @@ const parseDateInput = (input: string): Date | null => { }; export const FieldTypeDate = memo( - ({ - required, - error, - slots, - onClear, - valueFormatPreview, - ...props - }: FieldTypeDateProps) => { - const textFieldRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - - /** - * Clear the input value - */ - const handleClear = () => { - if (props.onChange) props.onChange(null, null); - if (textFieldRef.current) textFieldRef.current.value = ""; - onClear && onClear(); - }; - - /** - * Open the date picker - */ - const handleOpen = () => { - setIsOpen(true); - }; - - useEffect(() => { - if (textFieldRef.current && isOpen) { - /** - * This Perform a check if there's no value set - * When the user clicks on the input field, set the value to the current date - */ - if (props.value === null) { - props.onChange(new Date(), null); - textFieldRef.current.value = format(new Date(), "MMM dd, yyyy"); - textFieldRef.current.setSelectionRange(0, 3); + forwardRef( + ( + { + required, + error, + slots, + onClear, + valueFormatPreview, + ...props + }: FieldTypeDateProps, + ref + ) => { + const textFieldRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + + /** + * Clear the input value + */ + const handleClear = () => { + if (props.onChange) props.onChange(null, null); + if (textFieldRef.current) textFieldRef.current.value = ""; + onClear && onClear(); + }; + + /** + * Open the date picker + */ + const handleOpen = () => { + setIsOpen(true); + }; + + useEffect(() => { + if (textFieldRef.current && isOpen) { + /** + * This Perform a check if there's no value set + * When the user clicks on the input field, set the value to the current date + */ + if (props.value === null) { + props.onChange(new Date(), null); + textFieldRef.current.value = format(new Date(), "MMM dd, yyyy"); + textFieldRef.current.setSelectionRange(0, 3); + } + + /** + * Delay the focus to the input field to + * ensure the picker is open before focusing to the field + */ + setTimeout(() => { + textFieldRef.current?.focus(); + }); } /** - * Delay the focus to the input field to - * ensure the picker is open before focusing to the field + * This handles the case when the user selects a date from the picker + * directly and not use the input field to manually enter the date */ - setTimeout(() => { - textFieldRef.current?.focus(); - }); - } + if (!isOpen && props.value) { + textFieldRef.current.value = format(props.value, "MMM dd, yyyy"); + } + textFieldRef.current.blur(); + }, [isOpen]); /** - * This handles the case when the user selects a date from the picker - * directly and not use the input field to manually enter the date + * handles the case when the value is set from the parent component or db values */ - if (!isOpen && props.value) { - textFieldRef.current.value = format(props.value, "MMM dd, yyyy"); - } - textFieldRef.current.blur(); - }, [isOpen]); - - /** - * handles the case when the value is set from the parent component or db values - */ - useEffect(() => { - if (props.value) { - textFieldRef.current.value = format(props.value, "MMM dd, yyyy"); - } - }, [props.value]); - - return ( - - - - { - setIsOpen(false); - }} - {...props} - inputRef={textFieldRef} - disableHighlightToday={!!props.value} - slots={{ - field: CustomField, - openPickerIcon: CalendarTodayRoundedIcon, - ...slots, - }} - slotProps={{ - desktopPaper: { - sx: { - mt: 1, - - "& .MuiDateCalendar-root .MuiPickersSlideTransition-root": { - minHeight: 0, - pb: 2, - pt: 1.5, + useEffect(() => { + if (props.value) { + textFieldRef.current.value = format(props.value, "MMM dd, yyyy"); + } + }, []); + + useImperativeHandle( + ref, + () => { + return { + setDefaultDate() { + textFieldRef.current.value = format(new Date(), "MMM dd, yyyy"); + }, + }; + }, + [] + ); + + return ( + + + + { + setIsOpen(false); + }} + {...props} + inputRef={textFieldRef} + disableHighlightToday={!!props.value} + slots={{ + field: CustomField, + openPickerIcon: CalendarTodayRoundedIcon, + ...slots, + }} + slotProps={{ + desktopPaper: { + sx: { + mt: 1, + + "& .MuiDateCalendar-root .MuiPickersSlideTransition-root": + { + minHeight: 0, + pb: 2, + pt: 1.5, + }, }, }, - }, - field: { - //@ts-expect-error - OnClick type does not exist on fieldProps - onClick: handleOpen, - onFocus: handleOpen, - error, - onChange: (e: any) => { - const inputDate = e.target.value; - const parsedDate = parseDateInput(inputDate); - - if (parsedDate) { - props.onChange(parsedDate, null); - } + field: { + //@ts-expect-error - OnClick type does not exist on fieldProps + onClick: handleOpen, + onFocus: handleOpen, + error, + onChange: (e: any) => { + const inputDate = e.target.value; + const parsedDate = parseDateInput(inputDate); + + if (parsedDate) { + props.onChange(parsedDate, null); + } + }, + onKeyDown: (evt: KeyboardEvent) => { + if (evt.key === "Enter") { + setIsOpen(false); + textFieldRef.current?.blur(); + } + + if (evt.key === "Tab") { + setIsOpen(false); + } + }, }, - onKeyDown: (evt: KeyboardEvent) => { - if (evt.key === "Enter") { - setIsOpen(false); - textFieldRef.current?.blur(); - } + inputAdornment: { + position: "start", }, - }, - inputAdornment: { - position: "start", - }, - openPickerButton: { - tabIndex: -1, - size: "small", - }, - openPickerIcon: { - sx: { - fontSize: 20, + openPickerButton: { + tabIndex: -1, + size: "small", }, - }, - }} - /> - - - {!!slots?.timePicker && slots.timePicker} - - - - {(valueFormatPreview || props.value) && ( - - Stored as{" "} - {valueFormatPreview ?? moment(props.value).format("yyyy-MM-DD")} - - )} - - ); - } + openPickerIcon: { + sx: { + fontSize: 20, + }, + }, + }} + /> + + + {!!slots?.timePicker && slots.timePicker} + + + + {(valueFormatPreview || props.value) && ( + + Stored as{" "} + {valueFormatPreview ?? moment(props.value).format("yyyy-MM-DD")} + + )} + + ); + } + ) ); function CustomField(props: any) { diff --git a/src/shell/components/FieldTypeDateTime/index.tsx b/src/shell/components/FieldTypeDateTime/index.tsx index 9ec561d6f4..015c76c407 100644 --- a/src/shell/components/FieldTypeDateTime/index.tsx +++ b/src/shell/components/FieldTypeDateTime/index.tsx @@ -1,7 +1,6 @@ -import { useEffect, useState, useMemo, useRef } from "react"; -import { TextField, Autocomplete, Typography, Tooltip } from "@mui/material"; +import { useEffect, useState, useRef } from "react"; +import { TextField, Autocomplete, Tooltip } from "@mui/material"; import moment from "moment"; -import { isEqual } from "lodash"; import { FieldTypeDate } from "../FieldTypeDate"; import { @@ -31,6 +30,7 @@ export const FieldTypeDateTime = ({ }: FieldTypeDateTimeProps) => { const timeFieldRef = useRef(null); const optionsRef = useRef(null); + const dateFieldRef = useRef(null); const [timeKeyCount, setTimeKeyCount] = useState(0); const [isTimeFieldActive, setIsTimeFieldActive] = useState(false); const [inputValue, setInputValue] = useState(""); @@ -49,9 +49,9 @@ export const FieldTypeDateTime = ({ }, [value]); useEffect(() => { - const { time, index } = getClosestTimeSuggestion(inputValue); + const { time, index } = getClosestTimeSuggestion(inputValue.trim()); - setInvalidInput(!!inputValue ? !time : false); + setInvalidInput(!!inputValue.trim() ? !time : false); const timeOptionElements = optionsRef.current?.querySelectorAll( "li.MuiAutocomplete-option" @@ -79,6 +79,7 @@ export const FieldTypeDateTime = ({ name={name} required={required} value={dateString ? moment(dateString).toDate() : null} + ref={dateFieldRef} valueFormatPreview={ dateString && timeString ? `${dateString} ${timeString}` : null } @@ -164,8 +165,10 @@ export const FieldTypeDateTime = ({ }} slotProps={{ paper: { + elevation: 8, sx: { width: 184, + mt: 1, }, }, }} @@ -182,13 +185,16 @@ export const FieldTypeDateTime = ({ placeholder="HH:MM" error={invalidInput || error} onClick={() => { - setIsTimeFieldActive(true); if (!dateString && !timeString) { onChange( `${moment().format("yyyy-MM-DD")} 00:00:00.000000` ); + dateFieldRef.current?.setDefaultDate(); } }} + onFocus={() => { + setIsTimeFieldActive(true); + }} onBlur={() => { if (!inputValue) { setInputValue( diff --git a/src/shell/components/FieldTypeDateTime/util.ts b/src/shell/components/FieldTypeDateTime/util.ts index 89f2872e92..9820ed7883 100644 --- a/src/shell/components/FieldTypeDateTime/util.ts +++ b/src/shell/components/FieldTypeDateTime/util.ts @@ -121,10 +121,16 @@ export const getDerivedTime = (userInput: string) => { return ""; } - let periodOfTime = userInput.split(" ")?.[1]; - const timeInput = userInput.split(" ")?.[0]; - const hourInput = timeInput?.split(":")?.[0]; - let minuteInput = timeInput?.split(":")?.[1]; + const matchedPeriodOfTime = userInput.match( + /(? { } // Determines wether we'll try to match am or pm times - if (!periodOfTime) { - periodOfTime = +hourInput >= 7 && +hourInput <= 11 ? "am" : "pm"; - } else if (periodOfTime === "a") { - periodOfTime = "am"; - } else if (periodOfTime === "p") { - periodOfTime = "pm"; + if (!periodOfTimeValue) { + periodOfTimeValue = +hourInput >= 7 && +hourInput <= 11 ? "am" : "pm"; + } else if (periodOfTimeValue.startsWith("a")) { + periodOfTimeValue = "am"; + } else if (periodOfTimeValue.startsWith("p")) { + periodOfTimeValue = "pm"; } - return `${hourInput}:${minuteInput} ${periodOfTime}`; + return `${hourInput}:${minuteInput} ${periodOfTimeValue}`; }; export const getClosestTimeSuggestion = (input: string) => { @@ -153,10 +159,20 @@ export const getClosestTimeSuggestion = (input: string) => { const derivedTime = getDerivedTime(input); const matchedTimeIndex = TIME_OPTIONS.findIndex((time) => { + const timeToTest = new Date(`01/01/2024 ${derivedTime}`).getTime() / 1000; + + // Makes sure that 11:53 pm to 11:59 pm are being matched to 12:00 am since it is the closest time option + if ( + timeToTest >= 1704124380 && + timeToTest <= 1704124740 && + time.inputValue === "12:00 am" + ) { + return time.inputValue; + } + return ( Math.abs( - new Date(`01/01/2024 ${time.inputValue}`).getTime() / 1000 - - new Date(`01/01/2024 ${derivedTime}`).getTime() / 1000 + new Date(`01/01/2024 ${time.inputValue}`).getTime() / 1000 - timeToTest ) <= 420 ); });