Skip to content

Commit

Permalink
Merge branch 'dev' into auto-sort-on-sort
Browse files Browse the repository at this point in the history
# Conflicts:
#	lib/components/form/call-taker/date-time-picker.tsx
  • Loading branch information
miles-grant-ibigroup committed Dec 19, 2024
2 parents 5678c22 + d4f9ba8 commit 8a2d566
Show file tree
Hide file tree
Showing 9 changed files with 483 additions and 427 deletions.
412 changes: 213 additions & 199 deletions __tests__/components/viewers/__snapshots__/nearby-view.js.snap

Large diffs are not rendered by default.

28 changes: 25 additions & 3 deletions lib/components/app/responsive-webapp.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ class ResponsiveWebapp extends Component {

/** Lifecycle methods **/

// Check if the position has changed enough to update the currentPosition
// (prevent constant updates in nearby view)
// .001 works out to be about 94-300 ft depending on the longitude.
positionShouldUpdate = (position) => {
const { currentPosition } = this.props
if (!currentPosition.coords) return true
const latChanged =
Math.abs(
position?.coords?.latitude - currentPosition?.coords?.latitude
) >= 0.001
const lonChanged =
Math.abs(
position?.coords?.longitude - currentPosition?.coords?.longitude
) >= 0.001

return latChanged || lonChanged
}

/* eslint-disable-next-line complexity */
componentDidUpdate(prevProps) {
const {
Expand Down Expand Up @@ -179,11 +197,13 @@ class ResponsiveWebapp extends Component {
if (isMobile()) {
// Test location availability on load
getCurrentPosition(intl)
// Also, watch for changes in position on mobile
// Watch for position changing on mobile
navigator.geolocation.watchPosition(
// On success
(position) => {
receivedPositionResponse({ position })
if (this.positionShouldUpdate(position)) {
receivedPositionResponse({ position })
}
},
// On error
(error) => {
Expand Down Expand Up @@ -426,10 +446,12 @@ class RouterWrapperWithAuth0 extends Component {
}

const mapStateToWrapperProps = (state) => {
const { homeTimezone, map, persistence, reactRouter } = state.otp.config
const { homeTimezone, location, map, persistence, reactRouter } =
state.otp.config
return {
auth0Config: getAuth0Config(persistence),
autoFly: map.autoFlyOnTripFormUpdate,
currentPosition: location?.currentPosition,
defaultLocale: getDefaultLocale(state.otp.config, state.user.loggedInUser),
homeTimezone,
locale: state.otp.ui.locale,
Expand Down
199 changes: 112 additions & 87 deletions lib/components/form/call-taker/date-time-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import { IntlShape, useIntl } from 'react-intl'
import { isMatch, parse } from 'date-fns'
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import coreUtils from '@opentripplanner/core-utils'
import React, { useEffect, useRef, useState } from 'react'
import React, {
ChangeEvent,
useCallback,
useEffect,
useRef,
useState
} from 'react'

import { AppReduxState, FilterType, SortType } from '../../../util/state-types'
import {
Expand Down Expand Up @@ -73,6 +79,30 @@ const safeFormat = (date: Date | '', time: string, options?: OptionsWithTZ) => {
}
return ''
}
/**
* Parse a time input expressed in the agency time zone.
* @returns A date if the parsing succeeded, or null.
*/
const parseInputAsTime = (
homeTimezone: string,
timeInput: string = getCurrentTime(homeTimezone),
date: string = getCurrentDate(homeTimezone)
) => {
if (!timeInput) timeInput = getCurrentTime(homeTimezone)

// Match one of the supported time formats
const matchedTimeFormat = SUPPORTED_TIME_FORMATS.find((timeFormat) =>
isMatch(timeInput, timeFormat)
)
if (matchedTimeFormat) {
const resolvedDateTime = format(
parse(timeInput, matchedTimeFormat, new Date()),
'HH:mm:ss'
)
return toDate(`${date}T${resolvedDateTime}`)
}
return ''
}

type Props = {
date?: string
Expand Down Expand Up @@ -125,69 +155,36 @@ const DateTimeOptions = ({
)
const [date, setDate] = useState<string | undefined>(initialDate)
const [time, setTime] = useState<string | undefined>(initialTime)
const [typedTime, setTypedTime] = useState<string | undefined>(initialTime)
const [typedTime, setTypedTime] = useState<string | undefined>(
safeFormat(parseInputAsTime(homeTimezone, time, date), timeFormat, {
timeZone: homeTimezone
})
)

const timeRef = useRef(null)

const intl = useIntl()

/**
* Parse a time input expressed in the agency time zone.
* @returns A date if the parsing succeeded, or null.
*/
const parseInputAsTime = (
timeInput: string = getCurrentTime(homeTimezone),
date: string = getCurrentDate(homeTimezone)
) => {
if (!timeInput) timeInput = getCurrentTime(homeTimezone)

// Match one of the supported time formats
const matchedTimeFormat = SUPPORTED_TIME_FORMATS.find((timeFormat) =>
isMatch(timeInput, timeFormat)
)
if (matchedTimeFormat) {
const resolvedDateTime = format(
parse(timeInput, matchedTimeFormat, new Date()),
'HH:mm:ss'
)
return toDate(`${date}T${resolvedDateTime}`)
}
return ''
}

const dateTime = parseInputAsTime(time, date)
const dateTime = parseInputAsTime(homeTimezone, time, date)

// Update state when external state is updated
useEffect(() => {
if (initialDate !== date) setDate(initialDate)
if (initialTime !== time) {
setTime(initialTime)
handleTimeChange(initialTime || '')
}
// This effect is design to flow from state to component only
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialTime, initialDate])

useEffect(() => {
// Don't update if still typing
if (timeRef.current !== document.activeElement) {
setTypedTime(
safeFormat(dateTime, timeFormat, {
timeZone: homeTimezone
}) ||
// TODO: there doesn't seem to be an intl object present?
'Invalid Time'
)
}
}, [time])

useEffect(() => {
if (initialDepartArrive && departArrive !== initialDepartArrive) {
setDepartArrive(initialDepartArrive)
}
// This effect is design to flow from state to component only
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialDepartArrive])

useEffect(() => {
if (departArrive === 'NOW') setTypedTime('')
}, [departArrive])

// Handler for setting the query parameters
useEffect(() => {
if (safeFormat(dateTime, OTP_API_DATE_FORMAT, {}) !== '' && setQueryParam) {
Expand All @@ -201,43 +198,65 @@ const DateTimeOptions = ({
})
})
}

if (
syncSortWithDepartArrive &&
DepartArriveTypeMap[departArrive] !== sort.type
) {
importedUpdateItineraryFilter({
sort: {
...sort,
direction: DepartArriveDirectionMap[departArrive] || sort.direction,
type: DepartArriveTypeMap[departArrive]
}
})
}
}, [dateTime, departArrive, homeTimezone, setQueryParam])

// Handler for updating the time and date fields when NOW is selected
useEffect(() => {
if (departArrive === 'NOW') {
setTime(getCurrentTime(homeTimezone))
setDate(getCurrentDate(homeTimezone))
setTypedTime(
safeFormat(dateTime, timeFormat, {
timeZone: homeTimezone
const handleDepartArriveChange = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
const newValue = e.target.value as DepartArriveValue
setDepartArrive(newValue)

// Handler for updating the time and date fields when NOW is selected
if (newValue === 'NOW') {
handleTimeChange(getCurrentTime(homeTimezone))
setDate(getCurrentDate(homeTimezone))
setTypedTime(
safeFormat(dateTime, timeFormat, {
timeZone: homeTimezone
})
)
}

// Update sort type if needed
if (
syncSortWithDepartArrive &&
DepartArriveTypeMap[newValue] !== sort.type
) {
importedUpdateItineraryFilter({
sort: {
...sort,
direction: DepartArriveDirectionMap[departArrive] || sort.direction,
type: DepartArriveTypeMap[newValue]
}
})
)
}
}, [departArrive, setTime, setDate, homeTimezone])
}
},
[syncSortWithDepartArrive, sort, importedUpdateItineraryFilter]
)

const unsetNow = () => {
const unsetNow = useCallback(() => {
if (departArrive === 'NOW') setDepartArrive('DEPART')
}
}, [departArrive])

const handleTimeChange = useCallback(
(newTime: string) => {
setTime(newTime)
// Only update typed time if not actively typing
if (timeRef.current !== document.activeElement) {
setTypedTime(
safeFormat(dateTime, timeFormat, {
timeZone: homeTimezone
}) || 'Invalid Time'
)
}
},
[dateTime, timeFormat, homeTimezone]
)

return (
<>
<select
onBlur={(e) => setDepartArrive(e.target.value as DepartArriveValue)}
onChange={(e) => setDepartArrive(e.target.value as DepartArriveValue)}
onBlur={handleDepartArriveChange}
onChange={handleDepartArriveChange}
onKeyDown={onKeyDown}
value={departArrive}
>
Expand All @@ -262,11 +281,14 @@ const DateTimeOptions = ({
>
<input
className="datetime-slim"
onChange={(e) => {
setTime(e.target.value)
setTypedTime(e.target.value)
unsetNow()
}}
onChange={useCallback(
(e) => {
handleTimeChange(e.target.value)
setTypedTime(e.target.value)
unsetNow()
},
[handleTimeChange, setTypedTime, unsetNow]
)}
onFocus={(e) => e.target.select()}
onKeyDown={onKeyDown}
ref={timeRef}
Expand All @@ -283,15 +305,18 @@ const DateTimeOptions = ({
<input
className="datetime-slim"
disabled={!dateTime}
onChange={(e) => {
if (!e.target.value) {
e.preventDefault()
// TODO: prevent selection from advancing to next field
return
}
setDate(e.target.value)
unsetNow()
}}
onChange={useCallback(
(e) => {
if (!e.target.value) {
e.preventDefault()
// TODO: prevent selection from advancing to next field
return
}
setDate(e.target.value)
unsetNow()
},
[unsetNow, setDate]
)}
onKeyDown={onKeyDown}
style={{
fontSize: '14px',
Expand Down
2 changes: 1 addition & 1 deletion lib/components/mobile/navigation-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class MobileNavigationBar extends Component {
const backButtonText = intl.formatMessage({ id: 'common.forms.back' })

return (
<header>
<header style={{ height: '50px' }}>
<Navbar className="mobile-navbar-container" fixedTop fluid>
<Navbar.Header>
<Navbar.Brand>
Expand Down
Loading

0 comments on commit 8a2d566

Please sign in to comment.