diff --git a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap index 93507dd74..69365bedb 100644 --- a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap +++ b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap @@ -50,7 +50,7 @@ Object { "filter": "ALL", "sort": Object { "direction": "ASC", - "type": "BEST", + "type": null, }, }, "location": Object { diff --git a/lib/actions/user.js b/lib/actions/user.js index 61c43ffd7..4eb46f5a7 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -1,5 +1,6 @@ import { createAction } from 'redux-actions' +import { routeTo } from './ui' import { addTrip, addUser, @@ -40,7 +41,7 @@ export function fetchUserMonitoredTrips (accessToken) { if (otpMiddleware) { const { data: trips, status: fetchStatus } = await getTrips(otpMiddleware, accessToken) if (fetchStatus === 'success') { - dispatch(setCurrentUserMonitoredTrips(trips)) + dispatch(setCurrentUserMonitoredTrips(trips.data)) } } } @@ -154,6 +155,9 @@ export function createOrUpdateUserMonitoredTrip (tripData, isNew, silentOnSucces // Reload user's monitored trips after add/update. await dispatch(fetchUserMonitoredTrips(accessToken)) + + // Finally, navigate to the saved trips page. + dispatch(routeTo('/savedtrips')) } else { alert(`An error was encountered:\n${JSON.stringify(result)}`) } diff --git a/lib/components/form/error-message.js b/lib/components/form/error-message.js index 9dd6a9e61..9a9cb8cf1 100644 --- a/lib/components/form/error-message.js +++ b/lib/components/form/error-message.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import { connect } from 'react-redux' import TripTools from '../narrative/trip-tools' -import { getActiveSearch } from '../../util/state' +import { getActiveErrors } from '../../util/state' class ErrorMessage extends Component { static propTypes = { @@ -45,9 +45,8 @@ class ErrorMessage extends Component { // connect to the redux store const mapStateToProps = (state, ownProps) => { - const activeSearch = getActiveSearch(state.otp) return { - error: activeSearch && activeSearch.response && activeSearch.response.error, + error: getActiveErrors(state.otp)[0], currentQuery: state.otp.currentQuery, errorMessages: state.otp.config.errorMessages } diff --git a/lib/components/mobile/results-screen.js b/lib/components/mobile/results-screen.js index ef3290e77..c5b8448d5 100644 --- a/lib/components/mobile/results-screen.js +++ b/lib/components/mobile/results-screen.js @@ -16,7 +16,12 @@ import MobileNavigationBar from './navigation-bar' import { MobileScreens, setMobileScreen } from '../../actions/ui' import { setUseRealtimeResponse } from '../../actions/narrative' import { clearActiveSearch } from '../../actions/form' -import { getActiveItineraries, getActiveSearch, getRealtimeEffects } from '../../util/state' +import { + getActiveErrors, + getActiveItineraries, + getActiveSearch, + getRealtimeEffects +} from '../../util/state' const LocationContainer = styled.div` font-weight: 300; @@ -239,7 +244,7 @@ const mapStateToProps = (state, ownProps) => { return { query: state.otp.currentQuery, realtimeEffects, - error: response && response.error, + error: getActiveErrors(state.otp)[0], resultCount: response ? activeSearch.query.routingType === 'ITINERARY' diff --git a/lib/components/narrative/narrative-routing-results.js b/lib/components/narrative/narrative-routing-results.js index a3dcd9e4b..294080dfd 100644 --- a/lib/components/narrative/narrative-routing-results.js +++ b/lib/components/narrative/narrative-routing-results.js @@ -6,7 +6,11 @@ import Loading from './loading' import TabbedItineraries from './tabbed-itineraries' import ErrorMessage from '../form/error-message' -import { getActiveSearch, getActiveItineraries } from '../../util/state' +import { + getActiveErrors, + getActiveItineraries, + getActiveSearch +} from '../../util/state' import { setMainPanelContent } from '../../actions/ui' class NarrativeRoutingResults extends Component { @@ -47,7 +51,7 @@ const mapStateToProps = (state, ownProps) => { const pending = activeSearch ? Boolean(activeSearch.pending) : false return { mainPanelContent: state.otp.ui.mainPanelContent, - error: activeSearch && activeSearch.response && activeSearch.response.error, + error: getActiveErrors(state.otp)[0], itineraries: getActiveItineraries(state.otp), pending, routingType: activeSearch && activeSearch.query.routingType diff --git a/lib/components/user/existing-account-display.js b/lib/components/user/existing-account-display.js index 0513914f4..7aaed3284 100644 --- a/lib/components/user/existing-account-display.js +++ b/lib/components/user/existing-account-display.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React from 'react' import LinkButton from './link-button' import StackedPaneDisplay from './stacked-pane-display' @@ -6,38 +6,41 @@ import StackedPaneDisplay from './stacked-pane-display' /** * This component handles the existing account display. */ -class ExistingAccountDisplay extends Component { - render () { - const { onCancel, onComplete, panes } = this.props - const paneSequence = [ - { - pane: () =>

Edit my trips

, - title: 'My trips' - }, - { - pane: panes.terms, - props: { disableCheckTerms: true }, - title: 'Terms' - }, - { - pane: panes.notifications, - title: 'Notifications' - }, - { - pane: panes.locations, - title: 'My locations' - } - ] +const ExistingAccountDisplay = props => { + // The props include Formik props that provide access to the current user data + // and to its own blur/change/submit event handlers that automate the state. + // We forward the props to each pane so that their individual controls + // can be wired to be managed by Formik. + const { onCancel, panes } = props + const paneSequence = [ + { + pane: () =>

Edit my trips

, + title: 'My trips' + }, + { + pane: panes.terms, + props: { ...props, disableCheckTerms: true }, + title: 'Terms' + }, + { + pane: panes.notifications, + props, + title: 'Notifications' + }, + { + pane: panes.locations, + props, + title: 'My locations' + } + ] - return ( - - ) - } + return ( + + ) } export default ExistingAccountDisplay diff --git a/lib/components/user/favorite-locations-pane.js b/lib/components/user/favorite-locations-pane.js index e143a70e2..0bced0b30 100644 --- a/lib/components/user/favorite-locations-pane.js +++ b/lib/components/user/favorite-locations-pane.js @@ -1,8 +1,8 @@ -import clone from 'lodash/cloneDeep' +import { Field, FieldArray } from 'formik' import memoize from 'lodash.memoize' -import PropTypes from 'prop-types' import React, { Component } from 'react' import { + Button, ControlLabel, FormControl, FormGroup, @@ -37,135 +37,112 @@ const NewLocationFormControl = styled(FormControl)` ` // Helper filter functions. -const isHome = loc => loc.type === 'home' -const isWork = loc => loc.type === 'work' -const notHomeOrWork = loc => loc.type !== 'home' && loc.type !== 'work' +export const isHome = loc => loc.type === 'home' +export const isWork = loc => loc.type === 'work' /** - * User's saved locations editor. + * Helper function that adds a new address to the Formik state + * using the Formik-provided arrayHelpers object. */ -class FavoriteLocationsPane extends Component { - static propTypes = { - onUserDataChange: PropTypes.func.isRequired, - userData: PropTypes.object.isRequired +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 = '' } +} - _handleAddNewLocation = e => { - const value = e.target.value || '' - if (value.trim().length > 0) { - const { userData, onUserDataChange } = this.props - // FIXME: remove assigning [] when null. - const { savedLocations = [] } = userData - - // Create a copy of savedLocations and add the new location to the copied array. - const newLocations = clone(savedLocations) - newLocations.push({ - address: value.trim(), - icon: 'map-marker', - type: 'custom' - }) - - // Event onChange will trigger after this and before rerender, - // so DO empty the input box value so the user can enter their next location. - e.target.value = null - - onUserDataChange({ savedLocations: newLocations }) +/** + * 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() + } } - } - - _handleAddressChange = memoize( - location => e => { - const { userData, onUserDataChange } = this.props - // FIXME: remove assigning [] when null. - const { savedLocations = [] } = userData - const value = e.target.value - const isValueEmpty = !value || value === '' - const nonEmptyLocation = isValueEmpty ? null : location - - // Update location address, ohterwise it stalls the input box. - location.address = value - - // Create a new array for savedLocations. - let newLocations = [] - - // Add home/work as first entries to the new state only if - // - user edited home/work to non-empty, or - // - user edited another location and home/work is in savedLocations. - const homeLocation = (isHome(location) && nonEmptyLocation) || savedLocations.find(isHome) - if (homeLocation) newLocations.push(homeLocation) - - const workLocation = (isWork(location) && nonEmptyLocation) || savedLocations.find(isWork) - if (workLocation) newLocations.push(workLocation) - - // Add the rest if it is not home or work - // and if the new address of this one is not null or empty. - newLocations = newLocations.concat(savedLocations - .filter(notHomeOrWork) - .filter(loc => loc !== location || !isValueEmpty) - ) + ) - onUserDataChange({ savedLocations: newLocations }) + _handleNewAddressBlur = memoize( + arrayHelpers => e => { + addNewAddress(arrayHelpers, e) } ) render () { - const { userData } = this.props - // FIXME: remove assigning [] when null. - const { savedLocations = [] } = userData - - // Build an 'effective' list of locations for display, - // where at least one 'home' and one 'work', are always present even if blank. - // In theory there could be multiple home or work locations. - // Just pick the first one. - const homeLocation = savedLocations.find(isHome) || { - address: null, - icon: 'home', - type: 'home' - } - const workLocation = savedLocations.find(isWork) || { - address: null, - icon: 'briefcase', - type: 'work' - } - - const effectiveLocations = [ - homeLocation, - workLocation, - ...savedLocations.filter(notHomeOrWork) - ] + 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: - {effectiveLocations.map((loc, index) => ( - - - - - - - - - ))} - - {/* For adding a location. */} - - - - - - - - + ( + <> + {savedLocations.map((loc, index) => { + const isHomeOrWork = loc === homeLocation || loc === workLocation + return ( + + + + + + + {!isHomeOrWork && ( + + + + )} + + + ) + })} + + {/* For adding a new location. */} + + + + + + + + + + )} + />
) } diff --git a/lib/components/user/form-navigation-buttons.js b/lib/components/user/form-navigation-buttons.js index e81017177..9064485ab 100644 --- a/lib/components/user/form-navigation-buttons.js +++ b/lib/components/user/form-navigation-buttons.js @@ -22,11 +22,12 @@ const FormNavigationButtons = ({ okayButton }) => ( -