From a8160f33a28a50617d0f5cd7911c900b7690c1e6 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 11 Sep 2020 11:45:48 -0400 Subject: [PATCH 001/265] feat(field-trip): add field trip module (WIP) --- example.js | 9 +- lib/actions/call-taker.js | 26 +++++ lib/components/admin/call-taker-controls.js | 110 ++++++++++++++------ lib/components/admin/field-trip-windows.js | 67 ++++++++++++ lib/index.js | 2 + lib/reducers/call-taker.js | 30 +++++- 6 files changed, 208 insertions(+), 36 deletions(-) create mode 100644 lib/components/admin/field-trip-windows.js diff --git a/example.js b/example.js index 59a3f0dc8..fb6693761 100644 --- a/example.js +++ b/example.js @@ -19,6 +19,7 @@ import { CallTakerControls, CallTakerPanel, CallTakerWindows, + FieldTripWindows, DefaultMainPanel, DesktopNav, BatchRoutingPanel, @@ -96,7 +97,13 @@ class OtpRRExample extends Component { {otpConfig.datastoreUrl ? : null} - {otpConfig.datastoreUrl ? : null} + {otpConfig.datastoreUrl + ? <> + + + + : null + } diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index 25ccb2ae7..96d2b1241 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -17,10 +17,14 @@ const requestingCalls = createAction('REQUESTING_CALLS') const requestingQueries = createAction('REQUESTING_QUERIES') const storeSession = createAction('STORE_SESSION') +const receivedFieldTrips = createAction('RECEIVED_FIELD_TRIPS') +const requestingFieldTrips = createAction('REQUESTING_FIELD_TRIPS') + /// PUBLIC ACTIONS export const beginCall = createAction('BEGIN_CALL') export const toggleCallHistory = createAction('TOGGLE_CALL_HISTORY') +export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS') /** * End the active call and store the queries made during the call. @@ -133,6 +137,28 @@ export function fetchCalls () { } } +/** + * Fetch latest calls for a particular session. + */ +export function fetchFieldTrips () { + return function (dispatch, getState) { + dispatch(requestingFieldTrips()) + const {callTaker, otp} = getState() + if (sessionIsInvalid(callTaker.session)) return + const {datastoreUrl} = otp.config + const {sessionId} = callTaker.session + fetch(`${datastoreUrl}/fieldtrip/getRequestsSummary?${qs.stringify({sessionId})}`) + .then(res => res.json()) + .then(fieldTrips => { + console.log('GET field trips response', fieldTrips) + dispatch(receivedFieldTrips({fieldTrips})) + }) + .catch(err => { + alert(`Could not fetch field trips: ${JSON.stringify(err)}`) + }) + } +} + /** * @return {boolean} - whether a calltaker session is invalid */ diff --git a/lib/components/admin/call-taker-controls.js b/lib/components/admin/call-taker-controls.js index d5b8f1833..244dce7b8 100644 --- a/lib/components/admin/call-taker-controls.js +++ b/lib/components/admin/call-taker-controls.js @@ -10,6 +10,7 @@ import Icon from '../narrative/icon' const RED = '#C35134' const BLUE = '#1C4D89' const GREEN = '#6B931B' +const PURPLE = '#8134D3' /** * This component displays the controls for the Call Taker/Field Trip modules, @@ -21,10 +22,17 @@ const GREEN = '#6B931B' */ class CallTakerControls extends Component { componentWillReceiveProps (nextProps) { - const {session} = nextProps + const { + callTakerEnabled, + fetchCalls, + fetchFieldTrips, + fieldTripEnabled, + session + } = nextProps // Once session is available, fetch calls. if (session && !this.props.session) { - this.props.fetchCalls() + if (callTakerEnabled) fetchCalls() + if (fieldTripEnabled) fetchFieldTrips() } } @@ -57,10 +65,12 @@ class CallTakerControls extends Component { _onToggleCallHistory = () => this.props.toggleCallHistory() + _onToggleFieldTrips = () => this.props.toggleFieldTrips() + _callInProgress = () => Boolean(this.props.activeCall) render () { - const {session} = this.props + const {callTakerEnabled, fieldTripEnabled, session} = this.props // If no valid session is found, do not show calltaker controls. if (!session) return null // FIXME: styled component @@ -75,21 +85,23 @@ class CallTakerControls extends Component { return ( <> {/* Start/End Call button */} - + }} + className='call-taker-button' + onClick={this._onClickCall} + > + {this._renderCallButton()} + + } {this._callInProgress() ? - - + {callTakerEnabled && + + } {/* Field Trip toggle button TODO */} + {fieldTripEnabled && + + } ) } @@ -132,19 +163,30 @@ class CallTakerControls extends Component { const mapStateToProps = (state, ownProps) => { return { activeCall: state.callTaker.activeCall, + callTakerEnabled: Boolean(state.otp.config.modules.find(m => m.id === 'call')), + fieldTripEnabled: Boolean(state.otp.config.modules.find(m => m.id === 'ft')), session: state.callTaker.session } } -const {beginCall, endCall, fetchCalls, toggleCallHistory} = callTakerActions +const { + beginCall, + endCall, + fetchCalls, + fetchFieldTrips, + toggleCallHistory, + toggleFieldTrips +} = callTakerActions const mapDispatchToProps = { beginCall, endCall, fetchCalls, + fetchFieldTrips, routingQuery, setMainPanelContent, - toggleCallHistory + toggleCallHistory, + toggleFieldTrips } export default connect(mapStateToProps, mapDispatchToProps)(CallTakerControls) diff --git a/lib/components/admin/field-trip-windows.js b/lib/components/admin/field-trip-windows.js new file mode 100644 index 000000000..1fca110c1 --- /dev/null +++ b/lib/components/admin/field-trip-windows.js @@ -0,0 +1,67 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import * as callTakerActions from '../../actions/call-taker' +import CallRecord from './call-record' +import DraggableWindow from './draggable-window' +import Icon from '../narrative/icon' + +/** + * Collects the various draggable windows used in the Call Taker module to + * display, for example, the call record list and (TODO) the list of field trips. + */ +class FieldTripWindows extends Component { + render () { + const {callTaker, fetchQueries, searches} = this.props + const {activeCall, fieldTrip} = callTaker + console.log(fieldTrip) + return ( + <> + {fieldTrip.visible + // Active call window + ? Field Trip Requests} + onClickClose={this.props.toggleCallHistory} + > + {activeCall + ? + : null + } + {fieldTrip.requests.data.length > 0 + ? fieldTrip.requests.data.map((request, i) => ( +
+ {request.startLocation} to {request.endLocation} +
+ )) + :
No calls in history
+ } +
+ : null + } + + ) + } +} + +const mapStateToProps = (state, ownProps) => { + return { + callTaker: state.callTaker, + currentQuery: state.otp.currentQuery, + searches: state.otp.searches + } +} + +const { + fetchQueries, + toggleCallHistory +} = callTakerActions + +const mapDispatchToProps = { fetchQueries, toggleCallHistory } + +export default connect(mapStateToProps, mapDispatchToProps)(FieldTripWindows) diff --git a/lib/index.js b/lib/index.js index c23962411..0007a8669 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,6 @@ import CallTakerControls from './components/admin/call-taker-controls' import CallTakerWindows from './components/admin/call-taker-windows' +import FieldTripWindows from './components/admin/field-trip-windows' import DateTimeModal from './components/form/date-time-modal' import DateTimePreview from './components/form/date-time-preview' @@ -65,6 +66,7 @@ export { DateTimePreview, DefaultSearchForm, ErrorMessage, + FieldTripWindows, LocationField, PlanTripButton, SettingsPreview, diff --git a/lib/reducers/call-taker.js b/lib/reducers/call-taker.js index 57f5e885c..92cdfce61 100644 --- a/lib/reducers/call-taker.js +++ b/lib/reducers/call-taker.js @@ -7,6 +7,7 @@ import {getTimestamp} from '../util/state' const { randId } = coreUtils.storage const UPPER_RIGHT_CORNER = {x: 604, y: 53} +const LOWER_RIGHT_CORNER = {x: 604, y: 400} const FETCH_STATUS = { UNFETCHED: 0, @@ -26,7 +27,14 @@ function createCallTakerReducer () { data: [] } }, - fieldTrips: [], + fieldTrip: { + position: LOWER_RIGHT_CORNER, + requests: { + status: FETCH_STATUS.UNFETCHED, + data: [] + }, + visible: false, + }, session: null } return (state = initialState, action) => { @@ -58,6 +66,21 @@ function createCallTakerReducer () { callHistory: { calls: { $set: calls } } }) } + case 'REQUESTING_FIELD_TRIPS': { + return update(state, { + fieldTrip: { requests: { status: { $set: FETCH_STATUS.FETCHING } } } + }) + } + case 'RECEIVED_FIELD_TRIPS': { + const data = action.payload.fieldTrips + const requests = { + status: FETCH_STATUS.FETCHED, + data: data.sort((a, b) => moment(b.endTime) - moment(a.endTime)) + } + return update(state, { + fieldTrip: { requests: { $set: requests } } + }) + } case 'RECEIVED_QUERIES': { const {callId, queries} = action.payload const {data} = state.callHistory.calls @@ -95,6 +118,11 @@ function createCallTakerReducer () { callHistory: { visible: { $set: !state.callHistory.visible } } }) } + case 'TOGGLE_FIELD_TRIPS': { + return update(state, { + fieldTrip: { visible: { $set: !state.fieldTrip.visible } } + }) + } case 'END_CALL': { return update(state, { activeCall: { $set: null } From 55fb0713142c9c9a7b9604252da38f9e33de961d Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 11 Sep 2020 13:30:42 -0400 Subject: [PATCH 002/265] refactor(field-trip): add search/filtering for field trip requests --- lib/actions/call-taker.js | 1 + lib/components/admin/call-taker-windows.js | 14 ++-- lib/components/admin/draggable-window.js | 5 +- lib/components/admin/field-trip-windows.js | 95 ++++++++++++++++++---- lib/reducers/call-taker.js | 13 ++- 5 files changed, 101 insertions(+), 27 deletions(-) diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index 96d2b1241..df15ec0a7 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -23,6 +23,7 @@ const requestingFieldTrips = createAction('REQUESTING_FIELD_TRIPS') /// PUBLIC ACTIONS export const beginCall = createAction('BEGIN_CALL') +export const setFieldTripFilter = createAction('SET_FIELD_TRIP_FILTER') export const toggleCallHistory = createAction('TOGGLE_CALL_HISTORY') export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS') diff --git a/lib/components/admin/call-taker-windows.js b/lib/components/admin/call-taker-windows.js index b575cc2ca..7b1073373 100644 --- a/lib/components/admin/call-taker-windows.js +++ b/lib/components/admin/call-taker-windows.js @@ -12,7 +12,7 @@ import Icon from '../narrative/icon' */ class CallTakerWindows extends Component { render () { - const {callTaker, fetchQueries, searches} = this.props + const {callTaker, fetchQueries, searches, toggleCallHistory} = this.props const {activeCall, callHistory} = callTaker return ( <> @@ -23,7 +23,7 @@ class CallTakerWindows extends Component { defaultPosition: callHistory.position }} header={ Call history} - onClickClose={this.props.toggleCallHistory} + onClickClose={toggleCallHistory} > {activeCall ? { } } -const { - fetchQueries, - toggleCallHistory -} = callTakerActions - -const mapDispatchToProps = { fetchQueries, toggleCallHistory } +const mapDispatchToProps = { + fetchQueries: callTakerActions.fetchQueries, + toggleCallHistory: callTakerActions.toggleCallHistory +} export default connect(mapStateToProps, mapDispatchToProps)(CallTakerWindows) diff --git a/lib/components/admin/draggable-window.js b/lib/components/admin/draggable-window.js index 1159cdd39..34d12d569 100644 --- a/lib/components/admin/draggable-window.js +++ b/lib/components/admin/draggable-window.js @@ -7,7 +7,7 @@ const noop = () => {} export default class DraggableWindow extends Component { render () { - const {children, draggableProps, header} = this.props + const {children, draggableProps, header, style} = this.props const GREY_BORDER = '#777 1.3px solid' return (
req.status !== 'cancelled' && (!req.inboundTripStatus || !req.outboundTripStatus)}, + {id: 'planned', label: 'Planned', filter: (req) => req.status !== 'cancelled' && req.inboundTripStatus && req.outboundTripStatus}, + {id: 'cancelled', label: 'Cancelled', filter: (req) => req.status === 'cancelled'}, + {id: 'past', label: 'Past', filter: (req) => req.travelDate && moment(req.travelDate).diff(moment(), 'days') < 0}, + {id: 'all', label: 'All', filter: (req) => true} +] + +const SEARCH_FIELDS = [ + 'address', + 'ccLastFour', + 'ccName', + 'ccType', + 'checkNumber', + 'city', + 'classpassId', + 'emailAddress', + 'endLocation', + 'grade', + 'phoneNumber', + 'schoolName', + 'startLocation', + 'submitterNotes', + 'teacherName' +] + /** * Collects the various draggable windows used in the Call Taker module to * display, for example, the call record list and (TODO) the list of field trips. */ class FieldTripWindows extends Component { + _onSearchChange = e => { + this.props.setFieldTripFilter({search: e.target.value}) + } + + _onTabChange = e => { + this.props.setFieldTripFilter({tab: e.target.name}) + } + render () { - const {callTaker, fetchQueries, searches} = this.props - const {activeCall, fieldTrip} = callTaker - console.log(fieldTrip) + const {callTaker, searches, toggleFieldTrips} = this.props + const {activeFieldTrip, fieldTrip} = callTaker + const {filter} = fieldTrip + const activeTab = TABS.find(tab => tab.id === filter.tab) + const visibleRequests = fieldTrip.requests.data + .filter(ft => { + if (activeTab) return activeTab.filter(ft) + else return true + }) + .filter(ft => !filter.search || SEARCH_FIELDS.some(key => { + const value = ft[key] + if (value) return value.toLowerCase().indexOf(filter.search.toLowerCase()) !== -1 + else return false + })) return ( <> {fieldTrip.visible @@ -24,22 +70,42 @@ class FieldTripWindows extends Component { defaultPosition: fieldTrip.position }} header={ Field Trip Requests} - onClickClose={this.props.toggleCallHistory} + onClickClose={toggleFieldTrips} + style={{width: '450px'}} > - {activeCall + + {TABS.map(tab => { + const active = tab.id === filter.tab + const requestCount = fieldTrip.requests.data.filter(tab.filter).length + return ( + + ) + })} + {activeFieldTrip ? : null } - {fieldTrip.requests.data.length > 0 - ? fieldTrip.requests.data.map((request, i) => ( + {visibleRequests.length > 0 + ? visibleRequests.map((request, i) => (
{request.startLocation} to {request.endLocation}
)) - :
No calls in history
+ :
No field trips found.
} : null @@ -57,11 +123,10 @@ const mapStateToProps = (state, ownProps) => { } } -const { - fetchQueries, - toggleCallHistory -} = callTakerActions - -const mapDispatchToProps = { fetchQueries, toggleCallHistory } +const mapDispatchToProps = { + fetchQueries: callTakerActions.fetchQueries, + setFieldTripFilter: callTakerActions.setFieldTripFilter, + toggleFieldTrips: callTakerActions.toggleFieldTrips +} export default connect(mapStateToProps, mapDispatchToProps)(FieldTripWindows) diff --git a/lib/reducers/call-taker.js b/lib/reducers/call-taker.js index 92cdfce61..5a5120027 100644 --- a/lib/reducers/call-taker.js +++ b/lib/reducers/call-taker.js @@ -7,7 +7,7 @@ import {getTimestamp} from '../util/state' const { randId } = coreUtils.storage const UPPER_RIGHT_CORNER = {x: 604, y: 53} -const LOWER_RIGHT_CORNER = {x: 604, y: 400} +const LOWER_RIGHT_CORNER = {x: 504, y: 400} const FETCH_STATUS = { UNFETCHED: 0, @@ -19,6 +19,7 @@ const FETCH_STATUS = { function createCallTakerReducer () { const initialState = { activeCall: null, + activeFieldTrip: null, callHistory: { position: UPPER_RIGHT_CORNER, visible: false, @@ -28,12 +29,15 @@ function createCallTakerReducer () { } }, fieldTrip: { + filter: { + tab: 'new' + }, position: LOWER_RIGHT_CORNER, requests: { status: FETCH_STATUS.UNFETCHED, data: [] }, - visible: false, + visible: false }, session: null } @@ -81,6 +85,11 @@ function createCallTakerReducer () { fieldTrip: { requests: { $set: requests } } }) } + case 'SET_FIELD_TRIP_FILTER': { + return update(state, { + fieldTrip: { filter: { $merge: action.payload } } + }) + } case 'RECEIVED_QUERIES': { const {callId, queries} = action.payload const {data} = state.callHistory.calls From 231f372e5c9957a48c59570e24357fcc52099f81 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 11 Sep 2020 16:58:13 -0400 Subject: [PATCH 003/265] refactor(field-trip): add field trip details component --- lib/actions/call-taker.js | 25 ++++ lib/components/admin/call-taker-windows.js | 2 +- lib/components/admin/draggable-window.js | 1 - lib/components/admin/field-trip-details.js | 62 ++++++++ lib/components/admin/field-trip-windows.js | 161 ++++++++++++++++----- lib/reducers/call-taker.js | 14 +- 6 files changed, 223 insertions(+), 42 deletions(-) create mode 100644 lib/components/admin/field-trip-details.js diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index df15ec0a7..f9e0e6820 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -19,11 +19,14 @@ const storeSession = createAction('STORE_SESSION') const receivedFieldTrips = createAction('RECEIVED_FIELD_TRIPS') const requestingFieldTrips = createAction('REQUESTING_FIELD_TRIPS') +const receivedFieldTripDetails = createAction('RECEIVED_FIELD_TRIP_DETAILS') +const requestingFieldTripDetails = createAction('REQUESTING_FIELD_TRIP_DETAILS') /// PUBLIC ACTIONS export const beginCall = createAction('BEGIN_CALL') export const setFieldTripFilter = createAction('SET_FIELD_TRIP_FILTER') +export const setActiveFieldTrip = createAction('SET_ACTIVE_FIELD_TRIP') export const toggleCallHistory = createAction('TOGGLE_CALL_HISTORY') export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS') @@ -160,6 +163,28 @@ export function fetchFieldTrips () { } } +/** + * Fetch latest calls for a particular session. + */ +export function fetchFieldTripDetails (requestId) { + return function (dispatch, getState) { + dispatch(requestingFieldTripDetails()) + const {callTaker, otp} = getState() + if (sessionIsInvalid(callTaker.session)) return + const {datastoreUrl} = otp.config + const {sessionId} = callTaker.session + fetch(`${datastoreUrl}/fieldtrip/getRequest?${qs.stringify({requestId, sessionId})}`) + .then(res => res.json()) + .then(fieldTrip => { + console.log('GET field trip details response', fieldTrip) + dispatch(receivedFieldTripDetails({fieldTrip})) + }) + .catch(err => { + alert(`Could not fetch field trips: ${JSON.stringify(err)}`) + }) + } +} + /** * @return {boolean} - whether a calltaker session is invalid */ diff --git a/lib/components/admin/call-taker-windows.js b/lib/components/admin/call-taker-windows.js index 7b1073373..35a2399d4 100644 --- a/lib/components/admin/call-taker-windows.js +++ b/lib/components/admin/call-taker-windows.js @@ -22,7 +22,7 @@ class CallTakerWindows extends Component { draggableProps={{ defaultPosition: callHistory.position }} - header={ Call history} + header={

Call history

} onClickClose={toggleCallHistory} > {activeCall diff --git a/lib/components/admin/draggable-window.js b/lib/components/admin/draggable-window.js index 34d12d569..6705b158f 100644 --- a/lib/components/admin/draggable-window.js +++ b/lib/components/admin/draggable-window.js @@ -39,7 +39,6 @@ export default class DraggableWindow extends Component { style={{ borderBottom: GREY_BORDER, cursor: 'move', - fontSize: 'large', paddingBottom: '5px' }} > diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js new file mode 100644 index 000000000..6397cc2f2 --- /dev/null +++ b/lib/components/admin/field-trip-details.js @@ -0,0 +1,62 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import * as callTakerActions from '../../actions/call-taker' +import DraggableWindow from './draggable-window' +import Icon from '../narrative/icon' + +/** + * Shows the details for the active Field Trip Request. + */ +class FieldTripDetails extends Component { + _onCloseActiveFieldTrip = () => { + this.props.setActiveFieldTrip(null) + } + + render () { + const {callTaker, request} = this.props + if (!request) return null + const {id, schoolName} = request + const {fieldTrip} = callTaker + const defaultPosition = {...fieldTrip.position} + defaultPosition.x = defaultPosition.x - 460 + return ( + + {schoolName} Trip (#{id}) + + } + onClickClose={this._onCloseActiveFieldTrip} + style={{width: '450px'}} + > + {Object.keys(request).map(key => { + const value = typeof request[key] === 'object' + ? 'TODO: object' + : request[key] + return (
{key}: {value}
) + })} +
+ ) + } +} + +const mapStateToProps = (state, ownProps) => { + const {activeId, requests} = state.callTaker.fieldTrip + const request = requests.data.find(req => req.id === activeId) + return { + callTaker: state.callTaker, + currentQuery: state.otp.currentQuery, + request + } +} + +const mapDispatchToProps = { + fetchQueries: callTakerActions.fetchQueries, + setActiveFieldTrip: callTakerActions.setActiveFieldTrip, + setFieldTripFilter: callTakerActions.setFieldTripFilter, + toggleFieldTrips: callTakerActions.toggleFieldTrips +} + +export default connect(mapStateToProps, mapDispatchToProps)(FieldTripDetails) diff --git a/lib/components/admin/field-trip-windows.js b/lib/components/admin/field-trip-windows.js index a2be65a20..9774e146a 100644 --- a/lib/components/admin/field-trip-windows.js +++ b/lib/components/admin/field-trip-windows.js @@ -1,10 +1,11 @@ import moment from 'moment' import React, { Component } from 'react' +import { Badge } from 'react-bootstrap' import { connect } from 'react-redux' import * as callTakerActions from '../../actions/call-taker' -import CallRecord from './call-record' import DraggableWindow from './draggable-window' +import FieldTripDetails from './field-trip-details' import Icon from '../narrative/icon' const TABS = [ @@ -34,22 +35,35 @@ const SEARCH_FIELDS = [ ] /** - * Collects the various draggable windows used in the Call Taker module to - * display, for example, the call record list and (TODO) the list of field trips. + * Collects the various draggable windows for the Field Trip module. */ class FieldTripWindows extends Component { + _onClickFieldTrip = (request) => { + const {callTaker, fetchFieldTripDetails, setActiveFieldTrip} = this.props + if (request.id === callTaker.fieldTrip.activeId) { + this._onCloseActiveFieldTrip() + } else { + setActiveFieldTrip(request.id) + fetchFieldTripDetails(request.id) + } + } + + _onCloseActiveFieldTrip = () => { + this.props.setActiveFieldTrip(null) + } + _onSearchChange = e => { this.props.setFieldTripFilter({search: e.target.value}) } _onTabChange = e => { - this.props.setFieldTripFilter({tab: e.target.name}) + this.props.setFieldTripFilter({tab: e.currentTarget.name}) } render () { - const {callTaker, searches, toggleFieldTrips} = this.props - const {activeFieldTrip, fieldTrip} = callTaker - const {filter} = fieldTrip + const {callTaker, toggleFieldTrips} = this.props + const {fieldTrip} = callTaker + const {activeId, filter} = fieldTrip const activeTab = TABS.find(tab => tab.id === filter.tab) const visibleRequests = fieldTrip.requests.data .filter(ft => { @@ -64,57 +78,125 @@ class FieldTripWindows extends Component { return ( <> {fieldTrip.visible - // Active call window ? Field Trip Requests} + header={ + <> +

+ Field Trip Requests{' '} + +

+ {TABS.map(tab => { + const active = tab.id === filter.tab + const style = { + borderRadius: 5, + backgroundColor: active ? 'navy' : undefined, + color: active ? 'white' : undefined, + padding: '2px 3px' + } + const requestCount = fieldTrip.requests.data.filter(tab.filter).length + return ( + + ) + })} + + } onClickClose={toggleFieldTrips} style={{width: '450px'}} > - - {TABS.map(tab => { - const active = tab.id === filter.tab - const requestCount = fieldTrip.requests.data.filter(tab.filter).length - return ( - - ) - })} - {activeFieldTrip - ? - : null - } {visibleRequests.length > 0 ? visibleRequests.map((request, i) => ( -
- {request.startLocation} to {request.endLocation} -
+ )) :
No field trips found.
}
: null } + ) } } +class FieldTripRequestRecord extends Component { + _onClick = () => { + const {onClick, request} = this.props + onClick(request) + } + + _getStatusIcon = (status) => status + ? + : + + render () { + const {active, request} = this.props + const style = { + backgroundColor: active ? 'lightgrey' : undefined, + borderBottom: '1px solid grey' + } + const { + endLocation, + id, + inboundTripStatus, + outboundTripStatus, + schoolName, + startLocation, + teacherName, + timeStamp + } = request + return ( +
  • + +
  • + ) + } +} + const mapStateToProps = (state, ownProps) => { return { callTaker: state.callTaker, @@ -124,7 +206,8 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - fetchQueries: callTakerActions.fetchQueries, + fetchFieldTripDetails: callTakerActions.fetchFieldTripDetails, + setActiveFieldTrip: callTakerActions.setActiveFieldTrip, setFieldTripFilter: callTakerActions.setFieldTripFilter, toggleFieldTrips: callTakerActions.toggleFieldTrips } diff --git a/lib/reducers/call-taker.js b/lib/reducers/call-taker.js index 5a5120027..c37728b86 100644 --- a/lib/reducers/call-taker.js +++ b/lib/reducers/call-taker.js @@ -19,7 +19,6 @@ const FETCH_STATUS = { function createCallTakerReducer () { const initialState = { activeCall: null, - activeFieldTrip: null, callHistory: { position: UPPER_RIGHT_CORNER, visible: false, @@ -29,6 +28,7 @@ function createCallTakerReducer () { } }, fieldTrip: { + activeId: null, filter: { tab: 'new' }, @@ -90,6 +90,18 @@ function createCallTakerReducer () { fieldTrip: { filter: { $merge: action.payload } } }) } + case 'SET_ACTIVE_FIELD_TRIP': { + return update(state, { + fieldTrip: { activeId: { $set: action.payload } } + }) + } + case 'RECEIVED_FIELD_TRIP_DETAILS': { + const {fieldTrip} = action.payload + const index = state.fieldTrip.requests.data.findIndex(req => req.id === fieldTrip.id) + return update(state, { + fieldTrip: { requests: { data: { [index]: { $set: fieldTrip } } } } + }) + } case 'RECEIVED_QUERIES': { const {callId, queries} = action.payload const {data} = state.callHistory.calls From 3e25117dc9215c87416860e32596a643f29c4521 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Thu, 22 Oct 2020 13:55:07 -0400 Subject: [PATCH 004/265] refactor(calltaker): increase limit for fetch calls --- lib/actions/call-taker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index f9e0e6820..5b7cd6d44 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -128,7 +128,7 @@ export function fetchCalls () { if (sessionIsInvalid(callTaker.session)) return const {datastoreUrl} = otp.config const {sessionId} = callTaker.session - const limit = 10 + const limit = 30 fetch(`${datastoreUrl}/calltaker/call?${qs.stringify({limit, sessionId})}`) .then(res => res.json()) .then(calls => { From 55c5f5b100cd425903c4369a37a47dbc306ff9f8 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 10 Sep 2020 18:31:38 -0400 Subject: [PATCH 005/265] feat(FavoriteLocationsPane): Add locations using LocationField. --- .../user/favorite-locations-pane.js | 110 ++++++++++-------- 1 file changed, 60 insertions(+), 50 deletions(-) diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations-pane.js index 0bced0b30..10541015e 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations-pane.js @@ -1,5 +1,7 @@ import { Field, FieldArray } from 'formik' import memoize from 'lodash.memoize' +import LocationField from '@opentripplanner/location-field' +import { connect } from 'react-redux' import React, { Component } from 'react' import { Button, @@ -11,6 +13,8 @@ import { import FontAwesome from 'react-fontawesome' import styled from 'styled-components' +import { getShowUserSettings } from '../../util/state' + // Styles. const fancyAddLocationCss = ` background-color: #337ab7; @@ -22,68 +26,40 @@ const StyledAddon = styled(InputGroup.Addon)` const NewLocationAddon = styled(StyledAddon)` ${fancyAddLocationCss} ` -const NewLocationFormControl = styled(FormControl)` - ${fancyAddLocationCss} - ::placeholder { - color: #fff; - } - &:focus { - background-color: unset; - color: unset; - ::placeholder { - color: unset; - } - } -` // Helper filter functions. export const isHome = loc => loc.type === 'home' export const isWork = loc => loc.type === 'work' -/** - * Helper function that adds a new address to the Formik state - * using the Formik-provided arrayHelpers object. - */ -function addNewAddress (arrayHelpers, e) { - const value = (e.target.value || '').trim() - if (value.length > 0) { - arrayHelpers.push({ - address: value, - icon: 'map-marker', - type: 'custom' - }) - - // Empty the input box value so the user can enter their next location. - e.target.value = '' - } -} - /** * User's saved locations editor. * TODO: Discuss and improve handling of location details (type, coordinates...). */ class FavoriteLocationsPane extends Component { - _handleNewAddressKeyDown = memoize( - arrayHelpers => e => { - if (e.keyCode === 13) { - // On the user pressing enter (keyCode 13) on the new location input, - // add new address to user's savedLocations... - addNewAddress(arrayHelpers, e) - - // ... but don't submit the form. - e.preventDefault() - } - } - ) - - _handleNewAddressBlur = memoize( - arrayHelpers => e => { - addNewAddress(arrayHelpers, e) + _handleAddNewLocation = memoize( + arrayHelpers => ({ location }) => { + arrayHelpers.push({ + address: location.name, + icon: 'map-marker', + lat: location.lat, + lon: location.lon, + name: 'My place', + type: 'custom' + }) } ) render () { - const { values: userData } = this.props + const { + currentPosition, + geocoderConfig, + nearbyStops, + sessionSearches, + showUserSettings, + stopsIndex, + values: userData + } = this.props + const { savedLocations } = userData const homeLocation = savedLocations.find(isHome) const workLocation = savedLocations.find(isWork) @@ -126,18 +102,34 @@ class FavoriteLocationsPane extends Component { ) })} - {/* For adding a new location. */} + {/* For adding a location. */} + {/* + */} + + @@ -148,4 +140,22 @@ class FavoriteLocationsPane extends Component { } } -export default FavoriteLocationsPane +// connect to redux store +// Get a subset of props that ConnectedLocationField would. +const mapStateToProps = (state, ownProps) => { + const { config, transitIndex } = state.otp + const { currentPosition, nearbyStops, sessionSearches } = location + return { + currentPosition, + geocoderConfig: config.geocoder, + nearbyStops, + sessionSearches, + showUserSettings: getShowUserSettings(state.otp), + stopsIndex: transitIndex.stops + } +} + +const mapDispatchToProps = { +} + +export default connect(mapStateToProps, mapDispatchToProps)(FavoriteLocationsPane) From d13a712bf479416d5586b2d4cee8e0b561a35e87 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 16 Sep 2020 14:33:31 -0400 Subject: [PATCH 006/265] refactor(FavoriteLocationsPane): Expand use of LocationField. --- lib/components/user/favorite-locations-pane.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations-pane.js index 10541015e..35a99d5e2 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations-pane.js @@ -68,6 +68,7 @@ class FavoriteLocationsPane extends Component {
    Add the places you frequent often to save time planning trips: + ( From ca1d2793d47f18b96499d955c20a6e30e42b7ba8 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 1 Dec 2020 20:25:26 -0500 Subject: [PATCH 007/265] refactor(FavoriteLocationsPane): Improve LocationField styling. --- .../user/favorite-locations-pane.js | 71 +++++++++++++------ 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations-pane.js index 35a99d5e2..9861b65c2 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations-pane.js @@ -1,14 +1,22 @@ import { Field, FieldArray } from 'formik' import memoize from 'lodash.memoize' import LocationField from '@opentripplanner/location-field' +import { + DropdownContainer, + FormGroup, + Input, + InputGroup, + InputGroupAddon, + MenuItemA +} from '@opentripplanner/location-field/lib/styled' import { connect } from 'react-redux' import React, { Component } from 'react' import { Button, ControlLabel, FormControl, - FormGroup, - InputGroup + FormGroup as BsFormGroup, + InputGroup as BsInputGroup } from 'react-bootstrap' import FontAwesome from 'react-fontawesome' import styled from 'styled-components' @@ -20,13 +28,39 @@ const fancyAddLocationCss = ` background-color: #337ab7; color: #fff; ` -const StyledAddon = styled(InputGroup.Addon)` +const StyledAddon = styled(BsInputGroup.Addon)` min-width: 40px; ` const NewLocationAddon = styled(StyledAddon)` ${fancyAddLocationCss} ` +const StyledLocationField = styled(LocationField)` + margin-bottom: 0; + width: 100%; + + ${DropdownContainer} { + width: 0; + & > button { + display: none; + } + } + + ${InputGroup} { + border: none; + display: block; + } + ${Input} { + border: 1px solid #ccc; + border-bottom-right-radius 4px; + border-top-right-radius 4px; + font-size: 100%; + line-height: 20px; + padding: 6px 12px; + width: 100%; + } +` + // Helper filter functions. export const isHome = loc => loc.type === 'home' export const isWork = loc => loc.type === 'work' @@ -68,7 +102,6 @@ class FavoriteLocationsPane extends Component {
    Add the places you frequent often to save time planning trips: - ( @@ -76,8 +109,8 @@ class FavoriteLocationsPane extends Component { {savedLocations.map((loc, index) => { const isHomeOrWork = loc === homeLocation || loc === workLocation return ( - - + + @@ -87,7 +120,7 @@ class FavoriteLocationsPane extends Component { // onBlur, onChange, and value are passed automatically. /> {!isHomeOrWork && ( - + - + )} - - + + ) })} {/* For adding a location. */} - - + + - {/* - - */} - - - + + )} /> From 34f99bd4d614fe9242c57e144c61027e2ad09158 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 2 Dec 2020 12:57:41 -0500 Subject: [PATCH 008/265] refactor(FavoriteLocationsPane): Extract FavoriteLocation component. --- .../user/favorite-locations-pane.js | 104 +++++++++++------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations-pane.js index 9861b65c2..f8deb4d00 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations-pane.js @@ -3,11 +3,8 @@ import memoize from 'lodash.memoize' import LocationField from '@opentripplanner/location-field' import { DropdownContainer, - FormGroup, - Input, - InputGroup, - InputGroupAddon, - MenuItemA + Input as LocationFieldInput, + InputGroup as LocationFieldInputGroup } from '@opentripplanner/location-field/lib/styled' import { connect } from 'react-redux' import React, { Component } from 'react' @@ -15,8 +12,8 @@ import { Button, ControlLabel, FormControl, - FormGroup as BsFormGroup, - InputGroup as BsInputGroup + FormGroup, + InputGroup } from 'react-bootstrap' import FontAwesome from 'react-fontawesome' import styled from 'styled-components' @@ -28,7 +25,7 @@ const fancyAddLocationCss = ` background-color: #337ab7; color: #fff; ` -const StyledAddon = styled(BsInputGroup.Addon)` +const StyledAddon = styled(InputGroup.Addon)` min-width: 40px; ` const NewLocationAddon = styled(StyledAddon)` @@ -46,11 +43,11 @@ const StyledLocationField = styled(LocationField)` } } - ${InputGroup} { + ${LocationFieldInputGroup} { border: none; display: block; } - ${Input} { + ${LocationFieldInput} { border: 1px solid #ccc; border-bottom-right-radius 4px; border-top-right-radius 4px; @@ -70,8 +67,19 @@ export const isWork = loc => loc.type === 'work' * TODO: Discuss and improve handling of location details (type, coordinates...). */ class FavoriteLocationsPane extends Component { + constructor () { + super() + this.state = { + location: null + } + } + _handleAddNewLocation = memoize( arrayHelpers => ({ location }) => { + // Set, then unset the location state to reset the LocationField after address is entered. + // (both this.setState calls should trigger componentDidUpdate on LocationField). + this.setState({ location }) + arrayHelpers.push({ address: location.name, icon: 'map-marker', @@ -80,6 +88,8 @@ class FavoriteLocationsPane extends Component { name: 'My place', type: 'custom' }) + + this.setState({ location: null }) } ) @@ -93,6 +103,7 @@ class FavoriteLocationsPane extends Component { stopsIndex, values: userData } = this.props + const { location } = this.state const { savedLocations } = userData const homeLocation = savedLocations.find(isHome) @@ -109,41 +120,26 @@ class FavoriteLocationsPane extends Component { {savedLocations.map((loc, index) => { const isHomeOrWork = loc === homeLocation || loc === workLocation return ( - - - - - - - {!isHomeOrWork && ( - - - - )} - - + arrayHelpers.remove(index)} + /> ) })} {/* For adding a location. */} - - + + - - + + )} /> @@ -184,4 +180,36 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { } +/** + * Renders a user favorite location, + * and lets the user edit the details (name, address, type) of the location. + */ +const FavoriteLocation = ({ index, isFixed, location, onDelete }) => { + return ( + + + + + + + {!isFixed && ( + + + + )} + + + ) +} + export default connect(mapStateToProps, mapDispatchToProps)(FavoriteLocationsPane) From a8f031db4b33892763a0ccfde66e27ae1564a3d6 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 2 Dec 2020 15:02:44 -0500 Subject: [PATCH 009/265] refactor(FavoriteLocation): Convert component into full class. --- .../user/favorite-locations-pane.js | 121 +++++++++--------- 1 file changed, 63 insertions(+), 58 deletions(-) diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations-pane.js index f8deb4d00..fe7de01d7 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations-pane.js @@ -1,4 +1,4 @@ -import { Field, FieldArray } from 'formik' +import { FieldArray } from 'formik' import memoize from 'lodash.memoize' import LocationField from '@opentripplanner/location-field' import { @@ -6,16 +6,16 @@ import { Input as LocationFieldInput, InputGroup as LocationFieldInputGroup } from '@opentripplanner/location-field/lib/styled' -import { connect } from 'react-redux' import React, { Component } from 'react' import { Button, ControlLabel, - FormControl, FormGroup, + Glyphicon, InputGroup } from 'react-bootstrap' import FontAwesome from 'react-fontawesome' +import { connect } from 'react-redux' import styled from 'styled-components' import { getShowUserSettings } from '../../util/state' @@ -64,7 +64,6 @@ export const isWork = loc => loc.type === 'work' /** * User's saved locations editor. - * TODO: Discuss and improve handling of location details (type, coordinates...). */ class FavoriteLocationsPane extends Component { constructor () { @@ -85,7 +84,7 @@ class FavoriteLocationsPane extends Component { icon: 'map-marker', lat: location.lat, lon: location.lon, - name: 'My place', + name: location.name, // Was 'My place' type: 'custom' }) @@ -94,15 +93,7 @@ class FavoriteLocationsPane extends Component { ) render () { - const { - currentPosition, - geocoderConfig, - nearbyStops, - sessionSearches, - showUserSettings, - stopsIndex, - values: userData - } = this.props + const { values: userData } = this.props const { location } = this.state const { savedLocations } = userData @@ -121,12 +112,11 @@ class FavoriteLocationsPane extends Component { const isHomeOrWork = loc === homeLocation || loc === workLocation return ( arrayHelpers.remove(index)} /> ) })} @@ -137,21 +127,12 @@ class FavoriteLocationsPane extends Component { - - @@ -162,8 +143,7 @@ class FavoriteLocationsPane extends Component { } } -// connect to redux store -// Get a subset of props that ConnectedLocationField would. +// Get a subset of redux states that ConnectedLocationField would use. const mapStateToProps = (state, ownProps) => { const { config, transitIndex } = state.otp const { currentPosition, nearbyStops, sessionSearches } = location @@ -176,40 +156,65 @@ const mapStateToProps = (state, ownProps) => { stopsIndex: transitIndex.stops } } - -const mapDispatchToProps = { -} +const mapDispatchToProps = {} +/** + * Styled LocationField defined at top of file, with the props from redux state above. + */ +const ConnectedLocationField = connect(mapStateToProps, mapDispatchToProps)(StyledLocationField) /** * Renders a user favorite location, * and lets the user edit the details (name, address, type) of the location. */ -const FavoriteLocation = ({ index, isFixed, location, onDelete }) => { - return ( - - - - - - - {!isFixed && ( - - - - )} - - - ) +class FavoriteLocation extends Component { + _handleDelete = () => { + const { arrayHelpers, index } = this.props + arrayHelpers.remove(index) + } + + _handleChange = ({ location }) => { + const { arrayHelpers, index, location: oldLocation } = this.props + + arrayHelpers.replace(index, { + address: location.name, + icon: oldLocation.icon, + lat: location.lat, + lon: location.lon, + name: location.name, // Was 'My place' + type: oldLocation.type + }) + } + + render () { + const { isFixed, location } = this.props + return ( + + + + + + + {!isFixed && ( + + + + )} + + + ) + } } -export default connect(mapStateToProps, mapDispatchToProps)(FavoriteLocationsPane) +export default FavoriteLocationsPane From 150eba63d71f2ab1032c44d14d49155281bf0805 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 2 Dec 2020 16:54:59 -0500 Subject: [PATCH 010/265] refactor(FavoriteLocation): Move event handling and state to inside component. --- .../user/favorite-locations-pane.js | 173 ++++++++++-------- lib/components/user/user-account-screen.js | 14 +- 2 files changed, 101 insertions(+), 86 deletions(-) diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations-pane.js index fe7de01d7..a8da77724 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations-pane.js @@ -1,6 +1,4 @@ import { FieldArray } from 'formik' -import memoize from 'lodash.memoize' -import LocationField from '@opentripplanner/location-field' import { DropdownContainer, Input as LocationFieldInput, @@ -15,10 +13,10 @@ import { InputGroup } from 'react-bootstrap' import FontAwesome from 'react-fontawesome' -import { connect } from 'react-redux' import styled from 'styled-components' -import { getShowUserSettings } from '../../util/state' +import LocationField from '../form/connected-location-field' +import { isBlank } from '../../util/ui' // Styles. const fancyAddLocationCss = ` @@ -45,7 +43,6 @@ const StyledLocationField = styled(LocationField)` ${LocationFieldInputGroup} { border: none; - display: block; } ${LocationFieldInput} { border: 1px solid #ccc; @@ -62,40 +59,26 @@ const StyledLocationField = styled(LocationField)` export const isHome = loc => loc.type === 'home' export const isWork = loc => loc.type === 'work' +// Defaults for home and work +export const BLANK_HOME = { + address: '', + icon: 'home', + name: '', + type: 'home' +} +export const BLANK_WORK = { + address: '', + icon: 'briefcase', + name: '', + type: 'work' +} + /** * User's saved locations editor. */ class FavoriteLocationsPane extends Component { - constructor () { - super() - this.state = { - location: null - } - } - - _handleAddNewLocation = memoize( - arrayHelpers => ({ location }) => { - // Set, then unset the location state to reset the LocationField after address is entered. - // (both this.setState calls should trigger componentDidUpdate on LocationField). - this.setState({ location }) - - arrayHelpers.push({ - address: location.name, - icon: 'map-marker', - lat: location.lat, - lon: location.lon, - name: location.name, // Was 'My place' - type: 'custom' - }) - - this.setState({ location: null }) - } - ) - render () { const { values: userData } = this.props - const { location } = this.state - const { savedLocations } = userData const homeLocation = savedLocations.find(isHome) const workLocation = savedLocations.find(isWork) @@ -122,19 +105,10 @@ class FavoriteLocationsPane extends Component { })} {/* For adding a location. */} - - - - - - - - + )} /> @@ -143,33 +117,33 @@ class FavoriteLocationsPane extends Component { } } -// Get a subset of redux states that ConnectedLocationField would use. -const mapStateToProps = (state, ownProps) => { - const { config, transitIndex } = state.otp - const { currentPosition, nearbyStops, sessionSearches } = location - return { - currentPosition, - geocoderConfig: config.geocoder, - nearbyStops, - sessionSearches, - showUserSettings: getShowUserSettings(state.otp), - stopsIndex: transitIndex.stops - } -} -const mapDispatchToProps = {} -/** - * Styled LocationField defined at top of file, with the props from redux state above. - */ -const ConnectedLocationField = connect(mapStateToProps, mapDispatchToProps)(StyledLocationField) - /** * Renders a user favorite location, * and lets the user edit the details (name, address, type) of the location. */ class FavoriteLocation extends Component { + constructor () { + super() + this.state = { + newLocation: null + } + } + _handleDelete = () => { - const { arrayHelpers, index } = this.props - arrayHelpers.remove(index) + const { arrayHelpers, index, isFixed, location } = this.props + if (isFixed) { + // For 'Home' and 'Work', replace with the default blank locations instead of deleting. + let newLocation + if (isHome(location)) { + newLocation = BLANK_HOME + } else if (isWork(location)) { + newLocation = BLANK_WORK + } + + this._handleChange({ location: newLocation }) + } else { + arrayHelpers.remove(index) + } } _handleChange = ({ location }) => { @@ -185,22 +159,71 @@ class FavoriteLocation extends Component { }) } + _handleNew = ({ location }) => { + const { arrayHelpers } = this.props + + // Set, then unset the location state to reset the LocationField after address is entered. + // (both this.setState calls should trigger componentDidUpdate on LocationField). + this.setState({ newLocation: location }) + + arrayHelpers.push({ + address: location.name, + icon: 'map-marker', + lat: location.lat, + lon: location.lon, + name: location.name, // Was 'My place' + type: 'custom' + }) + + this.setState({ newLocation: null }) + } + render () { - const { isFixed, location } = this.props + const { isFixed, isNew } = this.props + + // When true, newLocation makes the control add a new location instead of editing an existing one. + // The appearance is also different to differentiate the control with others. + let icon + let iconTitle + let handler + // Don't display a delete button if the item is fixed and its address is blank, + // or for the new location row. + let hideDelete + let location + let placeholder + let InputAddon + if (isNew) { + location = this.state.newLocation + icon = 'plus' + iconTitle = null + handler = this._handleNew + hideDelete = true + placeholder = 'Add another place' + InputAddon = NewLocationAddon + } else { + location = this.props.location + icon = location.icon + iconTitle = location.type + handler = this._handleChange + hideDelete = isFixed && isBlank(location.address) + placeholder = isFixed ? `Add ${location.type}` : 'Enter a favorite address' + InputAddon = StyledAddon + } + return ( - - - - + + + - {!isFixed && ( + {!hideDelete && ( + + )} + + + ) + } +} + +const customLocationType = { + icon: 'map-marker', + text: 'Custom', + type: 'custom' +} + +const locationTypes = [ + // TODO: add more non-home/work types + { + icon: 'cutlery', + text: 'Dining', + type: 'dining' + }, + customLocationType +] + +/** + * Displays a dropdown for selecting one of multiple location types. + */ +const LocationTypeSelector = ({ DropdownButtonComponent, id, onChange, selectedType }) => { + // Fall back to the 'custom' icon if the desired type is not found. + const locationType = locationTypes.find(t => t.type === selectedType) || customLocationType + + return ( + } + > + {locationTypes.map((t, index) => ( + + {t.text} + + ))} + + ) +} + +export default FavoriteLocation diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations-pane.js index 24bebed9c..e91aef174 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations-pane.js @@ -1,279 +1,47 @@ import { FieldArray } from 'formik' -import React, { Component } from 'react' -import { - Button, - ControlLabel, - DropdownButton, - FormGroup, - Glyphicon, - InputGroup, - MenuItem -} from 'react-bootstrap' -import FontAwesome from 'react-fontawesome' -import styled from 'styled-components' +import React from 'react' +import { ControlLabel } from 'react-bootstrap' -import FavoriteLocationField from '../form/favorite-location-field' - -const StyledDropdown = styled(DropdownButton)` - background-color: #eee; - width: 40px; -` -const NewLocationDropdownButton = styled(StyledDropdown)` - background-color: #337ab7; - color: #fff; -` -const StyledAddon = styled(InputGroup.Addon)` - width: 40px; -` - -// Helper filter functions. -export const isHome = loc => loc.type === 'home' -export const isWork = loc => loc.type === 'work' - -// Defaults for home and work -export const BLANK_HOME = { - address: '', - icon: 'home', - name: '', - type: 'home' -} -export const BLANK_WORK = { - address: '', - icon: 'briefcase', - name: '', - type: 'work' -} +import FavoriteLocation, { isHome, isWork } from './favorite-location' /** * User's saved locations editor. */ -class FavoriteLocationsPane extends Component { - render () { - const { values: userData } = this.props - const { savedLocations } = userData - const homeLocation = savedLocations.find(isHome) - const workLocation = savedLocations.find(isWork) - - return ( -
    - Add the places you frequent often to save time planning trips: - - ( - <> - {savedLocations.map((loc, index) => { - const isHomeOrWork = loc === homeLocation || loc === workLocation - return ( - - ) - })} - - {/* For adding a location. */} - - - )} - /> -
    - ) - } -} - -/** - * Creates a new favorite location object from a location returned by LocationField, with the specified type and icon. - */ -function makeFavoriteLocation (baseLocation, type, icon) { - const { lat, lon, name } = baseLocation - return { - address: name, - icon, - lat, - lon, - name, - type - } -} - -/** - * Renders a user favorite location, - * and lets the user edit the details (name, address, type) of the location. - */ -class FavoriteLocation extends Component { - constructor () { - super() - this.state = { - newLocation: null - } - } - - _handleDelete = () => { - const { arrayHelpers, index, isFixed, location } = this.props - if (isFixed) { - // For 'Home' and 'Work', replace with the default blank locations instead of deleting. - let newLocation - if (isHome(location)) { - newLocation = BLANK_HOME - } else if (isWork(location)) { - newLocation = BLANK_WORK - } - - this._handleLocationChange({ location: newLocation }) - } else { - arrayHelpers.remove(index) - } - } - - _handleLocationChange = ({ location }) => { - const { arrayHelpers, index, location: oldLocation } = this.props - const { icon, type } = oldLocation - arrayHelpers.replace(index, makeFavoriteLocation(location, type, icon)) - } - - _handleLocationTypeChange = ({ icon, type }) => { - const { arrayHelpers, index, location } = this.props - arrayHelpers.replace(index, makeFavoriteLocation(location, type, icon)) - } - - _handleNew = ({ location }) => { - const { arrayHelpers } = this.props - - // Set, then unset the location state to reset the LocationField after address is entered. - // (both this.setState calls should trigger componentDidUpdate on LocationField). - this.setState({ newLocation: location }) - - arrayHelpers.push(makeFavoriteLocation(location, 'custom', 'map-marker')) - - this.setState({ newLocation: null }) - } - - render () { - const { index, isFixed, isNew } = this.props - - // When true, isNew makes the control add a new location instead of editing an existing one. - // The appearance is also different to differentiate the control with others. - let icon - let iconTitle - let handler - // Don't display a delete button if the item is fixed and its address is blank, - // or for the new location row. - let hideDelete - let location - let placeholder - let DropdownBtn - if (isNew) { - location = this.state.newLocation - icon = 'plus' - iconTitle = null - handler = this._handleNew - hideDelete = true - placeholder = 'Add another place' - DropdownBtn = NewLocationDropdownButton - } else { - location = this.props.location - icon = location.icon - iconTitle = location.type - handler = this._handleLocationChange - hideDelete = false // isFixed && isBlank(location.address) - placeholder = isFixed ? `Add ${location.type}` : 'Enter a favorite address' - DropdownBtn = StyledDropdown - } - - // Show a dropdown for editing the icon, unless isFixed = true (for 'home' and 'work' locations). - // The dropdown has a predefined list of items for location types. - const iconControl = isFixed - ? ( - - - - ) - : ( - - ) - - return ( - - - {iconControl} - {/* wrapping element with z-index override needed for showing location menu on top of other controls. */} - - - - - {/* FIXME: hide the delete button for 'home' and 'work' after user clicks it - (there are challenging layouts). - */} - {!hideDelete && ( - - - - )} - - - ) - } -} - -const customLocationType = { - icon: 'map-marker', - text: 'Custom', - type: 'custom' -} - -const locationTypes = [ - // TODO: add more non-home/work types - { - icon: 'cutlery', - text: 'Dining', - type: 'dining' - }, - customLocationType -] - -/** - * Displays a dropdown for selecting one of multiple location types. - */ -const LocationTypeSelector = ({ DropdownButtonComponent, id, onChange, selectedType }) => { - // Fall back to the 'custom' icon if the desired type is not found. - const locationType = locationTypes.find(t => t.type === selectedType) || customLocationType +const FavoriteLocationsPane = ({ values: userData }) => { + const { savedLocations } = userData + const homeLocation = savedLocations.find(isHome) + const workLocation = savedLocations.find(isWork) return ( - } - > - {locationTypes.map((t, index) => ( - - {t.text} - - ))} - +
    + Add the places you frequent often to save time planning trips: + + ( + <> + {savedLocations.map((loc, index) => { + const isHomeOrWork = loc === homeLocation || loc === workLocation + return ( + + ) + })} + + {/* For adding a location. */} + + + )} + /> +
    ) } diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.js index 74447e3e5..9994c28e8 100644 --- a/lib/components/user/user-account-screen.js +++ b/lib/components/user/user-account-screen.js @@ -12,7 +12,8 @@ import { isNewUser } from '../../util/user' import DesktopNav from '../app/desktop-nav' import AccountSetupFinishPane from './account-setup-finish-pane' import ExistingAccountDisplay from './existing-account-display' -import FavoriteLocationsPane, { isHome, isWork, BLANK_HOME, BLANK_WORK } from './favorite-locations-pane' +import { isHome, isWork, BLANK_HOME, BLANK_WORK } from './favorite-location' +import FavoriteLocationsPane from './favorite-locations-pane' import NewAccountWizard from './new-account-wizard' import NotificationPrefsPane from './notification-prefs-pane' import TermsOfUsePane from './terms-of-use-pane' From 923b74db4d7187a4573b56f6a5b6373ab8afb349 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 4 Dec 2020 10:14:57 -0500 Subject: [PATCH 013/265] refactor(FavoriteLocation): Tweak comments --- lib/components/user/favorite-location.js | 3 ++- lib/components/user/favorite-locations-pane.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/components/user/favorite-location.js b/lib/components/user/favorite-location.js index 3dbe764c8..7992ac1f1 100644 --- a/lib/components/user/favorite-location.js +++ b/lib/components/user/favorite-location.js @@ -12,6 +12,7 @@ import styled from 'styled-components' import FavoriteLocationField from '../form/favorite-location-field' +// Styles const StyledDropdown = styled(DropdownButton)` background-color: #eee; width: 40px; @@ -59,7 +60,7 @@ function makeFavoriteLocation (baseLocation, type, icon) { /** * Renders a user favorite location, - * and lets the user edit the details (name, address, type) of the location. + * and lets the user edit the details (address, type, TODO: nickname) of the location. */ class FavoriteLocation extends Component { constructor () { diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations-pane.js index e91aef174..9ea9c3def 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations-pane.js @@ -5,7 +5,7 @@ import { ControlLabel } from 'react-bootstrap' import FavoriteLocation, { isHome, isWork } from './favorite-location' /** - * User's saved locations editor. + * Renders an editable list user's favorite locations, and lets the user add a new one. */ const FavoriteLocationsPane = ({ values: userData }) => { const { savedLocations } = userData From 6e2c0b9b9501e4bd7f177e4456b928e879d32063 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 9 Dec 2020 10:09:16 -0500 Subject: [PATCH 014/265] refactor(connectLocationField): Encapsulate code to connect LocationField to store. --- lib/components/form/connect-location-field.js | 62 +++++++++++++++++++ .../form/connected-location-field.js | 35 +---------- .../form/favorite-location-field.js | 59 ------------------ .../form/intermediate-place-field.js | 34 ++-------- lib/components/user/favorite-location.js | 37 ++++++++++- 5 files changed, 104 insertions(+), 123 deletions(-) create mode 100644 lib/components/form/connect-location-field.js delete mode 100644 lib/components/form/favorite-location-field.js diff --git a/lib/components/form/connect-location-field.js b/lib/components/form/connect-location-field.js new file mode 100644 index 000000000..b7de0be84 --- /dev/null +++ b/lib/components/form/connect-location-field.js @@ -0,0 +1,62 @@ +import { connect } from 'react-redux' + +import * as mapActions from '../../actions/map' +import * as locationActions from '../../actions/location' +import * as apiActions from '../../actions/api' +import { getActiveSearch, getShowUserSettings } from '../../util/state' + +/** + * This higher-order component connects the given (styled) LocationField to the redux store. + * It encapsulates the props mapping that must be done explicitly otherwise, + * even when styling a LocationField component that is already connected to the redux store. + * @param LocationFieldComponent The LocationFieldComponent to connect. + * @param options Optional object with the following optional boolean props that determine whether the corresponding + * redux state/action is passed to the component (all default to true): + * - clearLocation + * - location + * - onLocationSelected + * @returns The connected component. + */ +export default function connectLocationField (LocationFieldComponent, options = {}) { + const { + clearLocation: clearLocationProp = true, + location: locationProp = true, + onLocationSelected: onLocationSelectedProp = true + } = options + + const mapStateToProps = (state, ownProps) => { + const { config, currentQuery, location, transitIndex, user } = state.otp + const { currentPosition, nearbyStops, sessionSearches } = location + const activeSearch = getActiveSearch(state.otp) + const query = activeSearch ? activeSearch.query : currentQuery + + const stateToProps = { + currentPosition, + geocoderConfig: config.geocoder, + nearbyStops, + sessionSearches, + showUserSettings: getShowUserSettings(state.otp), + stopsIndex: transitIndex.stops, + userLocationsAndRecentPlaces: [...user.locations, ...user.recentPlaces] + } + if (locationProp) { + stateToProps.location = query[ownProps.locationType] + } + + return stateToProps + } + + const mapDispatchToProps = { + addLocationSearch: locationActions.addLocationSearch, + findNearbyStops: apiActions.findNearbyStops, + getCurrentPosition: locationActions.getCurrentPosition + } + if (clearLocationProp) { + mapDispatchToProps.clearLocation = mapActions.clearLocation + } + if (onLocationSelectedProp) { + mapDispatchToProps.onLocationSelected = mapActions.onLocationSelected + } + + return connect(mapStateToProps, mapDispatchToProps)(LocationFieldComponent) +} diff --git a/lib/components/form/connected-location-field.js b/lib/components/form/connected-location-field.js index 1f497f84f..e7001683d 100644 --- a/lib/components/form/connected-location-field.js +++ b/lib/components/form/connected-location-field.js @@ -7,13 +7,9 @@ import { InputGroupAddon, MenuItemA } from '@opentripplanner/location-field/lib/styled' -import { connect } from 'react-redux' import styled from 'styled-components' -import { clearLocation, onLocationSelected } from '../../actions/map' -import { addLocationSearch, getCurrentPosition } from '../../actions/location' -import { findNearbyStops } from '../../actions/api' -import { getActiveSearch, getShowUserSettings } from '../../util/state' +import connectLocationField from './connect-location-field' const StyledLocationField = styled(LocationField)` width: 100%; @@ -55,31 +51,4 @@ const StyledLocationField = styled(LocationField)` } ` -// connect to redux store - -const mapStateToProps = (state, ownProps) => { - const { config, currentQuery, location, transitIndex, user } = state.otp - const { currentPosition, nearbyStops, sessionSearches } = location - const activeSearch = getActiveSearch(state.otp) - const query = activeSearch ? activeSearch.query : currentQuery - return { - currentPosition, - geocoderConfig: config.geocoder, - location: query[ownProps.locationType], - nearbyStops, - sessionSearches, - showUserSettings: getShowUserSettings(state.otp), - stopsIndex: transitIndex.stops, - userLocationsAndRecentPlaces: [...user.locations, ...user.recentPlaces] - } -} - -const mapDispatchToProps = { - addLocationSearch, - findNearbyStops, - getCurrentPosition, - onLocationSelected, - clearLocation -} - -export default connect(mapStateToProps, mapDispatchToProps)(StyledLocationField) +export default connectLocationField(StyledLocationField) diff --git a/lib/components/form/favorite-location-field.js b/lib/components/form/favorite-location-field.js deleted file mode 100644 index bb09e5b02..000000000 --- a/lib/components/form/favorite-location-field.js +++ /dev/null @@ -1,59 +0,0 @@ -import LocationField from '@opentripplanner/location-field' -import { - DropdownContainer, - Input as LocationFieldInput, - InputGroup as LocationFieldInputGroup -} from '@opentripplanner/location-field/lib/styled' -import { connect } from 'react-redux' -import styled from 'styled-components' - -import * as apiActions from '../../actions/api' -import * as locationActions from '../../actions/location' -import { getShowUserSettings } from '../../util/state' - -const StyledLocationField = styled(LocationField)` - width: 100%; - - ${DropdownContainer} { - width: 0; - & > button { - display: none; - } - } - - ${LocationFieldInputGroup} { - border: none; - width: 100%; - } - ${LocationFieldInput} { - display: table-cell; - font-size: 100%; - line-height: 20px; - padding: 6px 12px; - width: 100%; - } -` - -// connect LocationField to redux store -// TODO: Refactor. Each time we want to apply styles to LocationField, we need to reconnect it. -const mapStateToProps = (state, ownProps) => { - const { config, location, transitIndex, user } = state.otp - const { currentPosition, nearbyStops, sessionSearches } = location - return { - currentPosition, - geocoderConfig: config.geocoder, - nearbyStops, - sessionSearches, - showUserSettings: getShowUserSettings(state.otp), - stopsIndex: transitIndex.stops, - userLocationsAndRecentPlaces: [...user.locations, ...user.recentPlaces] - } -} - -const mapDispatchToProps = { - addLocationSearch: locationActions.addLocationSearch, - findNearbyStops: apiActions.findNearbyStops, - getCurrentPosition: locationActions.getCurrentPosition -} - -export default connect(mapStateToProps, mapDispatchToProps)(StyledLocationField) diff --git a/lib/components/form/intermediate-place-field.js b/lib/components/form/intermediate-place-field.js index 9d3f9aaae..aaa334b8b 100644 --- a/lib/components/form/intermediate-place-field.js +++ b/lib/components/form/intermediate-place-field.js @@ -8,13 +8,9 @@ import { MenuItemA } from '@opentripplanner/location-field/lib/styled' import React, {Component} from 'react' -import { connect } from 'react-redux' import styled from 'styled-components' -import { clearLocation } from '../../actions/map' -import { addLocationSearch, getCurrentPosition } from '../../actions/location' -import { findNearbyStops } from '../../actions/api' -import { getShowUserSettings } from '../../util/state' +import connectLocationField from './connect-location-field' const StyledIntermediatePlace = styled(LocationField)` width: 100%; @@ -78,27 +74,7 @@ class IntermediatePlaceField extends Component { } } -// connect to redux store - -const mapStateToProps = (state, ownProps) => { - const { config, location, transitIndex, user } = state.otp - const { currentPosition, nearbyStops, sessionSearches } = location - return { - currentPosition, - geocoderConfig: config.geocoder, - nearbyStops, - sessionSearches, - showUserSettings: getShowUserSettings(state.otp), - stopsIndex: transitIndex.stops, - userLocationsAndRecentPlaces: [...user.locations, ...user.recentPlaces] - } -} - -const mapDispatchToProps = { - addLocationSearch, - findNearbyStops, - getCurrentPosition, - clearLocation -} - -export default connect(mapStateToProps, mapDispatchToProps)(IntermediatePlaceField) +export default connectLocationField(IntermediatePlaceField, { + location: false, + onLocationSelected: false +}) diff --git a/lib/components/user/favorite-location.js b/lib/components/user/favorite-location.js index 7992ac1f1..00ada56fc 100644 --- a/lib/components/user/favorite-location.js +++ b/lib/components/user/favorite-location.js @@ -1,3 +1,9 @@ +import LocationField from '@opentripplanner/location-field' +import { + DropdownContainer, + Input as LocationFieldInput, + InputGroup as LocationFieldInputGroup +} from '@opentripplanner/location-field/lib/styled' import React, { Component } from 'react' import { Button, @@ -10,9 +16,36 @@ import { import FontAwesome from 'react-fontawesome' import styled from 'styled-components' -import FavoriteLocationField from '../form/favorite-location-field' +import connectLocationField from '../form/connect-location-field' + +// Style and connect LocationField to redux store. +const StyledLocationField = styled(LocationField)` + width: 100%; + ${DropdownContainer} { + width: 0; + & > button { + display: none; + } + } + ${LocationFieldInputGroup} { + border: none; + width: 100%; + } + ${LocationFieldInput} { + display: table-cell; + font-size: 100%; + line-height: 20px; + padding: 6px 12px; + width: 100%; + } +` +const FavoriteLocationField = connectLocationField(StyledLocationField, { + clearLocation: false, + location: false, + onLocationSelected: false +}) -// Styles +// Styles for other controls const StyledDropdown = styled(DropdownButton)` background-color: #eee; width: 40px; From 6221ab0eae9a40183025435c1a6f7dea97cd57df Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Wed, 9 Dec 2020 10:42:00 -0500 Subject: [PATCH 015/265] refactor(LocationField): refactor HOC for LocationField --- lib/components/form/connect-location-field.js | 42 +++++++------------ .../form/connected-location-field.js | 9 +++- .../form/intermediate-place-field.js | 5 +-- lib/components/user/favorite-location.js | 6 +-- 4 files changed, 24 insertions(+), 38 deletions(-) diff --git a/lib/components/form/connect-location-field.js b/lib/components/form/connect-location-field.js index b7de0be84..7e21049d7 100644 --- a/lib/components/form/connect-location-field.js +++ b/lib/components/form/connect-location-field.js @@ -1,29 +1,22 @@ import { connect } from 'react-redux' -import * as mapActions from '../../actions/map' import * as locationActions from '../../actions/location' import * as apiActions from '../../actions/api' import { getActiveSearch, getShowUserSettings } from '../../util/state' /** - * This higher-order component connects the given (styled) LocationField to the redux store. - * It encapsulates the props mapping that must be done explicitly otherwise, - * even when styling a LocationField component that is already connected to the redux store. - * @param LocationFieldComponent The LocationFieldComponent to connect. - * @param options Optional object with the following optional boolean props that determine whether the corresponding - * redux state/action is passed to the component (all default to true): - * - clearLocation - * - location - * - onLocationSelected + * This higher-order component connects the target (styled) LocationField to the + * redux store. + * @param StyledLocationField The input LocationField component to connect. + * @param options Optional object with the following optional props: + * - actions: a list of actions to include in mapDispatchToProps + * - includeLocation: whether to derive the location prop from + * the active query * @returns The connected component. */ -export default function connectLocationField (LocationFieldComponent, options = {}) { - const { - clearLocation: clearLocationProp = true, - location: locationProp = true, - onLocationSelected: onLocationSelectedProp = true - } = options - +export default function connectLocationField (StyledLocationField, options = {}) { + // By default, set actions to empty list and do not include location. + const {actions = [], includeLocation = false} = options const mapStateToProps = (state, ownProps) => { const { config, currentQuery, location, transitIndex, user } = state.otp const { currentPosition, nearbyStops, sessionSearches } = location @@ -33,15 +26,13 @@ export default function connectLocationField (LocationFieldComponent, options = const stateToProps = { currentPosition, geocoderConfig: config.geocoder, + location: includeLocation ? query[ownProps.locationType] : undefined, nearbyStops, sessionSearches, showUserSettings: getShowUserSettings(state.otp), stopsIndex: transitIndex.stops, userLocationsAndRecentPlaces: [...user.locations, ...user.recentPlaces] } - if (locationProp) { - stateToProps.location = query[ownProps.locationType] - } return stateToProps } @@ -49,14 +40,9 @@ export default function connectLocationField (LocationFieldComponent, options = const mapDispatchToProps = { addLocationSearch: locationActions.addLocationSearch, findNearbyStops: apiActions.findNearbyStops, - getCurrentPosition: locationActions.getCurrentPosition - } - if (clearLocationProp) { - mapDispatchToProps.clearLocation = mapActions.clearLocation - } - if (onLocationSelectedProp) { - mapDispatchToProps.onLocationSelected = mapActions.onLocationSelected + getCurrentPosition: locationActions.getCurrentPosition, + ...actions } - return connect(mapStateToProps, mapDispatchToProps)(LocationFieldComponent) + return connect(mapStateToProps, mapDispatchToProps)(StyledLocationField) } diff --git a/lib/components/form/connected-location-field.js b/lib/components/form/connected-location-field.js index e7001683d..6be2c99d6 100644 --- a/lib/components/form/connected-location-field.js +++ b/lib/components/form/connected-location-field.js @@ -9,6 +9,7 @@ import { } from '@opentripplanner/location-field/lib/styled' import styled from 'styled-components' +import * as mapActions from '../../actions/map' import connectLocationField from './connect-location-field' const StyledLocationField = styled(LocationField)` @@ -51,4 +52,10 @@ const StyledLocationField = styled(LocationField)` } ` -export default connectLocationField(StyledLocationField) +export default connectLocationField(StyledLocationField, { + actions: { + clearLocation: mapActions.clearLocation, + onLocationSelected: mapActions.onLocationSelected + }, + includeLocation: true +}) diff --git a/lib/components/form/intermediate-place-field.js b/lib/components/form/intermediate-place-field.js index aaa334b8b..82a8ba3f8 100644 --- a/lib/components/form/intermediate-place-field.js +++ b/lib/components/form/intermediate-place-field.js @@ -74,7 +74,4 @@ class IntermediatePlaceField extends Component { } } -export default connectLocationField(IntermediatePlaceField, { - location: false, - onLocationSelected: false -}) +export default connectLocationField(IntermediatePlaceField) diff --git a/lib/components/user/favorite-location.js b/lib/components/user/favorite-location.js index 00ada56fc..eb3f3dd5a 100644 --- a/lib/components/user/favorite-location.js +++ b/lib/components/user/favorite-location.js @@ -39,11 +39,7 @@ const StyledLocationField = styled(LocationField)` width: 100%; } ` -const FavoriteLocationField = connectLocationField(StyledLocationField, { - clearLocation: false, - location: false, - onLocationSelected: false -}) +const FavoriteLocationField = connectLocationField(StyledLocationField) // Styles for other controls const StyledDropdown = styled(DropdownButton)` From 95e1c9c21b825488e1fc041a9d956222cee93f66 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 9 Dec 2020 16:52:39 -0500 Subject: [PATCH 016/265] refactor(IntermediatePlaceField): Add back actions and unset location prop. --- lib/components/form/connect-location-field.js | 5 ++++- lib/components/form/intermediate-place-field.js | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/components/form/connect-location-field.js b/lib/components/form/connect-location-field.js index 7e21049d7..bcad6f05a 100644 --- a/lib/components/form/connect-location-field.js +++ b/lib/components/form/connect-location-field.js @@ -26,13 +26,16 @@ export default function connectLocationField (StyledLocationField, options = {}) const stateToProps = { currentPosition, geocoderConfig: config.geocoder, - location: includeLocation ? query[ownProps.locationType] : undefined, nearbyStops, sessionSearches, showUserSettings: getShowUserSettings(state.otp), stopsIndex: transitIndex.stops, userLocationsAndRecentPlaces: [...user.locations, ...user.recentPlaces] } + // Leave stateToProps.location completely unset unless includeLocation is true. + if (includeLocation) { + stateToProps.location = query[ownProps.locationType] + } return stateToProps } diff --git a/lib/components/form/intermediate-place-field.js b/lib/components/form/intermediate-place-field.js index 82a8ba3f8..59c1650ba 100644 --- a/lib/components/form/intermediate-place-field.js +++ b/lib/components/form/intermediate-place-field.js @@ -10,6 +10,7 @@ import { import React, {Component} from 'react' import styled from 'styled-components' +import * as mapActions from '../../actions/map' import connectLocationField from './connect-location-field' const StyledIntermediatePlace = styled(LocationField)` @@ -74,4 +75,8 @@ class IntermediatePlaceField extends Component { } } -export default connectLocationField(IntermediatePlaceField) +export default connectLocationField(IntermediatePlaceField, { + actions: { + clearLocation: mapActions.clearLocation + } +}) From 4cd86a3c3c433adc3a18cd147f9ec0d7c7accac8 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 9 Dec 2020 17:38:01 -0500 Subject: [PATCH 017/265] refactor(FontAwesome): Replace with narrative/icon wrapper. --- lib/components/user/after-signin-screen.js | 7 ++----- lib/components/user/favorite-location.js | 8 ++++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/components/user/after-signin-screen.js b/lib/components/user/after-signin-screen.js index 9ed1ac6a8..6808ad4ee 100644 --- a/lib/components/user/after-signin-screen.js +++ b/lib/components/user/after-signin-screen.js @@ -1,9 +1,9 @@ import * as routerActions from 'connected-react-router' import React, { Component } from 'react' -import FontAwesome from 'react-fontawesome' import { connect } from 'react-redux' import * as uiActions from '../../actions/ui' +import Icon from '../narrative/icon' import { isNewUser } from '../../util/user' import withLoggedInUserSupport from './with-logged-in-user-support' @@ -38,10 +38,7 @@ class AfterSignInScreen extends Component {

    Signed In...
    - +

    ) diff --git a/lib/components/user/favorite-location.js b/lib/components/user/favorite-location.js index 7992ac1f1..1d0a1a8c0 100644 --- a/lib/components/user/favorite-location.js +++ b/lib/components/user/favorite-location.js @@ -7,10 +7,10 @@ import { InputGroup, MenuItem } from 'react-bootstrap' -import FontAwesome from 'react-fontawesome' import styled from 'styled-components' import FavoriteLocationField from '../form/favorite-location-field' +import Icon from '../narrative/icon' // Styles const StyledDropdown = styled(DropdownButton)` @@ -148,7 +148,7 @@ class FavoriteLocation extends Component { const iconControl = isFixed ? ( - + ) : ( @@ -222,11 +222,11 @@ const LocationTypeSelector = ({ DropdownButtonComponent, id, onChange, selectedT } + title={} > {locationTypes.map((t, index) => ( - {t.text} + {t.text} ))} From cae2ba746809b92e8b103378e5752c2c385ec492 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 10 Dec 2020 09:32:53 -0500 Subject: [PATCH 018/265] refactor(connectLocationField): Add comment detail about leaving location prop unset. --- lib/components/form/connect-location-field.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/components/form/connect-location-field.js b/lib/components/form/connect-location-field.js index bcad6f05a..a593dd117 100644 --- a/lib/components/form/connect-location-field.js +++ b/lib/components/form/connect-location-field.js @@ -32,7 +32,9 @@ export default function connectLocationField (StyledLocationField, options = {}) stopsIndex: transitIndex.stops, userLocationsAndRecentPlaces: [...user.locations, ...user.recentPlaces] } - // Leave stateToProps.location completely unset unless includeLocation is true. + // Set the location prop only if includeLocation is specified, else leave unset. + // Otherwise, the StyledLocationField component will use the fixed undefined/null value as location + // and will not respond to user input. if (includeLocation) { stateToProps.location = query[ownProps.locationType] } From eccfa5ccd2006c56e279ee185ea4f48fbbe62008 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 10 Dec 2020 09:56:13 -0500 Subject: [PATCH 019/265] refactor(mobile/NavigationBar): Replace FontAwesome with narrative/Icon --- lib/components/mobile/navigation-bar.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/components/mobile/navigation-bar.js b/lib/components/mobile/navigation-bar.js index cac315e1c..74788126b 100644 --- a/lib/components/mobile/navigation-bar.js +++ b/lib/components/mobile/navigation-bar.js @@ -1,12 +1,12 @@ import PropTypes from 'prop-types' import React, { Component } from 'react' import { Navbar } from 'react-bootstrap' -import FontAwesome from 'react-fontawesome' import { connect } from 'react-redux' import { setMobileScreen } from '../../actions/ui' import AppMenu from '../app/app-menu' import NavLoginButtonAuth0 from '../../components/user/nav-login-button-auth0' +import Icon from '../narrative/icon' import { accountLinks, getAuth0Config } from '../../util/auth' class MobileNavigationBar extends Component { @@ -40,7 +40,15 @@ class MobileNavigationBar extends Component { {showBackButton - ?
    + ? ( +
    + +
    + ) : }
    From 4227cd6f467934ed4633e01d3aaf2580cddc7c69 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 4 Dec 2020 18:53:20 -0500 Subject: [PATCH 020/265] refactor(util/user): Move isHome and isWork to util. --- lib/components/user/favorite-locations-pane.js | 3 ++- lib/components/user/user-account-screen.js | 4 ++-- lib/util/user.js | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations-pane.js index 9ea9c3def..88acccdc9 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations-pane.js @@ -2,7 +2,8 @@ import { FieldArray } from 'formik' import React from 'react' import { ControlLabel } from 'react-bootstrap' -import FavoriteLocation, { isHome, isWork } from './favorite-location' +import FavoriteLocation from './favorite-location' +import { isHome, isWork } from '../../util/user' /** * Renders an editable list user's favorite locations, and lets the user add a new one. diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.js index 9994c28e8..c2143c74b 100644 --- a/lib/components/user/user-account-screen.js +++ b/lib/components/user/user-account-screen.js @@ -8,11 +8,11 @@ import * as yup from 'yup' import * as uiActions from '../../actions/ui' import * as userActions from '../../actions/user' import { RETURN_TO_CURRENT_ROUTE } from '../../util/ui' -import { isNewUser } from '../../util/user' +import { isNewUser, isHome, isWork } from '../../util/user' import DesktopNav from '../app/desktop-nav' import AccountSetupFinishPane from './account-setup-finish-pane' import ExistingAccountDisplay from './existing-account-display' -import { isHome, isWork, BLANK_HOME, BLANK_WORK } from './favorite-location' +import { BLANK_HOME, BLANK_WORK } from './favorite-location' import FavoriteLocationsPane from './favorite-locations-pane' import NewAccountWizard from './new-account-wizard' import NotificationPrefsPane from './notification-prefs-pane' diff --git a/lib/util/user.js b/lib/util/user.js index 827896cd2..da8f5949f 100644 --- a/lib/util/user.js +++ b/lib/util/user.js @@ -2,3 +2,8 @@ export function isNewUser (loggedInUser) { return !loggedInUser.hasConsentedToTerms } + +// Helper functions to determine if +// a location is home or work. +export const isHome = loc => loc.type === 'home' +export const isWork = loc => loc.type === 'work' From 9298f94739c2196c0a6f47717a9481c12ad1e306 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 10 Dec 2020 12:31:47 -0500 Subject: [PATCH 021/265] refactor(NewFavoriteLocation): Split NewFavoriteLocation from FavoriteLocation Also move FavoriteLocationField and FavoriteLocationDropdown to favorite-location-controls --- .../user/favorite-location-controls.js | 74 +++++++++ lib/components/user/favorite-location.js | 146 +++--------------- .../user/favorite-locations-pane.js | 6 +- lib/components/user/new-favorite-location.js | 91 +++++++++++ 4 files changed, 187 insertions(+), 130 deletions(-) create mode 100644 lib/components/user/favorite-location-controls.js create mode 100644 lib/components/user/new-favorite-location.js diff --git a/lib/components/user/favorite-location-controls.js b/lib/components/user/favorite-location-controls.js new file mode 100644 index 000000000..69724a438 --- /dev/null +++ b/lib/components/user/favorite-location-controls.js @@ -0,0 +1,74 @@ +import LocationField from '@opentripplanner/location-field' +import { + DropdownContainer, + Input as LocationFieldInput, + InputGroup as LocationFieldInputGroup +} from '@opentripplanner/location-field/lib/styled' +import React from 'react' +import { InputGroup, MenuItem } from 'react-bootstrap' +import styled from 'styled-components' + +import connectLocationField from '../form/connect-location-field' +import Icon from '../narrative/icon' + +// Styles +const customLocationType = { + icon: 'map-marker', + text: 'Custom', + type: 'custom' +} + +const locationTypes = [ + // TODO: add more non-home/work types + { + icon: 'cutlery', + text: 'Dining', + type: 'dining' + }, + customLocationType +] + +/** + * Displays a dropdown for selecting one of multiple location types. + */ +export const FavoriteLocationTypeDropdown = ({ DropdownButtonComponent, id, onChange, selectedType }) => { + // Fall back to the 'custom' icon if the desired type is not found. + const locationType = locationTypes.find(t => t.type === selectedType) || customLocationType + + return ( + } + > + {locationTypes.map((t, index) => ( + + {t.text} + + ))} + + ) +} + +// Style and connect LocationField to redux store. +const StyledLocationField = styled(LocationField)` + width: 100%; + ${DropdownContainer} { + width: 0; + & > button { + display: none; + } + } + ${LocationFieldInputGroup} { + border: none; + width: 100%; + } + ${LocationFieldInput} { + display: table-cell; + font-size: 100%; + line-height: 20px; + padding: 6px 12px; + width: 100%; + } +` +export const FavoriteLocationField = connectLocationField(StyledLocationField) diff --git a/lib/components/user/favorite-location.js b/lib/components/user/favorite-location.js index 745ea0cc4..2393968b2 100644 --- a/lib/components/user/favorite-location.js +++ b/lib/components/user/favorite-location.js @@ -1,63 +1,26 @@ -import LocationField from '@opentripplanner/location-field' -import { - DropdownContainer, - Input as LocationFieldInput, - InputGroup as LocationFieldInputGroup -} from '@opentripplanner/location-field/lib/styled' import React, { Component } from 'react' import { Button, DropdownButton, FormGroup, Glyphicon, - InputGroup, - MenuItem + InputGroup } from 'react-bootstrap' import styled from 'styled-components' -import connectLocationField from '../form/connect-location-field' import Icon from '../narrative/icon' - -// Style and connect LocationField to redux store. -const StyledLocationField = styled(LocationField)` - width: 100%; - ${DropdownContainer} { - width: 0; - & > button { - display: none; - } - } - ${LocationFieldInputGroup} { - border: none; - width: 100%; - } - ${LocationFieldInput} { - display: table-cell; - font-size: 100%; - line-height: 20px; - padding: 6px 12px; - width: 100%; - } -` -const FavoriteLocationField = connectLocationField(StyledLocationField) +import { isHome, isWork } from '../../util/user' +import { FavoriteLocationField, FavoriteLocationTypeDropdown } from './favorite-location-controls' // Styles for other controls const StyledDropdown = styled(DropdownButton)` background-color: #eee; width: 40px; ` -const NewLocationDropdownButton = styled(StyledDropdown)` - background-color: #337ab7; - color: #fff; -` const StyledAddon = styled(InputGroup.Addon)` width: 40px; ` -// Helper filter functions. -export const isHome = loc => loc.type === 'home' -export const isWork = loc => loc.type === 'work' - // Defaults for home and work export const BLANK_HOME = { address: '', @@ -140,52 +103,23 @@ class FavoriteLocation extends Component { } render () { - const { index, isFixed, isNew } = this.props - - // When true, isNew makes the control add a new location instead of editing an existing one. - // The appearance is also different to differentiate the control with others. - let icon - let iconTitle - let handler - // Don't display a delete button if the item is fixed and its address is blank, - // or for the new location row. - let hideDelete - let location - let placeholder - let DropdownBtn - if (isNew) { - location = this.state.newLocation - icon = 'plus' - iconTitle = null - handler = this._handleNew - hideDelete = true - placeholder = 'Add another place' - DropdownBtn = NewLocationDropdownButton - } else { - location = this.props.location - // TODO: lookup icon using location.type - icon = location.icon - iconTitle = location.type - handler = this._handleLocationChange - hideDelete = false // isFixed && isBlank(location.address) - placeholder = isFixed ? `Add ${location.type}` : 'Enter a favorite address' - DropdownBtn = StyledDropdown - } + const { index, isFixed, location } = this.props + const placeholder = isFixed ? `Add ${location.type}` : 'Enter a favorite address' // Show a dropdown for editing the icon, unless isFixed = true (for 'home' and 'work' locations). // The dropdown has a predefined list of items for location types. const iconControl = isFixed ? ( - - + + ) : ( - ) @@ -199,7 +133,7 @@ class FavoriteLocation extends Component { inputPlaceholder={placeholder} location={location} locationType='to' - onLocationSelected={handler} + onLocationSelected={this._handleLocationChange} showClearButton={false} /> @@ -207,59 +141,19 @@ class FavoriteLocation extends Component { {/* FIXME: hide the delete button for 'home' and 'work' after user clicks it (there are challenging layouts). */} - {!hideDelete && ( - - - - )} + + + ) } } -const customLocationType = { - icon: 'map-marker', - text: 'Custom', - type: 'custom' -} - -const locationTypes = [ - // TODO: add more non-home/work types - { - icon: 'cutlery', - text: 'Dining', - type: 'dining' - }, - customLocationType -] - -/** - * Displays a dropdown for selecting one of multiple location types. - */ -const LocationTypeSelector = ({ DropdownButtonComponent, id, onChange, selectedType }) => { - // Fall back to the 'custom' icon if the desired type is not found. - const locationType = locationTypes.find(t => t.type === selectedType) || customLocationType - - return ( - } - > - {locationTypes.map((t, index) => ( - - {t.text} - - ))} - - ) -} - export default FavoriteLocation diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations-pane.js index 88acccdc9..51d1083e6 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations-pane.js @@ -3,6 +3,7 @@ import React from 'react' import { ControlLabel } from 'react-bootstrap' import FavoriteLocation from './favorite-location' +import NewFavoriteLocation from './new-favorite-location' import { isHome, isWork } from '../../util/user' /** @@ -35,10 +36,7 @@ const FavoriteLocationsPane = ({ values: userData }) => { })} {/* For adding a location. */} - + )} /> diff --git a/lib/components/user/new-favorite-location.js b/lib/components/user/new-favorite-location.js new file mode 100644 index 000000000..7aa8dadb5 --- /dev/null +++ b/lib/components/user/new-favorite-location.js @@ -0,0 +1,91 @@ +import React, { Component } from 'react' +import { + DropdownButton, + FormGroup, + InputGroup +} from 'react-bootstrap' +import styled from 'styled-components' + +import { FavoriteLocationField, FavoriteLocationTypeDropdown } from './favorite-location-controls' + +// Styles +const NewLocationDropdownButton = styled(DropdownButton)` + background-color: #337ab7; + color: #fff; + width: 40px; +` + +/** + * Creates a new favorite location object from a location returned by LocationField, with the specified type and icon. + */ +function makeFavoriteLocation (baseLocation, type, icon) { + const { lat, lon, name } = baseLocation + return { + address: name, + icon, + lat, + lon, + name, + type + } +} + +const blankNewLocationState = { + newLocation: null, + newLocationType: 'custom' +} + +/** + * Renders a user favorite location, + * and lets the user edit the details (address, type, TODO: nickname) of the location. + */ +class NewFavoriteLocation extends Component { + constructor () { + super() + this.state = blankNewLocationState + } + + _handleLocationTypeChange = ({ icon, type }) => { + // Update location type in dropdown. + this.setState({ newLocationType: type }) + } + + _handleNew = ({ location }) => { + const { arrayHelpers } = this.props + const { newLocationType } = this.state + + arrayHelpers.push(makeFavoriteLocation(location, newLocationType, 'map-marker')) + + // Unset the location state to reset the LocationField after address is entered. + this.setState(blankNewLocationState) + } + + render () { + const { newLocation, newLocationType } = this.state + return ( + + + + + {/* wrapper with z-index override needed for showing location menu on top of other controls. TODO: refactor. */} + + + + + + ) + } +} + +export default NewFavoriteLocation From bcce0e4b6ce0cc0606208d6eac75dc7380fd8f77 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 10 Dec 2020 14:55:39 -0500 Subject: [PATCH 022/265] refactor(FavoriteLocation): Replace isFixed prop with placeholder prop --- lib/components/user/favorite-location.js | 61 +++---------------- .../user/favorite-locations-pane.js | 43 ++++++++----- lib/components/user/new-favorite-location.js | 16 +---- lib/components/user/user-account-screen.js | 3 +- 4 files changed, 40 insertions(+), 83 deletions(-) diff --git a/lib/components/user/favorite-location.js b/lib/components/user/favorite-location.js index 2393968b2..f979edd50 100644 --- a/lib/components/user/favorite-location.js +++ b/lib/components/user/favorite-location.js @@ -9,7 +9,6 @@ import { import styled from 'styled-components' import Icon from '../narrative/icon' -import { isHome, isWork } from '../../util/user' import { FavoriteLocationField, FavoriteLocationTypeDropdown } from './favorite-location-controls' // Styles for other controls @@ -21,24 +20,10 @@ const StyledAddon = styled(InputGroup.Addon)` width: 40px; ` -// Defaults for home and work -export const BLANK_HOME = { - address: '', - icon: 'home', - name: '', - type: 'home' -} -export const BLANK_WORK = { - address: '', - icon: 'briefcase', - name: '', - type: 'work' -} - /** * Creates a new favorite location object from a location returned by LocationField, with the specified type and icon. */ -function makeFavoriteLocation (baseLocation, type, icon) { +export function makeFavoriteLocation (baseLocation, type, icon) { const { lat, lon, name } = baseLocation return { address: name, @@ -55,25 +40,10 @@ function makeFavoriteLocation (baseLocation, type, icon) { * and lets the user edit the details (address, type, TODO: nickname) of the location. */ class FavoriteLocation extends Component { - constructor () { - super() - this.state = { - newLocation: null - } - } - _handleDelete = () => { - const { arrayHelpers, index, isFixed, location } = this.props - if (isFixed) { - // For 'Home' and 'Work', replace with the default blank locations instead of deleting. - let newLocation - if (isHome(location)) { - newLocation = BLANK_HOME - } else if (isWork(location)) { - newLocation = BLANK_WORK - } - - this._handleLocationChange({ location: newLocation }) + const { arrayHelpers, index, placeholder } = this.props + if (placeholder) { + this._handleLocationChange({ location: placeholder }) } else { arrayHelpers.remove(index) } @@ -90,25 +60,14 @@ class FavoriteLocation extends Component { arrayHelpers.replace(index, makeFavoriteLocation(location, type, icon)) } - _handleNew = ({ location }) => { - const { arrayHelpers } = this.props - - // Set, then unset the location state to reset the LocationField after address is entered. - // (both this.setState calls should trigger componentDidUpdate on LocationField). - this.setState({ newLocation: location }) - - arrayHelpers.push(makeFavoriteLocation(location, 'custom', 'map-marker')) - - this.setState({ newLocation: null }) - } - render () { - const { index, isFixed, location } = this.props - const placeholder = isFixed ? `Add ${location.type}` : 'Enter a favorite address' + const { index, location, placeholder } = this.props + const placeholderText = placeholder ? `Add ${location.type}` : 'Enter a favorite address' - // Show a dropdown for editing the icon, unless isFixed = true (for 'home' and 'work' locations). + // Show a dropdown for editing the icon, shown unless a placeholder is set, + // in which case the type is considered fixed (e.g. for 'home' and 'work' locations). // The dropdown has a predefined list of items for location types. - const iconControl = isFixed + const iconControl = placeholder ? ( @@ -130,7 +89,7 @@ class FavoriteLocation extends Component { {/* wrapper with z-index override needed for showing location menu on top of other controls. */} { const { savedLocations } = userData - const homeLocation = savedLocations.find(isHome) - const workLocation = savedLocations.find(isWork) return (
    @@ -22,18 +38,15 @@ const FavoriteLocationsPane = ({ values: userData }) => { name='savedLocations' render={arrayHelpers => ( <> - {savedLocations.map((loc, index) => { - const isHomeOrWork = loc === homeLocation || loc === workLocation - return ( - - ) - })} + {savedLocations.map((loc, index) => ( + + ))} {/* For adding a location. */} diff --git a/lib/components/user/new-favorite-location.js b/lib/components/user/new-favorite-location.js index 7aa8dadb5..e96177a1d 100644 --- a/lib/components/user/new-favorite-location.js +++ b/lib/components/user/new-favorite-location.js @@ -6,6 +6,7 @@ import { } from 'react-bootstrap' import styled from 'styled-components' +import { makeFavoriteLocation } from './favorite-location' import { FavoriteLocationField, FavoriteLocationTypeDropdown } from './favorite-location-controls' // Styles @@ -15,21 +16,6 @@ const NewLocationDropdownButton = styled(DropdownButton)` width: 40px; ` -/** - * Creates a new favorite location object from a location returned by LocationField, with the specified type and icon. - */ -function makeFavoriteLocation (baseLocation, type, icon) { - const { lat, lon, name } = baseLocation - return { - address: name, - icon, - lat, - lon, - name, - type - } -} - const blankNewLocationState = { newLocation: null, newLocationType: 'custom' diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.js index c2143c74b..0b1ac5df0 100644 --- a/lib/components/user/user-account-screen.js +++ b/lib/components/user/user-account-screen.js @@ -12,8 +12,7 @@ import { isNewUser, isHome, isWork } from '../../util/user' import DesktopNav from '../app/desktop-nav' import AccountSetupFinishPane from './account-setup-finish-pane' import ExistingAccountDisplay from './existing-account-display' -import { BLANK_HOME, BLANK_WORK } from './favorite-location' -import FavoriteLocationsPane from './favorite-locations-pane' +import FavoriteLocationsPane, { BLANK_HOME, BLANK_WORK } from './favorite-locations-pane' import NewAccountWizard from './new-account-wizard' import NotificationPrefsPane from './notification-prefs-pane' import TermsOfUsePane from './terms-of-use-pane' From 23f082922c1857739b3094d1a885b01c1a6bc0cd Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 11 Dec 2020 12:23:12 -0500 Subject: [PATCH 023/265] feat(FavoriteLocation): Allow user to edit nickname of favorite location. --- .../user/favorite-location-controls.js | 4 +- lib/components/user/favorite-location.js | 115 +++++++++++++++--- .../user/favorite-locations-pane.js | 4 +- 3 files changed, 101 insertions(+), 22 deletions(-) diff --git a/lib/components/user/favorite-location-controls.js b/lib/components/user/favorite-location-controls.js index 69724a438..1362bcbcf 100644 --- a/lib/components/user/favorite-location-controls.js +++ b/lib/components/user/favorite-location-controls.js @@ -39,7 +39,7 @@ export const FavoriteLocationTypeDropdown = ({ DropdownButtonComponent, id, onCh } + title={} > {locationTypes.map((t, index) => ( @@ -67,7 +67,7 @@ const StyledLocationField = styled(LocationField)` display: table-cell; font-size: 100%; line-height: 20px; - padding: 6px 12px; + padding: 6px 0px; width: 100%; } ` diff --git a/lib/components/user/favorite-location.js b/lib/components/user/favorite-location.js index f979edd50..c935a2f7c 100644 --- a/lib/components/user/favorite-location.js +++ b/lib/components/user/favorite-location.js @@ -1,7 +1,9 @@ import React, { Component } from 'react' import { Button, + ControlLabel, DropdownButton, + FormControl, FormGroup, Glyphicon, InputGroup @@ -12,29 +14,76 @@ import Icon from '../narrative/icon' import { FavoriteLocationField, FavoriteLocationTypeDropdown } from './favorite-location-controls' // Styles for other controls -const StyledDropdown = styled(DropdownButton)` - background-color: #eee; - width: 40px; +const fieldHeight = '64px' +const iconWidth = '55px' + +export const LocationTypeDropdown = styled(DropdownButton)` + border-right: none; + height: ${fieldHeight}; + width: ${iconWidth}; +` +export const FixedLocationType = styled(InputGroup.Addon)` + background: transparent; + border-right: none; + max-width: ${iconWidth}; + width: ${iconWidth}; +` +const LocationNameWrapper = styled.span` + box-sizing: border-box; + display: block; + padding: 0 0px; + width: 100%; ` -const StyledAddon = styled(InputGroup.Addon)` - width: 40px; +const LocationNameInput = styled(FormControl)` + border: none; + border-bottom: 1px solid #eee; + box-shadow: none; + font-weight: bold; + height: inherit; + padding: 4px 0; + width: 100%; +` +const FixedLocationName = styled(ControlLabel)` + border-bottom: 1px solid #eee; + display: block; + margin: 0; + padding: 4px 0; +` +const DeleteWrapper = styled(InputGroup.Button)` + height: ${fieldHeight}; + & > button { + border-left: none; + height: 100%; + } ` /** * Creates a new favorite location object from a location returned by LocationField, with the specified type and icon. */ -export function makeFavoriteLocation (baseLocation, type, icon) { +export function makeFavoriteLocation (baseLocation, type, icon, nickname) { const { lat, lon, name } = baseLocation return { address: name, icon, lat, lon, - name, + name: nickname, type } } +/** + * Create a LocationField location object from a persisted user location object. + */ +function makeLocationFieldLocation (favoriteLocation) { + const { address, lat, lon } = favoriteLocation + return { + lat, + lon, + name: address + } +} + /** * Renders a user favorite location, * and lets the user edit the details (address, type, TODO: nickname) of the location. @@ -51,13 +100,27 @@ class FavoriteLocation extends Component { _handleLocationChange = ({ location }) => { const { arrayHelpers, index, location: oldLocation } = this.props - const { icon, type } = oldLocation - arrayHelpers.replace(index, makeFavoriteLocation(location, type, icon)) + const { icon, name, type } = oldLocation + arrayHelpers.replace(index, makeFavoriteLocation(location, type, icon, name)) + } + + _handleLocationNameChange = e => { + const { arrayHelpers, index, location } = this.props + const newLocation = { + ...location, + name: e.target.value + } + arrayHelpers.replace(index, newLocation) } _handleLocationTypeChange = ({ icon, type }) => { const { arrayHelpers, index, location } = this.props - arrayHelpers.replace(index, makeFavoriteLocation(location, type, icon)) + const newLocation = { + ...location, + icon, + type + } + arrayHelpers.replace(index, newLocation) } render () { @@ -69,28 +132,44 @@ class FavoriteLocation extends Component { // The dropdown has a predefined list of items for location types. const iconControl = placeholder ? ( - - - + + + ) : ( ) + const locationNameContents = placeholder + ? ( + + {location.name} + + ) + : ( + + ) return ( {iconControl} {/* wrapper with z-index override needed for showing location menu on top of other controls. */} - + + + {locationNameContents} + + - + ) diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations-pane.js index d26b2cd40..1de7355e5 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations-pane.js @@ -9,13 +9,13 @@ import NewFavoriteLocation from './new-favorite-location' export const BLANK_HOME = { address: '', icon: 'home', - name: '', + name: 'Home', type: 'home' } export const BLANK_WORK = { address: '', icon: 'briefcase', - name: '', + name: 'Work', type: 'work' } From b5b3739a2a63157c758ea734c3e8507793f94cfc Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 11 Dec 2020 16:11:03 -0500 Subject: [PATCH 024/265] refactor(FixedFavoriteLocation): Extract FixedFavoriteLocation. Move shared code around. --- .../user/favorite-location-controls.js | 112 ++++++++++++++- lib/components/user/favorite-location.js | 135 ++++-------------- .../user/favorite-locations-pane.js | 43 ++---- .../user/fixed-favorite-location.js | 70 +++++++++ lib/components/user/new-favorite-location.js | 51 +++---- lib/components/user/user-account-screen.js | 16 ++- lib/util/user.js | 1 + 7 files changed, 253 insertions(+), 175 deletions(-) create mode 100644 lib/components/user/fixed-favorite-location.js diff --git a/lib/components/user/favorite-location-controls.js b/lib/components/user/favorite-location-controls.js index 1362bcbcf..4b9c970fe 100644 --- a/lib/components/user/favorite-location-controls.js +++ b/lib/components/user/favorite-location-controls.js @@ -5,13 +5,19 @@ import { InputGroup as LocationFieldInputGroup } from '@opentripplanner/location-field/lib/styled' import React from 'react' -import { InputGroup, MenuItem } from 'react-bootstrap' -import styled from 'styled-components' +import { DropdownButton, InputGroup, MenuItem } from 'react-bootstrap' +import styled, { css } from 'styled-components' import connectLocationField from '../form/connect-location-field' import Icon from '../narrative/icon' -// Styles +/** + * This module contains components common to the favorite location components. + */ + +export const FIELD_HEIGHT_PX = '64px' +export const ICON_WIDTH_PX = '55px' + const customLocationType = { icon: 'map-marker', text: 'Custom', @@ -28,25 +34,91 @@ const locationTypes = [ customLocationType ] +const locationTypeCss = css` + background: transparent; + border-right: none; + height: ${FIELD_HEIGHT_PX}; + width: ${ICON_WIDTH_PX}; +` + +/** + * A styled input group addon showing the location type icon. + */ +export const FixedLocationType = styled(InputGroup.Addon)` + ${locationTypeCss} +` + +/** + * A styled input group dropdown showing the location type icon. + */ +const LocationTypeDropdown = styled(DropdownButton)` + ${locationTypeCss} +` + +/** + * Wrapper for styling around location editing components. + */ +export const LocationNameWrapper = styled.span` + box-sizing: border-box; + display: block; + padding: 0 0px; + width: 100%; +` + +/** + * An invisible component added to form groups to force the + * middle form-control element to occupy ther full remaining width. + */ +export const InvisibleAddon = styled(InputGroup.Addon)` + background: none; +` + +/** + * Creates a new favorite location object from a location returned by LocationField, with the specified type and icon. + */ +export function makeFavoriteLocation (baseLocation, type, icon, nickname) { + const { lat, lon, name } = baseLocation + return { + address: name, + icon, + lat, + lon, + name: nickname, + type + } +} + +/** + * Create a LocationField location object from a persisted user location object. + */ +export function makeLocationFieldLocation (favoriteLocation) { + const { address, lat, lon } = favoriteLocation + return { + lat, + lon, + name: address + } +} + /** * Displays a dropdown for selecting one of multiple location types. */ -export const FavoriteLocationTypeDropdown = ({ DropdownButtonComponent, id, onChange, selectedType }) => { +export const FavoriteLocationTypeDropdown = ({ id, onChange, selectedType }) => { // Fall back to the 'custom' icon if the desired type is not found. const locationType = locationTypes.find(t => t.type === selectedType) || customLocationType return ( - } + title={} > {locationTypes.map((t, index) => ( {t.text} ))} - + ) } @@ -71,4 +143,30 @@ const StyledLocationField = styled(LocationField)` width: 100%; } ` +/** + * Styled LocationField for picking favorite locations using the geocoder. + */ export const FavoriteLocationField = connectLocationField(StyledLocationField) + +/** + * A wrapper of Bootstrap's class 'form-control' to wrap multiple components that display location info, + * with z-index override needed for showing the location suggestions menu on top of other components. + */ +export const LocationWrapper = ({children, style, ...props}) => ( + + {children} + +) diff --git a/lib/components/user/favorite-location.js b/lib/components/user/favorite-location.js index c935a2f7c..b0a333b13 100644 --- a/lib/components/user/favorite-location.js +++ b/lib/components/user/favorite-location.js @@ -1,8 +1,6 @@ import React, { Component } from 'react' import { Button, - ControlLabel, - DropdownButton, FormControl, FormGroup, Glyphicon, @@ -10,30 +8,17 @@ import { } from 'react-bootstrap' import styled from 'styled-components' -import Icon from '../narrative/icon' -import { FavoriteLocationField, FavoriteLocationTypeDropdown } from './favorite-location-controls' +import { + FavoriteLocationField, + FavoriteLocationTypeDropdown, + FIELD_HEIGHT_PX, + LocationNameWrapper, + LocationWrapper, + makeFavoriteLocation, + makeLocationFieldLocation +} from './favorite-location-controls' // Styles for other controls -const fieldHeight = '64px' -const iconWidth = '55px' - -export const LocationTypeDropdown = styled(DropdownButton)` - border-right: none; - height: ${fieldHeight}; - width: ${iconWidth}; -` -export const FixedLocationType = styled(InputGroup.Addon)` - background: transparent; - border-right: none; - max-width: ${iconWidth}; - width: ${iconWidth}; -` -const LocationNameWrapper = styled.span` - box-sizing: border-box; - display: block; - padding: 0 0px; - width: 100%; -` const LocationNameInput = styled(FormControl)` border: none; border-bottom: 1px solid #eee; @@ -43,59 +28,22 @@ const LocationNameInput = styled(FormControl)` padding: 4px 0; width: 100%; ` -const FixedLocationName = styled(ControlLabel)` - border-bottom: 1px solid #eee; - display: block; - margin: 0; - padding: 4px 0; -` const DeleteWrapper = styled(InputGroup.Button)` - height: ${fieldHeight}; + height: ${FIELD_HEIGHT_PX}; & > button { border-left: none; height: 100%; } ` -/** - * Creates a new favorite location object from a location returned by LocationField, with the specified type and icon. - */ -export function makeFavoriteLocation (baseLocation, type, icon, nickname) { - const { lat, lon, name } = baseLocation - return { - address: name, - icon, - lat, - lon, - name: nickname, - type - } -} - -/** - * Create a LocationField location object from a persisted user location object. - */ -function makeLocationFieldLocation (favoriteLocation) { - const { address, lat, lon } = favoriteLocation - return { - lat, - lon, - name: address - } -} - /** * Renders a user favorite location, - * and lets the user edit the details (address, type, TODO: nickname) of the location. + * and lets the user edit the details (address, type, nickname) of the location. */ class FavoriteLocation extends Component { _handleDelete = () => { - const { arrayHelpers, index, placeholder } = this.props - if (placeholder) { - this._handleLocationChange({ location: placeholder }) - } else { - arrayHelpers.remove(index) - } + const { arrayHelpers, index } = this.props + arrayHelpers.remove(index) } _handleLocationChange = ({ location }) => { @@ -124,61 +72,34 @@ class FavoriteLocation extends Component { } render () { - const { index, location, placeholder } = this.props - const placeholderText = placeholder ? `Add ${location.type}` : 'Enter a favorite address' - - // Show a dropdown for editing the icon, shown unless a placeholder is set, - // in which case the type is considered fixed (e.g. for 'home' and 'work' locations). - // The dropdown has a predefined list of items for location types. - const iconControl = placeholder - ? ( - - - - ) - : ( - - ) - const locationNameContents = placeholder - ? ( - - {location.name} - - ) - : ( - - ) + const { index, location } = this.props return ( - {iconControl} - {/* wrapper with z-index override needed for showing location menu on top of other controls. */} - + + + - {locationNameContents} + - + - {/* FIXME: hide the delete button for 'home' and 'work' after user clicks it - (there are challenging layouts). - */} +
    + : null + } + + ) + } + return (
    {/* Header Block */} @@ -291,64 +352,7 @@ class StopViewer extends Component { {stopData && (
    {this._renderControls()} - {hasStopTimesAndRoutes - ? <> -
    - {Object.values(stopTimesByPattern) - .sort(patternComparator) - .map(patternTimes => { - // Only add pattern row if route is found. - // FIXME: there is currently a bug with the alernative transit index - // where routes are not associated with the stop if the only stoptimes - // for the stop are drop off only. See https://github.com/ibi-group/trimet-mod-otp/issues/217 - if (!patternTimes.route) { - console.warn(`Cannot render stop times for missing route ID: ${getRouteIdForPattern(patternTimes.pattern)}`) - return null - } - return ( - - ) - }) - } -
    - {!scheduleView - // If showing next arrivals, include auto update controls. - ?
    - - -
    - : null - } - - :
    No stop times found for date.
    - } + {contents} {/* Future: add stop details */}
    )} From d7b82f1b2c8d36d3aeaff7e591c1604d3319a34b Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 15 Dec 2020 14:48:49 -0500 Subject: [PATCH 028/265] refactor(util/viewer): Add args to not use icon/countdown in stop time. Non-breaking change (adds args to getFormattedStopTime signature) --- lib/util/viewer.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/util/viewer.js b/lib/util/viewer.js index c1f8d1aaf..9e3bf1fe4 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -24,7 +24,7 @@ const ONE_DAY_IN_SECONDS = 86400 * @param {string} timeFormat A valid moment.js formatting string * @param {boolean} useSchedule Whether to use scheduled departure (otherwise uses realtime) */ -export function getFormattedStopTime (stopTime, homeTimezone, soonText = 'Due', timeFormat, useSchedule = false) { +export function getFormattedStopTime (stopTime, homeTimezone, soonText = 'Due', timeFormat, useSchedule = false, showIcon = true, useCountdown = true) { const departureTime = useSchedule ? stopTime.scheduledDeparture : stopTime.realtimeDeparture @@ -48,7 +48,7 @@ export function getFormattedStopTime (stopTime, homeTimezone, soonText = 'Due', // the week when showing arrival time/day. const departsInFuture = secondsUntilDeparture > 0 // Show the exact time if the departure happens within an hour. - const showCountdown = secondsUntilDeparture < ONE_HOUR_IN_SECONDS && departsInFuture + const showCountdown = useCountdown && secondsUntilDeparture < ONE_HOUR_IN_SECONDS && departsInFuture // Use "soon text" (e.g., Due) if vehicle is approaching. const countdownString = secondsUntilDeparture < 60 @@ -67,16 +67,18 @@ export function getFormattedStopTime (stopTime, homeTimezone, soonText = 'Due', const showDayOfWeek = !vehicleDepartsToday && !showCountdown return (
    -
    - {stopTime.realtimeState === 'UPDATED' - ? - : - } -
    + {showIcon && ( +
    + {stopTime.realtimeState === 'UPDATED' + ? + : + } +
    + )}
    {showDayOfWeek &&
    {departureDay.format('dddd')}
    From eb5feeee3781b8a197ee352a869ec3902b69dc8d Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 15 Dec 2020 15:54:38 -0500 Subject: [PATCH 029/265] fix(util/viewer): Extract headsign if stop times don't have them. --- lib/components/viewers/stop-schedule-table.js | 32 +++++++++++-------- lib/util/viewer.js | 13 +++++++- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 187b388d0..89ac48634 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -3,6 +3,7 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import styled from 'styled-components' +import { isBlank } from '../../util/ui' import { getFormattedStopTime, getStopTimesByPattern } from '../../util/viewer' const { getTimeFormat } = coreUtils.time @@ -25,13 +26,16 @@ const StyledTable = styled.table` } ` -const RouteTd = styled.td` +const DestHeader = styled.th` + width: 100%; +` +const RouteCell = styled.td` font-weight: bold; ` -const DestTd = styled.td` +const DestCell = styled.td` width: 100%; ` -const TimeTd = styled.td` +const TimeCell = styled.td` font-weight: bold; text-align: right; white-space: nowrap; @@ -50,17 +54,19 @@ class StopScheduleTable extends Component { // Merge stop times, so that we can sort them across all route patterns. let mergedStopTimes = [] - Object.values(stopTimesByPattern).forEach(pattern => { + Object.values(stopTimesByPattern).forEach(pattern => { //TODO:breakup pattern vars const filteredTimes = pattern.times //TODO refactor - Copied from util/viewers .filter(stopTime => { return stopTime.stopIndex < stopTime.stopCount - 1 // ensure that this isn't the last stop }) .map(stopTime => { - // Add a route attribute to each stop time for rendering route info. + // Add the route attribute and headsign to each stop time for rendering route info. + const headsign = isBlank(stopTime.headsign) ? pattern.pattern.headsign : stopTime.headsign return { ...stopTime, - route: pattern.route + route: pattern.route, + headsign } }) mergedStopTimes = mergedStopTimes.concat(filteredTimes) // reduce? @@ -79,21 +85,21 @@ class StopScheduleTable extends Component { Block Route - To + To Departure - {mergedStopTimes.map(stopTime => { + {mergedStopTimes.map((stopTime, index) => { const { blockId, headsign, route } = stopTime const routeName = route.shortName ? route.shortName : route.longName - const time = getFormattedStopTime(stopTime, homeTimezone, stopViewerArriving, timeFormat, true) + const time = getFormattedStopTime(stopTime, homeTimezone, stopViewerArriving, timeFormat, true, false, false) return ( - + {blockId} - {routeName} - {headsign} - {time} + {routeName} + {headsign} + {time} ) })} diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 9e3bf1fe4..af5d80e08 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -4,6 +4,7 @@ import 'moment-timezone' import React from 'react' import Icon from '../components/narrative/icon' +import { isBlank } from './ui' const { formatDuration, @@ -135,8 +136,18 @@ export function getStopTimesByPattern (stopData) { if (stopData && stopData.routes && stopData.stopTimes) { stopData.stopTimes.forEach(patternTimes => { const routeId = getRouteIdForPattern(patternTimes.pattern) - const headsign = patternTimes.times[0] && patternTimes.times[0].headsign + + let headsign = patternTimes.times[0] && patternTimes.times[0].headsign + // In case stop time headsign is blank, extract headsign from the pattern 'desc' attribute + // (format: '49 to ()[ from ( r.id === routeId) From 99e60d2943ca4eadaa69b9b4aee580be658e5b6e Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 15 Dec 2020 16:02:49 -0500 Subject: [PATCH 030/265] fix(StopViewer): Enlarge date picker so date is not clipped in Chrome. --- lib/components/viewers/stop-viewer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 36f4bc74b..0cb88bc88 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -235,7 +235,7 @@ class StopViewer extends Component { type='date' value={this.state.date} style={{ - width: '115px', + width: '125px', border: 'none', outline: 'none' }} From 0e5496df3a9e2c7210d2a44ebf23634f902b87dc Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 15 Dec 2020 17:32:35 -0500 Subject: [PATCH 031/265] refactor(StopScheduleTable): Scroll to the first departure from now. --- lib/components/viewers/stop-schedule-table.js | 42 +++++++++++++++++-- lib/util/viewer.js | 14 ++++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 89ac48634..4c5767c62 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -1,10 +1,10 @@ import coreUtils from '@opentripplanner/core-utils' -import React, { Component } from 'react' +import React, { Component, createRef } from 'react' import { connect } from 'react-redux' import styled from 'styled-components' import { isBlank } from '../../util/ui' -import { getFormattedStopTime, getStopTimesByPattern } from '../../util/viewer' +import { getFormattedStopTime, getSecondsUntilDeparture, getStopTimesByPattern } from '../../util/viewer' const { getTimeFormat } = coreUtils.time @@ -42,6 +42,23 @@ const TimeCell = styled.td` ` class StopScheduleTable extends Component { + firstDepartureRef = createRef() + + /* + * Scroll to the first stop time that is departing from now. + */ + _scrollToFirstDeparture = () => { + this.firstDepartureRef.current.scrollIntoView() + } + + componentDidMount () { + this._scrollToFirstDeparture() + } + + componentDidUpdate () { + this._scrollToFirstDeparture() + } + render () { const { homeTimezone, stopData, stopViewerArriving, timeFormat } = this.props const hasStopTimesAndRoutes = !!(stopData && stopData.stopTimes && stopData.stopTimes.length > 0 && stopData.routes) @@ -79,6 +96,22 @@ class StopScheduleTable extends Component { return aTime - bTime }) + // Find the next stop time that is departing. We will scroll to that stop time entry. + let firstDepartureFromNow + if (mergedStopTimes.length) { + // Search closest departure starting from the last stop time for this pattern. + const lastStopTime = mergedStopTimes[mergedStopTimes.length - 1] + + firstDepartureFromNow = mergedStopTimes.reduce((firstStopTime, stopTime) => { + const firstStopTimeSeconds = getSecondsUntilDeparture(firstStopTime, true) + const stopTimeSeconds = getSecondsUntilDeparture(stopTime, true) + + return stopTimeSeconds < firstStopTimeSeconds && stopTimeSeconds >= 0 + ? stopTime + : firstStopTime + }, lastStopTime) + } + return ( @@ -94,8 +127,11 @@ class StopScheduleTable extends Component { const { blockId, headsign, route } = stopTime const routeName = route.shortName ? route.shortName : route.longName const time = getFormattedStopTime(stopTime, homeTimezone, stopViewerArriving, timeFormat, true, false, false) + // Add ref to scroll to the first stop time departing from now. + const refProp = stopTime === firstDepartureFromNow ? this.firstDepartureRef : null + return ( - + {blockId} {routeName} {headsign} diff --git a/lib/util/viewer.js b/lib/util/viewer.js index af5d80e08..505466eeb 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -15,6 +15,18 @@ const { const ONE_HOUR_IN_SECONDS = 3600 const ONE_DAY_IN_SECONDS = 86400 +/** + * Computes the seconds until departure for a given stop time, + * based either on the scheduled or the realtime departure time. + */ +export function getSecondsUntilDeparture (stopTime, useSchedule) { + const departureTime = useSchedule + ? stopTime.scheduledDeparture + : stopTime.realtimeDeparture + + return (departureTime + stopTime.serviceDay) - (Date.now() / 1000) +} + /** * Helper method to generate stop time w/ status icon * @@ -44,7 +56,7 @@ export function getFormattedStopTime (stopTime, homeTimezone, soonText = 'Due', const vehicleDepartsToday = now.dayOfYear() === departureDay.dayOfYear() // Determine whether to show departure as countdown (e.g. "5 min") or as HH:mm // time. - const secondsUntilDeparture = (departureTime + stopTime.serviceDay) - now.unix() + const secondsUntilDeparture = getSecondsUntilDeparture(stopTime, useSchedule) //(departureTime + stopTime.serviceDay) - now.unix() // Determine if vehicle arrives after midnight in order to advance the day of // the week when showing arrival time/day. const departsInFuture = secondsUntilDeparture > 0 From c96f86ab3fd23b667ae896149344da75f4f48051 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 15 Dec 2020 17:58:03 -0500 Subject: [PATCH 032/265] refactor: Refactor various files. --- lib/components/viewers/pattern-row.js | 11 +++---- lib/components/viewers/stop-schedule-table.js | 32 +++++++++---------- lib/components/viewers/stop-viewer.js | 4 +-- lib/util/viewer.js | 22 ++++++++++--- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/lib/components/viewers/pattern-row.js b/lib/components/viewers/pattern-row.js index c1c325651..90aadd809 100644 --- a/lib/components/viewers/pattern-row.js +++ b/lib/components/viewers/pattern-row.js @@ -2,7 +2,7 @@ import React, { Component } from 'react' import { VelocityTransitionGroup } from 'velocity-react' import Icon from '../narrative/icon' -import { getFormattedStopTime, getStatusLabel } from '../../util/viewer' +import { getFormattedStopTime, getStatusLabel, stopTimeComparator } from '../../util/viewer' /** * Represents a single pattern row for displaying arrival times in the stop @@ -34,11 +34,7 @@ export default class PatternRow extends Component { if (hasStopTimes) { sortedStopTimes = stopTimes .concat() - .sort((a, b) => { - const aTime = a.serviceDay + a.realtimeDeparture - const bTime = b.serviceDay + b.realtimeDeparture - return aTime - bTime - }) + .sort(stopTimeComparator) // We request only x departures per pattern, but the patterns are merged // according to shared headsigns, so we need to slice the stop times // here as well to ensure only x times are shown per route/headsign combo. @@ -123,6 +119,7 @@ export default class PatternRow extends Component { ) } + // FIXME: remove _renderScheduleView = () => { const { pattern, @@ -148,7 +145,7 @@ export default class PatternRow extends Component { // according to shared headsigns, so we need to slice the stop times // here as well to ensure only x times are shown per route/headsign combo. // This is applied after the sort, so we're keeping the soonest departures. - //.slice(0, stopViewerConfig.numberOfDepartures) + .slice(0, stopViewerConfig.numberOfDepartures) } else { // Do not include pattern row if it has no stop times. return null diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 4c5767c62..88185d326 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -4,7 +4,13 @@ import { connect } from 'react-redux' import styled from 'styled-components' import { isBlank } from '../../util/ui' -import { getFormattedStopTime, getSecondsUntilDeparture, getStopTimesByPattern } from '../../util/viewer' +import { + excludeLastStop, + getFormattedStopTime, + getSecondsUntilDeparture, + getStopTimesByPattern, + stopTimeComparator +} from '../../util/viewer' const { getTimeFormat } = coreUtils.time @@ -71,35 +77,27 @@ class StopScheduleTable extends Component { // Merge stop times, so that we can sort them across all route patterns. let mergedStopTimes = [] - Object.values(stopTimesByPattern).forEach(pattern => { //TODO:breakup pattern vars - const filteredTimes = pattern.times - //TODO refactor - Copied from util/viewers - .filter(stopTime => { - return stopTime.stopIndex < stopTime.stopCount - 1 // ensure that this isn't the last stop - }) + Object.values(stopTimesByPattern).forEach(({ pattern, route, times }) => { + const filteredTimes = times + .filter(excludeLastStop) .map(stopTime => { // Add the route attribute and headsign to each stop time for rendering route info. - const headsign = isBlank(stopTime.headsign) ? pattern.pattern.headsign : stopTime.headsign + const headsign = isBlank(stopTime.headsign) ? pattern.headsign : stopTime.headsign return { ...stopTime, - route: pattern.route, + route, headsign } }) - mergedStopTimes = mergedStopTimes.concat(filteredTimes) // reduce? + mergedStopTimes = mergedStopTimes.concat(filteredTimes) }) - //TODO Refactor - Copied from pattern-row - mergedStopTimes = mergedStopTimes.sort((a, b) => { - const aTime = a.serviceDay + a.scheduledDeparture - const bTime = b.serviceDay + b.scheduledDeparture - return aTime - bTime - }) + mergedStopTimes = mergedStopTimes.sort(stopTimeComparator) // Find the next stop time that is departing. We will scroll to that stop time entry. let firstDepartureFromNow if (mergedStopTimes.length) { - // Search closest departure starting from the last stop time for this pattern. + // Search starting from the last stop time (largest seconds until departure) for this pattern. const lastStopTime = mergedStopTimes[mergedStopTimes.length - 1] firstDepartureFromNow = mergedStopTimes.reduce((firstStopTime, stopTime) => { diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 0cb88bc88..fd6317eee 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -107,9 +107,7 @@ class StopViewer extends Component { findStopTimesForStop({ date, - stopId: viewedStop.stopId//, - // startTime: this._getStartTimeForDate(date), - // timeRange: 86400 + stopId: viewedStop.stopId }) } diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 505466eeb..7b7cca913 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -56,7 +56,7 @@ export function getFormattedStopTime (stopTime, homeTimezone, soonText = 'Due', const vehicleDepartsToday = now.dayOfYear() === departureDay.dayOfYear() // Determine whether to show departure as countdown (e.g. "5 min") or as HH:mm // time. - const secondsUntilDeparture = getSecondsUntilDeparture(stopTime, useSchedule) //(departureTime + stopTime.serviceDay) - now.unix() + const secondsUntilDeparture = getSecondsUntilDeparture(stopTime, useSchedule) // Determine if vehicle arrives after midnight in order to advance the day of // the week when showing arrival time/day. const departsInFuture = secondsUntilDeparture > 0 @@ -143,6 +143,13 @@ export function getStatusLabel (delay) { ) } +/** + * @returns true if the given stopTime does not correspond to the last stop visited by a pattern. + */ +export function excludeLastStop ({ stopIndex, stopCount }) { + return stopIndex < stopCount - 1 +} + export function getStopTimesByPattern (stopData) { const stopTimesByPattern = {} if (stopData && stopData.routes && stopData.stopTimes) { @@ -179,9 +186,7 @@ export function getStopTimesByPattern (stopData) { times: [] } } - const filteredTimes = patternTimes.times.filter(stopTime => { - return stopTime.stopIndex < stopTime.stopCount - 1 // ensure that this isn't the last stop - }) + const filteredTimes = patternTimes.times.filter(excludeLastStop) stopTimesByPattern[id].times = stopTimesByPattern[id].times.concat(filteredTimes) }) } @@ -210,3 +215,12 @@ export function getModeFromRoute (route) { } return route.mode || modeLookup[route.type] } + +/** + * Compares departure times of two stops. + */ +export function stopTimeComparator (a, b) { + const aTime = a.serviceDay + a.scheduledDeparture + const bTime = b.serviceDay + b.scheduledDeparture + return aTime - bTime +} From 29e7d901e89cb2cd64636a4451c4679f9fe61033 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 15 Dec 2020 19:13:32 -0500 Subject: [PATCH 033/265] refactor: Tweak layout, make tests pass. --- lib/actions/api.js | 4 ++-- lib/components/viewers/stop-schedule-table.js | 15 ++++++--------- lib/util/viewer.js | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index af4114070..a21c90973 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -9,7 +9,7 @@ import { createAction } from 'redux-actions' import qs from 'qs' import { rememberPlace } from './map' -import { getStopViewerConfig, queryIsValid } from '../util/state' +import { queryIsValid } from '../util/state' import { getSecureFetchOptions } from '../util/middleware' if (typeof (fetch) === 'undefined') require('isomorphic-fetch') @@ -557,7 +557,7 @@ export function findStopTimesForStop (params) { // If other params not provided, fall back on defaults from stop viewer config. // const queryParams = { ...getStopViewerConfig(getState().otp), ...otherParams } - //TODO: parform an additional query with ^^ and without date path if the one below returns no stop times at all. + // TODO: parform an additional query with ^^ and without date path if the one below returns no stop times at all. const queryParams = {...otherParams} // If no start time is provided and no date is provided in params, diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 88185d326..714dfd97b 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -32,15 +32,9 @@ const StyledTable = styled.table` } ` -const DestHeader = styled.th` - width: 100%; -` const RouteCell = styled.td` font-weight: bold; ` -const DestCell = styled.td` - width: 100%; -` const TimeCell = styled.td` font-weight: bold; text-align: right; @@ -54,7 +48,10 @@ class StopScheduleTable extends Component { * Scroll to the first stop time that is departing from now. */ _scrollToFirstDeparture = () => { - this.firstDepartureRef.current.scrollIntoView() + const { current } = this.firstDepartureRef + if (current) { + current.scrollIntoView() + } } componentDidMount () { @@ -116,7 +113,7 @@ class StopScheduleTable extends Component { Block Route - To + To Departure @@ -132,7 +129,7 @@ class StopScheduleTable extends Component { {blockId} {routeName} - {headsign} + {headsign} {time} ) diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 7b7cca913..be14bab3d 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -92,7 +92,7 @@ export function getFormattedStopTime (stopTime, homeTimezone, soonText = 'Due', }
    )} -
    +
    {showDayOfWeek &&
    {departureDay.format('dddd')}
    } From 4a33ffd52772c06d4302edbdd09c52c9766a1bae Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 16 Dec 2020 10:11:07 -0500 Subject: [PATCH 034/265] fix(UserAccountScreen): Fix blank home/work place labels on new accounts --- lib/components/user/user-account-screen.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.js index d5f560119..01235007d 100644 --- a/lib/components/user/user-account-screen.js +++ b/lib/components/user/user-account-screen.js @@ -36,13 +36,13 @@ const validationSchema = yup.object({ const BLANK_HOME = { address: '', icon: 'home', - name: '', + name: 'Home', type: 'home' } const BLANK_WORK = { address: '', icon: 'briefcase', - name: '', + name: 'Work', type: 'work' } From c7e23684606c801429e3ee9b12499f81641293a7 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 16 Dec 2020 12:14:45 -0500 Subject: [PATCH 035/265] refactor(StopTimeCell): Extract component, reduce render complexity. --- lib/components/viewers/pattern-row.js | 26 +++- lib/components/viewers/stop-schedule-table.js | 15 +- lib/components/viewers/stop-time-cell.js | 146 ++++++++++++++++++ lib/util/viewer.js | 94 +---------- 4 files changed, 180 insertions(+), 101 deletions(-) create mode 100644 lib/components/viewers/stop-time-cell.js diff --git a/lib/components/viewers/pattern-row.js b/lib/components/viewers/pattern-row.js index 90aadd809..86d6956bb 100644 --- a/lib/components/viewers/pattern-row.js +++ b/lib/components/viewers/pattern-row.js @@ -2,7 +2,8 @@ import React, { Component } from 'react' import { VelocityTransitionGroup } from 'velocity-react' import Icon from '../narrative/icon' -import { getFormattedStopTime, getStatusLabel, stopTimeComparator } from '../../util/viewer' +import { getStatusLabel, stopTimeComparator } from '../../util/viewer' +import StopTimeCell from './stop-time-cell' /** * Represents a single pattern row for displaying arrival times in the stop @@ -57,7 +58,12 @@ export default class PatternRow extends Component { {/* next departure preview */} {hasStopTimes && (
    - {getFormattedStopTime(sortedStopTimes[0], homeTimezone, stopViewerArriving, timeFormat)} +
    )} @@ -95,7 +101,12 @@ export default class PatternRow extends Component { To {stopTime.headsign}
    - {getFormattedStopTime(stopTime, homeTimezone, stopViewerArriving, timeFormat)} +
    {stopTime.realtimeState === 'UPDATED' @@ -173,7 +184,6 @@ export default class PatternRow extends Component { {hasStopTimes && ( sortedStopTimes.map((stopTime, i) => { // Get formatted scheduled departure time. - const time = getFormattedStopTime(stopTime, homeTimezone, stopViewerArriving, timeFormat, true) return (
    - {time} +
    ) diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 714dfd97b..7177b332c 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -6,11 +6,11 @@ import styled from 'styled-components' import { isBlank } from '../../util/ui' import { excludeLastStop, - getFormattedStopTime, getSecondsUntilDeparture, getStopTimesByPattern, stopTimeComparator } from '../../util/viewer' +import StopTimeCell from './stop-time-cell' const { getTimeFormat } = coreUtils.time @@ -121,7 +121,6 @@ class StopScheduleTable extends Component { {mergedStopTimes.map((stopTime, index) => { const { blockId, headsign, route } = stopTime const routeName = route.shortName ? route.shortName : route.longName - const time = getFormattedStopTime(stopTime, homeTimezone, stopViewerArriving, timeFormat, true, false, false) // Add ref to scroll to the first stop time departing from now. const refProp = stopTime === firstDepartureFromNow ? this.firstDepartureRef : null @@ -130,7 +129,17 @@ class StopScheduleTable extends Component { {blockId} {routeName} {headsign} - {time} + + + ) })} diff --git a/lib/components/viewers/stop-time-cell.js b/lib/components/viewers/stop-time-cell.js new file mode 100644 index 000000000..fd16228ed --- /dev/null +++ b/lib/components/viewers/stop-time-cell.js @@ -0,0 +1,146 @@ +import coreUtils from '@opentripplanner/core-utils' +import moment from 'moment' +import 'moment-timezone' +import PropTypes from 'prop-types' +import React from 'react' + +import Icon from '../narrative/icon' +import { getSecondsUntilDeparture } from '../../util/viewer' + +const { + formatDuration, + formatSecondsAfterMidnight, + getUserTimezone +} = coreUtils.time + +const ONE_HOUR_IN_SECONDS = 3600 +const ONE_DAY_IN_SECONDS = 86400 + +/** + * Renders an icon based on the given stop time realtime state. + */ +function renderIcon (stopTime) { + const iconType = stopTime.realtimeState === 'UPDATED' ? 'rss' : 'clock-o' + return ( +
    + +
    + ) +} + +/** + * If the browser timezone is not the homeTimezone, appends a time zone designation (e.g., PDT) + * to designate times in the homeTimezone + * (e.g., user in New York, but viewing a trip planner for service based in Los Angeles). + */ +function getTimeFormat (timeFormat, homeTimezone) { + const userTimeZone = getUserTimezone() + const inHomeTimezone = homeTimezone && homeTimezone === userTimeZone + return inHomeTimezone ? timeFormat : `${timeFormat} z` +} + +/** + * Renders a stop time as either schedule or countdown, with an optional status icon. + * Stop time that apply to a different day have an additional text showing the day of departure. + */ +const StopTimeCell = ({ + homeTimezone, + showIcon, + soonText, + stopTime, + timeFormat, + useCountdown, + useSchedule +}) => { + const departureTime = useSchedule + ? stopTime.scheduledDeparture + : stopTime.realtimeDeparture + const now = moment().tz(homeTimezone) + const serviceDay = moment(stopTime.serviceDay * 1000).tz(homeTimezone) + + // Determine if arrival occurs on different day, making sure to account for + // any extra days added to the service day if it arrives after midnight. Note: + // this can handle the rare (and non-existent?) case where an arrival occurs + // 48:00 hours (or more) from the start of the service day. + const departureTimeRemainder = departureTime % ONE_DAY_IN_SECONDS + const daysAfterServiceDay = (departureTime - departureTimeRemainder) / ONE_DAY_IN_SECONDS + const departureDay = serviceDay.add(daysAfterServiceDay, 'day') + const vehicleDepartsToday = now.dayOfYear() === departureDay.dayOfYear() + + // Determine whether to show departure as countdown (e.g. "5 min") or as HH:mm + // time. + const secondsUntilDeparture = getSecondsUntilDeparture(stopTime, useSchedule) + // Determine if vehicle arrives after midnight in order to advance the day of + // the week when showing arrival time/day. + const departsInFuture = secondsUntilDeparture > 0 + // Show the exact time if the departure happens within an hour. + const showCountdown = useCountdown && secondsUntilDeparture < ONE_HOUR_IN_SECONDS && departsInFuture + + // Use "soon text" (e.g., Due) if vehicle is approaching. + const countdownString = secondsUntilDeparture < 60 + ? soonText + : formatDuration(secondsUntilDeparture) + const formattedTime = formatSecondsAfterMidnight( + departureTime, + // Only show timezone (e.g., PDT) if user is not in home time zone (e.g., user + // in New York, but viewing a trip planner for service based in Los Angeles). + getTimeFormat(timeFormat, homeTimezone) + ) + // We only want to show the day of the week if the arrival is on a + // different day and we're not showing the countdown string. This avoids + // cases such as when it's Wednesday at 11:55pm and an arrival occurs at + // Thursday 12:19am. We don't want the time to read: 'Thursday, 24 minutes'. + const showDayOfWeek = !vehicleDepartsToday && !showCountdown + return ( +
    + {showIcon && renderIcon(stopTime)} + +
    + {showDayOfWeek && +
    {departureDay.format('dddd')}
    + } +
    + {showCountdown + // Show countdown string (e.g., 3 min or Due) + ? countdownString + // Show formatted time (with timezone if user is not in home timezone) + : formattedTime + } +
    +
    +
    + ) +} + +StopTimeCell.propTypes = { + /** If configured, the timezone of the area */ + homeTimezone: PropTypes.any, + /** Whether to display an icon next to the departure time */ + showIcon: PropTypes.bool, + /** The text to display for imminent departure times */ + soonText: PropTypes.string, + /** A stopTime object as received from a transit index API */ + stopTime: PropTypes.any.isRequired, + /** A valid moment.js formatting string */ + timeFormat: PropTypes.string.isRequired, + /** + * Whether to display a countdown in minutes instead of the + * scheduled time for departures within an hour. + */ + useCountdown: PropTypes.bool, + /** Whether to use scheduled departure (otherwise uses realtime) */ + useSchedule: PropTypes.bool +} + +StopTimeCell.defaultProps = { + homeTimezone: null, + showIcon: true, + soonText: 'Due', + useCountdown: true, + useSchedule: false +} + +export default StopTimeCell diff --git a/lib/util/viewer.js b/lib/util/viewer.js index be14bab3d..6bdc91ce6 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -1,19 +1,9 @@ import coreUtils from '@opentripplanner/core-utils' -import moment from 'moment' -import 'moment-timezone' import React from 'react' -import Icon from '../components/narrative/icon' import { isBlank } from './ui' -const { - formatDuration, - formatSecondsAfterMidnight, - getUserTimezone -} = coreUtils.time - -const ONE_HOUR_IN_SECONDS = 3600 -const ONE_DAY_IN_SECONDS = 86400 +const { formatDuration } = coreUtils.time /** * Computes the seconds until departure for a given stop time, @@ -27,88 +17,6 @@ export function getSecondsUntilDeparture (stopTime, useSchedule) { return (departureTime + stopTime.serviceDay) - (Date.now() / 1000) } -/** - * Helper method to generate stop time w/ status icon - * - * @param {Object} stopTime A stopTime object as received from a transit index API - * @param {string} [homeTimezone] If configured, the timezone of the area - * @param {string} [soonText='Due'] The text to display for departure times - * about to depart in a short amount of time - * @param {string} timeFormat A valid moment.js formatting string - * @param {boolean} useSchedule Whether to use scheduled departure (otherwise uses realtime) - */ -export function getFormattedStopTime (stopTime, homeTimezone, soonText = 'Due', timeFormat, useSchedule = false, showIcon = true, useCountdown = true) { - const departureTime = useSchedule - ? stopTime.scheduledDeparture - : stopTime.realtimeDeparture - const userTimeZone = getUserTimezone() - const inHomeTimezone = homeTimezone && homeTimezone === userTimeZone - - const now = moment().tz(homeTimezone) - const serviceDay = moment(stopTime.serviceDay * 1000).tz(homeTimezone) - // Determine if arrival occurs on different day, making sure to account for - // any extra days added to the service day if it arrives after midnight. Note: - // this can handle the rare (and non-existent?) case where an arrival occurs - // 48:00 hours (or more) from the start of the service day. - const departureTimeRemainder = departureTime % ONE_DAY_IN_SECONDS - const daysAfterServiceDay = (departureTime - departureTimeRemainder) / ONE_DAY_IN_SECONDS - const departureDay = serviceDay.add(daysAfterServiceDay, 'day') - const vehicleDepartsToday = now.dayOfYear() === departureDay.dayOfYear() - // Determine whether to show departure as countdown (e.g. "5 min") or as HH:mm - // time. - const secondsUntilDeparture = getSecondsUntilDeparture(stopTime, useSchedule) - // Determine if vehicle arrives after midnight in order to advance the day of - // the week when showing arrival time/day. - const departsInFuture = secondsUntilDeparture > 0 - // Show the exact time if the departure happens within an hour. - const showCountdown = useCountdown && secondsUntilDeparture < ONE_HOUR_IN_SECONDS && departsInFuture - - // Use "soon text" (e.g., Due) if vehicle is approaching. - const countdownString = secondsUntilDeparture < 60 - ? soonText - : formatDuration(secondsUntilDeparture) - const formattedTime = formatSecondsAfterMidnight( - departureTime, - // Only show timezone (e.g., PDT) if user is not in home time zone (e.g., user - // in New York, but viewing a trip planner for service based in Los Angeles). - inHomeTimezone ? timeFormat : `${timeFormat} z` - ) - // We only want to show the day of the week if the arrival is on a - // different day and we're not showing the countdown string. This avoids - // cases such as when it's Wednesday at 11:55pm and an arrival occurs at - // Thursday 12:19am. We don't want the time to read: 'Thursday, 24 minutes'. - const showDayOfWeek = !vehicleDepartsToday && !showCountdown - return ( -
    - {showIcon && ( -
    - {stopTime.realtimeState === 'UPDATED' - ? - : - } -
    - )} -
    - {showDayOfWeek && -
    {departureDay.format('dddd')}
    - } -
    - {showCountdown - // Show countdown string (e.g., 3 min or Due) - ? countdownString - // Show formatted time (with timezone if user is not in home timezone) - : formattedTime - } -
    -
    -
    - ) -} - export function getRouteIdForPattern (pattern) { const patternIdParts = pattern.id.split(':') const routeId = patternIdParts[0] + ':' + patternIdParts[1] From 3787ba84d82b1cbc2bc3528c3f8ab19f9b6e1063 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 16 Dec 2020 12:26:36 -0500 Subject: [PATCH 036/265] refactor(UserAccountScreen): Force name for home and work locations. --- lib/components/user/user-account-screen.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.js index 01235007d..c8290fc3b 100644 --- a/lib/components/user/user-account-screen.js +++ b/lib/components/user/user-account-screen.js @@ -50,6 +50,7 @@ const BLANK_WORK = { * Makes a copy of the logged-in user data for the Formik initial state, with: * - the 'home' and 'work' locations at the top of the savedLocations list * so they are always shown and shown at the top of the FavoriteLocationsPane. + * The name field is set to 'Home' and 'Work' regardless of the value that was persisted. * Note: In the returned value, savedLocations is always a valid array. * - initial values for phone number/code fields used by Formik. */ @@ -59,6 +60,10 @@ function cloneForFormik (userData) { const homeLocation = savedLocations.find(isHome) || BLANK_HOME const workLocation = savedLocations.find(isWork) || BLANK_WORK + + homeLocation.name = BLANK_HOME.name + workLocation.name = BLANK_WORK.name + const reorderedLocations = [ homeLocation, workLocation, From 4feae74cd11075f67e6dd12a2f3fd12f055612f2 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 16 Dec 2020 12:32:31 -0500 Subject: [PATCH 037/265] test(StopViewer): Update snapshots --- .../viewers/__snapshots__/stop-viewer.js.snap | 792 +++++++++++------- 1 file changed, 512 insertions(+), 280 deletions(-) diff --git a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap index ae2ae0a98..c755c26ec 100644 --- a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap +++ b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap @@ -657,27 +657,43 @@ exports[`components > viewers > stop viewer should render countdown times after
    -
    -
    - +
    +
    - viewers > stop viewer should render countdown times after } type="clock-o" > - viewers > stop viewer should render countdown times after } } type="clock-o" - /> - - -
    -
    + + + +
    +
    -
    - 52 min + > +
    + 52 min +
    -
    +
    viewers > stop viewer should render countdown times for st
    -
    -
    - +
    +
    - viewers > stop viewer should render countdown times for st } type="clock-o" > - viewers > stop viewer should render countdown times for st } } type="clock-o" - /> - - -
    -
    + + + +
    +
    -
    - 52 min + > +
    + 52 min +
    -
    +
    viewers > stop viewer should render times after midnight w
    -
    -
    - +
    +
    - viewers > stop viewer should render times after midnight w } type="clock-o" > - viewers > stop viewer should render times after midnight w } } type="clock-o" - /> - - -
    -
    + > + + + +
    - Thursday -
    -
    - 00:51 +
    + Thursday +
    +
    + 00:51 +
    -
    +
    viewers > stop viewer should render with OTP transit index
    -
    -
    - +
    +
    - viewers > stop viewer should render with OTP transit index } type="clock-o" > - viewers > stop viewer should render with OTP transit index } } type="clock-o" - /> - - -
    -
    + > + + + +
    - Monday -
    -
    - 18:00 +
    + Monday +
    +
    + 18:00 +
    -
    +
    viewers > stop viewer should render with OTP transit index
    -
    -
    - +
    +
    - viewers > stop viewer should render with OTP transit index } type="clock-o" > - viewers > stop viewer should render with OTP transit index } } type="clock-o" - /> - - -
    -
    + > + + + +
    - Tuesday -
    -
    - 16:11 +
    + Tuesday +
    +
    + 16:11 +
    -
    +
    viewers > stop viewer should render with OTP transit index
    -
    -
    - +
    +
    - viewers > stop viewer should render with OTP transit index } type="clock-o" > - viewers > stop viewer should render with OTP transit index } } type="clock-o" - /> - - -
    -
    + > + + + +
    - Tuesday -
    -
    - 15:22 +
    + Tuesday +
    +
    + 15:22 +
    -
    +
    viewers > stop viewer should render with OTP transit index
    -
    -
    - +
    +
    - viewers > stop viewer should render with OTP transit index } type="clock-o" > - viewers > stop viewer should render with OTP transit index } } type="clock-o" - /> - - -
    -
    + > + + + +
    - Tuesday -
    -
    - 14:28 +
    + Tuesday +
    +
    + 14:28 +
    -
    +
    viewers > stop viewer should render with TriMet transit in
    -
    -
    - +
    +
    - viewers > stop viewer should render with TriMet transit in } type="clock-o" > - viewers > stop viewer should render with TriMet transit in } } type="clock-o" - /> - - -
    -
    + > + + + +
    - Monday -
    -
    - 17:45 +
    + Monday +
    +
    + 17:45 +
    -
    +
    Date: Wed, 16 Dec 2020 14:47:34 -0500 Subject: [PATCH 038/265] refactor(StopScheduleTable): Do not render times for pattern without route object. --- lib/components/viewers/stop-schedule-table.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 7177b332c..88e8a4ca9 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -6,6 +6,7 @@ import styled from 'styled-components' import { isBlank } from '../../util/ui' import { excludeLastStop, + getRouteIdForPattern, getSecondsUntilDeparture, getStopTimesByPattern, stopTimeComparator @@ -75,6 +76,16 @@ class StopScheduleTable extends Component { // Merge stop times, so that we can sort them across all route patterns. let mergedStopTimes = [] Object.values(stopTimesByPattern).forEach(({ pattern, route, times }) => { + // TODO: refactor the IF block below (copied fromn StopViewer). + // Only add pattern if route is found. + // FIXME: there is currently a bug with the alernative transit index + // where routes are not associated with the stop if the only stoptimes + // for the stop are drop off only. See https://github.com/ibi-group/trimet-mod-otp/issues/217 + if (!route) { + console.warn(`Cannot render stop times for missing route ID: ${getRouteIdForPattern(pattern)}`) + return + } + const filteredTimes = times .filter(excludeLastStop) .map(stopTime => { From 4d1967be6375c5b2f8d77618b42c34879ec32c80 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 16 Dec 2020 15:37:33 -0500 Subject: [PATCH 039/265] fix(viewers.css): Make scroll area start below header. This prevents overlap of the header content with the scrolling content (e.g. long route lists or long schedules). --- example.css | 6 +++++- lib/components/viewers/viewers.css | 15 ++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/example.css b/example.css index ab6a1ea42..108bba29f 100644 --- a/example.css +++ b/example.css @@ -30,10 +30,14 @@ padding: 0px 15px; } +/* Necessary for defining the height in the main panel nested elements. */ +.main-row main { + height: 100%; +} + .sidebar { height: 100%; padding: 10px; - overflow-y: scroll; box-shadow: 3px 0px 12px #00000052; z-index: 1000; } diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index 9233de9f2..bd8f17cdc 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -1,11 +1,15 @@ /* shared stop/trip viewer styles */ -.otp .stop-viewer-header, .otp .trip-viewer-header, .otp .route-viewer-header { +.otp .route-viewer-header, .otp .stop-viewer-header, .otp .trip-viewer-header { background-color: #ddd; padding: 12px; - position: sticky; /* Ensure header bar sticks to top of scrolling div */ - top: 0px; - z-index: 1; /* Ensure sticky header stays on top of viewer body */ +} + +.otp .stop-viewer, .otp .route-viewer, .otp .trip-viewer { + display: flex; + flex-direction: column; + flex-flow: column; + height: 100%; } /* Remove arrows on date input */ @@ -13,7 +17,8 @@ -webkit-appearance: none; } -.otp .stop-viewer-body, .otp .trip-viewer-body { +.otp .route-viewer-body, .otp .stop-viewer-body, .otp .trip-viewer-body { + overflow-y: auto; padding: 12px; } From 20d73b6a884f98fb3fa82701d14a2bf06584f3f1 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 16 Dec 2020 17:04:05 -0500 Subject: [PATCH 040/265] refactor(StopScheduleTable): Make table headers sticky. --- lib/components/viewers/stop-schedule-table.js | 14 +++++++------- lib/components/viewers/stop-viewer.js | 15 ++++++++++++--- lib/components/viewers/viewers.css | 3 ++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 88e8a4ca9..b6874fbf4 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -18,10 +18,14 @@ const { getTimeFormat } = coreUtils.time // Styles for the schedule table and its contents. const StyledTable = styled.table` border-spacing: collapse; - margin-top: 20px; + height: 100%; width: 100%; th { + background-color: #fff; + box-shadow: 0 1px 0px 0px #ccc; font-size: 75%; + position: sticky; + top: 0px; } tr > * { border-bottom: 1px solid #ccc; @@ -60,20 +64,16 @@ class StopScheduleTable extends Component { } componentDidUpdate () { + // Should only happen if user changes date and a new stopData is passed. this._scrollToFirstDeparture() } render () { const { homeTimezone, stopData, stopViewerArriving, timeFormat } = this.props - const hasStopTimesAndRoutes = !!(stopData && stopData.stopTimes && stopData.stopTimes.length > 0 && stopData.routes) - - if (!hasStopTimesAndRoutes) { - return
    No stop times found for date.
    - } - const stopTimesByPattern = getStopTimesByPattern(stopData) // Merge stop times, so that we can sort them across all route patterns. + // (stopData is assumed valid per StopViewer render condition.) let mergedStopTimes = [] Object.values(stopTimesByPattern).forEach(({ pattern, route, times }) => { // TODO: refactor the IF block below (copied fromn StopViewer). diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index fd6317eee..04f6719cb 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types' import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' +import styled from 'styled-components' import Icon from '../narrative/icon' import { setMainPanelContent, toggleAutoRefresh } from '../../actions/ui' @@ -27,6 +28,12 @@ const defaultState = { scheduleView: false } +// A scrollable container for the contents of the stop viewer body. +const Scrollable = styled.div` + height: 100%; + overflow-y: auto; +` + class StopViewer extends Component { state = defaultState @@ -212,7 +219,7 @@ class StopViewer extends Component { stopId = stopData.id.includes(':') ? stopData.id.split(':')[1] : stopData.id } return ( -
    +
    Stop ID: {stopId}
    - {!scheduleView - // If showing next arrivals, include auto update controls. - ?
    - + +
    } ) diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 6bdc91ce6..78099e3a4 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -52,28 +52,50 @@ export function getStatusLabel (delay) { } /** + * Filter predicate used, so that the stop viewer only shows departures from a stop + * (arrivals at a terminus stop are not shown in the stop viewer). * @returns true if the given stopTime does not correspond to the last stop visited by a pattern. */ -export function excludeLastStop ({ stopIndex, stopCount }) { - return stopIndex < stopCount - 1 +export function excludeLastStop (stopTime) { + return stopTime.stopIndex < stopTime.stopCount - 1 +} + +/** + * Checks that the given route object from an OTP pattern is valid. + * If it is not, logs a warning message. + * + * FIXME: there is currently a bug with the alernative transit index + * where routes are not associated with the stop if the only stoptimes + * for the stop are drop off only. See https://github.com/ibi-group/trimet-mod-otp/issues/217 + * + * @param {*} route The route of an OTP pattern to check. + * @param {*} routeId The route id to show for the specified route. + * @returns true if route is not null. + */ +export function routeIsValid (route, routeId) { + if (!route) { + console.warn(`Cannot render stop times for missing route ID: ${routeId}`) + return false + } + return true } export function getStopTimesByPattern (stopData) { const stopTimesByPattern = {} if (stopData && stopData.routes && stopData.stopTimes) { - stopData.stopTimes.forEach(patternTimes => { - const routeId = getRouteIdForPattern(patternTimes.pattern) + stopData.stopTimes.forEach(({ pattern, times }) => { + const routeId = getRouteIdForPattern(pattern) - let headsign = patternTimes.times[0] && patternTimes.times[0].headsign + let headsign = times[0] && times[0].headsign // In case stop time headsign is blank, extract headsign from the pattern 'desc' attribute // (format: '49 to ()[ from ( Date: Wed, 16 Dec 2020 19:56:19 -0500 Subject: [PATCH 043/265] refactor(StopLiveTable): Extract component. Unconnect StopScheduleTable. --- lib/components/viewers/stop-live-table.js | 157 +++++++++++++++ lib/components/viewers/stop-schedule-table.js | 24 +-- lib/components/viewers/stop-viewer.js | 181 ++++-------------- 3 files changed, 196 insertions(+), 166 deletions(-) create mode 100644 lib/components/viewers/stop-live-table.js diff --git a/lib/components/viewers/stop-live-table.js b/lib/components/viewers/stop-live-table.js new file mode 100644 index 000000000..1d5f5f88c --- /dev/null +++ b/lib/components/viewers/stop-live-table.js @@ -0,0 +1,157 @@ +import moment from 'moment' +import 'moment-timezone' +import coreUtils from '@opentripplanner/core-utils' +import React, { Component } from 'react' + +import Icon from '../narrative/icon' +import PatternRow from './pattern-row' +import { + getRouteIdForPattern, + getStopTimesByPattern, + routeIsValid +} from '../../util/viewer' + +const { getUserTimezone } = coreUtils.time + +const defaultState = { + spin: false, + timer: null +} + +class StopLiveTable extends Component { + state = defaultState + + _refreshStopTimes = () => { + const { findStopTimesForStop, viewedStop } = this.props + findStopTimesForStop({ stopId: viewedStop.stopId }) + // TODO: GraphQL approach would just call findStop again. + // findStop({ stopId: viewedStop.stopId }) + this.setState({ spin: true }) + window.setTimeout(this._stopSpin, 1000) + } + + _onToggleAutoRefresh = () => { + const { autoRefreshStopTimes, toggleAutoRefresh } = this.props + if (autoRefreshStopTimes) { + toggleAutoRefresh(false) + } else { + // Turn on auto-refresh and refresh immediately to give user feedback. + this._refreshStopTimes() + toggleAutoRefresh(true) + } + } + + _stopSpin = () => this.setState({ spin: false }) + + _startAutoRefresh = () => { + const timer = window.setInterval(this._refreshStopTimes, 10000) + this.setState({ timer }) + } + + _stopAutoRefresh = () => { + window.clearInterval(this.state.timer) + } + + componentDidMount () { + // Turn on stop times refresh if enabled. + if (this.props.autoRefreshStopTimes) this._startAutoRefresh() + } + + componentWillUnmount () { + // Turn off auto refresh unconditionally (just in case). + this._stopAutoRefresh() + } + + componentDidUpdate (prevProps) { + // Handle stopping or starting the auto refresh timer. + if (prevProps.autoRefreshStopTimes && !this.props.autoRefreshStopTimes) this._stopAutoRefresh() + else if (!prevProps.autoRefreshStopTimes && this.props.autoRefreshStopTimes) this._startAutoRefresh() + } + + render () { + const { + homeTimezone, + stopData, + stopViewerArriving, + stopViewerConfig, + timeFormat, + transitOperators + } = this.props + const { spin } = this.state + + // construct a lookup table mapping pattern (e.g. 'ROUTE_ID-HEADSIGN') to + // an array of stoptimes + const stopTimesByPattern = getStopTimesByPattern(stopData) + const routeComparator = coreUtils.route.makeRouteComparator( + transitOperators + ) + const patternHeadsignComparator = coreUtils.route.makeStringValueComparator( + pattern => pattern.pattern.headsign + ) + const patternComparator = (patternA, patternB) => { + // first sort by routes + const routeCompareValue = routeComparator( + patternA.route, + patternB.route + ) + if (routeCompareValue !== 0) return routeCompareValue + + // if same route, sort by headsign + return patternHeadsignComparator(patternA, patternB) + } + + return ( + <> +
    + {Object.values(stopTimesByPattern) + .sort(patternComparator) + .map(({ id, pattern, route, times }) => { + // Only add pattern if route info is returned by OTP. + return routeIsValid(route, getRouteIdForPattern(pattern)) + ? ( + + ) + : null + }) + } +
    + + {/* Auto update controls for realtime arrivals */}. +
    + {/* eslint-disable-next-line jsx-a11y/label-has-for */} + + +
    + } + + ) + } +} + +export default StopLiveTable diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 8ae15cda4..429a3cc70 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -1,6 +1,4 @@ -import coreUtils from '@opentripplanner/core-utils' import React, { Component, createRef } from 'react' -import { connect } from 'react-redux' import styled from 'styled-components' import { isBlank } from '../../util/ui' @@ -14,8 +12,6 @@ import { } from '../../util/viewer' import StopTimeCell from './stop-time-cell' -const { getTimeFormat } = coreUtils.time - // Styles for the schedule table and its contents. const StyledTable = styled.table` border-spacing: collapse; @@ -70,7 +66,7 @@ class StopScheduleTable extends Component { } render () { - const { homeTimezone, stopData, stopViewerArriving, timeFormat } = this.props + const { homeTimezone, stopData, timeFormat } = this.props const stopTimesByPattern = getStopTimesByPattern(stopData) // Merge stop times, so that we can sort them across all route patterns. @@ -138,7 +134,6 @@ class StopScheduleTable extends Component { { - const { config, transitIndex, ui } = state.otp - return { - homeTimezone: config.homeTimezone, - stopData: transitIndex.stops[ui.viewedStop.stopId], - stopViewerArriving: config.language.stopViewerArriving, - timeFormat: getTimeFormat(config) - } -} - -const mapDispatchToProps = { -} - -export default connect(mapStateToProps, mapDispatchToProps)(StopScheduleTable) +export default StopScheduleTable diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index df0449690..19dad29f2 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -9,21 +9,15 @@ import { connect } from 'react-redux' import styled from 'styled-components' import Icon from '../narrative/icon' -import { setMainPanelContent, toggleAutoRefresh } from '../../actions/ui' -import { findStop, findStopTimesForStop } from '../../actions/api' -import { forgetStop, rememberStop, setLocation } from '../../actions/map' -import PatternRow from './pattern-row' +import * as uiActions from '../../actions/ui' +import * as apiActions from '../../actions/api' +import * as mapActions from '../../actions/map' +import StopLiveTable from './stop-live-table' import StopScheduleTable from './stop-schedule-table' import { getShowUserSettings, getStopViewerConfig } from '../../util/state' -import { - getRouteIdForPattern, - getStopTimesByPattern, - routeIsValid -} from '../../util/viewer' const { getTimeFormat, - getUserTimezone, OTP_API_DATE_FORMAT } = coreUtils.time @@ -64,47 +58,9 @@ class StopViewer extends Component { _onClickPlanFrom = () => this._setLocationFromStop('from') - _refreshStopTimes = () => { - const { findStopTimesForStop, viewedStop } = this.props - findStopTimesForStop({ stopId: viewedStop.stopId }) - // TODO: GraphQL approach would just call findStop again. - // findStop({ stopId: viewedStop.stopId }) - this.setState({ spin: true }) - window.setTimeout(this._stopSpin, 1000) - } - - _onToggleAutoRefresh = () => { - const { autoRefreshStopTimes, toggleAutoRefresh } = this.props - if (autoRefreshStopTimes) { - toggleAutoRefresh(false) - } else { - // Turn on auto-refresh and refresh immediately to give user feedback. - this._refreshStopTimes() - toggleAutoRefresh(true) - } - } - - _stopSpin = () => this.setState({ spin: false }) - componentDidMount () { // Load the viewed stop in the store when the Stop Viewer first mounts this.props.findStop({ stopId: this.props.viewedStop.stopId }) - // Turn on stop times refresh if enabled. - if (this.props.autoRefreshStopTimes) this._startAutoRefresh() - } - - componentWillUnmount () { - // Turn off auto refresh unconditionally (just in case). - this._stopAutoRefresh() - } - - _startAutoRefresh = () => { - const timer = window.setInterval(this._refreshStopTimes, 10000) - this.setState({ timer }) - } - - _stopAutoRefresh = () => { - window.clearInterval(this.state.timer) } _toggleFavorite = () => { @@ -125,14 +81,8 @@ class StopViewer extends Component { _toggleScheduleView = () => { const {date, scheduleView: isShowingScheduleView} = this.state if (!isShowingScheduleView) { - // If not currently showing schedule view, fetch schedules for current - // date and turn off auto refresh. - this._stopAutoRefresh() + // If not currently showing schedule view, fetch schedules for current date. this._findStopTimesForDate(date) - } else { - // Otherwise, turn on auto refresh. - this._startAutoRefresh() - this._refreshStopTimes() } this.setState({scheduleView: !isShowingScheduleView}) } @@ -140,8 +90,6 @@ class StopViewer extends Component { _isFavorite = () => this.props.stopData && this.props.favoriteStops.findIndex(s => s.id === this.props.stopData.id) !== -1 - // refresh the stop in the store if the viewed stop changes w/ the - // Stop Viewer already mounted componentDidUpdate (prevProps) { if ( prevProps.viewedStop && @@ -152,17 +100,8 @@ class StopViewer extends Component { this.setState(defaultState) this.props.findStop({ stopId: this.props.viewedStop.stopId }) } - // Handle stopping or starting the auto refresh timer. - if (prevProps.autoRefreshStopTimes && !this.props.autoRefreshStopTimes) this._stopAutoRefresh() - else if (!prevProps.autoRefreshStopTimes && this.props.autoRefreshStopTimes) this._startAutoRefresh() } - /** - * Get today at midnight (morning) in seconds since epoch. - * FIXME: handle timezone diffs? - */ - _getStartTimeForDate = date => moment(date).startOf('day').unix() - handleDateChange = evt => { const date = evt.target.value this._findStopTimesForDate(date) @@ -257,91 +196,45 @@ class StopViewer extends Component { render () { const { + autoRefreshStopTimes, + findStopTimesForStop, homeTimezone, stopData, stopViewerArriving, stopViewerConfig, timeFormat, - transitOperators + toggleAutoRefresh, + transitOperators, + viewedStop } = this.props - const { scheduleView, spin } = this.state + const { scheduleView } = this.state const hasStopTimesAndRoutes = !!(stopData && stopData.stopTimes && stopData.stopTimes.length > 0 && stopData.routes) - // construct a lookup table mapping pattern (e.g. 'ROUTE_ID-HEADSIGN') to - // an array of stoptimes - const stopTimesByPattern = getStopTimesByPattern(stopData) - const routeComparator = coreUtils.route.makeRouteComparator( - transitOperators - ) - const patternHeadsignComparator = coreUtils.route.makeStringValueComparator( - pattern => pattern.pattern.headsign - ) - const patternComparator = (patternA, patternB) => { - // first sort by routes - const routeCompareValue = routeComparator( - patternA.route, - patternB.route - ) - if (routeCompareValue !== 0) return routeCompareValue - - // if same route, sort by headsign - return patternHeadsignComparator(patternA, patternB) - } - let contents if (!hasStopTimesAndRoutes) { contents =
    No stop times found for date.
    } else if (scheduleView) { - contents = + contents = ( + + ) } else { contents = ( - <> -
    - {Object.values(stopTimesByPattern) - .sort(patternComparator) - .map(({ id, pattern, route, times }) => { - // Only add pattern if route info is returned by OTP. - return routeIsValid(route, getRouteIdForPattern(pattern)) - ? ( - - ) - : null - }) - } -
    - {/* Auto update controls for realtime arrivals */}. -
    - - -
    - } - + ) } @@ -384,13 +277,13 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - findStop, - findStopTimesForStop, - forgetStop, - rememberStop, - setLocation, - setMainPanelContent, - toggleAutoRefresh + findStop: apiActions.findStop, + findStopTimesForStop: apiActions.findStopTimesForStop, + forgetStop: mapActions.forgetStop, + rememberStop: mapActions.rememberStop, + setLocation: mapActions.setLocation, + setMainPanelContent: uiActions.setMainPanelContent, + toggleAutoRefresh: uiActions.toggleAutoRefresh } export default connect(mapStateToProps, mapDispatchToProps)(StopViewer) From d92b50a6d163fee901aca81ddd1b462893d45333 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 16 Dec 2020 20:15:27 -0500 Subject: [PATCH 044/265] test(StopViewer): Make tets pass. Update snapshots. --- .../viewers/__snapshots__/stop-viewer.js.snap | 5837 ++++++++++------- lib/util/viewer.js | 15 +- 2 files changed, 3611 insertions(+), 2241 deletions(-) diff --git a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap index c755c26ec..26af2cde4 100644 --- a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap +++ b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap @@ -265,7 +265,13 @@ exports[`components > viewers > stop viewer should render countdown times after
    -
    +
    Stop ID @@ -489,179 +495,219 @@ exports[`components > viewers > stop viewer should render countdown times after
    -
    - +
    + -
    -
    -
    - - 20 - - To - Gresham TC -
    -
    - + viewers > stop viewer should render countdown times after "stopIndex": 38, "timepoint": true, "tripId": "TriMet:9230375", - } + }, + Object { + "arrivalDelay": 0, + "blockId": "2043", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 93120, + "realtimeDeparture": 93120, + "realtimeState": "SCHEDULED", + "scheduledArrival": 93120, + "scheduledDeparture": 93120, + "serviceDay": 1565161200, + "stopCount": 132, + "stopId": "TriMet:9860", + "stopIndex": 38, + "timepoint": true, + "tripId": "TriMet:9230376", + }, + Object { + "arrivalDelay": 0, + "blockId": "2049", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 96780, + "realtimeDeparture": 96780, + "realtimeState": "SCHEDULED", + "scheduledArrival": 96780, + "scheduledDeparture": 96780, + "serviceDay": 1565161200, + "stopCount": 132, + "stopId": "TriMet:9860", + "stopIndex": 38, + "timepoint": true, + "tripId": "TriMet:9230377", + }, + Object { + "arrivalDelay": 0, + "blockId": "2067", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 13980, + "realtimeDeparture": 13980, + "realtimeState": "SCHEDULED", + "scheduledArrival": 13980, + "scheduledDeparture": 13980, + "serviceDay": 1565247600, + "stopCount": 132, + "stopId": "TriMet:9860", + "stopIndex": 38, + "timepoint": true, + "tripId": "TriMet:9230305", + }, + Object { + "arrivalDelay": 0, + "blockId": "2034", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 17580, + "realtimeDeparture": 17580, + "realtimeState": "SCHEDULED", + "scheduledArrival": 17580, + "scheduledDeparture": 17580, + "serviceDay": 1565247600, + "stopCount": 132, + "stopId": "TriMet:9860", + "stopIndex": 38, + "timepoint": true, + "tripId": "TriMet:9230306", + }, + Object { + "arrivalDelay": 0, + "blockId": "2069", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 19020, + "realtimeDeparture": 19020, + "realtimeState": "SCHEDULED", + "scheduledArrival": 19020, + "scheduledDeparture": 19020, + "serviceDay": 1565247600, + "stopCount": 132, + "stopId": "TriMet:9860", + "stopIndex": 38, + "timepoint": true, + "tripId": "TriMet:9230307", + }, + ] + } + stopViewerConfig={ + Object { + "numberOfDepartures": 3, + "timeRange": 345600, } - timeFormat="HH:mm" - useCountdown={true} - useSchedule={false} + } + timeFormat="HH:mm" + > +
    -
    +
    + + 20 + + To + Gresham TC +
    +
    - - +
    - + + + + + +
    +
    - - + > +
    + 52 min +
    +
    +
    +
    -
    - 52 min -
    +
    - -
    -
    - -
    +
    + + +
    +
    - + +
    -
    - - +
    } - } - > - - - - - - - 00:17 - -
    +
    +
    +
    @@ -1030,7 +1222,13 @@ exports[`components > viewers > stop viewer should render countdown times for st
    -
    +
    Stop ID @@ -1254,89 +1452,120 @@ exports[`components > viewers > stop viewer should render countdown times for st
    -
    - +
    + -
    -
    -
    - - 20 - - To - Gresham TC -
    -
    - + viewers > stop viewer should render countdown times for st "stopIndex": 38, "timepoint": true, "tripId": "TriMet:9230375", - } + }, + ] + } + stopViewerConfig={ + Object { + "numberOfDepartures": 3, + "timeRange": 345600, } - timeFormat="HH:mm" - useCountdown={true} - useSchedule={false} + } + timeFormat="HH:mm" + > +
    -
    +
    - + 20 + + To + Gresham TC +
    +
    + - +
    - + + + + + +
    +
    - - + > +
    + 52 min +
    +
    +
    +
    -
    - 52 min -
    +
    - -
    -
    - -
    +
    + + +
    +
    - + +
    -
    - - +
    } - } - > - - - - - - - 00:17 - -
    + +
    +
    @@ -1804,7 +2089,13 @@ exports[`components > viewers > stop viewer should render times after midnight w
    -
    +
    Stop ID @@ -2028,179 +2319,219 @@ exports[`components > viewers > stop viewer should render times after midnight w
    -
    - +
    + -
    -
    -
    - - 20 - - To - Gresham TC -
    -
    - + viewers > stop viewer should render times after midnight w "stopIndex": 38, "timepoint": true, "tripId": "TriMet:9230375", - } + }, + Object { + "arrivalDelay": 0, + "blockId": "2043", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 93120, + "realtimeDeparture": 93120, + "realtimeState": "SCHEDULED", + "scheduledArrival": 93120, + "scheduledDeparture": 93120, + "serviceDay": 1565161200, + "stopCount": 132, + "stopId": "TriMet:9860", + "stopIndex": 38, + "timepoint": true, + "tripId": "TriMet:9230376", + }, + Object { + "arrivalDelay": 0, + "blockId": "2049", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 96780, + "realtimeDeparture": 96780, + "realtimeState": "SCHEDULED", + "scheduledArrival": 96780, + "scheduledDeparture": 96780, + "serviceDay": 1565161200, + "stopCount": 132, + "stopId": "TriMet:9860", + "stopIndex": 38, + "timepoint": true, + "tripId": "TriMet:9230377", + }, + Object { + "arrivalDelay": 0, + "blockId": "2067", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 13980, + "realtimeDeparture": 13980, + "realtimeState": "SCHEDULED", + "scheduledArrival": 13980, + "scheduledDeparture": 13980, + "serviceDay": 1565247600, + "stopCount": 132, + "stopId": "TriMet:9860", + "stopIndex": 38, + "timepoint": true, + "tripId": "TriMet:9230305", + }, + Object { + "arrivalDelay": 0, + "blockId": "2034", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 17580, + "realtimeDeparture": 17580, + "realtimeState": "SCHEDULED", + "scheduledArrival": 17580, + "scheduledDeparture": 17580, + "serviceDay": 1565247600, + "stopCount": 132, + "stopId": "TriMet:9860", + "stopIndex": 38, + "timepoint": true, + "tripId": "TriMet:9230306", + }, + Object { + "arrivalDelay": 0, + "blockId": "2069", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 19020, + "realtimeDeparture": 19020, + "realtimeState": "SCHEDULED", + "scheduledArrival": 19020, + "scheduledDeparture": 19020, + "serviceDay": 1565247600, + "stopCount": 132, + "stopId": "TriMet:9860", + "stopIndex": 38, + "timepoint": true, + "tripId": "TriMet:9230307", + }, + ] + } + stopViewerConfig={ + Object { + "numberOfDepartures": 3, + "timeRange": 345600, } - timeFormat="HH:mm" - useCountdown={true} - useSchedule={false} + } + timeFormat="HH:mm" + > +
    -
    +
    + + 20 + + To + Gresham TC +
    +
    - - +
    - + + + + + +
    +
    - - + > +
    + Thursday +
    +
    + 00:51 +
    +
    +
    +
    -
    - Thursday -
    -
    - 00:51 -
    + + + + + +
    - -
    -
    - -
    +
    + + +
    +
    - + +
    -
    - - +
    } - } - > - - - - - - - 00:17 - -
    +
    +
    +
    @@ -2935,7 +3412,13 @@ exports[`components > viewers > stop viewer should render with OTP transit index
    -
    +
    Stop ID @@ -3159,177 +3642,475 @@ exports[`components > viewers > stop viewer should render with OTP transit index
    -
    - +
    + -
    -
    -
    - - 20 - - To - Gresham TC -
    -
    - + viewers > stop viewer should render with OTP transit index "stopIndex": 42, "timepoint": false, "tripId": "TriMet:9230358", - } - } - timeFormat="HH:mm" - useCountdown={true} - useSchedule={false} + }, + Object { + "arrivalDelay": 0, + "blockId": "2047", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 66668, + "realtimeDeparture": 66668, + "realtimeState": "SCHEDULED", + "scheduledArrival": 66668, + "scheduledDeparture": 66668, + "serviceDay": 1564988400, + "stopCount": 132, + "stopId": "TriMet:715", + "stopIndex": 42, + "timepoint": false, + "tripId": "TriMet:9230360", + }, + Object { + "arrivalDelay": 0, + "blockId": "2048", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 67628, + "realtimeDeparture": 67628, + "realtimeState": "SCHEDULED", + "scheduledArrival": 67628, + "scheduledDeparture": 67628, + "serviceDay": 1564988400, + "stopCount": 132, + "stopId": "TriMet:715", + "stopIndex": 42, + "timepoint": false, + "tripId": "TriMet:9230361", + }, + Object { + "arrivalDelay": 0, + "blockId": "2046", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 65759, + "realtimeDeparture": 65759, + "realtimeState": "SCHEDULED", + "scheduledArrival": 65759, + "scheduledDeparture": 65759, + "serviceDay": 1564988400, + "stopCount": 132, + "stopId": "TriMet:715", + "stopIndex": 42, + "timepoint": false, + "tripId": "TriMet:9230359", + }, + Object { + "arrivalDelay": 0, + "blockId": "2036", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 70028, + "realtimeDeparture": 70028, + "realtimeState": "SCHEDULED", + "scheduledArrival": 70028, + "scheduledDeparture": 70028, + "serviceDay": 1564988400, + "stopCount": 132, + "stopId": "TriMet:715", + "stopIndex": 42, + "timepoint": false, + "tripId": "TriMet:9230363", + }, + Object { + "arrivalDelay": 0, + "blockId": "2071", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 72436, + "realtimeDeparture": 72436, + "realtimeState": "SCHEDULED", + "scheduledArrival": 72436, + "scheduledDeparture": 72436, + "serviceDay": 1564988400, + "stopCount": 132, + "stopId": "TriMet:715", + "stopIndex": 42, + "timepoint": false, + "tripId": "TriMet:9230365", + }, + ] + } + stopViewerConfig={ + Object { + "numberOfDepartures": 3, + "timeRange": 345600, + } + } + timeFormat="HH:mm" + > +
    -
    +
    + + 20 + + To + Gresham TC +
    +
    - - +
    - + + + + + +
    +
    - - + > +
    + Monday +
    +
    + 18:00 +
    +
    +
    +
    -
    - Monday -
    -
    - 18:00 -
    + + + + + +
    - -
    -
    - -
    -
    - + + +
    + + - -
    - - -
    -
    - -
    -
    -
    - - 36 - - To - Tualatin Park & Ride -
    -
    - viewers > stop viewer should render with OTP transit index "stopIndex": 0, "timepoint": true, "tripId": "TriMet:9231858", - } + }, + Object { + "arrivalDelay": 0, + "blockId": "3670", + "departureDelay": 0, + "headsign": "Tualatin Park & Ride", + "realtime": false, + "realtimeArrival": 61740, + "realtimeDeparture": 61740, + "realtimeState": "SCHEDULED", + "scheduledArrival": 61740, + "scheduledDeparture": 61740, + "serviceDay": 1565074800, + "stopCount": 63, + "stopId": "TriMet:715", + "stopIndex": 0, + "timepoint": true, + "tripId": "TriMet:9231860", + }, + Object { + "arrivalDelay": 0, + "blockId": "3668", + "departureDelay": 0, + "headsign": "Tualatin Park & Ride", + "realtime": false, + "realtimeArrival": 58260, + "realtimeDeparture": 58260, + "realtimeState": "SCHEDULED", + "scheduledArrival": 58260, + "scheduledDeparture": 58260, + "serviceDay": 1565161200, + "stopCount": 63, + "stopId": "TriMet:715", + "stopIndex": 0, + "timepoint": true, + "tripId": "TriMet:9231858", + }, + ] + } + stopViewerConfig={ + Object { + "numberOfDepartures": 3, + "timeRange": 345600, } - timeFormat="HH:mm" - useCountdown={true} - useSchedule={false} + } + timeFormat="HH:mm" + > +
    -
    +
    + + 36 + + To + Tualatin Park & Ride +
    +
    - - +
    - + + + + + +
    +
    - - + > +
    + Tuesday +
    +
    + 16:11 +
    +
    +
    +
    -
    - Tuesday -
    -
    - 16:11 -
    + + + + + +
    - -
    -
    - -
    -
    - + + +
    + + - -
    - - -
    -
    - -
    -
    -
    - - 94 - - To - King City -
    -
    - viewers > stop viewer should render with OTP transit index "stopIndex": 0, "timepoint": true, "tripId": "TriMet:9238192", - } + }, + Object { + "arrivalDelay": 0, + "blockId": "9472", + "departureDelay": 0, + "headsign": "King City", + "realtime": false, + "realtimeArrival": 55320, + "realtimeDeparture": 55320, + "realtimeState": "SCHEDULED", + "scheduledArrival": 55320, + "scheduledDeparture": 55320, + "serviceDay": 1565161200, + "stopCount": 23, + "stopId": "TriMet:715", + "stopIndex": 0, + "timepoint": true, + "tripId": "TriMet:9238192", + }, + Object { + "arrivalDelay": 0, + "blockId": "9472", + "departureDelay": 0, + "headsign": "King City", + "realtime": false, + "realtimeArrival": 55320, + "realtimeDeparture": 55320, + "realtimeState": "SCHEDULED", + "scheduledArrival": 55320, + "scheduledDeparture": 55320, + "serviceDay": 1565247600, + "stopCount": 23, + "stopId": "TriMet:715", + "stopIndex": 0, + "timepoint": true, + "tripId": "TriMet:9238192", + }, + ] + } + stopViewerConfig={ + Object { + "numberOfDepartures": 3, + "timeRange": 345600, } - timeFormat="HH:mm" - useCountdown={true} - useSchedule={false} + } + timeFormat="HH:mm" + > +
    -
    +
    - + 94 + + To + King City +
    +
    + - +
    - + + + + + +
    +
    - - + > +
    + Tuesday +
    +
    + 15:22 +
    +
    +
    +
    -
    - Tuesday -
    -
    - 15:22 -
    + + + + + +
    - -
    -
    - -
    -
    - + + +
    + + - -
    - - -
    -
    - -
    -
    -
    - - 94 - - To - Sherwood -
    -
    - viewers > stop viewer should render with OTP transit index "stopIndex": 0, "timepoint": true, "tripId": "TriMet:9238187", - } + }, + Object { + "arrivalDelay": 0, + "blockId": "9372", + "departureDelay": 0, + "headsign": "Sherwood", + "realtime": false, + "realtimeArrival": 54120, + "realtimeDeparture": 54120, + "realtimeState": "SCHEDULED", + "scheduledArrival": 54120, + "scheduledDeparture": 54120, + "serviceDay": 1565074800, + "stopCount": 40, + "stopId": "TriMet:715", + "stopIndex": 0, + "timepoint": true, + "tripId": "TriMet:9238189", + }, + Object { + "arrivalDelay": 0, + "blockId": "9474", + "departureDelay": 0, + "headsign": "Sherwood", + "realtime": false, + "realtimeArrival": 56880, + "realtimeDeparture": 56880, + "realtimeState": "SCHEDULED", + "scheduledArrival": 56880, + "scheduledDeparture": 56880, + "serviceDay": 1565074800, + "stopCount": 40, + "stopId": "TriMet:715", + "stopIndex": 0, + "timepoint": true, + "tripId": "TriMet:9238194", + }, + Object { + "arrivalDelay": 0, + "blockId": "9470", + "departureDelay": 0, + "headsign": "Sherwood", + "realtime": false, + "realtimeArrival": 54720, + "realtimeDeparture": 54720, + "realtimeState": "SCHEDULED", + "scheduledArrival": 54720, + "scheduledDeparture": 54720, + "serviceDay": 1565074800, + "stopCount": 34, + "stopId": "TriMet:715", + "stopIndex": 0, + "timepoint": true, + "tripId": "TriMet:9238190", + }, + Object { + "arrivalDelay": 0, + "blockId": "9470", + "departureDelay": 0, + "headsign": "Sherwood", + "realtime": false, + "realtimeArrival": 54720, + "realtimeDeparture": 54720, + "realtimeState": "SCHEDULED", + "scheduledArrival": 54720, + "scheduledDeparture": 54720, + "serviceDay": 1565161200, + "stopCount": 34, + "stopId": "TriMet:715", + "stopIndex": 0, + "timepoint": true, + "tripId": "TriMet:9238190", + }, + Object { + "arrivalDelay": 0, + "blockId": "9470", + "departureDelay": 0, + "headsign": "Sherwood", + "realtime": false, + "realtimeArrival": 54720, + "realtimeDeparture": 54720, + "realtimeState": "SCHEDULED", + "scheduledArrival": 54720, + "scheduledDeparture": 54720, + "serviceDay": 1565247600, + "stopCount": 34, + "stopId": "TriMet:715", + "stopIndex": 0, + "timepoint": true, + "tripId": "TriMet:9238190", + }, + ] + } + stopViewerConfig={ + Object { + "numberOfDepartures": 3, + "timeRange": 345600, } - timeFormat="HH:mm" - useCountdown={true} - useSchedule={false} + } + timeFormat="HH:mm" + > +
    -
    +
    + + 94 + + To + Sherwood +
    +
    - - +
    - + + + + + +
    +
    - - + > +
    + Tuesday +
    +
    + 14:28 +
    +
    +
    +
    -
    - Tuesday -
    -
    - 14:28 -
    + + + + + +
    - -
    -
    - -
    +
    + + +
    +
    - + +
    -
    - - +
    } - } - > - - - - - - - 17:50 - -
    + +
    +
    @@ -4890,7 +5814,13 @@ exports[`components > viewers > stop viewer should render with TriMet transit in
    -
    +
    Stop ID @@ -5114,179 +6044,472 @@ exports[`components > viewers > stop viewer should render with TriMet transit in
    -
    - +
    + -
    -
    -
    - - 20 - - To - Gresham TC -
    -
    - + viewers > stop viewer should render with TriMet transit in "stopIndex": 42, "timepoint": false, "tripId": "TriMet:9230357", - } + }, + Object { + "arrivalDelay": 0, + "blockId": "2045", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 64859, + "realtimeDeparture": 64859, + "realtimeState": "SCHEDULED", + "scheduledArrival": 64859, + "scheduledDeparture": 64859, + "serviceDay": 1564988400, + "stopCount": 132, + "stopId": "TriMet:715", + "stopIndex": 42, + "timepoint": false, + "tripId": "TriMet:9230358", + }, + Object { + "arrivalDelay": 0, + "blockId": "2047", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 66668, + "realtimeDeparture": 66668, + "realtimeState": "SCHEDULED", + "scheduledArrival": 66668, + "scheduledDeparture": 66668, + "serviceDay": 1564988400, + "stopCount": 132, + "stopId": "TriMet:715", + "stopIndex": 42, + "timepoint": false, + "tripId": "TriMet:9230360", + }, + Object { + "arrivalDelay": 0, + "blockId": "2046", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 65759, + "realtimeDeparture": 65759, + "realtimeState": "SCHEDULED", + "scheduledArrival": 65759, + "scheduledDeparture": 65759, + "serviceDay": 1564988400, + "stopCount": 132, + "stopId": "TriMet:715", + "stopIndex": 42, + "timepoint": false, + "tripId": "TriMet:9230359", + }, + Object { + "arrivalDelay": 0, + "blockId": "2036", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 70028, + "realtimeDeparture": 70028, + "realtimeState": "SCHEDULED", + "scheduledArrival": 70028, + "scheduledDeparture": 70028, + "serviceDay": 1564988400, + "stopCount": 132, + "stopId": "TriMet:715", + "stopIndex": 42, + "timepoint": false, + "tripId": "TriMet:9230363", + }, + Object { + "arrivalDelay": 0, + "blockId": "2071", + "departureDelay": 0, + "headsign": "Gresham TC", + "realtime": false, + "realtimeArrival": 72436, + "realtimeDeparture": 72436, + "realtimeState": "SCHEDULED", + "scheduledArrival": 72436, + "scheduledDeparture": 72436, + "serviceDay": 1564988400, + "stopCount": 132, + "stopId": "TriMet:715", + "stopIndex": 42, + "timepoint": false, + "tripId": "TriMet:9230365", + }, + ] + } + stopViewerConfig={ + Object { + "numberOfDepartures": 3, + "timeRange": 345600, } - timeFormat="HH:mm" - useCountdown={true} - useSchedule={false} + } + timeFormat="HH:mm" + > +
    -
    +
    + + 20 + + To + Gresham TC +
    +
    - - +
    - + + + + + +
    +
    - - + > +
    + Monday +
    +
    + 17:45 +
    +
    +
    +
    -
    - Monday -
    -
    - 17:45 -
    + + + + + +
    - -
    -
    - -
    +
    + + +
    +
    - + +
    -
    - - +
    } - } - > - - - - - - - 17:38 - -
    +
    +
    +
    diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 78099e3a4..a65069bb5 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -105,13 +105,14 @@ export function getStopTimesByPattern (stopData) { // route only performs a drop-off at the stop... not quite sure. So a // check is needed to make sure we don't add data for routes not found // from the routes query. - if (routeIsValid(route, routeId)) { - stopTimesByPattern[id] = { - id, - route, - pattern, - times: [] - } + if (!routeIsValid(route, routeId)) { + return + } + stopTimesByPattern[id] = { + id, + route, + pattern, + times: [] } } // Exclude the last stop, as the stop viewer doesn't show arrival times to a terminus stop. From 4d0d8dd4d808f061851cb474395939830e028934 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 16 Dec 2020 20:23:25 -0500 Subject: [PATCH 045/265] refactor(utils/viewer): Tweak comment --- lib/util/viewer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/util/viewer.js b/lib/util/viewer.js index a65069bb5..5e03538bf 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -148,7 +148,8 @@ export function getModeFromRoute (route) { } /** - * Compares departure times of two stops. + * Comparator to sort stop times by their departure times + * (in chronological order - 9:13am, 9:15am, etc.) */ export function stopTimeComparator (a, b) { const aTime = a.serviceDay + a.scheduledDeparture From 9c02587769b8ada1d00aa14c7aa54ce9862df9dd Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Thu, 17 Dec 2020 11:38:55 -0500 Subject: [PATCH 046/265] refactor(stop-schedule-table): add highlight row to scrollIntoView --- lib/components/viewers/stop-live-table.js | 7 +++-- lib/components/viewers/stop-schedule-table.js | 27 ++++++++++++++----- lib/components/viewers/viewers.css | 11 ++++++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/lib/components/viewers/stop-live-table.js b/lib/components/viewers/stop-live-table.js index 1d5f5f88c..b4876e505 100644 --- a/lib/components/viewers/stop-live-table.js +++ b/lib/components/viewers/stop-live-table.js @@ -18,6 +18,10 @@ const defaultState = { timer: null } +/** + * Table showing next arrivals (refreshing every 10 seconds) for the specified + * stop organized by route pattern. + */ class StopLiveTable extends Component { state = defaultState @@ -125,7 +129,7 @@ class StopLiveTable extends Component { }
    - {/* Auto update controls for realtime arrivals */}. + {/* Auto update controls for realtime arrivals */}
    {/* eslint-disable-next-line jsx-a11y/label-has-for */}
    - } ) } diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 429a3cc70..21fadcba1 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -43,16 +43,20 @@ const TimeCell = styled.td` white-space: nowrap; ` +/** + * Table showing scheduled departure times for the specified stop organized + * chronologically. + */ class StopScheduleTable extends Component { - firstDepartureRef = createRef() + targetDepartureRef = createRef() /* * Scroll to the first stop time that is departing from now. */ _scrollToFirstDeparture = () => { - const { current } = this.firstDepartureRef + const { current } = this.targetDepartureRef if (current) { - current.scrollIntoView() + current.scrollIntoView({ behavior: 'smooth', block: 'start' }) } } @@ -121,12 +125,23 @@ class StopScheduleTable extends Component { {mergedStopTimes.map((stopTime, index) => { const { blockId, headsign, route } = stopTime + // Highlight if this row is the imminent departure. + const highlightRow = stopTime === firstDepartureFromNow + const className = highlightRow ? 'highlighted-item' : null + // FIXME: This is a bit of a hack to account for the sticky table + // header interfering with the scrollIntoView. If the next stop time + // is the imminent departure, we'll set the scrollTo to this row (the + // stop time prior), which effectively applies an offset for the + // scroll. If next row does not exist, default to this row. + const nextStopTime = mergedStopTimes[index + 1] + const scrollToRow = nextStopTime + ? nextStopTime === firstDepartureFromNow + : highlightRow const routeName = route.shortName ? route.shortName : route.longName // Add ref to scroll to the first stop time departing from now. - const refProp = stopTime === firstDepartureFromNow ? this.firstDepartureRef : null - + const refProp = scrollToRow ? this.targetDepartureRef : null return ( - + {blockId} {routeName} {headsign} diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index cfa02e405..5d0c0fcf0 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -13,6 +13,17 @@ height: 100%; } +@keyframes yellowfade { + from { background: yellow; } + to { background: transparent; } +} + +/* Used to briefly highlight an element and then fade to transparent. */ +.highlighted-item { + animation-name: yellowfade; + animation-duration: 1.5s; +} + /* Remove arrows on date input */ .otp .stop-viewer-body input[type="date"]::-webkit-inner-spin-button { -webkit-appearance: none; From e9adb3196aef21447e847ce62721c38845e29d6b Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Thu, 17 Dec 2020 13:04:01 -0500 Subject: [PATCH 047/265] refactor(stop-schedules): skip scrollTo if not today; update snaps --- .../viewers/__snapshots__/stop-viewer.js.snap | 10 ---------- lib/components/viewers/stop-schedule-table.js | 10 ++++++---- lib/components/viewers/stop-viewer.js | 5 +++-- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap index 26af2cde4..ee1a76cac 100644 --- a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap +++ b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap @@ -990,7 +990,6 @@ exports[`components > viewers > stop viewer should render countdown times after
    - .
    viewers > stop viewer should render countdown times after 00:17
    - }
    @@ -1758,7 +1756,6 @@ exports[`components > viewers > stop viewer should render countdown times for st
    - .
    viewers > stop viewer should render countdown times for st 00:17
    - }
    @@ -2823,7 +2819,6 @@ exports[`components > viewers > stop viewer should render times after midnight w
    - .
    viewers > stop viewer should render times after midnight w 00:17
    - }
    @@ -5230,7 +5224,6 @@ exports[`components > viewers > stop viewer should render with OTP transit index
    - .
    viewers > stop viewer should render with OTP transit index 17:50
    - }
    @@ -6801,7 +6793,6 @@ exports[`components > viewers > stop viewer should render with TriMet transit in
    - .
    viewers > stop viewer should render with TriMet transit in 17:38
    - }
    diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 21fadcba1..601506293 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -1,3 +1,4 @@ +import moment from 'moment' import React, { Component, createRef } from 'react' import styled from 'styled-components' @@ -70,7 +71,7 @@ class StopScheduleTable extends Component { } render () { - const { homeTimezone, stopData, timeFormat } = this.props + const { date, homeTimezone, stopData, timeFormat } = this.props const stopTimesByPattern = getStopTimesByPattern(stopData) // Merge stop times, so that we can sort them across all route patterns. @@ -95,10 +96,11 @@ class StopScheduleTable extends Component { }) mergedStopTimes = mergedStopTimes.sort(stopTimeComparator) - - // Find the next stop time that is departing. We will scroll to that stop time entry. + const today = moment().startOf('day').format('YYYY-MM-DD') + // Find the next stop time that is departing. + // We will scroll to that stop time entry (if showing schedules for today). let firstDepartureFromNow - if (mergedStopTimes.length) { + if (mergedStopTimes.length && date === today) { // Search starting from the last stop time (largest seconds until departure) for this pattern. const lastStopTime = mergedStopTimes[mergedStopTimes.length - 1] diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 19dad29f2..b39e4fedc 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -207,15 +207,16 @@ class StopViewer extends Component { transitOperators, viewedStop } = this.props - const { scheduleView } = this.state + const { date, scheduleView } = this.state const hasStopTimesAndRoutes = !!(stopData && stopData.stopTimes && stopData.stopTimes.length > 0 && stopData.routes) - + console.log(date) let contents if (!hasStopTimesAndRoutes) { contents =
    No stop times found for date.
    } else if (scheduleView) { contents = ( Date: Mon, 21 Dec 2020 18:02:22 -0500 Subject: [PATCH 048/265] refactor(StopScheduleTable): Move things around, fix rendering. --- lib/actions/api.js | 8 ++ lib/components/viewers/stop-schedule-table.js | 79 +++++-------------- lib/components/viewers/stop-time-cell.js | 26 ++---- lib/components/viewers/stop-viewer.js | 2 +- lib/components/viewers/viewers.css | 10 ++- lib/util/viewer.js | 69 +++++++++++++++- 6 files changed, 108 insertions(+), 86 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index a21c90973..bd7529f9c 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -570,6 +570,14 @@ export function findStopTimesForStop (params) { const nowInSeconds = Math.floor((new Date()).getTime() / 1000) queryParams.startTime = nowInSeconds } + + // Clear existing stop times to prevent extra rendering of stop viewer while schedule is + // fetched and a schedule for the same date/stop was shown before. This also has the nice side + // effect to force a render, initially showing the top row, after changing the schedule view date + // and all schedules for the new date are the same as the previous date. + dispatch(findStopTimesForStopResponse({ stopId, stopTimes: [] })) + + // (Re-)fetch stop times for the stop. dispatch(createQueryAction( `index/stops/${stopId}/stoptimes${datePath}?${qs.stringify(queryParams)}`, findStopTimesForStopResponse, diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 601506293..73499a5f9 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -2,16 +2,11 @@ import moment from 'moment' import React, { Component, createRef } from 'react' import styled from 'styled-components' -import { isBlank } from '../../util/ui' import { - excludeLastStop, - getRouteIdForPattern, - getSecondsUntilDeparture, - getStopTimesByPattern, - routeIsValid, - stopTimeComparator + formatDepartureTime, + getFirstDepartureFromNow, + mergeAndSortStopTimes } from '../../util/viewer' -import StopTimeCell from './stop-time-cell' // Styles for the schedule table and its contents. const StyledTable = styled.table` @@ -51,7 +46,7 @@ const TimeCell = styled.td` class StopScheduleTable extends Component { targetDepartureRef = createRef() - /* + /** * Scroll to the first stop time that is departing from now. */ _scrollToFirstDeparture = () => { @@ -72,47 +67,16 @@ class StopScheduleTable extends Component { render () { const { date, homeTimezone, stopData, timeFormat } = this.props - const stopTimesByPattern = getStopTimesByPattern(stopData) + const mergedStopTimes = mergeAndSortStopTimes(stopData) - // Merge stop times, so that we can sort them across all route patterns. - // (stopData is assumed valid per StopViewer render condition.) - let mergedStopTimes = [] - Object.values(stopTimesByPattern).forEach(({ pattern, route, times }) => { - // Only add pattern if route info is returned by OTP. - if (routeIsValid(route, getRouteIdForPattern(pattern))) { - const filteredTimes = times - .filter(excludeLastStop) - .map(stopTime => { - // Add the route attribute and headsign to each stop time for rendering route info. - const headsign = isBlank(stopTime.headsign) ? pattern.headsign : stopTime.headsign - return { - ...stopTime, - route, - headsign - } - }) - mergedStopTimes = mergedStopTimes.concat(filteredTimes) - } - }) - - mergedStopTimes = mergedStopTimes.sort(stopTimeComparator) + // FIXME: Shift today back one day if the current time is between midnight and the end of the service day. const today = moment().startOf('day').format('YYYY-MM-DD') // Find the next stop time that is departing. // We will scroll to that stop time entry (if showing schedules for today). - let firstDepartureFromNow - if (mergedStopTimes.length && date === today) { - // Search starting from the last stop time (largest seconds until departure) for this pattern. - const lastStopTime = mergedStopTimes[mergedStopTimes.length - 1] - - firstDepartureFromNow = mergedStopTimes.reduce((firstStopTime, stopTime) => { - const firstStopTimeSeconds = getSecondsUntilDeparture(firstStopTime, true) - const stopTimeSeconds = getSecondsUntilDeparture(stopTime, true) - - return stopTimeSeconds < firstStopTimeSeconds && stopTimeSeconds >= 0 - ? stopTime - : firstStopTime - }, lastStopTime) - } + const shouldHighlightFirstDeparture = mergedStopTimes.length && date === today + const highlightedStopTime = shouldHighlightFirstDeparture + ? getFirstDepartureFromNow(mergedStopTimes) + : null return ( @@ -127,8 +91,8 @@ class StopScheduleTable extends Component { {mergedStopTimes.map((stopTime, index) => { const { blockId, headsign, route } = stopTime - // Highlight if this row is the imminent departure. - const highlightRow = stopTime === firstDepartureFromNow + // Highlight if this row is the imminent departure and schedule is shown for today. + const highlightRow = stopTime === highlightedStopTime const className = highlightRow ? 'highlighted-item' : null // FIXME: This is a bit of a hack to account for the sticky table // header interfering with the scrollIntoView. If the next stop time @@ -137,9 +101,15 @@ class StopScheduleTable extends Component { // scroll. If next row does not exist, default to this row. const nextStopTime = mergedStopTimes[index + 1] const scrollToRow = nextStopTime - ? nextStopTime === firstDepartureFromNow + ? nextStopTime === highlightedStopTime : highlightRow const routeName = route.shortName ? route.shortName : route.longName + const formattedTime = formatDepartureTime( + stopTime.scheduledDeparture, + timeFormat, + homeTimezone + ) + // Add ref to scroll to the first stop time departing from now. const refProp = scrollToRow ? this.targetDepartureRef : null return ( @@ -147,16 +117,7 @@ class StopScheduleTable extends Component { {blockId} {routeName} {headsign} - - - + {formattedTime} ) })} diff --git a/lib/components/viewers/stop-time-cell.js b/lib/components/viewers/stop-time-cell.js index fd16228ed..e1440131b 100644 --- a/lib/components/viewers/stop-time-cell.js +++ b/lib/components/viewers/stop-time-cell.js @@ -5,13 +5,9 @@ import PropTypes from 'prop-types' import React from 'react' import Icon from '../narrative/icon' -import { getSecondsUntilDeparture } from '../../util/viewer' +import { formatDepartureTime, getSecondsUntilDeparture } from '../../util/viewer' -const { - formatDuration, - formatSecondsAfterMidnight, - getUserTimezone -} = coreUtils.time +const { formatDuration } = coreUtils.time const ONE_HOUR_IN_SECONDS = 3600 const ONE_DAY_IN_SECONDS = 86400 @@ -31,17 +27,6 @@ function renderIcon (stopTime) { ) } -/** - * If the browser timezone is not the homeTimezone, appends a time zone designation (e.g., PDT) - * to designate times in the homeTimezone - * (e.g., user in New York, but viewing a trip planner for service based in Los Angeles). - */ -function getTimeFormat (timeFormat, homeTimezone) { - const userTimeZone = getUserTimezone() - const inHomeTimezone = homeTimezone && homeTimezone === userTimeZone - return inHomeTimezone ? timeFormat : `${timeFormat} z` -} - /** * Renders a stop time as either schedule or countdown, with an optional status icon. * Stop time that apply to a different day have an additional text showing the day of departure. @@ -83,11 +68,10 @@ const StopTimeCell = ({ const countdownString = secondsUntilDeparture < 60 ? soonText : formatDuration(secondsUntilDeparture) - const formattedTime = formatSecondsAfterMidnight( + const formattedTime = formatDepartureTime( departureTime, - // Only show timezone (e.g., PDT) if user is not in home time zone (e.g., user - // in New York, but viewing a trip planner for service based in Los Angeles). - getTimeFormat(timeFormat, homeTimezone) + timeFormat, + homeTimezone ) // We only want to show the day of the week if the arrival is on a // different day and we're not showing the countdown string. This avoids diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index b39e4fedc..722ac45fe 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -209,7 +209,7 @@ class StopViewer extends Component { } = this.props const { date, scheduleView } = this.state const hasStopTimesAndRoutes = !!(stopData && stopData.stopTimes && stopData.stopTimes.length > 0 && stopData.routes) - console.log(date) + let contents if (!hasStopTimesAndRoutes) { contents =
    No stop times found for date.
    diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index 5d0c0fcf0..6eb5e1650 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -14,14 +14,16 @@ } @keyframes yellowfade { - from { background: yellow; } - to { background: transparent; } + from { background: yellow; } + to { background: transparent; } } /* Used to briefly highlight an element and then fade to transparent. */ .highlighted-item { - animation-name: yellowfade; - animation-duration: 1.5s; + /* Waits until scrolling for this item is (almost) finished before starting the fading effect. */ + animation-delay: 0.5s; + animation-duration: 1.5s; + animation-name: yellowfade; } /* Remove arrows on date input */ diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 5e03538bf..f415f7696 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -3,7 +3,11 @@ import React from 'react' import { isBlank } from './ui' -const { formatDuration } = coreUtils.time +const { + formatDuration, + formatSecondsAfterMidnight, + getUserTimezone +} = coreUtils.time /** * Computes the seconds until departure for a given stop time, @@ -156,3 +160,66 @@ export function stopTimeComparator (a, b) { const bTime = b.serviceDay + b.scheduledDeparture return aTime - bTime } + +/** + * Find the stop time corresponding to the first departure + * (the closest departure past the current time). + */ +export function getFirstDepartureFromNow (mergedStopTimes) { + // Search starting from the last stop time (largest seconds until departure) for this pattern. + const lastStopTime = mergedStopTimes[mergedStopTimes.length - 1] + + let firstStopTime = lastStopTime + mergedStopTimes.forEach(stopTime => { + const firstStopTimeSeconds = getSecondsUntilDeparture(firstStopTime, true) + const stopTimeSeconds = getSecondsUntilDeparture(stopTime, true) + + if (stopTimeSeconds < firstStopTimeSeconds && stopTimeSeconds >= 0) { + firstStopTime = stopTime + } + }) + return firstStopTime +} + +/** + * Merges and sorts the stop time entries from the patterns in the given stopData object. + */ +export function mergeAndSortStopTimes (stopData) { + const stopTimesByPattern = getStopTimesByPattern(stopData) + + // Merge stop times, so that we can sort them across all route patterns. + // (stopData is assumed valid per StopViewer render condition.) + let mergedStopTimes = [] + Object.values(stopTimesByPattern).forEach(({ pattern, route, times }) => { + // Only add pattern if route info is returned by OTP. + if (routeIsValid(route, getRouteIdForPattern(pattern))) { + const filteredTimes = times + .filter(excludeLastStop) + .map(stopTime => { + // Add the route attribute and headsign to each stop time for rendering route info. + const headsign = isBlank(stopTime.headsign) ? pattern.headsign : stopTime.headsign + return { + ...stopTime, + route, + headsign + } + }) + mergedStopTimes = mergedStopTimes.concat(filteredTimes) + } + }) + + return mergedStopTimes.sort(stopTimeComparator) +} + +/** + * Formats a departure time according to a time format, + * and appends a time zone indication if the user is not in the "home time zone" + * (e.g. cases where a user in New York looks at a schedule in Los Angeles). + */ +export function formatDepartureTime (departureTime, timeFormat, homeTimezone) { + const userTimeZone = getUserTimezone() + const inHomeTimezone = homeTimezone && homeTimezone === userTimeZone + const format = inHomeTimezone ? timeFormat : `${timeFormat} z` + + return formatSecondsAfterMidnight(departureTime, format) +} From 8ebacce5ee93b982c345db26b44396c6ce1329d6 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 21 Dec 2020 21:55:34 -0500 Subject: [PATCH 049/265] fix(utils/viewer): Format time with timezone suffix. --- lib/components/viewers/stop-schedule-table.js | 5 ++++- lib/util/viewer.js | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 73499a5f9..f0ba5e29d 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -38,6 +38,9 @@ const TimeCell = styled.td` text-align: right; white-space: nowrap; ` +const TimeHeader = styled.th` + text-align: right; +` /** * Table showing scheduled departure times for the specified stop organized @@ -85,7 +88,7 @@ class StopScheduleTable extends Component { Block Route To - Departure + Departure diff --git a/lib/util/viewer.js b/lib/util/viewer.js index f415f7696..6eee30f48 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -1,3 +1,5 @@ +import moment from 'moment' +import 'moment-timezone' import coreUtils from '@opentripplanner/core-utils' import React from 'react' @@ -5,7 +7,6 @@ import { isBlank } from './ui' const { formatDuration, - formatSecondsAfterMidnight, getUserTimezone } = coreUtils.time @@ -221,5 +222,10 @@ export function formatDepartureTime (departureTime, timeFormat, homeTimezone) { const inHomeTimezone = homeTimezone && homeTimezone === userTimeZone const format = inHomeTimezone ? timeFormat : `${timeFormat} z` - return formatSecondsAfterMidnight(departureTime, format) + // FIXME: fix OTP-UI's formatSecondsAfterMidnight method per below. + return moment() + .tz(homeTimezone) + .startOf('day') + .seconds(departureTime) + .format(format) } From fac518e21590d006975dd444f26a0c07d7de7328 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 22 Dec 2020 10:09:36 -0500 Subject: [PATCH 050/265] refactor(actions/api): Pass stop viewer config params as before to fetch departures. --- lib/actions/api.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index bd7529f9c..195b1e801 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -9,7 +9,7 @@ import { createAction } from 'redux-actions' import qs from 'qs' import { rememberPlace } from './map' -import { queryIsValid } from '../util/state' +import { getStopViewerConfig, queryIsValid } from '../util/state' import { getSecureFetchOptions } from '../util/middleware' if (typeof (fetch) === 'undefined') require('isomorphic-fetch') @@ -556,9 +556,8 @@ export function findStopTimesForStop (params) { } // If other params not provided, fall back on defaults from stop viewer config. - // const queryParams = { ...getStopViewerConfig(getState().otp), ...otherParams } - // TODO: parform an additional query with ^^ and without date path if the one below returns no stop times at all. - const queryParams = {...otherParams} + // Note: query params don't apply with the OTP /date endpoint. + const queryParams = { ...getStopViewerConfig(getState().otp), ...otherParams } // If no start time is provided and no date is provided in params, // pass in the current time. Note: this is not From 4bd58959771c9acc35dd8eeea1bb5e4ace13c818 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 22 Dec 2020 10:10:13 -0500 Subject: [PATCH 051/265] refactor(StopTimeTable): Rename to LiveStopTimes --- .../viewers/{stop-live-table.js => live-stop-times.js} | 4 ++-- lib/components/viewers/stop-viewer.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename lib/components/viewers/{stop-live-table.js => live-stop-times.js} (98%) diff --git a/lib/components/viewers/stop-live-table.js b/lib/components/viewers/live-stop-times.js similarity index 98% rename from lib/components/viewers/stop-live-table.js rename to lib/components/viewers/live-stop-times.js index b4876e505..d4a824809 100644 --- a/lib/components/viewers/stop-live-table.js +++ b/lib/components/viewers/live-stop-times.js @@ -22,7 +22,7 @@ const defaultState = { * Table showing next arrivals (refreshing every 10 seconds) for the specified * stop organized by route pattern. */ -class StopLiveTable extends Component { +class LiveStopTimes extends Component { state = defaultState _refreshStopTimes = () => { @@ -157,4 +157,4 @@ class StopLiveTable extends Component { } } -export default StopLiveTable +export default LiveStopTimes diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 722ac45fe..89d69c78f 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -12,7 +12,7 @@ import Icon from '../narrative/icon' import * as uiActions from '../../actions/ui' import * as apiActions from '../../actions/api' import * as mapActions from '../../actions/map' -import StopLiveTable from './stop-live-table' +import LiveStopTimes from './live-stop-times' import StopScheduleTable from './stop-schedule-table' import { getShowUserSettings, getStopViewerConfig } from '../../util/state' @@ -224,7 +224,7 @@ class StopViewer extends Component { ) } else { contents = ( - Date: Tue, 22 Dec 2020 13:23:22 -0500 Subject: [PATCH 052/265] refactor(StopViewer): Display banner if user not in the homeTimezone. --- lib/components/viewers/stop-schedule-table.js | 17 +++++-------- lib/components/viewers/stop-time-cell.js | 11 +++----- lib/components/viewers/stop-viewer.js | 25 +++++++++++++++++-- lib/util/viewer.js | 25 +------------------ 4 files changed, 34 insertions(+), 44 deletions(-) diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index f0ba5e29d..562f9788e 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -1,13 +1,15 @@ import moment from 'moment' +import coreUtils from '@opentripplanner/core-utils' import React, { Component, createRef } from 'react' import styled from 'styled-components' import { - formatDepartureTime, getFirstDepartureFromNow, mergeAndSortStopTimes } from '../../util/viewer' +const { formatSecondsAfterMidnight } = coreUtils.time + // Styles for the schedule table and its contents. const StyledTable = styled.table` border-spacing: collapse; @@ -38,9 +40,6 @@ const TimeCell = styled.td` text-align: right; white-space: nowrap; ` -const TimeHeader = styled.th` - text-align: right; -` /** * Table showing scheduled departure times for the specified stop organized @@ -69,7 +68,7 @@ class StopScheduleTable extends Component { } render () { - const { date, homeTimezone, stopData, timeFormat } = this.props + const { date, stopData, timeFormat } = this.props const mergedStopTimes = mergeAndSortStopTimes(stopData) // FIXME: Shift today back one day if the current time is between midnight and the end of the service day. @@ -88,7 +87,7 @@ class StopScheduleTable extends Component { Block Route To - Departure + Departure @@ -107,11 +106,7 @@ class StopScheduleTable extends Component { ? nextStopTime === highlightedStopTime : highlightRow const routeName = route.shortName ? route.shortName : route.longName - const formattedTime = formatDepartureTime( - stopTime.scheduledDeparture, - timeFormat, - homeTimezone - ) + const formattedTime = formatSecondsAfterMidnight(stopTime.scheduledDeparture, timeFormat) // Add ref to scroll to the first stop time departing from now. const refProp = scrollToRow ? this.targetDepartureRef : null diff --git a/lib/components/viewers/stop-time-cell.js b/lib/components/viewers/stop-time-cell.js index e1440131b..c0c5a8517 100644 --- a/lib/components/viewers/stop-time-cell.js +++ b/lib/components/viewers/stop-time-cell.js @@ -5,9 +5,9 @@ import PropTypes from 'prop-types' import React from 'react' import Icon from '../narrative/icon' -import { formatDepartureTime, getSecondsUntilDeparture } from '../../util/viewer' +import { getSecondsUntilDeparture } from '../../util/viewer' -const { formatDuration } = coreUtils.time +const { formatDuration, formatSecondsAfterMidnight } = coreUtils.time const ONE_HOUR_IN_SECONDS = 3600 const ONE_DAY_IN_SECONDS = 86400 @@ -68,11 +68,8 @@ const StopTimeCell = ({ const countdownString = secondsUntilDeparture < 60 ? soonText : formatDuration(secondsUntilDeparture) - const formattedTime = formatDepartureTime( - departureTime, - timeFormat, - homeTimezone - ) + const formattedTime = formatSecondsAfterMidnight(departureTime, timeFormat) + // We only want to show the day of the week if the arrival is on a // different day and we're not showing the countdown string. This avoids // cases such as when it's Wednesday at 11:55pm and an arrival occurs at diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 89d69c78f..093efe05f 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -4,7 +4,7 @@ import coreUtils from '@opentripplanner/core-utils' import FromToLocationPicker from '@opentripplanner/from-to-location-picker' import PropTypes from 'prop-types' import React, { Component } from 'react' -import { Button } from 'react-bootstrap' +import { Alert, Button, Glyphicon } from 'react-bootstrap' import { connect } from 'react-redux' import styled from 'styled-components' @@ -18,6 +18,7 @@ import { getShowUserSettings, getStopViewerConfig } from '../../util/state' const { getTimeFormat, + getUserTimezone, OTP_API_DATE_FORMAT } = coreUtils.time @@ -153,14 +154,32 @@ class StopViewer extends Component { * TODO: Can this use SetFromToButtons? */ _renderControls = () => { - const {stopData} = this.props + const {homeTimezone, stopData} = this.props const {scheduleView} = this.state + const userTimeZone = getUserTimezone() + const inHomeTimezone = homeTimezone && homeTimezone === userTimeZone + // Rewrite stop ID to not include Agency prefix, if present // TODO: make this functionality configurable? let stopId if (stopData && stopData.id) { stopId = stopData.id.includes(':') ? stopData.id.split(':')[1] : stopData.id } + + let timezoneWarning + if (!inHomeTimezone) { + const timezoneCode = moment().tz(homeTimezone).format('z') + + // Display a banner about the departure timezone if user's timezone is not the configured 'homeTimezone' + // (e.g. cases where a user in New York looks at a schedule in Los Angeles). + timezoneWarning = ( + + Departure times + are shown in {timezoneCode}. + + ) + } + return (
    @@ -190,6 +209,8 @@ class StopViewer extends Component { required onChange={this.handleDateChange} />} + + {timezoneWarning}
    ) } diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 6eee30f48..90dea11e0 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -1,14 +1,9 @@ -import moment from 'moment' -import 'moment-timezone' import coreUtils from '@opentripplanner/core-utils' import React from 'react' import { isBlank } from './ui' -const { - formatDuration, - getUserTimezone -} = coreUtils.time +const { formatDuration } = coreUtils.time /** * Computes the seconds until departure for a given stop time, @@ -211,21 +206,3 @@ export function mergeAndSortStopTimes (stopData) { return mergedStopTimes.sort(stopTimeComparator) } - -/** - * Formats a departure time according to a time format, - * and appends a time zone indication if the user is not in the "home time zone" - * (e.g. cases where a user in New York looks at a schedule in Los Angeles). - */ -export function formatDepartureTime (departureTime, timeFormat, homeTimezone) { - const userTimeZone = getUserTimezone() - const inHomeTimezone = homeTimezone && homeTimezone === userTimeZone - const format = inHomeTimezone ? timeFormat : `${timeFormat} z` - - // FIXME: fix OTP-UI's formatSecondsAfterMidnight method per below. - return moment() - .tz(homeTimezone) - .startOf('day') - .seconds(departureTime) - .format(format) -} From 172352f9950ae8eae8acf33925f48dce6a06e59c Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 22 Dec 2020 13:27:57 -0500 Subject: [PATCH 053/265] test(StopViewer): Update snapshots --- .../viewers/__snapshots__/stop-viewer.js.snap | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap index ee1a76cac..ff6079f65 100644 --- a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap +++ b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap @@ -499,7 +499,7 @@ exports[`components > viewers > stop viewer should render countdown times after
    - viewers > stop viewer should render countdown times after 00:17
    - +
    @@ -1454,7 +1454,7 @@ exports[`components > viewers > stop viewer should render countdown times for st
    - viewers > stop viewer should render countdown times for st 00:17
    - +
    @@ -2319,7 +2319,7 @@ exports[`components > viewers > stop viewer should render times after midnight w
    - viewers > stop viewer should render times after midnight w 00:17
    - +
    @@ -3640,7 +3640,7 @@ exports[`components > viewers > stop viewer should render with OTP transit index
    - viewers > stop viewer should render with OTP transit index 17:50
    - +
    @@ -6040,7 +6040,7 @@ exports[`components > viewers > stop viewer should render with TriMet transit in
    - viewers > stop viewer should render with TriMet transit in 17:38
    - +
    From 1a8d55937a4e889c5deee6fb3c022f20c4174ad4 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 22 Dec 2020 13:42:45 -0500 Subject: [PATCH 054/265] style: Alphabetize imports and props --- lib/components/viewers/live-stop-times.js | 15 ++++++++------- lib/components/viewers/stop-time-cell.js | 4 ++-- lib/components/viewers/stop-viewer.js | 6 +++--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/components/viewers/live-stop-times.js b/lib/components/viewers/live-stop-times.js index d4a824809..794ab9839 100644 --- a/lib/components/viewers/live-stop-times.js +++ b/lib/components/viewers/live-stop-times.js @@ -114,13 +114,13 @@ class LiveStopTimes extends Component { return routeIsValid(route, getRouteIdForPattern(pattern)) ? ( ) @@ -134,16 +134,17 @@ class LiveStopTimes extends Component { {/* eslint-disable-next-line jsx-a11y/label-has-for */}
    - {stopTime.realtimeState === 'UPDATED' - ? getStatusLabel(stopTime.departureDelay) - :
    - Scheduled -
    - } +
    ) diff --git a/lib/components/viewers/status-label.js b/lib/components/viewers/status-label.js new file mode 100644 index 000000000..06e63194d --- /dev/null +++ b/lib/components/viewers/status-label.js @@ -0,0 +1,43 @@ +import React from 'react' +import styled from 'styled-components' + +import { getTripStatus } from '../../util/viewer' + +/** + * Renders a colored label denoting a trip realtime status. + */ +const StatusLabel = ({ stopTime }) => { + const { departureDelay: delay, realtimeState } = stopTime + const preset = getTripStatus(realtimeState === 'UPDATED', delay) + + return ( + + + + ) +} + +// Keep the '5 min' string on the same line. +export const DelayText = styled.span` + white-space: nowrap; +` + +/** + * A simple label that renders a string such as '5 min late' or 'on time' + * while keeping the '5 min' portion on the same line. + */ +export const BaseStatusLabel = ({ delay, preset }) => { + const { getFormattedDuration, status } = preset + + if (getFormattedDuration) { + return ( + <> + {getFormattedDuration(delay)} {status} + + ) + } + + return <>{status} +} + +export default StatusLabel diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index 9233de9f2..5cd2819a9 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -142,14 +142,15 @@ } .otp .stop-viewer .trip-table .status-label { - width: 100%; - text-align: center; - text-transform: uppercase; - font-weight: 500; border-radius: 2px; - font-size: 11px; color: white; + display: block; + font-size: 11px; + font-weight: 500; padding: 2px 0px 0px 0px; + text-align: center; + text-transform: uppercase; + width: 100%; } /* trip viewer styles */ diff --git a/lib/util/viewer.js b/lib/util/viewer.js index c1f8d1aaf..c6735706e 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -100,34 +100,6 @@ export function getRouteIdForPattern (pattern) { return routeId } -// helper method to generate status label -export function getStatusLabel (delay) { - // late departure - if (delay > 60) { - return ( -
    - {formatDuration(delay)} Late -
    - ) - } - - // early departure - if (delay < -60) { - return ( -
    - {formatDuration(Math.abs(delay))} Early -
    - ) - } - - // on-time departure - return ( -
    - On Time -
    - ) -} - export function getStopTimesByPattern (stopData) { const stopTimesByPattern = {} if (stopData && stopData.routes && stopData.stopTimes) { @@ -185,3 +157,48 @@ export function getModeFromRoute (route) { } return route.mode || modeLookup[route.type] } + +/** + * Preset strings and colors for transit trip realtime status. + */ +export const STATUS_PRESETS = { + EARLY: { + color: '#337ab7', + getFormattedDuration: delay => formatDuration(Math.abs(delay)), + status: 'early' + }, + LATE: { + color: '#d9534f', + getFormattedDuration: delay => formatDuration(delay), + status: 'late' + }, + ON_TIME: { + color: '#5cb85c', + status: 'on time' + }, + SCHEDULED: { + color: '#bbb', + status: 'scheduled' + } +} + +/** + * Obtains the preset status above (on-time, late...) for the specified realtime status and delay. + */ +export function getTripStatus (isRealtime, delay) { + if (isRealtime) { + if (delay > 60) { + // late departure + return STATUS_PRESETS.LATE + } else if (delay < -60) { + // early departure + return STATUS_PRESETS.EARLY + } else { + // on-time departure + return STATUS_PRESETS.ON_TIME + } + } else { + // Schedule only + return STATUS_PRESETS.SCHEDULED + } +} From ced7d9221f0ad6ada60563f452610feb8f9ed297 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 22 Dec 2020 13:29:56 -0800 Subject: [PATCH 056/265] refactor(utils/viewer): Renam arg tweak comment per PR feedback --- lib/util/viewer.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 90dea11e0..a77994f72 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -158,15 +158,15 @@ export function stopTimeComparator (a, b) { } /** - * Find the stop time corresponding to the first departure + * Finds the stop time corresponding to the first departure * (the closest departure past the current time). */ -export function getFirstDepartureFromNow (mergedStopTimes) { - // Search starting from the last stop time (largest seconds until departure) for this pattern. - const lastStopTime = mergedStopTimes[mergedStopTimes.length - 1] +export function getFirstDepartureFromNow (stopTimes) { + // Search starting from the last stop time (largest seconds until departure). + const lastStopTime = stopTimes[stopTimes.length - 1] let firstStopTime = lastStopTime - mergedStopTimes.forEach(stopTime => { + stopTimes.forEach(stopTime => { const firstStopTimeSeconds = getSecondsUntilDeparture(firstStopTime, true) const stopTimeSeconds = getSecondsUntilDeparture(stopTime, true) From 08253eff2bb186584000d7209a5635d6de86dbfb Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 22 Dec 2020 17:47:25 -0500 Subject: [PATCH 057/265] fix(StopViewer): Fix autorefresh in live arrivals. Also perform a fetch when switching to live arrivals. Use styled components on timezone alert box. --- lib/actions/api.js | 8 +++++--- lib/components/viewers/stop-viewer.js | 25 +++++++++++++++++-------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index 195b1e801..f29f0d12f 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -546,7 +546,7 @@ const findStopTimesForStopError = createAction('FIND_STOP_TIMES_FOR_STOP_ERROR') /** * Stop times for stop query (used in stop viewer). */ -export function findStopTimesForStop (params) { +export function findStopTimesForStop (params, clearData) { return function (dispatch, getState) { const { date, stopId, ...otherParams } = params let datePath = '' @@ -570,11 +570,13 @@ export function findStopTimesForStop (params) { queryParams.startTime = nowInSeconds } - // Clear existing stop times to prevent extra rendering of stop viewer while schedule is + // If reuqested, clear existing stop times to prevent extra rendering of stop viewer while schedule is // fetched and a schedule for the same date/stop was shown before. This also has the nice side // effect to force a render, initially showing the top row, after changing the schedule view date // and all schedules for the new date are the same as the previous date. - dispatch(findStopTimesForStopResponse({ stopId, stopTimes: [] })) + if (clearData) { + dispatch(findStopTimesForStopResponse({ stopId, stopTimes: [] })) + } // (Re-)fetch stop times for the stop. dispatch(createQueryAction( diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 5fe5c1f67..6bb7a0705 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -32,6 +32,12 @@ const Scrollable = styled.div` height: 100%; overflow-y: auto; ` +// Alert with custom styles +const StyledAlert = styled(Alert)` + margin: 5px 0; + padding: 5px 10px; + text-align: center; +` class StopViewer extends Component { state = defaultState @@ -70,21 +76,24 @@ class StopViewer extends Component { else rememberStop(stopData) } - _findStopTimesForDate = date => { + _findStopTimesForDate = (date) => { const { findStopTimesForStop, viewedStop } = this.props + // When toggling views or changing dates, + // fetch stop times, and clear the existing stop times while fetching. findStopTimesForStop({ date, stopId: viewedStop.stopId - }) + }, true) } _toggleScheduleView = () => { const {date, scheduleView: isShowingScheduleView} = this.state - if (!isShowingScheduleView) { - // If not currently showing schedule view, fetch schedules for current date. - this._findStopTimesForDate(date) - } + + // If not currently showing schedule view, fetch schedules for current date. + // Otherwise fetch next arrivals. + this._findStopTimesForDate(!isShowingScheduleView ? date : null) + this.setState({scheduleView: !isShowingScheduleView}) } @@ -173,10 +182,10 @@ class StopViewer extends Component { // Display a banner about the departure timezone if user's timezone is not the configured 'homeTimezone' // (e.g. cases where a user in New York looks at a schedule in Los Angeles). timezoneWarning = ( - + Departure times are shown in {timezoneCode}. - + ) } From 86b1e863f443b24dd9ed5117ddc48cee85f331e2 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 22 Dec 2020 15:03:34 -0800 Subject: [PATCH 058/265] style(StopViewer): Remove () around single arg --- lib/components/viewers/stop-viewer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 6bb7a0705..c614fb48d 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -76,7 +76,7 @@ class StopViewer extends Component { else rememberStop(stopData) } - _findStopTimesForDate = (date) => { + _findStopTimesForDate = date => { const { findStopTimesForStop, viewedStop } = this.props // When toggling views or changing dates, From 4a8695a4113acda976cddd35c2c8f39e8318ad9e Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 23 Dec 2020 12:52:28 -0800 Subject: [PATCH 059/265] refactor(BaseStatusLabel): Remove color override for 'scheduled' status, various refactors. --- .../line-itin/realtime-time-column.js | 16 +++---- lib/components/viewers/base-status-label.js | 32 ++++++++++++++ lib/components/viewers/pattern-row.js | 23 +++++++++- lib/components/viewers/status-label.js | 43 ------------------- lib/util/viewer.js | 13 +++--- 5 files changed, 67 insertions(+), 60 deletions(-) create mode 100644 lib/components/viewers/base-status-label.js delete mode 100644 lib/components/viewers/status-label.js diff --git a/lib/components/narrative/line-itin/realtime-time-column.js b/lib/components/narrative/line-itin/realtime-time-column.js index ef6313f0c..d5a123ce9 100644 --- a/lib/components/narrative/line-itin/realtime-time-column.js +++ b/lib/components/narrative/line-itin/realtime-time-column.js @@ -8,8 +8,8 @@ import PropTypes from 'prop-types' import React from 'react' import styled from 'styled-components' -import { getTripStatus, STATUS_PRESETS } from '../../../util/viewer' -import { DelayText, BaseStatusLabel } from '../../viewers/status-label' +import { getTripStatus, TRIP_STATUS } from '../../../util/viewer' +import BaseStatusLabel, { DelayText } from '../../viewers/base-status-label' const TimeStruck = styled.div` text-decoration: line-through; @@ -51,8 +51,8 @@ function RealtimeTimeColumn ({ const originalFormattedTime = originalTimeMillis && formatTime(originalTimeMillis, timeOptions) - const preset = getTripStatus(isRealtimeTransitLeg, delaySeconds) - const isEarlyOrLate = preset === STATUS_PRESETS.EARLY || preset === STATUS_PRESETS.LATE + const status = getTripStatus(isRealtimeTransitLeg, delaySeconds) + const isEarlyOrLate = status === TRIP_STATUS.EARLY || status === TRIP_STATUS.LATE // If the transit vehicle is not on time, strike the original scheduled time // and display the updated time underneath. @@ -66,15 +66,13 @@ function RealtimeTimeColumn ({ :
    {formattedTime}
    return ( -
    +
    {renderedTime} - +
    ) } -export default RealtimeTimeColumn - RealtimeTimeColumn.propTypes = { isDestination: PropTypes.bool.isRequired, leg: legType.isRequired, @@ -84,3 +82,5 @@ RealtimeTimeColumn.propTypes = { RealtimeTimeColumn.defaultProps = { timeOptions: null } + +export default RealtimeTimeColumn diff --git a/lib/components/viewers/base-status-label.js b/lib/components/viewers/base-status-label.js new file mode 100644 index 000000000..9028cfe9f --- /dev/null +++ b/lib/components/viewers/base-status-label.js @@ -0,0 +1,32 @@ +import React from 'react' +import styled from 'styled-components' + +// If shown, keep the '5 min' portion of the status string on the same line. +export const DelayText = styled.span` + white-space: nowrap; +` + +/** + * A simple label that renders a string such as '5 min late' or 'on time' + * while keeping the '5 min' portion on the same line. + */ +const BaseStatusLabel = ({ className, delay, style, tripStatus }) => { + const { getFormattedDuration, status } = tripStatus + let content + + if (typeof getFormattedDuration === 'function') { + content = ( + <> + {getFormattedDuration(delay)} {status} + + ) + } else { + content = status + } + + return ( +
    {content}
    + ) +} + +export default BaseStatusLabel diff --git a/lib/components/viewers/pattern-row.js b/lib/components/viewers/pattern-row.js index 0bb926fd6..afb730201 100644 --- a/lib/components/viewers/pattern-row.js +++ b/lib/components/viewers/pattern-row.js @@ -2,8 +2,8 @@ import React, { Component } from 'react' import { VelocityTransitionGroup } from 'velocity-react' import Icon from '../narrative/icon' -import { getFormattedStopTime } from '../../util/viewer' -import StatusLabel from './status-label' +import { getFormattedStopTime, getTripStatus } from '../../util/viewer' +import BaseStatusLabel from './base-status-label' /** * Represents a single pattern row for displaying arrival times in the stop @@ -206,3 +206,22 @@ export default class PatternRow extends Component { ) } } + +/** + * Renders a colored label denoting a trip realtime status. + */ +const StatusLabel = ({ stopTime }) => { + const { departureDelay: delay, realtimeState } = stopTime + const status = getTripStatus(realtimeState === 'UPDATED', delay) + // Use a default background color if the status object doesn't set a color (e.g. for 'Scheduled' status). + const backgroundColor = status.color || '#bbb' + + return ( + + ) +} diff --git a/lib/components/viewers/status-label.js b/lib/components/viewers/status-label.js deleted file mode 100644 index 06e63194d..000000000 --- a/lib/components/viewers/status-label.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react' -import styled from 'styled-components' - -import { getTripStatus } from '../../util/viewer' - -/** - * Renders a colored label denoting a trip realtime status. - */ -const StatusLabel = ({ stopTime }) => { - const { departureDelay: delay, realtimeState } = stopTime - const preset = getTripStatus(realtimeState === 'UPDATED', delay) - - return ( - - - - ) -} - -// Keep the '5 min' string on the same line. -export const DelayText = styled.span` - white-space: nowrap; -` - -/** - * A simple label that renders a string such as '5 min late' or 'on time' - * while keeping the '5 min' portion on the same line. - */ -export const BaseStatusLabel = ({ delay, preset }) => { - const { getFormattedDuration, status } = preset - - if (getFormattedDuration) { - return ( - <> - {getFormattedDuration(delay)} {status} - - ) - } - - return <>{status} -} - -export default StatusLabel diff --git a/lib/util/viewer.js b/lib/util/viewer.js index c6735706e..0901f623e 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -161,7 +161,7 @@ export function getModeFromRoute (route) { /** * Preset strings and colors for transit trip realtime status. */ -export const STATUS_PRESETS = { +export const TRIP_STATUS = { EARLY: { color: '#337ab7', getFormattedDuration: delay => formatDuration(Math.abs(delay)), @@ -177,28 +177,27 @@ export const STATUS_PRESETS = { status: 'on time' }, SCHEDULED: { - color: '#bbb', status: 'scheduled' } } /** - * Obtains the preset status above (on-time, late...) for the specified realtime status and delay. + * Obtains one of the preset states above (on-time, late...) for the specified realtime status and delay. */ export function getTripStatus (isRealtime, delay) { if (isRealtime) { if (delay > 60) { // late departure - return STATUS_PRESETS.LATE + return TRIP_STATUS.LATE } else if (delay < -60) { // early departure - return STATUS_PRESETS.EARLY + return TRIP_STATUS.EARLY } else { // on-time departure - return STATUS_PRESETS.ON_TIME + return TRIP_STATUS.ON_TIME } } else { // Schedule only - return STATUS_PRESETS.SCHEDULED + return TRIP_STATUS.SCHEDULED } } From 18d2ad34cbe71bb22bf09392bd33791d58dfce3b Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 29 Dec 2020 17:41:50 -0500 Subject: [PATCH 060/265] refactor(favorite-locations): Move location components to subfolder. --- .../{ => favorite-locations}/favorite-location-controls.js | 4 ++-- .../user/{ => favorite-locations}/favorite-location.js | 0 .../user/{ => favorite-locations}/favorite-locations-pane.js | 2 +- .../user/{ => favorite-locations}/fixed-favorite-location.js | 2 +- .../user/{ => favorite-locations}/new-favorite-location.js | 2 +- lib/components/user/user-account-screen.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename lib/components/user/{ => favorite-locations}/favorite-location-controls.js (97%) rename lib/components/user/{ => favorite-locations}/favorite-location.js (100%) rename lib/components/user/{ => favorite-locations}/favorite-locations-pane.js (96%) rename lib/components/user/{ => favorite-locations}/fixed-favorite-location.js (97%) rename lib/components/user/{ => favorite-locations}/new-favorite-location.js (97%) diff --git a/lib/components/user/favorite-location-controls.js b/lib/components/user/favorite-locations/favorite-location-controls.js similarity index 97% rename from lib/components/user/favorite-location-controls.js rename to lib/components/user/favorite-locations/favorite-location-controls.js index 4b9c970fe..fc7950fac 100644 --- a/lib/components/user/favorite-location-controls.js +++ b/lib/components/user/favorite-locations/favorite-location-controls.js @@ -8,8 +8,8 @@ import React from 'react' import { DropdownButton, InputGroup, MenuItem } from 'react-bootstrap' import styled, { css } from 'styled-components' -import connectLocationField from '../form/connect-location-field' -import Icon from '../narrative/icon' +import connectLocationField from '../../form/connect-location-field' +import Icon from '../../narrative/icon' /** * This module contains components common to the favorite location components. diff --git a/lib/components/user/favorite-location.js b/lib/components/user/favorite-locations/favorite-location.js similarity index 100% rename from lib/components/user/favorite-location.js rename to lib/components/user/favorite-locations/favorite-location.js diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations/favorite-locations-pane.js similarity index 96% rename from lib/components/user/favorite-locations-pane.js rename to lib/components/user/favorite-locations/favorite-locations-pane.js index 9891dc649..5b27860d9 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations/favorite-locations-pane.js @@ -2,7 +2,7 @@ import { FieldArray } from 'formik' import React from 'react' import { ControlLabel } from 'react-bootstrap' -import { isHomeOrWork } from '../../util/user' +import { isHomeOrWork } from '../../../util/user' import FavoriteLocation from './favorite-location' import FixedFavoriteLocation from './fixed-favorite-location' import NewFavoriteLocation from './new-favorite-location' diff --git a/lib/components/user/fixed-favorite-location.js b/lib/components/user/favorite-locations/fixed-favorite-location.js similarity index 97% rename from lib/components/user/fixed-favorite-location.js rename to lib/components/user/favorite-locations/fixed-favorite-location.js index 64db73c6b..b487edbdf 100644 --- a/lib/components/user/fixed-favorite-location.js +++ b/lib/components/user/favorite-locations/fixed-favorite-location.js @@ -6,7 +6,7 @@ import { } from 'react-bootstrap' import styled from 'styled-components' -import Icon from '../narrative/icon' +import Icon from '../../narrative/icon' import { FavoriteLocationField, FixedLocationType, diff --git a/lib/components/user/new-favorite-location.js b/lib/components/user/favorite-locations/new-favorite-location.js similarity index 97% rename from lib/components/user/new-favorite-location.js rename to lib/components/user/favorite-locations/new-favorite-location.js index 99b8adf9a..65031a363 100644 --- a/lib/components/user/new-favorite-location.js +++ b/lib/components/user/favorite-locations/new-favorite-location.js @@ -4,7 +4,7 @@ import { InputGroup } from 'react-bootstrap' -import Icon from '../narrative/icon' +import Icon from '../../narrative/icon' import { FavoriteLocationField, FIELD_HEIGHT_PX, diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.js index c8290fc3b..abc62bec0 100644 --- a/lib/components/user/user-account-screen.js +++ b/lib/components/user/user-account-screen.js @@ -12,7 +12,7 @@ import { isNewUser, isHome, isWork } from '../../util/user' import DesktopNav from '../app/desktop-nav' import AccountSetupFinishPane from './account-setup-finish-pane' import ExistingAccountDisplay from './existing-account-display' -import FavoriteLocationsPane from './favorite-locations-pane' +import FavoriteLocationsPane from './favorite-locations/favorite-locations-pane' import NewAccountWizard from './new-account-wizard' import NotificationPrefsPane from './notification-prefs-pane' import TermsOfUsePane from './terms-of-use-pane' From 38630f24e44ee43a1f294b6971094f72261519b2 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 4 Jan 2021 16:37:21 -0500 Subject: [PATCH 061/265] refactor(realtime-status-label): encapsulate more of the status label logic in component --- .../line-itin/realtime-time-column.js | 43 +++------ lib/components/viewers/base-status-label.js | 32 ------- lib/components/viewers/pattern-row.js | 31 ++---- .../viewers/realtime-status-label.js | 95 +++++++++++++++++++ lib/util/viewer.js | 42 +++----- 5 files changed, 130 insertions(+), 113 deletions(-) delete mode 100644 lib/components/viewers/base-status-label.js create mode 100644 lib/components/viewers/realtime-status-label.js diff --git a/lib/components/narrative/line-itin/realtime-time-column.js b/lib/components/narrative/line-itin/realtime-time-column.js index d5a123ce9..1208d1afc 100644 --- a/lib/components/narrative/line-itin/realtime-time-column.js +++ b/lib/components/narrative/line-itin/realtime-time-column.js @@ -8,21 +8,13 @@ import PropTypes from 'prop-types' import React from 'react' import styled from 'styled-components' -import { getTripStatus, TRIP_STATUS } from '../../../util/viewer' -import BaseStatusLabel, { DelayText } from '../../viewers/base-status-label' +import RealtimeStatusLabel, { DelayText, MainContent } from '../../viewers/realtime-status-label' -const TimeStruck = styled.div` - text-decoration: line-through; -` - -const TimeBlock = styled.div` - line-height: 1em; - margin-bottom: 4px; -` - -const StyledStatusLabel = styled(BaseStatusLabel)` - font-size: 80%; - line-height: 1em; +const StyledStatusLabel = styled(RealtimeStatusLabel)` + ${MainContent} { + font-size: 80%; + line-height: 1em; + } ${DelayText} { display: block; } @@ -51,25 +43,12 @@ function RealtimeTimeColumn ({ const originalFormattedTime = originalTimeMillis && formatTime(originalTimeMillis, timeOptions) - const status = getTripStatus(isRealtimeTransitLeg, delaySeconds) - const isEarlyOrLate = status === TRIP_STATUS.EARLY || status === TRIP_STATUS.LATE - - // If the transit vehicle is not on time, strike the original scheduled time - // and display the updated time underneath. - const renderedTime = isEarlyOrLate - ? ( - - {originalFormattedTime} -
    {formattedTime}
    -
    - ) - :
    {formattedTime}
    - return ( -
    - {renderedTime} - -
    + ) } diff --git a/lib/components/viewers/base-status-label.js b/lib/components/viewers/base-status-label.js deleted file mode 100644 index 9028cfe9f..000000000 --- a/lib/components/viewers/base-status-label.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react' -import styled from 'styled-components' - -// If shown, keep the '5 min' portion of the status string on the same line. -export const DelayText = styled.span` - white-space: nowrap; -` - -/** - * A simple label that renders a string such as '5 min late' or 'on time' - * while keeping the '5 min' portion on the same line. - */ -const BaseStatusLabel = ({ className, delay, style, tripStatus }) => { - const { getFormattedDuration, status } = tripStatus - let content - - if (typeof getFormattedDuration === 'function') { - content = ( - <> - {getFormattedDuration(delay)} {status} - - ) - } else { - content = status - } - - return ( -
    {content}
    - ) -} - -export default BaseStatusLabel diff --git a/lib/components/viewers/pattern-row.js b/lib/components/viewers/pattern-row.js index afb730201..583bd9014 100644 --- a/lib/components/viewers/pattern-row.js +++ b/lib/components/viewers/pattern-row.js @@ -2,8 +2,8 @@ import React, { Component } from 'react' import { VelocityTransitionGroup } from 'velocity-react' import Icon from '../narrative/icon' -import { getFormattedStopTime, getTripStatus } from '../../util/viewer' -import BaseStatusLabel from './base-status-label' +import { getFormattedStopTime } from '../../util/viewer' +import RealtimeStatusLabel from './realtime-status-label' /** * Represents a single pattern row for displaying arrival times in the stop @@ -91,6 +91,7 @@ export default class PatternRow extends Component { {/* list of upcoming trips */} {hasStopTimes && ( sortedStopTimes.map((stopTime, i) => { + const { departureDelay: delay, realtimeState } = stopTime return (
    - +
    ) @@ -206,22 +212,3 @@ export default class PatternRow extends Component { ) } } - -/** - * Renders a colored label denoting a trip realtime status. - */ -const StatusLabel = ({ stopTime }) => { - const { departureDelay: delay, realtimeState } = stopTime - const status = getTripStatus(realtimeState === 'UPDATED', delay) - // Use a default background color if the status object doesn't set a color (e.g. for 'Scheduled' status). - const backgroundColor = status.color || '#bbb' - - return ( - - ) -} diff --git a/lib/components/viewers/realtime-status-label.js b/lib/components/viewers/realtime-status-label.js new file mode 100644 index 000000000..2e5a93462 --- /dev/null +++ b/lib/components/viewers/realtime-status-label.js @@ -0,0 +1,95 @@ +import { formatDuration } from '@opentripplanner/core-utils/lib/time' +import React from 'react' +import styled from 'styled-components' + +import { getTripStatus, REALTIME_STATUS } from '../../util/viewer' + +// If shown, keep the '5 min' portion of the status string on the same line. +export const DelayText = styled.span` + white-space: nowrap; +` + +export const MainContent = styled.div`` + +const Container = styled.div` +${props => props.withBackground + ? `background-color: ${props.color};` + : `color: ${props.color};` +} +` + +const TimeStruck = styled.div` + text-decoration: line-through; +` + +const TimeBlock = styled.div` + line-height: 1em; + margin-bottom: 4px; +` + +const STATUS = { + EARLY: { + color: '#337ab7', + label: 'early' + }, + LATE: { + color: '#d9534f', + label: 'late' + }, + ON_TIME: { + color: '#5cb85c', + label: 'on time' + }, + SCHEDULED: { + label: 'scheduled' + } +} + +/** + * This component renders a string such as '5 min late' or 'on time' + * while keeping the '5 min' portion on the same line. + * + * If the formatted time/original time values (e.g. 5:11 pm) are provided, they + * will be rendered above the status. Also, this can optionally be rendered with + * a background color for a label-like presentation. + */ +const RealtimeStatusLabel = ({ withBackground, className, delay, isRealtime, originalTime, time }) => { + const status = getTripStatus(isRealtime, delay) + const isEarlyOrLate = status === REALTIME_STATUS.EARLY || status === REALTIME_STATUS.LATE + // Use a default background color if the status object doesn't set a color + // (e.g. for 'Scheduled' status), but only in withBackground mode. + const color = STATUS[status].color || (withBackground && '#bbb') + // Render time if provided. + let renderedTime + if (time) { + // If transit vehicle is not on time, strike the original scheduled time + // and display the updated time underneath. + renderedTime = isEarlyOrLate + ? ( + + {originalTime} +
    {time}
    +
    + ) + :
    {time}
    + } + return ( + + {renderedTime} + + {isEarlyOrLate && + + {formatDuration(Math.abs(delay))} + + } + {STATUS[status].label} + + + ) +} + +export default RealtimeStatusLabel diff --git a/lib/util/viewer.js b/lib/util/viewer.js index 0901f623e..bf3c0a4f4 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -159,45 +159,33 @@ export function getModeFromRoute (route) { } /** - * Preset strings and colors for transit trip realtime status. + * Enum to represent transit realtime status for trips/stop times. */ -export const TRIP_STATUS = { - EARLY: { - color: '#337ab7', - getFormattedDuration: delay => formatDuration(Math.abs(delay)), - status: 'early' - }, - LATE: { - color: '#d9534f', - getFormattedDuration: delay => formatDuration(delay), - status: 'late' - }, - ON_TIME: { - color: '#5cb85c', - status: 'on time' - }, - SCHEDULED: { - status: 'scheduled' - } +export const REALTIME_STATUS = { + EARLY: 'EARLY', + LATE: 'LATE', + ON_TIME: 'ON_TIME', + SCHEDULED: 'SCHEDULED' } /** - * Obtains one of the preset states above (on-time, late...) for the specified realtime status and delay. + * Get one of the realtime states (on-time, late...) if a leg/stoptime is + * registering realtime info and given a delay value in seconds. */ -export function getTripStatus (isRealtime, delay) { +export function getTripStatus (isRealtime, delaySeconds) { if (isRealtime) { - if (delay > 60) { + if (delaySeconds > 60) { // late departure - return TRIP_STATUS.LATE - } else if (delay < -60) { + return REALTIME_STATUS.LATE + } else if (delaySeconds < -60) { // early departure - return TRIP_STATUS.EARLY + return REALTIME_STATUS.EARLY } else { // on-time departure - return TRIP_STATUS.ON_TIME + return REALTIME_STATUS.ON_TIME } } else { // Schedule only - return TRIP_STATUS.SCHEDULED + return REALTIME_STATUS.SCHEDULED } } From 508bc80e47cf4716845973fae5d97f2470f4b4e3 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 5 Jan 2021 07:30:03 -0800 Subject: [PATCH 062/265] fix(RealtimeStatusLabel): Add spacer between delay text and status text. --- lib/components/viewers/realtime-status-label.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/components/viewers/realtime-status-label.js b/lib/components/viewers/realtime-status-label.js index 2e5a93462..5c169bfb5 100644 --- a/lib/components/viewers/realtime-status-label.js +++ b/lib/components/viewers/realtime-status-label.js @@ -84,6 +84,8 @@ const RealtimeStatusLabel = ({ withBackground, className, delay, isRealtime, ori {isEarlyOrLate && {formatDuration(Math.abs(delay))} + {/* A spacer is needed between '5 min' and 'early/late'. */} + {' '} } {STATUS[status].label} From 0c0ebc356341965986750eb39a061f6530d666ee Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Tue, 5 Jan 2021 10:38:25 -0500 Subject: [PATCH 063/265] refactor(stop-schedule-table): use fetch status for stop viewer loading spinner --- lib/actions/api.js | 12 +++--------- lib/components/viewers/stop-schedule-table.js | 6 ++++++ lib/components/viewers/stop-viewer.js | 8 +------- lib/reducers/call-taker.js | 8 +------- lib/reducers/create-otp-reducer.js | 12 ++++++++++++ lib/util/constants.js | 7 +++++++ 6 files changed, 30 insertions(+), 23 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index f29f0d12f..2ddef0958 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -540,14 +540,16 @@ export function findGeometryForTrip (params) { ) } +const fetchingStopTimesForStop = createAction('FETCHING_STOP_TIMES_FOR_STOP') const findStopTimesForStopResponse = createAction('FIND_STOP_TIMES_FOR_STOP_RESPONSE') const findStopTimesForStopError = createAction('FIND_STOP_TIMES_FOR_STOP_ERROR') /** * Stop times for stop query (used in stop viewer). */ -export function findStopTimesForStop (params, clearData) { +export function findStopTimesForStop (params) { return function (dispatch, getState) { + dispatch(fetchingStopTimesForStop(params)) const { date, stopId, ...otherParams } = params let datePath = '' if (date) { @@ -570,14 +572,6 @@ export function findStopTimesForStop (params, clearData) { queryParams.startTime = nowInSeconds } - // If reuqested, clear existing stop times to prevent extra rendering of stop viewer while schedule is - // fetched and a schedule for the same date/stop was shown before. This also has the nice side - // effect to force a render, initially showing the top row, after changing the schedule view date - // and all schedules for the new date are the same as the previous date. - if (clearData) { - dispatch(findStopTimesForStopResponse({ stopId, stopTimes: [] })) - } - // (Re-)fetch stop times for the stop. dispatch(createQueryAction( `index/stops/${stopId}/stoptimes${datePath}?${qs.stringify(queryParams)}`, diff --git a/lib/components/viewers/stop-schedule-table.js b/lib/components/viewers/stop-schedule-table.js index 562f9788e..409d23a88 100644 --- a/lib/components/viewers/stop-schedule-table.js +++ b/lib/components/viewers/stop-schedule-table.js @@ -3,6 +3,8 @@ import coreUtils from '@opentripplanner/core-utils' import React, { Component, createRef } from 'react' import styled from 'styled-components' +import Loading from '../narrative/loading' +import {FETCH_STATUS} from '../../util/constants' import { getFirstDepartureFromNow, mergeAndSortStopTimes @@ -69,6 +71,10 @@ class StopScheduleTable extends Component { render () { const { date, stopData, timeFormat } = this.props + // Show loading spinner if times are still being fetched. + if (stopData.fetchStatus === FETCH_STATUS.FETCHING) { + return + } const mergedStopTimes = mergeAndSortStopTimes(stopData) // FIXME: Shift today back one day if the current time is between midnight and the end of the service day. diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index c614fb48d..33fc76066 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -78,13 +78,7 @@ class StopViewer extends Component { _findStopTimesForDate = date => { const { findStopTimesForStop, viewedStop } = this.props - - // When toggling views or changing dates, - // fetch stop times, and clear the existing stop times while fetching. - findStopTimesForStop({ - date, - stopId: viewedStop.stopId - }, true) + findStopTimesForStop({ date, stopId: viewedStop.stopId }) } _toggleScheduleView = () => { diff --git a/lib/reducers/call-taker.js b/lib/reducers/call-taker.js index 57f5e885c..25c0c79fc 100644 --- a/lib/reducers/call-taker.js +++ b/lib/reducers/call-taker.js @@ -2,19 +2,13 @@ import coreUtils from '@opentripplanner/core-utils' import update from 'immutability-helper' import moment from 'moment' +import {FETCH_STATUS} from '../util/constants' import {getTimestamp} from '../util/state' const { randId } = coreUtils.storage const UPPER_RIGHT_CORNER = {x: 604, y: 53} -const FETCH_STATUS = { - UNFETCHED: 0, - FETCHING: 1, - FETCHED: 2, - ERROR: -1 -} - function createCallTakerReducer () { const initialState = { activeCall: null, diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index ce2565504..782810dbe 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -5,6 +5,7 @@ import objectPath from 'object-path' import coreUtils from '@opentripplanner/core-utils' import { MainPanelContent, MobileScreens } from '../actions/ui' +import {FETCH_STATUS} from '../util/constants' import {getTimestamp} from '../util/state' import {isBatchRoutingEnabled} from '../util/itinerary' @@ -772,11 +773,22 @@ function createOtpReducer (config, initialQuery) { } } }) + case 'FETCHING_STOP_TIMES_FOR_STOP': + return update(state, { + transitIndex: { + stops: { + [action.payload.stopId]: { + fetchStatus: { $set: FETCH_STATUS.FETCHING } + } + } + } + }) case 'FIND_STOP_TIMES_FOR_STOP_RESPONSE': return update(state, { transitIndex: { stops: { [action.payload.stopId]: { + fetchStatus: { $set: FETCH_STATUS.FETCHED }, stopTimes: { $set: action.payload.stopTimes }, stopTimesLastUpdated: { $set: new Date().getTime() } } diff --git a/lib/util/constants.js b/lib/util/constants.js index 9563256ae..6acdb5eee 100644 --- a/lib/util/constants.js +++ b/lib/util/constants.js @@ -3,6 +3,13 @@ export const AUTH0_SCOPE = '' export const DEFAULT_APP_TITLE = 'OpenTripPlanner' export const PERSISTENCE_STRATEGY_OTP_MIDDLEWARE = 'otp_middleware' +export const FETCH_STATUS = { + UNFETCHED: 0, + FETCHING: 1, + FETCHED: 2, + ERROR: -1 +} + // Gets the root URL, e.g. https://otp-instance.example.com:8080, computed once for all. // TODO: support root URLs that involve paths or subfolders, as in https://otp-ui.example.com/path-to-ui/ export const URL_ROOT = `${window.location.protocol}//${window.location.host}` From 29f7062adba6246856dcf47deb3b7af272ef86bc Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Tue, 5 Jan 2021 16:42:53 -0500 Subject: [PATCH 064/265] refactor(draggable-window): fix draggable handle margins --- lib/components/admin/draggable-window.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/components/admin/draggable-window.js b/lib/components/admin/draggable-window.js index 6705b158f..95868dcba 100644 --- a/lib/components/admin/draggable-window.js +++ b/lib/components/admin/draggable-window.js @@ -28,7 +28,7 @@ export default class DraggableWindow extends Component { width: '350px', backgroundColor: 'white', borderRadius: '5%', - padding: '10px', + paddingBottom: '10px', boxShadow: '2px 2px 8px', border: GREY_BORDER, ...style @@ -39,7 +39,7 @@ export default class DraggableWindow extends Component { style={{ borderBottom: GREY_BORDER, cursor: 'move', - paddingBottom: '5px' + padding: '5px' }} >
    ) diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index 31848c508..6eb5e1650 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -5,7 +5,8 @@ padding: 12px; } -.otp .route-viewer, .otp .stop-viewer, .otp .trip-viewer { +.otp .route-viewer, .otp .stop-viewer, .otp .trip-viewer, +.otp .stop-viewer-body { display: flex; flex-direction: column; flex-flow: column; From de849dedf2154a4a164ebfa879a3bed3d06180ce Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 7 Jan 2021 09:47:16 -0500 Subject: [PATCH 068/265] refactor(StopViewer): Flush schedule scrollbar to outer edge of component. --- lib/components/viewers/stop-viewer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 9bd73f2b8..ea3218835 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -29,8 +29,9 @@ const defaultState = { // A scrollable container for the contents of the stop viewer body. const Scrollable = styled.div` - height: 100%; + margin-right: -12px; overflow-y: auto; + padding-right: 12px; ` // Alert with custom styles const StyledAlert = styled(Alert)` From 5bf3ea46f83ade228b7a9c17672ca324c496213b Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 7 Jan 2021 09:58:12 -0500 Subject: [PATCH 069/265] test(StopViewer): Update snapshots. --- .../viewers/__snapshots__/stop-viewer.js.snap | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap index ff6079f65..03738bb7b 100644 --- a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap +++ b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap @@ -497,7 +497,7 @@ exports[`components > viewers > stop viewer should render countdown times after
    viewers > stop viewer should render countdown times for st
    viewers > stop viewer should render times after midnight w
    viewers > stop viewer should render with OTP transit index
    viewers > stop viewer should render with TriMet transit in
    Date: Thu, 7 Jan 2021 12:17:09 -0500 Subject: [PATCH 070/265] refactor(call-taker): move components to files, fix date/time --- lib/components/app/call-taker-panel.js | 449 +----------------- lib/components/form/add-place-button.js | 24 + .../form/call-taker/advanced-options.js | 228 +++++++++ .../form/call-taker/date-time-options.js | 182 +++++++ .../form/call-taker/mode-dropdown.js | 86 ++++ lib/components/form/form.css | 6 + 6 files changed, 548 insertions(+), 427 deletions(-) create mode 100644 lib/components/form/add-place-button.js create mode 100644 lib/components/form/call-taker/advanced-options.js create mode 100644 lib/components/form/call-taker/date-time-options.js create mode 100644 lib/components/form/call-taker/mode-dropdown.js diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index 1b0d31d59..2973d7e20 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -1,95 +1,22 @@ -import { - OTP_API_DATE_FORMAT, - OTP_API_TIME_FORMAT -} from '@opentripplanner/core-utils/lib/time' -import { hasBike, hasTransit } from '@opentripplanner/core-utils/lib/itinerary' import { storeItem } from '@opentripplanner/core-utils/lib/storage' -import {SubmodeSelector} from '@opentripplanner/trip-form' -import * as TripFormClasses from '@opentripplanner/trip-form/lib/styled' -import moment from 'moment' import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import Select from 'react-select' -import styled from 'styled-components' import * as apiActions from '../../actions/api' import * as formActions from '../../actions/form' +import AddPlaceButton from '../form/add-place-button' +import AdvancedOptions from '../form/call-taker/advanced-options' +import DateTimeOptions from '../form/call-taker/date-time-options' +import ModeDropdown from '../form/call-taker/mode-dropdown' import IntermediatePlace from '../form/intermediate-place-field' import LocationField from '../form/connected-location-field' -import { modeButtonButtonCss } from '../form/styled' import SwitchButton from '../form/switch-button' import UserSettings from '../form/user-settings' import NarrativeItineraries from '../narrative/narrative-itineraries' -import { ComponentContext } from '../../util/contexts' import { hasValidLocation, getActiveSearch, getShowUserSettings } from '../../util/state' import ViewerContainer from '../viewers/viewer-container' -// FIXME: move to styled.js? -export const StyledSubmodeSelector = styled(SubmodeSelector)` - ${TripFormClasses.SubmodeSelector.Row} { - > * { - padding: 3px 5px 3px 0px; - } - > :last-child { - padding-right: 0px; - } - ${TripFormClasses.ModeButton.Button} { - padding: 6px 12px; - } - svg, - img { - margin-left: 0px; - } - } - ${TripFormClasses.SubmodeSelector.InlineRow} { - margin: -3px 0px; - } - - ${TripFormClasses.SubmodeSelector} { - ${modeButtonButtonCss} - } -` - -const departureOptions = [ - { - // Default option. - value: 'NOW', - children: 'Now' - }, - { - value: 'DEPART', - children: 'Depart at' - }, - { - value: 'ARRIVE', - children: 'Arrive by' - } -] - -const modeOptions = [ - { - // Default option. - value: 'TRANSIT', - children: 'Transit' - }, - { - value: 'WALK', - children: 'Walk only' - }, - { - value: 'BICYCLE', - children: 'Bike only' - }, - { - value: 'BICYCLE,TRANSIT', - children: 'Bike to transit' - } -] - -const metersToMiles = meters => Math.round(meters * 0.000621371 * 100) / 100 -const milesToMeters = miles => miles / 0.000621371 - /** * This is the main panel/sidebar for the Call Taker/Field Trip module. It * currently also serves as the main panel for the FDOT RMCE trip comparison view @@ -117,15 +44,6 @@ class CallTakerPanel extends Component { routingQuery() } - modeToOptionValue = mode => { - const isTransit = hasTransit(mode) - const isBike = hasBike(mode) - if (isTransit && isBike) return 'BICYCLE,TRANSIT' - else if (isTransit) return 'TRANSIT' - // Currently handles bicycle - else return mode - } - _addPlace = (result, index) => { const intermediatePlaces = [...this.props.currentQuery.intermediatePlaces] || [] if (result && index !== undefined) { @@ -145,25 +63,6 @@ class CallTakerPanel extends Component { this.props.setQueryParam({intermediatePlaces}) } - _setMode = evt => { - const {value: mode} = evt.target - const transitIsSelected = mode.indexOf('TRANSIT') !== -1 - if (transitIsSelected) { - // Collect transit modes and selected access mode. - const accessMode = mode === 'TRANSIT' ? 'WALK' : 'BICYCLE' - // If no transit is selected, selected all available. Otherwise, default - // to state. - const transitModes = this.state.transitModes.length > 0 - ? this.state.transitModes - : this.props.modes.transitModes.map(m => m.mode) - const newModes = [accessMode, ...transitModes].join(',') - this.setState({transitModes}) - this.props.setQueryParam({ mode: newModes }) - } else { - this.props.setQueryParam({ mode }) - } - } - _onHideAdvancedClick = () => { const expandAdvanced = !this.state.expandAdvanced // FIXME move logic to action @@ -177,8 +76,6 @@ class CallTakerPanel extends Component { * keyboard-only operation of the trip planning form. Note: it generally should * not be passed to buttons or other elements that natively rely on the Enter * key. - * - * FIXME: Should we use a proper submit button instead? */ _handleFormKeyDown = (evt) => { switch (evt.keyCode) { @@ -207,8 +104,6 @@ class CallTakerPanel extends Component { // FIXME: Remove showPlanTripButton const showPlanTripButton = mainPanelContent === 'EDIT_DATETIME' || mainPanelContent === 'EDIT_SETTINGS' - // const mostRecentQuery = activeSearch ? activeSearch.query : null - // const planDisabled = isEqual(currentQuery, mostRecentQuery) const { departArrive, date, @@ -229,9 +124,6 @@ class CallTakerPanel extends Component { padding: '0px 8px 5px', display: expandAdvanced ? 'none' : undefined } - // Only permit adding intermediate place if from/to is defined. - const maxPlacesDefined = intermediatePlaces.length >= 3 - const addIntermediateDisabled = !from || !to || maxPlacesDefined return ( {/* FIXME: should this be a styled component */} @@ -273,26 +165,16 @@ class CallTakerPanel extends Component { showClearButton={!mobile} />
    } />
    - +
    - + selectedTransitModes={this.state.transitModes} /> @@ -328,12 +206,13 @@ class CallTakerPanel extends Component { Advanced options
    - + setQueryParam={setQueryParam} />
    @@ -361,290 +240,6 @@ class CallTakerPanel extends Component { } } -class CallTakerAdvancedOptions extends Component { - constructor (props) { - super(props) - this.state = { - expandAdvanced: props.expandAdvanced, - routeOptions: [], - transitModes: props.modes.transitModes.map(m => m.mode) - } - } - - static contextType = ComponentContext - - componentWillMount () { - // Fetch routes for banned/preferred routes selectors. - this.props.findRoutes() - } - - componentWillReceiveProps (nextProps) { - const {routes} = nextProps - // Once routes are available, map them to the route options format. - if (routes && !this.props.routes) { - const routeOptions = Object.values(routes).map(this.routeToOption) - this.setState({routeOptions}) - } - } - - _setBannedRoutes = options => { - const bannedRoutes = options ? options.map(o => o.value).join(',') : '' - this.props.setQueryParam({ bannedRoutes }) - } - - _setPreferredRoutes = options => { - const preferredRoutes = options ? options.map(o => (o.value)).join(',') : '' - this.props.setQueryParam({ preferredRoutes }) - } - - _isBannedRouteOptionDisabled = option => { - // Disable routes that are preferred already. - const preferredRoutes = this.getRouteList('preferredRoutes') - return preferredRoutes && preferredRoutes.find(o => o.value === option.value) - } - - _isPreferredRouteOptionDisabled = option => { - // Disable routes that are banned already. - const bannedRoutes = this.getRouteList('bannedRoutes') - return bannedRoutes && bannedRoutes.find(o => o.value === option.value) - } - - getDistanceStep = distanceInMeters => { - // Determine step for max walk/bike based on current value. Increment by a - // quarter mile if dealing with small values, whatever number will round off - // the number if it is not an integer, or default to one mile. - return metersToMiles(distanceInMeters) <= 2 - ? '.25' - : metersToMiles(distanceInMeters) % 1 !== 0 - ? `${metersToMiles(distanceInMeters) % 1}` - : '1' - } - - _onSubModeChange = changedMode => { - // Get previous transit modes from state and all modes from query. - const transitModes = [...this.state.transitModes] - const allModes = this.props.currentQuery.mode.split(',') - const index = transitModes.indexOf(changedMode) - if (index === -1) { - // If submode was not selected, add it. - transitModes.push(changedMode) - allModes.push(changedMode) - } else { - // Otherwise, remove it. - transitModes.splice(index, 1) - const i = allModes.indexOf(changedMode) - allModes.splice(i, 1) - } - // Update transit modes in state. - this.setState({transitModes}) - // Update all modes in query (set to walk if all transit modes inactive). - this.props.setQueryParam({ mode: allModes.join(',') || 'WALK' }) - } - - _setMaxWalkDistance = evt => { - this.props.setQueryParam({ maxWalkDistance: milesToMeters(evt.target.value) }) - } - - /** - * Get list of routes for specified key (either 'bannedRoutes' or - * 'preferredRoutes'). - */ - getRouteList = key => { - const routesParam = this.props.currentQuery[key] - const idList = routesParam ? routesParam.split(',') : [] - if (this.state.routeOptions) { - return this.state.routeOptions.filter(o => idList.indexOf(o.value) !== -1) - } else { - // If route list is not available, default labels to route IDs. - return idList.map(id => ({value: id, label: id})) - } - } - - routeToOption = route => { - if (!route) return null - const {id, longName, shortName} = route - // For some reason the OTP API expects route IDs in this double - // underscore format - // FIXME: This replace is flimsy! What if there are more colons? - const value = id.replace(':', '__') - const label = shortName - ? `${shortName}${longName ? ` - ${longName}` : ''}` - : longName - return {value, label} - } - - render () { - const { currentQuery, modes } = this.props - const { ModeIcon } = this.context - - const {maxBikeDistance, maxWalkDistance, mode} = currentQuery - const bannedRoutes = this.getRouteList('bannedRoutes') - const preferredRoutes = this.getRouteList('preferredRoutes') - const transitModes = modes.transitModes.map(modeObj => { - const modeStr = modeObj.mode || modeObj - return { - id: modeStr, - selected: this.state.transitModes.indexOf(modeStr) !== -1, - text: ( - - - - ), - title: modeObj.label - } - }) - // FIXME: Set units via config. - const unitsString = '(mi.)' - return ( -
    -
    - - {hasBike(mode) - ? - : null - } - -
    - -
    - ) - } -} - -const TIME_FORMATS = [ - 'HH:mm:ss', - 'h:mm:ss a', - 'h:mm:ssa', - 'h:mm a', - 'h:mma', - 'h:mm', - 'HHmm', - 'hmm', - 'ha', - 'h', - 'HH:mm' -].map(format => `YYYY-MM-DDT${format}`) - -class DateTimeOptions extends Component { - _setDepartArrive = evt => { - const {value: departArrive} = evt.target - if (departArrive === 'NOW') { - this.props.setQueryParam({ - departArrive, - date: moment().format(OTP_API_DATE_FORMAT), - time: moment().format(OTP_API_TIME_FORMAT) - }) - } else { - this.props.setQueryParam({ departArrive }) - } - } - - handleDateChange = evt => { - this.props.setQueryParam({ date: evt.target.value }) - } - - handleTimeChange = evt => { - const timeInput = evt.target.value - console.log(timeInput) - const date = moment().startOf('day').format('YYYY-MM-DD') - const time = moment(date + 'T' + timeInput, TIME_FORMATS) - this.props.setQueryParam({ time: time.format(OTP_API_TIME_FORMAT) }) - } - - render () { - const {date, departArrive, time} = this.props - const leaveNow = departArrive === 'NOW' && !date && !time - const dateTime = moment(`${date} ${time}`) - const cleanDate = dateTime.format('YYYY-MM-DD') - const cleanTime = dateTime.format('HH:mm') - return ( - <> - - {leaveNow - ? null - : - {cleanTime} - - - } - {leaveNow - ? null - : - } - - ) - } -} - // connect to the redux store const mapStateToProps = (state, ownProps) => { const showUserSettings = getShowUserSettings(state.otp) diff --git a/lib/components/form/add-place-button.js b/lib/components/form/add-place-button.js new file mode 100644 index 000000000..f75167864 --- /dev/null +++ b/lib/components/form/add-place-button.js @@ -0,0 +1,24 @@ +import React from 'react' + +const AddPlaceButton = ({from, intermediatePlaces, onClick, to}) => { + // Only permit adding intermediate place if from/to is defined. + const maxPlacesDefined = intermediatePlaces.length >= 3 + const disabled = !from || !to || maxPlacesDefined + return ( + + ) +} + +export default AddPlaceButton diff --git a/lib/components/form/call-taker/advanced-options.js b/lib/components/form/call-taker/advanced-options.js new file mode 100644 index 000000000..057f68d68 --- /dev/null +++ b/lib/components/form/call-taker/advanced-options.js @@ -0,0 +1,228 @@ +import { hasBike } from '@opentripplanner/core-utils/lib/itinerary' +import {SubmodeSelector} from '@opentripplanner/trip-form' +import * as TripFormClasses from '@opentripplanner/trip-form/lib/styled' +import React, { Component } from 'react' +import Select from 'react-select' +import styled from 'styled-components' + +import { modeButtonButtonCss } from '../styled' +import { ComponentContext } from '../../../util/contexts' + +export const StyledSubmodeSelector = styled(SubmodeSelector)` + ${TripFormClasses.SubmodeSelector.Row} { + > * { + padding: 3px 5px 3px 0px; + } + > :last-child { + padding-right: 0px; + } + ${TripFormClasses.ModeButton.Button} { + padding: 6px 12px; + } + svg, + img { + margin-left: 0px; + } + } + ${TripFormClasses.SubmodeSelector.InlineRow} { + margin: -3px 0px; + } + + ${TripFormClasses.SubmodeSelector} { + ${modeButtonButtonCss} + } +` + +const metersToMiles = meters => Math.round(meters * 0.000621371 * 100) / 100 +const milesToMeters = miles => miles / 0.000621371 + +export default class AdvancedOptions extends Component { + constructor (props) { + super(props) + this.state = { + expandAdvanced: props.expandAdvanced, + routeOptions: [], + transitModes: props.modes.transitModes.map(m => m.mode) + } + } + + static contextType = ComponentContext + + componentWillMount () { + // Fetch routes for banned/preferred routes selectors. + this.props.findRoutes() + } + + componentWillReceiveProps (nextProps) { + const {routes} = nextProps + // Once routes are available, map them to the route options format. + if (routes && !this.props.routes) { + const routeOptions = Object.values(routes).map(this.routeToOption) + this.setState({routeOptions}) + } + } + + _setBannedRoutes = options => { + const bannedRoutes = options ? options.map(o => o.value).join(',') : '' + this.props.setQueryParam({ bannedRoutes }) + } + + _setPreferredRoutes = options => { + const preferredRoutes = options ? options.map(o => (o.value)).join(',') : '' + this.props.setQueryParam({ preferredRoutes }) + } + + _isBannedRouteOptionDisabled = option => { + // Disable routes that are preferred already. + const preferredRoutes = this.getRouteList('preferredRoutes') + return preferredRoutes && preferredRoutes.find(o => o.value === option.value) + } + + _isPreferredRouteOptionDisabled = option => { + // Disable routes that are banned already. + const bannedRoutes = this.getRouteList('bannedRoutes') + return bannedRoutes && bannedRoutes.find(o => o.value === option.value) + } + + getDistanceStep = distanceInMeters => { + // Determine step for max walk/bike based on current value. Increment by a + // quarter mile if dealing with small values, whatever number will round off + // the number if it is not an integer, or default to one mile. + return metersToMiles(distanceInMeters) <= 2 + ? '.25' + : metersToMiles(distanceInMeters) % 1 !== 0 + ? `${metersToMiles(distanceInMeters) % 1}` + : '1' + } + + _onSubModeChange = changedMode => { + // Get previous transit modes from state and all modes from query. + const transitModes = [...this.state.transitModes] + const allModes = this.props.currentQuery.mode.split(',') + const index = transitModes.indexOf(changedMode) + if (index === -1) { + // If submode was not selected, add it. + transitModes.push(changedMode) + allModes.push(changedMode) + } else { + // Otherwise, remove it. + transitModes.splice(index, 1) + const i = allModes.indexOf(changedMode) + allModes.splice(i, 1) + } + // Update transit modes in state. + this.setState({transitModes}) + // Update all modes in query (set to walk if all transit modes inactive). + this.props.setQueryParam({ mode: allModes.join(',') || 'WALK' }) + } + + _setMaxWalkDistance = evt => { + this.props.setQueryParam({ maxWalkDistance: milesToMeters(evt.target.value) }) + } + + /** + * Get list of routes for specified key (either 'bannedRoutes' or + * 'preferredRoutes'). + */ + getRouteList = key => { + const routesParam = this.props.currentQuery[key] + const idList = routesParam ? routesParam.split(',') : [] + if (this.state.routeOptions) { + return this.state.routeOptions.filter(o => idList.indexOf(o.value) !== -1) + } else { + // If route list is not available, default labels to route IDs. + return idList.map(id => ({value: id, label: id})) + } + } + + routeToOption = route => { + if (!route) return null + const {id, longName, shortName} = route + // For some reason the OTP API expects route IDs in this double + // underscore format + // FIXME: This replace is flimsy! What if there are more colons? + const value = id.replace(':', '__') + const label = shortName + ? `${shortName}${longName ? ` - ${longName}` : ''}` + : longName + return {value, label} + } + + render () { + const { currentQuery, modes, onKeyDown } = this.props + const { ModeIcon } = this.context + + const {maxBikeDistance, maxWalkDistance, mode} = currentQuery + const bannedRoutes = this.getRouteList('bannedRoutes') + const preferredRoutes = this.getRouteList('preferredRoutes') + const transitModes = modes.transitModes.map(modeObj => { + const modeStr = modeObj.mode || modeObj + return { + id: modeStr, + selected: this.state.transitModes.indexOf(modeStr) !== -1, + text: ( + + + + ), + title: modeObj.label + } + }) + // FIXME: Set units via config. + const unitsString = '(mi.)' + return ( +
    +
    + + {hasBike(mode) + ? + : null + } + +
    + +
    + ) + } +} diff --git a/lib/components/form/call-taker/date-time-options.js b/lib/components/form/call-taker/date-time-options.js new file mode 100644 index 000000000..b0d507be7 --- /dev/null +++ b/lib/components/form/call-taker/date-time-options.js @@ -0,0 +1,182 @@ +import { + OTP_API_DATE_FORMAT, + OTP_API_TIME_FORMAT +} from '@opentripplanner/core-utils/lib/time' +import moment from 'moment' +import React, { Component } from 'react' +import { OverlayTrigger, Tooltip } from 'react-bootstrap' + +const departureOptions = [ + { + // Default option. + value: 'NOW', + children: 'Now' + }, + { + value: 'DEPART', + children: 'Depart at' + }, + { + value: 'ARRIVE', + children: 'Arrive by' + } +] + +const TIME_FORMATS = [ + 'HH:mm:ss', + 'h:mm:ss a', + 'h:mm:ssa', + 'h:mm a', + 'h:mma', + 'h:mm', + 'HHmm', + 'hmm', + 'ha', + 'h', + 'HH:mm' +].map(format => `YYYY-MM-DDT${format}`) + +function nowAsTimeString () { + return moment().format(OTP_API_TIME_FORMAT) +} + +export default class DateTimeOptions extends Component { + state = {} + + componentDidMount () { + this._startAutoRefresh() + } + + componentWillUnmount () { + this._stopAutoRefresh() + } + + componentWillReceiveProps (nextProps) { + const {departArrive} = nextProps + // If departArrive has been changed to leave now, update time input to now. + if (departArrive === 'NOW' && !this.props.departArrive !== 'NOW') { + this.setState({timeInput: nowAsTimeString()}) + } + } + + _startAutoRefresh = () => { + console.log('starting auto refresh') + const timer = window.setInterval(this._refreshDateTime, 1000) + this.setState({ timer }) + } + + _stopAutoRefresh = () => { + console.log('stopping auto refresh') + window.clearInterval(this.state.timer) + this.setState({timer: null}) + } + + _refreshDateTime = () => { + const now = nowAsTimeString() + this.setState({ timeInput: now }) + // Update query param if the current time has changed (i.e., on minute ticks). + if (now !== this.props.time) { + this.props.setQueryParam({ + date: moment().format(OTP_API_DATE_FORMAT), + time: nowAsTimeString() + }) + } + } + + _setDepartArrive = evt => { + const {value: departArrive} = evt.target + if (departArrive === 'NOW') { + // If setting to leave now, update date/time and start auto refresh to keep + // form input in sync. + this.props.setQueryParam({ + departArrive, + date: moment().format(OTP_API_DATE_FORMAT), + time: nowAsTimeString() + }) + if (!this.state.timer) { + this._startAutoRefresh() + } + } else { + // If set to depart at/arrive by, stop auto refresh. + this._stopAutoRefresh() + this.props.setQueryParam({ departArrive }) + } + } + + handleDateChange = evt => { + const {departArrive: prevDepartArrive} = this.props + const params = { date: evt.target.value } + // If previously set to leave now, change to depart at when date changes. + if (prevDepartArrive === 'NOW') params.departArrive = 'DEPART' + this.props.setQueryParam(params) + } + + handleTimeFocus = evt => evt.target.select() + + handleTimeChange = evt => { + if (this.state.timer) this._stopAutoRefresh() + const {departArrive: prevDepartArrive} = this.props + const timeInput = evt.target.value + const date = moment().startOf('day').format('YYYY-MM-DD') + const time = moment(date + 'T' + timeInput, TIME_FORMATS) + const params = { time: time.format(OTP_API_TIME_FORMAT) } + // If previously set to leave now, change to depart at when time changes. + if (prevDepartArrive === 'NOW') params.departArrive = 'DEPART' + this.props.setQueryParam(params) + this.setState({timeInput}) + } + + render () { + const {date, departArrive, time} = this.props + const dateTime = moment(`${date} ${time}`) + const cleanDate = dateTime.format('YYYY-MM-DD') + const cleanTime = dateTime.format('HH:mm') + return ( + <> + + + {cleanTime}} + placement='bottom' + > + + + + + + ) + } +} diff --git a/lib/components/form/call-taker/mode-dropdown.js b/lib/components/form/call-taker/mode-dropdown.js new file mode 100644 index 000000000..886b47595 --- /dev/null +++ b/lib/components/form/call-taker/mode-dropdown.js @@ -0,0 +1,86 @@ +import { hasBike, hasTransit } from '@opentripplanner/core-utils/lib/itinerary' +import React, { Component } from 'react' + +const modeOptions = [ + { + // Default option. + value: 'TRANSIT', + children: 'Transit' + }, + { + value: 'WALK', + children: 'Walk only' + }, + { + value: 'BICYCLE', + children: 'Bike only' + }, + { + value: 'BICYCLE,TRANSIT', + children: 'Bike to transit' + }, + { + value: 'TRANSIT,CAR_HAIL', + children: 'Uber to transit' + } + // { + // value: 'TRANSIT,CAR_HAIL,WALK', + // children: 'Uber to transit' + // } +] + +export default class ModeDropdown extends Component { + modeToOptionValue = mode => { + const isTransit = hasTransit(mode) + const isBike = hasBike(mode) + const isCarHail = mode.indexOf('CAR_HAIL') !== -1 + if (isCarHail) return 'TRANSIT,CAR_HAIL' + else if (isTransit && isBike) return 'BICYCLE,TRANSIT' + else if (isTransit) return 'TRANSIT' + // Currently handles bicycle + else return mode + } + + _onChange = evt => { + const {value: mode} = evt.target + const transitIsSelected = mode.indexOf('TRANSIT') !== -1 + if (transitIsSelected) { + const modes = mode.split(',') + const transitIndex = modes.indexOf('TRANSIT') + modes.splice(transitIndex, 1) + // Collect transit modes and selected access mode. + const accessModes = mode === 'TRANSIT' ? ['WALK'] : modes + // If no transit is selected, selected all available. Otherwise, default + // to state. + const transitModes = this.props.selectedTransitModes.length > 0 + ? this.props.selectedTransitModes + : this.props.modes.transitModes.map(m => m.mode) + const newModes = [...transitModes, ...accessModes].join(',') + this.setState({transitModes}) + const params = { mode: newModes } + if (newModes.indexOf('CAR_HAIL')) { + // FIXME: Use config companies + params.companies = 'UBER' + } + this.props.onChange(params) + } else { + this.props.onChange({ mode }) + } + } + + render () { + const {mode, onKeyDown} = this.props + return ( + + ) + } +} diff --git a/lib/components/form/form.css b/lib/components/form/form.css index 99d562fff..49e2d16c2 100644 --- a/lib/components/form/form.css +++ b/lib/components/form/form.css @@ -69,6 +69,12 @@ -webkit-appearance: none; } +/* Prevent the calendar picker from having a white margin that covers the date */ +.otp .search-options input[type="date"]::-webkit-calendar-picker-indicator { + background-color: rgba(0, 0, 0, 0.0); + margin-left: 0px; +} + .otp .search-options input[type="time"]::-webkit-inner-spin-button { -webkit-appearance: none; } From f773146cab67ebb3b4dfcf45c5eb23cda9b34058 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Thu, 7 Jan 2021 14:16:13 -0500 Subject: [PATCH 071/265] refactor(mode-dropdown): use modes from config --- lib/components/app/call-taker-panel.js | 6 +- .../form/call-taker/date-time-options.js | 1 + .../form/call-taker/mode-dropdown.js | 92 +++++++++---------- 3 files changed, 49 insertions(+), 50 deletions(-) diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index 2973d7e20..37508ef8b 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -184,15 +184,17 @@ class CallTakerPanel extends Component { time={time} /> + selectedTransitModes={this.state.transitModes} + updateTransitModes={transitModes => this.setState({transitModes})} />
    ) } diff --git a/lib/components/form/call-taker/date-time-options.js b/lib/components/form/call-taker/date-time-options.js index 93f269c21..74d190d2e 100644 --- a/lib/components/form/call-taker/date-time-options.js +++ b/lib/components/form/call-taker/date-time-options.js @@ -65,21 +65,26 @@ function momentToQueryParams (time = moment()) { * @type {Object} */ export default class DateTimeOptions extends Component { - state = {} + state = { + timeInput: '' + } componentDidMount () { - this._startAutoRefresh() + if (this.props.departArrive === 'NOW') { + this._startAutoRefresh() + } } componentWillUnmount () { this._stopAutoRefresh() } - componentWillReceiveProps (nextProps) { - const {departArrive} = nextProps - // If departArrive has been changed to leave now, update time input to now. - if (departArrive === 'NOW' && !this.props.departArrive !== 'NOW') { - this._updateTimeInput() + componentDidUpdate (prevProps) { + const {departArrive} = this.props + // If departArrive has been changed to leave now, begin auto refresh. + if (departArrive !== prevProps.departArrive) { + if (departArrive === 'NOW') this._startAutoRefresh() + else this._stopAutoRefresh() } } @@ -89,6 +94,7 @@ export default class DateTimeOptions extends Component { this.setState({timeInput: time.format('H:mm')}) _startAutoRefresh = () => { + console.log('starting autorefresh') const timer = window.setInterval(this._refreshDateTime, 1000) this.setState({ timer }) } @@ -156,7 +162,8 @@ export default class DateTimeOptions extends Component { } render () { - const {date, departArrive, time, timeFormat} = this.props + const {date, departArrive, onKeyDown, time, timeFormat} = this.props + const {timeInput} = this.state const dateTime = moment(`${date} ${time}`) const cleanTime = dateTime.format(timeFormat) return ( @@ -164,7 +171,7 @@ export default class DateTimeOptions extends Component { Date: Mon, 1 Feb 2021 10:51:57 -0500 Subject: [PATCH 122/265] refactor(call-taker): remove log statements --- lib/components/app/call-taker-panel.js | 1 - lib/components/form/call-taker/date-time-options.js | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index d2d2ede98..055b2a8d0 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -92,7 +92,6 @@ class CallTakerPanel extends Component { } render () { - console.log(this.props) const { activeSearch, currentQuery, diff --git a/lib/components/form/call-taker/date-time-options.js b/lib/components/form/call-taker/date-time-options.js index 74d190d2e..feb48b1e2 100644 --- a/lib/components/form/call-taker/date-time-options.js +++ b/lib/components/form/call-taker/date-time-options.js @@ -94,7 +94,6 @@ export default class DateTimeOptions extends Component { this.setState({timeInput: time.format('H:mm')}) _startAutoRefresh = () => { - console.log('starting autorefresh') const timer = window.setInterval(this._refreshDateTime, 1000) this.setState({ timer }) } From 115362d16ece9e9bf596eae3640e1300fbd84c3b Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 1 Feb 2021 17:06:41 -0500 Subject: [PATCH 123/265] refactor(field-trip): hook up UI with call back end --- lib/actions/call-taker.js | 118 ++++++++++++++++++++- lib/components/admin/draggable-window.js | 4 +- lib/components/admin/field-trip-details.js | 52 ++++++--- lib/components/admin/field-trip-notes.js | 29 ++--- lib/components/admin/field-trip-windows.js | 55 ++++++---- lib/components/admin/trip-status.js | 52 ++++++++- lib/components/admin/updatable.js | 17 ++- lib/util/call-taker.js | 7 ++ 8 files changed, 275 insertions(+), 59 deletions(-) diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index ec4fb8439..b0929234d 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -1,8 +1,12 @@ -import { getUrlParams } from '@opentripplanner/core-utils/lib/query' +import { getUrlParams, planParamsToQueryAsync } from '@opentripplanner/core-utils/lib/query' +import { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } from '@opentripplanner/core-utils/lib/time' +import moment from 'moment' import qs from 'qs' import { createAction } from 'redux-actions' -import {searchToQuery} from '../util/call-taker' +import {routingQuery} from './api' +import {setQueryParam} from './form' +import {getGroupSize, searchToQuery} from '../util/call-taker' import {URL_ROOT} from '../util/constants' import {getTimestamp} from '../util/state' @@ -191,7 +195,115 @@ export function addFieldTripNote (request, note) { ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { - alert(`Error storing call queries: ${JSON.stringify(err)}`) + alert(`Error adding field trip note: ${JSON.stringify(err)}`) + }) + } +} + +/** + * Delete a specific note for a field trip request. + */ +export function deleteFieldTripNote (request, noteId) { + return function (dispatch, getState) { + const {callTaker, otp} = getState() + const {datastoreUrl} = otp.config + if (sessionIsInvalid(callTaker.session)) return + const {sessionId} = callTaker.session + const queryData = new FormData() + queryData.append('sessionId', sessionId) + queryData.append('noteId', noteId) + return fetch(`${datastoreUrl}/fieldtrip/deleteNote`, + {method: 'POST', body: queryData} + ) + .then(() => dispatch(fetchFieldTripDetails(request.id))) + .catch(err => { + alert(`Error deleting field trip note: ${JSON.stringify(err)}`) + }) + } +} + +/** + * Edit teacher (AKA submitter) notes for a field trip request. + */ +export function editSubmitterNotes (request, submitterNotes) { + return function (dispatch, getState) { + const {callTaker, otp} = getState() + const {datastoreUrl} = otp.config + if (sessionIsInvalid(callTaker.session)) return + const {sessionId} = callTaker.session + const queryData = new FormData() + queryData.append('sessionId', sessionId) + queryData.append('notes', submitterNotes) + queryData.append('requestId', request.id) + return fetch(`${datastoreUrl}/fieldtrip/editSubmitterNotes`, + {method: 'POST', body: queryData} + ) + .then(() => dispatch(fetchFieldTripDetails(request.id))) + .catch(err => { + alert(`Error editing submitter notes: ${JSON.stringify(err)}`) + }) + } +} + +export function planOutbound (request) { + return async function (dispatch, getState) { + const {config} = getState().otp + // this.clearTrip() + const locations = await planParamsToQueryAsync({ + fromPlace: request.startLocation, + toPlace: request.endLocation + }, config) + const queryParams = { + date: moment(request.travelDate).format(OTP_API_DATE_FORMAT), + departArrive: 'ARRIVE', + groupSize: getGroupSize(request), + time: moment(request.arriveDestinationTime).format(OTP_API_TIME_FORMAT), + ...locations + } + dispatch(setQueryParam(queryParams)) + dispatch(routingQuery()) + } +} + +export function planInbound (request) { + return async function (dispatch, getState) { + const {config} = getState().otp + const locations = await planParamsToQueryAsync({ + fromPlace: request.endLocation, + toPlace: request.startLocation + }, config) + // this.clearTrip() + const queryParams = { + date: moment(request.travelDate).format(OTP_API_DATE_FORMAT), + departArrive: 'DEPART', + groupSize: getGroupSize(request), + time: moment(request.leaveDestinationTime).format(OTP_API_TIME_FORMAT), + ...locations + } + dispatch(setQueryParam(queryParams)) + dispatch(routingQuery()) + } +} + +/** + * Set field trip request status (e.g., cancelled). + */ +export function setRequestStatus (request, status) { + return function (dispatch, getState) { + const {callTaker, otp} = getState() + const {datastoreUrl} = otp.config + if (sessionIsInvalid(callTaker.session)) return + const {sessionId} = callTaker.session + const queryData = new FormData() + queryData.append('sessionId', sessionId) + queryData.append('status', status) + queryData.append('requestId', request.id) + return fetch(`${datastoreUrl}/fieldtrip/setRequestStatus`, + {method: 'POST', body: queryData} + ) + .then(() => dispatch(fetchFieldTripDetails(request.id))) + .catch(err => { + alert(`Error setting request status: ${JSON.stringify(err)}`) }) } } diff --git a/lib/components/admin/draggable-window.js b/lib/components/admin/draggable-window.js index 2af771633..74dcfaf0b 100644 --- a/lib/components/admin/draggable-window.js +++ b/lib/components/admin/draggable-window.js @@ -43,8 +43,10 @@ export default class DraggableWindow extends Component { }} > {header} diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index e41d0aec6..527cd8e03 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -1,4 +1,3 @@ -import { getTimeFormat } from '@opentripplanner/core-utils/lib/time' import React, { Component } from 'react' import { DropdownButton, MenuItem } from 'react-bootstrap' import { connect } from 'react-redux' @@ -39,16 +38,26 @@ class FieldTripDetails extends Component { expandNotes: true } + _editSubmitterNotes = (val) => this.props.editSubmitterNotes(this.props.request, val) + _onCloseActiveFieldTrip = () => this.props.setActiveFieldTrip(null) _onClickCancel = () => { - + const {request, setRequestStatus} = this.props + if (confirm('Are you sure you want to cancel this request? Any associated trips will be deleted.')) { + setRequestStatus(request, 'cancelled') + } } _toggleNotes = () => this.setState({expandNotes: !this.state.expandNotes}) render () { - const {addFieldTripNote, callTaker, request, timeFormat} = this.props + const { + addFieldTripNote, + callTaker, + deleteFieldTripNote, + request + } = this.props if (!request) return null const { ccLastFour, @@ -64,8 +73,10 @@ class FieldTripDetails extends Component { paymentPreference, requireInvoice, schoolName, + submitterNotes, teacherName, - ticketType + ticketType, + travelDate } = request const total = numStudents + numChaperones + numFreeStudents const {fieldTrip} = callTaker @@ -117,10 +128,17 @@ class FieldTripDetails extends Component { style={{width: '450px'}} > -
    Group Information
    +
    Group Information ({travelDate})

    {schoolName}

    Teacher: {teacherName}

    +

    + +

    @@ -133,31 +151,31 @@ class FieldTripDetails extends Component {

    {numFreeStudents} students under 7

    {numStudents} chaperones

    - - +
    Payment information

    Ticket type: {TICKET_TYPES[ticketType]}

    Payment preference: {PAYMENT_PREFS[paymentPreference]}

    -

    Invoice required:

    +

    Invoice required:

    Class Pass Hop Card #: {classpassId}

    Credit card type: {ccType}

    Name on credit card: {ccName}

    Credit card last 4 digits: {ccLastFour}

    Check/Money order number: {checkNumber}

    +
    ) @@ -170,16 +188,18 @@ const mapStateToProps = (state, ownProps) => { return { callTaker: state.callTaker, currentQuery: state.otp.currentQuery, - request, - timeFormat: getTimeFormat(state.otp.config) + request } } const mapDispatchToProps = { addFieldTripNote: callTakerActions.addFieldTripNote, + deleteFieldTripNote: callTakerActions.deleteFieldTripNote, + editSubmitterNotes: callTakerActions.editSubmitterNotes, fetchQueries: callTakerActions.fetchQueries, setActiveFieldTrip: callTakerActions.setActiveFieldTrip, setFieldTripFilter: callTakerActions.setFieldTripFilter, + setRequestStatus: callTakerActions.setRequestStatus, toggleFieldTrips: callTakerActions.toggleFieldTrips } diff --git a/lib/components/admin/field-trip-notes.js b/lib/components/admin/field-trip-notes.js index c7aaaa46e..d87ce5aaf 100644 --- a/lib/components/admin/field-trip-notes.js +++ b/lib/components/admin/field-trip-notes.js @@ -6,9 +6,7 @@ import Icon from '../narrative/icon' import { Button, Full, - Header, - P, - Val + Header } from './styled' const Quote = styled.p` @@ -20,10 +18,13 @@ const Footer = styled.footer` font-size: x-small; ` -const Note = ({note}) => { +const Note = ({note, onClickDelete}) => { return (
    {note.note} +
    {note.userName} on {note.timeStamp}
    ) @@ -43,11 +44,10 @@ const Feedback = ({feedback}) => { export default class FieldTripNotes extends Component { _getNotesCount = () => { const {request} = this.props - const {feedback, notes, submitterNotes} = request + const {feedback, notes} = request let notesCount = 0 if (notes && notes.length) notesCount += notes.length if (feedback && feedback.length) notesCount += feedback.length - if (submitterNotes) notesCount++ return notesCount } @@ -61,13 +61,20 @@ export default class FieldTripNotes extends Component { if (note) addFieldTripNote(request, {note, type}) } + _deleteNote = (note) => { + const {deleteFieldTripNote, request} = this.props + if (confirm(`Are you sure you want to delete note: ${note.note}?`)) { + console.log('OK deleting') + deleteFieldTripNote(request, note.id) + } + } + render () { const {expanded, onClickToggle, request} = this.props if (!request) return null const { feedback, - notes, - submitterNotes + notes } = request const internalNotes = [] const operationalNotes = [] @@ -92,8 +99,6 @@ export default class FieldTripNotes extends Component { {expanded && <> -
    Teacher notes
    -

    {submitterNotes}

    User feedback
    {feedback && feedback.length > 0 ? feedback.map((f, i) => ) @@ -101,12 +106,12 @@ export default class FieldTripNotes extends Component { }
    Internal agent notes
    {internalNotes && internalNotes.length > 0 - ? internalNotes.map(n => ) + ? internalNotes.map(n => ) : 'No internal notes submitted.' }
    Operational notes
    {operationalNotes && operationalNotes.length > 0 - ? operationalNotes.map(n => ) + ? operationalNotes.map(n => ) : 'No operational notes submitted.' } diff --git a/lib/components/admin/field-trip-windows.js b/lib/components/admin/field-trip-windows.js index 64d426315..73fd29ae7 100644 --- a/lib/components/admin/field-trip-windows.js +++ b/lib/components/admin/field-trip-windows.js @@ -7,6 +7,8 @@ import * as callTakerActions from '../../actions/call-taker' import DraggableWindow from './draggable-window' import FieldTripDetails from './field-trip-details' import Icon from '../narrative/icon' +import Loading from '../narrative/loading' +import {FETCH_STATUS} from '../../util/constants' // List of tabs used for filtering field trips. const TABS = [ @@ -50,6 +52,8 @@ class FieldTripWindows extends Component { } } + _onClickRefresh = () => this.props.fetchFieldTrips() + _onCloseActiveFieldTrip = () => { this.props.setActiveFieldTrip(null) } @@ -99,17 +103,25 @@ class FieldTripWindows extends Component { <>

    Field Trip Requests{' '} - + + + +

    {TABS.map(tab => { const active = tab.id === filter.tab @@ -137,15 +149,17 @@ class FieldTripWindows extends Component { onClickClose={toggleFieldTrips} style={{width: '450px'}} > - {visibleRequests.length > 0 - ? visibleRequests.map((request, i) => ( - - )) - :
    No field trips found.
    + {fieldTrip.requests.status === FETCH_STATUS.FETCHING + ? + : visibleRequests.length > 0 + ? visibleRequests.map((request, i) => ( + + )) + :
    No field trips found.
    } : null @@ -222,6 +236,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { fetchFieldTripDetails: callTakerActions.fetchFieldTripDetails, + fetchFieldTrips: callTakerActions.fetchFieldTrips, setActiveFieldTrip: callTakerActions.setActiveFieldTrip, setFieldTripFilter: callTakerActions.setFieldTripFilter, toggleFieldTrips: callTakerActions.toggleFieldTrips diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index 40d4ecd9e..a0e99a318 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -1,6 +1,11 @@ +import { planParamsToQueryAsync } from '@opentripplanner/core-utils/lib/query' +import { getTimeFormat } from '@opentripplanner/core-utils/lib/time' import moment from 'moment' import React, {Component} from 'react' +import { connect } from 'react-redux' +import * as callTakerActions from '../../actions/call-taker' +import * as formActions from '../../actions/form' import { B, Button, @@ -9,7 +14,7 @@ import { P } from './styled' -export default class TripStatus extends Component { +class TripStatus extends Component { _formatTime = (time) => moment(time).format(this.props.timeFormat) _formatTripStatus = (tripStatus) => { @@ -27,6 +32,28 @@ export default class TripStatus extends Component { ) } + _onPlanTrip = async () => { + const {outbound, planInbound, planOutbound, request} = this.props + const trip = this._getTrip() + if (!trip) { + // Construct params from request details + if (outbound) planOutbound(request) + else planInbound(request) + } else { + // Populate params from saved query params + const params = await planParamsToQueryAsync(JSON.parse(trip.queryParams)) + this.props.setQueryParam(params, trip.id) + } + } + + _getTrip = () => { + const {outbound, request} = this.props + if (!request || !request.trips) return null + return outbound + ? request.trips[0] + : request.trips[1] + } + render () { const {outbound, request} = this.props const { @@ -38,9 +65,14 @@ export default class TripStatus extends Component { outboundTripStatus, startLocation } = request + if (!request) { + console.warn('Could not find field trip request') + return null + } const status = outbound ? outboundTripStatus : inboundTripStatus const start = outbound ? startLocation : endLocation const end = outbound ? endLocation : startLocation + const trip = this._getTrip() return (
    @@ -58,7 +90,7 @@ export default class TripStatus extends Component { leave at {this._formatTime(leaveDestinationTime)}

    : <> -

    From {endLocation} to {startLocation}

    +

    From {start} to {end}

    Due back at {this._formatTime(arriveSchoolTime)}

    } @@ -67,3 +99,19 @@ export default class TripStatus extends Component { ) } } + +const mapStateToProps = (state, ownProps) => { + return { + callTaker: state.callTaker, + currentQuery: state.otp.currentQuery, + timeFormat: getTimeFormat(state.otp.config) + } +} + +const mapDispatchToProps = { + planInbound: callTakerActions.planInbound, + planOutbound: callTakerActions.planOutbound, + setQueryParam: formActions.setQueryParam +} + +export default connect(mapStateToProps, mapDispatchToProps)(TripStatus) diff --git a/lib/components/admin/updatable.js b/lib/components/admin/updatable.js index 727f2e856..a36f073c6 100644 --- a/lib/components/admin/updatable.js +++ b/lib/components/admin/updatable.js @@ -4,17 +4,24 @@ import { Button } from 'react-bootstrap' import { Val } from './styled' export default class Updatable extends Component { + // static defaultProps = { + // onUpdate: (val) => console.log(val) + // } + _onClick = () => { - const {field, value} = this.props - const newValue = window.prompt(`Please input new value for ${field}`, value) - console.log(newValue) - // FIXME: UPDATE request + const {fieldName, onUpdate, value} = this.props + const newValue = window.prompt( + `Please input new value for ${fieldName}:`, + value + ) + if (newValue !== null) onUpdate(newValue) } render () { - const {value} = this.props + const {fieldName, value} = this.props return ( <> + {fieldName}:{' '} {value} + : <> + + + + } + +

    + {fields.map(f => { + const input = ( + + ) + return ( +

    + {valueFirst + ? <>{input} {f.label} + : <>{f.label}: {input} + } +

    + ) + })} + + ) + } +} + +class InputToggle extends Component { + _onChange = (evt) => { + const {fieldName, inputProps = {}, onChange} = this.props + let value = evt.target.value + if (inputProps.type === 'number') { + value = +evt.target.value + } + onChange(fieldName, value) + } + render () { + const {inputProps, fieldName, isEditing, options, style, value} = this.props + if (isEditing) { + if (options) { + return ( + + ) + } else { + return + } + } + return {options ? options[value] : value} + } +} diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index 527cd8e03..29a53b5a7 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -1,23 +1,26 @@ +import { getDateFormat } from '@opentripplanner/core-utils/lib/time' +import moment from 'moment' import React, { Component } from 'react' import { DropdownButton, MenuItem } from 'react-bootstrap' import { connect } from 'react-redux' import * as callTakerActions from '../../actions/call-taker' import DraggableWindow from './draggable-window' +import EditableSection from './editable-section' import FieldTripNotes from './field-trip-notes' import Icon from '../narrative/icon' import { B, Button, Container, - Half, Full, + Half, Header, - P, - Val + P } from './styled' import TripStatus from './trip-status' import Updatable from './updatable' +import {getGroupSize} from '../../util/call-taker' const TICKET_TYPES = { own_tickets: 'Will use own tickets', @@ -30,14 +33,31 @@ const PAYMENT_PREFS = { fax_cc: 'Will fax credit card info to TriMet', mail_check: 'Will mail check to TriMet' } + +const inputProps = { + min: 0, + step: 1, + type: 'number' +} +const GROUP_FIELDS = [ + {inputProps, fieldName: 'numStudents', label: 'students 7 or older'}, + {inputProps, fieldName: 'numFreeStudents', label: 'students under 7'}, + {inputProps, fieldName: 'numChaperones', label: 'chaperones'} +] +const PAYMENT_FIELDS = [ + {label: 'Ticket type', fieldName: 'ticketType', options: TICKET_TYPES}, + {label: 'Payment preference', fieldName: 'paymentPreference', options: PAYMENT_PREFS}, + {label: 'Invoice required', fieldName: 'requireInvoice', options: ['Yes', 'No']}, + {label: 'Class Pass Hop Card #', fieldName: 'classpassId'}, + {label: 'Credit card type', fieldName: 'ccType'}, + {label: 'Name on credit card', fieldName: 'ccName'}, + {label: 'Credit card last 4 digits', fieldName: 'ccLastFour'}, + {label: 'Check/Money order number', fieldName: 'checkNumber'} +] /** * Shows the details for the active Field Trip Request. */ class FieldTripDetails extends Component { - state ={ - expandNotes: true - } - _editSubmitterNotes = (val) => this.props.editSubmitterNotes(this.props.request, val) _onCloseActiveFieldTrip = () => this.props.setActiveFieldTrip(null) @@ -49,36 +69,25 @@ class FieldTripDetails extends Component { } } - _toggleNotes = () => this.setState({expandNotes: !this.state.expandNotes}) - render () { const { addFieldTripNote, callTaker, + dateFormat, deleteFieldTripNote, - request + request, + setRequestGroupSize, + setRequestPaymentInfo } = this.props if (!request) return null const { - ccLastFour, - ccName, - ccType, - checkNumber, - classpassId, id, notes, - numChaperones, - numFreeStudents, - numStudents, - paymentPreference, - requireInvoice, schoolName, submitterNotes, teacherName, - ticketType, travelDate } = request - const total = numStudents + numChaperones + numFreeStudents const {fieldTrip} = callTaker const defaultPosition = {...fieldTrip.position} const internalNotes = [] @@ -89,6 +98,8 @@ class FieldTripDetails extends Component { }) defaultPosition.x = defaultPosition.x - 460 defaultPosition.y = defaultPosition.y - 100 + const travelDateAsMoment = moment(travelDate) + const total = getGroupSize(request) return ( } header={ -

    +

    {schoolName} Trip (#{id}) +
    + + Travel date: {travelDateAsMoment.format(dateFormat)}{' '} + ({travelDateAsMoment.fromNow()}) + +

    } height='375px' @@ -128,28 +145,28 @@ class FieldTripDetails extends Component { style={{width: '450px'}} > -
    Group Information ({travelDate})
    +
    Group Information

    {schoolName}

    Teacher: {teacherName}

    } onUpdate={this._editSubmitterNotes} value={submitterNotes} />

    -

    - Total group size: {total} - -

    -

    {numStudents} students 7 or older

    -

    {numFreeStudents} students under 7

    -

    {numStudents} chaperones

    + {total} total group size} + inputStyle={{lineHeight: '0.8em', padding: '0px', width: '50px'}} + onChange={setRequestGroupSize} + request={request} + valueFirst + />
    -
    - Payment information -
    -

    Ticket type: {TICKET_TYPES[ticketType]}

    -

    Payment preference: {PAYMENT_PREFS[paymentPreference]}

    -

    Invoice required:

    -

    Class Pass Hop Card #: {classpassId}

    -

    Credit card type: {ccType}

    -

    Name on credit card: {ccName}

    -

    Credit card last 4 digits: {ccLastFour}

    -

    Check/Money order number: {checkNumber}

    + + Payment information +
    + } + inputStyle={{lineHeight: '0.8em', padding: '0px', width: '100px'}} + onChange={setRequestPaymentInfo} + request={request} + />
    @@ -188,6 +203,7 @@ const mapStateToProps = (state, ownProps) => { return { callTaker: state.callTaker, currentQuery: state.otp.currentQuery, + dateFormat: getDateFormat(state.otp.config), request } } @@ -199,6 +215,8 @@ const mapDispatchToProps = { fetchQueries: callTakerActions.fetchQueries, setActiveFieldTrip: callTakerActions.setActiveFieldTrip, setFieldTripFilter: callTakerActions.setFieldTripFilter, + setRequestGroupSize: callTakerActions.setRequestGroupSize, + setRequestPaymentInfo: callTakerActions.setRequestPaymentInfo, setRequestStatus: callTakerActions.setRequestStatus, toggleFieldTrips: callTakerActions.toggleFieldTrips } diff --git a/lib/components/admin/field-trip-notes.js b/lib/components/admin/field-trip-notes.js index d87ce5aaf..9ff371bce 100644 --- a/lib/components/admin/field-trip-notes.js +++ b/lib/components/admin/field-trip-notes.js @@ -1,5 +1,5 @@ import React, { Component } from 'react' -import { Badge } from 'react-bootstrap' +import { Badge, Button as BsButton } from 'react-bootstrap' import styled from 'styled-components' import Icon from '../narrative/icon' @@ -21,10 +21,14 @@ const Footer = styled.footer` const Note = ({note, onClickDelete}) => { return (
    + onClickDelete(note)} + > + + {note.note} -
    {note.userName} on {note.timeStamp}
    ) @@ -63,14 +67,14 @@ export default class FieldTripNotes extends Component { _deleteNote = (note) => { const {deleteFieldTripNote, request} = this.props - if (confirm(`Are you sure you want to delete note: ${note.note}?`)) { + if (confirm(`Are you sure you want to delete note "${note.note}"?`)) { console.log('OK deleting') deleteFieldTripNote(request, note.id) } } render () { - const {expanded, onClickToggle, request} = this.props + const {request} = this.props if (!request) return null const { feedback, @@ -87,9 +91,6 @@ export default class FieldTripNotes extends Component {
    Notes/Feedback{' '} {this._getNotesCount()} - @@ -97,24 +98,22 @@ export default class FieldTripNotes extends Component { Ops. note
    - {expanded && - <> -
    User feedback
    - {feedback && feedback.length > 0 - ? feedback.map((f, i) => ) - : 'No feedback submitted.' - } -
    Internal agent notes
    - {internalNotes && internalNotes.length > 0 - ? internalNotes.map(n => ) - : 'No internal notes submitted.' - } -
    Operational notes
    - {operationalNotes && operationalNotes.length > 0 - ? operationalNotes.map(n => ) - : 'No operational notes submitted.' - } - +
    User feedback
    + {feedback && feedback.length > 0 + ? feedback.map((f, i) => ) + : 'No feedback submitted.' + } +
    Internal agent notes
    + {internalNotes && internalNotes.length > 0 + ? internalNotes.map(n => + ) + : 'No internal notes submitted.' + } +
    Operational notes
    + {operationalNotes && operationalNotes.length > 0 + ? operationalNotes.map(n => + ) + : 'No operational notes submitted.' } ) diff --git a/lib/components/admin/styled.js b/lib/components/admin/styled.js index 02d3d7603..d09e6c30d 100644 --- a/lib/components/admin/styled.js +++ b/lib/components/admin/styled.js @@ -26,6 +26,7 @@ export const Header = styled.h4` ` export const P = styled.p` + font-size: 0.9em; margin-bottom: 0px; ` diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index a0e99a318..f50ab1368 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -1,4 +1,3 @@ -import { planParamsToQueryAsync } from '@opentripplanner/core-utils/lib/query' import { getTimeFormat } from '@opentripplanner/core-utils/lib/time' import moment from 'moment' import React, {Component} from 'react' @@ -6,6 +5,7 @@ import { connect } from 'react-redux' import * as callTakerActions from '../../actions/call-taker' import * as formActions from '../../actions/form' +import Icon from '../narrative/icon' import { B, Button, @@ -13,88 +13,80 @@ import { Header, P } from './styled' +import { getTrip } from '../../util/call-taker' class TripStatus extends Component { + _getTrip = () => getTrip(this.props.request, this.props.outbound) + _formatTime = (time) => moment(time).format(this.props.timeFormat) - _formatTripStatus = (tripStatus) => { - if (!tripStatus) { + _formatTripStatus = () => { + if (!this._getStatus()) { return ( No itineraries planned! Click Plan to plan trip. ) } + const trip = this._getTrip() + if (!trip) return Error finding trip! return ( - {JSON.stringify(tripStatus)} + {trip.groupItineraries.length} group itineraries, planned by{' '} + {trip.createdBy} at {trip.timeStamp} ) } - _onPlanTrip = async () => { - const {outbound, planInbound, planOutbound, request} = this.props - const trip = this._getTrip() - if (!trip) { - // Construct params from request details - if (outbound) planOutbound(request) - else planInbound(request) - } else { - // Populate params from saved query params - const params = await planParamsToQueryAsync(JSON.parse(trip.queryParams)) - this.props.setQueryParam(params, trip.id) - } - } - - _getTrip = () => { + _getStatus = () => { const {outbound, request} = this.props - if (!request || !request.trips) return null - return outbound - ? request.trips[0] - : request.trips[1] + return outbound ? request.outboundTripStatus : request.inboundTripStatus } + _getStatusIcon = () => this._getStatus() + ? + : + + _onPlanTrip = () => this.props.planTrip(this.props.request, this.props.outbound) + + _onSaveTrip = () => this.props.saveRequestTrip(this.props.request, this.props.outbound) + render () { const {outbound, request} = this.props const { arriveDestinationTime, arriveSchoolTime, endLocation, - inboundTripStatus, leaveDestinationTime, - outboundTripStatus, startLocation } = request if (!request) { console.warn('Could not find field trip request') return null } - const status = outbound ? outboundTripStatus : inboundTripStatus const start = outbound ? startLocation : endLocation const end = outbound ? endLocation : startLocation - const trip = this._getTrip() return (
    + {this._getStatusIcon()} {outbound ? 'Outbound' : 'Inbound'} trip - {status && - - }

    From {start} to {end}

    {outbound ?

    - Arriving at {this._formatTime(arriveDestinationTime)},{' '} - leave at {this._formatTime(leaveDestinationTime)} + Arriving at {this._formatTime(arriveDestinationTime)}

    : <> -

    From {start} to {end}

    -

    Due back at {this._formatTime(arriveSchoolTime)}

    +

    + Leave at {this._formatTime(leaveDestinationTime)},{' '} + due back at {this._formatTime(arriveSchoolTime)} +

    } -

    {this._formatTripStatus(status)}

    +

    {this._formatTripStatus()}

    ) } @@ -109,8 +101,8 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - planInbound: callTakerActions.planInbound, - planOutbound: callTakerActions.planOutbound, + planTrip: callTakerActions.planTrip, + saveRequestTrip: callTakerActions.saveRequestTrip, setQueryParam: formActions.setQueryParam } diff --git a/lib/components/admin/updatable.js b/lib/components/admin/updatable.js index a36f073c6..c69800b55 100644 --- a/lib/components/admin/updatable.js +++ b/lib/components/admin/updatable.js @@ -18,10 +18,10 @@ export default class Updatable extends Component { } render () { - const {fieldName, value} = this.props + const {fieldName, label, value} = this.props return ( <> - {fieldName}:{' '} + {label || fieldName}:{' '} {value}
    + {groupSize !== null && maxGroupSize && + + Group size:{' '} + + + } + : <> + - : <> - - - - } - -

    + + + } + {fields.map(f => { const input = ( { const {fieldName, inputProps = {}, onChange} = this.props diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index 29a53b5a7..5be84aa05 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -4,7 +4,7 @@ import React, { Component } from 'react' import { DropdownButton, MenuItem } from 'react-bootstrap' import { connect } from 'react-redux' -import * as callTakerActions from '../../actions/call-taker' +import * as fieldTripActions from '../../actions/field-trip' import DraggableWindow from './draggable-window' import EditableSection from './editable-section' import FieldTripNotes from './field-trip-notes' @@ -13,10 +13,12 @@ import { B, Button, Container, - Full, + FullWithMargin, Half, Header, - P + InlineHeader, + P, + Text } from './styled' import TripStatus from './trip-status' import Updatable from './updatable' @@ -60,6 +62,9 @@ const PAYMENT_FIELDS = [ class FieldTripDetails extends Component { _editSubmitterNotes = (val) => this.props.editSubmitterNotes(this.props.request, val) + _getRequestLink = (path, isPublic = false) => + `${this.props.datastoreUrl}/${isPublic ? 'public/' : ''}fieldtrip/${path}?requestId=${this.props.request.id}` + _onCloseActiveFieldTrip = () => this.props.setActiveFieldTrip(null) _onClickCancel = () => { @@ -112,10 +117,16 @@ class FieldTripDetails extends Component { id='field-trip-menu' title='View' > - + Feedback link - + Receipt link @@ -160,8 +171,8 @@ class FieldTripDetails extends Component { {total} total group size} fields={GROUP_FIELDS} - header={{total} total group size} inputStyle={{lineHeight: '0.8em', padding: '0px', width: '50px'}} onChange={setRequestGroupSize} request={request} @@ -173,19 +184,20 @@ class FieldTripDetails extends Component { request={request} /> - + + // Use inline header so that 'Change' button appears on top line. + children={ + Payment information - + } + fields={PAYMENT_FIELDS} inputStyle={{lineHeight: '0.8em', padding: '0px', width: '100px'}} onChange={setRequestPaymentInfo} request={request} /> - + { return { callTaker: state.callTaker, currentQuery: state.otp.currentQuery, + datastoreUrl: state.otp.config.datastoreUrl, dateFormat: getDateFormat(state.otp.config), request } } const mapDispatchToProps = { - addFieldTripNote: callTakerActions.addFieldTripNote, - deleteFieldTripNote: callTakerActions.deleteFieldTripNote, - editSubmitterNotes: callTakerActions.editSubmitterNotes, - fetchQueries: callTakerActions.fetchQueries, - setActiveFieldTrip: callTakerActions.setActiveFieldTrip, - setFieldTripFilter: callTakerActions.setFieldTripFilter, - setRequestGroupSize: callTakerActions.setRequestGroupSize, - setRequestPaymentInfo: callTakerActions.setRequestPaymentInfo, - setRequestStatus: callTakerActions.setRequestStatus, - toggleFieldTrips: callTakerActions.toggleFieldTrips + addFieldTripNote: fieldTripActions.addFieldTripNote, + deleteFieldTripNote: fieldTripActions.deleteFieldTripNote, + editSubmitterNotes: fieldTripActions.editSubmitterNotes, + fetchQueries: fieldTripActions.fetchQueries, + setActiveFieldTrip: fieldTripActions.setActiveFieldTrip, + setFieldTripFilter: fieldTripActions.setFieldTripFilter, + setRequestGroupSize: fieldTripActions.setRequestGroupSize, + setRequestPaymentInfo: fieldTripActions.setRequestPaymentInfo, + setRequestStatus: fieldTripActions.setRequestStatus, + toggleFieldTrips: fieldTripActions.toggleFieldTrips } export default connect(mapStateToProps, mapDispatchToProps)(FieldTripDetails) diff --git a/lib/components/admin/field-trip-windows.js b/lib/components/admin/field-trip-windows.js index 73fd29ae7..5fb4e0af8 100644 --- a/lib/components/admin/field-trip-windows.js +++ b/lib/components/admin/field-trip-windows.js @@ -3,7 +3,7 @@ import React, { Component } from 'react' import { Badge } from 'react-bootstrap' import { connect } from 'react-redux' -import * as callTakerActions from '../../actions/call-taker' +import * as fieldTripActions from '../../actions/field-trip' import DraggableWindow from './draggable-window' import FieldTripDetails from './field-trip-details' import Icon from '../narrative/icon' @@ -235,11 +235,11 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - fetchFieldTripDetails: callTakerActions.fetchFieldTripDetails, - fetchFieldTrips: callTakerActions.fetchFieldTrips, - setActiveFieldTrip: callTakerActions.setActiveFieldTrip, - setFieldTripFilter: callTakerActions.setFieldTripFilter, - toggleFieldTrips: callTakerActions.toggleFieldTrips + fetchFieldTripDetails: fieldTripActions.fetchFieldTripDetails, + fetchFieldTrips: fieldTripActions.fetchFieldTrips, + setActiveFieldTrip: fieldTripActions.setActiveFieldTrip, + setFieldTripFilter: fieldTripActions.setFieldTripFilter, + toggleFieldTrips: fieldTripActions.toggleFieldTrips } export default connect(mapStateToProps, mapDispatchToProps)(FieldTripWindows) diff --git a/lib/components/admin/styled.js b/lib/components/admin/styled.js index d09e6c30d..aae999930 100644 --- a/lib/components/admin/styled.js +++ b/lib/components/admin/styled.js @@ -1,5 +1,5 @@ import { Button as BsButton } from 'react-bootstrap' -import styled from 'styled-components' +import styled, {css} from 'styled-components' export const B = styled.strong`` @@ -20,16 +20,32 @@ export const Full = styled.div` width: 100% ` +export const FullWithMargin = styled(Full)` + margin-top: 10px; +` + export const Header = styled.h4` margin-bottom: 5px; width: 100%; ` -export const P = styled.p` +export const InlineHeader = styled(Header)` + display: inline; +` + +export const textCss = css` font-size: 0.9em; margin-bottom: 0px; ` +export const P = styled.p` + ${textCss} +` + +export const Text = styled.span` + ${textCss} +` + export const Val = styled.span` :empty:before { color: grey; diff --git a/lib/components/admin/trip-status.js b/lib/components/admin/trip-status.js index f50ab1368..b48b9123b 100644 --- a/lib/components/admin/trip-status.js +++ b/lib/components/admin/trip-status.js @@ -3,7 +3,7 @@ import moment from 'moment' import React, {Component} from 'react' import { connect } from 'react-redux' -import * as callTakerActions from '../../actions/call-taker' +import * as fieldTripActions from '../../actions/field-trip' import * as formActions from '../../actions/form' import Icon from '../narrative/icon' import { @@ -101,8 +101,8 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - planTrip: callTakerActions.planTrip, - saveRequestTrip: callTakerActions.saveRequestTrip, + planTrip: fieldTripActions.planTrip, + saveRequestTrip: fieldTripActions.saveRequestTrip, setQueryParam: formActions.setQueryParam } diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index 5a949173f..a71099ef2 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -5,7 +5,7 @@ import { Button } from 'react-bootstrap' import { connect } from 'react-redux' import * as apiActions from '../../actions/api' -import * as callTakerActions from '../../actions/call-taker' +import * as fieldTripActions from '../../actions/field-trip' import * as formActions from '../../actions/form' import AddPlaceButton from '../form/add-place-button' import AdvancedOptions from '../form/call-taker/advanced-options' @@ -289,7 +289,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { findRoutes: apiActions.findRoutes, routingQuery: apiActions.routingQuery, - setGroupSize: callTakerActions.setGroupSize, + setGroupSize: fieldTripActions.setGroupSize, setQueryParam: formActions.setQueryParam } diff --git a/lib/util/call-taker.js b/lib/util/call-taker.js index 0ba06def1..6c11f844b 100644 --- a/lib/util/call-taker.js +++ b/lib/util/call-taker.js @@ -6,6 +6,17 @@ function placeToLatLonStr (place) { return `${place.lat.toFixed(6)},${place.lon.toFixed(6)}` } +/** + * @return {boolean} - whether a calltaker session is invalid + */ +export function sessionIsInvalid (session) { + if (!session || !session.sessionId) { + console.error('No valid OTP datastore session found.') + return true + } + return false +} + /** * Utility to map an OTP MOD UI search object to a Call Taker datastore query * object. From 337bb963a3a6b9c536576a55595b39fb66d7117e Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 3 Feb 2021 10:17:06 -0500 Subject: [PATCH 131/265] refactor(AccountPage): Remove unused prop. --- lib/components/user/account-page.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/components/user/account-page.js b/lib/components/user/account-page.js index 3807dc563..4f2136f7e 100644 --- a/lib/components/user/account-page.js +++ b/lib/components/user/account-page.js @@ -50,8 +50,7 @@ class AccountPage extends Component { const mapStateToProps = (state, ownProps) => { return { - loggedInUser: state.user.loggedInUser, - trips: state.user.loggedInUserMonitoredTrips + loggedInUser: state.user.loggedInUser } } From 7b4ee668e411088a1195af2cf2ccad459a8025d3 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Wed, 3 Feb 2021 10:20:20 -0500 Subject: [PATCH 132/265] refactor(updatable): remove commented code --- lib/components/admin/updatable.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/components/admin/updatable.js b/lib/components/admin/updatable.js index c69800b55..16bd37246 100644 --- a/lib/components/admin/updatable.js +++ b/lib/components/admin/updatable.js @@ -4,10 +4,6 @@ import { Button } from 'react-bootstrap' import { Val } from './styled' export default class Updatable extends Component { - // static defaultProps = { - // onUpdate: (val) => console.log(val) - // } - _onClick = () => { const {fieldName, onUpdate, value} = this.props const newValue = window.prompt( From 060ebb07dedca149891a1535efc72e1e92a4050d Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 3 Feb 2021 10:45:21 -0500 Subject: [PATCH 133/265] refactor(BackLink): Extract back button link. --- lib/components/user/back-link.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 lib/components/user/back-link.js diff --git a/lib/components/user/back-link.js b/lib/components/user/back-link.js new file mode 100644 index 000000000..2743f4226 --- /dev/null +++ b/lib/components/user/back-link.js @@ -0,0 +1,25 @@ +import React from 'react' +import { Button } from 'react-bootstrap' +import styled from 'styled-components' + +import Icon from '../narrative/icon' + +const StyledButton = styled(Button)` + padding: 0; +` + +const navigateBack = () => window.history.back() + +/** + * Back link that navigates to the previous location in browser history. + */ +const BackLink = () => ( + + Back + +) + +export default BackLink From 907e9a891388b95b2ede511b92c75306a39490cb Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Wed, 3 Feb 2021 10:48:19 -0500 Subject: [PATCH 134/265] refactor(field-trip): move non-payment fields to group info --- lib/components/admin/editable-section.js | 7 +++- lib/components/admin/field-trip-details.js | 46 ++++++---------------- lib/util/call-taker.js | 34 ++++++++++++++++ 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/lib/components/admin/editable-section.js b/lib/components/admin/editable-section.js index cee1149ad..57c089e35 100644 --- a/lib/components/admin/editable-section.js +++ b/lib/components/admin/editable-section.js @@ -56,7 +56,12 @@ export default class EditableSection extends Component { return ( <> {children} - + {!isEditing ? + {title} + + )} { paneSequence.map(({ pane: Pane, props, title }, index) => ( @@ -25,7 +31,7 @@ const StackedPaneDisplay = ({ onCancel, paneSequence, title }) => ( text: 'Cancel' }} okayButton={{ - text: 'Save Preferences', + text: 'Save', type: 'submit' }} /> From bdcebdff2bab6237bb804ea49e91e58d44bdb00c Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 3 Feb 2021 11:49:04 -0500 Subject: [PATCH 141/265] refactor(SequentialPaneDisplay): Improve routing Remove regex for routing. --- lib/components/user/sequential-pane-display.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/components/user/sequential-pane-display.js b/lib/components/user/sequential-pane-display.js index 3ded05eaf..09e8dead3 100644 --- a/lib/components/user/sequential-pane-display.js +++ b/lib/components/user/sequential-pane-display.js @@ -19,8 +19,8 @@ class SequentialPaneDisplay extends Component { * Routes to the next pane URL. */ _routeTo = nextId => { - const { currentPath, activePaneId, routeTo } = this.props - routeTo(`${currentPath.replace(new RegExp(`/${activePaneId}$`), `/${nextId}`)}`) + const { parentPath, routeTo } = this.props + routeTo(`${parentPath}/${nextId}`) } _handleToNextPane = async e => { @@ -78,9 +78,10 @@ class SequentialPaneDisplay extends Component { const mapStateToProps = (state, ownProps) => { const { activePaneId, paneSequence } = ownProps + const { pathname } = state.router.location return { activePane: paneSequence[activePaneId], - currentPath: state.router.location.pathname + parentPath: pathname.substr(0, pathname.lastIndexOf('/')) } } From 209318fb291171582ca4ef30180f9969f4cee40c Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 3 Feb 2021 12:36:28 -0500 Subject: [PATCH 142/265] improvement(PlaceLocationField): Accommodate mobile view. --- lib/components/user/places/place-editor.js | 20 +++++++++++++----- .../user/places/place-location-field.js | 21 +++++++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/components/user/places/place-editor.js b/lib/components/user/places/place-editor.js index 4965fec76..29d42b335 100644 --- a/lib/components/user/places/place-editor.js +++ b/lib/components/user/places/place-editor.js @@ -1,4 +1,5 @@ import { Field } from 'formik' +import coreUtils from '@opentripplanner/core-utils' import React, { Component } from 'react' import { FormControl, @@ -17,6 +18,8 @@ import { PlaceLocationField } from './place-location-field' +const { isMobile } = coreUtils.ui + // Styled components const LargeIcon = styled(Icon)` font-size: 150%; @@ -25,6 +28,12 @@ const FixedPlaceIcon = styled(LargeIcon)` margin-right: 10px; padding-top: 6px; ` +const Flex = styled.div` + display: flex; +` +const FlexFormGroup = styled(FormGroup)` + flex-grow: 1; +` const MESSAGES = { SET_PLACE_NAME: 'Set place name' @@ -91,23 +100,24 @@ class PlaceEditor extends Component { )} -
    + {/* For fixed places, just show the icon for place type instead of all inputs and selectors */} {isFixed && } - - + {errors.address && {errors.address}} - -
    + +
    ) } diff --git a/lib/components/user/places/place-location-field.js b/lib/components/user/places/place-location-field.js index a94e7daa8..97d275cbf 100644 --- a/lib/components/user/places/place-location-field.js +++ b/lib/components/user/places/place-location-field.js @@ -1,8 +1,11 @@ import LocationField from '@opentripplanner/location-field' import { DropdownContainer, + FormGroup, Input, - InputGroup + InputGroup, + InputGroupAddon, + MenuItemList } from '@opentripplanner/location-field/lib/styled' import styled from 'styled-components' @@ -30,9 +33,8 @@ const StyledLocationField = styled(LocationField)` display: none; } } - ${InputGroup} { - border: none; - width: 100%; + ${FormGroup} { + display: block; } ${Input} { display: table-cell; @@ -45,6 +47,17 @@ const StyledLocationField = styled(LocationField)` color: #999; } } + ${InputGroup} { + border: none; + width: 100%; + } + ${InputGroupAddon} { + display: none; + } + ${MenuItemList} { + margin-left: ${props => props.static ? '-50px' : '0px'}; + position: absolute; + } ` /** * Styled LocationField for setting a favorite place locations using the geocoder. From b207eb64bdd53e48e90abfe5f78d7bb196807603 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 3 Feb 2021 12:40:12 -0500 Subject: [PATCH 143/265] refactor(connected-links): Remove empty mapDispatchToProps --- lib/components/form/connected-links.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/components/form/connected-links.js b/lib/components/form/connected-links.js index 1809901ac..422a136c5 100644 --- a/lib/components/form/connected-links.js +++ b/lib/components/form/connected-links.js @@ -30,13 +30,10 @@ const mapStateToProps = (state, ownProps) => { } } -const mapDispatchToProps = { -} - // Enhance routing components, connect the result to redux, // and export. export default { - Link: connect(mapStateToProps, mapDispatchToProps)(connectLink(Link)), - LinkContainer: connect(mapStateToProps, mapDispatchToProps)(connectLink(LinkContainer)), - Redirect: connect(mapStateToProps, mapDispatchToProps)(connectLink(Redirect)) + Link: connect(mapStateToProps)(connectLink(Link)), + LinkContainer: connect(mapStateToProps)(connectLink(LinkContainer)), + Redirect: connect(mapStateToProps)(connectLink(Redirect)) } From 8dc59719a85afdc13f901d439d9304a701c47e9c Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 5 Feb 2021 11:14:49 -0500 Subject: [PATCH 144/265] Revert "refactor(StackedPaneDisplay): Experiment with Save button on top." This reverts commit 42a08c15a62bae19466052598a2a2416f8641a1a. --- lib/components/user/stacked-pane-display.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/components/user/stacked-pane-display.js b/lib/components/user/stacked-pane-display.js index b22626053..529fb1964 100644 --- a/lib/components/user/stacked-pane-display.js +++ b/lib/components/user/stacked-pane-display.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types' import React from 'react' -import { Button } from 'react-bootstrap' import FormNavigationButtons from './form-navigation-buttons' import { PageHeading, StackedPaneContainer } from './styled' @@ -10,12 +9,7 @@ import { PageHeading, StackedPaneContainer } from './styled' */ const StackedPaneDisplay = ({ onCancel, paneSequence, title }) => ( <> - {title && ( - - - {title} - - )} + {title && {title}} { paneSequence.map(({ pane: Pane, props, title }, index) => ( @@ -31,7 +25,7 @@ const StackedPaneDisplay = ({ onCancel, paneSequence, title }) => ( text: 'Cancel' }} okayButton={{ - text: 'Save', + text: 'Save Preferences', type: 'submit' }} /> From 4801a00160dbc1d75bb375d39d2cbb0bb4b4c34e Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 5 Feb 2021 12:25:54 -0500 Subject: [PATCH 145/265] refactor(Trip panes): Address PR comments --- .../user/monitored-trip/trip-basics-pane.js | 33 ++++--------------- .../monitored-trip/trip-notifications-pane.js | 19 ++++++----- package.json | 1 + yarn.lock | 14 ++++++++ 4 files changed, 31 insertions(+), 36 deletions(-) diff --git a/lib/components/user/monitored-trip/trip-basics-pane.js b/lib/components/user/monitored-trip/trip-basics-pane.js index 0f86fa4c5..1c97c1338 100644 --- a/lib/components/user/monitored-trip/trip-basics-pane.js +++ b/lib/components/user/monitored-trip/trip-basics-pane.js @@ -1,5 +1,6 @@ import { Field } from 'formik' -import React, { Component, createRef } from 'react' +import FormikErrorFocus from 'formik-error-focus' +import React, { Component } from 'react' import { ControlLabel, FormControl, @@ -51,29 +52,6 @@ const allDays = [ * and lets the user edit the trip name and day. */ class TripBasicsPane extends Component { - /** - * Link to the DOM for the trip name label at the top of the form, - * so we can scroll to show the trip name and days if the user attempts - * to submit the form and there is a validation error for these data. - */ - tripNameLabelRef = createRef() - - /** - * Scroll to the trip name/days fields if submitting and there is an error on these fields. - * (inspired by https://gist.github.com/dphrag/4db3b453e02567a0bb52592679554a5b) - */ - _scrollToTripDataIfInvalid = () => { - const { isSubmitting, isValidating, errors } = this.props - const keys = Object.keys(errors) - - if (keys.length > 0 && isSubmitting && !isValidating) { - const { current } = this.tripNameLabelRef - if (current) { - current.scrollIntoView({ behavior: 'smooth', block: 'start' }) - } - } - } - /** * For new trips only, update the Formik state to * uncheck days for which the itinerary is not available. @@ -101,7 +79,6 @@ class TripBasicsPane extends Component { componentDidUpdate (prevProps) { this._updateNewTripItineraryExistence(prevProps) - this._scrollToTripDataIfInvalid() } componentWillUnmount () { @@ -142,8 +119,7 @@ class TripBasicsPane extends Component { - {/* Put a ref in a native DOM element for access to scroll function. */} - Please provide a name for this trip: + Please provide a name for this trip: {/* onBlur, onChange, and value are passed automatically. */} @@ -183,6 +159,9 @@ class TripBasicsPane extends Component { } {monitoredDaysValidationState && Please select at least one day to monitor.} + + {/* Scroll to the trip name/days fields if submitting and there is an error on these fields. */} +
    ) diff --git a/lib/components/user/monitored-trip/trip-notifications-pane.js b/lib/components/user/monitored-trip/trip-notifications-pane.js index ffc86e636..e323c5c38 100644 --- a/lib/components/user/monitored-trip/trip-notifications-pane.js +++ b/lib/components/user/monitored-trip/trip-notifications-pane.js @@ -12,9 +12,9 @@ const notificationChannelLabels = { // Element styles const SettingsList = styled.ul` - border-spacing: 10px; + border-spacing: 0 10px; display: table; - padding-left: 20px; + padding-left: 0; width: 100%; label { font-weight: inherit; @@ -32,11 +32,12 @@ const SettingsListWithAlign = styled(SettingsList)` display: table-row; & > * { display: table-cell; + margin-left: 10px; } } ` -const InlineField = styled(Field)` +const InlineFormControl = styled(FormControl)` display: inline-block; margin: 0 0.5em; width: auto; @@ -54,18 +55,18 @@ const SettingsToggle = styled.button` /** * A label followed by a dropdown control. */ -const Select = ({ children, FieldComponent = Field, label, name }) => ( - // FieldComponent is kept outside of
    {' '} [Active call] diff --git a/lib/components/admin/call-taker-controls.js b/lib/components/admin/call-taker-controls.js index a92fd99ee..a5f4ff803 100644 --- a/lib/components/admin/call-taker-controls.js +++ b/lib/components/admin/call-taker-controls.js @@ -5,24 +5,20 @@ import * as apiActions from '../../actions/api' import * as callTakerActions from '../../actions/call-taker' import * as fieldTripActions from '../../actions/field-trip' import * as uiActions from '../../actions/ui' -import CallTimeCounter from './call-time-counter' import Icon from '../narrative/icon' - -const RED = '#C35134' -const BLUE = '#1C4D89' -const GREEN = '#6B931B' -const PURPLE = '#8134D3' - -const ControlsContainer = styled.div` - position: relative; -` +import { + CallHistoryButton, + CallTimeCounter, + ControlsContainer, + FieldTripsButton, + ToggleCallButton +} from './styled' /** * This component displays the controls for the Call Taker/Field Trip modules, * including: * - start/end call button * - view call list - * TODO * - view field trip list */ class CallTakerControls extends Component { @@ -46,7 +42,7 @@ class CallTakerControls extends Component { else this.props.beginCall() } - _renderCallButton = () => { + _renderCallButtonIcon = () => { // Show stop button if call not in progress. if (this._callInProgress()) { return ( @@ -78,94 +74,47 @@ class CallTakerControls extends Component { const {callTakerEnabled, fieldTripEnabled, session} = this.props // If no valid session is found, do not show calltaker controls. if (!session) return null - // FIXME: styled component - const circleButtonStyle = { - position: 'absolute', - zIndex: 999999, - color: 'white', - borderRadius: '50%', - border: 'none', - boxShadow: '2px 2px 4px #000000' - } return ( {/* Start/End Call button */} {callTakerEnabled && - + {this._renderCallButtonIcon()} + } {this._callInProgress() - ? + ? : null } {/* Call History toggle button */} {callTakerEnabled && - + } - {/* Field Trip toggle button TODO */} + {/* Field Trip toggle button */} {fieldTripEnabled && - + } ) diff --git a/lib/components/admin/call-time-counter.js b/lib/components/admin/call-time-counter.js index 1d8dfe763..1383687a4 100644 --- a/lib/components/admin/call-time-counter.js +++ b/lib/components/admin/call-time-counter.js @@ -43,9 +43,14 @@ export default class CallTimeCounter extends Component { } render () { - const {className, style} = this.props + const {className} = this.props return ( -
    +
    {this._formatSeconds(this.state.counterString)}
    ) diff --git a/lib/components/admin/field-trip-list.js b/lib/components/admin/field-trip-list.js index eee84f397..61ddeae27 100644 --- a/lib/components/admin/field-trip-list.js +++ b/lib/components/admin/field-trip-list.js @@ -7,7 +7,7 @@ import * as fieldTripActions from '../../actions/field-trip' import DraggableWindow from './draggable-window' import Icon from '../narrative/icon' import Loading from '../narrative/loading' -import {WindowHeader} from './styled' +import {FieldTripRecordButton, WindowHeader} from './styled' import {FETCH_STATUS} from '../../util/constants' // List of tabs used for filtering field trips. @@ -194,26 +194,29 @@ class FieldTripRequestRecord extends Component { className='list-unstyled' style={style} > - + + + Submitted by {teacherName} on {timeStamp} + + + {startLocation} to {endLocation} + + ) } diff --git a/lib/components/admin/field-trip-notes.js b/lib/components/admin/field-trip-notes.js index 662aaba4f..a190f5f5d 100644 --- a/lib/components/admin/field-trip-notes.js +++ b/lib/components/admin/field-trip-notes.js @@ -18,30 +18,26 @@ const Footer = styled.footer` font-size: x-small; ` -const Note = ({note, onClickDelete}) => { - return ( -
    - onClickDelete(note)} - > - - - {note.note} -
    {note.userName} on {note.timeStamp}
    -
    - ) -} +const Note = ({note, onClickDelete}) => ( +
    + onClickDelete(note)} + > + + + {note.note} +
    {note.userName} on {note.timeStamp}
    +
    +) -const Feedback = ({feedback}) => { - return ( -
    - {feedback.feedback} -
    {feedback.userName} on {feedback.timeStamp}
    -
    - ) -} +const Feedback = ({feedback}) => ( +
    + {feedback.feedback} +
    {feedback.userName} on {feedback.timeStamp}
    +
    +) /** * Renders the various notes/feedback for a field trip request. */ @@ -68,7 +64,6 @@ export default class FieldTripNotes extends Component { _deleteNote = (note) => { const {deleteFieldTripNote, request} = this.props if (confirm(`Are you sure you want to delete note "${note.note}"?`)) { - console.log('OK deleting') deleteFieldTripNote(request, note.id) } } diff --git a/lib/components/admin/field-trip-windows.js b/lib/components/admin/field-trip-windows.js index 33302e468..ddb6ee7ca 100644 --- a/lib/components/admin/field-trip-windows.js +++ b/lib/components/admin/field-trip-windows.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React from 'react' import { connect } from 'react-redux' import FieldTripDetails from './field-trip-details' @@ -10,34 +10,31 @@ const MARGIN = 15 /** * Collects the various draggable windows for the Field Trip module. */ -class FieldTripWindows extends Component { - render () { - const {callTaker} = this.props - const {fieldTrip} = callTaker - // Do not render details or list if visible is false. - if (!fieldTrip.visible) return null - return ( - <> - - - - ) - } +const FieldTripWindows = ({callTaker}) => { + const {fieldTrip} = callTaker + // Do not render details or list if visible is false. + if (!fieldTrip.visible) return null + return ( + <> + + + + ) } const mapStateToProps = (state, ownProps) => { diff --git a/lib/components/admin/styled.js b/lib/components/admin/styled.js index 942007575..c5f53259d 100644 --- a/lib/components/admin/styled.js +++ b/lib/components/admin/styled.js @@ -1,6 +1,71 @@ import { Button as BsButton } from 'react-bootstrap' import styled, {css} from 'styled-components' +import DefaultCounter from './call-time-counter' + +// Call Taker Controls Components + +const RED = '#C35134' +const BLUE = '#1C4D89' +const GREEN = '#6B931B' +const PURPLE = '#8134D3' + +const circleButtonStyle = css` + border: none; + border-radius: 50%; + box-shadow: 2px 2px 4px #000000; + color: white; + position: absolute; + z-index: 999999; +` + +export const CallHistoryButton = styled.button` + ${circleButtonStyle} + background-color: ${GREEN}; + height: 40px; + margin-left: 69px; + top: 140px; + width: 40px; +` + +export const CallTimeCounter = styled(DefaultCounter)` + background-color: ${BLUE}; + border-radius: 20px; + box-shadow: 2px 2px 4px #000000; + color: white; + font-weight: 600; + margin-left: -8px; + position: absolute; + text-align: center; + top: 241px; + width: 80px; + z-index: 999999; +` + +export const ControlsContainer = styled.div` + position: relative; +` + +export const FieldTripsButton = styled.button` + ${circleButtonStyle} + background-color: ${PURPLE}; + height: 50px; + margin-left: 80px; + top: 190px; + width: 50px; +` + +export const ToggleCallButton = styled.button` + ${circleButtonStyle} + background-color: ${props => props.callInProgress ? RED : BLUE}; + height: 80px; + margin-left: -8px; + top: 154px; + width: 80px; +` + +// Field Trip Windows Components + export const Bold = styled.strong`` export const Button = styled(BsButton)` @@ -16,6 +81,15 @@ export const Half = styled.div` width: 50% ` +// Make sure button extends to end of window. +export const FieldTripRecordButton = styled.button` + display: inline-block; + width: 100%; + width: -moz-available; + width: -webkit-fill-available; + width: fill-available; +` + export const Full = styled.div` width: 100% ` From 70f449c2c26d870e4d2a4e6c3a7e77bd4fa5b63a Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 16 Feb 2021 15:46:47 -0500 Subject: [PATCH 171/265] fix(FavoritePlaceScreen): Do not show account header while setting up account. --- lib/components/app/responsive-webapp.js | 9 +++++---- .../user/places/favorite-place-screen.js | 17 ++++++++++------- .../user/places/favorite-places-list.js | 9 ++++++--- lib/util/constants.js | 1 + 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 9bec869c4..045b03d75 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -24,6 +24,7 @@ import { AUTH0_SCOPE, ACCOUNT_SETTINGS_PATH, CREATE_ACCOUNT_PATH, + CREATE_ACCOUNT_PLACES_PATH, CREATE_ACCOUNT_VERIFY_PATH, PLACES_PATH, TRIPS_PATH, @@ -162,11 +163,11 @@ const mapStateToProps = (state, ownProps) => { activeItinerary: getActiveItinerary(state.otp), activeSearchId: state.otp.activeSearchId, currentPosition: state.otp.location.currentPosition, - query: state.otp.currentQuery, - searches: state.otp.searches, - mobileScreen: state.otp.ui.mobileScreen, initZoomOnLocate: state.otp.config.map && state.otp.config.map.initZoomOnLocate, + mobileScreen: state.otp.ui.mobileScreen, modeGroups: state.otp.config.modeGroups, + query: state.otp.currentQuery, + searches: state.otp.searches, title } } @@ -237,7 +238,7 @@ class RouterWrapperWithAuth0 extends Component { /> { - const { isCreating, placeIndex } = this.props - return isCreating + const { isNewPlace, placeIndex } = this.props + return isNewPlace ? BLANK_PLACE : (user.savedLocations ? user.savedLocations[placeIndex] @@ -86,7 +87,7 @@ class FavoritePlaceScreen extends Component { } render () { - const { isCreating, loggedInUser } = this.props + const { isCreating, isNewPlace, loggedInUser } = this.props // Get the places as shown (and not as retrieved from db), so that the index passed from URL applies // (indexes 0 and 1 are for Home and Work locations). const place = this._getPlaceToEdit(loggedInUser) @@ -95,7 +96,7 @@ class FavoritePlaceScreen extends Component { let heading if (!place) { heading = 'Place not found' - } else if (isCreating) { + } else if (isNewPlace) { heading = 'Add a new place' } else if (isFixed) { heading = `Edit ${place.name}` @@ -104,7 +105,7 @@ class FavoritePlaceScreen extends Component { } return ( - + { - const placeIndex = ownProps.match.params.id + const { params, path } = ownProps.match + const placeIndex = params.id return { - isCreating: placeIndex === 'new', + isCreating: path.startsWith(CREATE_ACCOUNT_PLACES_PATH), + isNewPlace: placeIndex === 'new', loggedInUser: state.user.loggedInUser, placeIndex } diff --git a/lib/components/user/places/favorite-places-list.js b/lib/components/user/places/favorite-places-list.js index c73a1669c..c82348bee 100644 --- a/lib/components/user/places/favorite-places-list.js +++ b/lib/components/user/places/favorite-places-list.js @@ -3,7 +3,7 @@ import { ControlLabel } from 'react-bootstrap' import { connect } from 'react-redux' import * as userActions from '../../../actions/user' -import { PLACES_PATH } from '../../../util/constants' +import { CREATE_ACCOUNT_PLACES_PATH, PLACES_PATH } from '../../../util/constants' import { isHomeOrWork } from '../../../util/user' import FavoritePlaceRow from './favorite-place-row' @@ -11,7 +11,7 @@ import FavoritePlaceRow from './favorite-place-row' * Renders an editable list user's favorite locations, and lets the user add a new one. * Additions, edits, and deletions of places take effect immediately. */ -const FavoritePlacesList = ({ deleteUserPlace, loggedInUser }) => { +const FavoritePlacesList = ({ deleteUserPlace, isCreating, loggedInUser }) => { const { savedLocations } = loggedInUser return (
    @@ -22,7 +22,7 @@ const FavoritePlacesList = ({ deleteUserPlace, loggedInUser }) => { isFixed={isHomeOrWork(place)} key={index} onDelete={() => deleteUserPlace(index)} - path={`${PLACES_PATH}/${index}`} + path={`${isCreating ? CREATE_ACCOUNT_PLACES_PATH : PLACES_PATH}/${index}`} place={place} /> ) @@ -37,7 +37,10 @@ const FavoritePlacesList = ({ deleteUserPlace, loggedInUser }) => { // connect to the redux store const mapStateToProps = (state, ownProps) => { + const path = state.router.location.pathname + const isCreating = path === CREATE_ACCOUNT_PLACES_PATH return { + isCreating, loggedInUser: state.user.loggedInUser } } diff --git a/lib/util/constants.js b/lib/util/constants.js index 51aaa9133..34e2b6a09 100644 --- a/lib/util/constants.js +++ b/lib/util/constants.js @@ -11,6 +11,7 @@ export const PLACES_PATH = `${ACCOUNT_PATH}/places` export const CREATE_ACCOUNT_PATH = `${ACCOUNT_PATH}/create` export const CREATE_ACCOUNT_TERMS_PATH = `${CREATE_ACCOUNT_PATH}/terms` export const CREATE_ACCOUNT_VERIFY_PATH = `${CREATE_ACCOUNT_PATH}/verify` +export const CREATE_ACCOUNT_PLACES_PATH = `${CREATE_ACCOUNT_PATH}/places` export const CREATE_TRIP_PATH = `${TRIPS_PATH}/new` // Gets the root URL, e.g. https://otp-instance.example.com:8080, computed once for all. From 411caf13976af1e18b2f2b18a255f4f445547838 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 16 Feb 2021 16:26:01 -0500 Subject: [PATCH 172/265] refactor(FavoritePlaceScreen): Use FormNavigationButtons. --- lib/components/user/back-link.js | 27 ------------------ .../user/places/favorite-place-screen.js | 28 ++++++++++--------- lib/util/ui.js | 5 ++++ 3 files changed, 20 insertions(+), 40 deletions(-) delete mode 100644 lib/components/user/back-link.js diff --git a/lib/components/user/back-link.js b/lib/components/user/back-link.js deleted file mode 100644 index 7f2cc82e5..000000000 --- a/lib/components/user/back-link.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react' -import { Button } from 'react-bootstrap' -import styled from 'styled-components' - -import { IconWithMargin } from './styled' - -const StyledButton = styled(Button)` - display: block; - padding: 0; -` - -const navigateBack = () => window.history.back() - -/** - * Back link that navigates to the previous location in browser history. - */ -const BackLink = () => ( - - - Back - -) - -export default BackLink diff --git a/lib/components/user/places/favorite-place-screen.js b/lib/components/user/places/favorite-place-screen.js index 9ceafe7a3..bca4281bb 100644 --- a/lib/components/user/places/favorite-place-screen.js +++ b/lib/components/user/places/favorite-place-screen.js @@ -2,25 +2,18 @@ import { withAuthenticationRequired } from '@auth0/auth0-react' import clone from 'clone' import { Form, Formik } from 'formik' import React, { Component } from 'react' -import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import styled from 'styled-components' import * as yup from 'yup' import AccountPage from '../account-page' -import BackLink from '../back-link' import * as userActions from '../../../actions/user' +import FormNavigationButtons from '../form-navigation-buttons' +import { PageHeading } from '../styled' import { CREATE_ACCOUNT_PLACES_PATH } from '../../../util/constants' -import { RETURN_TO_CURRENT_ROUTE } from '../../../util/ui' +import { navigateBack, RETURN_TO_CURRENT_ROUTE } from '../../../util/ui' import { isHomeOrWork, PLACE_TYPES } from '../../../util/user' import withLoggedInUserSupport from '../with-logged-in-user-support' import PlaceEditor from './place-editor' -import { PageHeading } from '../styled' - -// Styled components -const SaveButton = styled(Button)` - float: right; -` const BLANK_PLACE = { ...PLACE_TYPES.custom, @@ -51,7 +44,7 @@ class FavoritePlaceScreen extends Component { await saveUserPlace(placeToSave, placeIndex) // Return to previous location when done. - window.history.back() + navigateBack() } /** @@ -106,7 +99,6 @@ class FavoritePlaceScreen extends Component { return ( -
    - {place && Save} {heading}
    @@ -135,6 +126,17 @@ class FavoritePlaceScreen extends Component { } }
    + +
    ) } diff --git a/lib/util/ui.js b/lib/util/ui.js index ee0892b92..f947128f7 100644 --- a/lib/util/ui.js +++ b/lib/util/ui.js @@ -39,3 +39,8 @@ export function getErrorStates (props) { return errorStates } + +/** + * Browser navigate back. + */ +export const navigateBack = () => window.history.back() From 84bda39372f14be0c79c1243f235c286e5be49b2 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 16 Feb 2021 17:14:50 -0500 Subject: [PATCH 173/265] refactor(FavoritePlaceScreen): Add space before back/save buttons. --- .../user/places/favorite-place-screen.js | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/lib/components/user/places/favorite-place-screen.js b/lib/components/user/places/favorite-place-screen.js index bca4281bb..93ce9ebdd 100644 --- a/lib/components/user/places/favorite-place-screen.js +++ b/lib/components/user/places/favorite-place-screen.js @@ -3,6 +3,7 @@ import clone from 'clone' import { Form, Formik } from 'formik' import React, { Component } from 'react' import { connect } from 'react-redux' +import styled from 'styled-components' import * as yup from 'yup' import AccountPage from '../account-page' @@ -21,6 +22,11 @@ const BLANK_PLACE = { name: '' } +// Make space between place details and form buttons. +const Container = styled.div` + margin-bottom: 100px; +` + // The form fields to validate. const validationSchemaShape = { address: yup.string().required('Please set a location for this place'), @@ -116,27 +122,28 @@ class FavoritePlaceScreen extends Component {
    {heading}
    - - {place - ? - :

    Sorry, the requested place was not found.

    - } + + {place + ? + :

    Sorry, the requested place was not found.

    + } +
    + + ) } } - - ) } From 127e97d87b201d8e5d3080ad0488ac1a1da70889 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 16 Feb 2021 17:55:26 -0500 Subject: [PATCH 174/265] refactor(PlaceLocationField): Accommodate Back/Save buttons below the field. --- lib/components/user/places/place-editor.js | 5 ++++- lib/components/user/places/place-location-field.js | 5 +---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/components/user/places/place-editor.js b/lib/components/user/places/place-editor.js index 9329011f9..26122061c 100644 --- a/lib/components/user/places/place-editor.js +++ b/lib/components/user/places/place-editor.js @@ -113,11 +113,14 @@ class PlaceEditor extends Component { {errors.address && {errors.address}} diff --git a/lib/components/user/places/place-location-field.js b/lib/components/user/places/place-location-field.js index 31a75fdba..ebd0862d6 100644 --- a/lib/components/user/places/place-location-field.js +++ b/lib/components/user/places/place-location-field.js @@ -59,7 +59,7 @@ const StyledLocationField = styled(LocationField)` } ${MenuItemList} { position: absolute; - ${props => props.static ? 'width: 100%;' : ''} + ${props => props.isMobile ? 'left: -60px;' : ''} } ${MenuItemA} { &:focus, &:hover { @@ -67,9 +67,6 @@ const StyledLocationField = styled(LocationField)` text-decoration: none; } } - ${MenuItemA}, ${MenuItemHeader}, ${MenuItemLi} { - ${props => props.static ? 'padding-left: 0; padding-right: 0;' : ''} - } ` /** * Styled LocationField for setting a favorite place locations using the geocoder. From 9da1fecfd8e0a3124ec569f06f40f1afb12d73ed Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 16 Feb 2021 17:59:47 -0500 Subject: [PATCH 175/265] refactor(PlaceLocationField): Fix lint --- lib/components/user/places/place-location-field.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/components/user/places/place-location-field.js b/lib/components/user/places/place-location-field.js index ebd0862d6..f6e85e1d7 100644 --- a/lib/components/user/places/place-location-field.js +++ b/lib/components/user/places/place-location-field.js @@ -6,8 +6,6 @@ import { InputGroup, InputGroupAddon, MenuItemA, - MenuItemHeader, - MenuItemLi, MenuItemList } from '@opentripplanner/location-field/lib/styled' import styled from 'styled-components' From a083c8bc052be716ce8fd25f16b521a9180c1d23 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 17 Feb 2021 14:32:16 -0500 Subject: [PATCH 176/265] Revert "refactor(PlaceLocationField): Fix lint" This reverts commit 9da1fecfd8e0a3124ec569f06f40f1afb12d73ed. --- lib/components/user/places/place-location-field.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/components/user/places/place-location-field.js b/lib/components/user/places/place-location-field.js index f6e85e1d7..ebd0862d6 100644 --- a/lib/components/user/places/place-location-field.js +++ b/lib/components/user/places/place-location-field.js @@ -6,6 +6,8 @@ import { InputGroup, InputGroupAddon, MenuItemA, + MenuItemHeader, + MenuItemLi, MenuItemList } from '@opentripplanner/location-field/lib/styled' import styled from 'styled-components' From 7985f671fad9a45fabff94077c13612e3d123d95 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 17 Feb 2021 14:32:37 -0500 Subject: [PATCH 177/265] Revert "refactor(PlaceLocationField): Accommodate Back/Save buttons below the field." This reverts commit 127e97d87b201d8e5d3080ad0488ac1a1da70889. --- lib/components/user/places/place-editor.js | 5 +---- lib/components/user/places/place-location-field.js | 5 ++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/components/user/places/place-editor.js b/lib/components/user/places/place-editor.js index 26122061c..9329011f9 100644 --- a/lib/components/user/places/place-editor.js +++ b/lib/components/user/places/place-editor.js @@ -113,14 +113,11 @@ class PlaceEditor extends Component { {errors.address && {errors.address}} diff --git a/lib/components/user/places/place-location-field.js b/lib/components/user/places/place-location-field.js index ebd0862d6..31a75fdba 100644 --- a/lib/components/user/places/place-location-field.js +++ b/lib/components/user/places/place-location-field.js @@ -59,7 +59,7 @@ const StyledLocationField = styled(LocationField)` } ${MenuItemList} { position: absolute; - ${props => props.isMobile ? 'left: -60px;' : ''} + ${props => props.static ? 'width: 100%;' : ''} } ${MenuItemA} { &:focus, &:hover { @@ -67,6 +67,9 @@ const StyledLocationField = styled(LocationField)` text-decoration: none; } } + ${MenuItemA}, ${MenuItemHeader}, ${MenuItemLi} { + ${props => props.static ? 'padding-left: 0; padding-right: 0;' : ''} + } ` /** * Styled LocationField for setting a favorite place locations using the geocoder. From 326199f4e5bdd0e8a67538c01021973b558fc316 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 17 Feb 2021 15:01:13 -0500 Subject: [PATCH 178/265] refactor(FavoritePlaceScreen): Use FormNavigationButtons on desktop views, back link o/w. --- lib/components/user/back-link.js | 27 +++++++++++++++++++ .../user/places/favorite-place-screen.js | 16 +++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 lib/components/user/back-link.js diff --git a/lib/components/user/back-link.js b/lib/components/user/back-link.js new file mode 100644 index 000000000..7f2cc82e5 --- /dev/null +++ b/lib/components/user/back-link.js @@ -0,0 +1,27 @@ +import React from 'react' +import { Button } from 'react-bootstrap' +import styled from 'styled-components' + +import { IconWithMargin } from './styled' + +const StyledButton = styled(Button)` + display: block; + padding: 0; +` + +const navigateBack = () => window.history.back() + +/** + * Back link that navigates to the previous location in browser history. + */ +const BackLink = () => ( + + + Back + +) + +export default BackLink diff --git a/lib/components/user/places/favorite-place-screen.js b/lib/components/user/places/favorite-place-screen.js index 93ce9ebdd..c8ba6f4df 100644 --- a/lib/components/user/places/favorite-place-screen.js +++ b/lib/components/user/places/favorite-place-screen.js @@ -1,13 +1,16 @@ import { withAuthenticationRequired } from '@auth0/auth0-react' import clone from 'clone' import { Form, Formik } from 'formik' +import coreUtils from '@opentripplanner/core-utils' import React, { Component } from 'react' +import { Button } from 'react-bootstrap' import { connect } from 'react-redux' import styled from 'styled-components' import * as yup from 'yup' import AccountPage from '../account-page' import * as userActions from '../../../actions/user' +import BackLink from '../back-link' import FormNavigationButtons from '../form-navigation-buttons' import { PageHeading } from '../styled' import { CREATE_ACCOUNT_PLACES_PATH } from '../../../util/constants' @@ -16,6 +19,12 @@ import { isHomeOrWork, PLACE_TYPES } from '../../../util/user' import withLoggedInUserSupport from '../with-logged-in-user-support' import PlaceEditor from './place-editor' +const { isMobile } = coreUtils.ui + +// Styled components +const SaveButton = styled(Button)` + float: right; +` const BLANK_PLACE = { ...PLACE_TYPES.custom, address: '', @@ -91,6 +100,7 @@ class FavoritePlaceScreen extends Component { // (indexes 0 and 1 are for Home and Work locations). const place = this._getPlaceToEdit(loggedInUser) const isFixed = place && isHomeOrWork(place) + const isMobileView = isMobile() let heading if (!place) { @@ -105,6 +115,7 @@ class FavoritePlaceScreen extends Component { return ( + {isMobileView && }
    + {isMobileView && place && Save} {heading}
    @@ -129,7 +141,7 @@ class FavoritePlaceScreen extends Component { } - + />} ) } From f31f73ca685175c0a845616c2cd635c947bb3785 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 19 Feb 2021 11:46:01 -0500 Subject: [PATCH 179/265] fix(batch-routing): add mobile search screen for batch routing --- lib/components/app/batch-routing-panel.js | 166 +------------ lib/components/app/styled.js | 97 -------- ...settings-panel.js => batch-preferences.js} | 8 +- lib/components/form/batch-settings.js | 202 ++++++++++++++++ lib/components/form/batch-styled.js | 222 ++++++++++++++++++ lib/components/form/mode-buttons.js | 2 +- lib/components/form/settings-preview.js | 2 +- lib/components/form/styled.js | 142 +---------- lib/components/mobile/batch-search-screen.js | 71 ++++++ lib/components/mobile/main.js | 12 +- lib/components/mobile/mobile.css | 21 ++ 11 files changed, 546 insertions(+), 399 deletions(-) delete mode 100644 lib/components/app/styled.js rename lib/components/form/{batch-settings-panel.js => batch-preferences.js} (94%) create mode 100644 lib/components/form/batch-settings.js create mode 100644 lib/components/form/batch-styled.js create mode 100644 lib/components/mobile/batch-search-screen.js diff --git a/lib/components/app/batch-routing-panel.js b/lib/components/app/batch-routing-panel.js index 93d2e1975..c663d9965 100644 --- a/lib/components/app/batch-routing-panel.js +++ b/lib/components/app/batch-routing-panel.js @@ -5,77 +5,12 @@ import styled from 'styled-components' import * as apiActions from '../../actions/api' import * as formActions from '../../actions/form' -import BatchSettingsPanel from '../form/batch-settings-panel' +import BatchSettings from '../form/batch-settings' import LocationField from '../form/connected-location-field' -import DateTimeModal from '../form/date-time-modal' -import ModeButtons, {MODE_OPTIONS, StyledModeButton} from '../form/mode-buttons' -// import UserSettings from '../form/user-settings' -import Icon from '../narrative/icon' import NarrativeItineraries from '../narrative/narrative-itineraries' -import { - BatchSettingsPanelContainer, - DateTimeModalContainer, - Dot, - MainSettingsRow, - PlanTripButton, - SettingsPreview, - StyledDateTimePreview -} from './styled' -import { hasValidLocation, getActiveSearch, getShowUserSettings } from '../../util/state' +import { getActiveSearch, getShowUserSettings } from '../../util/state' import ViewerContainer from '../viewers/viewer-container' -/** - * Simple utility to check whether a list of mode strings contains the provided - * mode. This handles exact match and prefix/suffix matches (i.e., checking - * 'BICYCLE' will return true if 'BICYCLE' or 'BICYCLE_RENT' is in the list). - * - * FIXME: This might need to be modified to be a bit looser in how it handles - * the 'contains' check. E.g., we might not want to remove WALK,TRANSIT if walk - * is turned off, but we DO want to remove it if TRANSIT is turned off. - */ -function listHasMode (modes, mode) { - return modes.some(m => mode.indexOf(m) !== -1) -} - -function combinationHasAnyOfModes (combination, modes) { - return combination.mode.split(',').some(m => listHasMode(modes, m)) -} - -// List of possible modes that can be selected via mode buttons. -const POSSIBLE_MODES = MODE_OPTIONS.map(b => b.mode) - -const ModeButtonsFullWidthContainer = styled.div` - display: flex; - justify-content: space-between; - margin-bottom: 5px; -` - -// Define Mode Button styled components here to avoid circular imports. I.e., we -// cannot define them in styled.js (because mode-buttons.js imports buttonCss -// and then we would need to import ModeButtons/StyledModeButton from that file -// in turn). -const StyledModeButtonsFullWidth = styled(ModeButtons)` - &:last-child { - margin-right: 0px; - } -` - -const ModeButtonsContainerCompressed = styled.div` - display: contents; -` - -const ModeButtonsCompressed = styled(ModeButtons)` - ${StyledModeButton} { - border-radius: 0px; - } - &:first-child { - border-radius: 5px 0px 0px 5px; - } - &:last-child { - margin-right: 5px; - border-radius: 0px 5px 5px 0px; - } -` // Style for setting the top of the narrative itineraries based on the width of the window. // If the window width is less than 1200px (Bootstrap's "large" size), the // mode buttons will be shown on their own row, meaning that the @@ -97,56 +32,8 @@ const NarrativeContainer = styled.div` * Main panel for the batch/trip comparison form. */ class BatchRoutingPanel extends Component { - state = { - expanded: null, - selectedModes: POSSIBLE_MODES - } - - _onClickMode = (mode) => { - const {possibleCombinations, setQueryParam} = this.props - const {selectedModes} = this.state - const index = selectedModes.indexOf(mode) - const enableMode = index === -1 - const newModes = [...selectedModes] - if (enableMode) newModes.push(mode) - else newModes.splice(index, 1) - // Update selected modes for mode buttons. - this.setState({selectedModes: newModes}) - // Update the available mode combinations based on the new modes selection. - const disabledModes = POSSIBLE_MODES.filter(m => !newModes.includes(m)) - // Do not include combination if any of its modes are found in disabled - // modes list. - const newCombinations = possibleCombinations - .filter(c => !combinationHasAnyOfModes(c, disabledModes)) - setQueryParam({combinations: newCombinations}) - } - - _planTrip = () => { - const {currentQuery, routingQuery} = this.props - // Check for any validation issues in query. - const issues = [] - if (!hasValidLocation(currentQuery, 'from')) issues.push('from') - if (!hasValidLocation(currentQuery, 'to')) issues.push('to') - if (issues.length > 0) { - // TODO: replace with less obtrusive validation. - window.alert(`Please define the following fields to plan a trip: ${issues.join(', ')}`) - return - } - // Close any expanded panels. - this.setState({expanded: null}) - // Plan trip. - routingQuery() - } - - _updateExpanded = (type) => ({expanded: this.state.expanded === type ? null : type}) - - _toggleDateTime = () => this.setState(this._updateExpanded('DATE_TIME')) - - _toggleSettings = () => this.setState(this._updateExpanded('SETTINGS')) - render () { - const {config, currentQuery, mobile} = this.props - const {expanded, selectedModes} = this.state + const {mobile} = this.props const actionText = mobile ? 'tap' : 'click' return ( @@ -160,52 +47,7 @@ class BatchRoutingPanel extends Component { locationType='to' showClearButton={!mobile} /> - - - - - - {coreUtils.query.isNotDefaultQuery(currentQuery, config) && - - } - - - - - - - - - - - {expanded === 'DATE_TIME' && - - - - } - {expanded === 'SETTINGS' && - - - - } + {/* FIXME: Add back user settings (home, work, etc.) once connected to the middleware persistence. !activeSearch && showUserSettings && diff --git a/lib/components/app/styled.js b/lib/components/app/styled.js deleted file mode 100644 index 71b92d005..000000000 --- a/lib/components/app/styled.js +++ /dev/null @@ -1,97 +0,0 @@ -import styled, {css} from 'styled-components' - -import DateTimePreview from '../form/date-time-preview' - -const SHADOW = 'inset 0px 0px 5px #c1c1c1' - -const activeCss = css` - background: #e5e5e5; - -webkit-box-shadow: ${SHADOW}; - -moz-box-shadow: ${SHADOW}; - box-shadow: ${SHADOW}; - outline: none; -` - -export const buttonCss = css` - height: 45px; - width: 45px; - margin: 0px; - border: 0px; - border-radius: 5px; - &:active { - ${activeCss} - } -` - -export const Button = styled.button` - ${buttonCss} -` - -export const StyledDateTimePreview = styled(DateTimePreview)` - ${buttonCss} - background-color: rgb(239, 239, 239); - cursor: pointer; - font-size: 12px; - margin-right: 5px; - padding: 7px 5px; - text-align: left; - white-space: nowrap; - width: 120px; - ${props => props.expanded ? activeCss : null} -` -export const SettingsPreview = styled(Button)` - line-height: 22px; - margin-right: 5px; - padding: 10px 0px; - position: relative; - ${props => props.expanded ? activeCss : null} -` - -export const Dot = styled.div` - position: absolute; - top: -3px; - right: -3px; - width: 10px; - height: 10px; - border-radius: 5px; - background-color: #f00; -` - -export const PlanTripButton = styled(Button)` - background-color: #F5F5A7; - margin-left: auto; - padding: 5px; - &:active { - ${activeCss} - background-color: #ededaf - } -` - -const expandableBoxCss = css` - background-color: rgb(239, 239, 239); - box-shadow: rgba(0, 0, 0, 0.32) 7px 12px 10px; - height: 245px; - border-radius: 5px 5px 5px 5px; - left: 10px; - position: absolute; - right: 10px; - z-index: 99999; -` - -export const DateTimeModalContainer = styled.div` - ${expandableBoxCss} - padding: 10px 20px; -` - -export const BatchSettingsPanelContainer = styled.div` - ${expandableBoxCss} - padding: 5px 10px; -` - -export const MainSettingsRow = styled.div` - align-items: top; - display: flex; - flex-direction: row; - justify-content: flex-start; - margin-bottom: 5px; -` diff --git a/lib/components/form/batch-settings-panel.js b/lib/components/form/batch-preferences.js similarity index 94% rename from lib/components/form/batch-settings-panel.js rename to lib/components/form/batch-preferences.js index 4e5dbb0e0..29718eac2 100644 --- a/lib/components/form/batch-settings-panel.js +++ b/lib/components/form/batch-preferences.js @@ -6,10 +6,10 @@ import { setQueryParam } from '../../actions/form' import { ComponentContext } from '../../util/contexts' import { getShowUserSettings } from '../../util/state' -import { StyledBatchSettingsPanel } from './styled' +import { StyledBatchPreferences } from './batch-styled' import UserTripSettings from './user-trip-settings' -class BatchSettingsPanel extends Component { +class BatchPreferences extends Component { static contextType = ComponentContext render () { @@ -26,7 +26,7 @@ class BatchSettingsPanel extends Component {
    {showUserSettings && } - mode.indexOf(m) !== -1) +} + +function combinationHasAnyOfModes (combination, modes) { + return combination.mode.split(',').some(m => listHasMode(modes, m)) +} + +// List of possible modes that can be selected via mode buttons. +const POSSIBLE_MODES = MODE_OPTIONS.map(b => b.mode) + +const ModeButtonsFullWidthContainer = styled.div` + display: flex; + justify-content: space-between; + margin-bottom: 5px; +` + +// Define Mode Button styled components here to avoid circular imports. I.e., we +// cannot define them in styled.js (because mode-buttons.js imports buttonCss +// and then we would need to import ModeButtons/StyledModeButton from that file +// in turn). +const StyledModeButtonsFullWidth = styled(ModeButtons)` + &:last-child { + margin-right: 0px; + } +` + +const ModeButtonsContainerCompressed = styled.div` + display: contents; +` + +const ModeButtonsCompressed = styled(ModeButtons)` + ${StyledModeButton} { + border-radius: 0px; + } + &:first-child { + border-radius: 5px 0px 0px 5px; + } + &:last-child { + margin-right: 5px; + border-radius: 0px 5px 5px 0px; + } +` + +/** + * Main panel for the batch/trip comparison form. + */ +class BatchSettings extends Component { + state = { + expanded: null, + selectedModes: POSSIBLE_MODES + } + + _onClickMode = (mode) => { + const {possibleCombinations, setQueryParam} = this.props + const {selectedModes} = this.state + const index = selectedModes.indexOf(mode) + const enableMode = index === -1 + const newModes = [...selectedModes] + if (enableMode) newModes.push(mode) + else newModes.splice(index, 1) + // Update selected modes for mode buttons. + this.setState({selectedModes: newModes}) + // Update the available mode combinations based on the new modes selection. + const disabledModes = POSSIBLE_MODES.filter(m => !newModes.includes(m)) + // Do not include combination if any of its modes are found in disabled + // modes list. + const newCombinations = possibleCombinations + .filter(c => !combinationHasAnyOfModes(c, disabledModes)) + setQueryParam({combinations: newCombinations}) + } + + _planTrip = () => { + const {currentQuery, routingQuery} = this.props + // Check for any validation issues in query. + const issues = [] + if (!hasValidLocation(currentQuery, 'from')) issues.push('from') + if (!hasValidLocation(currentQuery, 'to')) issues.push('to') + if (issues.length > 0) { + // TODO: replace with less obtrusive validation. + window.alert(`Please define the following fields to plan a trip: ${issues.join(', ')}`) + return + } + // Close any expanded panels. + this.setState({expanded: null}) + // Plan trip. + routingQuery() + } + + _updateExpanded = (type) => ({expanded: this.state.expanded === type ? null : type}) + + _toggleDateTime = () => this.setState(this._updateExpanded('DATE_TIME')) + + _toggleSettings = () => this.setState(this._updateExpanded('SETTINGS')) + + render () { + const {config, currentQuery} = this.props + const {expanded, selectedModes} = this.state + return ( + <> + + + + + + {coreUtils.query.isNotDefaultQuery(currentQuery, config) && + + } + + + + + + + + + + + {expanded === 'DATE_TIME' && + + + + } + {expanded === 'SETTINGS' && + + + + } + + ) + } +} + +// connect to the redux store +const mapStateToProps = (state, ownProps) => { + const showUserSettings = getShowUserSettings(state.otp) + return { + activeSearch: getActiveSearch(state.otp), + config: state.otp.config, + currentQuery: state.otp.currentQuery, + expandAdvanced: state.otp.user.expandAdvanced, + possibleCombinations: state.otp.config.modes.combinations, + showUserSettings + } +} + +const mapDispatchToProps = { + routingQuery: apiActions.routingQuery, + setQueryParam: formActions.setQueryParam +} + +export default connect(mapStateToProps, mapDispatchToProps)(BatchSettings) diff --git a/lib/components/form/batch-styled.js b/lib/components/form/batch-styled.js new file mode 100644 index 000000000..7bd3fbb38 --- /dev/null +++ b/lib/components/form/batch-styled.js @@ -0,0 +1,222 @@ +import * as TripFormClasses from '@opentripplanner/trip-form/lib/styled' +import { SettingsSelectorPanel } from '@opentripplanner/trip-form' +import styled, {css} from 'styled-components' + +import DateTimePreview from './date-time-preview' +import {commonInputCss, modeButtonButtonCss} from './styled' + +const SHADOW = 'inset 0px 0px 5px #c1c1c1' + +const activeCss = css` + background: #e5e5e5; + -webkit-box-shadow: ${SHADOW}; + -moz-box-shadow: ${SHADOW}; + box-shadow: ${SHADOW}; + outline: none; +` + +export const buttonCss = css` + height: 45px; + width: 45px; + margin: 0px; + border: 0px; + border-radius: 5px; + &:active { + ${activeCss} + } +` + +export const Button = styled.button` + ${buttonCss} +` + +export const StyledDateTimePreview = styled(DateTimePreview)` + ${buttonCss} + background-color: rgb(239, 239, 239); + cursor: pointer; + font-size: 12px; + margin-right: 5px; + padding: 7px 5px; + text-align: left; + white-space: nowrap; + width: 120px; + ${props => props.expanded ? activeCss : null} +` +export const SettingsPreview = styled(Button)` + line-height: 22px; + margin-right: 5px; + padding: 10px 0px; + position: relative; + ${props => props.expanded ? activeCss : null} +` + +export const PlanTripButton = styled(Button)` + background-color: #F5F5A7; + margin-left: auto; + padding: 5px; + &:active { + ${activeCss} + background-color: #ededaf + } +` + +const expandableBoxCss = css` + background-color: rgb(239, 239, 239); + box-shadow: rgba(0, 0, 0, 0.32) 7px 12px 10px; + height: 245px; + border-radius: 5px 5px 5px 5px; + left: 10px; + position: absolute; + right: 10px; + z-index: 99999; +` + +export const DateTimeModalContainer = styled.div` + ${expandableBoxCss} + padding: 10px 20px; +` + +export const BatchPreferencesContainer = styled.div` + ${expandableBoxCss} + padding: 5px 10px; +` + +export const MainSettingsRow = styled.div` + align-items: top; + display: flex; + flex-direction: row; + justify-content: flex-start; + margin-bottom: 5px; +` + +// FIXME: This is identical to StyledSettingsSelectorPanel, with a +// couple of items set to display: none (SettingsHeader and ModeSelector) +export const StyledBatchPreferences = styled(SettingsSelectorPanel)` + ${modeButtonButtonCss} + + ${TripFormClasses.SettingLabel} { + color: #808080; + font-size: 14px; + font-weight: 100; + letter-spacing: 1px; + padding-top: 8px; + text-transform: uppercase; + } + ${TripFormClasses.SettingsHeader} { + display: none; + color: #333333; + font-size: 18px; + margin: 16px 0px; + } + ${TripFormClasses.SettingsSection} { + margin-bottom: 16px; + } + ${TripFormClasses.DropdownSelector} { + select { + ${commonInputCss} + -webkit-appearance: none; + border-radius: 3px; + font-size: 14px; + height: 34px; + line-height: 1.42857; + margin-bottom: 20px; + + &:focus { + border-color: #66afe9; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); + outline: 0; + } + } + > div:last-child::after { + box-sizing: border-box; + color: #000; + content: "▼"; + font-size: 67%; + pointer-events: none; + position: absolute; + right: 8px; + top: 10px; + } + } + + ${TripFormClasses.ModeSelector} { + display: none; + font-weight: 300; + ${TripFormClasses.ModeButton.Button} { + box-shadow: none; + outline: none; + padding: 3px; + } + ${TripFormClasses.ModeButton.Title} { + font-size: 10px; + line-height: 12px; + padding: 4px 0px 0px; + + &.active { + font-weight: 600; + } + } + } + ${TripFormClasses.ModeSelector.MainRow} { + box-sizing: border-box; + font-size: 170%; + margin: 0px -10px 18px; + padding: 0px 5px; + ${TripFormClasses.ModeButton.Button} { + height: 54px; + width: 100%; + &.active { + font-weight: 600; + } + } + } + ${TripFormClasses.ModeSelector.SecondaryRow} { + margin: 0px -10px 10px; + ${TripFormClasses.ModeButton.Button} { + font-size: 130%; + font-weight: 800; + height: 46px; + > svg { + margin: 0 0.20em; + } + } + } + ${TripFormClasses.ModeSelector.TertiaryRow} { + font-size: 80%; + font-weight: 300; + margin: 0px -10px 10px; + text-align: center; + ${TripFormClasses.ModeButton.Button} { + height: 36px; + } + } + ${TripFormClasses.SubmodeSelector.Row} { + font-size: 12px; + > * { + padding: 3px 5px 3px 0px; + } + > :last-child { + padding-right: 0px; + } + ${TripFormClasses.ModeButton.Button} { + height: 35px; + } + svg, + img { + margin-left: 0px; + } + } + ${TripFormClasses.SubmodeSelector} { + ${TripFormClasses.SettingLabel} { + margin-bottom: 0; + } + } + ${TripFormClasses.SubmodeSelector.InlineRow} { + margin: -3px 0px; + svg, + img { + height: 18px; + max-width: 32px; + } + } +` diff --git a/lib/components/form/mode-buttons.js b/lib/components/form/mode-buttons.js index 735573c57..5dd575aad 100644 --- a/lib/components/form/mode-buttons.js +++ b/lib/components/form/mode-buttons.js @@ -3,7 +3,7 @@ import styled from 'styled-components' import Icon from '../narrative/icon' import { ComponentContext } from '../../util/contexts' -import {buttonCss} from '../app/styled' +import {buttonCss} from './batch-styled' export const MODE_OPTIONS = [ { diff --git a/lib/components/form/settings-preview.js b/lib/components/form/settings-preview.js index 212631025..8e8d26ba5 100644 --- a/lib/components/form/settings-preview.js +++ b/lib/components/form/settings-preview.js @@ -4,7 +4,7 @@ import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import { Dot } from '../app/styled' +import { Dot } from './styled' import { mergeMessages } from '../../util/messages' class SettingsPreview extends Component { diff --git a/lib/components/form/styled.js b/lib/components/form/styled.js index c7ccb2e80..9c2bede6c 100644 --- a/lib/components/form/styled.js +++ b/lib/components/form/styled.js @@ -26,7 +26,7 @@ const commonButtonCss = css` } ` -const commonInputCss = css` +export const commonInputCss = css` background: none; border: 1px solid #ccc; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); @@ -43,137 +43,17 @@ export const modeButtonButtonCss = css` } ` -export const StyledSettingsSelectorPanel = styled(SettingsSelectorPanel)` - ${modeButtonButtonCss} - - ${TripFormClasses.SettingLabel} { - color: #808080; - font-size: 14px; - font-weight: 100; - letter-spacing: 1px; - padding-top: 8px; - text-transform: uppercase; - } - ${TripFormClasses.SettingsHeader} { - color: #333333; - font-size: 18px; - margin: 16px 0px; - } - ${TripFormClasses.SettingsSection} { - margin-bottom: 16px; - } - ${TripFormClasses.DropdownSelector} { - select { - ${commonInputCss} - -webkit-appearance: none; - border-radius: 3px; - font-size: 14px; - height: 34px; - line-height: 1.42857; - margin-bottom: 20px; - - &:focus { - border-color: #66afe9; - box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); - outline: 0; - } - } - > div:last-child::after { - box-sizing: border-box; - color: #000; - content: "▼"; - font-size: 67%; - pointer-events: none; - position: absolute; - right: 8px; - top: 10px; - } - } - - ${TripFormClasses.ModeSelector} { - font-weight: 300; - ${TripFormClasses.ModeButton.Button} { - box-shadow: none; - outline: none; - padding: 3px; - } - ${TripFormClasses.ModeButton.Title} { - font-size: 10px; - line-height: 12px; - padding: 4px 0px 0px; - - &.active { - font-weight: 600; - } - } - } - ${TripFormClasses.ModeSelector.MainRow} { - box-sizing: border-box; - font-size: 170%; - margin: 0px -10px 18px; - padding: 0px 5px; - ${TripFormClasses.ModeButton.Button} { - height: 54px; - width: 100%; - &.active { - font-weight: 600; - } - } - } - ${TripFormClasses.ModeSelector.SecondaryRow} { - margin: 0px -10px 10px; - ${TripFormClasses.ModeButton.Button} { - font-size: 130%; - font-weight: 800; - height: 46px; - > svg { - margin: 0 0.20em; - } - } - } - ${TripFormClasses.ModeSelector.TertiaryRow} { - font-size: 80%; - font-weight: 300; - margin: 0px -10px 10px; - text-align: center; - ${TripFormClasses.ModeButton.Button} { - height: 36px; - } - } - ${TripFormClasses.SubmodeSelector.Row} { - font-size: 12px; - > * { - padding: 3px 5px 3px 0px; - } - > :last-child { - padding-right: 0px; - } - ${TripFormClasses.ModeButton.Button} { - height: 35px; - } - svg, - img { - margin-left: 0px; - } - } - ${TripFormClasses.SubmodeSelector} { - ${TripFormClasses.SettingLabel} { - margin-bottom: 0; - } - } - ${TripFormClasses.SubmodeSelector.InlineRow} { - margin: -3px 0px; - svg, - img { - height: 18px; - max-width: 32px; - } - } +export const Dot = styled.div` + position: absolute; + top: -3px; + right: -3px; + width: 10px; + height: 10px; + border-radius: 5px; + background-color: #f00; ` -// FIXME: This is identical to StyledSettingsSelectorPanel, with a -// couple of items set to display: none (SettingsHeader and ModeSelector) -export const StyledBatchSettingsPanel = styled(SettingsSelectorPanel)` +export const StyledSettingsSelectorPanel = styled(SettingsSelectorPanel)` ${modeButtonButtonCss} ${TripFormClasses.SettingLabel} { @@ -185,7 +65,6 @@ export const StyledBatchSettingsPanel = styled(SettingsSelectorPanel)` text-transform: uppercase; } ${TripFormClasses.SettingsHeader} { - display: none; color: #333333; font-size: 18px; margin: 16px 0px; @@ -222,7 +101,6 @@ export const StyledBatchSettingsPanel = styled(SettingsSelectorPanel)` } ${TripFormClasses.ModeSelector} { - display: none; font-weight: 300; ${TripFormClasses.ModeButton.Button} { box-shadow: none; diff --git a/lib/components/mobile/batch-search-screen.js b/lib/components/mobile/batch-search-screen.js new file mode 100644 index 000000000..e6582fa96 --- /dev/null +++ b/lib/components/mobile/batch-search-screen.js @@ -0,0 +1,71 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' + +import BatchSettings from '../form/batch-settings' +import DefaultMap from '../map/default-map' +import LocationField from '../form/connected-location-field' +import SwitchButton from '../form/switch-button' + +import MobileContainer from './container' +import MobileNavigationBar from './navigation-bar' + +import { MobileScreens, setMobileScreen } from '../../actions/ui' + +const { + SET_DATETIME, + SET_FROM_LOCATION, + SET_TO_LOCATION +} = MobileScreens + +class BatchSearchScreen extends Component { + static propTypes = { + map: PropTypes.element, + setMobileScreen: PropTypes.func + } + + _fromFieldClicked = () => this.props.setMobileScreen(SET_FROM_LOCATION) + + _toFieldClicked = () => this.props.setMobileScreen(SET_TO_LOCATION) + + _expandDateTimeClicked = () => this.props.setMobileScreen(SET_DATETIME) + + render () { + return ( + + +
    + + +
    + } /> +
    + +
    +
    + +
    +
    + ) + } +} + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + return { } +} + +const mapDispatchToProps = { + setMobileScreen +} + +export default connect(mapStateToProps, mapDispatchToProps)(BatchSearchScreen) diff --git a/lib/components/mobile/main.js b/lib/components/mobile/main.js index a7ae607d2..03463e90b 100644 --- a/lib/components/mobile/main.js +++ b/lib/components/mobile/main.js @@ -2,6 +2,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' +import BatchSearchScreen from './batch-search-screen' import MobileDateTimeScreen from './date-time-screen' import MobileOptionsScreen from './options-screen' import MobileLocationSearch from './location-search' @@ -13,6 +14,7 @@ import MobileTripViewer from './trip-viewer' import MobileRouteViewer from './route-viewer' import { MobileScreens, MainPanelContent, setMobileScreen } from '../../actions/ui' +import { isBatchRoutingEnabled } from '../../util/itinerary' import { getActiveItinerary } from '../../util/state' class MobileMain extends Component { @@ -43,7 +45,7 @@ class MobileMain extends Component { } render () { - const { map, title, uiState } = this.props + const { config, map, title, uiState } = this.props // check for route viewer if (uiState.mainPanelContent === MainPanelContent.ROUTE_VIEWER) { @@ -69,8 +71,13 @@ class MobileMain extends Component { ) case MobileScreens.SEARCH_FORM: + // Render batch search screen if batch routing enabled. Otherwise, + // default to standard search screen. + const SearchScreen = isBatchRoutingEnabled(config) + ? BatchSearchScreen + : MobileSearchScreen return ( - @@ -112,6 +119,7 @@ class MobileMain extends Component { const mapStateToProps = (state, ownProps) => { return { + config: state.otp.config, uiState: state.otp.ui, currentQuery: state.otp.currentQuery, currentPosition: state.otp.location.currentPosition, diff --git a/lib/components/mobile/mobile.css b/lib/components/mobile/mobile.css index 679e44a63..1e6475c6e 100644 --- a/lib/components/mobile/mobile.css +++ b/lib/components/mobile/mobile.css @@ -96,6 +96,7 @@ left: 0; right: 0; height: 250px; + box-shadow: 3px 0px 12px #00000052; } .otp.mobile .search-map { @@ -106,6 +107,26 @@ bottom: 0; } +/* Batch routing search screen */ + +.otp.mobile .batch-search-settings { + position: fixed; + top: 50px; + left: 0; + right: 0; + height: 216px; + z-index: 99999999; + box-shadow: 3px 0px 12px #00000052; +} + +.otp.mobile .batch-search-map { + position: fixed; + top: 266px; + left: 0; + right: 0; + bottom: 0; +} + /* Detailed options screen */ .otp.mobile .options-main-content { From 0252d7035aae0752c076c4abce3f9a55e462f28a Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 22 Feb 2021 09:44:20 -0500 Subject: [PATCH 180/265] refactor(batch-routing): remove redundant mode filter dropdown --- lib/components/narrative/narrative-itineraries.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/components/narrative/narrative-itineraries.js b/lib/components/narrative/narrative-itineraries.js index eb8585e9a..a96ba5a3d 100644 --- a/lib/components/narrative/narrative-itineraries.js +++ b/lib/components/narrative/narrative-itineraries.js @@ -152,17 +152,6 @@ class NarrativeItineraries extends Component { }}> {resultText}
    - { // FIXME: Enable only when ITINERARY/BATCH routing type enabled. - - }
    + + ) } From 2aa70565baac1fdca833965b0e315c3758316500 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 23 Feb 2021 15:53:31 -0500 Subject: [PATCH 191/265] Update lib/components/form/mode-buttons.js Co-authored-by: Landon Reed --- lib/components/form/mode-buttons.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/form/mode-buttons.js b/lib/components/form/mode-buttons.js index 688730e40..0a15aa020 100644 --- a/lib/components/form/mode-buttons.js +++ b/lib/components/form/mode-buttons.js @@ -59,7 +59,7 @@ const ModeButton = ({className, item, onClick, selected}) => { return ( {label}} - placement='top' + placement='bottom' > +
    +
    + ) + } - /* Old navbar alert - if (showRealtimeAnnotation) { - headerAction = ( - - ) - */ + renderLocationsSummary = () => { + const { query } = this.props - const locationsSummary = ( + return ( @@ -156,29 +161,34 @@ class MobileResultsScreen extends Component { ) + } - if (error) { - return ( - - - {locationsSummary} -
    -
    - -
    - -
    -
    -
    - ) - } + render () { + const { + activeItineraryIndex, + error, + realtimeEffects, + resultCount, + useRealtime + } = this.props + const { expanded } = this.state - // Construct the 'dots' - const dots = [] - for (let i = 0; i < resultCount; i++) { - dots.push(
    ) + const narrativeContainerStyle = expanded + ? { top: 140, overflowY: 'auto' } + : { height: 80, overflowY: 'hidden' } + + // Ensure that narrative covers map. + narrativeContainerStyle.backgroundColor = 'white' + + let headerAction = null + const showRealtimeAnnotation = realtimeEffects.isAffectedByRealtimeData && ( + realtimeEffects.exceedsThreshold || + realtimeEffects.routesDiffer || + !useRealtime + ) + + if (error) { + return this.renderError() } return ( @@ -190,10 +200,10 @@ class MobileResultsScreen extends Component { } headerAction={headerAction} /> - {locationsSummary} + {this.renderLocationsSummary()}
    - {this.props.map} +
    - -
    {dots}
    + {this.renderDots()} ) } diff --git a/lib/components/mobile/welcome-screen.js b/lib/components/mobile/welcome-screen.js index d31712c05..31ced700b 100644 --- a/lib/components/mobile/welcome-screen.js +++ b/lib/components/mobile/welcome-screen.js @@ -12,8 +12,6 @@ import { setLocationToCurrent } from '../../actions/map' class MobileWelcomeScreen extends Component { static propTypes = { - map: PropTypes.element, - setLocationToCurrent: PropTypes.func, setMobileScreen: PropTypes.func } @@ -36,10 +34,9 @@ class MobileWelcomeScreen extends Component { } render () { - const { title } = this.props return ( - +
    ({ ...l, type: 'suggested' }))) @@ -243,8 +243,8 @@ export function getInitialState (userDefinedConfig, initialQuery) { } } -function createOtpReducer (config, initialQuery) { - const initialState = getInitialState(config, initialQuery) +function createOtpReducer (config) { + const initialState = getInitialState(config) // validate the initial state validateInitialState(initialState) From 2dd18214428e945fb4ea19511ab205f00233acf7 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 1 Mar 2021 16:44:30 -0500 Subject: [PATCH 193/265] refactor(TripNotificationPane): Use delayThreshold to manage departure and arrival delay thres. --- .../user/monitored-trip/saved-trip-screen.js | 25 ++++++++++++++++--- .../monitored-trip/trip-notifications-pane.js | 7 ++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/lib/components/user/monitored-trip/saved-trip-screen.js b/lib/components/user/monitored-trip/saved-trip-screen.js index eac8aad36..35f098fc7 100644 --- a/lib/components/user/monitored-trip/saved-trip-screen.js +++ b/lib/components/user/monitored-trip/saved-trip-screen.js @@ -74,14 +74,20 @@ class SavedTripScreen extends Component { } } /** - * Persists changes to the edited trip. + * Persists changes to the edited trip. This includes removing the + * delayThreshold field that was defined solely for UI editing, and passing its value + * to both 'arrivalVarianceMinutesThreshold' and 'departureVarianceMinutesThreshold'. * On success, this operation will also make the browser * navigate to the Saved trips page. * @param {*} monitoredTrip The trip edited state to be saved, provided by Formik. */ _updateMonitoredTrip = monitoredTrip => { - const { isCreating, createOrUpdateUserMonitoredTrip } = this.props - createOrUpdateUserMonitoredTrip(monitoredTrip, isCreating) + const { createOrUpdateUserMonitoredTrip, isCreating } = this.props + const { delayThreshold, ...tripToPersist } = monitoredTrip + tripToPersist.arrivalVarianceMinutesThreshold = delayThreshold + tripToPersist.departureVarianceMinutesThreshold = delayThreshold + + createOrUpdateUserMonitoredTrip(tripToPersist, isCreating) } /** @@ -144,11 +150,22 @@ class SavedTripScreen extends Component { .notOneOf(otherTripNames, 'Another saved trip already uses this name. Please choose a different name.') const validationSchema = yup.object(clonedSchemaShape) + // Create a modified copy of monitoredTrip for editing. + const clonedMonitoredTrip = clone(monitoredTrip) + + // To spare users the complexity of the departure/arrival delay thresholds, + // we create an overall delayThreshold field for UI editing, initialized as the smallest + // between 'arrivalVarianceMinutesThreshold' and 'departureVarianceMinutesThreshold'. + clonedMonitoredTrip.delayThreshold = Math.min( + monitoredTrip.arrivalVarianceMinutesThreshold, + monitoredTrip.departureVarianceMinutesThreshold + ) + screenContents = (

    Notify me via {notificationChannelLabels[notificationChannel]} when:

    @@ -131,7 +128,7 @@ class TripNotificationsPane extends Component {
  • - before it begins, until it ends. + before it begins until it ends.
  • )} From 2487512b5fcab0e8b23e8f2d2b80065db3a2d3cf Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 4 Mar 2021 12:27:58 -0500 Subject: [PATCH 197/265] refactor(TripNotificationsPane): Move common delayThreshold logic to trip notif. pane. --- .../user/monitored-trip/saved-trip-screen.js | 22 +++-------------- .../monitored-trip/trip-notifications-pane.js | 24 +++++++++++++++---- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/lib/components/user/monitored-trip/saved-trip-screen.js b/lib/components/user/monitored-trip/saved-trip-screen.js index a6498ddc4..4973f421b 100644 --- a/lib/components/user/monitored-trip/saved-trip-screen.js +++ b/lib/components/user/monitored-trip/saved-trip-screen.js @@ -76,20 +76,14 @@ class SavedTripScreen extends Component { } } /** - * Persists changes to the edited trip. This includes removing the - * delayThreshold field that was defined solely for UI editing, and passing its value - * to both 'arrivalVarianceMinutesThreshold' and 'departureVarianceMinutesThreshold'. + * Persists changes to the edited trip. * On success, this operation will also make the browser * navigate to the Saved trips page. * @param {*} monitoredTrip The trip edited state to be saved, provided by Formik. */ _updateMonitoredTrip = monitoredTrip => { const { createOrUpdateUserMonitoredTrip, isCreating } = this.props - const { delayThreshold, ...tripToPersist } = monitoredTrip - tripToPersist.arrivalVarianceMinutesThreshold = delayThreshold - tripToPersist.departureVarianceMinutesThreshold = delayThreshold - - createOrUpdateUserMonitoredTrip(tripToPersist, isCreating) + createOrUpdateUserMonitoredTrip(monitoredTrip, isCreating) } /** @@ -128,19 +122,9 @@ class SavedTripScreen extends Component { */ _getTripToEdit = () => { const { isCreating, monitoredTrips, tripId } = this.props - const tripToEdit = clone(isCreating + return isCreating ? this._createMonitoredTrip() : monitoredTrips.find(trip => trip.id === tripId) - ) - - // To spare users the complexity of the departure/arrival delay thresholds, - // we add an overall delayThreshold field defined as the smallest - // between 'arrivalVarianceMinutesThreshold' and 'departureVarianceMinutesThreshold'. - tripToEdit.delayThreshold = Math.min( - tripToEdit.arrivalVarianceMinutesThreshold, - tripToEdit.departureVarianceMinutesThreshold - ) - return tripToEdit } render () { diff --git a/lib/components/user/monitored-trip/trip-notifications-pane.js b/lib/components/user/monitored-trip/trip-notifications-pane.js index c93ac310b..b3c818edc 100644 --- a/lib/components/user/monitored-trip/trip-notifications-pane.js +++ b/lib/components/user/monitored-trip/trip-notifications-pane.js @@ -84,9 +84,20 @@ class TripNotificationsPane extends Component { this.setState({ showAdvancedSettings: !this.state.showAdvancedSettings }) } + _handleDelayThresholdChange = e => { + const { setFieldValue } = this.props + const threshold = e.target.value + setFieldValue('arrivalVarianceMinutesThreshold', threshold) + setFieldValue('departureVarianceMinutesThreshold', threshold) + } + render () { - const { notificationChannel } = this.props + const { notificationChannel, values } = this.props const areNotificationsDisabled = notificationChannel === 'none' + const delayThreshold = Math.min( + values.arrivalVarianceMinutesThreshold, + values.departureVarianceMinutesThreshold + ) let notificationSettingsContent if (areNotificationsDisabled) { @@ -126,14 +137,17 @@ class TripNotificationsPane extends Component {
  • - +
  • From ec90a40654f77b92877e010ee0adf79730443dc3 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 4 Mar 2021 12:37:29 -0500 Subject: [PATCH 198/265] refactor(TripNotificationPane): Add comments, rename vars. --- .../user/monitored-trip/trip-notifications-pane.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/components/user/monitored-trip/trip-notifications-pane.js b/lib/components/user/monitored-trip/trip-notifications-pane.js index b3c818edc..302e928d4 100644 --- a/lib/components/user/monitored-trip/trip-notifications-pane.js +++ b/lib/components/user/monitored-trip/trip-notifications-pane.js @@ -85,6 +85,8 @@ class TripNotificationsPane extends Component { } _handleDelayThresholdChange = e => { + // To spare users the complexity of the departure/arrival delay thresholds, + // set both the arrival and departure variance delays to the selected value. const { setFieldValue } = this.props const threshold = e.target.value setFieldValue('arrivalVarianceMinutesThreshold', threshold) @@ -94,7 +96,9 @@ class TripNotificationsPane extends Component { render () { const { notificationChannel, values } = this.props const areNotificationsDisabled = notificationChannel === 'none' - const delayThreshold = Math.min( + // Define a common trip delay field for simplicity, set to the smallest between the + // retrieved departure/arrival delay attributes. + const commonDelayThreshold = Math.min( values.arrivalVarianceMinutesThreshold, values.departureVarianceMinutesThreshold ) @@ -137,12 +141,12 @@ class TripNotificationsPane extends Component {
  • - + From d79da54c3b6fcef16d0c2f301d185ad08dc95a70 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Thu, 4 Mar 2021 14:49:19 -0500 Subject: [PATCH 199/265] refactor(field-trip): add reactivate request feature --- lib/components/admin/field-trip-details.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index 55d13e885..db2fefdee 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -47,10 +47,14 @@ class FieldTripDetails extends Component { _onCloseActiveFieldTrip = () => this.props.setActiveFieldTrip(null) - _onClickCancel = () => { + _onToggleStatus = () => { const {request, setRequestStatus} = this.props - if (confirm('Are you sure you want to cancel this request? Any associated trips will be deleted.')) { - setRequestStatus(request, 'cancelled') + if (request.status !== 'cancelled') { + if (confirm('Are you sure you want to cancel this request? Any associated trips will be deleted.')) { + setRequestStatus(request, 'cancelled') + } + } else { + setRequestStatus(request, 'active') } } @@ -70,6 +74,7 @@ class FieldTripDetails extends Component { invoiceRequired, notes, schoolName, + status, submitterNotes, teacherName, ticketType, @@ -82,6 +87,7 @@ class FieldTripDetails extends Component { else operationalNotes.push(note) }) const travelDateAsMoment = moment(travelDate) + const cancelled = status === 'cancelled' return (
  • } From 0de242b48f1ec0105016538d6029cb2763cdb558 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Thu, 4 Mar 2021 15:27:06 -0500 Subject: [PATCH 200/265] refactor(field-trip): improve search performance --- lib/components/admin/draggable-window.js | 2 + lib/components/admin/field-trip-details.js | 1 - lib/components/admin/field-trip-list.js | 44 +++++++++++++++------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/lib/components/admin/draggable-window.js b/lib/components/admin/draggable-window.js index 3fbfba51d..a43789139 100644 --- a/lib/components/admin/draggable-window.js +++ b/lib/components/admin/draggable-window.js @@ -19,6 +19,8 @@ export default class DraggableWindow extends Component { const GREY_BORDER = '#777 1.3px solid' return ( diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index db2fefdee..fa2da4f9c 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -214,7 +214,6 @@ const mapDispatchToProps = { editSubmitterNotes: fieldTripActions.editSubmitterNotes, fetchQueries: fieldTripActions.fetchQueries, setActiveFieldTrip: fieldTripActions.setActiveFieldTrip, - setFieldTripFilter: fieldTripActions.setFieldTripFilter, setRequestGroupSize: fieldTripActions.setRequestGroupSize, setRequestPaymentInfo: fieldTripActions.setRequestPaymentInfo, setRequestStatus: fieldTripActions.setRequestStatus, diff --git a/lib/components/admin/field-trip-list.js b/lib/components/admin/field-trip-list.js index 61ddeae27..8516421db 100644 --- a/lib/components/admin/field-trip-list.js +++ b/lib/components/admin/field-trip-list.js @@ -58,8 +58,21 @@ class FieldTripList extends Component { this.props.setActiveFieldTrip(null) } - _onSearchChange = e => { - this.props.setFieldTripFilter({search: e.target.value}) + /** + * Change search input selectively. This is to prevent excessive rendering + * each time the search input changes (on TriMet's production instance there + * are thousands of field trip requests). + */ + _handleSearchKeyUp = e => { + const {callTaker, setFieldTripFilter} = this.props + const {search} = callTaker.fieldTrip.filter + const newSearch = e.target.value + // Update filter if Enter is pressed or search value is entirely cleared. + const newSearchEntered = e.keyCode === 13 && newSearch !== search + const searchCleared = search && !newSearch + if (newSearchEntered || searchCleared) { + setFieldTripFilter({search: newSearch}) + } } _onTabChange = e => { @@ -81,13 +94,18 @@ class FieldTripList extends Component { // If search input is found, only include field trips with at least one // field that matches search criteria. if (search) { - isVisible = SEARCH_FIELDS.some(key => { - const value = ft[key] - let hasMatch = false - if (value) { - hasMatch = value.toLowerCase().indexOf(search.toLowerCase()) !== -1 - } - return hasMatch + // Split the search terms by whitespace and check that the request has + // values that match every term. + const searchTerms = search.toLowerCase().split(' ') + isVisible = searchTerms.every(term => { + return SEARCH_FIELDS.some(key => { + const value = (ft[key] || '').toLowerCase() + let hasMatch = false + if (value) { + hasMatch = value.indexOf(term) !== -1 + } + return hasMatch + }) }) } return isVisible @@ -107,15 +125,15 @@ class FieldTripList extends Component { From b58c8977f34d7b87ceca1e9e6c832742d2b1850c Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Thu, 4 Mar 2021 16:27:31 -0500 Subject: [PATCH 201/265] refactor(field-trip): address PR #230 comments --- lib/actions/field-trip.js | 31 ++++-- lib/components/admin/field-trip-details.js | 110 +++++++++++---------- lib/components/admin/field-trip-list.js | 61 +----------- lib/util/call-taker.js | 109 ++++++++++++++++++++ 4 files changed, 197 insertions(+), 114 deletions(-) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index fdcdc01f9..652ec9270 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -28,16 +28,20 @@ export const toggleFieldTrips = createAction('TOGGLE_FIELD_TRIPS') * Fetch all field trip requests (as summaries). */ export function fetchFieldTrips () { - return function (dispatch, getState) { + return async function (dispatch, getState) { dispatch(requestingFieldTrips()) const {callTaker, otp} = getState() if (sessionIsInvalid(callTaker.session)) return const {datastoreUrl} = otp.config const {sessionId} = callTaker.session - fetch(`${datastoreUrl}/fieldtrip/getRequestsSummary?${qs.stringify({sessionId})}`) - .then(res => res.json()) - .then(fieldTrips => dispatch(receivedFieldTrips({fieldTrips}))) - .catch(err => alert(`Error fetching field trips: ${JSON.stringify(err)}`)) + let fieldTrips = [] + try { + const res = await fetch(`${datastoreUrl}/fieldtrip/getRequestsSummary?${qs.stringify({sessionId})}`) + fieldTrips = await res.json() + } catch (e) { + alert(`Error fetching field trips: ${JSON.stringify(e)}`) + } + dispatch(receivedFieldTrips({fieldTrips})) } } @@ -68,7 +72,6 @@ export function addFieldTripNote (request, note) { const {callTaker, otp} = getState() const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return - console.log(callTaker.session) const {sessionId, username} = callTaker.session const queryData = new FormData() queryData.append('sessionId', sessionId) @@ -200,6 +203,20 @@ export function saveRequestTrip (request, outbound, groupPlan) { } } +/** + * @typedef {Object} ValidationCheck + * @property {boolean} isValid - Whether the check is valid + * @property {string} message - The message explaining why the check returned + * invalid. + */ + +/** + * Checks that a group plan is valid for a given request, i.e., that it occurs + * on the requested travel date. + * @param request field trip request + * @param groupPlan the group plan to check + * @return {ValidationCheck} + */ function checkPlanValidity (request, groupPlan) { if (groupPlan == null) { return { @@ -225,7 +242,7 @@ function checkPlanValidity (request, groupPlan) { // FIXME More checks? E.g., origin/destination - return { isValid: true } + return { isValid: true, message: null } } // TODO: Enable saveTrip for field trip request. diff --git a/lib/components/admin/field-trip-details.js b/lib/components/admin/field-trip-details.js index fa2da4f9c..91d2f2af6 100644 --- a/lib/components/admin/field-trip-details.js +++ b/lib/components/admin/field-trip-details.js @@ -58,10 +58,66 @@ class FieldTripDetails extends Component { } } + _renderFooter = () => { + const cancelled = this.props.request.status === 'cancelled' + return ( +
    + + + Feedback link + + + Receipt link + + + +
    + ) + } + + _renderHeader = () => { + const {dateFormat, request} = this.props + const { + id, + schoolName, + travelDate + } = request + const travelDateAsMoment = moment(travelDate) + return ( + + {schoolName} Trip (#{id}) +
    + + Travel date: {travelDateAsMoment.format(dateFormat)}{' '} + ({travelDateAsMoment.fromNow()}) + +
    +
    + ) + } + render () { const { addFieldTripNote, - dateFormat, deleteFieldTripNote, request, setRequestGroupSize, @@ -70,15 +126,12 @@ class FieldTripDetails extends Component { } = this.props if (!request) return null const { - id, invoiceRequired, notes, schoolName, - status, submitterNotes, teacherName, - ticketType, - travelDate + ticketType } = request const internalNotes = [] const operationalNotes = [] @@ -86,53 +139,10 @@ class FieldTripDetails extends Component { if (note.type === 'internal') internalNotes.push(note) else operationalNotes.push(note) }) - const travelDateAsMoment = moment(travelDate) - const cancelled = status === 'cancelled' return ( - - - Feedback link - - - Receipt link - - - -
    - } - header={ - - {schoolName} Trip (#{id}) -
    - - Travel date: {travelDateAsMoment.format(dateFormat)}{' '} - ({travelDateAsMoment.fromNow()}) - -
    -
    - } + footer={this._renderFooter()} + header={this._renderHeader()} height='375px' onClickClose={this._onCloseActiveFieldTrip} style={style} diff --git a/lib/components/admin/field-trip-list.js b/lib/components/admin/field-trip-list.js index 8516421db..84152aa2d 100644 --- a/lib/components/admin/field-trip-list.js +++ b/lib/components/admin/field-trip-list.js @@ -1,4 +1,3 @@ -import moment from 'moment' import React, { Component } from 'react' import { Badge } from 'react-bootstrap' import { connect } from 'react-redux' @@ -8,36 +7,9 @@ import DraggableWindow from './draggable-window' import Icon from '../narrative/icon' import Loading from '../narrative/loading' import {FieldTripRecordButton, WindowHeader} from './styled' +import {getVisibleRequests, TABS} from '../../util/call-taker' import {FETCH_STATUS} from '../../util/constants' -// List of tabs used for filtering field trips. -const TABS = [ - {id: 'new', label: 'New', filter: (req) => req.status !== 'cancelled' && (!req.inboundTripStatus || !req.outboundTripStatus)}, - {id: 'planned', label: 'Planned', filter: (req) => req.status !== 'cancelled' && req.inboundTripStatus && req.outboundTripStatus}, - {id: 'cancelled', label: 'Cancelled', filter: (req) => req.status === 'cancelled'}, - {id: 'past', label: 'Past', filter: (req) => req.travelDate && moment(req.travelDate).diff(moment(), 'days') < 0}, - {id: 'all', label: 'All', filter: (req) => true} -] - -// List of fields in field trip object to which user's text search input applies. -const SEARCH_FIELDS = [ - 'address', - 'ccLastFour', - 'ccName', - 'ccType', - 'checkNumber', - 'city', - 'classpassId', - 'emailAddress', - 'endLocation', - 'grade', - 'phoneNumber', - 'schoolName', - 'startLocation', - 'submitterNotes', - 'teacherName' -] - /** * Displays a searchable list of field trip requests in a draggable window. */ @@ -80,36 +52,10 @@ class FieldTripList extends Component { } render () { - const {callTaker, style, toggleFieldTrips} = this.props + const {callTaker, style, toggleFieldTrips, visibleRequests} = this.props const {fieldTrip} = callTaker const {activeId, filter} = fieldTrip const {search} = filter - const activeTab = TABS.find(tab => tab.id === filter.tab) - const visibleRequests = fieldTrip.requests.data - .filter(ft => { - let isVisible = false - // First, filter field trip on whether it should be visible for the - // active tab. - if (activeTab) isVisible = activeTab.filter(ft) - // If search input is found, only include field trips with at least one - // field that matches search criteria. - if (search) { - // Split the search terms by whitespace and check that the request has - // values that match every term. - const searchTerms = search.toLowerCase().split(' ') - isVisible = searchTerms.every(term => { - return SEARCH_FIELDS.some(key => { - const value = (ft[key] || '').toLowerCase() - let hasMatch = false - if (value) { - hasMatch = value.indexOf(term) !== -1 - } - return hasMatch - }) - }) - } - return isVisible - }) return ( { return { callTaker: state.callTaker, currentQuery: state.otp.currentQuery, - searches: state.otp.searches + searches: state.otp.searches, + visibleRequests: getVisibleRequests(state) } } diff --git a/lib/util/call-taker.js b/lib/util/call-taker.js index e1740911d..d4717b623 100644 --- a/lib/util/call-taker.js +++ b/lib/util/call-taker.js @@ -1,5 +1,6 @@ import {isTransit} from '@opentripplanner/core-utils/lib/itinerary' import {randId} from '@opentripplanner/core-utils/lib/storage' +import moment from 'moment' import {getRoutingParams} from '../actions/api' import {getTimestamp} from './state' @@ -38,6 +39,57 @@ export const PAYMENT_FIELDS = [ {label: 'Check/Money order number', fieldName: 'checkNumber'} ] +// List of tabs used for filtering field trips. +export const TABS = [ + { + id: 'new', + label: 'New', + filter: (req) => req.status !== 'cancelled' && + (!req.inboundTripStatus || !req.outboundTripStatus) + }, + { + id: 'planned', + label: 'Planned', + filter: (req) => req.status !== 'cancelled' && + req.inboundTripStatus && req.outboundTripStatus + }, + { + id: 'cancelled', + label: 'Cancelled', + filter: (req) => req.status === 'cancelled' + }, + { + id: 'past', + label: 'Past', + filter: (req) => req.travelDate && + moment(req.travelDate).diff(moment(), 'days') < 0 + }, + { + id: 'all', + label: 'All', + filter: (req) => true + } +] + +// List of fields in field trip object to which user's text search input applies. +const SEARCH_FIELDS = [ + 'address', + 'ccLastFour', + 'ccName', + 'ccType', + 'checkNumber', + 'city', + 'classpassId', + 'emailAddress', + 'endLocation', + 'grade', + 'phoneNumber', + 'schoolName', + 'startLocation', + 'submitterNotes', + 'teacherName' +] + export function constructNewCall () { return { startTime: getTimestamp(), @@ -61,6 +113,44 @@ export function sessionIsInvalid (session) { return false } +/** + * Get visible field trip requests according to the currently selected tab and + * search terms. + */ +export function getVisibleRequests (state) { + const {callTaker} = state + const {fieldTrip} = callTaker + const {filter} = fieldTrip + const {search, tab} = filter + const activeTab = TABS.find(t => t.id === tab) + return fieldTrip.requests.data.filter(request => { + let includedInTab = false + let includedInSearch = false + // First, filter field trip on whether it should be visible for the + // active tab. + if (activeTab) includedInTab = activeTab.filter(request) + // If search input is found, only include field trips with at least one + // field that matches search criteria. + if (search) { + // Split the search terms by whitespace and check that the request has + // values that match every term. + const searchTerms = search.toLowerCase().split(' ') + includedInSearch = searchTerms.every(term => { + return SEARCH_FIELDS.some(key => { + const value = (request[key] || '').toLowerCase() + let hasMatch = false + if (value) { + hasMatch = value.indexOf(term) !== -1 + } + return hasMatch + }) + }) + return includedInTab && includedInSearch + } + return includedInTab + }) +} + /** * Utility to map an OTP MOD UI search object to a Call Taker datastore query * object. @@ -106,6 +196,13 @@ export function getTripFromRequest (request, outbound = false) { return trip } +/** + * Create trip plan from plan data with itineraries. Note: this is based on + * original code in OpenTripPlanner: + * https://github.com/ibi-group/OpenTripPlanner/blob/fdf972e590b809014e3f80160aeb6dde209dd1d4/src/client/js/otp/modules/planner/TripPlan.js#L27-L38 + * + * FIXME: This still needs to be implemented for the field trip module. + */ export function createTripPlan (planData, queryParams) { const tripPlan = { earliestStartTime: null, @@ -121,6 +218,13 @@ export function createTripPlan (planData, queryParams) { return {...tripPlan, ...timeBounds} } +/** + * Calculate time bounds for all of the itineraries. Note: this is based on + * original code in OpenTripPlanner: + * https://github.com/ibi-group/OpenTripPlanner/blob/fdf972e590b809014e3f80160aeb6dde209dd1d4/src/client/js/otp/modules/planner/TripPlan.js#L53-L66 + * + * FIXME: This still needs to be implemented for the field trip module. + */ function calculateTimeBounds (itineraries) { let earliestStartTime = null let latestEndTime = null @@ -132,8 +236,13 @@ function calculateTimeBounds (itineraries) { ? itin.getEndTime() : latestEndTime }) + return {earliestStartTime, latestEndTime} } +/** + * Create itinerary. Note this is based on original code in OpenTripPlanner: + * https://github.com/ibi-group/OpenTripPlanner/blob/46e1f9ffd9a55f0c5409d25a34769cdaff2d8cbb/src/client/js/otp/modules/planner/Itinerary.js#L27-L40 + */ function createItinerary (itinData, tripPlan) { const itin = { itinData, From b1a84b6a583f5a7950feac17f1d22fe514d64ec5 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 4 Mar 2021 17:31:44 -0500 Subject: [PATCH 202/265] refactor(BatchResultsScreen): Fix fitting itinerary after expanding map. --- lib/components/mobile/batch-results-screen.js | 91 ++++++++++--------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index 85694b826..401f8e07b 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -10,18 +10,17 @@ import Map from '../map/map' import ErrorMessage from '../form/error-message' import Icon from '../narrative/icon' import NarrativeItineraries from '../narrative/narrative-itineraries' - -import MobileContainer from './container' -import MobileNavigationBar from './navigation-bar' - -import { MobileScreens, setMobileScreen } from '../../actions/ui' import { clearActiveSearch } from '../../actions/form' +import { MobileScreens, setMobileScreen } from '../../actions/ui' import { getActiveError, getActiveItineraries, getActiveSearch } from '../../util/state' +import MobileNavigationBar from './navigation-bar' +import MobileContainer from './container' + const LocationContainer = styled.div` font-weight: 300; overflow: hidden; @@ -51,7 +50,7 @@ const ExpandMapButton = styled(Button)` bottom: 16px; position: absolute; right: 10px; - zIndex: 999999; + z-index: 999999; ` class BatchMobileResultsScreen extends Component { @@ -83,7 +82,7 @@ class BatchMobileResultsScreen extends Component { } } - _setItineraryExpanded (itineraryExpanded) { + _setItineraryExpanded = itineraryExpanded => { this.setState({ itineraryExpanded }) } @@ -94,7 +93,6 @@ class BatchMobileResultsScreen extends Component { _toggleMapExpanded = () => { this.setState({ mapExpanded: !this.state.mapExpanded }) - // Also find a way to recenter (and rerender) the map. } renderError = () => { @@ -142,6 +140,25 @@ class BatchMobileResultsScreen extends Component { ) } + renderMap () { + const { mapExpanded } = this.state + return ( +
    + {/* Extra container for positioning the expand map button relative to the map. */} +
    + + + + {mapExpanded ? 'Show results' : 'Expand map'} + +
    +
    + ) + } + render () { const { error, @@ -170,39 +187,32 @@ class BatchMobileResultsScreen extends Component { /> {this.renderLocationsSummary()} - {!itineraryExpanded && ( -
    - {/* Extra container for positioning the expand map button relative to the map. */} -
    - - - - {mapExpanded ? 'Show results' : 'Expand map'} - -
    -
    - )} - {!mapExpanded && ( -
    - -
    - )} + {mapExpanded + // Set up two separate renderings of the map according to map expanded state, + // so that it is properly sized and itineraries fit under either conditions. + // (Otherwise, if just the narrative is added/removed, the map doesn't resize properly.) + ? this.renderMap() + : ( + <> + {!itineraryExpanded && this.renderMap()} +
    + +
    + + )} ) } @@ -219,7 +229,6 @@ const mapStateToProps = (state, ownProps) => { const itineraries = getActiveItineraries(state.otp) return { - activeLeg: activeSearch ? activeSearch.activeLeg : null, error: getActiveError(state.otp), query: state.otp.currentQuery, resultCount: From 9c21e13a74aa04281f3ef589a26029c675d0b011 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 4 Mar 2021 18:05:08 -0500 Subject: [PATCH 203/265] refactor(BatchResultsScreen): Tweak styles. --- lib/components/mobile/batch-results-screen.js | 44 +++++++++++++++---- lib/components/mobile/container.js | 6 +-- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index 401f8e07b..127a384dd 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -21,6 +21,21 @@ import { import MobileNavigationBar from './navigation-bar' import MobileContainer from './container' +const StyledMobileContainer = styled(MobileContainer)` + bottom: 0; + display: flex; + flex-direction: column; + left: 0; + padding-top: 50px; + position: fixed; + right: 0; + top: 0; + + .options > .header { + margin: 10px; + } +` + const LocationContainer = styled.div` font-weight: 300; overflow: hidden; @@ -46,8 +61,19 @@ const StyledLocationIcon = styled(LocationIcon)` margin: 3px; ` +const ResultsMap = styled.div` + flex-grow: 1; + position: unset; +` + +const MapContainer = styled.div` + height: 100%; + position: relative; + width: 100%; +` + const ExpandMapButton = styled(Button)` - bottom: 16px; + bottom: 25px; position: absolute; right: 10px; z-index: 999999; @@ -143,9 +169,11 @@ class BatchMobileResultsScreen extends Component { renderMap () { const { mapExpanded } = this.state return ( -
    - {/* Extra container for positioning the expand map button relative to the map. */} -
    + + {/* Extra container for positioning the expand map button relative to the map. + (ResultsMap above has position =) + */} + {mapExpanded ? 'Show results' : 'Expand map'} -
    -
    + + ) } @@ -178,7 +206,7 @@ class BatchMobileResultsScreen extends Component { } return ( - + 1 ? 's' : ''}` @@ -213,7 +241,7 @@ class BatchMobileResultsScreen extends Component {
    )} - + ) } } diff --git a/lib/components/mobile/container.js b/lib/components/mobile/container.js index 021d536f4..0795ae7c7 100644 --- a/lib/components/mobile/container.js +++ b/lib/components/mobile/container.js @@ -2,10 +2,10 @@ import React, { Component } from 'react' export default class MobileContainer extends Component { render () { - const { className, style } = this.props + const { children, className } = this.props return ( -
    - {this.props.children} +
    + {children}
    ) } From 90be15adc7545c80397c06ec4d2881cd76a61605 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 5 Mar 2021 14:32:07 -0500 Subject: [PATCH 204/265] refactor(mobile/ResultsHeaderAndError): Extract component. Refactors. --- lib/components/mobile/batch-results-screen.js | 213 +++--------------- .../mobile/results-header-and-error.js | 142 ++++++++++++ lib/components/mobile/results-screen.js | 167 +++----------- 3 files changed, 208 insertions(+), 314 deletions(-) create mode 100644 lib/components/mobile/results-header-and-error.js diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index 127a384dd..b26bd9ef0 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -1,75 +1,30 @@ -import coreUtils from '@opentripplanner/core-utils' -import LocationIcon from '@opentripplanner/location-icon' import PropTypes from 'prop-types' import React, { Component } from 'react' -import { Button, Col, Row } from 'react-bootstrap' +import { Button } from 'react-bootstrap' import { connect } from 'react-redux' import styled from 'styled-components' import Map from '../map/map' -import ErrorMessage from '../form/error-message' import Icon from '../narrative/icon' import NarrativeItineraries from '../narrative/narrative-itineraries' -import { clearActiveSearch } from '../../actions/form' -import { MobileScreens, setMobileScreen } from '../../actions/ui' -import { - getActiveError, - getActiveItineraries, - getActiveSearch -} from '../../util/state' +import { getActiveError } from '../../util/state' -import MobileNavigationBar from './navigation-bar' import MobileContainer from './container' +import ResultsHeaderAndError from './results-header-and-error' const StyledMobileContainer = styled(MobileContainer)` - bottom: 0; - display: flex; - flex-direction: column; - left: 0; - padding-top: 50px; - position: fixed; - right: 0; - top: 0; - .options > .header { margin: 10px; } -` - -const LocationContainer = styled.div` - font-weight: 300; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -` - -const LocationSummaryContainer = styled.div` - height: 50px; - padding-right: 10px; -` - -const LocationsSummaryColFromTo = styled(Col)` - font-size: 1.1em; - line-height: 1.2em; -` - -const LocationsSummaryRow = styled(Row)` - padding: 4px 8px; -` -const StyledLocationIcon = styled(LocationIcon)` - margin: 3px; -` - -const ResultsMap = styled.div` - flex-grow: 1; - position: unset; -` - -const MapContainer = styled.div` - height: 100%; - position: relative; - width: 100%; + &.otp.mobile .mobile-narrative-container { + bottom: 0; + left: 0; + overflow-y: auto; + padding: 0; + position: fixed; + right: 0; + } ` const ExpandMapButton = styled(Button)` @@ -81,9 +36,7 @@ const ExpandMapButton = styled(Button)` class BatchMobileResultsScreen extends Component { static propTypes = { - query: PropTypes.object, - resultCount: PropTypes.number, - setMobileScreen: PropTypes.func + error: PropTypes.object } constructor () { @@ -94,13 +47,6 @@ class BatchMobileResultsScreen extends Component { } } - componentDidMount () { - // Get the target element that we want to persist scrolling for - // FIXME Do we need to add something that removes the listeners when - // component unmounts? - coreUtils.ui.enableScrollForSelector('.mobile-narrative-container') - } - componentDidUpdate (prevProps) { // Check if the active leg changed if (this.props.activeLeg !== prevProps.activeLeg) { @@ -112,111 +58,34 @@ class BatchMobileResultsScreen extends Component { this.setState({ itineraryExpanded }) } - _editSearchClicked = () => { - this.props.clearActiveSearch() - this.props.setMobileScreen(MobileScreens.SEARCH_FORM) - } - _toggleMapExpanded = () => { this.setState({ mapExpanded: !this.state.mapExpanded }) } - renderError = () => { - const { error } = this.props - - return ( - - - {this.renderLocationsSummary()} -
    -
    - -
    - -
    -
    -
    - ) - } - - renderLocationsSummary = () => { - const { query } = this.props - - return ( - - - - - { query.from ? query.from.name : '' } - - - { query.to ? query.to.name : '' } - - - - - - - - ) - } - renderMap () { const { mapExpanded } = this.state return ( - - {/* Extra container for positioning the expand map button relative to the map. - (ResultsMap above has position =) - */} - - - - - {mapExpanded ? 'Show results' : 'Expand map'} - - - +
    + + + + {mapExpanded ? 'Show results' : 'Expand map'} + +
    ) } render () { - const { - error, - resultCount - } = this.props + const { error } = this.props const { itineraryExpanded, mapExpanded } = this.state - const narrativeContainerStyle = itineraryExpanded - ? { flexGrow: 1, overflowY: 'auto', padding: 0, position: 'unset' } - : { flex: '0 0 60%', overflowY: 'auto', padding: 0, position: 'unset' } - - // Ensure that narrative covers map. - narrativeContainerStyle.backgroundColor = 'white' - - if (error) { - return this.renderError() - } - return ( - 1 ? 's' : ''}` - : 'Waiting...' - } - /> - - {this.renderLocationsSummary()} - - {mapExpanded + + {!error && (mapExpanded // Set up two separate renderings of the map according to map expanded state, // so that it is properly sized and itineraries fit under either conditions. // (Otherwise, if just the narrative is added/removed, the map doesn't resize properly.) @@ -224,11 +93,7 @@ class BatchMobileResultsScreen extends Component { : ( <> {!itineraryExpanded && this.renderMap()} -
    +
    - )} + )) + } ) } @@ -249,28 +115,9 @@ class BatchMobileResultsScreen extends Component { // connect to the redux store const mapStateToProps = (state, ownProps) => { - const activeSearch = getActiveSearch(state.otp) - const {useRealtime} = state.otp - const response = !activeSearch - ? null - : useRealtime ? activeSearch.response : activeSearch.nonRealtimeResponse - - const itineraries = getActiveItineraries(state.otp) return { - error: getActiveError(state.otp), - query: state.otp.currentQuery, - resultCount: - response - ? activeSearch.query.routingType === 'ITINERARY' - ? itineraries.length - : response.otp.profile.length - : null + error: getActiveError(state.otp) } } -const mapDispatchToProps = { - clearActiveSearch, - setMobileScreen -} - -export default connect(mapStateToProps, mapDispatchToProps)(BatchMobileResultsScreen) +export default connect(mapStateToProps)(BatchMobileResultsScreen) diff --git a/lib/components/mobile/results-header-and-error.js b/lib/components/mobile/results-header-and-error.js new file mode 100644 index 000000000..1f05c0e23 --- /dev/null +++ b/lib/components/mobile/results-header-and-error.js @@ -0,0 +1,142 @@ +import LocationIcon from '@opentripplanner/location-icon' +import PropTypes from 'prop-types' +import React, { Component } from 'react' +import { Button, Col, Row } from 'react-bootstrap' +import { connect } from 'react-redux' +import styled from 'styled-components' + +import Map from '../map/map' +import ErrorMessage from '../form/error-message' +import { clearActiveSearch } from '../../actions/form' +import { MobileScreens, setMobileScreen } from '../../actions/ui' +import { + getActiveError, + getActiveItineraries, + getActiveSearch +} from '../../util/state' + +import MobileNavigationBar from './navigation-bar' + +const LocationContainer = styled.div` + font-weight: 300; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` + +const LocationSummaryContainer = styled.div` + height: 50px; + left: 0; + padding-right: 10px; + position: fixed; + right: 0; + top: 50px; +` + +const LocationsSummaryColFromTo = styled(Col)` + font-size: 1.1em; + line-height: 1.2em; +` + +const LocationsSummaryRow = styled(Row)` + padding: 4px 8px; +` + +const StyledLocationIcon = styled(LocationIcon)` + margin: 3px; +` + +class ResultsHeaderAndError extends Component { + static propTypes = { + query: PropTypes.object, + resultCount: PropTypes.number, + setMobileScreen: PropTypes.func + } + + _editSearchClicked = () => { + this.props.clearActiveSearch() + this.props.setMobileScreen(MobileScreens.SEARCH_FORM) + } + + render () { + const { error, query, resultCount } = this.props + const headerText = error + ? 'No Trip Found' + : (resultCount + ? `We Found ${resultCount} Option${resultCount > 1 ? 's' : ''}` + : 'Waiting...' + ) + + return ( + <> + + + + + + + { query.from ? query.from.name : '' } + + + { query.to ? query.to.name : '' } + + + + + + + + + {error && ( + <> +
    +
    + +
    + +
    +
    + + )} + + ) + } +} + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + const activeSearch = getActiveSearch(state.otp) + const {useRealtime} = state.otp + const response = !activeSearch + ? null + : useRealtime ? activeSearch.response : activeSearch.nonRealtimeResponse + + const itineraries = getActiveItineraries(state.otp) + return { + error: getActiveError(state.otp), + query: state.otp.currentQuery, + resultCount: + response + ? activeSearch.query.routingType === 'ITINERARY' + ? itineraries.length + : response.otp.profile.length + : null + } +} + +const mapDispatchToProps = { + clearActiveSearch, + setMobileScreen +} + +export default connect(mapStateToProps, mapDispatchToProps)(ResultsHeaderAndError) diff --git a/lib/components/mobile/results-screen.js b/lib/components/mobile/results-screen.js index 93bb9bb61..3ad65961d 100644 --- a/lib/components/mobile/results-screen.js +++ b/lib/components/mobile/results-screen.js @@ -1,21 +1,11 @@ import coreUtils from '@opentripplanner/core-utils' -import LocationIcon from '@opentripplanner/location-icon' import PropTypes from 'prop-types' import React, { Component } from 'react' -import { Button, Col, Row } from 'react-bootstrap' import { connect } from 'react-redux' -import styled from 'styled-components' import Map from '../map/map' -import ErrorMessage from '../form/error-message' import ItineraryCarousel from '../narrative/itinerary-carousel' - -import MobileContainer from './container' -import MobileNavigationBar from './navigation-bar' - -import { MobileScreens, setMobileScreen } from '../../actions/ui' import { setUseRealtimeResponse } from '../../actions/narrative' -import { clearActiveSearch } from '../../actions/form' import { getActiveError, getActiveItineraries, @@ -23,41 +13,18 @@ import { getRealtimeEffects } from '../../util/state' -const LocationContainer = styled.div` - font-weight: 300; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -` - -const LocationSummaryContainer = styled.div` - height: 50px; - left: 0; - padding-right: 10px; - position: fixed; - right: 0; - top: 50px; -` - -const LocationsSummaryColFromTo = styled(Col)` - font-size: 1.1em; - line-height: 1.2em; -` - -const LocationsSummaryRow = styled(Row)` - padding: 4px 8px; -` - -const StyledLocationIcon = styled(LocationIcon)` - margin: 3px; -` +import ResultsHeaderAndError from './results-header-and-error' +import MobileContainer from './container' class MobileResultsScreen extends Component { static propTypes = { activeItineraryIndex: PropTypes.number, + activeLeg: PropTypes.number, + error: PropTypes.object, query: PropTypes.object, + realtimeEffects: PropTypes.object, resultCount: PropTypes.number, - setMobileScreen: PropTypes.func + useRealtime: PropTypes.bool } constructor () { @@ -86,11 +53,6 @@ class MobileResultsScreen extends Component { this.refs['narrative-container'].scrollTop = 0 } - _editSearchClicked = () => { - this.props.clearActiveSearch() - this.props.setMobileScreen(MobileScreens.SEARCH_FORM) - } - _optionClicked = () => { this._setExpanded(!this.state.expanded) } @@ -107,8 +69,8 @@ class MobileResultsScreen extends Component { for (let i = 0; i < resultCount; i++) { dots.push(
    ) } @@ -118,57 +80,11 @@ class MobileResultsScreen extends Component { ) } - renderError = () => { - const { error } = this.props - - return ( - - - {this.renderLocationsSummary()} -
    -
    - -
    - -
    -
    -
    - ) - } - - renderLocationsSummary = () => { - const { query } = this.props - - return ( - - - - - { query.from ? query.from.name : '' } - - - { query.to ? query.to.name : '' } - - - - - - - - ) - } - render () { const { activeItineraryIndex, error, realtimeEffects, - resultCount, useRealtime } = this.props const { expanded } = this.state @@ -180,54 +96,45 @@ class MobileResultsScreen extends Component { // Ensure that narrative covers map. narrativeContainerStyle.backgroundColor = 'white' - let headerAction = null const showRealtimeAnnotation = realtimeEffects.isAffectedByRealtimeData && ( realtimeEffects.exceedsThreshold || realtimeEffects.routesDiffer || !useRealtime ) - if (error) { - return this.renderError() - } - return ( - 1 ? 's' : ''}` - : 'Waiting...' - } - headerAction={headerAction} - /> - {this.renderLocationsSummary()} - -
    - -
    - -
    - Option {activeItineraryIndex + 1} - -
    + + {!error && ( + <> +
    + +
    -
    - -
    - {this.renderDots()} + style={{ bottom: expanded ? null : 100, top: expanded ? 100 : null }} + > + Option {activeItineraryIndex + 1} + +
    + +
    + +
    + {this.renderDots()} + + )} ) } @@ -261,8 +168,6 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - clearActiveSearch, - setMobileScreen, setUseRealtimeResponse } From 50d8d23b62953c0d0bfe9eb0135d3616bb539459 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 5 Mar 2021 14:48:53 -0500 Subject: [PATCH 205/265] style(mobile/*results*): Tweak comments and sort props and imports. --- lib/components/mobile/batch-results-screen.js | 2 +- lib/components/mobile/results-header-and-error.js | 12 ++++++------ lib/components/mobile/results-screen.js | 14 +++++++------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index b26bd9ef0..2067894ca 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -86,7 +86,7 @@ class BatchMobileResultsScreen extends Component { {!error && (mapExpanded - // Set up two separate renderings of the map according to map expanded state, + // Set up two separate renderings of the map according mapExpanded, // so that it is properly sized and itineraries fit under either conditions. // (Otherwise, if just the narrative is added/removed, the map doesn't resize properly.) ? this.renderMap() diff --git a/lib/components/mobile/results-header-and-error.js b/lib/components/mobile/results-header-and-error.js index 1f05c0e23..da6b4c78a 100644 --- a/lib/components/mobile/results-header-and-error.js +++ b/lib/components/mobile/results-header-and-error.js @@ -5,10 +5,10 @@ import { Button, Col, Row } from 'react-bootstrap' import { connect } from 'react-redux' import styled from 'styled-components' -import Map from '../map/map' +import * as formActions from '../../actions/form' +import * as uiActions from '../../actions/ui' import ErrorMessage from '../form/error-message' -import { clearActiveSearch } from '../../actions/form' -import { MobileScreens, setMobileScreen } from '../../actions/ui' +import Map from '../map/map' import { getActiveError, getActiveItineraries, @@ -55,7 +55,7 @@ class ResultsHeaderAndError extends Component { _editSearchClicked = () => { this.props.clearActiveSearch() - this.props.setMobileScreen(MobileScreens.SEARCH_FORM) + this.props.setMobileScreen(uiActions.MobileScreens.SEARCH_FORM) } render () { @@ -135,8 +135,8 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - clearActiveSearch, - setMobileScreen + clearActiveSearch: formActions.clearActiveSearch, + setMobileScreen: uiActions.setMobileScreen } export default connect(mapStateToProps, mapDispatchToProps)(ResultsHeaderAndError) diff --git a/lib/components/mobile/results-screen.js b/lib/components/mobile/results-screen.js index 3ad65961d..b65db3215 100644 --- a/lib/components/mobile/results-screen.js +++ b/lib/components/mobile/results-screen.js @@ -3,9 +3,9 @@ import PropTypes from 'prop-types' import React, { Component } from 'react' import { connect } from 'react-redux' +import * as narrativeActions from '../../actions/narrative' import Map from '../map/map' import ItineraryCarousel from '../narrative/itinerary-carousel' -import { setUseRealtimeResponse } from '../../actions/narrative' import { getActiveError, getActiveItineraries, @@ -13,8 +13,8 @@ import { getRealtimeEffects } from '../../util/state' -import ResultsHeaderAndError from './results-header-and-error' import MobileContainer from './container' +import ResultsHeaderAndError from './results-header-and-error' class MobileResultsScreen extends Component { static propTypes = { @@ -152,23 +152,23 @@ const mapStateToProps = (state, ownProps) => { const realtimeEffects = getRealtimeEffects(state.otp) const itineraries = getActiveItineraries(state.otp) return { + activeItineraryIndex: activeSearch ? activeSearch.activeItinerary : null, + activeLeg: activeSearch ? activeSearch.activeLeg : null, + error: getActiveError(state.otp), query: state.otp.currentQuery, realtimeEffects, - error: getActiveError(state.otp), resultCount: response ? activeSearch.query.routingType === 'ITINERARY' ? itineraries.length : response.otp.profile.length : null, - useRealtime, - activeItineraryIndex: activeSearch ? activeSearch.activeItinerary : null, - activeLeg: activeSearch ? activeSearch.activeLeg : null + useRealtime } } const mapDispatchToProps = { - setUseRealtimeResponse + setUseRealtimeResponse: narrativeActions.setUseRealtimeResponse } export default connect(mapStateToProps, mapDispatchToProps)(MobileResultsScreen) From cd0a8fed364c696433f7846c5879bd26ff567317 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 8 Mar 2021 10:17:28 -0500 Subject: [PATCH 206/265] refactor(app-menu): preserve sessionId on start over --- lib/components/app/app-menu.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/components/app/app-menu.js b/lib/components/app/app-menu.js index 6bf4d9374..703c0752d 100644 --- a/lib/components/app/app-menu.js +++ b/lib/components/app/app-menu.js @@ -1,7 +1,9 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' +import qs from 'qs' import { connect } from 'react-redux' import { DropdownButton, MenuItem } from 'react-bootstrap' +import { withRouter } from 'react-router' import Icon from '../narrative/icon' @@ -19,11 +21,21 @@ class AppMenu extends Component { } _startOver = () => { - const { reactRouterConfig } = this.props + const { location, reactRouterConfig } = this.props + const { search } = location let startOverUrl = '/' if (reactRouterConfig && reactRouterConfig.basename) { startOverUrl += reactRouterConfig.basename } + // If search contains sessionId, preserve this so that the current session + // is not lost when the page reloads. + if (search) { + const params = qs.parse(search, { ignoreQueryPrefix: true }) + const { sessionId } = params + if (sessionId) { + startOverUrl += `?${qs.stringify({sessionId})}` + } + } window.location.href = startOverUrl } @@ -62,4 +74,4 @@ const mapDispatchToProps = { setMainPanelContent } -export default connect(mapStateToProps, mapDispatchToProps)(AppMenu) +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AppMenu)) From 6fd2676c6873e54c6810c570238f83c1f8184826 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 8 Mar 2021 10:26:33 -0500 Subject: [PATCH 207/265] refactor(field-trip): use object-to-formdata lib --- lib/actions/call-taker.js | 23 ++++++------ lib/actions/field-trip.js | 73 ++++++++++++++++++--------------------- package.json | 1 + yarn.lock | 5 +++ 4 files changed, 51 insertions(+), 51 deletions(-) diff --git a/lib/actions/call-taker.js b/lib/actions/call-taker.js index d03bbffc7..5a48b8284 100644 --- a/lib/actions/call-taker.js +++ b/lib/actions/call-taker.js @@ -1,4 +1,5 @@ import { getUrlParams } from '@opentripplanner/core-utils/lib/query' +import { serialize } from 'object-to-formdata' import qs from 'qs' import { createAction } from 'redux-actions' @@ -29,12 +30,16 @@ export function endCall () { return function (dispatch, getState) { const {callTaker, otp} = getState() const {activeCall, session} = callTaker + const { sessionId } = session if (sessionIsInvalid(session)) return - // Make POST request to store call. - const callData = new FormData() - callData.append('sessionId', session.sessionId) - callData.append('call.startTime', activeCall.startTime) - callData.append('call.endTime', getTimestamp()) + // Make POST request to store new call. + const callData = serialize({ + sessionId, + call: { + startTime: activeCall.startTime, + endTime: getTimestamp() + } + }) fetch(`${otp.config.datastoreUrl}/calltaker/call`, {method: 'POST', body: callData} ) @@ -144,13 +149,7 @@ export function saveQueriesForCall (call) { const search = otp.searches[searchId] const query = searchToQuery(search, call, otp.config) const {sessionId} = callTaker.session - const queryData = new FormData() - queryData.append('sessionId', sessionId) - queryData.append('query.queryParams', query.queryParams) - queryData.append('query.fromPlace', query.fromPlace) - queryData.append('query.toPlace', query.toPlace) - queryData.append('query.timeStamp', query.timeStamp) - queryData.append('query.call.id', call.id) + const queryData = serialize({ sessionId, query }) return fetch(`${datastoreUrl}/calltaker/callQuery?sessionId=${sessionId}`, {method: 'POST', body: queryData} ) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 652ec9270..0e8927ee3 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -1,6 +1,7 @@ import { planParamsToQueryAsync } from '@opentripplanner/core-utils/lib/query' import { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } from '@opentripplanner/core-utils/lib/time' import moment from 'moment' +import { serialize } from 'object-to-formdata' import qs from 'qs' import { createAction } from 'redux-actions' @@ -73,14 +74,13 @@ export function addFieldTripNote (request, note) { const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return const {sessionId, username} = callTaker.session - const queryData = new FormData() - queryData.append('sessionId', sessionId) - queryData.append('note.note', note.note) - queryData.append('note.type', note.type) - queryData.append('note.userName', username) - queryData.append('requestId', request.id) + const noteData = serialize({ + sessionId, + note: {...note, userName: username}, + requestId: request.id + }) return fetch(`${datastoreUrl}/fieldtrip/addNote`, - {method: 'POST', body: queryData} + {method: 'POST', body: noteData} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -98,11 +98,8 @@ export function deleteFieldTripNote (request, noteId) { const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return const {sessionId} = callTaker.session - const queryData = new FormData() - queryData.append('sessionId', sessionId) - queryData.append('noteId', noteId) return fetch(`${datastoreUrl}/fieldtrip/deleteNote`, - {method: 'POST', body: queryData} + {method: 'POST', body: serialize({ noteId, sessionId })} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -171,12 +168,13 @@ export function editSubmitterNotes (request, submitterNotes) { const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return const {sessionId} = callTaker.session - const queryData = new FormData() - queryData.append('sessionId', sessionId) - queryData.append('notes', submitterNotes) - queryData.append('requestId', request.id) + const noteData = serialize({ + notes: submitterNotes, + requestId: request.id, + sessionId + }) return fetch(`${datastoreUrl}/fieldtrip/editSubmitterNotes`, - {method: 'POST', body: queryData} + {method: 'POST', body: noteData} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -365,7 +363,8 @@ function planInbound (request) { } /** - * Set group size for a field trip request. + * Set group size for a field trip request. Group size consists of numStudents, + * numFreeStudents, and numChaperones. */ export function setRequestGroupSize (request, groupSize) { return function (dispatch, getState) { @@ -373,14 +372,13 @@ export function setRequestGroupSize (request, groupSize) { const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return const {sessionId} = callTaker.session - const queryData = new FormData() - queryData.append('sessionId', sessionId) - queryData.append('numStudents', groupSize.numStudents) - queryData.append('numFreeStudents', groupSize.numFreeStudents) - queryData.append('numChaperones', groupSize.numChaperones) - queryData.append('requestId', request.id) + const groupSizeData = serialize({ + ...groupSize, + requestId: request.id, + sessionId + }) return fetch(`${datastoreUrl}/fieldtrip/setRequestGroupSize`, - {method: 'POST', body: queryData} + {method: 'POST', body: groupSizeData} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -398,17 +396,13 @@ export function setRequestPaymentInfo (request, paymentInfo) { const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return const {sessionId} = callTaker.session - const queryData = new FormData() - queryData.append('sessionId', sessionId) - queryData.append('classpassId', paymentInfo.classpassId) - queryData.append('paymentPreference', paymentInfo.paymentPreference) - queryData.append('ccType', paymentInfo.ccType) - queryData.append('ccName', paymentInfo.ccName) - queryData.append('ccLastFour', paymentInfo.ccLastFour) - queryData.append('checkNumber', paymentInfo.checkNumber) - queryData.append('requestId', request.id) + const paymentData = serialize({ + ...paymentInfo, + requestId: request.id, + sessionId + }) return fetch(`${datastoreUrl}/fieldtrip/setRequestPaymentInfo`, - {method: 'POST', body: queryData} + {method: 'POST', body: paymentData} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { @@ -426,12 +420,13 @@ export function setRequestStatus (request, status) { const {datastoreUrl} = otp.config if (sessionIsInvalid(callTaker.session)) return const {sessionId} = callTaker.session - const queryData = new FormData() - queryData.append('sessionId', sessionId) - queryData.append('status', status) - queryData.append('requestId', request.id) + const statusData = serialize({ + requestId: request.id, + sessionId, + status + }) return fetch(`${datastoreUrl}/fieldtrip/setRequestStatus`, - {method: 'POST', body: queryData} + {method: 'POST', body: statusData} ) .then(() => dispatch(fetchFieldTripDetails(request.id))) .catch(err => { diff --git a/package.json b/package.json index fcd696904..84fb839d7 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "moment-timezone": "^0.5.23", "object-hash": "^1.3.1", "object-path": "^0.11.5", + "object-to-formdata": "^4.1.0", "prop-types": "^15.6.0", "qs": "^6.3.0", "react": "^16.9.0", diff --git a/yarn.lock b/yarn.lock index 5b5d81889..e4bf37728 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11552,6 +11552,11 @@ object-path@^0.11.5: resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.5.tgz#d4e3cf19601a5140a55a16ad712019a9c50b577a" integrity sha512-jgSbThcoR/s+XumvGMTMf81QVBmah+/Q7K7YduKeKVWL7N111unR2d6pZZarSk6kY/caeNxUDyxOvMWyzoU2eg== +object-to-formdata@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object-to-formdata/-/object-to-formdata-4.1.0.tgz#0d7bdd6f9e4efa8c0075a770c11ec92b7bf6c560" + integrity sha512-4Ti3VLTspWOUt5QIBl5/BpvLBnr4tbFpZ/FpXKWQFqLslvGIB3ug3jurW/k8iIpoyE6HcZhhmQ6mFiOq1tKGEQ== + object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" From b708163ca79bb1711122ecbe3842507659d4f4ed Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 8 Mar 2021 11:09:01 -0500 Subject: [PATCH 208/265] fix(ResponsiveWebApp): Implement terms routes and component stubs. --- example.js | 21 ++++++++++++- lib/components/app/app-frame.js | 40 ++++++++++++++++++++++++ lib/components/app/responsive-webapp.js | 13 +++++++- lib/components/user/account-page.js | 18 +++-------- lib/components/user/terms-of-use-pane.js | 6 ++-- lib/util/constants.js | 2 ++ 6 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 lib/components/app/app-frame.js diff --git a/example.js b/example.js index 1845f3ee5..7cd41139c 100644 --- a/example.js +++ b/example.js @@ -47,16 +47,35 @@ if (useCustomIcons) { MyModeIcon = CustomIcons.CustomModeIcon } +// Stubs for terms of service/storage for development purposes only. +// These components should be placed in their own files with appropriate content. +const TermsOfService = () => ( + <> +

    Terms of Service

    +

    Content for terms of service.

    + +) +const TermsOfStorage = () => ( + <> +

    Terms of Storage

    +

    Content for terms of storage.

    + +) + // define some application-wide components that should be used in // various places. The following components can be provided here: // - ItineraryBody (required) // - ItineraryFooter (optional) // - LegIcon (required) // - ModeIcon (required) +// - PrivacyPolicy (optional) +// - TermsAndConditions (optional) const components = { ItineraryBody: DefaultItinerary, LegIcon: MyLegIcon, - ModeIcon: MyModeIcon + ModeIcon: MyModeIcon, + TermsOfService, + TermsOfStorage } // Get the initial query from config (for demo/testing purposes). diff --git a/lib/components/app/app-frame.js b/lib/components/app/app-frame.js new file mode 100644 index 000000000..e6dc00129 --- /dev/null +++ b/lib/components/app/app-frame.js @@ -0,0 +1,40 @@ +import React from 'react' +import { Col, Row } from 'react-bootstrap' + +import DesktopNav from './desktop-nav' + +/** + * This component defines the general application frame, to which + * content and an optional sub-navigation component can be inserted. + */ +const AppFrame = ({ children, SubNav }) => ( +
    + {/* TODO: Do mobile view. */} + + {SubNav && } +
    + + + {children} + + +
    +
    +) + +/** + * Creates a simple wrapper component consisting of an AppFrame that surrounds + * the provided component. (Displays "no content" if none provided.) + */ +export function frame (Component) { + return () => ( + + {Component + ? + :

    No content provided.

    + } +
    + ) +} + +export default AppFrame diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 1a57a467f..b2f3f5f22 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -9,7 +9,6 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import { Route, Switch, withRouter } from 'react-router' -import PrintLayout from './print-layout' import * as authActions from '../../actions/auth' import * as callTakerActions from '../../actions/call-taker' import * as configActions from '../../actions/config' @@ -17,7 +16,9 @@ import * as formActions from '../../actions/form' import * as locationActions from '../../actions/location' import * as mapActions from '../../actions/map' import * as uiActions from '../../actions/ui' +import { frame } from '../app/app-frame' import { RedirectWithQuery } from '../form/connected-links' +import PrintLayout from './print-layout' import { getAuth0Config } from '../../util/auth' import { ACCOUNT_PATH, @@ -28,6 +29,8 @@ import { CREATE_ACCOUNT_PLACES_PATH, CREATE_ACCOUNT_VERIFY_PATH, PLACES_PATH, + TERMS_OF_SERVICE_PATH, + TERMS_OF_STORAGE_PATH, TRIPS_PATH, URL_ROOT } from '../../util/constants' @@ -280,6 +283,14 @@ class RouterWrapperWithAuth0 extends Component { component={UserAccountScreen} path={[`${CREATE_ACCOUNT_PATH}/:step`, ACCOUNT_SETTINGS_PATH]} /> + + - {/* TODO: Do mobile view. */} - - {subnav && } -
    - - - {children} - - -
    -
    + + {children} + ) } } diff --git a/lib/components/user/terms-of-use-pane.js b/lib/components/user/terms-of-use-pane.js index e220d4588..fa723c582 100644 --- a/lib/components/user/terms-of-use-pane.js +++ b/lib/components/user/terms-of-use-pane.js @@ -1,6 +1,8 @@ import React from 'react' import { Checkbox, ControlLabel, FormGroup } from 'react-bootstrap' +import { TERMS_OF_SERVICE_PATH, TERMS_OF_STORAGE_PATH } from '../../util/constants' + /** * User terms of use pane. */ @@ -28,7 +30,7 @@ const TermsOfUsePane = ({ onChange={disableCheckTerms ? null : handleChange} > {/* TODO: Implement the link */} - I have read and consent to the Terms of Service for using the Trip Planner. + I have read and consent to the Terms of Service for using the Trip Planner. @@ -41,7 +43,7 @@ const TermsOfUsePane = ({ > {/* TODO: Implement the link */} Optional: I consent to the Trip Planner storing my historical planned trips in order to - improve transit services in my area. More info... + improve transit services in my area. More info...
    diff --git a/lib/util/constants.js b/lib/util/constants.js index 2d5f69964..b37da6ead 100644 --- a/lib/util/constants.js +++ b/lib/util/constants.js @@ -19,6 +19,8 @@ export const CREATE_ACCOUNT_TERMS_PATH = `${CREATE_ACCOUNT_PATH}/terms` export const CREATE_ACCOUNT_VERIFY_PATH = `${CREATE_ACCOUNT_PATH}/verify` export const CREATE_ACCOUNT_PLACES_PATH = `${CREATE_ACCOUNT_PATH}/places` export const CREATE_TRIP_PATH = `${TRIPS_PATH}/new` +export const TERMS_OF_SERVICE_PATH = `/terms-of-service` +export const TERMS_OF_STORAGE_PATH = `/terms-of-storage` // Gets the root URL, e.g. https://otp-instance.example.com:8080, computed once for all. // TODO: support root URLs that involve paths or subfolders, as in https://otp-ui.example.com/path-to-ui/ From f95638817c7736ad80cb0d6a83f1e52ee3581ebc Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 8 Mar 2021 15:41:33 -0500 Subject: [PATCH 209/265] refactor(NotFound): Add enhanced format for not found content. --- lib/components/app/app-frame.js | 3 ++- lib/components/app/not-found.js | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 lib/components/app/not-found.js diff --git a/lib/components/app/app-frame.js b/lib/components/app/app-frame.js index e6dc00129..a1719ba8a 100644 --- a/lib/components/app/app-frame.js +++ b/lib/components/app/app-frame.js @@ -2,6 +2,7 @@ import React from 'react' import { Col, Row } from 'react-bootstrap' import DesktopNav from './desktop-nav' +import NotFound from './not-found' /** * This component defines the general application frame, to which @@ -31,7 +32,7 @@ export function frame (Component) { {Component ? - :

    No content provided.

    + : }
    ) diff --git a/lib/components/app/not-found.js b/lib/components/app/not-found.js new file mode 100644 index 000000000..4733b08b7 --- /dev/null +++ b/lib/components/app/not-found.js @@ -0,0 +1,19 @@ +import React from 'react' +import { Alert, Glyphicon } from 'react-bootstrap' +import styled from 'styled-components' + +const StyledAlert = styled(Alert)` + margin-top: 25px; +` + +/** + * Displays a not-found alert if some content is not found. + */ +const NotFound = () => ( + +

    Content not found

    +

    The content you requested is not available.

    +
    +) + +export default NotFound From a1e63b3cd18544d40eeae993ecaaa4933f88072c Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Tue, 9 Mar 2021 16:27:33 -0800 Subject: [PATCH 210/265] refactor: refactor ResponsiveWebapp and some other things BREAKING CHANGE: refactored example.js, ResponsiveWebapp and associated component signatures to use components provided to component context --- __tests__/reducers/create-otp-reducer.js | 2 +- __tests__/test-utils/mock-data/store.js | 7 +- example.js | 156 ++++++------------ lib/components/app/batch-routing-panel.js | 1 - lib/components/app/responsive-webapp.js | 48 +++++- lib/components/mobile/main.js | 23 +-- lib/components/mobile/navigation-bar.js | 12 +- lib/components/mobile/results-screen.js | 131 ++++++++------- lib/components/mobile/welcome-screen.js | 5 +- .../narrative/narrative-itineraries.js | 1 - lib/index.js | 11 +- lib/reducers/create-otp-reducer.js | 8 +- 12 files changed, 200 insertions(+), 205 deletions(-) diff --git a/__tests__/reducers/create-otp-reducer.js b/__tests__/reducers/create-otp-reducer.js index 05fcdf475..979268d0b 100644 --- a/__tests__/reducers/create-otp-reducer.js +++ b/__tests__/reducers/create-otp-reducer.js @@ -6,6 +6,6 @@ describe('lib > reducers > create-otp-reducer', () => { it('should be able to create the initial state', () => { setDefaultTestTime() - expect(getInitialState({}, {})).toMatchSnapshot() + expect(getInitialState({})).toMatchSnapshot() }) }) diff --git a/__tests__/test-utils/mock-data/store.js b/__tests__/test-utils/mock-data/store.js index 90e7a696e..724435f0d 100644 --- a/__tests__/test-utils/mock-data/store.js +++ b/__tests__/test-utils/mock-data/store.js @@ -22,10 +22,11 @@ const storeMiddleWare = [ * Get the initial stop of the redux reducer for otp-rr */ export function getMockInitialState () { - const mockConfig = {} - const mockInitialQuery = {} + const mockConfig = { + initialQuery: {} + } return clone({ - otp: getInitialState(mockConfig, mockInitialQuery), + otp: getInitialState(mockConfig), router: connectRouter(history) }) } diff --git a/example.js b/example.js index 1845f3ee5..0f4484e8c 100644 --- a/example.js +++ b/example.js @@ -4,35 +4,37 @@ import 'es6-math' import {ClassicLegIcon, ClassicModeIcon} from '@opentripplanner/icons' import { createHashHistory } from 'history' import { connectRouter, routerMiddleware } from 'connected-react-router' -import React, { Component } from 'react' +import React from 'react' import { render } from 'react-dom' import { createStore, combineReducers, applyMiddleware, compose } from 'redux' import { Provider } from 'react-redux' import thunk from 'redux-thunk' import createLogger from 'redux-logger' -// import Bootstrap Grid components for layout -import { Grid, Row, Col } from 'react-bootstrap' - // import OTP-RR components import { + BatchRoutingPanel, + BatchSearchScreen, CallTakerControls, CallTakerPanel, CallTakerWindows, DefaultItinerary, DefaultMainPanel, - DesktopNav, - BatchRoutingPanel, - Map, - MobileMain, + MobileSearchScreen, ResponsiveWebapp, createCallTakerReducer, createOtpReducer, - createUserReducer + createUserReducer, + otpUtils } from './lib' // load the OTP configuration import otpConfig from './config.yml' +const isBatchRoutingEnabled = otpUtils.itinerary.isBatchRoutingEnabled( + otpConfig +) +const isCallTakerModuleEnabled = !!otpConfig.datastoreUrl + // Set useCustomIcons to true to override classic icons with the exports from // custom-icons.js const useCustomIcons = false @@ -49,18 +51,32 @@ if (useCustomIcons) { // define some application-wide components that should be used in // various places. The following components can be provided here: +// - defaultMobileTitle (required) // - ItineraryBody (required) // - ItineraryFooter (optional) // - LegIcon (required) +// - MainControls (optional) +// - MainPanel (required) +// - MapWindows (optional) +// - MobileSearchScreen (required) // - ModeIcon (required) const components = { + defaultMobileTitle: () =>
    OpenTripPlanner
    , ItineraryBody: DefaultItinerary, LegIcon: MyLegIcon, + MainControls: isCallTakerModuleEnabled ? CallTakerControls : null, + MainPanel: isCallTakerModuleEnabled + ? CallTakerPanel + : isBatchRoutingEnabled + ? BatchRoutingPanel + : DefaultMainPanel, + MapWindows: isCallTakerModuleEnabled ? CallTakerWindows : null, + MobileSearchScreen: isBatchRoutingEnabled + ? BatchSearchScreen + : MobileSearchScreen, ModeIcon: MyModeIcon } -// Get the initial query from config (for demo/testing purposes). -const {initialQuery} = otpConfig const history = createHashHistory() const middleware = [ thunk, @@ -76,109 +92,41 @@ if (process.env.NODE_ENV === 'development') { const store = createStore( combineReducers({ callTaker: createCallTakerReducer(), - otp: createOtpReducer(otpConfig, initialQuery), + otp: createOtpReducer(otpConfig), user: createUserReducer(), router: connectRouter(history) }), compose(applyMiddleware(...middleware)) ) -// define a simple responsive UI using Bootstrap and OTP-RR -class OtpRRExample extends Component { - render () { - /** desktop view **/ - const desktopView = ( -
    - - - - - {/* - Note: the main tag provides a way for users of screen readers - to skip to the primary page content. - TODO: Find a better place. - */} -
    - {/* TODO: extract the BATCH elements out of CallTakerPanel. */} - {otpConfig.datastoreUrl - ? - : otpConfig.routingTypes.find(t => t.key === 'BATCH') - ? - : - } -
    - - {otpConfig.datastoreUrl ? : null} - - {otpConfig.datastoreUrl ? : null} - - -
    -
    -
    - ) - - /** mobile view **/ - const mobileView = ( - //
    Needed for accessibility checks. TODO: Find a better place. -
    - } - title={
    OpenTripPlanner
    } - /> -
    - ) - - /** - * The main webapp. - * - * Note: the ResponsiveWebapp creates a React context provider - * (./util/contexts#ComponentContext to be specific) to supply custom - * components to various other subcomponents throughout otp-react-redux. If - * the ResponsiveWebapp is not used and instead some subcomponents that use - * the components in the `components` variable are imported and rendered - * outside of the ResponsiveWebapp component, then the ComponentContext will - * need to wrap that component in order for the subcomponents to be able to - * access the component context. For example: - * - * ```js - * import RouteViewer from 'otp-react-redux/build/components/viewers/route-viewer' - * import { ComponentContext } from 'otp-react-redux/build/util/contexts' - * - * const components = { - * ModeIcon: MyCustomModeIconComponent - * } - * const ContextAwareRouteViewer = () => ( - * - * - * - * ) - * ``` - */ - return ( - - ) - } -} - // render the app render( ( - { /** - * If not using router history, simply include OtpRRExample here: - * e.g. - * - */ - } - + {/** + * Note: the ResponsiveWebapp creates a React context provider + * (./util/contexts#ComponentContext to be specific) to supply custom + * components to various other subcomponents throughout otp-react-redux. If + * the ResponsiveWebapp is not used and instead some subcomponents that use + * the components in the `components` variable are imported and rendered + * outside of the ResponsiveWebapp component, then the ComponentContext will + * need to wrap that component in order for the subcomponents to be able to + * access the component context. For example: + * + * ```js + * import RouteViewer from 'otp-react-redux/build/components/viewers/route-viewer' + * import { ComponentContext } from 'otp-react-redux/build/util/contexts' + * + * const components = { ModeIcon: MyCustomModeIconComponent } + * const ContextAwareRouteViewer = () => ( + * + * + * + * ) + * ``` + */} + - ) - , - + ), document.getElementById('root') ) diff --git a/lib/components/app/batch-routing-panel.js b/lib/components/app/batch-routing-panel.js index 14fee5bc5..0cc9a36a8 100644 --- a/lib/components/app/batch-routing-panel.js +++ b/lib/components/app/batch-routing-panel.js @@ -1,4 +1,3 @@ -import coreUtils from '@opentripplanner/core-utils' import React, { Component } from 'react' import { connect } from 'react-redux' import styled from 'styled-components' diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 7b2b9b27d..2d1ac0c4d 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -5,6 +5,7 @@ import isEqual from 'lodash.isequal' import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' import React, { Component } from 'react' +import { Col, Grid, Row } from 'react-bootstrap' import { connect } from 'react-redux' import { Redirect, Route, Switch, withRouter } from 'react-router' @@ -16,6 +17,9 @@ import * as formActions from '../../actions/form' import * as locationActions from '../../actions/location' import * as mapActions from '../../actions/map' import * as uiActions from '../../actions/ui' +import Map from '../map/map' +import MobileMain from '../mobile/main' +import DesktopNav from './desktop-nav' import { getAuth0Config } from '../../util/auth' import { ACCOUNT_PATH, @@ -39,12 +43,12 @@ const { isMobile } = coreUtils.ui class ResponsiveWebapp extends Component { static propTypes = { - desktopView: PropTypes.element, initZoomOnLocate: PropTypes.number, - mobileView: PropTypes.element, query: PropTypes.object } + static contextType = ComponentContext + /** Lifecycle methods **/ componentDidUpdate (prevProps) { @@ -144,9 +148,45 @@ class ResponsiveWebapp extends Component { window.removeEventListener('popstate', this.props.handleBackButtonPress) } + renderDesktopView = () => { + const { MainControls, MainPanel, MapWindows } = this.context + return ( +
    + + + + + {/* + Note: the main tag provides a way for users of screen readers + to skip to the primary page content. + TODO: Find a better place. + */} +
    + {} +
    + + {MainControls && } + + {MapWindows && } + + +
    +
    +
    + ) + } + + renderMobileView = () => { + return ( + //
    Needed for accessibility checks. TODO: Find a better place. +
    + +
    + ) + } + render () { - const { desktopView, mobileView } = this.props - return isMobile() ? mobileView : desktopView + return isMobile() ? this.renderMobileView() : this.renderDesktopView() } } diff --git a/lib/components/mobile/main.js b/lib/components/mobile/main.js index 03463e90b..dc9eb2344 100644 --- a/lib/components/mobile/main.js +++ b/lib/components/mobile/main.js @@ -2,19 +2,17 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import BatchSearchScreen from './batch-search-screen' import MobileDateTimeScreen from './date-time-screen' import MobileOptionsScreen from './options-screen' import MobileLocationSearch from './location-search' import MobileWelcomeScreen from './welcome-screen' import MobileResultsScreen from './results-screen' -import MobileSearchScreen from './search-screen' import MobileStopViewer from './stop-viewer' import MobileTripViewer from './trip-viewer' import MobileRouteViewer from './route-viewer' import { MobileScreens, MainPanelContent, setMobileScreen } from '../../actions/ui' -import { isBatchRoutingEnabled } from '../../util/itinerary' +import { ComponentContext } from '../../util/contexts' import { getActiveItinerary } from '../../util/state' class MobileMain extends Component { @@ -26,6 +24,8 @@ class MobileMain extends Component { uiState: PropTypes.object } + static contextType = ComponentContext + componentDidUpdate (prevProps) { // Check if we are in the welcome screen and both locations have been set OR // auto-detect is denied and one location is set @@ -45,7 +45,8 @@ class MobileMain extends Component { } render () { - const { config, map, title, uiState } = this.props + const { MobileSearchScreen } = this.context + const { uiState } = this.props // check for route viewer if (uiState.mainPanelContent === MainPanelContent.ROUTE_VIEWER) { @@ -60,7 +61,7 @@ class MobileMain extends Component { switch (uiState.mobileScreen) { case MobileScreens.WELCOME_SCREEN: - return + return case MobileScreens.SET_INITIAL_LOCATION: return ( @@ -73,14 +74,8 @@ class MobileMain extends Component { case MobileScreens.SEARCH_FORM: // Render batch search screen if batch routing enabled. Otherwise, // default to standard search screen. - const SearchScreen = isBatchRoutingEnabled(config) - ? BatchSearchScreen - : MobileSearchScreen return ( - + ) case MobileScreens.SET_FROM_LOCATION: @@ -106,9 +101,7 @@ class MobileMain extends Component { return case MobileScreens.RESULTS_SUMMARY: - return ( - - ) + return default: return

    Invalid mobile screen

    } diff --git a/lib/components/mobile/navigation-bar.js b/lib/components/mobile/navigation-bar.js index cac315e1c..41d4e274e 100644 --- a/lib/components/mobile/navigation-bar.js +++ b/lib/components/mobile/navigation-bar.js @@ -8,6 +8,7 @@ import { setMobileScreen } from '../../actions/ui' import AppMenu from '../app/app-menu' import NavLoginButtonAuth0 from '../../components/user/nav-login-button-auth0' import { accountLinks, getAuth0Config } from '../../util/auth' +import { ComponentContext } from '../../util/contexts' class MobileNavigationBar extends Component { static propTypes = { @@ -15,10 +16,11 @@ class MobileNavigationBar extends Component { headerAction: PropTypes.element, headerText: PropTypes.string, showBackButton: PropTypes.bool, - setMobileScreen: PropTypes.func, - title: PropTypes.element + setMobileScreen: PropTypes.func } + static contextType = ComponentContext + _backButtonPressed = () => { const { backScreen, onBackClicked } = this.props if (backScreen) this.props.setMobileScreen(this.props.backScreen) @@ -26,12 +28,12 @@ class MobileNavigationBar extends Component { } render () { + const { defaultMobileTitle } = this.context const { auth0Config, headerAction, headerText, - showBackButton, - title + showBackButton } = this.props return ( @@ -49,7 +51,7 @@ class MobileNavigationBar extends Component {
    {headerText ?
    {headerText}
    - :
    {title}
    + :
    {defaultMobileTitle}
    }
    diff --git a/lib/components/mobile/results-screen.js b/lib/components/mobile/results-screen.js index 4fe96e7d8..93bb9bb61 100644 --- a/lib/components/mobile/results-screen.js +++ b/lib/components/mobile/results-screen.js @@ -6,7 +6,7 @@ import { Button, Col, Row } from 'react-bootstrap' import { connect } from 'react-redux' import styled from 'styled-components' -import DefaultMap from '../map/default-map' +import Map from '../map/map' import ErrorMessage from '../form/error-message' import ItineraryCarousel from '../narrative/itinerary-carousel' @@ -55,10 +55,8 @@ const StyledLocationIcon = styled(LocationIcon)` class MobileResultsScreen extends Component { static propTypes = { activeItineraryIndex: PropTypes.number, - map: PropTypes.element, query: PropTypes.object, resultCount: PropTypes.number, - setMobileScreen: PropTypes.func } @@ -97,46 +95,53 @@ class MobileResultsScreen extends Component { this._setExpanded(!this.state.expanded) } - _toggleRealtime = () => this.props.setUseRealtimeResponse({useRealtime: !this.props.useRealtime}) + _toggleRealtime = () => this.props.setUseRealtimeResponse( + {useRealtime: !this.props.useRealtime} + ) - render () { - const { - activeItineraryIndex, - error, - query, - realtimeEffects, - resultCount, - useRealtime - } = this.props - const { expanded } = this.state + renderDots = () => { + const { activeItineraryIndex, resultCount } = this.props - const narrativeContainerStyle = expanded - ? { top: 140, overflowY: 'auto' } - : { height: 80, overflowY: 'hidden' } + // Construct the 'dots' + const dots = [] + for (let i = 0; i < resultCount; i++) { + dots.push( +
    + ) + } - // Ensure that narrative covers map. - narrativeContainerStyle.backgroundColor = 'white' + return ( +
    {dots}
    + ) + } - let headerAction = null - const showRealtimeAnnotation = realtimeEffects.isAffectedByRealtimeData && ( - realtimeEffects.exceedsThreshold || - realtimeEffects.routesDiffer || - !useRealtime + renderError = () => { + const { error } = this.props + + return ( + + + {this.renderLocationsSummary()} +
    +
    + +
    + +
    +
    +
    ) + } - /* Old navbar alert - if (showRealtimeAnnotation) { - headerAction = ( - - ) - */ + renderLocationsSummary = () => { + const { query } = this.props - const locationsSummary = ( + return ( @@ -156,29 +161,34 @@ class MobileResultsScreen extends Component { ) + } - if (error) { - return ( - - - {locationsSummary} -
    -
    - -
    - -
    -
    -
    - ) - } + render () { + const { + activeItineraryIndex, + error, + realtimeEffects, + resultCount, + useRealtime + } = this.props + const { expanded } = this.state - // Construct the 'dots' - const dots = [] - for (let i = 0; i < resultCount; i++) { - dots.push(
    ) + const narrativeContainerStyle = expanded + ? { top: 140, overflowY: 'auto' } + : { height: 80, overflowY: 'hidden' } + + // Ensure that narrative covers map. + narrativeContainerStyle.backgroundColor = 'white' + + let headerAction = null + const showRealtimeAnnotation = realtimeEffects.isAffectedByRealtimeData && ( + realtimeEffects.exceedsThreshold || + realtimeEffects.routesDiffer || + !useRealtime + ) + + if (error) { + return this.renderError() } return ( @@ -190,10 +200,10 @@ class MobileResultsScreen extends Component { } headerAction={headerAction} /> - {locationsSummary} + {this.renderLocationsSummary()}
    - {this.props.map} +
    - -
    {dots}
    + {this.renderDots()} ) } diff --git a/lib/components/mobile/welcome-screen.js b/lib/components/mobile/welcome-screen.js index d31712c05..31ced700b 100644 --- a/lib/components/mobile/welcome-screen.js +++ b/lib/components/mobile/welcome-screen.js @@ -12,8 +12,6 @@ import { setLocationToCurrent } from '../../actions/map' class MobileWelcomeScreen extends Component { static propTypes = { - map: PropTypes.element, - setLocationToCurrent: PropTypes.func, setMobileScreen: PropTypes.func } @@ -36,10 +34,9 @@ class MobileWelcomeScreen extends Component { } render () { - const { title } = this.props return ( - +
    ({ ...l, type: 'suggested' }))) @@ -243,8 +243,8 @@ export function getInitialState (userDefinedConfig, initialQuery) { } } -function createOtpReducer (config, initialQuery) { - const initialState = getInitialState(config, initialQuery) +function createOtpReducer (config) { + const initialState = getInitialState(config) // validate the initial state validateInitialState(initialState) From 56336abc4970933a33e615233243084776049018 Mon Sep 17 00:00:00 2001 From: Evan Siroky Date: Tue, 9 Mar 2021 18:03:39 -0800 Subject: [PATCH 211/265] fix: make expanded itinerary body show up in ItineraryCarousel --- .../narrative/itinerary-carousel.js | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/components/narrative/itinerary-carousel.js b/lib/components/narrative/itinerary-carousel.js index 631475db7..912126d88 100644 --- a/lib/components/narrative/itinerary-carousel.js +++ b/lib/components/narrative/itinerary-carousel.js @@ -86,17 +86,21 @@ class ItineraryCarousel extends Component { index={activeItinerary} onChangeIndex={this._onSwipe} > - {itineraries.map((itinerary, index) => ( - - ))} + {itineraries.map((itinerary, index) => { + const active = index === activeItinerary + return ( + + ) + })}
    ) From a217bc79739f6100008c2c332e9000ecd67661fc Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Mon, 22 Feb 2021 10:32:19 -0500 Subject: [PATCH 212/265] refactor(batch-routing): fix lint, fully remove itin filter --- .../viewers/__snapshots__/stop-viewer.js.snap | 16 +++++++------- .../__snapshots__/create-otp-reducer.js.snap | 1 - .../narrative/narrative-itineraries.js | 22 ++++++++----------- lib/components/viewers/stop-time-cell.js | 2 +- lib/reducers/create-otp-reducer.js | 5 ++--- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap index 1de873e4f..47cd8838d 100644 --- a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap +++ b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap @@ -874,7 +874,7 @@ exports[`components > viewers > stop viewer should render countdown times after >
    viewers > stop viewer should render countdown times for st >
    viewers > stop viewer should render times after midnight w >
    viewers > stop viewer should render with OTP transit index >
    viewers > stop viewer should render with OTP transit index >
    viewers > stop viewer should render with OTP transit index >
    viewers > stop viewer should render with OTP transit index >
    viewers > stop viewer should render with TriMet transit in >
    { - const {sort, updateItineraryFilter} = this.props - const {value} = evt.target - updateItineraryFilter({filter: value, sort}) - } - _onSortChange = evt => { const {value: type} = evt.target - const {filter, sort, updateItineraryFilter} = this.props - updateItineraryFilter({filter, sort: {...sort, type}}) + const {sort, updateItineraryFilter} = this.props + updateItineraryFilter({sort: {...sort, type}}) } _onSortDirChange = () => { - const {filter, sort, updateItineraryFilter} = this.props + const {sort, updateItineraryFilter} = this.props const direction = sort.direction === 'ASC' ? 'DESC' : 'ASC' - updateItineraryFilter({filter, sort: {...sort, direction}}) + updateItineraryFilter({sort: {...sort, direction}}) } _toggleRealtimeItineraryClick = (e) => { @@ -157,7 +151,10 @@ class NarrativeItineraries extends Component { style={{marginRight: '5px'}}> - @@ -222,7 +219,7 @@ class NarrativeItineraries extends Component { const mapStateToProps = (state, ownProps) => { const activeSearch = getActiveSearch(state.otp) const {modes} = state.otp.config - const {filter, sort} = state.otp.filter + const {sort} = state.otp.filter const pending = activeSearch ? Boolean(activeSearch.pending) : false const itineraries = getActiveItineraries(state.otp) const realtimeEffects = getRealtimeEffects(state.otp) @@ -237,7 +234,6 @@ const mapStateToProps = (state, ownProps) => { activeItinerary: activeSearch && activeSearch.activeItinerary, activeLeg: activeSearch && activeSearch.activeLeg, activeStep: activeSearch && activeSearch.activeStep, - filter, modes, sort, timeFormat: coreUtils.time.getTimeFormat(state.otp.config), diff --git a/lib/components/viewers/stop-time-cell.js b/lib/components/viewers/stop-time-cell.js index 2bf548394..4ff44cefa 100644 --- a/lib/components/viewers/stop-time-cell.js +++ b/lib/components/viewers/stop-time-cell.js @@ -58,7 +58,7 @@ const StopTimeCell = ({ return (
    -
    +
    Date: Wed, 10 Mar 2021 15:20:13 -0500 Subject: [PATCH 213/265] refactor(batch-settings): remove unused import --- lib/components/form/batch-settings.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/components/form/batch-settings.js b/lib/components/form/batch-settings.js index 818eadbc7..88deaadac 100644 --- a/lib/components/form/batch-settings.js +++ b/lib/components/form/batch-settings.js @@ -8,7 +8,6 @@ import * as formActions from '../../actions/form' import BatchPreferences from './batch-preferences' import DateTimeModal from './date-time-modal' import ModeButtons, {MODE_OPTIONS, StyledModeButton} from './mode-buttons' -// import UserSettings from '../form/user-settings' import Icon from '../narrative/icon' import { BatchPreferencesContainer, From 50c031dcf0c376e345299e93311572be9728db46 Mon Sep 17 00:00:00 2001 From: Rob Gregg Date: Thu, 11 Mar 2021 15:15:33 +0000 Subject: [PATCH 214/265] fix(narrative-itineraries.js, default-itinerary.js): Added setActiveLeg as a prop to ItineraryBody c Both files were not passing setActiveLeg as a prop so I imported where necessary and passed in the prop. fix #338 --- lib/components/narrative/default/default-itinerary.js | 4 +++- lib/components/narrative/narrative-itineraries.js | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 36170108e..bd72789bc 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -6,6 +6,7 @@ import ItineraryBody from '../line-itin/connected-itinerary-body' import ItinerarySummary from './itinerary-summary' import SimpleRealtimeAnnotation from '../simple-realtime-annotation' import { getTotalFareAsString } from '../../../util/state' +import { setActiveLeg } from "../../../actions/narrative"; const { isBicycle, isTransit } = coreUtils.itinerary const { formatDuration, formatTime } = coreUtils.time @@ -121,6 +122,7 @@ export default class DefaultItinerary extends NarrativeItinerary { expanded, itinerary, LegIcon, + setActiveLeg, showRealtimeAnnotation, timeFormat } = this.props @@ -184,7 +186,7 @@ export default class DefaultItinerary extends NarrativeItinerary { {(active && expanded) && <> {showRealtimeAnnotation && } - + }
    diff --git a/lib/components/narrative/narrative-itineraries.js b/lib/components/narrative/narrative-itineraries.js index eb8585e9a..7867b7265 100644 --- a/lib/components/narrative/narrative-itineraries.js +++ b/lib/components/narrative/narrative-itineraries.js @@ -15,11 +15,12 @@ import { import Icon from '../narrative/icon' import { ComponentContext } from '../../util/contexts' import { - getResponsesWithErrors, getActiveItineraries, getActiveSearch, - getRealtimeEffects + getRealtimeEffects, + getResponsesWithErrors } from '../../util/state' + import SaveTripButton from './save-trip-button' // TODO: move to utils? @@ -201,6 +202,7 @@ class NarrativeItineraries extends Component { itinerary={itinerary} key={index} LegIcon={LegIcon} + setActiveLeg={setActiveLeg} onClick={active ? this._toggleDetailedItinerary : undefined} routingType='ITINERARY' showRealtimeAnnotation={showRealtimeAnnotation} From a8137c4816f79176caca5aeb1196f71495b62cef Mon Sep 17 00:00:00 2001 From: Rob Gregg Date: Thu, 11 Mar 2021 17:19:11 +0000 Subject: [PATCH 215/265] fix(default-itinerary.js): Removed some unused code. The previous commit had a line of unused code. 338 --- lib/components/narrative/default/default-itinerary.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index bd72789bc..abd1eac24 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -6,7 +6,6 @@ import ItineraryBody from '../line-itin/connected-itinerary-body' import ItinerarySummary from './itinerary-summary' import SimpleRealtimeAnnotation from '../simple-realtime-annotation' import { getTotalFareAsString } from '../../../util/state' -import { setActiveLeg } from "../../../actions/narrative"; const { isBicycle, isTransit } = coreUtils.itinerary const { formatDuration, formatTime } = coreUtils.time From e802be61940f6d6454d6c6291b61e310bdf9ae21 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 12 Mar 2021 10:27:17 -0500 Subject: [PATCH 216/265] refactor(batch results): Mention component is required, fix typo. --- example.js | 1 + lib/components/mobile/batch-results-screen.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/example.js b/example.js index 8bc805c1b..57355139d 100644 --- a/example.js +++ b/example.js @@ -60,6 +60,7 @@ if (useCustomIcons) { // - MainControls (optional) // - MainPanel (required) // - MapWindows (optional) +// - MobileResultsScreen (required) // - MobileSearchScreen (required) // - ModeIcon (required) const components = { diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index 2067894ca..20cb9fa52 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -86,8 +86,8 @@ class BatchMobileResultsScreen extends Component { {!error && (mapExpanded - // Set up two separate renderings of the map according mapExpanded, - // so that it is properly sized and itineraries fit under either conditions. + // Set up two separate renderings of the map according to mapExpanded, + // so that the map is properly sized and itineraries fit under either conditions. // (Otherwise, if just the narrative is added/removed, the map doesn't resize properly.) ? this.renderMap() : ( From 7c1513e34b29b6548a7793176b9f44a10b195385 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 12 Mar 2021 11:10:02 -0500 Subject: [PATCH 217/265] refactor(example.js): Tweak comments --- example.js | 6 ++++-- lib/components/app/app-frame.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/example.js b/example.js index 51e8576ed..9c42dafe0 100644 --- a/example.js +++ b/example.js @@ -50,6 +50,8 @@ if (useCustomIcons) { } // Stubs for terms of service/storage for development purposes only. +// They are required if otpConfig.persistence.strategy === 'otp_middleware' +// (otherwise, a "Content not found" box will be shown). // These components should be placed in their own files with appropriate content. const TermsOfService = () => ( <> @@ -75,8 +77,8 @@ const TermsOfStorage = () => ( // - MapWindows (optional) // - MobileSearchScreen (required) // - ModeIcon (required) -// - PrivacyPolicy (optional) -// - TermsAndConditions (optional) +// - TermsOfService (required if otpConfig.persistence.strategy === 'otp_middleware') +// - TermsOfStorage (required if otpConfig.persistence.strategy === 'otp_middleware') const components = { defaultMobileTitle: () =>
    OpenTripPlanner
    , ItineraryBody: DefaultItinerary, diff --git a/lib/components/app/app-frame.js b/lib/components/app/app-frame.js index a1719ba8a..30aa4776f 100644 --- a/lib/components/app/app-frame.js +++ b/lib/components/app/app-frame.js @@ -25,7 +25,7 @@ const AppFrame = ({ children, SubNav }) => ( /** * Creates a simple wrapper component consisting of an AppFrame that surrounds - * the provided component. (Displays "no content" if none provided.) + * the provided component. (Displays "Content not found" if none provided.) */ export function frame (Component) { return () => ( From 94247f8fadc5ab8fd694d054ef4a78d0e5dccdb1 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 12 Mar 2021 13:37:29 -0500 Subject: [PATCH 218/265] refactor(field-trip): add search by request date --- lib/util/call-taker.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/util/call-taker.js b/lib/util/call-taker.js index d4717b623..f7138f281 100644 --- a/lib/util/call-taker.js +++ b/lib/util/call-taker.js @@ -136,6 +136,16 @@ export function getVisibleRequests (state) { // values that match every term. const searchTerms = search.toLowerCase().split(' ') includedInSearch = searchTerms.every(term => { + const splitBySlash = term.split('/') + if (splitBySlash.length > 1) { + // Potential date format detected in search term. + const [month, day] = splitBySlash + if (!isNaN(month) && !isNaN(day)) { + // If month and day seem to be numbers, check against request date. + const date = moment(request.travelDate) + return date.month() + 1 === +month && date.date() === +day + } + } return SEARCH_FIELDS.some(key => { const value = (request[key] || '').toLowerCase() let hasMatch = false From 8d500a2a4437843d3ebd6979469b1d7d49de28bd Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 19 Mar 2021 15:35:57 -0400 Subject: [PATCH 219/265] fix(auto-plan): clarify strategies for auto-plan --- example-config.yml | 9 +++ lib/actions/form.js | 109 +++++++++++++++++++++-------- lib/reducers/create-otp-reducer.js | 5 +- 3 files changed, 92 insertions(+), 31 deletions(-) diff --git a/example-config.yml b/example-config.yml index f9eb30667..6bd3c2640 100644 --- a/example-config.yml +++ b/example-config.yml @@ -15,6 +15,15 @@ api: # lon: -122.71607145667079 # name: Oregon Zoo, Portland, OR +### Define the strategies for how to handle auto-planning of a new trip when +### different query parameters are changes in the form. The default config is +### shown below, but if autoPlan is set to false, auto-plan will never occur. +### Other strategies besides those shown below are: ANY (any changed param will +### cause a re-plan). +# autoPlan: +# mobile: BOTH_LOCATIONS_CHANGED +# default: ONE_LOCATION_CHANGED + ### The persistence setting is used to enable the storage of places (home, work), ### recent searches/places, user overrides, and favorite stops. ### Pick the strategy that best suits your needs. diff --git a/lib/actions/form.js b/lib/actions/form.js index 0e2688b74..e08ed6cd6 100644 --- a/lib/actions/form.js +++ b/lib/actions/form.js @@ -78,48 +78,50 @@ export function parseUrlQueryString (params = getUrlParams()) { let debouncedPlanTrip // store as variable here, so it can be reused. let lastDebouncePlanTimeMs +/** + * This action is dispatched when a change between the old query and new query + * is detected. It handles checks for whether the trip should be replanned + * (based on autoPlan strategies) as well as updating the UI state (esp. for + * mobile). + */ export function formChanged (oldQuery, newQuery) { return function (dispatch, getState) { const otpState = getState().otp + const { config, currentQuery, ui } = otpState + const { autoPlan, debouncePlanTimeMs } = config const isMobile = coreUtils.ui.isMobile() - + const { + fromChanged, + oneLocationChanged, + shouldReplanTrip, + toChanged + } = checkShouldReplanTrip(autoPlan, isMobile, oldQuery, newQuery) // If departArrive is set to 'NOW', update the query time to current - if (otpState.currentQuery && otpState.currentQuery.departArrive === 'NOW') { - dispatch(settingQueryParam({ time: moment().format(coreUtils.time.OTP_API_TIME_FORMAT) })) + if (currentQuery.departArrive === 'NOW') { + const now = moment().format(coreUtils.time.OTP_API_TIME_FORMAT) + dispatch(settingQueryParam({ time: now })) } - - // Determine if either from/to location has changed - const fromChanged = !isEqual(oldQuery.from, newQuery.from) - const toChanged = !isEqual(oldQuery.to, newQuery.to) - // Only clear the main panel if a single location changed. This prevents // clearing the panel on load if the app is focused on a stop viewer but a // search query should also be visible. - const oneLocationChanged = (fromChanged && !toChanged) || (!fromChanged && toChanged) if (oneLocationChanged) { dispatch(setMainPanelContent(null)) } - - // Clear the current search and return to search screen on mobile when - // either location changes only if not currently on welcome screen (otherwise - // when the current position is auto-set the screen will change unexpectedly). - if ( - isMobile && - (fromChanged || toChanged) && - otpState.ui.mobileScreen !== MobileScreens.WELCOME_SCREEN - ) { - dispatch(clearActiveSearch()) - dispatch(setMobileScreen(MobileScreens.SEARCH_FORM)) - } - - // Check whether a trip should be auto-replanned - const { autoPlan, debouncePlanTimeMs } = otpState.config - const updatePlan = - autoPlan || - (!isMobile && oneLocationChanged) || // TODO: make autoplan configurable at the parameter level? - (isMobile && fromChanged && toChanged) - if (updatePlan && queryIsValid(otpState)) { // trip plan should be made - // check if debouncing function needs to be (re)created + if (!shouldReplanTrip) { + // If not replanning the trip, clear the current search when either + // location changes. + if (fromChanged || toChanged) { + dispatch(clearActiveSearch()) + // Return to search screen on mobile only if not currently on welcome + // screen (otherwise when the current position is auto-set the screen + // will change unexpectedly). + if (ui.mobileScreen !== MobileScreens.WELCOME_SCREEN) { + dispatch(setMobileScreen(MobileScreens.SEARCH_FORM)) + } + } + } else if (queryIsValid(otpState)) { + // If replanning trip and query is valid, + // check if debouncing function needs to be (re)created. if (!debouncedPlanTrip || lastDebouncePlanTimeMs !== debouncePlanTimeMs) { debouncedPlanTrip = debounce(() => dispatch(routingQuery()), debouncePlanTimeMs) lastDebouncePlanTimeMs = debouncePlanTimeMs @@ -128,3 +130,50 @@ export function formChanged (oldQuery, newQuery) { } } } + +/** + * Check if the trip should be replanned based on the auto plan strategy, + * whether the mobile view is active, and the old/new queries. Response type is + * an object containing various booleans. + */ +export function checkShouldReplanTrip (autoPlan, isMobile, oldQuery, newQuery) { + // Determine if either from/to location has changed + const fromChanged = !isEqual(oldQuery.from, newQuery.from) + const toChanged = !isEqual(oldQuery.to, newQuery.to) + const oneLocationChanged = (fromChanged && !toChanged) || (!fromChanged && toChanged) + // Check whether a trip should be auto-replanned + const strategy = isMobile && autoPlan?.mobile + ? autoPlan?.mobile + : autoPlan?.default + const shouldReplanTrip = evaluateAutoPlanStrategy( + strategy, + fromChanged, + toChanged, + oneLocationChanged + ) + return { + fromChanged, + oneLocationChanged, + shouldReplanTrip, + toChanged + } +} + +/** + * Shorthand method to evaluate auto plan strategy. It is assumed that this is + * being called within the context of the `formChanged` action, so presumably + * some query param has already changed. If further checking of query params is + * needed, additional strategies should be added. + */ +const evaluateAutoPlanStrategy = (strategy, fromChanged, toChanged, oneLocationChanged) => { + switch (strategy) { + case 'ONE_LOCATION_CHANGED': + if (oneLocationChanged) return true + break + case 'BOTH_LOCATIONS_CHANGED': + if (fromChanged && toChanged) return true + break + case 'ANY': return true + default: return false + } +} diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 863b94bbb..517272d63 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -66,7 +66,10 @@ function validateInitialState (initialState) { */ export function getInitialState (userDefinedConfig) { const defaultConfig = { - autoPlan: false, + autoPlan: { + mobile: 'BOTH_LOCATIONS_CHANGED', + default: 'ONE_LOCATION_CHANGED' + }, debouncePlanTimeMs: 0, language: {}, transitOperators: [], From c61a7065c69592bc0ca3965cadf20a42cdc1e3f7 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 19 Mar 2021 15:49:34 -0400 Subject: [PATCH 220/265] test: update snaphot --- __tests__/reducers/__snapshots__/create-otp-reducer.js.snap | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap index 385443409..35365d63f 100644 --- a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap +++ b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap @@ -4,7 +4,10 @@ exports[`lib > reducers > create-otp-reducer should be able to create the initia Object { "activeSearchId": 0, "config": Object { - "autoPlan": false, + "autoPlan": Object { + "default": "ONE_LOCATION_CHANGED", + "mobile": "BOTH_LOCATIONS_CHANGED", + }, "debouncePlanTimeMs": 0, "homeTimezone": "America/Los_Angeles", "language": Object {}, From 102ccad364017089f87614091e3caa3a00a4ae0f Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 19 Mar 2021 15:55:54 -0400 Subject: [PATCH 221/265] build(deps): update can-i-use db to silence warnings --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7920d4b60..cde8b58ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4322,9 +4322,9 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: integrity sha512-8SKJ12AFwG0ReMjPwRH+keFsX/ucw2bi6LC7upeXBvxjgrMqHaTxgYhkRGm+eOwUWvVcqXDgqM7QNlRJMhvXZg== caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000941, caniuse-lite@^1.0.30000980, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001135: - version "1.0.30001135" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001135.tgz#995b1eb94404a3c9a0d7600c113c9bb27f2cd8aa" - integrity sha512-ziNcheTGTHlu9g34EVoHQdIu5g4foc8EsxMGC7Xkokmvw0dqNtX8BS8RgCgFBaAiSp2IdjvBxNdh0ssib28eVQ== + version "1.0.30001203" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001203.tgz" + integrity sha512-/I9tvnzU/PHMH7wBPrfDMSuecDeUKerjCPX7D0xBbaJZPxoT9m+yYxt0zCTkcijCkjTdim3H56Zm0i5Adxch4w== capture-exit@^2.0.0: version "2.0.0" From c10d7ca6b05b877a62cce375389246a2f2d3a5ea Mon Sep 17 00:00:00 2001 From: Rob Gregg Date: Thu, 11 Mar 2021 15:15:33 +0000 Subject: [PATCH 222/265] fix(narrative-itineraries.js, default-itinerary.js): Added setActiveLeg as a prop to ItineraryBody c Both files were not passing setActiveLeg as a prop so I imported where necessary and passed in the prop. fix #338 --- lib/components/narrative/default/default-itinerary.js | 4 +++- lib/components/narrative/narrative-itineraries.js | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 36170108e..bd72789bc 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -6,6 +6,7 @@ import ItineraryBody from '../line-itin/connected-itinerary-body' import ItinerarySummary from './itinerary-summary' import SimpleRealtimeAnnotation from '../simple-realtime-annotation' import { getTotalFareAsString } from '../../../util/state' +import { setActiveLeg } from "../../../actions/narrative"; const { isBicycle, isTransit } = coreUtils.itinerary const { formatDuration, formatTime } = coreUtils.time @@ -121,6 +122,7 @@ export default class DefaultItinerary extends NarrativeItinerary { expanded, itinerary, LegIcon, + setActiveLeg, showRealtimeAnnotation, timeFormat } = this.props @@ -184,7 +186,7 @@ export default class DefaultItinerary extends NarrativeItinerary { {(active && expanded) && <> {showRealtimeAnnotation && } - + }
    diff --git a/lib/components/narrative/narrative-itineraries.js b/lib/components/narrative/narrative-itineraries.js index 219d0aaba..ce5377d3d 100644 --- a/lib/components/narrative/narrative-itineraries.js +++ b/lib/components/narrative/narrative-itineraries.js @@ -15,11 +15,12 @@ import { import Icon from '../narrative/icon' import { ComponentContext } from '../../util/contexts' import { - getResponsesWithErrors, getActiveItineraries, getActiveSearch, - getRealtimeEffects + getRealtimeEffects, + getResponsesWithErrors } from '../../util/state' + import SaveTripButton from './save-trip-button' // TODO: move to utils? @@ -196,6 +197,7 @@ class NarrativeItineraries extends Component { itinerary={itinerary} key={index} LegIcon={LegIcon} + setActiveLeg={setActiveLeg} onClick={active ? this._toggleDetailedItinerary : undefined} routingType='ITINERARY' showRealtimeAnnotation={showRealtimeAnnotation} From edea628d0568bf4d3c6f753241acdf89c5be9edd Mon Sep 17 00:00:00 2001 From: Rob Gregg Date: Thu, 11 Mar 2021 17:19:11 +0000 Subject: [PATCH 223/265] fix(default-itinerary.js): Removed some unused code. The previous commit had a line of unused code. 338 --- lib/components/narrative/default/default-itinerary.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index bd72789bc..abd1eac24 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -6,7 +6,6 @@ import ItineraryBody from '../line-itin/connected-itinerary-body' import ItinerarySummary from './itinerary-summary' import SimpleRealtimeAnnotation from '../simple-realtime-annotation' import { getTotalFareAsString } from '../../../util/state' -import { setActiveLeg } from "../../../actions/narrative"; const { isBicycle, isTransit } = coreUtils.itinerary const { formatDuration, formatTime } = coreUtils.time From 100c1d62160bc53210f50732875aa911ec4ed982 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 23 Mar 2021 12:05:43 -0400 Subject: [PATCH 224/265] fix(BatchResultsScreen): Fix leg focus, address some PR comments. --- lib/components/mobile/batch-results-screen.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index 20cb9fa52..fce24aa04 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -7,7 +7,7 @@ import styled from 'styled-components' import Map from '../map/map' import Icon from '../narrative/icon' import NarrativeItineraries from '../narrative/narrative-itineraries' -import { getActiveError } from '../../util/state' +import { getActiveError, getActiveSearch } from '../../util/state' import MobileContainer from './container' import ResultsHeaderAndError from './results-header-and-error' @@ -28,12 +28,15 @@ const StyledMobileContainer = styled(MobileContainer)` ` const ExpandMapButton = styled(Button)` - bottom: 25px; + bottom: 10px; + left: 10px; position: absolute; - right: 10px; z-index: 999999; ` +/** + * This component renders the mobile view of itinerary results from batch routing. + */ class BatchMobileResultsScreen extends Component { static propTypes = { error: PropTypes.object @@ -48,8 +51,10 @@ class BatchMobileResultsScreen extends Component { } componentDidUpdate (prevProps) { - // Check if the active leg changed if (this.props.activeLeg !== prevProps.activeLeg) { + // Check if the active leg has changed. If a different leg is selected, + // unexpand the itinerary to show the map focused on the selected leg + // (similar to the behavior of LineItinerary). this._setItineraryExpanded(false) } } @@ -71,7 +76,7 @@ class BatchMobileResultsScreen extends Component { bsSize='small' onClick={this._toggleMapExpanded} > - + {' '} {mapExpanded ? 'Show results' : 'Expand map'}
    @@ -115,7 +120,9 @@ class BatchMobileResultsScreen extends Component { // connect to the redux store const mapStateToProps = (state, ownProps) => { + const activeSearch = getActiveSearch(state.otp) return { + activeLeg: activeSearch ? activeSearch.activeLeg : null, error: getActiveError(state.otp) } } From df664e04cae78655b5e26907567b2b81004ebd72 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 23 Mar 2021 15:31:56 -0400 Subject: [PATCH 225/265] refactor(BatchResultsScreen): Show result screen with correct #itins if errors. --- lib/components/mobile/batch-results-screen.js | 22 ++++-------- lib/components/mobile/main.js | 36 +++++++++++++------ .../mobile/results-header-and-error.js | 24 ++++++------- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index fce24aa04..b22fd4b10 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -38,16 +38,9 @@ const ExpandMapButton = styled(Button)` * This component renders the mobile view of itinerary results from batch routing. */ class BatchMobileResultsScreen extends Component { - static propTypes = { - error: PropTypes.object - } - - constructor () { - super() - this.state = { - itineraryExpanded: false, - mapExpanded: false - } + state = { + itineraryExpanded: false, + mapExpanded: false } componentDidUpdate (prevProps) { @@ -84,13 +77,12 @@ class BatchMobileResultsScreen extends Component { } render () { - const { error } = this.props const { itineraryExpanded, mapExpanded } = this.state return ( - {!error && (mapExpanded + {mapExpanded // Set up two separate renderings of the map according to mapExpanded, // so that the map is properly sized and itineraries fit under either conditions. // (Otherwise, if just the narrative is added/removed, the map doesn't resize properly.) @@ -101,6 +93,7 @@ class BatchMobileResultsScreen extends Component {
    - )) + ) }
    ) @@ -122,8 +115,7 @@ class BatchMobileResultsScreen extends Component { const mapStateToProps = (state, ownProps) => { const activeSearch = getActiveSearch(state.otp) return { - activeLeg: activeSearch ? activeSearch.activeLeg : null, - error: getActiveError(state.otp) + activeLeg: activeSearch ? activeSearch.activeLeg : null } } diff --git a/lib/components/mobile/main.js b/lib/components/mobile/main.js index db28eafd4..805615a41 100644 --- a/lib/components/mobile/main.js +++ b/lib/components/mobile/main.js @@ -12,11 +12,13 @@ import MobileRouteViewer from './route-viewer' import { MobileScreens, MainPanelContent, setMobileScreen } from '../../actions/ui' import { ComponentContext } from '../../util/contexts' -import { getActiveItinerary } from '../../util/state' +import { getActiveItinerary, getResponsesWithErrors } from '../../util/state' class MobileMain extends Component { static propTypes = { + currentPosition: PropTypes.object, currentQuery: PropTypes.object, + errors: PropTypes.array, map: PropTypes.element, setMobileScreen: PropTypes.func, title: PropTypes.element, @@ -26,20 +28,30 @@ class MobileMain extends Component { static contextType = ComponentContext componentDidUpdate (prevProps) { + const { + activeItinerary, + currentPosition, + currentQuery, + errors, + setMobileScreen + } = this.props + // Check if we are in the welcome screen and both locations have been set OR // auto-detect is denied and one location is set if ( prevProps.uiState.mobileScreen === MobileScreens.WELCOME_SCREEN && ( - (this.props.currentQuery.from && this.props.currentQuery.to) || - (!this.props.currentPosition.coords && (this.props.currentQuery.from || this.props.currentQuery.to)) + (currentQuery.from && currentQuery.to) || + (!currentPosition.coords && (currentQuery.from || currentQuery.to)) ) ) { // If so, advance to main search screen - this.props.setMobileScreen(MobileScreens.SEARCH_FORM) + setMobileScreen(MobileScreens.SEARCH_FORM) } - if (!prevProps.activeItinerary && this.props.activeItinerary) { - this.props.setMobileScreen(MobileScreens.RESULTS_SUMMARY) + // Display the results screen if results have been returned + // (and an active itinerary has been set) or if there are errors. + if (!prevProps.activeItinerary && (activeItinerary || errors.length > 0)) { + setMobileScreen(MobileScreens.RESULTS_SUMMARY) } } @@ -110,12 +122,14 @@ class MobileMain extends Component { // connect to the redux store const mapStateToProps = (state, ownProps) => { + const { config, currentQuery, location, ui } = state.otp return { - config: state.otp.config, - uiState: state.otp.ui, - currentQuery: state.otp.currentQuery, - currentPosition: state.otp.location.currentPosition, - activeItinerary: getActiveItinerary(state.otp) + activeItinerary: getActiveItinerary(state.otp), + config, + currentPosition: location.currentPosition, + currentQuery, + errors: getResponsesWithErrors(state.otp), + uiState: ui } } diff --git a/lib/components/mobile/results-header-and-error.js b/lib/components/mobile/results-header-and-error.js index da6b4c78a..2a50db2c2 100644 --- a/lib/components/mobile/results-header-and-error.js +++ b/lib/components/mobile/results-header-and-error.js @@ -12,7 +12,8 @@ import Map from '../map/map' import { getActiveError, getActiveItineraries, - getActiveSearch + getActiveSearch, + getResponsesWithErrors } from '../../util/state' import MobileNavigationBar from './navigation-bar' @@ -48,6 +49,7 @@ const StyledLocationIcon = styled(LocationIcon)` class ResultsHeaderAndError extends Component { static propTypes = { + errors: PropTypes.array, query: PropTypes.object, resultCount: PropTypes.number, setMobileScreen: PropTypes.func @@ -59,8 +61,9 @@ class ResultsHeaderAndError extends Component { } render () { - const { error, query, resultCount } = this.props - const headerText = error + const { errors, query, resultCount } = this.props + const hasNoResult = resultCount === 0 && errors.length > 0 + const headerText = hasNoResult ? 'No Trip Found' : (resultCount ? `We Found ${resultCount} Option${resultCount > 1 ? 's' : ''}` @@ -90,11 +93,11 @@ class ResultsHeaderAndError extends Component { - {error && ( + {hasNoResult && ( <>
    - +
    +
    +
    + ) + } +} + +// connect to the redux store + +const mapStateToProps = (state, ownProps) => { + return {} +} + +const mapDispatchToProps = { + clearActiveSearch: formActions.clearActiveSearch, + setMobileScreen: uiActions.setMobileScreen +} + +export default connect(mapStateToProps, mapDispatchToProps)(ResultsError) diff --git a/lib/components/mobile/results-header-and-error.js b/lib/components/mobile/results-header.js similarity index 80% rename from lib/components/mobile/results-header-and-error.js rename to lib/components/mobile/results-header.js index 2a50db2c2..9c498da3d 100644 --- a/lib/components/mobile/results-header-and-error.js +++ b/lib/components/mobile/results-header.js @@ -7,10 +7,7 @@ import styled from 'styled-components' import * as formActions from '../../actions/form' import * as uiActions from '../../actions/ui' -import ErrorMessage from '../form/error-message' -import Map from '../map/map' import { - getActiveError, getActiveItineraries, getActiveSearch, getResponsesWithErrors @@ -47,7 +44,11 @@ const StyledLocationIcon = styled(LocationIcon)` margin: 3px; ` -class ResultsHeaderAndError extends Component { +/** + * This component renders the results header and an error message + * if no itinerary was found. + */ +class ResultsHeader extends Component { static propTypes = { errors: PropTypes.array, query: PropTypes.object, @@ -92,24 +93,6 @@ class ResultsHeaderAndError extends Component { - - {hasNoResult && ( - <> -
    -
    - -
    - -
    -
    - - )} ) } @@ -139,4 +122,4 @@ const mapDispatchToProps = { setMobileScreen: uiActions.setMobileScreen } -export default connect(mapStateToProps, mapDispatchToProps)(ResultsHeaderAndError) +export default connect(mapStateToProps, mapDispatchToProps)(ResultsHeader) diff --git a/lib/components/mobile/results-screen.js b/lib/components/mobile/results-screen.js index b65db3215..9ff918b83 100644 --- a/lib/components/mobile/results-screen.js +++ b/lib/components/mobile/results-screen.js @@ -14,7 +14,8 @@ import { } from '../../util/state' import MobileContainer from './container' -import ResultsHeaderAndError from './results-header-and-error' +import ResultsError from './results-error' +import ResultsHeader from './results-header' class MobileResultsScreen extends Component { static propTypes = { @@ -104,37 +105,39 @@ class MobileResultsScreen extends Component { return ( - - {!error && ( - <> -
    - -
    - -
    - Option {activeItineraryIndex + 1} - -
    - -
    - -
    - {this.renderDots()} - - )} + +
    + +
    + {error + ? + : ( + <> +
    + Option {activeItineraryIndex + 1} + +
    + +
    + +
    + {this.renderDots()} + + ) + }
    ) } From d615fd8acd12952e040676e53ec2e10013013b01 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 23 Mar 2021 17:49:05 -0400 Subject: [PATCH 228/265] style(ResultsScreen): Commit indents --- lib/components/mobile/batch-results-screen.js | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index 80b377730..f239b6f3a 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types' import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' @@ -11,7 +10,7 @@ import { getActiveItineraries, getActiveSearch, getResponsesWithErrors - } from '../../util/state' +} from '../../util/state' import MobileContainer from './container' import ResultsError from './results-error' @@ -100,19 +99,19 @@ class BatchMobileResultsScreen extends Component { {hasNoResult ? : ( -
    - -
    - ) +
    + +
    + ) } ) From 22beb2cc5596033f8e308ae9e54271aa51103daa Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 23 Mar 2021 17:56:39 -0400 Subject: [PATCH 229/265] style(mobile/main): Fix indent --- lib/components/mobile/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/components/mobile/main.js b/lib/components/mobile/main.js index fb8bf5278..4027f295e 100644 --- a/lib/components/mobile/main.js +++ b/lib/components/mobile/main.js @@ -14,7 +14,7 @@ import * as uiActions from '../../actions/ui' import { ComponentContext } from '../../util/contexts' import { getActiveSearch } from '../../util/state' -const { MobileScreens, MainPanelContent } = uiActions +const { MainPanelContent, MobileScreens } = uiActions class MobileMain extends Component { static propTypes = { @@ -33,7 +33,7 @@ class MobileMain extends Component { currentPosition, currentQuery, setMobileScreen - } = this.props + } = this.props // Check if we are in the welcome screen and both locations have been set OR // auto-detect is denied and one location is set From 15329feb4bfef8f2aa9e75faa63ba87b0e39d55b Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 24 Mar 2021 12:28:37 -0400 Subject: [PATCH 230/265] refactor(BoundsUpdatingOverlay): Remove fit bounds mobile restriction. --- lib/components/map/bounds-updating-overlay.js | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/lib/components/map/bounds-updating-overlay.js b/lib/components/map/bounds-updating-overlay.js index 230ae68c8..72e8695ad 100644 --- a/lib/components/map/bounds-updating-overlay.js +++ b/lib/components/map/bounds-updating-overlay.js @@ -87,29 +87,18 @@ class BoundsUpdatingOverlay extends MapLayer { // If no itinerary update but from/to locations are present, fit to those } else if (newFrom && newTo && (fromChanged || toChanged)) { - // On certain mobile devices (e.g., Android + Chrome), setting from and to - // locations via the location search component causes issues for this - // fitBounds invocation. The map does not appear to be visible when these - // prop changes are detected, so for now we should perhaps just skip this - // fitBounds on mobile. - // See https://github.com/opentripplanner/otp-react-redux/issues/133 for - // more info. - // TODO: Fix this so mobile devices will also update the bounds to the - // from/to locations. - if (!coreUtils.ui.isMobile()) { - const bounds = L.bounds([ - [newFrom.lat, newFrom.lon], - [newTo.lat, newTo.lon] - ]) - // Ensure bounds extend to include intermediatePlaces - extendBoundsByPlaces(bounds, newIntermediate) - const { x: left, y: bottom } = bounds.getBottomLeft() - const { x: right, y: top } = bounds.getTopRight() - map.fitBounds([ - [left, bottom], - [right, top] - ], { padding }) - } + const bounds = L.bounds([ + [newFrom.lat, newFrom.lon], + [newTo.lat, newTo.lon] + ]) + // Ensure bounds extend to include intermediatePlaces + extendBoundsByPlaces(bounds, newIntermediate) + const { x: left, y: bottom } = bounds.getBottomLeft() + const { x: right, y: top } = bounds.getTopRight() + map.fitBounds([ + [left, bottom], + [right, top] + ], { padding }) // If only from or to is set, pan to that } else if (newFrom && fromChanged) { From fc76d3ec47dc0ef419ee8a2e9e990aa8d1a6b1b0 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 24 Mar 2021 16:00:45 -0400 Subject: [PATCH 231/265] refactor(BatchResultsScreen): Investigate imperative resizing. --- lib/components/map/bounds-updating-overlay.js | 21 ++++++ .../map/connected-transitive-overlay.js | 5 ++ lib/components/map/default-map.js | 5 +- lib/components/map/map.js | 7 +- lib/components/mobile/batch-results-screen.js | 64 ++++++++++++++----- 5 files changed, 80 insertions(+), 22 deletions(-) diff --git a/lib/components/map/bounds-updating-overlay.js b/lib/components/map/bounds-updating-overlay.js index 72e8695ad..083547d2e 100644 --- a/lib/components/map/bounds-updating-overlay.js +++ b/lib/components/map/bounds-updating-overlay.js @@ -69,6 +69,10 @@ class BoundsUpdatingOverlay extends MapLayer { const oldIntermediate = oldProps.query && oldProps.query.intermediatePlaces const newIntermediate = newProps.query && newProps.query.intermediatePlaces const intermediateChanged = !isEqual(oldIntermediate, newIntermediate) + + // Also refit map if stateData prop has changed + const stateDataChanged = !isEqual(oldProps.stateData, newProps.stateData) + if ( (!oldItinBounds && newItinBounds) || (oldItinBounds && newItinBounds && !oldItinBounds.equals(newItinBounds)) @@ -122,6 +126,23 @@ class BoundsUpdatingOverlay extends MapLayer { const leg = newProps.itinerary.legs[newProps.activeLeg] const step = leg.steps[newProps.activeStep] map.panTo([step.lat, step.lon]) + } else if (stateDataChanged) { + // If a new stateData prop is passed, + // force a resize of the map, then refit the active itinerary or active leg. + map.invalidateSize({ animate: true, debounceMoveEnd: true }) + + if ( + newProps.itinerary && + newProps.activeLeg !== null + ) { + // Fit to active leg if set. + map.fitBounds( + getLeafletLegBounds(newProps.itinerary.legs[newProps.activeLeg]), + { padding } + ) + } else { + map.fitBounds(newItinBounds, { padding }) + } } } } diff --git a/lib/components/map/connected-transitive-overlay.js b/lib/components/map/connected-transitive-overlay.js index 9b2eed104..fbd5c7cf2 100644 --- a/lib/components/map/connected-transitive-overlay.js +++ b/lib/components/map/connected-transitive-overlay.js @@ -31,6 +31,11 @@ const mapStateToProps = (state, ownProps) => { transitiveData = activeSearch.response.otp } + if (transitiveData) { + // HACK: pass a prop change from map to transitive to force a render. + transitiveData.stateData = ownProps.stateData + } + return { activeItinerary: activeSearch && activeSearch.activeItinerary, routingType: activeSearch && activeSearch.query && activeSearch.query.routingType, diff --git a/lib/components/map/default-map.js b/lib/components/map/default-map.js index 3ce357508..bba144d83 100644 --- a/lib/components/map/default-map.js +++ b/lib/components/map/default-map.js @@ -121,6 +121,7 @@ class DefaultMap extends Component { carRentalStations, mapConfig, mapPopupLocation, + stateData, vehicleRentalQuery, vehicleRentalStations } = this.props @@ -151,11 +152,11 @@ class DefaultMap extends Component { zoom={mapConfig.initZoom || 13} > {/* The default overlays */} - + - + diff --git a/lib/components/map/map.js b/lib/components/map/map.js index c3474437d..2eaf50b52 100644 --- a/lib/components/map/map.js +++ b/lib/components/map/map.js @@ -14,11 +14,12 @@ class Map extends Component { } } - getComponentForView (view) { + getComponentForView = view => { + const { stateData } = this.props // TODO: allow a 'CUSTOM' type switch (view.type) { - case 'DEFAULT': return - case 'STYLIZED': return + case 'DEFAULT': return + case 'STYLIZED': return } } diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index f239b6f3a..e5a9b0056 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -84,37 +84,67 @@ class BatchMobileResultsScreen extends Component { const { errors, itineraries } = this.props const hasNoResult = itineraries.length === 0 && errors.length > 0 const { itineraryExpanded, mapExpanded } = this.state - +/* + const narrative = ( +
    + +
    + ) +*/ return ( - {mapExpanded + {/*mapExpanded // Set up two separate renderings of the map according to mapExpanded, // so that the map is properly sized and itineraries fit under either conditions. // (Otherwise, if just the narrative is added/removed, the map doesn't resize properly.) - ? this.renderMap() + ? <>{this.renderMap()} : ( <> {!itineraryExpanded && this.renderMap()} {hasNoResult ? - : ( -
    - -
    - ) + : narrative } ) + */} + +
    + + + {' '} + {mapExpanded ? 'Show results' : 'Expand map'} + +
    + {hasNoResult + ? + : ( +
    + +
    + ) }
    ) From 2e7757c9365880a36f3bb154f1beaa1fb3d16556 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 24 Mar 2021 16:21:05 -0400 Subject: [PATCH 232/265] refactor(map/default-map): Use key prop to force transitive remount on resize. --- lib/components/map/connected-transitive-overlay.js | 5 ----- lib/components/map/default-map.js | 9 ++++++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/components/map/connected-transitive-overlay.js b/lib/components/map/connected-transitive-overlay.js index fbd5c7cf2..9b2eed104 100644 --- a/lib/components/map/connected-transitive-overlay.js +++ b/lib/components/map/connected-transitive-overlay.js @@ -31,11 +31,6 @@ const mapStateToProps = (state, ownProps) => { transitiveData = activeSearch.response.otp } - if (transitiveData) { - // HACK: pass a prop change from map to transitive to force a render. - transitiveData.stateData = ownProps.stateData - } - return { activeItinerary: activeSearch && activeSearch.activeItinerary, routingType: activeSearch && activeSearch.query && activeSearch.query.routingType, diff --git a/lib/components/map/default-map.js b/lib/components/map/default-map.js index bba144d83..eb13ba0cc 100644 --- a/lib/components/map/default-map.js +++ b/lib/components/map/default-map.js @@ -156,7 +156,14 @@ class DefaultMap extends Component { - + {/* + HACK: Use the key prop to force a remount and full resizing of transitive + if the map container size changes, + per https://linguinecode.com/post/4-methods-to-re-render-react-component + Without it, transitive resolution will not match the map, + and transitive will appear blurry after e.g. the narrative is expanded. + */} + From 557bfc7387d771ce1a21c3c36fe7e0f65a1e28c4 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 24 Mar 2021 17:03:02 -0400 Subject: [PATCH 233/265] refactor(BatchResultsScreen): Refactor, tweak split height. --- lib/components/mobile/batch-results-screen.js | 63 ++++--------------- 1 file changed, 13 insertions(+), 50 deletions(-) diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index e5a9b0056..9e99aa550 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -38,6 +38,8 @@ const ExpandMapButton = styled(Button)` z-index: 999999; ` +const NARRATIVE_SPLIT_TOP_PERCENT = 45 + /** * This component renders the mobile view of itinerary results from batch routing. */ @@ -64,62 +66,20 @@ class BatchMobileResultsScreen extends Component { this.setState({ mapExpanded: !this.state.mapExpanded }) } - renderMap () { - const { mapExpanded } = this.state - return ( -
    - - - {' '} - {mapExpanded ? 'Show results' : 'Expand map'} - -
    - ) - } - render () { const { errors, itineraries } = this.props const hasNoResult = itineraries.length === 0 && errors.length > 0 const { itineraryExpanded, mapExpanded } = this.state -/* - const narrative = ( -
    - -
    - ) -*/ + const narrativeTop = mapExpanded ? '100%' : (itineraryExpanded ? '100px' : `${NARRATIVE_SPLIT_TOP_PERCENT}%`) + const mapBottom = mapExpanded ? 0 : `${100 - NARRATIVE_SPLIT_TOP_PERCENT}%` + return ( - {/*mapExpanded - // Set up two separate renderings of the map according to mapExpanded, - // so that the map is properly sized and itineraries fit under either conditions. - // (Otherwise, if just the narrative is added/removed, the map doesn't resize properly.) - ? <>{this.renderMap()} - : ( - <> - {!itineraryExpanded && this.renderMap()} - {hasNoResult - ? - : narrative - } - - ) - */} - -
    +
    : ( -
    +
    Date: Wed, 24 Mar 2021 18:20:10 -0400 Subject: [PATCH 234/265] refactor(BatchResultScreen): Move itin state to URL param, remove stateData prop. --- lib/actions/ui.js | 24 +++++++++++++-- lib/components/map/bounds-updating-overlay.js | 13 +++++---- lib/components/map/default-map.js | 12 +++++--- lib/components/map/map.js | 5 ++-- lib/components/mobile/batch-results-screen.js | 29 +++++++++++-------- 5 files changed, 57 insertions(+), 26 deletions(-) diff --git a/lib/actions/ui.js b/lib/actions/ui.js index 27cca104a..c24a5a324 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -3,9 +3,14 @@ import coreUtils from '@opentripplanner/core-utils' import { createAction } from 'redux-actions' import { matchPath } from 'react-router' -import { findRoute } from './api' +import { findRoute, setUrlSearch } from './api' import { setMapCenter, setMapZoom, setRouterId } from './config' -import { clearActiveSearch, parseUrlQueryString, setActiveSearch } from './form' +import { + clearActiveSearch, + parseUrlQueryString, + setActiveSearch, + settingQueryParam +} from './form' import { clearLocation } from './map' import { setActiveItinerary } from './narrative' import { getUiUrlParams } from '../util/state' @@ -145,6 +150,21 @@ export function handleBackButtonPress (e) { } } +/** + * Sets the itinerary view state ('full', 'split', or 'hidden') in: + * - the currentQuery otp redux state, + * - in the URL params. + */ +export function setItineraryView (value) { + return function (dispatch, getState) { + dispatch(settingQueryParam({ ui_itineraryView: value })) + + const urlParams = coreUtils.query.getUrlParams() + urlParams.ui_itineraryView = value + dispatch(setUrlSearch(urlParams)) + } +} + export const setMobileScreen = createAction('SET_MOBILE_SCREEN') /** diff --git a/lib/components/map/bounds-updating-overlay.js b/lib/components/map/bounds-updating-overlay.js index 083547d2e..6f2f550ae 100644 --- a/lib/components/map/bounds-updating-overlay.js +++ b/lib/components/map/bounds-updating-overlay.js @@ -70,8 +70,8 @@ class BoundsUpdatingOverlay extends MapLayer { const newIntermediate = newProps.query && newProps.query.intermediatePlaces const intermediateChanged = !isEqual(oldIntermediate, newIntermediate) - // Also refit map if stateData prop has changed - const stateDataChanged = !isEqual(oldProps.stateData, newProps.stateData) + // Also refit map if itineraryView prop has changed. + const itineraryViewChanged = oldProps.itineraryView !== newProps.itineraryView if ( (!oldItinBounds && newItinBounds) || @@ -126,10 +126,10 @@ class BoundsUpdatingOverlay extends MapLayer { const leg = newProps.itinerary.legs[newProps.activeLeg] const step = leg.steps[newProps.activeStep] map.panTo([step.lat, step.lon]) - } else if (stateDataChanged) { - // If a new stateData prop is passed, + } else if (itineraryViewChanged) { + // If itineraryView has changed, // force a resize of the map, then refit the active itinerary or active leg. - map.invalidateSize({ animate: true, debounceMoveEnd: true }) + map.invalidateSize(true) if ( newProps.itinerary && @@ -151,10 +151,13 @@ class BoundsUpdatingOverlay extends MapLayer { const mapStateToProps = (state, ownProps) => { const activeSearch = getActiveSearch(state.otp) + const urlParams = coreUtils.query.getUrlParams() + return { activeLeg: activeSearch && activeSearch.activeLeg, activeStep: activeSearch && activeSearch.activeStep, itinerary: getActiveItinerary(state.otp), + itineraryView: urlParams.ui_itineraryView, popupLocation: state.otp.ui.mapPopupLocation, query: state.otp.currentQuery } diff --git a/lib/components/map/default-map.js b/lib/components/map/default-map.js index eb13ba0cc..9a3157701 100644 --- a/lib/components/map/default-map.js +++ b/lib/components/map/default-map.js @@ -1,4 +1,5 @@ import BaseMap from '@opentripplanner/base-map' +import coreUtils from '@opentripplanner/core-utils' import React, { Component } from 'react' import { connect } from 'react-redux' import styled from 'styled-components' @@ -119,9 +120,9 @@ class DefaultMap extends Component { bikeRentalStations, carRentalQuery, carRentalStations, + itineraryView, mapConfig, mapPopupLocation, - stateData, vehicleRentalQuery, vehicleRentalStations } = this.props @@ -147,12 +148,12 @@ class DefaultMap extends Component { center={center} maxZoom={mapConfig.maxZoom} onClick={this.onMapClick} - popup={popup} onPopupClosed={this.onPopupClosed} + popup={popup} zoom={mapConfig.initZoom || 13} > {/* The default overlays */} - + @@ -163,7 +164,7 @@ class DefaultMap extends Component { Without it, transitive resolution will not match the map, and transitive will appear blurry after e.g. the narrative is expanded. */} - + @@ -214,9 +215,12 @@ const mapStateToProps = (state, ownProps) => { const overlays = state.otp.config.map && state.otp.config.map.overlays ? state.otp.config.map.overlays : [] + const urlParams = coreUtils.query.getUrlParams() + return { bikeRentalStations: state.otp.overlay.bikeRental.stations, carRentalStations: state.otp.overlay.carRental.stations, + itineraryView: urlParams.ui_itineraryView, mapConfig: state.otp.config.map, mapPopupLocation: state.otp.ui.mapPopupLocation, overlays, diff --git a/lib/components/map/map.js b/lib/components/map/map.js index 2eaf50b52..4edf47a0e 100644 --- a/lib/components/map/map.js +++ b/lib/components/map/map.js @@ -15,11 +15,10 @@ class Map extends Component { } getComponentForView = view => { - const { stateData } = this.props // TODO: allow a 'CUSTOM' type switch (view.type) { - case 'DEFAULT': return - case 'STYLIZED': return + case 'DEFAULT': return + case 'STYLIZED': return } } diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index 9e99aa550..592bf06ac 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -1,8 +1,10 @@ +import coreUtils from '@opentripplanner/core-utils' import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' import styled from 'styled-components' +import * as uiActions from '../../actions/ui' import Map from '../map/map' import Icon from '../narrative/icon' import NarrativeItineraries from '../narrative/narrative-itineraries' @@ -44,11 +46,6 @@ const NARRATIVE_SPLIT_TOP_PERCENT = 45 * This component renders the mobile view of itinerary results from batch routing. */ class BatchMobileResultsScreen extends Component { - state = { - itineraryExpanded: false, - mapExpanded: false - } - componentDidUpdate (prevProps) { if (this.props.activeLeg !== prevProps.activeLeg) { // Check if the active leg has changed. If a different leg is selected, @@ -59,17 +56,18 @@ class BatchMobileResultsScreen extends Component { } _setItineraryExpanded = itineraryExpanded => { - this.setState({ itineraryExpanded }) + this.props.setItineraryView(itineraryExpanded ? 'full' : 'split') } _toggleMapExpanded = () => { - this.setState({ mapExpanded: !this.state.mapExpanded }) + this.props.setItineraryView(this.props.itineraryView === 'hidden' ? 'split' : 'hidden') } render () { - const { errors, itineraries } = this.props + const { errors, itineraries, itineraryView } = this.props const hasNoResult = itineraries.length === 0 && errors.length > 0 - const { itineraryExpanded, mapExpanded } = this.state + const mapExpanded = itineraryView === 'hidden' + const itineraryExpanded = itineraryView === 'full' const narrativeTop = mapExpanded ? '100%' : (itineraryExpanded ? '100px' : `${NARRATIVE_SPLIT_TOP_PERCENT}%`) const mapBottom = mapExpanded ? 0 : `${100 - NARRATIVE_SPLIT_TOP_PERCENT}%` @@ -80,7 +78,7 @@ class BatchMobileResultsScreen extends Component { className='results-map' style={{bottom: mapBottom, display: itineraryExpanded ? 'none' : 'inherit'}} > - + { const activeSearch = getActiveSearch(state.otp) + const urlParams = coreUtils.query.getUrlParams() + return { activeLeg: activeSearch ? activeSearch.activeLeg : null, errors: getResponsesWithErrors(state.otp), - itineraries: getActiveItineraries(state.otp) + itineraries: getActiveItineraries(state.otp), + itineraryView: urlParams.ui_itineraryView } } -export default connect(mapStateToProps)(BatchMobileResultsScreen) +const mapDispatchToProps = { + setItineraryView: uiActions.setItineraryView +} + +export default connect(mapStateToProps, mapDispatchToProps)(BatchMobileResultsScreen) From 9e71d59fefc84fcd2ec47dd83929dfcefd63353c Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 24 Mar 2021 20:03:16 -0400 Subject: [PATCH 235/265] refactor(mobile/BatchResultsScreen): Tweak layout for no-results. --- lib/components/map/bounds-updating-overlay.js | 24 +++++++++---------- lib/components/mobile/batch-results-screen.js | 8 ++++++- lib/components/mobile/results-error.js | 4 ++-- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/components/map/bounds-updating-overlay.js b/lib/components/map/bounds-updating-overlay.js index 6f2f550ae..48afe3f61 100644 --- a/lib/components/map/bounds-updating-overlay.js +++ b/lib/components/map/bounds-updating-overlay.js @@ -128,20 +128,20 @@ class BoundsUpdatingOverlay extends MapLayer { map.panTo([step.lat, step.lon]) } else if (itineraryViewChanged) { // If itineraryView has changed, - // force a resize of the map, then refit the active itinerary or active leg. + // force a resize of the map before re-fitting the active itinerary or active leg. map.invalidateSize(true) - if ( - newProps.itinerary && - newProps.activeLeg !== null - ) { - // Fit to active leg if set. - map.fitBounds( - getLeafletLegBounds(newProps.itinerary.legs[newProps.activeLeg]), - { padding } - ) - } else { - map.fitBounds(newItinBounds, { padding }) + if (newProps.itinerary) { + if (newProps.activeLeg !== null) { + // Fit to active leg if set. + map.fitBounds( + getLeafletLegBounds(newProps.itinerary.legs[newProps.activeLeg]), + { padding } + ) + } else { + // Fit to whole itinerary otherwise. + map.fitBounds(newItinBounds, { padding }) + } } } } diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index 592bf06ac..ff655e57e 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -88,7 +88,13 @@ class BatchMobileResultsScreen extends Component {
    {hasNoResult - ? + ? : (
    +
    ) diff --git a/lib/components/narrative/narrative-itineraries.js b/lib/components/narrative/narrative-itineraries.js index 0dd79bbee..c61edce1a 100644 --- a/lib/components/narrative/narrative-itineraries.js +++ b/lib/components/narrative/narrative-itineraries.js @@ -12,7 +12,7 @@ import { setVisibleItinerary, updateItineraryFilter } from '../../actions/narrative' -import { ItineraryView } from '../../actions/ui' +import { ItineraryView, setItineraryView } from '../../actions/ui' import Icon from '../narrative/icon' import { ComponentContext } from '../../util/contexts' import { @@ -41,33 +41,20 @@ class NarrativeItineraries extends Component { activeItinerary: PropTypes.number, containerStyle: PropTypes.object, itineraries: PropTypes.array, - onToggleDetailedItinerary: PropTypes.func, pending: PropTypes.bool, setActiveItinerary: PropTypes.func, setActiveLeg: PropTypes.func, setActiveStep: PropTypes.func, + setItineraryView: PropTypes.func, setUseRealtimeResponse: PropTypes.func, useRealtime: PropTypes.bool } static contextType = ComponentContext - constructor (props) { - super(props) - this.state = { - // If URL indicates full (expanded) itinerary view, then show itinerary details. - showDetails: props.itineraryView === ItineraryView.FULL - } - } - _toggleDetailedItinerary = () => { - const showDetails = !this.state.showDetails - this.setState({ showDetails }) - - const { onToggleDetailedItinerary } = this.props - if (onToggleDetailedItinerary) { - onToggleDetailedItinerary(showDetails) - } + const { itineraryView, setItineraryView } = this.props + setItineraryView(itineraryView === ItineraryView.FULL ? ItineraryView.SPLIT : ItineraryView.FULL) } _onFilterChange = evt => { @@ -118,6 +105,7 @@ class NarrativeItineraries extends Component { containerStyle, errors, itineraries, + itineraryView, pending, realtimeEffects, sort, @@ -126,7 +114,8 @@ class NarrativeItineraries extends Component { const { ItineraryBody, LegIcon } = this.context if (!activeSearch) return null - const itineraryIsExpanded = activeItinerary !== undefined && activeItinerary !== null && this.state.showDetails + const showDetails = itineraryView === ItineraryView.FULL + const itineraryIsExpanded = activeItinerary !== undefined && activeItinerary !== null && showDetails const showRealtimeAnnotation = realtimeEffects.isAffectedByRealtimeData && ( realtimeEffects.exceedsThreshold || @@ -199,7 +188,7 @@ class NarrativeItineraries extends Component { return ( { setActiveStep: (index, step) => { dispatch(setActiveStep({index, step})) }, + setItineraryView: payload => dispatch(setItineraryView(payload)), setUseRealtimeResponse: payload => dispatch(setUseRealtimeResponse(payload)), setVisibleItinerary: payload => dispatch(setVisibleItinerary(payload)), updateItineraryFilter: payload => dispatch(updateItineraryFilter(payload)) From 7c82cd75c4bfd45860644e82e9371b260019d75e Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 30 Mar 2021 13:10:00 -0400 Subject: [PATCH 243/265] refactor(actions/ui): Rename ItineraryView.SPLIT>LIST --- lib/actions/ui.js | 4 ++-- lib/components/narrative/narrative-itineraries.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/actions/ui.js b/lib/actions/ui.js index a80f44239..ae0d9fe61 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -242,9 +242,9 @@ export const MobileScreens = { export const ItineraryView = { FULL: 'full', HIDDEN: 'hidden', - SPLIT: 'split' + LIST: 'list' } -const DEFAULT_ITINERARY_VIEW = ItineraryView.SPLIT +const DEFAULT_ITINERARY_VIEW = ItineraryView.LIST /** * Sets the itinerary view state (see values above) in the URL params diff --git a/lib/components/narrative/narrative-itineraries.js b/lib/components/narrative/narrative-itineraries.js index c61edce1a..d6eef1d72 100644 --- a/lib/components/narrative/narrative-itineraries.js +++ b/lib/components/narrative/narrative-itineraries.js @@ -54,7 +54,7 @@ class NarrativeItineraries extends Component { _toggleDetailedItinerary = () => { const { itineraryView, setItineraryView } = this.props - setItineraryView(itineraryView === ItineraryView.FULL ? ItineraryView.SPLIT : ItineraryView.FULL) + setItineraryView(itineraryView === ItineraryView.FULL ? ItineraryView.LIST : ItineraryView.FULL) } _onFilterChange = evt => { From d0a4f3531f62c34c9fad61e26e6a02c1b3e287fc Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 30 Mar 2021 15:07:15 -0400 Subject: [PATCH 244/265] refactor(BatchResultsScreen): Add uncommited refactors from 7c82cd7 --- lib/components/mobile/batch-results-screen.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index e49f8d631..036fe4895 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -59,11 +59,11 @@ class BatchMobileResultsScreen extends Component { } _setItineraryExpanded = itineraryExpanded => { - this.props.setItineraryView(itineraryExpanded ? VIEW.FULL : VIEW.SPLIT) + this.props.setItineraryView(itineraryExpanded ? VIEW.FULL : VIEW.LIST) } _toggleMapExpanded = () => { - this.props.setItineraryView(this.props.itineraryView === VIEW.HIDDEN ? VIEW.SPLIT : VIEW.HIDDEN) + this.props.setItineraryView(this.props.itineraryView === VIEW.HIDDEN ? VIEW.LIST : VIEW.HIDDEN) } render () { From df927a8f852ee3ec599d8f14bdd44406cf9376af Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 30 Mar 2021 16:05:30 -0400 Subject: [PATCH 245/265] refactor(mobile/ResultsHeader): Reset itineraryView state when clicking Edit. --- lib/components/mobile/results-header.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/components/mobile/results-header.js b/lib/components/mobile/results-header.js index 9c498da3d..588982134 100644 --- a/lib/components/mobile/results-header.js +++ b/lib/components/mobile/results-header.js @@ -59,6 +59,7 @@ class ResultsHeader extends Component { _editSearchClicked = () => { this.props.clearActiveSearch() this.props.setMobileScreen(uiActions.MobileScreens.SEARCH_FORM) + this.props.setItineraryView(uiActions.ItineraryView.LIST) } render () { @@ -119,6 +120,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { clearActiveSearch: formActions.clearActiveSearch, + setItineraryView: uiActions.setItineraryView, setMobileScreen: uiActions.setMobileScreen } From 7678132763b027dc21ff3d7784230b7fe5577913 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 31 Mar 2021 11:37:29 -0400 Subject: [PATCH 246/265] refactor(NarrativeItineraries): Toggle focus between leg and itinerary when clicking leg. --- lib/actions/ui.js | 5 +-- lib/components/map/bounds-updating-overlay.js | 2 +- lib/components/mobile/batch-results-screen.js | 18 ++++------ lib/components/mobile/results-header.js | 1 + .../narrative/narrative-itineraries.js | 35 +++++++++++++++---- 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/lib/actions/ui.js b/lib/actions/ui.js index ae0d9fe61..f3463eb49 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -240,11 +240,12 @@ export const MobileScreens = { * (currently only used in batch results). */ export const ItineraryView = { + DEFAULT: 'list', FULL: 'full', HIDDEN: 'hidden', + LEG: 'leg', LIST: 'list' } -const DEFAULT_ITINERARY_VIEW = ItineraryView.LIST /** * Sets the itinerary view state (see values above) in the URL params @@ -257,7 +258,7 @@ export function setItineraryView (value) { // If the itinerary value is changed, // set the desired ui query param, or remove it if same as default. if (value !== urlParams.ui_itineraryView) { - if (value !== DEFAULT_ITINERARY_VIEW) { + if (value !== ItineraryView.DEFAULT) { urlParams.ui_itineraryView = value } else if (urlParams.ui_itineraryView) { delete urlParams.ui_itineraryView diff --git a/lib/components/map/bounds-updating-overlay.js b/lib/components/map/bounds-updating-overlay.js index 4ba59ab06..e4ce308e6 100644 --- a/lib/components/map/bounds-updating-overlay.js +++ b/lib/components/map/bounds-updating-overlay.js @@ -125,7 +125,7 @@ class BoundsUpdatingOverlay extends MapLayer { // See https://github.com/opentripplanner/otp-react-redux/issues/133 for // more info. // TODO: Fix this so mobile devices will also update the bounds to the - // from/to locations. + // from/to locations. if (!coreUtils.ui.isMobile()) { const bounds = L.bounds([ [newFrom.lat, newFrom.lon], diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index 036fe4895..58f68c81b 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -49,21 +49,17 @@ const NARRATIVE_SPLIT_TOP_PERCENT = 45 * and features a split view between the map and itinerary results or narratives. */ class BatchMobileResultsScreen extends Component { - componentDidUpdate (prevProps) { - if (this.props.activeLeg !== prevProps.activeLeg) { - // Check if the active leg has changed. If a different leg is selected, - // unexpand the itinerary to show the map focused on the selected leg - // (similar to the behavior of LineItinerary). - this._setItineraryExpanded(false) - } + state = { + previousItineraryView: null } - _setItineraryExpanded = itineraryExpanded => { - this.props.setItineraryView(itineraryExpanded ? VIEW.FULL : VIEW.LIST) + _setItineraryView = view => { + this.setState({ previousItineraryView: this.props.itineraryView }) + this.props.setItineraryView(view) } _toggleMapExpanded = () => { - this.props.setItineraryView(this.props.itineraryView === VIEW.HIDDEN ? VIEW.LIST : VIEW.HIDDEN) + this._setItineraryView(this.props.itineraryView === VIEW.HIDDEN ? this.state.previousItineraryView : VIEW.HIDDEN) } render () { @@ -130,7 +126,7 @@ const mapStateToProps = (state, ownProps) => { activeLeg: activeSearch ? activeSearch.activeLeg : null, errors: getResponsesWithErrors(state.otp), itineraries: getActiveItineraries(state.otp), - itineraryView: urlParams.ui_itineraryView + itineraryView: urlParams.ui_itineraryView || VIEW.DEFAULT } } diff --git a/lib/components/mobile/results-header.js b/lib/components/mobile/results-header.js index 588982134..b615e007c 100644 --- a/lib/components/mobile/results-header.js +++ b/lib/components/mobile/results-header.js @@ -59,6 +59,7 @@ class ResultsHeader extends Component { _editSearchClicked = () => { this.props.clearActiveSearch() this.props.setMobileScreen(uiActions.MobileScreens.SEARCH_FORM) + // Reset itinerary view state to show the list of results. this.props.setItineraryView(uiActions.ItineraryView.LIST) } diff --git a/lib/components/narrative/narrative-itineraries.js b/lib/components/narrative/narrative-itineraries.js index d6eef1d72..d2e7fd9e9 100644 --- a/lib/components/narrative/narrative-itineraries.js +++ b/lib/components/narrative/narrative-itineraries.js @@ -52,9 +52,32 @@ class NarrativeItineraries extends Component { static contextType = ComponentContext + _setActiveLeg = (index, leg) => { + const { activeLeg, setActiveLeg, setItineraryView } = this.props + const isSameLeg = activeLeg === index + if (isSameLeg) { + // If clicking on the same leg again, reset it to null, + // and show the full itinerary (both desktop and mobile view) + setActiveLeg(null, null) + setItineraryView(ItineraryView.FULL) + } else { + // Focus on the newly selected leg. + setActiveLeg(index, leg) + setItineraryView(ItineraryView.LEG) + } + } + + _isShowingDetails = () => { + const { itineraryView } = this.props + return itineraryView === ItineraryView.FULL || itineraryView === ItineraryView.LEG + } + _toggleDetailedItinerary = () => { - const { itineraryView, setItineraryView } = this.props - setItineraryView(itineraryView === ItineraryView.FULL ? ItineraryView.LIST : ItineraryView.FULL) + const { setActiveLeg, setItineraryView } = this.props + const newView = this._isShowingDetails() ? ItineraryView.LIST : ItineraryView.FULL + setItineraryView(newView) + // Reset the active leg. + setActiveLeg(null, null) } _onFilterChange = evt => { @@ -105,7 +128,6 @@ class NarrativeItineraries extends Component { containerStyle, errors, itineraries, - itineraryView, pending, realtimeEffects, sort, @@ -114,7 +136,7 @@ class NarrativeItineraries extends Component { const { ItineraryBody, LegIcon } = this.context if (!activeSearch) return null - const showDetails = itineraryView === ItineraryView.FULL + const showDetails = this._isShowingDetails() const itineraryIsExpanded = activeItinerary !== undefined && activeItinerary !== null && showDetails const showRealtimeAnnotation = realtimeEffects.isAffectedByRealtimeData && ( @@ -193,12 +215,13 @@ class NarrativeItineraries extends Component { itinerary={itinerary} key={index} LegIcon={LegIcon} - setActiveLeg={setActiveLeg} onClick={active ? this._toggleDetailedItinerary : undefined} routingType='ITINERARY' showRealtimeAnnotation={showRealtimeAnnotation} sort={sort} {...this.props} + // Override setActiveLeg from props spreading + setActiveLeg={this._setActiveLeg} /> ) })} @@ -239,7 +262,7 @@ const mapStateToProps = (state, ownProps) => { errors: getResponsesWithErrors(state.otp), // swap out realtime itineraries with non-realtime depending on boolean itineraries, - itineraryView: urlParams.ui_itineraryView, + itineraryView: urlParams.ui_itineraryView || ItineraryView.DEFAULT, pending, realtimeEffects, activeItinerary: activeSearch && activeSearch.activeItinerary, From dffebbf445cca179f9b2043ab56cc499d05a330a Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 31 Mar 2021 11:49:42 -0400 Subject: [PATCH 247/265] refactor(NarrativeItineraries): Fix lint --- lib/components/narrative/narrative-itineraries.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/components/narrative/narrative-itineraries.js b/lib/components/narrative/narrative-itineraries.js index d2e7fd9e9..77738aa27 100644 --- a/lib/components/narrative/narrative-itineraries.js +++ b/lib/components/narrative/narrative-itineraries.js @@ -207,6 +207,11 @@ class NarrativeItineraries extends Component { const active = index === activeItinerary // Hide non-active itineraries. if (!active && itineraryIsExpanded) return null + const itineraryBodyProps = { + ...this.props, + // Override setActiveLeg from props spreading + setActiveLeg: this._setActiveLeg + } return ( ) })} From 77bfc8f678830e18fa9ed828d9d6e6652969747a Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 31 Mar 2021 15:28:49 -0400 Subject: [PATCH 248/265] refactor(mobile/results-screen): Convert options header into button+styles. --- lib/components/mobile/mobile.css | 1 - lib/components/mobile/results-screen.js | 19 +++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/components/mobile/mobile.css b/lib/components/mobile/mobile.css index 1e6475c6e..4b5409dc8 100644 --- a/lib/components/mobile/mobile.css +++ b/lib/components/mobile/mobile.css @@ -159,7 +159,6 @@ text-align: center; font-size: 20px; font-weight: 500; - padding-top: 5px; } .otp.mobile .mobile-narrative-container { diff --git a/lib/components/mobile/results-screen.js b/lib/components/mobile/results-screen.js index 9ff918b83..d2e20518e 100644 --- a/lib/components/mobile/results-screen.js +++ b/lib/components/mobile/results-screen.js @@ -2,6 +2,7 @@ import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' import React, { Component } from 'react' import { connect } from 'react-redux' +import styled from 'styled-components' import * as narrativeActions from '../../actions/narrative' import Map from '../map/map' @@ -17,6 +18,16 @@ import MobileContainer from './container' import ResultsError from './results-error' import ResultsHeader from './results-header' +const OptionExpanderButton = styled.button` + border: none; + bottom: ${props => props.expanded ? 'inherit' : '100px'}; + outline: none; + padding-bottom: 3px; + display: block; + top: ${props => props.expanded ? '100px' : 'inherit'}; + width: 100%; +` + class MobileResultsScreen extends Component { static propTypes = { activeItineraryIndex: PropTypes.number, @@ -113,14 +124,14 @@ class MobileResultsScreen extends Component { ? : ( <> -
    - Option {activeItineraryIndex + 1} + Option {activeItineraryIndex + 1} -
    +
    Date: Wed, 31 Mar 2021 16:11:11 -0400 Subject: [PATCH 249/265] refactor(mobile/ResultsError): Convert some components to styled- per PR comments --- lib/components/mobile/batch-results-screen.js | 39 ++++++++++++------- lib/components/mobile/mobile.css | 1 - lib/components/mobile/results-error.js | 11 ++++-- lib/components/mobile/results-screen.js | 26 ++++++------- 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index 58f68c81b..5a82e1131 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -40,10 +40,26 @@ const ExpandMapButton = styled(Button)` z-index: 999999; ` -const VIEW = uiActions.ItineraryView - const NARRATIVE_SPLIT_TOP_PERCENT = 45 +// Styles for the results map also include prop-independent styles copied from mobile.css. +const ResultsMap = styled.div` + bottom: ${props => props.expanded ? '0' : `${100 - NARRATIVE_SPLIT_TOP_PERCENT}%`}; + display: ${props => props.visible ? 'inherit' : 'none'}; + left: 0; + position: fixed; + right: 0; + top: 100px; +` + +const StyledResultsError = styled(ResultsError)` + display: ${props => props.visible ? 'inherit' : 'none'}; + top: ${props => props.visible ? (props.expanded ? '100px' : `${NARRATIVE_SPLIT_TOP_PERCENT}%`) : '100%'}; + transition: top 300ms; +` + +const VIEW = uiActions.ItineraryView + /** * This component renders the mobile view of itinerary results from batch routing, * and features a split view between the map and itinerary results or narratives. @@ -68,14 +84,13 @@ class BatchMobileResultsScreen extends Component { const mapExpanded = itineraryView === VIEW.HIDDEN const itineraryExpanded = itineraryView === VIEW.FULL const narrativeTop = mapExpanded ? '100%' : (itineraryExpanded ? '100px' : `${NARRATIVE_SPLIT_TOP_PERCENT}%`) - const mapBottom = mapExpanded ? 0 : `${100 - NARRATIVE_SPLIT_TOP_PERCENT}%` return ( -
    {' '} {mapExpanded ? 'Show results' : 'Expand map'} -
    + {hasNoResult - ? + expanded={itineraryExpanded} + visible={!mapExpanded} + /> : (
    +
    + ) + } +} + +// connect to the redux store + +const mapDispatchToProps = { + clearActiveSearch: formActions.clearActiveSearch, + setItineraryView: uiActions.setItineraryView, + setMobileScreen: uiActions.setMobileScreen +} + +export default connect(null, mapDispatchToProps)(EditSearchButton) diff --git a/lib/components/mobile/results-error.js b/lib/components/mobile/results-error.js index 2114ed00e..eb8728a1e 100644 --- a/lib/components/mobile/results-error.js +++ b/lib/components/mobile/results-error.js @@ -1,65 +1,35 @@ import PropTypes from 'prop-types' -import React, { Component } from 'react' -import { Button } from 'react-bootstrap' -import { connect } from 'react-redux' +import React from 'react' import styled from 'styled-components' -import * as formActions from '../../actions/form' -import * as uiActions from '../../actions/ui' import ErrorMessage from '../form/error-message' +import EditSearchButton from './edit-search-button' + /** * This component is used on mobile views to * render an error message if no results are found. */ -class ResultsError extends Component { - static propTypes = { - error: PropTypes.object, - setItineraryView: PropTypes.func, - setMobileScreen: PropTypes.func - } - - _editSearchClicked = () => { - // Reset itinerary view state to show the list of results *before* clearing the search. - // (Otherwise, if the map is expanded, the search is not cleared.) - this.props.setItineraryView(uiActions.ItineraryView.LIST) - this.props.clearActiveSearch() - this.props.setMobileScreen(uiActions.MobileScreens.SEARCH_FORM) - } - - render () { - const { className, error } = this.props - return ( -
    - -
    - -
    -
    - ) - } +const ResultsError = ({ className, error }) => ( +
    + +
    + + Back to Search + +
    +
    +) + +ResultsError.propTypes = { + error: PropTypes.object } const StyledResultsError = styled(ResultsError)` top: 300px; ` -// connect to the redux store - -const mapStateToProps = (state, ownProps) => { - return {} -} - -const mapDispatchToProps = { - clearActiveSearch: formActions.clearActiveSearch, - setItineraryView: uiActions.setItineraryView, - setMobileScreen: uiActions.setMobileScreen -} - -export default connect(mapStateToProps, mapDispatchToProps)(StyledResultsError) +export default StyledResultsError diff --git a/lib/components/mobile/results-header.js b/lib/components/mobile/results-header.js index 14b478e49..d0aa51c4b 100644 --- a/lib/components/mobile/results-header.js +++ b/lib/components/mobile/results-header.js @@ -1,18 +1,17 @@ import LocationIcon from '@opentripplanner/location-icon' import PropTypes from 'prop-types' import React, { Component } from 'react' -import { Button, Col, Row } from 'react-bootstrap' +import { Col, Row } from 'react-bootstrap' import { connect } from 'react-redux' import styled from 'styled-components' -import * as formActions from '../../actions/form' -import * as uiActions from '../../actions/ui' import { getActiveItineraries, getActiveSearch, getResponsesWithErrors } from '../../util/state' +import EditSearchButton from './edit-search-button' import MobileNavigationBar from './navigation-bar' const LocationContainer = styled.div` @@ -52,17 +51,7 @@ class ResultsHeader extends Component { static propTypes = { errors: PropTypes.array, query: PropTypes.object, - resultCount: PropTypes.number, - setItineraryView: PropTypes.func, - setMobileScreen: PropTypes.func - } - - _editSearchClicked = () => { - // Reset itinerary view state to show the list of results *before* clearing the search. - // (Otherwise, if the map is expanded, the search is not cleared.) - this.props.setItineraryView(uiActions.ItineraryView.LIST) - this.props.clearActiveSearch() - this.props.setMobileScreen(uiActions.MobileScreens.SEARCH_FORM) + resultCount: PropTypes.number } render () { @@ -90,10 +79,9 @@ class ResultsHeader extends Component { - + + Edit + @@ -121,10 +109,4 @@ const mapStateToProps = (state, ownProps) => { } } -const mapDispatchToProps = { - clearActiveSearch: formActions.clearActiveSearch, - setItineraryView: uiActions.setItineraryView, - setMobileScreen: uiActions.setMobileScreen -} - -export default connect(mapStateToProps, mapDispatchToProps)(ResultsHeader) +export default connect(mapStateToProps)(ResultsHeader) From af9f653f1d0dec934913ef1ec57fb42eeb995f42 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 9 Apr 2021 15:10:27 -0400 Subject: [PATCH 259/265] fix(actions/ui): Split ItineraryView hidden state. --- lib/actions/ui.js | 8 +++++--- lib/components/mobile/batch-results-screen.js | 9 ++++++--- lib/components/narrative/narrative-itineraries.js | 4 +++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/actions/ui.js b/lib/actions/ui.js index a435fe027..7379dfeee 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -243,12 +243,14 @@ export const ItineraryView = { DEFAULT: 'list', /** One itinerary is shown. (In mobile view, the map is hidden.) */ FULL: 'full', - /** Itinerary is hidden. (In mobile view, the map is expanded.) */ - HIDDEN: 'hidden', /** One itinerary is shown, itinerary and map are focused on a leg. (The mobile view is split.) */ LEG: 'leg', + /** One itinerary leg is hidden. (In mobile view, the map is expanded.) */ + LEG_HIDDEN: 'leg-hidden', /** The list of itineraries is shown. (The mobile view is split.) */ - LIST: 'list' + LIST: 'list', + /** The list of itineraries is hidden. (In mobile view, the map is expanded.) */ + LIST_HIDDEN: 'list-hidden' } /** diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index 45b505e20..da1750bb3 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -85,9 +85,12 @@ class BatchMobileResultsScreen extends Component { _toggleMapExpanded = () => { const { itineraryView, setItineraryView } = this.props - if (itineraryView !== ItineraryView.HIDDEN) { + if (itineraryView === ItineraryView.LEG) { this.setState({ previousSplitView: itineraryView }) - setItineraryView(ItineraryView.HIDDEN) + setItineraryView(ItineraryView.LEG_HIDDEN) + } else if (itineraryView === ItineraryView.LIST) { + this.setState({ previousSplitView: itineraryView }) + setItineraryView(ItineraryView.LIST_HIDDEN) } else { setItineraryView(this.state.previousSplitView) } @@ -96,7 +99,7 @@ class BatchMobileResultsScreen extends Component { render () { const { errors, itineraries, itineraryView } = this.props const hasErrorsAndNoResult = itineraries.length === 0 && errors.length > 0 - const mapExpanded = itineraryView === ItineraryView.HIDDEN + const mapExpanded = itineraryView === ItineraryView.LEG_HIDDEN || itineraryView === ItineraryView.LIST_HIDDEN const itineraryExpanded = itineraryView === ItineraryView.FULL return ( diff --git a/lib/components/narrative/narrative-itineraries.js b/lib/components/narrative/narrative-itineraries.js index 12beb7af9..cdcaec0ff 100644 --- a/lib/components/narrative/narrative-itineraries.js +++ b/lib/components/narrative/narrative-itineraries.js @@ -71,7 +71,9 @@ class NarrativeItineraries extends Component { _isShowingDetails = () => { const { itineraryView } = this.props - return itineraryView === ItineraryView.FULL || itineraryView === ItineraryView.LEG + return itineraryView === ItineraryView.FULL || + itineraryView === ItineraryView.LEG || + itineraryView === ItineraryView.LEG_HIDDEN } _toggleDetailedItinerary = () => { From 7b4ba824148e9dffd502d0341e29c716f424340c Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 9 Apr 2021 15:22:17 -0400 Subject: [PATCH 260/265] fix(deps): bump core-utils for default query override --- package.json | 2 +- yarn.lock | 131 +++++++-------------------------------------------- 2 files changed, 17 insertions(+), 116 deletions(-) diff --git a/package.json b/package.json index f7e931971..003740dbb 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dependencies": { "@auth0/auth0-react": "^1.1.0", "@opentripplanner/base-map": "^1.0.5", - "@opentripplanner/core-utils": "^3.0.4", + "@opentripplanner/core-utils": "^3.1.0", "@opentripplanner/endpoints-overlay": "^1.0.6", "@opentripplanner/from-to-location-picker": "^1.0.4", "@opentripplanner/geocoder": "^1.0.2", diff --git a/yarn.lock b/yarn.lock index 295ccc823..5099623ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,14 +22,7 @@ promise-polyfill "^8.1.3" unfetch "^4.1.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/code-frame@^7.12.11": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== @@ -67,16 +60,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.11.5", "@babel/generator@^7.11.6", "@babel/generator@^7.4.0", "@babel/generator@^7.9.4": - version "7.11.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" - integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA== - dependencies: - "@babel/types" "^7.11.5" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/generator@^7.12.11": +"@babel/generator@^7.11.5", "@babel/generator@^7.11.6", "@babel/generator@^7.12.11", "@babel/generator@^7.4.0", "@babel/generator@^7.9.4": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af" integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA== @@ -165,16 +149,7 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-function-name@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" - integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== - dependencies: - "@babel/helper-get-function-arity" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-function-name@^7.12.11": +"@babel/helper-function-name@^7.10.4", "@babel/helper-function-name@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42" integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA== @@ -183,14 +158,7 @@ "@babel/template" "^7.12.7" "@babel/types" "^7.12.11" -"@babel/helper-get-function-arity@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" - integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-get-function-arity@^7.12.10": +"@babel/helper-get-function-arity@^7.10.4", "@babel/helper-get-function-arity@^7.12.10": version "7.12.10" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf" integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag== @@ -285,26 +253,14 @@ dependencies: "@babel/types" "^7.11.0" -"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" - integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== - dependencies: - "@babel/types" "^7.11.0" - -"@babel/helper-split-export-declaration@^7.12.11": +"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0", "@babel/helper-split-export-declaration@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a" integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g== dependencies: "@babel/types" "^7.12.11" -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== - -"@babel/helper-validator-identifier@^7.12.11": +"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== @@ -342,12 +298,7 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.2.tgz#871807f10442b92ff97e4783b9b54f6a0ca812d0" integrity sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ== -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.5", "@babel/parser@^7.4.3": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" - integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== - -"@babel/parser@^7.12.11", "@babel/parser@^7.12.7": +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.5", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.4.3": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== @@ -1113,16 +1064,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.10.4", "@babel/template@^7.4.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" - integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/template@^7.12.7": +"@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.4.0": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== @@ -1131,22 +1073,7 @@ "@babel/parser" "^7.12.7" "@babel/types" "^7.12.7" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.11.5", "@babel/traverse@^7.4.3", "@babel/traverse@^7.9.0": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3" - integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.5" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.11.5" - "@babel/types" "^7.11.5" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" - -"@babel/traverse@^7.4.5": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.11.5", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.9.0": version "7.12.12" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376" integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w== @@ -1161,16 +1088,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.9.0": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" - integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" - to-fast-properties "^2.0.0" - -"@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.7": +"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.7", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.9.0": version "7.12.12" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299" integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ== @@ -1577,10 +1495,10 @@ "@opentripplanner/core-utils" "^3.0.4" prop-types "^15.7.2" -"@opentripplanner/core-utils@^3.0.0", "@opentripplanner/core-utils@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-3.0.4.tgz#075260be547c4af718335f8711383c26e6f311de" - integrity sha512-5yV9zIeNduDpiu7r1r2jjkLUy0ZOEAVOpMlaP9o8EeAdpH6jjkg25hiOzFkJNGnYFA5pemYkT6r/OP4JvDESvg== +"@opentripplanner/core-utils@^3.0.0", "@opentripplanner/core-utils@^3.0.4", "@opentripplanner/core-utils@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-3.1.0.tgz#4626807893874503c5d365b05e5db4a1b3bf163c" + integrity sha512-EYhIv6nQdmadmkQumuXGrEwvRrhUl8xDPckSv24y3o9FX7uk8h5Gqe4FPdjnBT1eOj/XUj0/fZzNcETHUvZvUg== dependencies: "@mapbox/polyline" "^1.1.0" "@opentripplanner/geocoder" "^1.0.2" @@ -7903,19 +7821,7 @@ highlight.js@^9.15.5, highlight.js@^9.7.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825" integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA== -history@^4.7.2: - version "4.9.0" - resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" - integrity sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^2.2.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^0.4.0" - -history@^4.8.0-beta.0, history@^4.9.0: +history@^4.7.2, history@^4.8.0-beta.0, history@^4.9.0: version "4.10.1" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== @@ -13131,12 +13037,7 @@ postcss-value-parser@^3.0.0, postcss-value-parser@^3.0.1, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d" - integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg== - -postcss-value-parser@^4.0.2: +postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== From 860d6dc0ab615ae09fba7e8c9305fe09d30811b4 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 9 Apr 2021 15:24:39 -0400 Subject: [PATCH 261/265] refactor(example-config.yml): add defaultQueryParams entry --- example-config.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/example-config.yml b/example-config.yml index f9eb30667..cd48c0c2e 100644 --- a/example-config.yml +++ b/example-config.yml @@ -15,6 +15,11 @@ api: # lon: -122.71607145667079 # name: Oregon Zoo, Portland, OR +### The default query parameters can be overridden be uncommenting this object. +### Note: the override values must be valid values within otp-ui's query-params.js +# defaultQueryParams: +# maxWalkDistance: 3219 # 2 miles in meters + ### The persistence setting is used to enable the storage of places (home, work), ### recent searches/places, user overrides, and favorite stops. ### Pick the strategy that best suits your needs. From 7a8b67e5e5555bb641af54e4415b83bfb99f767d Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 9 Apr 2021 16:23:28 -0400 Subject: [PATCH 262/265] refactor(state.js): remove noisy/unneeded log statement --- lib/util/state.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/util/state.js b/lib/util/state.js index a3459b805..4caab3f7f 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -99,7 +99,6 @@ export function getActiveItineraries (otpState) { }) return hasCar default: - console.warn(`Filter (${filter}) not supported`) return true } }) From 3f138bb57c187373a0dea19cc90aea90f8fb92bd Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 12 Apr 2021 09:36:31 -0400 Subject: [PATCH 263/265] refactor(NarrativeItineraries): Remove unused prop. --- lib/components/narrative/narrative-itineraries.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/components/narrative/narrative-itineraries.js b/lib/components/narrative/narrative-itineraries.js index cdcaec0ff..b241ac753 100644 --- a/lib/components/narrative/narrative-itineraries.js +++ b/lib/components/narrative/narrative-itineraries.js @@ -279,7 +279,6 @@ const mapStateToProps = (state, ownProps) => { activeSearch, activeStep: activeSearch && activeSearch.activeStep, errors: getResponsesWithErrors(state.otp), - filter: state.otp.filter, itineraries, itineraryView: urlParams.ui_itineraryView || ItineraryView.DEFAULT, modes, From 43fef77367b6d788c401198500108f49fff1f355 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 13 Apr 2021 11:39:22 -0400 Subject: [PATCH 264/265] refactor(actions/ui): Extract batch results and edit search actions. --- lib/actions/ui.js | 40 +++++- lib/components/mobile/batch-results-screen.js | 124 +++++++----------- lib/components/mobile/edit-search-button.js | 42 +----- lib/reducers/create-otp-reducer.js | 2 + 4 files changed, 97 insertions(+), 111 deletions(-) diff --git a/lib/actions/ui.js b/lib/actions/ui.js index 7379dfeee..955515798 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -253,6 +253,8 @@ export const ItineraryView = { LIST_HIDDEN: 'list-hidden' } +const setPreviousItineraryView = createAction('SET_PREVIOUS_ITINERARY_VIEW') + /** * Sets the itinerary view state (see values above) in the URL params * (currently only used in batch results). @@ -260,9 +262,11 @@ export const ItineraryView = { export function setItineraryView (value) { return function (dispatch, getState) { const urlParams = coreUtils.query.getUrlParams() + const prevItineraryView = urlParams.ui_itineraryView || ItineraryView.DEFAULT // If the itinerary value is changed, - // set the desired ui query param, or remove it if same as default. + // set the desired ui query param, or remove it if same as default, + // and store the current view as previousItineraryView. if (value !== urlParams.ui_itineraryView) { if (value !== ItineraryView.DEFAULT) { urlParams.ui_itineraryView = value @@ -271,6 +275,40 @@ export function setItineraryView (value) { } dispatch(setUrlSearch(urlParams)) + dispatch(setPreviousItineraryView(prevItineraryView)) + } + } +} + +/** + * Switch the mobile batch results view between full map view and the split state + * (itinerary list or itinerary leg view) that was in place prior. + */ +export function toggleBatchResultsMap () { + return function (dispatch, getState) { + const urlParams = coreUtils.query.getUrlParams() + const itineraryView = urlParams.ui_itineraryView || ItineraryView.DEFAULT + + if (itineraryView === ItineraryView.LEG) { + dispatch(setItineraryView(ItineraryView.LEG_HIDDEN)) + } else if (itineraryView === ItineraryView.LIST) { + dispatch(setItineraryView(ItineraryView.LIST_HIDDEN)) + } else { + const { previousItineraryView } = getState().otp.ui + dispatch(setItineraryView(previousItineraryView)) } } } + +/** + * Takes the user back to the mobile search screen in mobile views. + */ +export function showMobileSearchScreen () { + return function (dispatch, getState) { + // Reset itinerary view state to show the list of results *before* clearing the search. + // (Otherwise, if the map is expanded, the search is not cleared.) + dispatch(setItineraryView(ItineraryView.LIST)) + dispatch(clearActiveSearch()) + dispatch(setMobileScreen(MobileScreens.SEARCH_FORM)) + } +} diff --git a/lib/components/mobile/batch-results-screen.js b/lib/components/mobile/batch-results-screen.js index da1750bb3..51370a897 100644 --- a/lib/components/mobile/batch-results-screen.js +++ b/lib/components/mobile/batch-results-screen.js @@ -1,5 +1,5 @@ import coreUtils from '@opentripplanner/core-utils' -import React, { Component } from 'react' +import React from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' import styled, { css } from 'styled-components' @@ -72,79 +72,58 @@ const { ItineraryView } = uiActions * This component renders the mobile view of itinerary results from batch routing, * and features a split view between the map and itinerary results or narratives. */ -class BatchMobileResultsScreen extends Component { - state = { - // Holds the previous split view state - previousSplitView: null - } - - /** - * Switch between full map view and the split state (itinerary list or itinerary leg view) - * that was in place prior. - */ - _toggleMapExpanded = () => { - const { itineraryView, setItineraryView } = this.props - - if (itineraryView === ItineraryView.LEG) { - this.setState({ previousSplitView: itineraryView }) - setItineraryView(ItineraryView.LEG_HIDDEN) - } else if (itineraryView === ItineraryView.LIST) { - this.setState({ previousSplitView: itineraryView }) - setItineraryView(ItineraryView.LIST_HIDDEN) - } else { - setItineraryView(this.state.previousSplitView) - } - } - - render () { - const { errors, itineraries, itineraryView } = this.props - const hasErrorsAndNoResult = itineraries.length === 0 && errors.length > 0 - const mapExpanded = itineraryView === ItineraryView.LEG_HIDDEN || itineraryView === ItineraryView.LIST_HIDDEN - const itineraryExpanded = itineraryView === ItineraryView.FULL - - return ( - - - { + const hasErrorsAndNoResult = itineraries.length === 0 && errors.length > 0 + const mapExpanded = itineraryView === ItineraryView.LEG_HIDDEN || itineraryView === ItineraryView.LIST_HIDDEN + const itineraryExpanded = itineraryView === ItineraryView.FULL + + return ( + + + + + - - - {' '} - {mapExpanded ? 'Show results' : 'Expand map'} - - - {hasErrorsAndNoResult - ? {' '} + {mapExpanded ? 'Show results' : 'Expand map'} + + + {hasErrorsAndNoResult + ? + : ( + - : ( - - - - ) - } - - ) - } + > + + + ) + } + + ) } // connect to the redux store @@ -152,7 +131,6 @@ class BatchMobileResultsScreen extends Component { const mapStateToProps = (state, ownProps) => { const activeSearch = getActiveSearch(state.otp) const urlParams = coreUtils.query.getUrlParams() - return { activeLeg: activeSearch ? activeSearch.activeLeg : null, errors: getResponsesWithErrors(state.otp), @@ -162,7 +140,7 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - setItineraryView: uiActions.setItineraryView + toggleBatchResultsMap: uiActions.toggleBatchResultsMap } export default connect(mapStateToProps, mapDispatchToProps)(BatchMobileResultsScreen) diff --git a/lib/components/mobile/edit-search-button.js b/lib/components/mobile/edit-search-button.js index d1ad67ea3..bdf514857 100644 --- a/lib/components/mobile/edit-search-button.js +++ b/lib/components/mobile/edit-search-button.js @@ -1,52 +1,20 @@ -import PropTypes from 'prop-types' -import React, { Component } from 'react' +import React from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import * as formActions from '../../actions/form' import * as uiActions from '../../actions/ui' /** * Renders the "Edit" or "Back to search" button in mobile result views * that takes the user back to the mobile search screen. */ -class EditSearchButton extends Component { - static propTypes = { - clearActiveSearch: PropTypes.func, - setItineraryView: PropTypes.func, - setMobileScreen: PropTypes.func - } - - _handleClick = () => { - const { clearActiveSearch, setItineraryView, setMobileScreen } = this.props - - // Reset itinerary view state to show the list of results *before* clearing the search. - // (Otherwise, if the map is expanded, the search is not cleared.) - setItineraryView(uiActions.ItineraryView.LIST) - clearActiveSearch() - setMobileScreen(uiActions.MobileScreens.SEARCH_FORM) - } - - render () { - const { children, className, style } = this.props - return ( - - ) - } -} +const EditSearchButton = ({ showMobileSearchScreen, ...props }) => ( +