2024-01-16 01:13:52 +05:30
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import './CustomTimePicker.styles.scss' ;
2024-01-17 13:01:55 +05:30
import { Input , Popover , Tooltip , Typography } from 'antd' ;
2024-12-20 11:38:36 +04:30
import logEvent from 'api/common/logEvent' ;
2024-01-16 01:13:52 +05:30
import cx from 'classnames' ;
2024-02-12 00:23:19 +05:30
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal' ;
import {
2025-05-29 10:53:47 +07:00
CustomTimeType ,
2024-02-12 00:23:19 +05:30
FixedDurationSuggestionOptions ,
2024-03-29 14:53:48 +05:30
Options ,
2024-02-12 00:23:19 +05:30
RelativeDurationSuggestionOptions ,
2025-05-29 10:53:47 +07:00
Time ,
2024-02-12 00:23:19 +05:30
} from 'container/TopNav/DateTimeSelectionV2/config' ;
2024-01-16 01:13:52 +05:30
import dayjs from 'dayjs' ;
2024-03-29 14:53:48 +05:30
import { isValidTimeFormat } from 'lib/getMinMax' ;
import { defaultTo , isFunction , noop } from 'lodash-es' ;
2024-01-16 01:13:52 +05:30
import debounce from 'lodash-es/debounce' ;
import { CheckCircle , ChevronDown , Clock } from 'lucide-react' ;
2024-12-16 10:27:20 +04:30
import { useTimezone } from 'providers/Timezone' ;
2024-02-12 00:23:19 +05:30
import {
ChangeEvent ,
Dispatch ,
SetStateAction ,
2024-12-16 10:27:20 +04:30
useCallback ,
2024-02-12 00:23:19 +05:30
useEffect ,
2024-12-16 10:27:20 +04:30
useMemo ,
2024-02-12 00:23:19 +05:30
useState ,
} from 'react' ;
2024-02-14 16:47:39 +05:30
import { useLocation } from 'react-router-dom' ;
2024-01-16 01:13:52 +05:30
import { popupContainer } from 'utils/selectPopupContainer' ;
2024-02-12 00:23:19 +05:30
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent' ;
2024-01-17 13:01:55 +05:30
const maxAllowedMinTimeInMonths = 6 ;
2024-12-16 10:27:20 +04:30
type ViewType = 'datetime' | 'timezone' ;
const DEFAULT_VIEW : ViewType = 'datetime' ;
2024-01-17 13:01:55 +05:30
2024-01-16 01:13:52 +05:30
interface CustomTimePickerProps {
onSelect : ( value : string ) = > void ;
2024-01-17 13:01:55 +05:30
onError : ( value : boolean ) = > void ;
2024-01-16 01:13:52 +05:30
selectedValue : string ;
selectedTime : string ;
2024-03-29 14:53:48 +05:30
onValidCustomDateChange : ( {
time : [ t1 , t2 ] ,
timeStr ,
} : {
time : [ dayjs . Dayjs | null , dayjs . Dayjs | null ] ;
timeStr : string ;
} ) = > void ;
onCustomTimeStatusUpdate ? : ( isValid : boolean ) = > void ;
2024-02-12 00:23:19 +05:30
open : boolean ;
setOpen : Dispatch < SetStateAction < boolean > > ;
items : any [ ] ;
newPopover? : boolean ;
customDateTimeVisible? : boolean ;
setCustomDTPickerVisible? : Dispatch < SetStateAction < boolean > > ;
onCustomDateHandler ? : ( dateTimeRange : DateTimeRangeType ) = > void ;
handleGoLive ? : ( ) = > void ;
2025-05-29 10:53:47 +07:00
onTimeChange ? : (
interval : Time | CustomTimeType ,
dateTimeRange ? : [ number , number ] ,
) = > void ;
2024-01-16 01:13:52 +05:30
}
function CustomTimePicker ( {
onSelect ,
2024-01-17 13:01:55 +05:30
onError ,
2024-01-16 01:13:52 +05:30
items ,
selectedValue ,
selectedTime ,
2024-02-12 00:23:19 +05:30
open ,
setOpen ,
2024-01-16 01:13:52 +05:30
onValidCustomDateChange ,
2024-03-29 14:53:48 +05:30
onCustomTimeStatusUpdate ,
2024-02-12 00:23:19 +05:30
newPopover ,
customDateTimeVisible ,
setCustomDTPickerVisible ,
onCustomDateHandler ,
handleGoLive ,
2025-05-29 10:53:47 +07:00
onTimeChange ,
2024-01-16 01:13:52 +05:30
} : CustomTimePickerProps ) : JSX . Element {
const [
selectedTimePlaceholderValue ,
setSelectedTimePlaceholderValue ,
] = useState ( 'Select / Enter Time Range' ) ;
const [ inputValue , setInputValue ] = useState ( '' ) ;
const [ inputStatus , setInputStatus ] = useState < '' | 'error' | 'success' > ( '' ) ;
2024-01-17 13:01:55 +05:30
const [ inputErrorMessage , setInputErrorMessage ] = useState < string | null > (
null ,
) ;
2024-02-14 16:47:39 +05:30
const location = useLocation ( ) ;
2024-01-16 01:13:52 +05:30
const [ isInputFocused , setIsInputFocused ] = useState ( false ) ;
2024-12-16 10:27:20 +04:30
const [ activeView , setActiveView ] = useState < ViewType > ( DEFAULT_VIEW ) ;
const { timezone , browserTimezone } = useTimezone ( ) ;
const activeTimezoneOffset = timezone . offset ;
const isTimezoneOverridden = useMemo (
( ) = > timezone . offset !== browserTimezone . offset ,
[ timezone , browserTimezone ] ,
) ;
const handleViewChange = useCallback (
( newView : 'timezone' | 'datetime' ) : void = > {
if ( activeView !== newView ) {
setActiveView ( newView ) ;
}
setOpen ( true ) ;
} ,
[ activeView , setOpen ] ,
) ;
const [ isOpenedFromFooter , setIsOpenedFromFooter ] = useState ( false ) ;
2024-01-16 01:13:52 +05:30
const getSelectedTimeRangeLabel = (
selectedTime : string ,
selectedTimeValue : string ,
) : string = > {
if ( selectedTime === 'custom' ) {
2025-01-27 16:28:54 +04:30
// TODO(shaheer): if the user preference is 12 hour format, then convert the date range string to 12-hour format (pick this up while working on 12/24 hour preference feature)
// // Convert the date range string to 12-hour format
// const dates = selectedTimeValue.split(' - ');
// if (dates.length === 2) {
// const startDate = dayjs(dates[0], DATE_TIME_FORMATS.UK_DATETIME);
// const endDate = dayjs(dates[1], DATE_TIME_FORMATS.UK_DATETIME);
// return `${startDate.format(DATE_TIME_FORMATS.UK_DATETIME)} - ${endDate.format(
// DATE_TIME_FORMATS.UK_DATETIME,
// )}`;
// }
2024-01-16 01:13:52 +05:30
return selectedTimeValue ;
}
for ( let index = 0 ; index < Options . length ; index ++ ) {
if ( Options [ index ] . value === selectedTime ) {
return Options [ index ] . label ;
}
}
2024-03-29 14:53:48 +05:30
2024-02-12 00:23:19 +05:30
for (
let index = 0 ;
index < RelativeDurationSuggestionOptions . length ;
index ++
) {
if ( RelativeDurationSuggestionOptions [ index ] . value === selectedTime ) {
return RelativeDurationSuggestionOptions [ index ] . label ;
}
}
2024-03-29 14:53:48 +05:30
2024-02-12 00:23:19 +05:30
for ( let index = 0 ; index < FixedDurationSuggestionOptions . length ; index ++ ) {
if ( FixedDurationSuggestionOptions [ index ] . value === selectedTime ) {
return FixedDurationSuggestionOptions [ index ] . label ;
}
}
2024-01-16 01:13:52 +05:30
2024-03-29 14:53:48 +05:30
if ( isValidTimeFormat ( selectedTime ) ) {
return selectedTime ;
}
2024-01-16 01:13:52 +05:30
return '' ;
} ;
useEffect ( ( ) = > {
const value = getSelectedTimeRangeLabel ( selectedTime , selectedValue ) ;
setSelectedTimePlaceholderValue ( value ) ;
} , [ selectedTime , selectedValue ] ) ;
const hide = ( ) : void = > {
setOpen ( false ) ;
} ;
const handleOpenChange = ( newOpen : boolean ) : void = > {
setOpen ( newOpen ) ;
2024-03-11 14:39:17 +05:30
if ( ! newOpen ) {
setCustomDTPickerVisible ? . ( false ) ;
2024-12-16 10:27:20 +04:30
setActiveView ( 'datetime' ) ;
2024-03-11 14:39:17 +05:30
}
2024-01-16 01:13:52 +05:30
} ;
const debouncedHandleInputChange = debounce ( ( inputValue ) : void = > {
const isValidFormat = /^(\d+)([mhdw])$/ . test ( inputValue ) ;
if ( isValidFormat ) {
setInputStatus ( 'success' ) ;
2024-01-17 13:01:55 +05:30
onError ( false ) ;
setInputErrorMessage ( null ) ;
2024-01-16 01:13:52 +05:30
const match = inputValue . match ( /^(\d+)([mhdw])$/ ) ;
const value = parseInt ( match [ 1 ] , 10 ) ;
const unit = match [ 2 ] ;
const currentTime = dayjs ( ) ;
2024-01-17 13:01:55 +05:30
const maxAllowedMinTime = currentTime . subtract (
maxAllowedMinTimeInMonths ,
'month' ,
) ;
2024-01-16 01:13:52 +05:30
let minTime = null ;
switch ( unit ) {
case 'm' :
minTime = currentTime . subtract ( value , 'minute' ) ;
break ;
case 'h' :
minTime = currentTime . subtract ( value , 'hour' ) ;
break ;
case 'd' :
minTime = currentTime . subtract ( value , 'day' ) ;
break ;
case 'w' :
minTime = currentTime . subtract ( value , 'week' ) ;
break ;
default :
break ;
}
2024-02-14 17:26:33 +05:30
if ( minTime && ( ! minTime . isValid ( ) || minTime < maxAllowedMinTime ) ) {
2024-01-17 13:01:55 +05:30
setInputStatus ( 'error' ) ;
onError ( true ) ;
setInputErrorMessage ( 'Please enter time less than 6 months' ) ;
2024-03-29 14:53:48 +05:30
if ( isFunction ( onCustomTimeStatusUpdate ) ) {
onCustomTimeStatusUpdate ( true ) ;
}
2024-01-17 13:01:55 +05:30
} else {
2024-03-29 14:53:48 +05:30
onValidCustomDateChange ( {
time : [ minTime , currentTime ] ,
timeStr : inputValue ,
} ) ;
2024-01-17 13:01:55 +05:30
}
2024-01-16 01:13:52 +05:30
} else {
setInputStatus ( 'error' ) ;
2024-01-17 13:01:55 +05:30
onError ( true ) ;
setInputErrorMessage ( null ) ;
2024-03-29 14:53:48 +05:30
if ( isFunction ( onCustomTimeStatusUpdate ) ) {
onCustomTimeStatusUpdate ( false ) ;
}
2024-01-16 01:13:52 +05:30
}
} , 300 ) ;
const handleInputChange = ( event : ChangeEvent < HTMLInputElement > ) : void = > {
const inputValue = event . target . value ;
if ( inputValue . length > 0 ) {
setOpen ( false ) ;
} else {
setOpen ( true ) ;
}
setInputValue ( inputValue ) ;
// Call the debounced function with the input value
debouncedHandleInputChange ( inputValue ) ;
} ;
2024-02-12 00:23:19 +05:30
const handleSelect = ( label : string , value : string ) : void = > {
onSelect ( value ) ;
setSelectedTimePlaceholderValue ( label ) ;
setInputStatus ( '' ) ;
onError ( false ) ;
setInputErrorMessage ( null ) ;
setInputValue ( '' ) ;
if ( value !== 'custom' ) {
hide ( ) ;
}
} ;
2024-01-16 01:13:52 +05:30
const content = (
< div className = "time-selection-dropdown-content" >
< div className = "time-options-container" >
2024-02-12 00:23:19 +05:30
{ items ? . map ( ( { value , label } ) = > (
2024-01-16 01:13:52 +05:30
< div
onClick = { ( ) : void = > {
2024-02-12 00:23:19 +05:30
handleSelect ( label , value ) ;
2024-01-16 01:13:52 +05:30
} }
key = { value }
className = { cx (
'time-options-item' ,
selectedValue === value ? 'active' : '' ,
) }
>
{ label }
< / div >
) ) }
< / div >
< / div >
) ;
const handleFocus = ( ) : void = > {
setIsInputFocused ( true ) ;
2024-12-16 10:27:20 +04:30
setActiveView ( 'datetime' ) ;
2024-01-16 01:13:52 +05:30
} ;
const handleBlur = ( ) : void = > {
setIsInputFocused ( false ) ;
} ;
2024-02-14 16:47:39 +05:30
// this is required as TopNav component wraps the components and we need to clear the state on path change
useEffect ( ( ) = > {
setInputStatus ( '' ) ;
onError ( false ) ;
setInputErrorMessage ( null ) ;
setInputValue ( '' ) ;
// eslint-disable-next-line react-hooks/exhaustive-deps
} , [ location . pathname ] ) ;
2024-12-20 11:38:36 +04:30
const handleTimezoneHintClick = ( e : React.MouseEvent ) : void = > {
e . stopPropagation ( ) ;
handleViewChange ( 'timezone' ) ;
setIsOpenedFromFooter ( false ) ;
logEvent (
'DateTimePicker: Timezone picker opened from time range input badge' ,
{
page : location.pathname ,
} ,
) ;
} ;
2024-01-16 01:13:52 +05:30
return (
2024-01-17 13:01:55 +05:30
< div className = "custom-time-picker" >
< Popover
2024-01-24 16:09:32 +05:30
className = { cx (
'timeSelection-input-container' ,
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '' ,
) }
2024-01-17 13:01:55 +05:30
placement = "bottomRight"
getPopupContainer = { popupContainer }
2024-02-12 00:23:19 +05:30
rootClassName = "date-time-root"
content = {
newPopover ? (
< CustomTimePickerPopoverContent
setIsOpen = { setOpen }
customDateTimeVisible = { defaultTo ( customDateTimeVisible , false ) }
setCustomDTPickerVisible = { defaultTo ( setCustomDTPickerVisible , noop ) }
onCustomDateHandler = { defaultTo ( onCustomDateHandler , noop ) }
onSelectHandler = { handleSelect }
handleGoLive = { defaultTo ( handleGoLive , noop ) }
options = { items }
selectedTime = { selectedTime }
2024-12-16 10:27:20 +04:30
activeView = { activeView }
setActiveView = { setActiveView }
setIsOpenedFromFooter = { setIsOpenedFromFooter }
isOpenedFromFooter = { isOpenedFromFooter }
2025-05-29 10:53:47 +07:00
onTimeChange = { onTimeChange }
2024-02-12 00:23:19 +05:30
/ >
) : (
content
)
}
2024-01-17 13:01:55 +05:30
arrow = { false }
2024-06-20 19:19:42 +05:30
trigger = "click"
2024-01-17 13:01:55 +05:30
open = { open }
onOpenChange = { handleOpenChange }
2024-01-16 01:13:52 +05:30
style = { {
2024-01-17 13:01:55 +05:30
padding : 0 ,
2024-01-16 01:13:52 +05:30
} }
2024-01-17 13:01:55 +05:30
>
< Input
className = "timeSelection-input"
type = "text"
status = { inputValue && inputStatus === 'error' ? 'error' : '' }
placeholder = {
isInputFocused
? 'Time Format (1m or 2h or 3d or 4w)'
: selectedTimePlaceholderValue
}
value = { inputValue }
onFocus = { handleFocus }
onBlur = { handleBlur }
onChange = { handleInputChange }
prefix = {
inputValue && inputStatus === 'success' ? (
< CheckCircle size = { 14 } color = "#51E7A8" / >
) : (
< Tooltip title = "Enter time in format (e.g., 1m, 2h, 3d, 4w)" >
< Clock size = { 14 } / >
< / Tooltip >
)
}
suffix = {
2024-12-16 10:27:20 +04:30
< >
{ ! ! isTimezoneOverridden && activeTimezoneOffset && (
2024-12-20 11:38:36 +04:30
< div className = "timezone-badge" onClick = { handleTimezoneHintClick } >
2024-12-16 10:27:20 +04:30
< span > { activeTimezoneOffset } < / span >
< / div >
) }
< ChevronDown
size = { 14 }
onClick = { ( ) : void = > handleViewChange ( 'datetime' ) }
/ >
< / >
2024-01-17 13:01:55 +05:30
}
/ >
< / Popover >
{ inputStatus === 'error' && inputErrorMessage && (
< Typography.Title level = { 5 } className = "valid-format-error" >
{ inputErrorMessage }
< / Typography.Title >
) }
< / div >
2024-01-16 01:13:52 +05:30
) ;
}
export default CustomTimePicker ;
2024-02-12 00:23:19 +05:30
CustomTimePicker . defaultProps = {
newPopover : false ,
customDateTimeVisible : false ,
setCustomDTPickerVisible : noop ,
onCustomDateHandler : noop ,
handleGoLive : noop ,
2024-03-29 14:53:48 +05:30
onCustomTimeStatusUpdate : noop ,
2025-05-29 10:53:47 +07:00
onTimeChange : undefined ,
2024-02-12 00:23:19 +05:30
} ;