Skip to content

Commit 1a6c5f3

Browse files
authored
Merge pull request #290 from opentripplanner/trip-status-ui
Trip Status UI
2 parents 05b1b5b + 040224d commit 1a6c5f3

20 files changed

+606
-44
lines changed

Diff for: lib/actions/user.js

+69-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
import clone from 'clone'
2+
import moment from 'moment'
3+
import { planParamsToQuery } from '@opentripplanner/core-utils/lib/query'
4+
import { OTP_API_DATE_FORMAT } from '@opentripplanner/core-utils/lib/time'
5+
import qs from 'qs'
16
import { createAction } from 'redux-actions'
27

8+
import { routingQuery } from './api'
9+
import { setQueryParam } from './form'
310
import { routeTo } from './ui'
411
import { secureFetch } from '../util/middleware'
512
import { isNewUser } from '../util/user'
@@ -192,7 +199,12 @@ export function fetchUserMonitoredTrips () {
192199
* then, if that was successful, alerts (optional)
193200
* and refreshes the redux monitoredTrips with the updated trip.
194201
*/
195-
export function createOrUpdateUserMonitoredTrip (tripData, isNew, silentOnSuccess) {
202+
export function createOrUpdateUserMonitoredTrip (
203+
tripData,
204+
isNew,
205+
silentOnSuccess,
206+
noRedirect
207+
) {
196208
return async function (dispatch, getState) {
197209
const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
198210
const { id } = tripData
@@ -220,6 +232,8 @@ export function createOrUpdateUserMonitoredTrip (tripData, isNew, silentOnSucces
220232
// Reload user's monitored trips after add/update.
221233
await dispatch(fetchUserMonitoredTrips())
222234

235+
if (noRedirect) return
236+
223237
// Finally, navigate to the saved trips page.
224238
dispatch(routeTo('/savedtrips'))
225239
} else {
@@ -228,12 +242,39 @@ export function createOrUpdateUserMonitoredTrip (tripData, isNew, silentOnSucces
228242
}
229243
}
230244

245+
/**
246+
* Toggles the isActive status of a monitored trip
247+
*/
248+
export function togglePauseTrip (trip) {
249+
return function (dispatch, getState) {
250+
const clonedTrip = clone(trip)
251+
clonedTrip.isActive = !clonedTrip.isActive
252+
253+
// Silent update of existing trip.
254+
dispatch(createOrUpdateUserMonitoredTrip(clonedTrip, false, true, true))
255+
}
256+
}
257+
258+
/**
259+
* Toggles the snoozed status of a monitored trip
260+
*/
261+
export function toggleSnoozeTrip (trip) {
262+
return function (dispatch, getState) {
263+
const newTrip = clone(trip)
264+
newTrip.snoozed = !newTrip.snoozed
265+
266+
// Silent update of existing trip.
267+
dispatch(createOrUpdateUserMonitoredTrip(newTrip, false, true, true))
268+
}
269+
}
270+
231271
/**
232272
* Deletes a logged-in user's monitored trip,
233273
* then, if that was successful, refreshes the redux monitoredTrips state.
234274
*/
235-
export function deleteUserMonitoredTrip (tripId) {
275+
export function confirmAndDeleteUserMonitoredTrip (tripId) {
236276
return async function (dispatch, getState) {
277+
if (!confirm('Would you like to remove this trip?')) return
237278
const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
238279
const requestUrl = `${apiBaseUrl}${API_MONITORED_TRIP_PATH}/${tripId}`
239280

@@ -334,3 +375,29 @@ export function checkItineraryExistence (trip) {
334375
}
335376
}
336377
}
378+
379+
/**
380+
* Plans a new trip for the current date given the query parameters in the given
381+
* monitored trip
382+
*/
383+
export function planNewTripFromMonitoredTrip (monitoredTrip) {
384+
return function (dispatch, getState) {
385+
// update query params in store
386+
const newQuery = planParamsToQuery(qs.parse(monitoredTrip.queryParams))
387+
newQuery.date = moment().format(OTP_API_DATE_FORMAT)
388+
389+
dispatch(setQueryParam(newQuery))
390+
391+
dispatch(routeTo('/'))
392+
393+
// This prevents some kind of race condition whose origin I can't figure
394+
// out. Unless this is called after redux catches up with routing to the '/'
395+
// path, then the old path will be used and the screen won't change.
396+
// Therefore, this timeout occurs so that the view of the homepage has time
397+
// to render itself.
398+
// FIXME: remove hack
399+
setTimeout(() => {
400+
dispatch(routingQuery())
401+
}, 300)
402+
}
403+
}

Diff for: lib/components/app/responsive-webapp.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import { ComponentContext } from '../../util/contexts'
2222
import { getActiveItinerary, getTitle } from '../../util/state'
2323
import AfterSignInScreen from '../user/after-signin-screen'
2424
import BeforeSignInScreen from '../user/before-signin-screen'
25-
import SavedTripList from '../user/saved-trip-list'
26-
import SavedTripScreen from '../user/saved-trip-screen'
25+
import SavedTripList from '../user/monitored-trip/saved-trip-list'
26+
import SavedTripScreen from '../user/monitored-trip/saved-trip-screen'
2727
import UserAccountScreen from '../user/user-account-screen'
2828
import withLoggedInUserSupport from '../user/with-logged-in-user-support'
2929

Diff for: lib/components/user/saved-trip-editor.js renamed to lib/components/user/monitored-trip/saved-trip-editor.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react'
22

3-
import StackedPaneDisplay from './stacked-pane-display'
3+
import StackedPaneDisplay from '../stacked-pane-display'
44

55
/**
66
* This component handles editing of an existing trip.

Diff for: lib/components/user/saved-trip-list.js renamed to lib/components/user/monitored-trip/saved-trip-list.js

+21-25
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import { withAuthenticationRequired } from '@auth0/auth0-react'
2-
import clone from 'clone'
32
import React, { Component } from 'react'
43
import { Button, ButtonGroup, Glyphicon, Panel } from 'react-bootstrap'
54
import { connect } from 'react-redux'
65

7-
import * as uiActions from '../../actions/ui'
8-
import * as userActions from '../../actions/user'
9-
import DesktopNav from '../app/desktop-nav'
10-
import { RETURN_TO_CURRENT_ROUTE } from '../../util/ui'
11-
import AwaitingScreen from './awaiting-screen'
12-
import LinkButton from './link-button'
6+
import * as uiActions from '../../../actions/ui'
7+
import * as userActions from '../../../actions/user'
8+
import DesktopNav from '../../app/desktop-nav'
9+
import { RETURN_TO_CURRENT_ROUTE } from '../../../util/ui'
10+
import AwaitingScreen from '../awaiting-screen'
11+
import LinkButton from '../link-button'
1312
import TripSummaryPane from './trip-summary-pane'
14-
import withLoggedInUserSupport from './with-logged-in-user-support'
13+
import withLoggedInUserSupport from '../with-logged-in-user-support'
1514

1615
/**
1716
* This component displays the list of saved trips for the logged-in user.
@@ -88,24 +87,18 @@ class TripListItem extends Component {
8887
/**
8988
* Pauses or resumes the specified trip.
9089
*/
91-
_handlePauseOrResumeMonitoring = () => {
92-
const { createOrUpdateUserMonitoredTrip, trip } = this.props
93-
const newTrip = clone(trip)
94-
newTrip.isActive = !newTrip.isActive
95-
96-
// Silent update of existing trip.
97-
createOrUpdateUserMonitoredTrip(newTrip, false, true)
90+
_handleTogglePauseMonitoring = () => {
91+
const { togglePauseTrip, trip } = this.props
92+
togglePauseTrip(trip)
9893
}
9994

10095
/**
10196
* Deletes a trip from persistence.
10297
* (The operation also refetches the redux monitoredTrips for the logged-in user.)
10398
*/
104-
_handleDeleteTrip = async () => {
105-
if (confirm('Would you like to remove this trip?')) {
106-
const { deleteUserMonitoredTrip, trip } = this.props
107-
await deleteUserMonitoredTrip(trip.id)
108-
}
99+
_handleDeleteTrip = () => {
100+
const { confirmAndDeleteUserMonitoredTrip, trip } = this.props
101+
confirmAndDeleteUserMonitoredTrip(trip.id)
109102
}
110103

111104
render () {
@@ -118,7 +111,7 @@ class TripListItem extends Component {
118111
<Panel.Body>
119112
<TripSummaryPane monitoredTrip={trip} />
120113
<ButtonGroup>
121-
<Button bsSize='small' onClick={this._handlePauseOrResumeMonitoring}>
114+
<Button bsSize='small' onClick={this._handleTogglePauseMonitoring}>
122115
{trip.isActive
123116
? <><Glyphicon glyph='pause' /> Pause</>
124117
: <><Glyphicon glyph='play' /> Resume</>
@@ -141,9 +134,12 @@ class TripListItem extends Component {
141134
const itemMapStateToProps = () => ({})
142135

143136
const itemMapDispatchToProps = {
144-
createOrUpdateUserMonitoredTrip: userActions.createOrUpdateUserMonitoredTrip,
145-
deleteUserMonitoredTrip: userActions.deleteUserMonitoredTrip,
146-
routeTo: uiActions.routeTo
137+
confirmAndDeleteUserMonitoredTrip: userActions.confirmAndDeleteUserMonitoredTrip,
138+
routeTo: uiActions.routeTo,
139+
togglePauseTrip: userActions.togglePauseTrip
147140
}
148141

149-
const ConnectedTripListItem = connect(itemMapStateToProps, itemMapDispatchToProps)(TripListItem)
142+
const ConnectedTripListItem = connect(
143+
itemMapStateToProps,
144+
itemMapDispatchToProps
145+
)(TripListItem)

Diff for: lib/components/user/saved-trip-screen.js renamed to lib/components/user/monitored-trip/saved-trip-screen.js

+12-11
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ import React, { Component } from 'react'
55
import { connect } from 'react-redux'
66
import * as yup from 'yup'
77

8-
import * as uiActions from '../../actions/ui'
9-
import * as userActions from '../../actions/user'
10-
import DesktopNav from '../app/desktop-nav'
11-
import AwaitingScreen from './awaiting-screen'
8+
import * as uiActions from '../../../actions/ui'
9+
import * as userActions from '../../../actions/user'
10+
import DesktopNav from '../../app/desktop-nav'
11+
import AwaitingScreen from '../awaiting-screen'
1212
import SavedTripEditor from './saved-trip-editor'
1313
import TripBasicsPane from './trip-basics-pane'
1414
import TripNotificationsPane from './trip-notifications-pane'
1515
import TripSummaryPane from './trip-summary-pane'
16-
import { ALL_DAYS, arrayToDayFields, WEEKDAYS } from '../../util/monitored-trip'
17-
import { getActiveItineraries, getActiveSearch } from '../../util/state'
18-
import { RETURN_TO_CURRENT_ROUTE } from '../../util/ui'
19-
import withLoggedInUserSupport from './with-logged-in-user-support'
16+
import { ALL_DAYS, arrayToDayFields, WEEKDAYS } from '../../../util/monitored-trip'
17+
import { getActiveItineraries, getActiveSearch } from '../../../util/state'
18+
import { RETURN_TO_CURRENT_ROUTE } from '../../../util/ui'
19+
import withLoggedInUserSupport from '../with-logged-in-user-support'
2020

2121
// The validation schema shape for the form fields.
2222
// TODO: add fields here as they are implemented.
@@ -162,11 +162,12 @@ class SavedTripScreen extends Component {
162162
screenContents = (
163163
<Formik
164164
// Avoid validating on change as it is annoying. Validating on blur is enough.
165-
validateOnChange={false}
165+
enableReinitialize
166+
initialValues={clone(monitoredTrip)}
167+
onSubmit={this._updateMonitoredTrip}
166168
validateOnBlur
169+
validateOnChange={false}
167170
validationSchema={validationSchema}
168-
onSubmit={this._updateMonitoredTrip}
169-
initialValues={clone(monitoredTrip)}
170171
>
171172
{
172173
// Formik props provide access to the current user data state and errors

Diff for: lib/components/user/trip-basics-pane.js renamed to lib/components/user/monitored-trip/trip-basics-pane.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
import { connect } from 'react-redux'
1212
import styled from 'styled-components'
1313

14-
import * as userActions from '../../actions/user'
14+
import * as userActions from '../../../actions/user'
15+
import TripStatus from './trip-status'
1516
import TripSummary from './trip-summary'
1617

1718
// Styles.
@@ -102,6 +103,7 @@ class TripBasicsPane extends Component {
102103

103104
return (
104105
<div>
106+
<TripStatus monitoredTrip={monitoredTrip} />
105107
<ControlLabel>Selected itinerary:</ControlLabel>
106108
<TripSummary monitoredTrip={monitoredTrip} />
107109

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import moment from 'moment'
2+
import { formatDuration } from '@opentripplanner/core-utils/lib/time'
3+
4+
import baseRenderer from './base-renderer'
5+
6+
/**
7+
* Calculates various data for monitored trips that are currently active.
8+
*/
9+
export default function activeTripRenderer ({ monitoredTrip, timeFormat }) {
10+
const data = baseRenderer(monitoredTrip)
11+
12+
const tripEndMoment = moment(data.matchingItinerary.endTime)
13+
14+
// analyze whether the journey state indicates that the matching itinerary
15+
// has realtime data. It is assumed that realtime data will exist for
16+
// itineraries that have differing values of the scheduled arrival time
17+
// and matching itinerary arrival time.
18+
if (data.journeyState.hasRealtimeData) {
19+
// calculate the deviation from the scheduled arrival time (positive
20+
// value indicates delay)
21+
const arrivalDeviationSeconds = (
22+
data.matchingItinerary.endTime -
23+
data.journeyState.scheduledArrivalTimeEpochMillis
24+
) / 1000
25+
const deviationHumanDuration = formatDuration(
26+
Math.abs(arrivalDeviationSeconds)
27+
)
28+
if (Math.abs(arrivalDeviationSeconds) < data.ON_TIME_THRESHOLD_SECONDS) {
29+
// about on time
30+
data.panelBsStyle = 'success'
31+
data.headingText = 'Trip is in progress and is about on time.'
32+
} else if (arrivalDeviationSeconds > 0) {
33+
// delayed
34+
data.panelBsStyle = 'warning'
35+
data.headingText = `Trip is in progress and is delayed ${deviationHumanDuration}!`
36+
} else {
37+
// early
38+
data.panelBsStyle = 'warning'
39+
data.headingText = `Trip is in progress and is arriving ${deviationHumanDuration} earlier than expected!`
40+
}
41+
} else {
42+
data.panelBsStyle = 'info'
43+
data.headingText = 'Trip is in progress (no realtime updates available).'
44+
}
45+
46+
data.bodyText =
47+
`Trip is due to arrive at the destination at ${tripEndMoment.format(timeFormat)}.`
48+
49+
data.shouldRenderTogglePauseTripButton = true
50+
data.shouldRenderToggleSnoozeTripButton = true
51+
52+
return data
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import moment from 'moment'
2+
import { formatDuration } from '@opentripplanner/core-utils/lib/time'
3+
4+
/**
5+
* Calculate commonly-used pieces of data used to render the trip status
6+
* component. The monitoredTrip param can be undefined.
7+
*/
8+
export default function baseRenderer (monitoredTrip) {
9+
const data = {
10+
// the threshold for schedule deviation in seconds for whether an arrival or
11+
// departure time is to be considered on-time
12+
// TODO: get this from config or something
13+
ON_TIME_THRESHOLD_SECONDS: 120,
14+
// create some default display values in case another renderer doesn't
15+
// calculate these values
16+
body: 'Unknown trip state',
17+
headingText: 'Unknown trip state',
18+
lastCheckedText: 'Last checked time unknown',
19+
monitoredTrip: monitoredTrip,
20+
journeyState: monitoredTrip && monitoredTrip.journeyState,
21+
tripIsActive: monitoredTrip && monitoredTrip.isActive,
22+
tripIsSnoozed: monitoredTrip && monitoredTrip.snoozed
23+
}
24+
data.matchingItinerary =
25+
data.journeyState && data.journeyState.matchingItinerary
26+
27+
// set the last checked text if the journey state exists
28+
if (data.journeyState) {
29+
const secondsSinceLastCheck = moment().diff(
30+
moment(data.journeyState.lastCheckedEpochMillis),
31+
'seconds'
32+
)
33+
data.lastCheckedText =
34+
`Last checked: ${formatDuration(secondsSinceLastCheck)} ago`
35+
}
36+
37+
// set some alert data if the matching itinerary exists
38+
data.alerts = data.matchingItinerary && data.matchingItinerary.alerts
39+
data.hasMoreThanOneAlert = !!(data.alerts && data.alerts.length > 0)
40+
data.shouldRenderAlerts = data.hasMoreThanOneAlert
41+
42+
// set some defaults for the toggle buttons
43+
data.shouldRenderDeleteTripButton = false
44+
data.shouldRenderPlanNewTripButton = false
45+
data.shouldRenderTogglePauseTripButton = false
46+
data.shouldRenderToggleSnoozeTripButton = false
47+
48+
if (data.tripIsActive) {
49+
data.togglePauseTripButtonGlyphIcon = 'pause'
50+
data.togglePauseTripButtonText = 'Pause'
51+
} else {
52+
data.togglePauseTripButtonGlyphIcon = 'play'
53+
data.togglePauseTripButtonText = 'Resume'
54+
}
55+
56+
if (data.tripIsSnoozed) {
57+
data.toggleSnoozeTripButtonGlyphIcon = 'play'
58+
data.toggleSnoozeTripButtonText = 'Unsnooze trip analysis'
59+
} else {
60+
data.toggleSnoozeTripButtonGlyphIcon = 'pause'
61+
data.toggleSnoozeTripButtonText = 'Snooze for rest of today'
62+
}
63+
64+
return data
65+
}

0 commit comments

Comments
 (0)