From 6809ce199d82678526e30c0883a5920b7d1a7f9f Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:06:00 -0500 Subject: [PATCH 001/270] improvement(connect-location-field): Display loggedInUser.savedLocations in LocationField dropdown. --- lib/components/form/connect-location-field.js | 39 ++++++++++++++++++- .../user/places/place-location-field.js | 4 +- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/components/form/connect-location-field.js b/lib/components/form/connect-location-field.js index b22262f21..8bc2dbdfa 100644 --- a/lib/components/form/connect-location-field.js +++ b/lib/components/form/connect-location-field.js @@ -1,8 +1,22 @@ +import clone from 'clone' import { connect } from 'react-redux' import * as apiActions from '../../actions/api' import * as locationActions from '../../actions/location' +import Icon from '../narrative/icon' import { getActiveSearch, getShowUserSettings } from '../../util/state' +import { isBlank } from '../../util/ui' +import { isWork } from '../../util/user' + +// Custom user location icon to be consistent with the account page. +const UserLocationIcon = ({ userLocation }) => { + const { icon = 'marker' } = userLocation + // localStorage locations that are assigned the 'work' icon + // should be rendered as 'briefcase'. + const finalIcon = icon === 'work' ? 'briefcase' : icon + + return +} /** * This higher-order component connects the target (styled) LocationField to the @@ -16,13 +30,30 @@ import { getActiveSearch, getShowUserSettings } from '../../util/state' */ export default function connectLocationField (StyledLocationField, options = {}) { // By default, set actions to empty list and do not include location. - const {actions = [], includeLocation = false} = options + const { + actions = [], + excludeSavedLocations = false, + includeLocation = false + } = options const mapStateToProps = (state, ownProps) => { const { config, currentQuery, location, transitIndex, user } = state.otp + const { loggedInUser } = state.user const { currentPosition, nearbyStops, sessionSearches } = location const activeSearch = getActiveSearch(state.otp) const query = activeSearch ? activeSearch.query : currentQuery + // Clone loggedInUserLocations with changes to conform to LocationField requirements: + // - locations with blank addresses are removed. + // - "Work" location icon name is changed to 'work', + // - location.name is filled with the location address if available, + let loggedInUserLocations = loggedInUser ? clone(loggedInUser.savedLocations) : [] + loggedInUserLocations = loggedInUserLocations.filter(loc => !isBlank(loc.address)) + loggedInUserLocations.filter(isWork).forEach(loc => { loc.icon = 'work' }) + loggedInUserLocations.forEach(loc => { loc.name = loc.address }) + + // Holds saved locations unless excluded in options. + const userSavedLocations = !excludeSavedLocations ? [...loggedInUserLocations, ...user.locations] : [] + const stateToProps = { currentPosition, geocoderConfig: config.geocoder, @@ -30,7 +61,11 @@ export default function connectLocationField (StyledLocationField, options = {}) sessionSearches, showUserSettings: getShowUserSettings(state.otp), stopsIndex: transitIndex.stops, - userLocationsAndRecentPlaces: [...user.locations, ...user.recentPlaces] + + UserLocationIconComponent: UserLocationIcon, + //see notes regarding persistence strategy + //refactor obtaining the locations. + userLocationsAndRecentPlaces: [...userSavedLocations, ...user.recentPlaces] } // 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 diff --git a/lib/components/user/places/place-location-field.js b/lib/components/user/places/place-location-field.js index 31a75fdba..ee541cfc5 100644 --- a/lib/components/user/places/place-location-field.js +++ b/lib/components/user/places/place-location-field.js @@ -74,4 +74,6 @@ const StyledLocationField = styled(LocationField)` /** * Styled LocationField for setting a favorite place locations using the geocoder. */ -export const PlaceLocationField = connectLocationField(StyledLocationField) +export const PlaceLocationField = connectLocationField(StyledLocationField, { + excludeSavedLocations: true +}) From 9b933a3befee20887eb50a12ce840620dd329bd7 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:48:12 -0500 Subject: [PATCH 002/270] refactor(connect-location-field): Refactor user locations. Update JSDoc. --- lib/components/form/connect-location-field.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/components/form/connect-location-field.js b/lib/components/form/connect-location-field.js index 8bc2dbdfa..2d6398294 100644 --- a/lib/components/form/connect-location-field.js +++ b/lib/components/form/connect-location-field.js @@ -46,10 +46,13 @@ export default function connectLocationField (StyledLocationField, options = {}) // - locations with blank addresses are removed. // - "Work" location icon name is changed to 'work', // - location.name is filled with the location address if available, - let loggedInUserLocations = loggedInUser ? clone(loggedInUser.savedLocations) : [] - loggedInUserLocations = loggedInUserLocations.filter(loc => !isBlank(loc.address)) - loggedInUserLocations.filter(isWork).forEach(loc => { loc.icon = 'work' }) - loggedInUserLocations.forEach(loc => { loc.name = loc.address }) + const loggedInUserLocations = loggedInUser + ? clone(loggedInUser.savedLocations).filter(loc => !isBlank(loc.address)) + : [] + loggedInUserLocations.forEach(loc => { + if (isWork(loc)) loc.icon = 'work' + loc.name = loc.address + }) // Holds saved locations unless excluded in options. const userSavedLocations = !excludeSavedLocations ? [...loggedInUserLocations, ...user.locations] : [] @@ -61,7 +64,6 @@ export default function connectLocationField (StyledLocationField, options = {}) sessionSearches, showUserSettings: getShowUserSettings(state.otp), stopsIndex: transitIndex.stops, - UserLocationIconComponent: UserLocationIcon, //see notes regarding persistence strategy //refactor obtaining the locations. From 3f4678728e3884a45c6617b583f9e1b84151e690 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 10 Feb 2021 17:14:29 -0500 Subject: [PATCH 003/270] refactor(connect-location-field): Refactor locations, update JSDoc. --- lib/components/form/connect-location-field.js | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/components/form/connect-location-field.js b/lib/components/form/connect-location-field.js index 2d6398294..c5e88d2cd 100644 --- a/lib/components/form/connect-location-field.js +++ b/lib/components/form/connect-location-field.js @@ -1,4 +1,3 @@ -import clone from 'clone' import { connect } from 'react-redux' import * as apiActions from '../../actions/api' @@ -8,10 +7,12 @@ import { getActiveSearch, getShowUserSettings } from '../../util/state' import { isBlank } from '../../util/ui' import { isWork } from '../../util/user' -// Custom user location icon to be consistent with the account page. +/** + * Custom icon component that renders based on the user location icon prop. + */ const UserLocationIcon = ({ userLocation }) => { const { icon = 'marker' } = userLocation - // localStorage locations that are assigned the 'work' icon + // Places from localStorage that are assigned the 'work' icon // should be rendered as 'briefcase'. const finalIcon = icon === 'work' ? 'briefcase' : icon @@ -22,8 +23,9 @@ const UserLocationIcon = ({ userLocation }) => { * 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: + * @param options Optional object with the following optional props (see defaults in code): * - actions: a list of actions to include in mapDispatchToProps + * - excludeSavedLocations: whether to not render user-saved locations * - includeLocation: whether to derive the location prop from * the active query * @returns The connected component. @@ -47,14 +49,18 @@ export default function connectLocationField (StyledLocationField, options = {}) // - "Work" location icon name is changed to 'work', // - location.name is filled with the location address if available, const loggedInUserLocations = loggedInUser - ? clone(loggedInUser.savedLocations).filter(loc => !isBlank(loc.address)) + ? loggedInUser.savedLocations + .filter(loc => !isBlank(loc.address)) + .map(loc => ({ + ...loc, + icon: isWork(loc) ? 'work' : loc.icon, + name: loc.address + })) : [] - loggedInUserLocations.forEach(loc => { - if (isWork(loc)) loc.icon = 'work' - loc.name = loc.address - }) // Holds saved locations unless excluded in options. + // see notes regarding persistence strategy + // refactor obtaining the locations. const userSavedLocations = !excludeSavedLocations ? [...loggedInUserLocations, ...user.locations] : [] const stateToProps = { @@ -65,8 +71,6 @@ export default function connectLocationField (StyledLocationField, options = {}) showUserSettings: getShowUserSettings(state.otp), stopsIndex: transitIndex.stops, UserLocationIconComponent: UserLocationIcon, - //see notes regarding persistence strategy - //refactor obtaining the locations. userLocationsAndRecentPlaces: [...userSavedLocations, ...user.recentPlaces] } // Set the location prop only if includeLocation is specified, else leave unset. From 354110882458b89289aba739642ca18e4671b48b Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 11 Feb 2021 10:45:44 -0500 Subject: [PATCH 004/270] refactor(places): Address browser console errors. --- lib/components/user/places/favorite-place-row.js | 2 +- lib/components/user/places/place-editor.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/components/user/places/favorite-place-row.js b/lib/components/user/places/favorite-place-row.js index ca0ea0944..37fa60677 100644 --- a/lib/components/user/places/favorite-place-row.js +++ b/lib/components/user/places/favorite-place-row.js @@ -122,7 +122,7 @@ FavoritePlaceRow.propTypes = { place: PropTypes.shape({ address: PropTypes.string, icon: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, + name: PropTypes.string, type: PropTypes.string.isRequired }) } diff --git a/lib/components/user/places/place-editor.js b/lib/components/user/places/place-editor.js index b9d850751..9329011f9 100644 --- a/lib/components/user/places/place-editor.js +++ b/lib/components/user/places/place-editor.js @@ -83,9 +83,9 @@ class PlaceEditor extends Component { {Object.values(CUSTOM_PLACE_TYPES).map(({ icon, text, type }) => ( Date: Thu, 11 Feb 2021 15:11:53 -0500 Subject: [PATCH 005/270] feat: Add loggedInUser.savedLocations to main panel. --- lib/actions/user.js | 3 +- lib/components/form/connect-location-field.js | 10 +- lib/components/form/user-settings.js | 152 ++++++++++++------ .../user/places/favorite-place-row.js | 127 +++++++++------ .../user/places/favorite-places-list.js | 10 +- lib/util/user.js | 13 ++ 6 files changed, 200 insertions(+), 115 deletions(-) diff --git a/lib/actions/user.js b/lib/actions/user.js index 16157daa1..8ebd6d3d0 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -453,11 +453,12 @@ export function saveUserPlace (placeToSave, placeIndex) { /** * Delete the place data at the specified index for the logged-in user. */ -export function deleteUserPlace (placeIndex) { +export function deleteUserPlace (place) { return function (dispatch, getState) { if (!confirm('Would you like to remove this place?')) return const { loggedInUser } = getState().user + const placeIndex = loggedInUser.savedLocations.indexOf(place) loggedInUser.savedLocations.splice(placeIndex, 1) dispatch(createOrUpdateUser(loggedInUser, true)) diff --git a/lib/components/form/connect-location-field.js b/lib/components/form/connect-location-field.js index c5e88d2cd..1b3597322 100644 --- a/lib/components/form/connect-location-field.js +++ b/lib/components/form/connect-location-field.js @@ -5,7 +5,7 @@ import * as locationActions from '../../actions/location' import Icon from '../narrative/icon' import { getActiveSearch, getShowUserSettings } from '../../util/state' import { isBlank } from '../../util/ui' -import { isWork } from '../../util/user' +import { convertToLocationFieldLocation } from '../../util/user' /** * Custom icon component that renders based on the user location icon prop. @@ -47,15 +47,11 @@ export default function connectLocationField (StyledLocationField, options = {}) // Clone loggedInUserLocations with changes to conform to LocationField requirements: // - locations with blank addresses are removed. // - "Work" location icon name is changed to 'work', - // - location.name is filled with the location address if available, + // - location.name is filled with the location address if available. const loggedInUserLocations = loggedInUser ? loggedInUser.savedLocations .filter(loc => !isBlank(loc.address)) - .map(loc => ({ - ...loc, - icon: isWork(loc) ? 'work' : loc.icon, - name: loc.address - })) + .map(convertToLocationFieldLocation) : [] // Holds saved locations unless excluded in options. diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 71b80e41a..4f43fc61a 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -1,14 +1,18 @@ +import clone from 'clone' import moment from 'moment' import coreUtils from '@opentripplanner/core-utils' import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import Icon from '../narrative/icon' import { forgetSearch, toggleTracking } from '../../actions/api' import { setQueryParam } from '../../actions/form' import { forgetPlace, forgetStop, setLocation } from '../../actions/map' import { setViewedStop } from '../../actions/ui' +import Icon from '../narrative/icon' +import FavoritePlaceRow from '../user/places/favorite-place-row' +import { PLACES_PATH } from '../../util/constants' +import { convertToLocationFieldLocation, isHomeOrWork } from '../../util/user' const { getDetailText, formatStoredPlaceName, matchLatLon } = coreUtils.map const { summarizeQuery } = coreUtils.query @@ -31,42 +35,46 @@ class UserSettings extends Component { _enableTracking = () => !this.props.user.trackRecent && this.props.toggleTracking(true) - _getLocations = (user) => { - const locations = [...user.locations] - if (!locations.find(l => l.type === 'work')) { - locations.push({ - id: 'work', - type: 'work', - icon: 'briefcase', - name: 'click to add', - blank: true - }) - } - if (!locations.find(l => l.type === 'home')) { - locations.push({ - id: 'home', - type: 'home', - icon: 'home', - name: 'click to add', - blank: true - }) - } - return locations + _getLocations = () => { + //see notes regarding persistence strategy + //refactor obtaining the locations. + const { loggedInUser, user } = this.props + return clone(user.locations) + // const savedLocations = loggedInUser ? clone(loggedInUser.savedLocations) : [] + // Identify saved locations for non-'forgettability' purposes. + // savedLocations.forEach(l => { l.origin = 'loggedInUser' }) + + // return [...savedLocations, ...locations] } render () { - const { storageDisclaimer, user } = this.props + const { loggedInUser, storageDisclaimer, user } = this.props const { favoriteStops, trackRecent, recentPlaces, recentSearches } = user // Clone locations in order to prevent blank locations from seeping into the // app state/store. - const locations = this._getLocations(user) + const locations = this._getLocations() + const loggedInUserLocations = loggedInUser ? loggedInUser.savedLocations : [] + // Insert additional location types before 'custom' if needed. const order = ['home', 'work', 'suggested', 'stop', 'recent'] const sortedLocations = locations .sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)) + return (
    - {sortedLocations.map(location => { + {/* Middleware locations */} + {loggedInUserLocations.map((place, index) => { + return ( + + ) + } + )} + {sortedLocations.map((location, index) => { return })}
@@ -110,15 +118,15 @@ class UserSettings extends Component {
My preferences
Remember recent searches/places? + bsStyle='link' + className={trackRecent ? 'active' : ''} + onClick={this._enableTracking}>Yes + bsStyle='link' + className={!trackRecent ? 'active' : ''} + onClick={this._disableTracking}>No
{storageDisclaimer &&
@@ -133,6 +141,44 @@ class UserSettings extends Component { } } +/** + * Wraps FavoriteLocationRow with actions to set the to/from location. + */ +class PlaceRow extends Component { + _handleClick = () => { + const { place, query, setLocation } = this.props + + const location = convertToLocationFieldLocation(place) + //NOTE: this is common with the Place component below. + // If 'to' not set and 'from' does not match location, set as 'to'. + if ( + !query.to && ( + !query.from || !matchLatLon(location, query.from) + ) + ) { + setLocation({ location, locationType: 'to' }) + } else if ( + // Vice versa for setting as 'from'. + !query.from && + !matchLatLon(location, query.to) + ) { + setLocation({ location, locationType: 'from' }) + } + } + + render () { + const { path, place } = this.props + return ( + + ) + } +} + class Place extends Component { _onSelect = () => { const { location, query, setLocation } = this.props @@ -145,13 +191,13 @@ class Place extends Component { !query.from || !matchLatLon(location, query.from) ) ) { - setLocation({ locationType: 'to', location }) + setLocation({ location, locationType: 'to' }) } else if ( // Vice versa for setting as 'from'. !query.from && !matchLatLon(location, query.to) ) { - setLocation({ locationType: 'from', location }) + setLocation({ location, locationType: 'from' }) } } } @@ -186,10 +232,10 @@ class Place extends Component {
  • {showView && + title='View stop'> } {showForget && + bsStyle='link' + className='place-clear' + onClick={this._onForget} + style={{ width: `${BUTTON_WIDTH}px` }}>Clear }
  • ) @@ -238,24 +284,27 @@ class RecentSearch extends Component {
  • + > + Clear +
  • ) } @@ -267,6 +316,7 @@ const mapStateToProps = (state, ownProps) => { return { config: state.otp.config, currentPosition: state.otp.location.currentPosition, + loggedInUser: state.user.loggedInUser, nearbyStops: state.otp.location.nearbyStops, query: state.otp.currentQuery, sessionSearches: state.otp.location.sessionSearches, @@ -277,9 +327,9 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - forgetStop, forgetPlace, forgetSearch, + forgetStop, setLocation, setQueryParam, setViewedStop, diff --git a/lib/components/user/places/favorite-place-row.js b/lib/components/user/places/favorite-place-row.js index 37fa60677..07c97f221 100644 --- a/lib/components/user/places/favorite-place-row.js +++ b/lib/components/user/places/favorite-place-row.js @@ -1,8 +1,10 @@ import PropTypes from 'prop-types' -import React from 'react' +import React, { Component } from 'react' import { Button } from 'react-bootstrap' +import { connect } from 'react-redux' import styled, { css } from 'styled-components' +import * as userActions from '../../../actions/user' import { LinkContainerWithQuery } from '../../form/connected-links' import Icon from '../../narrative/icon' @@ -16,6 +18,7 @@ const Container = styled.div` ` const PlaceButton = styled(Button)` align-items: center; + background: none; display: flex; flex: 1 0 0; overflow: hidden; @@ -25,6 +28,7 @@ const PlaceButton = styled(Button)` const GreyIcon = styled(Icon)` color: #888; + flex-shrink: 0; margin-right: 10px; ` @@ -48,6 +52,7 @@ const deleteCss = css` const DeleteButton = styled(Button)` ${deleteCss} + background: none; ` const DeletePlaceholder = styled.span` @@ -55,67 +60,83 @@ const DeletePlaceholder = styled.span` ` const MESSAGES = { - EDIT: 'Edit this place', - DELETE: 'Delete this place' + DELETE: 'Delete this place', + EDIT: 'Edit this place' } /** * Renders a clickable button for editing a user's favorite place, * and lets the user delete the place. */ -const FavoritePlaceRow = ({ isFixed, onDelete, path, place }) => { - if (place) { - const { address, icon, name, type } = place - return ( - - - { + const { deleteUserPlace, place } = this.props + deleteUserPlace(place) + } + + render () { + const { isFixed, onClick, path, place } = this.props + if (place) { + const { address, icon, name, type } = place + const to = onClick ? null : path + const ariaLabel = to && MESSAGES.EDIT //What should it be if onClick is defined? + + return ( + + - - - {name && {name}} - {address || `Set your ${type} address`} - - - - - {/* For fixed places, show Delete only if an address has been provided. */} - {(!isFixed || address) - ? ( - - - - ) - : } - - ) - } else { + + + {name && {name}} + {address || `Set your ${type} address`} + + + + + {/* For fixed places, show Delete only if an address has been provided. */} + {(!isFixed || address) + ? ( + + + + ) + : } + + ) + } else { // If no place is passed, render the Add place button instead. - return ( - - - - + return ( + + + + Add another place - - - - - ) + + + + + ) + } } } FavoritePlaceRow.propTypes = { - /** Whether the place is fixed (e.g. 'Home', 'Work' are fixed.) */ + /** Whether the place is shown, even if address is blank (e.g. 'Home', 'Work') */ isFixed: PropTypes.bool, - /** Called when the delete button is clicked. */ - onDelete: PropTypes.func, + /** Called when the "main" button is clicked. Takes precedence over the path prop. */ + onClick: PropTypes.func, /** The path to navigate to on click. */ path: PropTypes.string.isRequired, /** The place to render. */ @@ -127,4 +148,14 @@ FavoritePlaceRow.propTypes = { }) } -export default FavoritePlaceRow +// connect to redux store + +const mapStateToProps = (state, ownProps) => { + return {} +} + +const mapDispatchToProps = { + deleteUserPlace: userActions.deleteUserPlace +} + +export default connect(mapStateToProps, mapDispatchToProps)(FavoritePlaceRow) diff --git a/lib/components/user/places/favorite-places-list.js b/lib/components/user/places/favorite-places-list.js index c73a1669c..604fea3db 100644 --- a/lib/components/user/places/favorite-places-list.js +++ b/lib/components/user/places/favorite-places-list.js @@ -2,7 +2,6 @@ import React from 'react' import { ControlLabel } from 'react-bootstrap' import { connect } from 'react-redux' -import * as userActions from '../../../actions/user' import { PLACES_PATH } from '../../../util/constants' import { isHomeOrWork } from '../../../util/user' import FavoritePlaceRow from './favorite-place-row' @@ -11,7 +10,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 = ({ loggedInUser }) => { const { savedLocations } = loggedInUser return (
    @@ -21,7 +20,6 @@ const FavoritePlacesList = ({ deleteUserPlace, loggedInUser }) => { deleteUserPlace(index)} path={`${PLACES_PATH}/${index}`} place={place} /> @@ -42,8 +40,4 @@ const mapStateToProps = (state, ownProps) => { } } -const mapDispatchToProps = { - deleteUserPlace: userActions.deleteUserPlace -} - -export default connect(mapStateToProps, mapDispatchToProps)(FavoritePlacesList) +export default connect(mapStateToProps)(FavoritePlacesList) diff --git a/lib/util/user.js b/lib/util/user.js index 041a74368..cb3589d84 100644 --- a/lib/util/user.js +++ b/lib/util/user.js @@ -80,3 +80,16 @@ export function positionHomeAndWorkFirst (userData) { userData.savedLocations = reorderedLocations } + +/** + * Convert an entry from persisted user savedLocations into LocationField locations: + * - The icon for "Work" places is changed to 'work', + * - The name attribute is filled with the place address if available. + */ +export function convertToLocationFieldLocation (place) { + return { + ...place, + icon: isWork(place) ? 'work' : place.icon, + name: place.address + } +} From 48184aba477c80f9561cb8a0100c3dc137125dbd Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 12 Feb 2021 11:49:05 -0500 Subject: [PATCH 006/270] refactor(user/places/place): Extract place components for main pnl/settings. --- lib/components/form/styled.js | 4 + lib/components/form/user-settings.js | 29 ++-- .../user/places/favorite-place-row.js | 162 +++++------------- .../user/places/favorite-places-list.js | 26 +-- .../user/places/main-panel-place.js | 56 ++++++ lib/components/user/places/place.js | 151 ++++++++++++++++ 6 files changed, 286 insertions(+), 142 deletions(-) create mode 100644 lib/components/user/places/main-panel-place.js create mode 100644 lib/components/user/places/place.js diff --git a/lib/components/form/styled.js b/lib/components/form/styled.js index c7ccb2e80..21bfbf3cf 100644 --- a/lib/components/form/styled.js +++ b/lib/components/form/styled.js @@ -324,3 +324,7 @@ export const StyledDateTimeSelector = styled(DateTimeSelector)` height: 35px; } ` + +export const UnpaddedList = styled.ul` + padding: 0; +` diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 4f43fc61a..2602f0640 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -9,8 +9,10 @@ import { forgetSearch, toggleTracking } from '../../actions/api' import { setQueryParam } from '../../actions/form' import { forgetPlace, forgetStop, setLocation } from '../../actions/map' import { setViewedStop } from '../../actions/ui' +import { LinkWithQuery } from '../form/connected-links' import Icon from '../narrative/icon' -import FavoritePlaceRow from '../user/places/favorite-place-row' +import { UnpaddedList } from './styled' +import MainPanelPlace from '../user/places/main-panel-place' import { PLACES_PATH } from '../../util/constants' import { convertToLocationFieldLocation, isHomeOrWork } from '../../util/user' @@ -61,7 +63,14 @@ class UserSettings extends Component { return (
    -
      +
      + My saved places ( + + manage + + ) +
      + {/* Middleware locations */} {loggedInUserLocations.map((place, index) => { return ( @@ -77,40 +86,40 @@ class UserSettings extends Component { {sortedLocations.map((location, index) => { return })} -
    +
    Favorite stops
    -
      + {favoriteStops.length > 0 ? favoriteStops.map(location => { return }) : No favorite stops } -
    + {trackRecent && recentPlaces.length > 0 &&

    Recent places
    -
      + {recentPlaces.map(location => { return })} -
    +
    } {trackRecent && recentSearches.length > 0 &&

    Recent searches
    -
      + {recentSearches .sort((a, b) => b.timestamp - a.timestamp) .map(search => { return }) } -
    +
    }
    @@ -169,7 +178,7 @@ class PlaceRow extends Component { render () { const { path, place } = this.props return ( - { @@ -75,79 +57,19 @@ class FavoritePlaceRow extends Component { } render () { - const { isFixed, onClick, path, place } = this.props - if (place) { - const { address, icon, name, type } = place - const to = onClick ? null : path - const ariaLabel = to && MESSAGES.EDIT //What should it be if onClick is defined? - - return ( - - - - - - {name && {name}} - {address || `Set your ${type} address`} - - - - - {/* For fixed places, show Delete only if an address has been provided. */} - {(!isFixed || address) - ? ( - - - - ) - : } - - ) - } else { - // If no place is passed, render the Add place button instead. - return ( - - - - - Add another place - - - - - ) - } + const { isFixed, path, place } = this.props + return ( + + ) } } -FavoritePlaceRow.propTypes = { - /** Whether the place is shown, even if address is blank (e.g. 'Home', 'Work') */ - isFixed: PropTypes.bool, - /** Called when the "main" button is clicked. Takes precedence over the path prop. */ - onClick: PropTypes.func, - /** The path to navigate to on click. */ - path: PropTypes.string.isRequired, - /** The place to render. */ - place: PropTypes.shape({ - address: PropTypes.string, - icon: PropTypes.string.isRequired, - name: PropTypes.string, - type: PropTypes.string.isRequired - }) -} - // connect to redux store const mapStateToProps = (state, ownProps) => { diff --git a/lib/components/user/places/favorite-places-list.js b/lib/components/user/places/favorite-places-list.js index 604fea3db..8c3de2302 100644 --- a/lib/components/user/places/favorite-places-list.js +++ b/lib/components/user/places/favorite-places-list.js @@ -2,6 +2,7 @@ import React from 'react' import { ControlLabel } from 'react-bootstrap' import { connect } from 'react-redux' +import { UnpaddedList } from '../../form/styled' import { PLACES_PATH } from '../../../util/constants' import { isHomeOrWork } from '../../../util/user' import FavoritePlaceRow from './favorite-place-row' @@ -15,19 +16,20 @@ const FavoritePlacesList = ({ loggedInUser }) => { return (
    Add the places you frequent often to save time planning trips: + + {savedLocations.map((place, index) => ( + + ) + )} - {savedLocations.map((place, index) => ( - - ) - )} - - {/* For adding a new place. */} - + {/* For adding a new place. */} + +
    ) } diff --git a/lib/components/user/places/main-panel-place.js b/lib/components/user/places/main-panel-place.js new file mode 100644 index 000000000..bcf27fd2e --- /dev/null +++ b/lib/components/user/places/main-panel-place.js @@ -0,0 +1,56 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import styled from 'styled-components' + +import * as userActions from '../../../actions/user' + +import Place, { DeletePlaceholder, PlaceName } from './place' + +const StyledPlace = styled(Place)` + ${PlaceName} { + margin-left: 0.25em; + } + ${DeletePlaceholder} { + width: 40px; + } +` + +/** + * Wrapper for the Place component in the main panel that + * handles deleting the place. + */ +class MainPanelPlace extends Component { + _handleDelete = () => { + const { deleteUserPlace, place } = this.props + deleteUserPlace(place) + } + + render () { + const { isFixed, onClick, path, place } = this.props + return ( + + ) + } +} + +// connect to redux store + +const mapStateToProps = (state, ownProps) => { + return {} +} + +const mapDispatchToProps = { + deleteUserPlace: userActions.deleteUserPlace +} + +export default connect(mapStateToProps, mapDispatchToProps)(MainPanelPlace) diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js new file mode 100644 index 000000000..678072912 --- /dev/null +++ b/lib/components/user/places/place.js @@ -0,0 +1,151 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { Button } from 'react-bootstrap' +import styled from 'styled-components' + +import { LinkContainerWithQuery } from '../../form/connected-links' +import Icon from '../../narrative/icon' + +const Container = styled.li` + align-items: stretch; + display: flex; +` +export const PlaceButton = styled(Button)` + background: none; + flex: 1 0 0; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; +` + +export const PlaceIcon = styled(Icon)`` + +export const PlaceDetail = styled.span`` + +export const PlaceContent = styled.span`` + +export const PlaceName = styled.span`` + +export const PlaceText = styled.span`` + +const DeleteButton = styled(Button)` + background: none; + height: 100%; + width: 100%; +` + +export const DeletePlaceholder = styled.span`` + +const MESSAGES = { + DELETE: 'Delete this place', + EDIT: 'Edit this place' +} + +/** + * Renders a clickable button for editing a user's favorite place, + * and a button to delete the place. + */ +const Place = ({ + buttonStyle, + className, + isFixed, + largeIcon, + onClick, + onDelete, + path, + place, + placeDetailClassName, + placeTextClassName +}) => { + if (place) { + const { address, icon, name, type } = place + const to = onClick ? null : path + const ariaLabel = to && MESSAGES.EDIT // What should it be if onClick is defined? + + return ( + + + + + {largeIcon && } + + + {!largeIcon && } + {name || 'Unnamed place'} + + + {address || `Set your ${type} address`} + + + + + + {/* For fixed places, show Delete only if an address has been provided. */} + + {(!isFixed || address) && ( + + + + )} + + + ) + } else { + // If no place is passed, render the Add place button instead. + return ( + + + + + + Add another place + + + + + + ) + } +} + +Place.propTypes = { + /** The Bootstrap style to apply to buttons. */ + buttonStyle: PropTypes.string, + /** Whether the place is shown, even if address is blank (e.g. 'Home', 'Work') */ + isFixed: PropTypes.bool, + /** Whether to render icons large. */ + largeIcon: PropTypes.bool, + /** Called when the "main" button is clicked. Takes precedence over the path prop. */ + onClick: PropTypes.func, + /** Called when the Delete button is clicked. */ + onDelete: PropTypes.func, + /** The path to navigate to on click. */ + path: PropTypes.string.isRequired, + /** The place to render. */ + place: PropTypes.shape({ + address: PropTypes.string, + icon: PropTypes.string.isRequired, + name: PropTypes.string, + type: PropTypes.string.isRequired + }), + /** CSS class name for the place details. */ + placeDetailClassName: PropTypes.string, + /** CSS class name for the place name. */ + placeTextClassName: PropTypes.string +} + +export default Place From cfc1700b0aaf50d602cdc5ed8f19c4594968d458 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 12 Feb 2021 19:08:02 -0500 Subject: [PATCH 007/270] refactor(user-settings): Refactor actions for different places --- lib/components/form/connected-links.js | 13 +- lib/components/form/user-settings.js | 166 ++++++------------ .../user/places/favorite-place-row.js | 10 +- .../user/places/main-panel-place.js | 48 ++--- lib/components/user/places/place.js | 61 ++++--- lib/util/user.js | 20 +++ 6 files changed, 150 insertions(+), 168 deletions(-) diff --git a/lib/components/form/connected-links.js b/lib/components/form/connected-links.js index d7fd9da77..f37dc7b47 100644 --- a/lib/components/form/connected-links.js +++ b/lib/components/form/connected-links.js @@ -17,11 +17,14 @@ import { Link } from 'react-router-dom' * the RoutingComponent's 'to' prop. */ const withQueryParams = RoutingComponent => - ({ children, queryParams, to, ...props }) => ( - - {children} - - ) + ({ children, queryParams, to, ...props }) => + to + ? ( + + {children} + + ) + : children // For connecting to the redux store const mapStateToProps = (state, ownProps) => { diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 2602f0640..a71e356db 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -9,18 +9,16 @@ import { forgetSearch, toggleTracking } from '../../actions/api' import { setQueryParam } from '../../actions/form' import { forgetPlace, forgetStop, setLocation } from '../../actions/map' import { setViewedStop } from '../../actions/ui' +import * as userActions from '../../actions/user' import { LinkWithQuery } from '../form/connected-links' -import Icon from '../narrative/icon' import { UnpaddedList } from './styled' import MainPanelPlace from '../user/places/main-panel-place' import { PLACES_PATH } from '../../util/constants' -import { convertToLocationFieldLocation, isHomeOrWork } from '../../util/user' +import { convertToLocationFieldLocation, convertToPlace } from '../../util/user' -const { getDetailText, formatStoredPlaceName, matchLatLon } = coreUtils.map +const { matchLatLon } = coreUtils.map const { summarizeQuery } = coreUtils.query -const BUTTON_WIDTH = 40 - class UserSettings extends Component { _disableTracking = () => { const { user, toggleTracking } = this.props @@ -74,7 +72,7 @@ class UserSettings extends Component { {/* Middleware locations */} {loggedInUserLocations.map((place, index) => { return ( - { + const { deleteUserPlace, place } = this.props + deleteUserPlace(place) + } + _handleClick = () => { const { place, query, setLocation } = this.props - const location = convertToLocationFieldLocation(place) - //NOTE: this is common with the Place component below. - // If 'to' not set and 'from' does not match location, set as 'to'. - if ( - !query.to && ( - !query.from || !matchLatLon(location, query.from) - ) - ) { - setLocation({ location, locationType: 'to' }) - } else if ( - // Vice versa for setting as 'from'. - !query.from && - !matchLatLon(location, query.to) - ) { - setLocation({ location, locationType: 'from' }) - } + setFromOrToLocation(location, setLocation, query) } render () { const { path, place } = this.props return ( @@ -194,20 +202,7 @@ class Place extends Component { if (location.blank) { window.alert(`Enter origin/destination in the form (or set via map click) and click the resulting marker to set as ${location.type} location.`) } else { - // If 'to' not set and 'from' does not match location, set as 'to'. - if ( - !query.to && ( - !query.from || !matchLatLon(location, query.from) - ) - ) { - setLocation({ location, locationType: 'to' }) - } else if ( - // Vice versa for setting as 'from'. - !query.from && - !matchLatLon(location, query.to) - ) { - setLocation({ location, locationType: 'from' }) - } + setFromOrToLocation(location, setLocation, query) } } @@ -222,56 +217,15 @@ class Place extends Component { else forgetPlace(location.id) } - _isViewable = () => this.props.location.type === 'stop' - - _isForgettable = () => - ['stop', 'home', 'work', 'recent'].indexOf(this.props.location.type) !== -1 - render () { const { location } = this.props - const { blank, icon } = location - const showView = this._isViewable() - const showForget = this._isForgettable() && !blank - // Determine how much to offset width of main button (based on visibility of - // other buttons sharing the same line). - let offset = 0 - if (showView) offset += BUTTON_WIDTH - if (showForget) offset += BUTTON_WIDTH return ( -
  • - - {showView && - - } - {showForget && - - } -
  • + ) } } @@ -289,32 +243,21 @@ class RecentSearch extends Component { const { search, user } = this.props const { query, timestamp } = search const name = summarizeQuery(query, user.locations) + const timeInfo = moment(timestamp).fromNow() + + const place = { + details: timeInfo, + icon: 'clock-o', + name, + title: `${name} (${timeInfo})` + } + return ( -
  • - - -
  • + ) } } @@ -336,6 +279,7 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { + deleteUserPlace: userActions.deleteUserPlace, forgetPlace, forgetSearch, forgetStop, diff --git a/lib/components/user/places/favorite-place-row.js b/lib/components/user/places/favorite-place-row.js index 8c4b175c3..6cdeae73f 100644 --- a/lib/components/user/places/favorite-place-row.js +++ b/lib/components/user/places/favorite-place-row.js @@ -57,10 +57,16 @@ class FavoritePlaceRow extends Component { } render () { - const { isFixed, path, place } = this.props + const { path, place } = this.props + const deleteAction = { + icon: 'trash-o', + onClick: this._handleDelete, + title: 'Delete place' + } + return ( { - const { deleteUserPlace, place } = this.props - deleteUserPlace(place) - } - render () { - const { isFixed, onClick, path, place } = this.props + const { onClick, onDelete, onView, path, place } = this.props + const deleteAction = { + icon: 'trash-o', + onClick: onDelete, + title: 'Delete place' + } + const viewAction = { + icon: 'search', + onClick: onView, + title: 'View stop' + } + + const actions = [] + const isFixed = isHomeOrWork(place) + if (place.type === 'stop') actions.push(viewAction) + if (!isFixed || place.address) actions.push(deleteAction) + return ( { - return {} -} - -const mapDispatchToProps = { - deleteUserPlace: userActions.deleteUserPlace -} - -export default connect(mapStateToProps, mapDispatchToProps)(MainPanelPlace) +export default MainPanelPlace diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 678072912..8e7b6271d 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -28,7 +28,7 @@ export const PlaceName = styled.span`` export const PlaceText = styled.span`` -const DeleteButton = styled(Button)` +const ActionButton = styled(Button)` background: none; height: 100%; width: 100%; @@ -46,33 +46,33 @@ const MESSAGES = { * and a button to delete the place. */ const Place = ({ + actions, buttonStyle, className, - isFixed, largeIcon, onClick, - onDelete, path, place, placeDetailClassName, placeTextClassName }) => { + const to = onClick ? null : path if (place) { - const { address, icon, name, type } = place - const to = onClick ? null : path - const ariaLabel = to && MESSAGES.EDIT // What should it be if onClick is defined? + const { address, details, icon, name, title, type } = place + const ariaLabel = title || (to && MESSAGES.EDIT) + //What should it be if onClick is defined? return ( @@ -80,36 +80,39 @@ const Place = ({ {!largeIcon && } - {name || 'Unnamed place'} + {name || address} + - {address || `Set your ${type} address`} + {name && (details || address || `Set your ${type} address`)} + - {/* For fixed places, show Delete only if an address has been provided. */} - - {(!isFixed || address) && ( - ( + + - - - )} - + + + + ))} + ) } else { // If no place is passed, render the Add place button instead. return ( - - + + Add another place @@ -123,23 +126,27 @@ const Place = ({ } Place.propTypes = { + /** The action buttons for the place. */ + actions: PropTypes.arrayOf(PropTypes.shape({ + icon: PropTypes.string, + onClick: PropTypes.func, + title: PropTypes.string + })), /** The Bootstrap style to apply to buttons. */ buttonStyle: PropTypes.string, - /** Whether the place is shown, even if address is blank (e.g. 'Home', 'Work') */ - isFixed: PropTypes.bool, /** Whether to render icons large. */ largeIcon: PropTypes.bool, /** Called when the "main" button is clicked. Takes precedence over the path prop. */ onClick: PropTypes.func, - /** Called when the Delete button is clicked. */ - onDelete: PropTypes.func, /** The path to navigate to on click. */ - path: PropTypes.string.isRequired, + path: PropTypes.string, /** The place to render. */ place: PropTypes.shape({ address: PropTypes.string, + details: PropTypes.string, icon: PropTypes.string.isRequired, name: PropTypes.string, + title: PropTypes.string, type: PropTypes.string.isRequired }), /** CSS class name for the place details. */ diff --git a/lib/util/user.js b/lib/util/user.js index cb3589d84..25e1c2814 100644 --- a/lib/util/user.js +++ b/lib/util/user.js @@ -1,3 +1,7 @@ +import coreUtils from '@opentripplanner/core-utils' + +const { getDetailText, formatStoredPlaceName } = coreUtils.map + /** * Determines whether a loggedInUser is a new user * that needs to complete the new account wizard. @@ -93,3 +97,19 @@ export function convertToLocationFieldLocation (place) { name: place.address } } + +/** + * Convert a LocationField entry to a persisted user savedLocations: + * - The icon for "Work" places is changed to 'briefcase', + * - The address attribute is filled with the 'name' if available. + */ +export function convertToPlace (location) { + return { + ...location, + address: location.name, + details: getDetailText(location), + icon: isWork(location) ? 'briefcase' : location.icon, + name: formatStoredPlaceName(location, false), + title: formatStoredPlaceName(location) + } +} From bf793ab8f73907e2262e53ad5fe28a30b8740306 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 12 Feb 2021 21:12:06 -0500 Subject: [PATCH 008/270] refactor(mastarm commit): user/place Refactor actions. Rename files. --- lib/components/form/user-settings.js | 30 +-- .../user/existing-account-display.js | 4 +- lib/components/user/new-account-wizard.js | 4 +- ...-places-list.js => favorite-place-list.js} | 12 +- ...avorite-place-row.js => favorite-place.js} | 28 ++- .../user/places/main-panel-place.js | 34 ++-- lib/components/user/places/place.js | 174 +++++++++++------- 7 files changed, 160 insertions(+), 126 deletions(-) rename lib/components/user/places/{favorite-places-list.js => favorite-place-list.js} (73%) rename lib/components/user/places/{favorite-place-row.js => favorite-place.js} (77%) diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index a71e356db..4d3ad4371 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -5,16 +5,16 @@ import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import { forgetSearch, toggleTracking } from '../../actions/api' -import { setQueryParam } from '../../actions/form' -import { forgetPlace, forgetStop, setLocation } from '../../actions/map' -import { setViewedStop } from '../../actions/ui' +import * as apiActions from '../../actions/api' +import * as formActions from '../../actions/form' +import * as mapActions from '../../actions/map' +import * as uiActions from '../../actions/ui' import * as userActions from '../../actions/user' import { LinkWithQuery } from '../form/connected-links' -import { UnpaddedList } from './styled' import MainPanelPlace from '../user/places/main-panel-place' import { PLACES_PATH } from '../../util/constants' import { convertToLocationFieldLocation, convertToPlace } from '../../util/user' +import { UnpaddedList } from './styled' const { matchLatLon } = coreUtils.map const { summarizeQuery } = coreUtils.query @@ -196,6 +196,9 @@ class SavedPlace extends Component { } } +/** + * Wraps MainPanelPlace for places from localStorage (recents, favorite stops...). + */ class Place extends Component { _onSelect = () => { const { location, query, setLocation } = this.props @@ -230,6 +233,9 @@ class Place extends Component { } } +/** + * Wraps MainPanelPlace for recent trip requests. + */ class RecentSearch extends Component { _onSelect = () => { const { search, setQueryParam } = this.props @@ -280,13 +286,13 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { deleteUserPlace: userActions.deleteUserPlace, - forgetPlace, - forgetSearch, - forgetStop, - setLocation, - setQueryParam, - setViewedStop, - toggleTracking + forgetPlace: mapActions.forgetPlace, + forgetSearch: apiActions.forgetSearch, + forgetStop: mapActions.forgetStop, + setLocation: mapActions.setLocation, + setQueryParam: formActions.setQueryParam, + setViewedStop: uiActions.setViewedStop, + toggleTracking: apiActions.toggleTracking } export default connect(mapStateToProps, mapDispatchToProps)(UserSettings) diff --git a/lib/components/user/existing-account-display.js b/lib/components/user/existing-account-display.js index 8ee8b2107..082163697 100644 --- a/lib/components/user/existing-account-display.js +++ b/lib/components/user/existing-account-display.js @@ -1,7 +1,7 @@ import React from 'react' import NotificationPrefsPane from './notification-prefs-pane' -import FavoritePlacesList from './places/favorite-places-list' +import FavoritePlaceList from './places/favorite-place-list' import StackedPaneDisplay from './stacked-pane-display' import TermsOfUsePane from './terms-of-use-pane' @@ -16,7 +16,7 @@ const ExistingAccountDisplay = props => { const { onCancel } = props const paneSequence = [ { - pane: FavoritePlacesList, + pane: FavoritePlaceList, props, title: 'My locations' }, diff --git a/lib/components/user/new-account-wizard.js b/lib/components/user/new-account-wizard.js index 2ff4553c8..fa91ed659 100644 --- a/lib/components/user/new-account-wizard.js +++ b/lib/components/user/new-account-wizard.js @@ -2,7 +2,7 @@ import React, { Component } from 'react' import AccountSetupFinishPane from './account-setup-finish-pane' import NotificationPrefsPane from './notification-prefs-pane' -import FavoritePlacesList from './places/favorite-places-list' +import FavoritePlaceList from './places/favorite-place-list' import SequentialPaneDisplay from './sequential-pane-display' import TermsOfUsePane from './terms-of-use-pane' import VerifyEmailPane from './verify-email-pane' @@ -62,7 +62,7 @@ class NewAccountWizard extends Component { }, places: { nextId: 'finish', - pane: FavoritePlacesList, + pane: FavoritePlaceList, prevId: 'notifications', props, title: 'Add your locations' diff --git a/lib/components/user/places/favorite-places-list.js b/lib/components/user/places/favorite-place-list.js similarity index 73% rename from lib/components/user/places/favorite-places-list.js rename to lib/components/user/places/favorite-place-list.js index 8c3de2302..3efc216a1 100644 --- a/lib/components/user/places/favorite-places-list.js +++ b/lib/components/user/places/favorite-place-list.js @@ -4,22 +4,20 @@ import { connect } from 'react-redux' import { UnpaddedList } from '../../form/styled' import { PLACES_PATH } from '../../../util/constants' -import { isHomeOrWork } from '../../../util/user' -import FavoritePlaceRow from './favorite-place-row' +import FavoritePlace from './favorite-place' /** * 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 = ({ loggedInUser }) => { +const FavoritePlaceList = ({ loggedInUser }) => { const { savedLocations } = loggedInUser return (
    Add the places you frequent often to save time planning trips: {savedLocations.map((place, index) => ( - { )} {/* For adding a new place. */} - +
    ) @@ -42,4 +40,4 @@ const mapStateToProps = (state, ownProps) => { } } -export default connect(mapStateToProps)(FavoritePlacesList) +export default connect(mapStateToProps)(FavoritePlaceList) diff --git a/lib/components/user/places/favorite-place-row.js b/lib/components/user/places/favorite-place.js similarity index 77% rename from lib/components/user/places/favorite-place-row.js rename to lib/components/user/places/favorite-place.js index 6cdeae73f..57c104c42 100644 --- a/lib/components/user/places/favorite-place-row.js +++ b/lib/components/user/places/favorite-place.js @@ -3,13 +3,14 @@ import { connect } from 'react-redux' import styled from 'styled-components' import * as userActions from '../../../actions/user' - import Place, { - DeletePlaceholder, + ActionButtonContainer, + EDIT_LABEL, + getActionsForPlace, PlaceButton, PlaceContent, PlaceDetail, - PlaceIcon + Icon } from './place' const FIELD_HEIGHT_PX = '60px' @@ -36,21 +37,21 @@ const StyledPlace = styled(Place)` color: #888; display: block; } - ${PlaceIcon} { + ${Icon} { color: #888; flex-shrink: 0; } - ${DeletePlaceholder} { + ${ActionButtonContainer} { margin-left: 4px; width: ${FIELD_HEIGHT_PX}; } ` /** - * Wrapper for the Place component in FavoritePlacesList that + * Wrapper for the Place component in FavoritePlaceList that * handles deleting the place. */ -class FavoritePlaceRow extends Component { +class FavoritePlace extends Component { _handleDelete = () => { const { deleteUserPlace, place } = this.props deleteUserPlace(place) @@ -58,19 +59,16 @@ class FavoritePlaceRow extends Component { render () { const { path, place } = this.props - const deleteAction = { - icon: 'trash-o', - onClick: this._handleDelete, - title: 'Delete place' - } + const label = place && EDIT_LABEL return ( ) } @@ -86,4 +84,4 @@ const mapDispatchToProps = { deleteUserPlace: userActions.deleteUserPlace } -export default connect(mapStateToProps, mapDispatchToProps)(FavoritePlaceRow) +export default connect(mapStateToProps, mapDispatchToProps)(FavoritePlace) diff --git a/lib/components/user/places/main-panel-place.js b/lib/components/user/places/main-panel-place.js index ec431e1ca..905e18279 100644 --- a/lib/components/user/places/main-panel-place.js +++ b/lib/components/user/places/main-panel-place.js @@ -1,8 +1,13 @@ import React, { Component } from 'react' import styled from 'styled-components' -import { isHomeOrWork } from '../../../util/user' -import Place, { DeletePlaceholder, PlaceDetail, PlaceName } from './place' +import Place, { + ActionButtonContainer, + EDIT_LABEL, + getActionsForPlace, + PlaceDetail, + PlaceName +} from './place' const StyledPlace = styled(Place)` ${PlaceName} { @@ -12,7 +17,7 @@ const StyledPlace = styled(Place)` display: block; height: 100%; } - ${DeletePlaceholder} { + ${ActionButtonContainer} { width: 40px; } ` @@ -24,25 +29,17 @@ const StyledPlace = styled(Place)` class MainPanelPlace extends Component { render () { const { onClick, onDelete, onView, path, place } = this.props - const deleteAction = { - icon: 'trash-o', - onClick: onDelete, - title: 'Delete place' - } - const viewAction = { - icon: 'search', - onClick: onView, - title: 'View stop' - } - const actions = [] - const isFixed = isHomeOrWork(place) - if (place.type === 'stop') actions.push(viewAction) - if (!isFixed || place.address) actions.push(deleteAction) + // Determine title and aria label. + const isPlanAction = !!onClick + const planLabel = 'Plan an itinerary using this place' + const label = isPlanAction ? planLabel : EDIT_LABEL + const title = isPlanAction ? `${place.title || place.address}\n${planLabel}` : EDIT_LABEL return ( ) } diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 8e7b6271d..249974c74 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -4,7 +4,8 @@ import { Button } from 'react-bootstrap' import styled from 'styled-components' import { LinkContainerWithQuery } from '../../form/connected-links' -import Icon from '../../narrative/icon' +import NarrativeIcon from '../../narrative/icon' +import { isHomeOrWork } from '../../../util/user' const Container = styled.li` align-items: stretch; @@ -18,7 +19,7 @@ export const PlaceButton = styled(Button)` text-overflow: ellipsis; ` -export const PlaceIcon = styled(Icon)`` +export const Icon = styled(NarrativeIcon)`` export const PlaceDetail = styled.span`` @@ -34,19 +35,49 @@ const ActionButton = styled(Button)` width: 100%; ` -export const DeletePlaceholder = styled.span`` +export const ActionButtonContainer = styled.span`` -const MESSAGES = { - DELETE: 'Delete this place', - EDIT: 'Edit this place' +export const EDIT_LABEL = 'Edit this place' + +/** + * Obtains the actions (e.g. delete, view) for the given place and handlers: + * - All places can be deleted, except Home and Work with empty addresses. + * - Only 'stop' locations can be 'viewed'. + */ +export function getActionsForPlace (place, onDelete, onView) { + if (!place) return [] + + const deleteAction = { + icon: 'trash-o', + onClick: onDelete, + title: 'Delete place' + } + const viewAction = { + icon: 'search', + onClick: onView, + title: 'View stop' + } + + const actions = [] + + const isFixed = isHomeOrWork(place) + if (onView && place.type === 'stop') { + actions.push(viewAction) + } + if (onDelete && (!isFixed || place.address)) { + actions.push(deleteAction) + } + + return actions } /** * Renders a clickable button for editing a user's favorite place, - * and a button to delete the place. + * and buttons for the provided actions (e.g. view, delete). */ const Place = ({ actions, + ariaLabel, buttonStyle, className, largeIcon, @@ -54,75 +85,74 @@ const Place = ({ path, place, placeDetailClassName, - placeTextClassName + placeTextClassName, + title }) => { const to = onClick ? null : path + const iconSize = largeIcon && '2x' + let placeButton if (place) { - const { address, details, icon, name, title, type } = place - const ariaLabel = title || (to && MESSAGES.EDIT) - //What should it be if onClick is defined? - - return ( - - - - - {largeIcon && } - - - {!largeIcon && } - {name || address} - - - - {name && (details || address || `Set your ${type} address`)} - - - - - - - {/* Action buttons */} - {actions && actions.map(({ icon: actionIcon, onClick: onAction, title }, index) => ( - + const { address, details, icon, name, type } = place + placeButton = ( + + + {largeIcon && } + + + {!largeIcon && } + {name || address} + + + + {name && (details || address || `Set your ${type} address`)} + + + + ) + } else { + placeButton = ( + + + + Add another place + + + ) + } + + return ( + + + {placeButton} + + + {/* Action buttons. If none, render a placeholder. */} + {actions && actions.length + ? actions.map(({ icon: actionIcon, onClick: onAction, title: actionTitle }, index) => ( + - + - - ))} - - - ) - } else { - // If no place is passed, render the Add place button instead. - return ( - - - - - - Add another place - - - - - - ) - } + + )) + : + } +
    + ) } Place.propTypes = { @@ -132,6 +162,8 @@ Place.propTypes = { onClick: PropTypes.func, title: PropTypes.string })), + /** The aria-label for the main button */ + ariaLabel: PropTypes.string, /** The Bootstrap style to apply to buttons. */ buttonStyle: PropTypes.string, /** Whether to render icons large. */ @@ -147,12 +179,14 @@ Place.propTypes = { icon: PropTypes.string.isRequired, name: PropTypes.string, title: PropTypes.string, - type: PropTypes.string.isRequired + type: PropTypes.string }), /** CSS class name for the place details. */ placeDetailClassName: PropTypes.string, /** CSS class name for the place name. */ - placeTextClassName: PropTypes.string + placeTextClassName: PropTypes.string, + /** The title for the main button */ + title: PropTypes.string } export default Place From 71f1ad8e6626b7af1e8a8de9bd8ac6949953c454 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 12 Feb 2021 21:15:14 -0500 Subject: [PATCH 009/270] refactor(MainPanelPlace): Convert to func component --- .../user/places/main-panel-place.js | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/lib/components/user/places/main-panel-place.js b/lib/components/user/places/main-panel-place.js index 905e18279..fc94a87ea 100644 --- a/lib/components/user/places/main-panel-place.js +++ b/lib/components/user/places/main-panel-place.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React from 'react' import styled from 'styled-components' import Place, { @@ -26,31 +26,33 @@ const StyledPlace = styled(Place)` * Wrapper for the Place component in the main panel that * handles deleting the place. */ -class MainPanelPlace extends Component { - render () { - const { onClick, onDelete, onView, path, place } = this.props +const MainPanelPlace = ({ + onClick, + onDelete, + onView, + path, + place +}) => { + // Determine title and aria label. + const isPlanAction = !!onClick + const planLabel = 'Plan an itinerary using this place' + const label = isPlanAction ? planLabel : EDIT_LABEL + const title = isPlanAction ? `${place.title || place.address}\n${planLabel}` : EDIT_LABEL - // Determine title and aria label. - const isPlanAction = !!onClick - const planLabel = 'Plan an itinerary using this place' - const label = isPlanAction ? planLabel : EDIT_LABEL - const title = isPlanAction ? `${place.title || place.address}\n${planLabel}` : EDIT_LABEL - - return ( - - ) - } + return ( + + ) } export default MainPanelPlace From 2bdb282f6df674a210b45a5ceb4db9b7dd86130a Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 17 Feb 2021 17:23:04 -0500 Subject: [PATCH 010/270] feat(UserSettings): Display loggedInUser's recent trip requests. --- lib/actions/user.js | 30 ++- lib/components/form/user-settings.js | 294 +++++++++++++++++---------- lib/reducers/create-user-reducer.js | 7 + 3 files changed, 217 insertions(+), 114 deletions(-) diff --git a/lib/actions/user.js b/lib/actions/user.js index 8ebd6d3d0..9abb1597d 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -15,12 +15,14 @@ import { isNewUser, positionHomeAndWorkFirst } from '../util/user' // Middleware API paths. const API_MONITORED_TRIP_PATH = '/api/secure/monitoredtrip' +const API_TRIP_HISTORY_PATH = '/api/secure/triprequests' const API_OTPUSER_PATH = '/api/secure/user' const API_OTPUSER_VERIFY_SMS_SUBPATH = '/verify_sms' const setAccessToken = createAction('SET_ACCESS_TOKEN') const setCurrentUser = createAction('SET_CURRENT_USER') const setCurrentUserMonitoredTrips = createAction('SET_CURRENT_USER_MONITORED_TRIPS') +const setCurrentUserTripRequests = createAction('SET_CURRENT_USER_TRIP_REQUESTS') const setLastPhoneSmsRequest = createAction('SET_LAST_PHONE_SMS_REQUEST') export const setPathBeforeSignIn = createAction('SET_PATH_BEFORE_SIGNIN') export const clearItineraryExistence = createAction('CLEAR_ITINERARY_EXISTENCE') @@ -95,6 +97,7 @@ function setUser (user, fetchTrips) { if (fetchTrips) { dispatch(fetchMonitoredTrips()) + dispatch(fetchTripRequests()) } } } @@ -207,9 +210,30 @@ export function resendVerificationEmail () { } /** - * Fetches the saved/monitored trips for a user. - * We use the accessToken to fetch the data regardless of - * whether the process to populate state.user is completed or not. + * Fetches the most recent (default 10) trip history for a user. + */ +export function fetchTripRequests () { + return async function (dispatch, getState) { + const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(getState()) + const requestUrl = `${apiBaseUrl}${API_TRIP_HISTORY_PATH}?userId=${loggedInUser.id}` + + const { data: requests, status: emptyStatus } = await secureFetch(`${requestUrl}&limit=0`, accessToken, apiKey, 'GET') + const DEFAULT_LIMIT = 10 + + //Is .then() better? + //Also figure out whether to convert trip requests into search objects here. + //Also figure out a way to deal with batch ids and remove requests of the same batch. + if (emptyStatus === 'success') { + const { data: trips, status } = await secureFetch(`${requestUrl}&offset=${requests.total - DEFAULT_LIMIT}`, accessToken, apiKey, 'GET') + if (status === 'success') { + dispatch(setCurrentUserTripRequests(trips.data)) + } + } + } +} + +/** + * Fetches the most saved/monitored trips for a user. */ export function fetchMonitoredTrips () { return async function (dispatch, getState) { diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 4d3ad4371..6887c9538 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -1,6 +1,7 @@ -import clone from 'clone' +/* eslint-disable complexity */ import moment from 'moment' import coreUtils from '@opentripplanner/core-utils' +import qs from 'qs' import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' @@ -17,7 +18,7 @@ import { convertToLocationFieldLocation, convertToPlace } from '../../util/user' import { UnpaddedList } from './styled' const { matchLatLon } = coreUtils.map -const { summarizeQuery } = coreUtils.query +const { planParamsToQuery, summarizeQuery } = coreUtils.query class UserSettings extends Component { _disableTracking = () => { @@ -35,116 +36,182 @@ class UserSettings extends Component { _enableTracking = () => !this.props.user.trackRecent && this.props.toggleTracking(true) - _getLocations = () => { - //see notes regarding persistence strategy - //refactor obtaining the locations. - const { loggedInUser, user } = this.props - return clone(user.locations) - // const savedLocations = loggedInUser ? clone(loggedInUser.savedLocations) : [] - // Identify saved locations for non-'forgettability' purposes. - // savedLocations.forEach(l => { l.origin = 'loggedInUser' }) - - // return [...savedLocations, ...locations] + _getLocations = user => { + const locations = [...user.locations] + if (!locations.find(l => l.type === 'work')) { + locations.push({ + blank: true, + icon: 'briefcase', + id: 'work', + name: 'click to add', + type: 'work' + }) + } + if (!locations.find(l => l.type === 'home')) { + locations.push({ + blank: true, + icon: 'home', + id: 'home', + name: 'click to add', + type: 'home' + }) + } + return locations } render () { - const { loggedInUser, storageDisclaimer, user } = this.props - const { favoriteStops, trackRecent, recentPlaces, recentSearches } = user - // Clone locations in order to prevent blank locations from seeping into the - // app state/store. - const locations = this._getLocations() - const loggedInUserLocations = loggedInUser ? loggedInUser.savedLocations : [] - // Insert additional location types before 'custom' if needed. - const order = ['home', 'work', 'suggested', 'stop', 'recent'] - const sortedLocations = locations - .sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)) + const { config, loggedInUser, loggedInUserTripRequests, persistenceStrategy, storageDisclaimer, user } = this.props + console.log(loggedInUserTripRequests) - return ( -
    -
    - My saved places ( - - manage - - ) -
    - - {/* Middleware locations */} - {loggedInUserLocations.map((place, index) => { - return ( - - ) + if (loggedInUser && persistenceStrategy === 'otp_middleware') { //use constants + const { trackRecent, recentSearches } = user//remove this + const loggedInUserLocations = loggedInUser ? loggedInUser.savedLocations : [] + return ( +
    +
    + My saved places ( + + manage + + ) +
    + + {loggedInUserLocations.map((place, index) => { + return ( + + ) + } + )} + + + {/* Show the triprequests if middleware is enabled strategy and user has opted in. */} +
    + {loggedInUser.storeTripHistory && loggedInUserTripRequests && loggedInUserTripRequests.length > 0 && +
    +
    +
    Recent searches
    + + {loggedInUserTripRequests + .map(tripReq => ({ + id: tripReq.id, + query: planParamsToQuery(qs.parse(tripReq.queryParams)), + timestamp: tripReq.dateCreated, + url: `${config.api.host}${config.api.path}/plan?${tripReq.queryParams}` + })) + .sort((a, b) => b.timestamp - a.timestamp) + .map(search => { + return + }) + } + +
    } - )} - {sortedLocations.map((location, index) => { - return - })} - -
    -
    Favorite stops
    - - {favoriteStops.length > 0 - ? favoriteStops.map(location => { - return - }) - : No favorite stops + {trackRecent && recentSearches.length > 0 && +
    +
    +
    Recent searches
    + + {recentSearches + .sort((a, b) => b.timestamp - a.timestamp) + .map(search => { + console.log(search) + return + }) + } + +
    } -
    - {trackRecent && recentPlaces.length > 0 && -
    -
    -
    Recent places
    - - {recentPlaces.map(location => { - return - })} - -
    - } - {trackRecent && recentSearches.length > 0 && -
    -
    -
    Recent searches
    - - {recentSearches - .sort((a, b) => b.timestamp - a.timestamp) - .map(search => { - return - }) - } - -
    - } -
    -
    -
    My preferences
    - Remember recent searches/places? - -
    - {storageDisclaimer && -
    -
    -
    - {storageDisclaimer} + ) + } else if (persistenceStrategy === 'localStorage') { + const { favoriteStops, trackRecent, recentPlaces, recentSearches } = user + // Clone locations in order to prevent blank locations from seeping into the + // app state/store. + const locations = this._getLocations(user) + // Insert additional location types before 'custom' if needed. + const order = ['home', 'work', 'suggested', 'stop', 'recent'] + const sortedLocations = locations + .sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)) + return ( +
    +
    + My saved places +
    + + {sortedLocations.map((location, index) => { + return + })} + +
    + +
    Favorite stops
    + + {favoriteStops.length > 0 + ? favoriteStops.map(location => { + return + }) + : No favorite stops + } + + + {trackRecent && recentPlaces.length > 0 && +
    +
    +
    Recent places
    + + {recentPlaces.map(location => { + return + })} +
    + } + {trackRecent && recentSearches.length > 0 && +
    +
    +
    Recent searches
    + + {recentSearches + .sort((a, b) => b.timestamp - a.timestamp) + .map(search => { + return + }) + } + +
    + } +
    +
    +
    My preferences
    + Remember recent searches/places? + +
    - } -
    - ) + {storageDisclaimer && +
    +
    +
    + {storageDisclaimer} +
    +
    + } +
    + ) + } + + return null } } @@ -271,16 +338,21 @@ class RecentSearch extends Component { // connect to redux store const mapStateToProps = (state, ownProps) => { + const { config, currentQuery, location, transitIndex, user } = state.otp + const { language, persistence = {} } = config + console.log(config) return { - config: state.otp.config, - currentPosition: state.otp.location.currentPosition, + config, + currentPosition: location.currentPosition, loggedInUser: state.user.loggedInUser, - nearbyStops: state.otp.location.nearbyStops, - query: state.otp.currentQuery, - sessionSearches: state.otp.location.sessionSearches, - stopsIndex: state.otp.transitIndex.stops, - storageDisclaimer: state.otp.config.language.storageDisclaimer, - user: state.otp.user + loggedInUserTripRequests: state.user.loggedInUserTripRequests, + nearbyStops: location.nearbyStops, + persistenceStrategy: persistence.enabled && persistence.strategy, + query: currentQuery, + sessionSearches: location.sessionSearches, + stopsIndex: transitIndex.stops, + storageDisclaimer: language.storageDisclaimer, + user } } diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index a542ac625..f034d7252 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -12,6 +12,7 @@ function createUserReducer () { }, loggedInUser: null, loggedInUserMonitoredTrips: null, + loggedInUserTripRequests: null, pathBeforeSignIn: null } @@ -34,6 +35,12 @@ function createUserReducer () { }) } + case 'SET_CURRENT_USER_TRIP_REQUESTS': { + return update(state, { + loggedInUserTripRequests: { $set: action.payload } + }) + } + case 'SET_PATH_BEFORE_SIGNIN': { return update(state, { pathBeforeSignIn: { $set: action.payload } From f54b11151b633427cf6595a9d68c3ab23556de95 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 18 Feb 2021 16:00:33 -0500 Subject: [PATCH 011/270] refactor(actions/api): Log first trip request for batch mw inquiries. --- lib/actions/api.js | 7 +++++- lib/components/form/user-settings.js | 22 +++------------- lib/components/user/places/favorite-place.js | 5 ++-- .../user/places/main-panel-place.js | 6 ++--- lib/components/user/places/place.js | 25 ++++++++----------- 5 files changed, 26 insertions(+), 39 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index 2e5df722e..10a92d739 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -150,6 +150,11 @@ export function routingQuery (searchId = null) { // // The advantage of using non-realtime route is that the middleware will be able to // record and provide the theoretical itinerary summary without having to query OTP again. + // + // If storeTripHistory is enabled, we only send the token for logging for the first request, + // subsequent requests from the `iterations` variable are identical + // (except for the mode combinations -- they are regenerated for every inquiry). + // // FIXME: Interestingly, and this could be from a side effect elsewhere, // when a logged-in user refreshes the page, the trip request in the URL is not recorded again // (state.user stays unpopulated until after this function is called). @@ -159,7 +164,7 @@ export function routingQuery (searchId = null) { user.loggedInUser && user.loggedInUser.storeTripHistory - fetch(constructRoutingQuery(otpState, true), getOtpFetchOptions(state, storeTripHistory)) + fetch(constructRoutingQuery(otpState, true), getOtpFetchOptions(state, storeTripHistory && i === 0)) .then(getJsonAndCheckResponse) .then(json => { // FIXME: This is only performed when ignoring realtimeupdates currently, just diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 6887c9538..adc20f926 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -64,7 +64,6 @@ class UserSettings extends Component { console.log(loggedInUserTripRequests) if (loggedInUser && persistenceStrategy === 'otp_middleware') { //use constants - const { trackRecent, recentSearches } = user//remove this const loggedInUserLocations = loggedInUser ? loggedInUser.savedLocations : [] return (
    @@ -90,7 +89,6 @@ class UserSettings extends Component { {/* Show the triprequests if middleware is enabled strategy and user has opted in. */} -
    {loggedInUser.storeTripHistory && loggedInUserTripRequests && loggedInUserTripRequests.length > 0 &&

    @@ -98,6 +96,7 @@ class UserSettings extends Component { {loggedInUserTripRequests .map(tripReq => ({ + canDelete: false, id: tripReq.id, query: planParamsToQuery(qs.parse(tripReq.queryParams)), timestamp: tripReq.dateCreated, @@ -111,21 +110,6 @@ class UserSettings extends Component {
    } - {trackRecent && recentSearches.length > 0 && -
    -
    -
    Recent searches
    - - {recentSearches - .sort((a, b) => b.timestamp - a.timestamp) - .map(search => { - console.log(search) - return - }) - } - -
    - }
    ) } else if (persistenceStrategy === 'localStorage') { @@ -314,7 +298,7 @@ class RecentSearch extends Component { render () { const { search, user } = this.props - const { query, timestamp } = search + const { canDelete = true, query, timestamp } = search const name = summarizeQuery(query, user.locations) const timeInfo = moment(timestamp).fromNow() @@ -328,7 +312,7 @@ class RecentSearch extends Component { return ( ) diff --git a/lib/components/user/places/favorite-place.js b/lib/components/user/places/favorite-place.js index 57c104c42..f75946552 100644 --- a/lib/components/user/places/favorite-place.js +++ b/lib/components/user/places/favorite-place.js @@ -4,7 +4,8 @@ import styled from 'styled-components' import * as userActions from '../../../actions/user' import Place, { - ActionButtonContainer, + ActionButton, + ActionButtonPlaceholder, EDIT_LABEL, getActionsForPlace, PlaceButton, @@ -41,7 +42,7 @@ const StyledPlace = styled(Place)` color: #888; flex-shrink: 0; } - ${ActionButtonContainer} { + ${ActionButton}, ${ActionButtonPlaceholder} { margin-left: 4px; width: ${FIELD_HEIGHT_PX}; } diff --git a/lib/components/user/places/main-panel-place.js b/lib/components/user/places/main-panel-place.js index fc94a87ea..b7acf1a22 100644 --- a/lib/components/user/places/main-panel-place.js +++ b/lib/components/user/places/main-panel-place.js @@ -2,7 +2,7 @@ import React from 'react' import styled from 'styled-components' import Place, { - ActionButtonContainer, + ActionButton, EDIT_LABEL, getActionsForPlace, PlaceDetail, @@ -17,7 +17,7 @@ const StyledPlace = styled(Place)` display: block; height: 100%; } - ${ActionButtonContainer} { + ${ActionButton} { width: 40px; } ` @@ -35,7 +35,7 @@ const MainPanelPlace = ({ }) => { // Determine title and aria label. const isPlanAction = !!onClick - const planLabel = 'Plan an itinerary using this place' + const planLabel = 'Plan an itinerary using this item' const label = isPlanAction ? planLabel : EDIT_LABEL const title = isPlanAction ? `${place.title || place.address}\n${planLabel}` : EDIT_LABEL diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 249974c74..0fd4468ad 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -29,13 +29,12 @@ export const PlaceName = styled.span`` export const PlaceText = styled.span`` -const ActionButton = styled(Button)` +export const ActionButton = styled(Button)` background: none; height: 100%; - width: 100%; ` -export const ActionButtonContainer = styled.span`` +export const ActionButtonPlaceholder = styled.span`` export const EDIT_LABEL = 'Edit this place' @@ -138,18 +137,16 @@ const Place = ({ {/* Action buttons. If none, render a placeholder. */} {actions && actions.length ? actions.map(({ icon: actionIcon, onClick: onAction, title: actionTitle }, index) => ( - - - - - + + + )) - : + : } ) From a4c4326c4f6c5ef8b1f19bae321203800b13466e Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 18 Feb 2021 21:21:37 -0500 Subject: [PATCH 012/270] refactor(create-user-reducer): Start merging local and middleware storage strategies. --- lib/actions/user.js | 24 ++++- lib/components/form/user-settings.js | 145 +++++++++++++++------------ lib/reducers/create-user-reducer.js | 66 +++++++++++- 3 files changed, 165 insertions(+), 70 deletions(-) diff --git a/lib/actions/user.js b/lib/actions/user.js index 9abb1597d..2675ffe7a 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -209,6 +209,23 @@ export function resendVerificationEmail () { } } +/** + * Converts a middleware trip request to the search format. + */ +function convertRequestToSearch (config) { + return function (tripRequest) { + const { id, dateCreated, queryParams } = tripRequest + const { host, path } = config.api + return { + canDelete: false, + id, + query: planParamsToQuery(qs.parse(queryParams)), + timestamp: dateCreated, + url: `${host}${path}/plan?${queryParams}` + } + } +} + /** * Fetches the most recent (default 10) trip history for a user. */ @@ -220,13 +237,12 @@ export function fetchTripRequests () { const { data: requests, status: emptyStatus } = await secureFetch(`${requestUrl}&limit=0`, accessToken, apiKey, 'GET') const DEFAULT_LIMIT = 10 - //Is .then() better? - //Also figure out whether to convert trip requests into search objects here. - //Also figure out a way to deal with batch ids and remove requests of the same batch. if (emptyStatus === 'success') { const { data: trips, status } = await secureFetch(`${requestUrl}&offset=${requests.total - DEFAULT_LIMIT}`, accessToken, apiKey, 'GET') if (status === 'success') { - dispatch(setCurrentUserTripRequests(trips.data)) + // Convert tripRequests to search format. + const convertedTrips = trips.data.map(convertRequestToSearch(getState().otp.config)) + dispatch(setCurrentUserTripRequests(convertedTrips)) } } } diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index adc20f926..32f4c2b27 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -1,7 +1,6 @@ /* eslint-disable complexity */ import moment from 'moment' import coreUtils from '@opentripplanner/core-utils' -import qs from 'qs' import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' @@ -14,17 +13,17 @@ import * as userActions from '../../actions/user' import { LinkWithQuery } from '../form/connected-links' import MainPanelPlace from '../user/places/main-panel-place' import { PLACES_PATH } from '../../util/constants' -import { convertToLocationFieldLocation, convertToPlace } from '../../util/user' +import { convertToLocationFieldLocation, convertToPlace, isHome, isWork } from '../../util/user' import { UnpaddedList } from './styled' const { matchLatLon } = coreUtils.map -const { planParamsToQuery, summarizeQuery } = coreUtils.query +const { summarizeQuery } = coreUtils.query class UserSettings extends Component { _disableTracking = () => { - const { user, toggleTracking } = this.props - if (!user.trackRecent) return - const hasRecents = user.recentPlaces.length > 0 || user.recentSearches.length > 0 + const { localUser, localUserTripRequests, toggleTracking } = this.props + if (!localUser.storeTripHistory) return + const hasRecents = localUser.recentPlaces.length > 0 || localUserTripRequests.length > 0 // If user has recents and does not confirm deletion, return without doing // anything. if (hasRecents && !window.confirm('You have recent searches and/or places stored. Disabling storage of recent places/searches will remove these items. Continue?')) { @@ -34,11 +33,11 @@ class UserSettings extends Component { toggleTracking(false) } - _enableTracking = () => !this.props.user.trackRecent && this.props.toggleTracking(true) + _enableTracking = () => !this.props.localUser.storeTripHistory && this.props.toggleTracking(true) _getLocations = user => { - const locations = [...user.locations] - if (!locations.find(l => l.type === 'work')) { + const locations = [...user.savedLocations] + if (!locations.find(isWork)) { locations.push({ blank: true, icon: 'briefcase', @@ -47,7 +46,7 @@ class UserSettings extends Component { type: 'work' }) } - if (!locations.find(l => l.type === 'home')) { + if (!locations.find(isHome)) { locations.push({ blank: true, icon: 'home', @@ -60,7 +59,17 @@ class UserSettings extends Component { } render () { - const { config, loggedInUser, loggedInUserTripRequests, persistenceStrategy, storageDisclaimer, user } = this.props + const { + forgetSearch, + localUser, + loggedInUser, + loggedInUserTripRequests, + persistenceStrategy, + setQueryParam, + storageDisclaimer, + tripRequests, + trueUser + } = this.props console.log(loggedInUserTripRequests) if (loggedInUser && persistenceStrategy === 'otp_middleware') { //use constants @@ -88,35 +97,19 @@ class UserSettings extends Component { )} - {/* Show the triprequests if middleware is enabled strategy and user has opted in. */} - {loggedInUser.storeTripHistory && loggedInUserTripRequests && loggedInUserTripRequests.length > 0 && -
    -
    -
    Recent searches
    - - {loggedInUserTripRequests - .map(tripReq => ({ - canDelete: false, - id: tripReq.id, - query: planParamsToQuery(qs.parse(tripReq.queryParams)), - timestamp: tripReq.dateCreated, - url: `${config.api.host}${config.api.path}/plan?${tripReq.queryParams}` - })) - .sort((a, b) => b.timestamp - a.timestamp) - .map(search => { - return - }) - } - -
    - } +
    ) } else if (persistenceStrategy === 'localStorage') { - const { favoriteStops, trackRecent, recentPlaces, recentSearches } = user + const { favoriteStops, storeTripHistory, recentPlaces } = localUser // Clone locations in order to prevent blank locations from seeping into the // app state/store. - const locations = this._getLocations(user) + const locations = this._getLocations(localUser) // Insert additional location types before 'custom' if needed. const order = ['home', 'work', 'suggested', 'stop', 'recent'] const sortedLocations = locations @@ -143,7 +136,7 @@ class UserSettings extends Component { } - {trackRecent && recentPlaces.length > 0 && + {storeTripHistory && recentPlaces.length > 0 &&

    Recent places
    @@ -154,20 +147,14 @@ class UserSettings extends Component {
    } - {trackRecent && recentSearches.length > 0 && -
    -
    -
    Recent searches
    - - {recentSearches - .sort((a, b) => b.timestamp - a.timestamp) - .map(search => { - return - }) - } - -
    - } + + +
    My preferences
    @@ -175,12 +162,12 @@ class UserSettings extends Component {
    {storageDisclaimer && @@ -287,19 +274,20 @@ class Place extends Component { /** * Wraps MainPanelPlace for recent trip requests. */ -class RecentSearch extends Component { +class TripRequest extends Component { _onSelect = () => { - const { search, setQueryParam } = this.props + const { setQueryParam, tripRequest } = this.props + const { id, query } = tripRequest // Update query params and initiate search. - setQueryParam(search.query, search.id) + setQueryParam(query, id) } - _onForget = () => this.props.forgetSearch(this.props.search.id) + _onForget = () => this.props.forgetSearch(this.props.tripRequest.id) render () { - const { search, user } = this.props - const { canDelete = true, query, timestamp } = search - const name = summarizeQuery(query, user.locations) + const { tripRequest, user } = this.props + const { canDelete = true, query, timestamp } = tripRequest + const name = summarizeQuery(query, user.savedLocations) const timeInfo = moment(timestamp).fromNow() const place = { @@ -319,24 +307,55 @@ class RecentSearch extends Component { } } +/** + * Renders a list of recent trip requests, most recent first, + * if permitted by user. + */ +const RecentTripRequests = ({ forgetSearch, setQueryParam, tripRequests, user }) => ( + user.storeTripHistory && tripRequests && tripRequests.length > 0 && ( +
    +
    +
    Recent searches
    + + {tripRequests + .sort((a, b) => b.timestamp - a.timestamp) + .map(tripReq => ( + + )) + } + +
    + ) +) // connect to redux store const mapStateToProps = (state, ownProps) => { - const { config, currentQuery, location, transitIndex, user } = state.otp + const { config, currentQuery, location, transitIndex } = state.otp const { language, persistence = {} } = config console.log(config) + + const { localUser, localUserTripRequests, loggedInUser, loggedInUserTripRequests } = state.user return { config, currentPosition: location.currentPosition, - loggedInUser: state.user.loggedInUser, - loggedInUserTripRequests: state.user.loggedInUserTripRequests, + localUser, // these users should be merged in mapStateToProps + localUserTripRequests, + loggedInUser, + loggedInUserTripRequests, nearbyStops: location.nearbyStops, persistenceStrategy: persistence.enabled && persistence.strategy, query: currentQuery, sessionSearches: location.sessionSearches, stopsIndex: transitIndex.stops, storageDisclaimer: language.storageDisclaimer, - user + tripRequests: loggedInUser ? loggedInUserTripRequests : localUserTripRequests, + trueUser: loggedInUser || localUser } } diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index f034d7252..07d012dfe 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -1,7 +1,66 @@ import update from 'immutability-helper' +import coreUtils from '@opentripplanner/core-utils' + +const { getItem } = coreUtils.storage + +/** + * Load select user settings stored locally if the persistence strategy is localStorage. + * Other settings not mentioned below are still loaded through createOtpReducer. + * The select user settings are: + * - Home and Work locations, + * - recent itinerary searches + * - whether recent searches and places should be tracked + * - recent places in trip plan searches, + * - favorite stops + * + * Note: If the persistence stragegy is otp_middleware, then user settings + * are fetched separately as soon as user login info is received + * (from one of the components that uses withLoggedInUserSupport). + */ +function loadUserFromLocalStoragePerConfig (config) { + const { persistence = {} } = config + const persistenceStrategy = persistence.enabled && persistence.strategy + if (persistenceStrategy === 'localStorage') { + // User's home and work locations + const home = getItem('home') + const work = getItem('work') + // Whether recent searches and places should be tracked in local storage. + const trackRecent = getItem('trackRecent', false) + const expandAdvanced = getItem('expandAdvanced', false) + // Recent places used in trip plan searches. + const recentPlaces = getItem('recent', []) + // List of user's favorite stops. + const favoriteStops = getItem('favoriteStops', []) + // Recent trip plan searches (excluding time/date parameters to avoid complexity). + const recentSearches = getItem('recentSearches', []) + // Filter valid locations found into locations list. + const locations = [home, work].filter(p => p) + // Add configurable locations to home and work locations + if (config.locations) { + locations.push(...config.locations.map(l => ({ ...l, type: 'suggested' }))) + } + + return { + localUser: { + expandAdvanced, // localUser only + favoriteStops, // localUser only - attn: legacy location format + recentPlaces, // localUser only - attn: legacy location format + savedLocations: locations, // attn: legacy location format + storeTripHistory: trackRecent + }, + localUserTripRequests: recentSearches // attn: legacy search format + } + } + + return { + localUser: null, + localUserTripRequests: null + } +} + +function createUserReducer (config) { + const localStorageState = loadUserFromLocalStoragePerConfig(config) -// TODO: port user-specific code from the otp reducer. -function createUserReducer () { const initialState = { accessToken: null, itineraryExistence: null, @@ -13,7 +72,8 @@ function createUserReducer () { loggedInUser: null, loggedInUserMonitoredTrips: null, loggedInUserTripRequests: null, - pathBeforeSignIn: null + pathBeforeSignIn: null, + ...localStorageState } return (state = initialState, action) => { From 49f12f82ca1b59b1ca7d1e6d92e855ce1f4468e5 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 19 Feb 2021 15:11:29 -0500 Subject: [PATCH 013/270] refactor(UserSettings): Merge local and middleware Place components. --- lib/actions/user.js | 12 +- lib/components/form/user-settings.js | 112 +++++++++--------- .../user/places/main-panel-place.js | 2 +- lib/components/user/places/place.js | 1 + lib/reducers/create-user-reducer.js | 73 +++++++++++- 5 files changed, 137 insertions(+), 63 deletions(-) diff --git a/lib/actions/user.js b/lib/actions/user.js index 2675ffe7a..7e93996d9 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -1,7 +1,6 @@ import clone from 'clone' import moment from 'moment' -import { planParamsToQuery } from '@opentripplanner/core-utils/lib/query' -import { OTP_API_DATE_FORMAT } from '@opentripplanner/core-utils/lib/time' +import coreUtils from '@opentripplanner/core-utils' import qs from 'qs' import { createAction } from 'redux-actions' @@ -13,12 +12,16 @@ import { secureFetch } from '../util/middleware' import { isBlank } from '../util/ui' import { isNewUser, positionHomeAndWorkFirst } from '../util/user' +const { planParamsToQuery } = coreUtils.query +const { OTP_API_DATE_FORMAT } = coreUtils.time + // Middleware API paths. const API_MONITORED_TRIP_PATH = '/api/secure/monitoredtrip' const API_TRIP_HISTORY_PATH = '/api/secure/triprequests' const API_OTPUSER_PATH = '/api/secure/user' const API_OTPUSER_VERIFY_SMS_SUBPATH = '/verify_sms' +// Middleware user actions const setAccessToken = createAction('SET_ACCESS_TOKEN') const setCurrentUser = createAction('SET_CURRENT_USER') const setCurrentUserMonitoredTrips = createAction('SET_CURRENT_USER_MONITORED_TRIPS') @@ -28,6 +31,11 @@ export const setPathBeforeSignIn = createAction('SET_PATH_BEFORE_SIGNIN') export const clearItineraryExistence = createAction('CLEAR_ITINERARY_EXISTENCE') const setitineraryExistence = createAction('SET_ITINERARY_EXISTENCE') +// localStorage user actions +export const deleteLocalUserRecentPlace = createAction('DELETE_LOCAL_USER_RECENT_PLACE') +export const deleteLocalUserSavedPlace = createAction('DELETE_LOCAL_USER_SAVED_PLACE') +export const deleteLocalUserStop = createAction('DELETE_LOCAL_USER_STOP') + function createNewUser (auth0User) { return { auth0UserId: auth0User.sub, diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 32f4c2b27..921c60796 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -13,7 +13,7 @@ import * as userActions from '../../actions/user' import { LinkWithQuery } from '../form/connected-links' import MainPanelPlace from '../user/places/main-panel-place' import { PLACES_PATH } from '../../util/constants' -import { convertToLocationFieldLocation, convertToPlace, isHome, isWork } from '../../util/user' +import { convertToPlace, isHome, isWork } from '../../util/user' import { UnpaddedList } from './styled' const { matchLatLon } = coreUtils.map @@ -60,17 +60,20 @@ class UserSettings extends Component { render () { const { + deleteLocalUserRecentPlace, + deleteLocalUserSavedPlace, + deleteLocalUserStop, + deleteUserPlace, forgetSearch, localUser, loggedInUser, - loggedInUserTripRequests, persistenceStrategy, setQueryParam, + setViewedStop, storageDisclaimer, tripRequests, trueUser } = this.props - console.log(loggedInUserTripRequests) if (loggedInUser && persistenceStrategy === 'otp_middleware') { //use constants const loggedInUserLocations = loggedInUser ? loggedInUser.savedLocations : [] @@ -86,9 +89,11 @@ class UserSettings extends Component { {loggedInUserLocations.map((place, index) => { return ( - @@ -110,7 +115,6 @@ class UserSettings extends Component { // Clone locations in order to prevent blank locations from seeping into the // app state/store. const locations = this._getLocations(localUser) - // Insert additional location types before 'custom' if needed. const order = ['home', 'work', 'suggested', 'stop', 'recent'] const sortedLocations = locations .sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)) @@ -120,18 +124,28 @@ class UserSettings extends Component { My saved places
    - {sortedLocations.map((location, index) => { - return - })} + {sortedLocations.map(location => ( + + ))}
    Favorite stops
    {favoriteStops.length > 0 - ? favoriteStops.map(location => { - return - }) + ? favoriteStops.map(location => ( + + )) : No favorite stops } @@ -141,9 +155,14 @@ class UserSettings extends Component {
    Recent places
    - {recentPlaces.map(location => { - return - })} + {recentPlaces.map(location => ( + + ))}
    } @@ -206,66 +225,40 @@ function setFromOrToLocation (location, setLocation, query) { } } -/** - * Wraps MainPanelPlace for places saved from loggedInUser. - */ -class SavedPlace extends Component { - _handleDelete = () => { - const { deleteUserPlace, place } = this.props - deleteUserPlace(place) - } - - _handleClick = () => { - const { place, query, setLocation } = this.props - const location = convertToLocationFieldLocation(place) - setFromOrToLocation(location, setLocation, query) - } - - render () { - const { path, place } = this.props - return ( - - ) - } -} - /** * Wraps MainPanelPlace for places from localStorage (recents, favorite stops...). */ class Place extends Component { _onSelect = () => { - const { location, query, setLocation } = this.props - if (location.blank) { - window.alert(`Enter origin/destination in the form (or set via map click) and click the resulting marker to set as ${location.type} location.`) + const { place, query, setLocation } = this.props + if (place.blank) { + window.alert(`Enter origin/destination in the form (or set via map click) and click the resulting marker to set as ${place.type} location.`) } else { - setFromOrToLocation(location, setLocation, query) + setFromOrToLocation(place, setLocation, query) } } _onView = () => { - const { location, setViewedStop } = this.props - setViewedStop({ stopId: location.id }) + const { place, onView } = this.props + onView({ stopId: place.id }) } - _onForget = () => { - const { forgetPlace, forgetStop, location } = this.props - if (location.type === 'stop') forgetStop(location.id) - else forgetPlace(location.id) + _onDelete = () => { + const { place, onDelete } = this.props + onDelete(place) } render () { - const { location } = this.props + const { isMiddleware, place, onDelete, onView, path } = this.props + // Needed to provide display details for localStorage places. + const placeWithDetails = isMiddleware ? place : convertToPlace(place) return ( ) } @@ -360,10 +353,11 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { + deleteLocalUserRecentPlace: userActions.deleteLocalUserRecentPlace, + deleteLocalUserSavedPlace: userActions.deleteLocalUserSavedPlace, + deleteLocalUserStop: userActions.deleteLocalUserStop, deleteUserPlace: userActions.deleteUserPlace, - forgetPlace: mapActions.forgetPlace, forgetSearch: apiActions.forgetSearch, - forgetStop: mapActions.forgetStop, setLocation: mapActions.setLocation, setQueryParam: formActions.setQueryParam, setViewedStop: uiActions.setViewedStop, diff --git a/lib/components/user/places/main-panel-place.js b/lib/components/user/places/main-panel-place.js index b7acf1a22..5ad34ad69 100644 --- a/lib/components/user/places/main-panel-place.js +++ b/lib/components/user/places/main-panel-place.js @@ -35,7 +35,7 @@ const MainPanelPlace = ({ }) => { // Determine title and aria label. const isPlanAction = !!onClick - const planLabel = 'Plan an itinerary using this item' + const planLabel = 'Plan an itinerary using this entry' const label = isPlanAction ? planLabel : EDIT_LABEL const title = isPlanAction ? `${place.title || place.address}\n${planLabel}` : EDIT_LABEL diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 0fd4468ad..544de0f01 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -140,6 +140,7 @@ const Place = ({ diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 07d012dfe..b5451561e 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -1,7 +1,11 @@ +/* eslint-disable complexity */ +import clone from 'clone' import update from 'immutability-helper' import coreUtils from '@opentripplanner/core-utils' -const { getItem } = coreUtils.storage +const { getItem, removeItem, storeItem } = coreUtils.storage + +const MAX_RECENT_STORAGE = 5 /** * Load select user settings stored locally if the persistence strategy is localStorage. @@ -125,6 +129,73 @@ function createUserReducer (config) { }) } + case 'DELETE_LOCAL_USER_RECENT_PLACE': { + if (!confirm('Would you like to remove this place?')) return state + + // This is used to delete a local user's recent location. + const placeId = action.payload.id + const recentPlaces = clone(state.localUser.recentPlaces) + // Remove recent from list of recent places + const removeIndex = recentPlaces.findIndex(l => l.id === placeId) + recentPlaces.splice(removeIndex, 1) + storeItem('recent', recentPlaces) + return removeIndex !== -1 + ? update(state, { localUser: { recentPlaces: { $splice: [[removeIndex, 1]] } } }) + : state + } + + case 'DELETE_LOCAL_USER_SAVED_PLACE': { + if (!confirm('Would you like to remove this place?')) return state + + // This is used to delete the local user's Home and Work (or other built-in) locations. + const placeId = action.payload.id + const removeIndex = state.localUser.savedLocations.findIndex(l => l.id === placeId) + removeItem(placeId) + return removeIndex !== -1 + ? update(state, { localUser: { savedLocations: { $splice: [[removeIndex, 1]] } } }) + : state + } + + case 'DELETE_LOCAL_USER_STOP': { + const stopId = action.payload.id + const favoriteStops = clone(state.localUser.favoriteStops) + // Remove stop from favorites + const removeIndex = favoriteStops.findIndex(l => l.id === stopId) + favoriteStops.splice(removeIndex, 1) + storeItem('favoriteStops', favoriteStops) + return removeIndex !== -1 + ? update(state, { localUser: { favoriteStops: { $splice: [[removeIndex, 1]] } } }) + : state + } + case 'REMEMBER_STOP': { + // Payload is stop data. We want to avoid saving other attributes that + // might be contained there (like lists of patterns). + const { id, name, lat, lon } = action.payload + const stop = { + icon: 'bus', + id, + lat, + lon, + name, + type: 'stop' + } + const favoriteStops = clone(state.localUser.favoriteStops) + if (favoriteStops.length >= MAX_RECENT_STORAGE) { + window.alert(`Cannot save more than ${MAX_RECENT_STORAGE} stops. Remove one before adding more.`) + return state + } + const index = favoriteStops.findIndex(s => s.id === stop.id) + // Do nothing if duplicate stop found. + if (index !== -1) { + console.warn(`Stop with id ${stop.id} already exists in favorites.`) + return state + } else { + favoriteStops.unshift(stop) + } + storeItem('favoriteStops', favoriteStops) + return update(state, { localUser: { favoriteStops: { $set: favoriteStops } } }) + } + default: return state } From f5de789f22fcad29218da5ff657cc26adca0eee3 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 19 Feb 2021 16:15:48 -0500 Subject: [PATCH 014/270] refactor(PlaceShortcut): Extract from UserSettings. Refactor place lists. --- lib/components/form/user-settings.js | 199 +++++++------------ lib/components/user/places/place-shortcut.js | 84 ++++++++ 2 files changed, 155 insertions(+), 128 deletions(-) create mode 100644 lib/components/user/places/place-shortcut.js diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 921c60796..c8a531d35 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ import moment from 'moment' import coreUtils from '@opentripplanner/core-utils' import React, { Component } from 'react' @@ -12,11 +11,11 @@ import * as uiActions from '../../actions/ui' import * as userActions from '../../actions/user' import { LinkWithQuery } from '../form/connected-links' import MainPanelPlace from '../user/places/main-panel-place' +import PlaceShortcut from '../user/places/place-shortcut' import { PLACES_PATH } from '../../util/constants' -import { convertToPlace, isHome, isWork } from '../../util/user' +import { isHome, isWork } from '../../util/user' import { UnpaddedList } from './styled' -const { matchLatLon } = coreUtils.map const { summarizeQuery } = coreUtils.query class UserSettings extends Component { @@ -77,32 +76,28 @@ class UserSettings extends Component { if (loggedInUser && persistenceStrategy === 'otp_middleware') { //use constants const loggedInUserLocations = loggedInUser ? loggedInUser.savedLocations : [] + const savedPlacesHeader = ( + <> + My saved places ( + + manage + + ) + + ) + return (
    -
    - My saved places ( - - manage - - ) -
    - - {loggedInUserLocations.map((place, index) => { - return ( - - ) - } - )} - + - order.indexOf(a.type) - order.indexOf(b.type)) return (
    -
    - My saved places -
    - - {sortedLocations.map(location => ( - - ))} - -
    + -
    Favorite stops
    - - {favoriteStops.length > 0 - ? favoriteStops.map(location => ( - - )) - : No favorite stops - } - + - {storeTripHistory && recentPlaces.length > 0 && -
    -
    -
    Recent places
    - - {recentPlaces.map(location => ( - - ))} - -
    - } + {storeTripHistory && } - { - const { place, query, setLocation } = this.props - if (place.blank) { - window.alert(`Enter origin/destination in the form (or set via map click) and click the resulting marker to set as ${place.type} location.`) - } else { - setFromOrToLocation(place, setLocation, query) - } - } - - _onView = () => { - const { place, onView } = this.props - onView({ stopId: place.id }) - } - - _onDelete = () => { - const { place, onDelete } = this.props - onDelete(place) - } - - render () { - const { isMiddleware, place, onDelete, onView, path } = this.props - // Needed to provide display details for localStorage places. - const placeWithDetails = isMiddleware ? place : convertToPlace(place) - return ( - - ) - } -} +const Places = ({ + basePath, + header, + isMiddleware, + onDelete, + onView, + places, + separator = true, + textIfEmpty +}) => ( + <> + {separator &&
    } + {header &&
    {header}
    } + + {places.length > 0 + ? places.map((location, index) => ( + + )) + : textIfEmpty && {textIfEmpty} + } + + +) /** * Wraps MainPanelPlace for recent trip requests. @@ -304,7 +247,7 @@ class TripRequest extends Component { * Renders a list of recent trip requests, most recent first, * if permitted by user. */ -const RecentTripRequests = ({ forgetSearch, setQueryParam, tripRequests, user }) => ( +const RecentTrips = ({ forgetSearch, setQueryParam, tripRequests, user }) => ( user.storeTripHistory && tripRequests && tripRequests.length > 0 && (

    diff --git a/lib/components/user/places/place-shortcut.js b/lib/components/user/places/place-shortcut.js new file mode 100644 index 000000000..c9514416d --- /dev/null +++ b/lib/components/user/places/place-shortcut.js @@ -0,0 +1,84 @@ +import coreUtils from '@opentripplanner/core-utils' +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import * as mapActions from '../../../actions/map' +import { convertToPlace } from '../../../util/user' +import MainPanelPlace from './main-panel-place' + +const { matchLatLon } = coreUtils.map + +/** + * A shortcut button that sets the provided place as the 'from' or 'to' Place. + * It wraps MainPanelPlace to let the user edit, view or delete the place. + */ +class PlaceShortcut extends Component { + /** + * Sets the from or to location based on the query using the provided action. + */ + _setFromOrToLocation = location => { + const { query, setLocation } = this.props + // If 'to' not set and 'from' does not match location, set as 'to'. + if ( + !query.to && ( + !query.from || !matchLatLon(location, query.from) + ) + ) { + setLocation({ location, locationType: 'to' }) + } else if ( + // Vice versa for setting as 'from'. + !query.from && + !matchLatLon(location, query.to) + ) { + setLocation({ location, locationType: 'from' }) + } + } + + _onSelect = () => { + const { place } = this.props + if (place.blank) { + window.alert(`Enter origin/destination in the form (or set via map click) and click the resulting marker to set as ${place.type} location.`) + } else { + this._setFromOrToLocation(place) + } + } + + _onView = () => { + const { place, onView } = this.props + onView({ stopId: place.id }) + } + + _onDelete = () => { + const { place, onDelete } = this.props + onDelete(place) + } + + render () { + const { isMiddleware, place, onDelete, onView, path } = this.props + // Provide display details for localStorage places. + const placeWithDetails = isMiddleware ? place : convertToPlace(place) + return ( + + ) + } +} + +// connect to redux store + +const mapStateToProps = (state, ownProps) => { + return { + query: state.otp.currentQuery + } +} + +const mapDispatchToProps = { + setLocation: mapActions.setLocation +} + +export default connect(mapStateToProps, mapDispatchToProps)(PlaceShortcut) From b4c0ba5b444e3911ca2149019b7c42264d83ed9a Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 19 Feb 2021 16:37:22 -0500 Subject: [PATCH 015/270] refactor(PlaceShortcut): Remove redundant prop --- lib/components/form/user-settings.js | 3 --- lib/components/user/places/place-shortcut.js | 11 ++++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index c8a531d35..fbe6a9f6e 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -91,7 +91,6 @@ class UserSettings extends Component { 0 ? places.map((location, index) => ( ) } From f63acdd6f7b788265ab79ebb21dc6c0d2eefda12 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 19 Feb 2021 20:10:14 -0500 Subject: [PATCH 016/270] refactor(user reducer): Move more actions from top to user reducer. --- lib/actions/map.js | 23 +++- lib/actions/user.js | 3 +- lib/components/form/user-settings.js | 73 +++++++----- .../map/connected-endpoints-overlay.js | 3 +- lib/components/user/places/place-shortcut.js | 8 +- lib/components/viewers/stop-viewer.js | 2 +- lib/reducers/create-otp-reducer.js | 107 ------------------ lib/reducers/create-user-reducer.js | 72 ++++++++++-- 8 files changed, 131 insertions(+), 160 deletions(-) diff --git a/lib/actions/map.js b/lib/actions/map.js index 26cbcbcef..9fdeda78b 100644 --- a/lib/actions/map.js +++ b/lib/actions/map.js @@ -19,12 +19,27 @@ import { clearActiveSearch } from './form' // Private actions const clearingLocation = createAction('CLEAR_LOCATION') const settingLocation = createAction('SET_LOCATION') +const deleteRecentPlace = createAction('DELETE_LOCAL_USER_RECENT_PLACE') +const deleteSavedPlace = createAction('DELETE_LOCAL_USER_SAVED_PLACE') // Public actions -export const forgetPlace = createAction('FORGET_PLACE') -export const rememberPlace = createAction('REMEMBER_PLACE') -export const forgetStop = createAction('FORGET_STOP') -export const rememberStop = createAction('REMEMBER_STOP') +export const rememberPlace = createAction('REMEMBER_LOCAL_USER_PLACE') +export const forgetStop = createAction('DELETE_LOCAL_USER_STOP') +export const rememberStop = createAction('REMEMBER_LOCAL_USER_STOP') + +/** + * Dispatches the action to delete a recent place or a saved place from localStorage + */ +export function forgetPlace (placeId) { + return function (dispatch, getState) { + // Recent place IDs contain the string literal 'recent'. + if (placeId.indexOf('recent') !== -1) { + dispatch(deleteRecentPlace(placeId)) + } else { + dispatch(deleteSavedPlace(placeId)) + } + } +} export function clearLocation (payload) { return function (dispatch, getState) { diff --git a/lib/actions/user.js b/lib/actions/user.js index 7e93996d9..11496ecf8 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -501,12 +501,11 @@ export function saveUserPlace (placeToSave, placeIndex) { /** * Delete the place data at the specified index for the logged-in user. */ -export function deleteUserPlace (place) { +export function deleteUserPlace (placeIndex) { return function (dispatch, getState) { if (!confirm('Would you like to remove this place?')) return const { loggedInUser } = getState().user - const placeIndex = loggedInUser.savedLocations.indexOf(place) loggedInUser.savedLocations.splice(placeIndex, 1) dispatch(createOrUpdateUser(loggedInUser, true)) diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index fbe6a9f6e..cfbab141e 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -14,6 +14,7 @@ import MainPanelPlace from '../user/places/main-panel-place' import PlaceShortcut from '../user/places/place-shortcut' import { PLACES_PATH } from '../../util/constants' import { isHome, isWork } from '../../util/user' + import { UnpaddedList } from './styled' const { summarizeQuery } = coreUtils.query @@ -22,7 +23,8 @@ class UserSettings extends Component { _disableTracking = () => { const { localUser, localUserTripRequests, toggleTracking } = this.props if (!localUser.storeTripHistory) return - const hasRecents = localUser.recentPlaces.length > 0 || localUserTripRequests.length > 0 + const hasRecents = (localUser.recentPlaces && localUser.recentPlaces.length > 0) || + (localUserTripRequests && localUserTripRequests.length > 0) // If user has recents and does not confirm deletion, return without doing // anything. if (hasRecents && !window.confirm('You have recent searches and/or places stored. Disabling storage of recent places/searches will remove these items. Continue?')) { @@ -74,8 +76,12 @@ class UserSettings extends Component { trueUser } = this.props - if (loggedInUser && persistenceStrategy === 'otp_middleware') { //use constants - const loggedInUserLocations = loggedInUser ? loggedInUser.savedLocations : [] + if (loggedInUser && persistenceStrategy === 'otp_middleware') { // use constants + // Add id attribute using index, that will be used to call deleteUserPlace. + const loggedInUserLocations = loggedInUser.savedLocations.map((loc, index) => ({ + ...loc, + id: index + })) const savedPlacesHeader = ( <> My saved places ( @@ -114,12 +120,14 @@ class UserSettings extends Component { .sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)) return (
    + {/* Sorted locations are shown regardless of tracking. */} + {/* Favorite stops are shown regardless of tracking. */} - {storeTripHistory && } + {storeTripHistory && ( + + )} ( - <> - {separator &&
    } - {header &&
    {header}
    } - - {places.length > 0 - ? places.map((location, index) => ( - - )) - : textIfEmpty && {textIfEmpty} - } - - -) +}) => { + const shouldRender = textIfEmpty || (places && places.length > 0) + return shouldRender && ( + <> + {separator &&
    } + {header &&
    {header}
    } + + {places.length > 0 + ? places.map((location, index) => ( + + )) + : textIfEmpty && {textIfEmpty} + } + + + ) +} /** * Wraps MainPanelPlace for recent trip requests. @@ -244,7 +257,9 @@ class TripRequest extends Component { * Renders a list of recent trip requests, most recent first, * if permitted by user. */ -const RecentTrips = ({ forgetSearch, setQueryParam, tripRequests, user }) => ( +const RecentTrips = ({ forgetSearch, setQueryParam, tripRequests = null, user }) => ( + // Note: tripRequests can be undefined, + // so we have to coerce it to null to make a valid render. user.storeTripHistory && tripRequests && tripRequests.length > 0 && (

    diff --git a/lib/components/map/connected-endpoints-overlay.js b/lib/components/map/connected-endpoints-overlay.js index 0e85d3d80..e41526550 100644 --- a/lib/components/map/connected-endpoints-overlay.js +++ b/lib/components/map/connected-endpoints-overlay.js @@ -21,10 +21,11 @@ const mapStateToProps = (state, ownProps) => { // Intermediate places doesn't trigger a re-plan, so for now default to // current query. FIXME: Determine with TriMet if this is desired behavior. const places = state.otp.currentQuery.intermediatePlaces.filter(p => p) + const user = state.user.trueUser return { fromLocation: from, intermediatePlaces: places, - locations: state.otp.user.locations, + locations: user ? user.savedLocations : [], showUserSettings, toLocation: to, visible: true diff --git a/lib/components/user/places/place-shortcut.js b/lib/components/user/places/place-shortcut.js index 963ac9589..452a4bb46 100644 --- a/lib/components/user/places/place-shortcut.js +++ b/lib/components/user/places/place-shortcut.js @@ -44,17 +44,17 @@ class PlaceShortcut extends Component { } _onView = () => { - const { place, onView } = this.props + const { onView, place } = this.props onView({ stopId: place.id }) } _onDelete = () => { - const { place, onDelete } = this.props - onDelete(place) + const { onDelete, place } = this.props + onDelete(place.id) } render () { - const { place, onDelete, onView, path } = this.props + const { onDelete, onView, path, place } = this.props // localStorage places (where path is not provided) need to be converted, // so the correct fields are passed to MainPanelPlace. const convertedPlace = path ? place : convertToPlace(place) diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 5efc37347..e7c1bcd45 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -356,7 +356,7 @@ const mapStateToProps = (state, ownProps) => { const stopViewerConfig = getStopViewerConfig(state.otp) return { autoRefreshStopTimes: state.otp.user.autoRefreshStopTimes, - favoriteStops: state.otp.user.favoriteStops, + favoriteStops: state.user.localUser.favoriteStops, homeTimezone: state.otp.config.homeTimezone, viewedStop: state.otp.ui.viewedStop, showUserSettings, diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 559c10bf0..0fb75cb82 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -9,7 +9,6 @@ import {getTimestamp} from '../util/state' import {isBatchRoutingEnabled} from '../util/itinerary' const { isTransit, getTransitModes } = coreUtils.itinerary -const { matchLatLon } = coreUtils.map const { filterProfileOptions } = coreUtils.profile const { ensureSingleAccessMode, @@ -444,95 +443,6 @@ function createOtpReducer (config, initialQuery) { case 'STORE_DEFAULT_SETTINGS': storeItem('defaultQuery', action.payload) return update(state, { user: { defaults: { $set: action.payload } } }) - case 'FORGET_PLACE': { - // Payload is the place ID. - // Recent place IDs contain the string literal 'recent'. - if (action.payload.indexOf('recent') !== -1) { - const recentPlaces = clone(state.user.recentPlaces) - // Remove recent from list of recent places - const removeIndex = recentPlaces.findIndex(l => l.id === action.payload) - recentPlaces.splice(removeIndex, 1) - storeItem('recent', recentPlaces) - return removeIndex !== -1 - ? update(state, { user: { recentPlaces: { $splice: [[removeIndex, 1]] } } }) - : state - } else { - const locations = clone(state.user.locations) - const removeIndex = locations.findIndex(l => l.id === action.payload) - removeItem(action.payload) - return removeIndex !== -1 - ? update(state, { user: { locations: { $splice: [[removeIndex, 1]] } } }) - : state - } - } - case 'REMEMBER_PLACE': { - const { location, type } = action.payload - switch (type) { - case 'recent': { - const recentPlaces = clone(state.user.recentPlaces) - const index = recentPlaces.findIndex(l => matchLatLon(l, location)) - // Replace recent place if duplicate found or add to list. - if (index !== -1) recentPlaces.splice(index, 1, location) - else recentPlaces.push(location) - const sortedPlaces = recentPlaces.sort((a, b) => b.timestamp - a.timestamp) - // Only keep up to 5 recent locations - // FIXME: Check for duplicates - if (recentPlaces.length >= MAX_RECENT_STORAGE) { - sortedPlaces.splice(MAX_RECENT_STORAGE) - } - storeItem('recent', recentPlaces) - return update(state, { user: { recentPlaces: { $set: sortedPlaces } } }) - } - default: { - const locations = clone(state.user.locations) - // Determine if location type (e.g., home or work) already exists in list - const index = locations.findIndex(l => l.type === type) - if (index !== -1) locations.splice(index, 1, location) - else locations.push(location) - storeItem(type, location) - return update(state, { user: { locations: { $set: locations } } }) - } - } - } - case 'FORGET_STOP': { - // Payload is the stop ID. - const favoriteStops = clone(state.user.favoriteStops) - // Remove stop from favorites - const removeIndex = favoriteStops.findIndex(l => l.id === action.payload) - favoriteStops.splice(removeIndex, 1) - storeItem('favoriteStops', favoriteStops) - return removeIndex !== -1 - ? update(state, { user: { favoriteStops: { $splice: [[removeIndex, 1]] } } }) - : state - } - case 'REMEMBER_STOP': { - // Payload is stop data. We want to avoid saving other attributes that - // might be contained there (like lists of patterns). - const { id, name, lat, lon } = action.payload - const stop = { - type: 'stop', - icon: 'bus', - id, - name, - lat, - lon - } - const favoriteStops = clone(state.user.favoriteStops) - if (favoriteStops.length >= MAX_RECENT_STORAGE) { - window.alert(`Cannot save more than ${MAX_RECENT_STORAGE} stops. Remove one before adding more.`) - return state - } - const index = favoriteStops.findIndex(s => s.id === stop.id) - // Do nothing if duplicate stop found. - if (index !== -1) { - console.warn(`Stop with id ${stop.id} already exists in favorites.`) - return state - } else { - favoriteStops.unshift(stop) - } - storeItem('favoriteStops', favoriteStops) - return update(state, { user: { favoriteStops: { $set: favoriteStops } } }) - } // FIXME: set up action case 'TOGGLE_ADVANCED_OPTIONS': storeItem('expandAdvanced', action.payload) @@ -540,23 +450,6 @@ function createOtpReducer (config, initialQuery) { return update(state, { user: { expandAdvanced: { $set: action.payload } } }) - case 'TOGGLE_TRACKING': { - storeItem('trackRecent', action.payload) - let recentPlaces = clone(state.user.recentPlaces) - let recentSearches = clone(state.user.recentSearches) - if (!action.payload) { - // If user disables tracking, remove recent searches and locations. - recentPlaces = [] - recentSearches = [] - removeItem('recent') - removeItem('recentSearches') - } - return update(state, { user: { - trackRecent: { $set: action.payload }, - recentPlaces: { $set: recentPlaces }, - recentSearches: { $set: recentSearches } - } }) - } case 'REMEMBER_SEARCH': const searches = clone(state.user.recentSearches) const duplicateIndex = searches.findIndex(s => isEqual(s.query, action.payload.query)) diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index b5451561e..1f87f5ac0 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -4,6 +4,7 @@ import update from 'immutability-helper' import coreUtils from '@opentripplanner/core-utils' const { getItem, removeItem, storeItem } = coreUtils.storage +const { matchLatLon } = coreUtils.map const MAX_RECENT_STORAGE = 5 @@ -49,10 +50,10 @@ function loadUserFromLocalStoragePerConfig (config) { expandAdvanced, // localUser only favoriteStops, // localUser only - attn: legacy location format recentPlaces, // localUser only - attn: legacy location format - savedLocations: locations, // attn: legacy location format + savedLocations: locations, storeTripHistory: trackRecent }, - localUserTripRequests: recentSearches // attn: legacy search format + localUserTripRequests: recentSearches } } @@ -130,10 +131,8 @@ function createUserReducer (config) { } case 'DELETE_LOCAL_USER_RECENT_PLACE': { - if (!confirm('Would you like to remove this place?')) return state - - // This is used to delete a local user's recent location. - const placeId = action.payload.id + // This is used to delete the local user's recent location that matches the provided id. + const placeId = action.payload const recentPlaces = clone(state.localUser.recentPlaces) // Remove recent from list of recent places const removeIndex = recentPlaces.findIndex(l => l.id === placeId) @@ -145,10 +144,9 @@ function createUserReducer (config) { } case 'DELETE_LOCAL_USER_SAVED_PLACE': { - if (!confirm('Would you like to remove this place?')) return state - - // This is used to delete the local user's Home and Work (or other built-in) locations. - const placeId = action.payload.id + // This is used to delete the local user's Home and Work (or other built-in) + // location that matches the provided id. + const placeId = action.payload const removeIndex = state.localUser.savedLocations.findIndex(l => l.id === placeId) removeItem(placeId) return removeIndex !== -1 @@ -157,7 +155,8 @@ function createUserReducer (config) { } case 'DELETE_LOCAL_USER_STOP': { - const stopId = action.payload.id + // This is used to delete the local user's favorite stop that matches the provided id. + const stopId = action.payload const favoriteStops = clone(state.localUser.favoriteStops) // Remove stop from favorites const removeIndex = favoriteStops.findIndex(l => l.id === stopId) @@ -167,7 +166,7 @@ function createUserReducer (config) { ? update(state, { localUser: { favoriteStops: { $splice: [[removeIndex, 1]] } } }) : state } - case 'REMEMBER_STOP': { + case 'REMEMBER_LOCAL_USER_STOP': { // Payload is stop data. We want to avoid saving other attributes that // might be contained there (like lists of patterns). const { id, name, lat, lon } = action.payload @@ -196,6 +195,55 @@ function createUserReducer (config) { return update(state, { localUser: { favoriteStops: { $set: favoriteStops } } }) } + case 'REMEMBER_LOCAL_USER_PLACE': { + const { location, type } = action.payload + switch (type) { + case 'recent': { + const recentPlaces = clone(state.localUser.recentPlaces) + const index = recentPlaces.findIndex(l => matchLatLon(l, location)) + // Replace recent place if duplicate found or add to list. + if (index !== -1) recentPlaces.splice(index, 1, location) + else recentPlaces.push(location) + const sortedPlaces = recentPlaces.sort((a, b) => b.timestamp - a.timestamp) + // Only keep up to 5 recent locations + // FIXME: Check for duplicates + if (recentPlaces.length >= MAX_RECENT_STORAGE) { + sortedPlaces.splice(MAX_RECENT_STORAGE) + } + storeItem('recent', recentPlaces) + return update(state, { localUser: { recentPlaces: { $set: sortedPlaces } } }) + } + default: { + const locations = clone(state.localUser.savedLocations) + // Determine if location type (e.g., home or work) already exists in list + const index = locations.findIndex(l => l.type === type) + if (index !== -1) locations.splice(index, 1, location) + else locations.push(location) + storeItem(type, location) + return update(state, { localUser: { locations: { $set: locations } } }) + } + } + } + + case 'TOGGLE_TRACKING': { + storeItem('trackRecent', action.payload) + let recentPlaces = clone(state.localUser.recentPlaces) + let recentSearches = clone(state.localUser.recentSearches) + if (!action.payload) { + // If user disables tracking, remove recent searches and locations. + recentPlaces = [] + recentSearches = [] + removeItem('recent') + removeItem('recentSearches') + } + return update(state, { + localUser: { + recentPlaces: { $set: recentPlaces }, + storeTripHistory: { $set: action.payload } + }, + localUserTripRequests: { $set: recentSearches } }) + } + default: return state } From fef91f66de453ff1ccfd9cbcd0537e0bafe1c157 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 22 Feb 2021 15:17:16 -0500 Subject: [PATCH 017/270] Revert "refactor(user reducer): Move more actions from top to user reducer." This reverts commit f63acdd6f7b788265ab79ebb21dc6c0d2eefda12. --- lib/actions/map.js | 23 +--- lib/actions/user.js | 3 +- lib/components/form/user-settings.js | 73 +++++------- .../map/connected-endpoints-overlay.js | 3 +- lib/components/user/places/place-shortcut.js | 8 +- lib/components/viewers/stop-viewer.js | 2 +- lib/reducers/create-otp-reducer.js | 107 ++++++++++++++++++ lib/reducers/create-user-reducer.js | 72 ++---------- 8 files changed, 160 insertions(+), 131 deletions(-) diff --git a/lib/actions/map.js b/lib/actions/map.js index 9fdeda78b..26cbcbcef 100644 --- a/lib/actions/map.js +++ b/lib/actions/map.js @@ -19,27 +19,12 @@ import { clearActiveSearch } from './form' // Private actions const clearingLocation = createAction('CLEAR_LOCATION') const settingLocation = createAction('SET_LOCATION') -const deleteRecentPlace = createAction('DELETE_LOCAL_USER_RECENT_PLACE') -const deleteSavedPlace = createAction('DELETE_LOCAL_USER_SAVED_PLACE') // Public actions -export const rememberPlace = createAction('REMEMBER_LOCAL_USER_PLACE') -export const forgetStop = createAction('DELETE_LOCAL_USER_STOP') -export const rememberStop = createAction('REMEMBER_LOCAL_USER_STOP') - -/** - * Dispatches the action to delete a recent place or a saved place from localStorage - */ -export function forgetPlace (placeId) { - return function (dispatch, getState) { - // Recent place IDs contain the string literal 'recent'. - if (placeId.indexOf('recent') !== -1) { - dispatch(deleteRecentPlace(placeId)) - } else { - dispatch(deleteSavedPlace(placeId)) - } - } -} +export const forgetPlace = createAction('FORGET_PLACE') +export const rememberPlace = createAction('REMEMBER_PLACE') +export const forgetStop = createAction('FORGET_STOP') +export const rememberStop = createAction('REMEMBER_STOP') export function clearLocation (payload) { return function (dispatch, getState) { diff --git a/lib/actions/user.js b/lib/actions/user.js index 11496ecf8..7e93996d9 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -501,11 +501,12 @@ export function saveUserPlace (placeToSave, placeIndex) { /** * Delete the place data at the specified index for the logged-in user. */ -export function deleteUserPlace (placeIndex) { +export function deleteUserPlace (place) { return function (dispatch, getState) { if (!confirm('Would you like to remove this place?')) return const { loggedInUser } = getState().user + const placeIndex = loggedInUser.savedLocations.indexOf(place) loggedInUser.savedLocations.splice(placeIndex, 1) dispatch(createOrUpdateUser(loggedInUser, true)) diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index cfbab141e..fbe6a9f6e 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -14,7 +14,6 @@ import MainPanelPlace from '../user/places/main-panel-place' import PlaceShortcut from '../user/places/place-shortcut' import { PLACES_PATH } from '../../util/constants' import { isHome, isWork } from '../../util/user' - import { UnpaddedList } from './styled' const { summarizeQuery } = coreUtils.query @@ -23,8 +22,7 @@ class UserSettings extends Component { _disableTracking = () => { const { localUser, localUserTripRequests, toggleTracking } = this.props if (!localUser.storeTripHistory) return - const hasRecents = (localUser.recentPlaces && localUser.recentPlaces.length > 0) || - (localUserTripRequests && localUserTripRequests.length > 0) + const hasRecents = localUser.recentPlaces.length > 0 || localUserTripRequests.length > 0 // If user has recents and does not confirm deletion, return without doing // anything. if (hasRecents && !window.confirm('You have recent searches and/or places stored. Disabling storage of recent places/searches will remove these items. Continue?')) { @@ -76,12 +74,8 @@ class UserSettings extends Component { trueUser } = this.props - if (loggedInUser && persistenceStrategy === 'otp_middleware') { // use constants - // Add id attribute using index, that will be used to call deleteUserPlace. - const loggedInUserLocations = loggedInUser.savedLocations.map((loc, index) => ({ - ...loc, - id: index - })) + if (loggedInUser && persistenceStrategy === 'otp_middleware') { //use constants + const loggedInUserLocations = loggedInUser ? loggedInUser.savedLocations : [] const savedPlacesHeader = ( <> My saved places ( @@ -120,14 +114,12 @@ class UserSettings extends Component { .sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)) return (
    - {/* Sorted locations are shown regardless of tracking. */} - {/* Favorite stops are shown regardless of tracking. */} - {storeTripHistory && ( - - )} + {storeTripHistory && } { - const shouldRender = textIfEmpty || (places && places.length > 0) - return shouldRender && ( - <> - {separator &&
    } - {header &&
    {header}
    } - - {places.length > 0 - ? places.map((location, index) => ( - - )) - : textIfEmpty && {textIfEmpty} - } - - - ) -} +}) => ( + <> + {separator &&
    } + {header &&
    {header}
    } + + {places.length > 0 + ? places.map((location, index) => ( + + )) + : textIfEmpty && {textIfEmpty} + } + + +) /** * Wraps MainPanelPlace for recent trip requests. @@ -257,9 +244,7 @@ class TripRequest extends Component { * Renders a list of recent trip requests, most recent first, * if permitted by user. */ -const RecentTrips = ({ forgetSearch, setQueryParam, tripRequests = null, user }) => ( - // Note: tripRequests can be undefined, - // so we have to coerce it to null to make a valid render. +const RecentTrips = ({ forgetSearch, setQueryParam, tripRequests, user }) => ( user.storeTripHistory && tripRequests && tripRequests.length > 0 && (

    diff --git a/lib/components/map/connected-endpoints-overlay.js b/lib/components/map/connected-endpoints-overlay.js index e41526550..0e85d3d80 100644 --- a/lib/components/map/connected-endpoints-overlay.js +++ b/lib/components/map/connected-endpoints-overlay.js @@ -21,11 +21,10 @@ const mapStateToProps = (state, ownProps) => { // Intermediate places doesn't trigger a re-plan, so for now default to // current query. FIXME: Determine with TriMet if this is desired behavior. const places = state.otp.currentQuery.intermediatePlaces.filter(p => p) - const user = state.user.trueUser return { fromLocation: from, intermediatePlaces: places, - locations: user ? user.savedLocations : [], + locations: state.otp.user.locations, showUserSettings, toLocation: to, visible: true diff --git a/lib/components/user/places/place-shortcut.js b/lib/components/user/places/place-shortcut.js index 452a4bb46..963ac9589 100644 --- a/lib/components/user/places/place-shortcut.js +++ b/lib/components/user/places/place-shortcut.js @@ -44,17 +44,17 @@ class PlaceShortcut extends Component { } _onView = () => { - const { onView, place } = this.props + const { place, onView } = this.props onView({ stopId: place.id }) } _onDelete = () => { - const { onDelete, place } = this.props - onDelete(place.id) + const { place, onDelete } = this.props + onDelete(place) } render () { - const { onDelete, onView, path, place } = this.props + const { place, onDelete, onView, path } = this.props // localStorage places (where path is not provided) need to be converted, // so the correct fields are passed to MainPanelPlace. const convertedPlace = path ? place : convertToPlace(place) diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index e7c1bcd45..5efc37347 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -356,7 +356,7 @@ const mapStateToProps = (state, ownProps) => { const stopViewerConfig = getStopViewerConfig(state.otp) return { autoRefreshStopTimes: state.otp.user.autoRefreshStopTimes, - favoriteStops: state.user.localUser.favoriteStops, + favoriteStops: state.otp.user.favoriteStops, homeTimezone: state.otp.config.homeTimezone, viewedStop: state.otp.ui.viewedStop, showUserSettings, diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 0fb75cb82..559c10bf0 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -9,6 +9,7 @@ import {getTimestamp} from '../util/state' import {isBatchRoutingEnabled} from '../util/itinerary' const { isTransit, getTransitModes } = coreUtils.itinerary +const { matchLatLon } = coreUtils.map const { filterProfileOptions } = coreUtils.profile const { ensureSingleAccessMode, @@ -443,6 +444,95 @@ function createOtpReducer (config, initialQuery) { case 'STORE_DEFAULT_SETTINGS': storeItem('defaultQuery', action.payload) return update(state, { user: { defaults: { $set: action.payload } } }) + case 'FORGET_PLACE': { + // Payload is the place ID. + // Recent place IDs contain the string literal 'recent'. + if (action.payload.indexOf('recent') !== -1) { + const recentPlaces = clone(state.user.recentPlaces) + // Remove recent from list of recent places + const removeIndex = recentPlaces.findIndex(l => l.id === action.payload) + recentPlaces.splice(removeIndex, 1) + storeItem('recent', recentPlaces) + return removeIndex !== -1 + ? update(state, { user: { recentPlaces: { $splice: [[removeIndex, 1]] } } }) + : state + } else { + const locations = clone(state.user.locations) + const removeIndex = locations.findIndex(l => l.id === action.payload) + removeItem(action.payload) + return removeIndex !== -1 + ? update(state, { user: { locations: { $splice: [[removeIndex, 1]] } } }) + : state + } + } + case 'REMEMBER_PLACE': { + const { location, type } = action.payload + switch (type) { + case 'recent': { + const recentPlaces = clone(state.user.recentPlaces) + const index = recentPlaces.findIndex(l => matchLatLon(l, location)) + // Replace recent place if duplicate found or add to list. + if (index !== -1) recentPlaces.splice(index, 1, location) + else recentPlaces.push(location) + const sortedPlaces = recentPlaces.sort((a, b) => b.timestamp - a.timestamp) + // Only keep up to 5 recent locations + // FIXME: Check for duplicates + if (recentPlaces.length >= MAX_RECENT_STORAGE) { + sortedPlaces.splice(MAX_RECENT_STORAGE) + } + storeItem('recent', recentPlaces) + return update(state, { user: { recentPlaces: { $set: sortedPlaces } } }) + } + default: { + const locations = clone(state.user.locations) + // Determine if location type (e.g., home or work) already exists in list + const index = locations.findIndex(l => l.type === type) + if (index !== -1) locations.splice(index, 1, location) + else locations.push(location) + storeItem(type, location) + return update(state, { user: { locations: { $set: locations } } }) + } + } + } + case 'FORGET_STOP': { + // Payload is the stop ID. + const favoriteStops = clone(state.user.favoriteStops) + // Remove stop from favorites + const removeIndex = favoriteStops.findIndex(l => l.id === action.payload) + favoriteStops.splice(removeIndex, 1) + storeItem('favoriteStops', favoriteStops) + return removeIndex !== -1 + ? update(state, { user: { favoriteStops: { $splice: [[removeIndex, 1]] } } }) + : state + } + case 'REMEMBER_STOP': { + // Payload is stop data. We want to avoid saving other attributes that + // might be contained there (like lists of patterns). + const { id, name, lat, lon } = action.payload + const stop = { + type: 'stop', + icon: 'bus', + id, + name, + lat, + lon + } + const favoriteStops = clone(state.user.favoriteStops) + if (favoriteStops.length >= MAX_RECENT_STORAGE) { + window.alert(`Cannot save more than ${MAX_RECENT_STORAGE} stops. Remove one before adding more.`) + return state + } + const index = favoriteStops.findIndex(s => s.id === stop.id) + // Do nothing if duplicate stop found. + if (index !== -1) { + console.warn(`Stop with id ${stop.id} already exists in favorites.`) + return state + } else { + favoriteStops.unshift(stop) + } + storeItem('favoriteStops', favoriteStops) + return update(state, { user: { favoriteStops: { $set: favoriteStops } } }) + } // FIXME: set up action case 'TOGGLE_ADVANCED_OPTIONS': storeItem('expandAdvanced', action.payload) @@ -450,6 +540,23 @@ function createOtpReducer (config, initialQuery) { return update(state, { user: { expandAdvanced: { $set: action.payload } } }) + case 'TOGGLE_TRACKING': { + storeItem('trackRecent', action.payload) + let recentPlaces = clone(state.user.recentPlaces) + let recentSearches = clone(state.user.recentSearches) + if (!action.payload) { + // If user disables tracking, remove recent searches and locations. + recentPlaces = [] + recentSearches = [] + removeItem('recent') + removeItem('recentSearches') + } + return update(state, { user: { + trackRecent: { $set: action.payload }, + recentPlaces: { $set: recentPlaces }, + recentSearches: { $set: recentSearches } + } }) + } case 'REMEMBER_SEARCH': const searches = clone(state.user.recentSearches) const duplicateIndex = searches.findIndex(s => isEqual(s.query, action.payload.query)) diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 1f87f5ac0..b5451561e 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -4,7 +4,6 @@ import update from 'immutability-helper' import coreUtils from '@opentripplanner/core-utils' const { getItem, removeItem, storeItem } = coreUtils.storage -const { matchLatLon } = coreUtils.map const MAX_RECENT_STORAGE = 5 @@ -50,10 +49,10 @@ function loadUserFromLocalStoragePerConfig (config) { expandAdvanced, // localUser only favoriteStops, // localUser only - attn: legacy location format recentPlaces, // localUser only - attn: legacy location format - savedLocations: locations, + savedLocations: locations, // attn: legacy location format storeTripHistory: trackRecent }, - localUserTripRequests: recentSearches + localUserTripRequests: recentSearches // attn: legacy search format } } @@ -131,8 +130,10 @@ function createUserReducer (config) { } case 'DELETE_LOCAL_USER_RECENT_PLACE': { - // This is used to delete the local user's recent location that matches the provided id. - const placeId = action.payload + if (!confirm('Would you like to remove this place?')) return state + + // This is used to delete a local user's recent location. + const placeId = action.payload.id const recentPlaces = clone(state.localUser.recentPlaces) // Remove recent from list of recent places const removeIndex = recentPlaces.findIndex(l => l.id === placeId) @@ -144,9 +145,10 @@ function createUserReducer (config) { } case 'DELETE_LOCAL_USER_SAVED_PLACE': { - // This is used to delete the local user's Home and Work (or other built-in) - // location that matches the provided id. - const placeId = action.payload + if (!confirm('Would you like to remove this place?')) return state + + // This is used to delete the local user's Home and Work (or other built-in) locations. + const placeId = action.payload.id const removeIndex = state.localUser.savedLocations.findIndex(l => l.id === placeId) removeItem(placeId) return removeIndex !== -1 @@ -155,8 +157,7 @@ function createUserReducer (config) { } case 'DELETE_LOCAL_USER_STOP': { - // This is used to delete the local user's favorite stop that matches the provided id. - const stopId = action.payload + const stopId = action.payload.id const favoriteStops = clone(state.localUser.favoriteStops) // Remove stop from favorites const removeIndex = favoriteStops.findIndex(l => l.id === stopId) @@ -166,7 +167,7 @@ function createUserReducer (config) { ? update(state, { localUser: { favoriteStops: { $splice: [[removeIndex, 1]] } } }) : state } - case 'REMEMBER_LOCAL_USER_STOP': { + case 'REMEMBER_STOP': { // Payload is stop data. We want to avoid saving other attributes that // might be contained there (like lists of patterns). const { id, name, lat, lon } = action.payload @@ -195,55 +196,6 @@ function createUserReducer (config) { return update(state, { localUser: { favoriteStops: { $set: favoriteStops } } }) } - case 'REMEMBER_LOCAL_USER_PLACE': { - const { location, type } = action.payload - switch (type) { - case 'recent': { - const recentPlaces = clone(state.localUser.recentPlaces) - const index = recentPlaces.findIndex(l => matchLatLon(l, location)) - // Replace recent place if duplicate found or add to list. - if (index !== -1) recentPlaces.splice(index, 1, location) - else recentPlaces.push(location) - const sortedPlaces = recentPlaces.sort((a, b) => b.timestamp - a.timestamp) - // Only keep up to 5 recent locations - // FIXME: Check for duplicates - if (recentPlaces.length >= MAX_RECENT_STORAGE) { - sortedPlaces.splice(MAX_RECENT_STORAGE) - } - storeItem('recent', recentPlaces) - return update(state, { localUser: { recentPlaces: { $set: sortedPlaces } } }) - } - default: { - const locations = clone(state.localUser.savedLocations) - // Determine if location type (e.g., home or work) already exists in list - const index = locations.findIndex(l => l.type === type) - if (index !== -1) locations.splice(index, 1, location) - else locations.push(location) - storeItem(type, location) - return update(state, { localUser: { locations: { $set: locations } } }) - } - } - } - - case 'TOGGLE_TRACKING': { - storeItem('trackRecent', action.payload) - let recentPlaces = clone(state.localUser.recentPlaces) - let recentSearches = clone(state.localUser.recentSearches) - if (!action.payload) { - // If user disables tracking, remove recent searches and locations. - recentPlaces = [] - recentSearches = [] - removeItem('recent') - removeItem('recentSearches') - } - return update(state, { - localUser: { - recentPlaces: { $set: recentPlaces }, - storeTripHistory: { $set: action.payload } - }, - localUserTripRequests: { $set: recentSearches } }) - } - default: return state } From 41b39796f7597e7fd8067f320f7c9ae4cfe5cb73 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 22 Feb 2021 16:29:20 -0500 Subject: [PATCH 018/270] refactor(user-settings): Delete recent search, refactor delete places. --- lib/actions/user.js | 5 +- lib/components/form/user-settings.js | 61 ++++++++++++-------- lib/components/user/places/favorite-place.js | 2 +- lib/components/user/places/place-shortcut.js | 8 +-- lib/components/user/places/place.js | 2 +- lib/components/viewers/stop-viewer.js | 2 +- lib/reducers/create-otp-reducer.js | 49 ---------------- lib/reducers/create-user-reducer.js | 37 +++++++----- 8 files changed, 70 insertions(+), 96 deletions(-) diff --git a/lib/actions/user.js b/lib/actions/user.js index 7e93996d9..f31db421e 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -34,7 +34,7 @@ const setitineraryExistence = createAction('SET_ITINERARY_EXISTENCE') // localStorage user actions export const deleteLocalUserRecentPlace = createAction('DELETE_LOCAL_USER_RECENT_PLACE') export const deleteLocalUserSavedPlace = createAction('DELETE_LOCAL_USER_SAVED_PLACE') -export const deleteLocalUserStop = createAction('DELETE_LOCAL_USER_STOP') +export const deleteLocalUserStop = createAction('FORGET_STOP') function createNewUser (auth0User) { return { @@ -501,12 +501,11 @@ export function saveUserPlace (placeToSave, placeIndex) { /** * Delete the place data at the specified index for the logged-in user. */ -export function deleteUserPlace (place) { +export function deleteUserPlace (placeIndex) { return function (dispatch, getState) { if (!confirm('Would you like to remove this place?')) return const { loggedInUser } = getState().user - const placeIndex = loggedInUser.savedLocations.indexOf(place) loggedInUser.savedLocations.splice(placeIndex, 1) dispatch(createOrUpdateUser(loggedInUser, true)) diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index fbe6a9f6e..28346b84f 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -22,7 +22,9 @@ class UserSettings extends Component { _disableTracking = () => { const { localUser, localUserTripRequests, toggleTracking } = this.props if (!localUser.storeTripHistory) return - const hasRecents = localUser.recentPlaces.length > 0 || localUserTripRequests.length > 0 + const hasRecents = (localUser.recentPlaces && localUser.recentPlaces.length > 0) || + (localUserTripRequests && localUserTripRequests.length > 0) + // If user has recents and does not confirm deletion, return without doing // anything. if (hasRecents && !window.confirm('You have recent searches and/or places stored. Disabling storage of recent places/searches will remove these items. Continue?')) { @@ -74,8 +76,12 @@ class UserSettings extends Component { trueUser } = this.props - if (loggedInUser && persistenceStrategy === 'otp_middleware') { //use constants - const loggedInUserLocations = loggedInUser ? loggedInUser.savedLocations : [] + if (loggedInUser && persistenceStrategy === 'otp_middleware') { // use constants + // Add id attribute using index, that will be used to call deleteUserPlace. + const loggedInUserLocations = loggedInUser.savedLocations.map((loc, index) => ({ + ...loc, + id: index + })) const savedPlacesHeader = ( <> My saved places ( @@ -114,12 +120,14 @@ class UserSettings extends Component { .sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)) return (
    + {/* Sorted locations are shown regardless of tracking. */} + {/* Favorite stops are shown regardless of tracking. */} ( - <> - {separator &&
    } - {header &&
    {header}
    } - - {places.length > 0 - ? places.map((location, index) => ( - - )) - : textIfEmpty && {textIfEmpty} - } - - -) +}) => { + const shouldRender = textIfEmpty || (places && places.length > 0) + return shouldRender && ( + <> + {separator &&
    } + {header &&
    {header}
    } + + {places.length > 0 + ? places.map((location, index) => ( + + )) + : textIfEmpty && {textIfEmpty} + } + + + ) +} /** * Wraps MainPanelPlace for recent trip requests. @@ -244,7 +255,9 @@ class TripRequest extends Component { * Renders a list of recent trip requests, most recent first, * if permitted by user. */ -const RecentTrips = ({ forgetSearch, setQueryParam, tripRequests, user }) => ( +const RecentTrips = ({ forgetSearch, setQueryParam, tripRequests = null, user }) => ( + // Note: tripRequests can be undefined, + // so we have to coerce it to null to make a valid render. user.storeTripHistory && tripRequests && tripRequests.length > 0 && (

    diff --git a/lib/components/user/places/favorite-place.js b/lib/components/user/places/favorite-place.js index f75946552..b8e587d68 100644 --- a/lib/components/user/places/favorite-place.js +++ b/lib/components/user/places/favorite-place.js @@ -55,7 +55,7 @@ const StyledPlace = styled(Place)` class FavoritePlace extends Component { _handleDelete = () => { const { deleteUserPlace, place } = this.props - deleteUserPlace(place) + deleteUserPlace(place.id) } render () { diff --git a/lib/components/user/places/place-shortcut.js b/lib/components/user/places/place-shortcut.js index 963ac9589..452a4bb46 100644 --- a/lib/components/user/places/place-shortcut.js +++ b/lib/components/user/places/place-shortcut.js @@ -44,17 +44,17 @@ class PlaceShortcut extends Component { } _onView = () => { - const { place, onView } = this.props + const { onView, place } = this.props onView({ stopId: place.id }) } _onDelete = () => { - const { place, onDelete } = this.props - onDelete(place) + const { onDelete, place } = this.props + onDelete(place.id) } render () { - const { place, onDelete, onView, path } = this.props + const { onDelete, onView, path, place } = this.props // localStorage places (where path is not provided) need to be converted, // so the correct fields are passed to MainPanelPlace. const convertedPlace = path ? place : convertToPlace(place) diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 544de0f01..08ad7677d 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -63,7 +63,7 @@ export function getActionsForPlace (place, onDelete, onView) { if (onView && place.type === 'stop') { actions.push(viewAction) } - if (onDelete && (!isFixed || place.address)) { + if (onDelete && !place.blank && (!isFixed || place.address)) { actions.push(deleteAction) } diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 5efc37347..e7c1bcd45 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -356,7 +356,7 @@ const mapStateToProps = (state, ownProps) => { const stopViewerConfig = getStopViewerConfig(state.otp) return { autoRefreshStopTimes: state.otp.user.autoRefreshStopTimes, - favoriteStops: state.otp.user.favoriteStops, + favoriteStops: state.user.localUser.favoriteStops, homeTimezone: state.otp.config.homeTimezone, viewedStop: state.otp.ui.viewedStop, showUserSettings, diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 559c10bf0..79259d81d 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -494,45 +494,6 @@ function createOtpReducer (config, initialQuery) { } } } - case 'FORGET_STOP': { - // Payload is the stop ID. - const favoriteStops = clone(state.user.favoriteStops) - // Remove stop from favorites - const removeIndex = favoriteStops.findIndex(l => l.id === action.payload) - favoriteStops.splice(removeIndex, 1) - storeItem('favoriteStops', favoriteStops) - return removeIndex !== -1 - ? update(state, { user: { favoriteStops: { $splice: [[removeIndex, 1]] } } }) - : state - } - case 'REMEMBER_STOP': { - // Payload is stop data. We want to avoid saving other attributes that - // might be contained there (like lists of patterns). - const { id, name, lat, lon } = action.payload - const stop = { - type: 'stop', - icon: 'bus', - id, - name, - lat, - lon - } - const favoriteStops = clone(state.user.favoriteStops) - if (favoriteStops.length >= MAX_RECENT_STORAGE) { - window.alert(`Cannot save more than ${MAX_RECENT_STORAGE} stops. Remove one before adding more.`) - return state - } - const index = favoriteStops.findIndex(s => s.id === stop.id) - // Do nothing if duplicate stop found. - if (index !== -1) { - console.warn(`Stop with id ${stop.id} already exists in favorites.`) - return state - } else { - favoriteStops.unshift(stop) - } - storeItem('favoriteStops', favoriteStops) - return update(state, { user: { favoriteStops: { $set: favoriteStops } } }) - } // FIXME: set up action case 'TOGGLE_ADVANCED_OPTIONS': storeItem('expandAdvanced', action.payload) @@ -570,16 +531,6 @@ function createOtpReducer (config, initialQuery) { } storeItem('recentSearches', sortedSearches) return update(state, { user: { searches: { $set: sortedSearches } } }) - case 'FORGET_SEARCH': { - const recentSearches = clone(state.user.recentSearches) - const index = recentSearches.findIndex(l => l.id === action.payload) - // Remove item from list of recent searches - recentSearches.splice(index, 1) - storeItem('recentSearches', recentSearches) - return index !== -1 - ? update(state, { user: { recentSearches: { $splice: [[index, 1]] } } }) - : state - } case 'SET_AUTOPLAN': return update(state, { config: { autoPlan: { $set: action.payload.autoPlan } } diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index b5451561e..8f1f13eff 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -47,12 +47,12 @@ function loadUserFromLocalStoragePerConfig (config) { return { localUser: { expandAdvanced, // localUser only - favoriteStops, // localUser only - attn: legacy location format - recentPlaces, // localUser only - attn: legacy location format - savedLocations: locations, // attn: legacy location format + favoriteStops, // localUser only + recentPlaces, // localUser only + savedLocations: locations, storeTripHistory: trackRecent }, - localUserTripRequests: recentSearches // attn: legacy search format + localUserTripRequests: recentSearches } } @@ -132,8 +132,8 @@ function createUserReducer (config) { case 'DELETE_LOCAL_USER_RECENT_PLACE': { if (!confirm('Would you like to remove this place?')) return state - // This is used to delete a local user's recent location. - const placeId = action.payload.id + // This is used to delete the local user's recent location that matches the provided id. + const placeId = action.payload const recentPlaces = clone(state.localUser.recentPlaces) // Remove recent from list of recent places const removeIndex = recentPlaces.findIndex(l => l.id === placeId) @@ -145,10 +145,9 @@ function createUserReducer (config) { } case 'DELETE_LOCAL_USER_SAVED_PLACE': { - if (!confirm('Would you like to remove this place?')) return state - - // This is used to delete the local user's Home and Work (or other built-in) locations. - const placeId = action.payload.id + // This is used to delete the local user's Home and Work (or other built-in) + // location that matches the provided id. + const placeId = action.payload const removeIndex = state.localUser.savedLocations.findIndex(l => l.id === placeId) removeItem(placeId) return removeIndex !== -1 @@ -156,10 +155,11 @@ function createUserReducer (config) { : state } - case 'DELETE_LOCAL_USER_STOP': { - const stopId = action.payload.id + case 'FORGET_STOP': { + const stopId = action.payload const favoriteStops = clone(state.localUser.favoriteStops) - // Remove stop from favorites + + // This is used to delete the local user's favorite stop that matches the provided id. const removeIndex = favoriteStops.findIndex(l => l.id === stopId) favoriteStops.splice(removeIndex, 1) storeItem('favoriteStops', favoriteStops) @@ -196,6 +196,17 @@ function createUserReducer (config) { return update(state, { localUser: { favoriteStops: { $set: favoriteStops } } }) } + case 'FORGET_SEARCH': { + const recentSearches = clone(state.localUserTripRequests) + const index = recentSearches.findIndex(l => l.id === action.payload) + // Remove item from list of recent searches + recentSearches.splice(index, 1) + storeItem('recentSearches', recentSearches) + return index !== -1 + ? update(state, { localUserTripRequests: { $splice: [[index, 1]] } }) + : state + } + default: return state } From 5404e816cff8707a2df0a1f09c8f2a39f468cda5 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 22 Feb 2021 17:00:53 -0500 Subject: [PATCH 019/270] refactor(BatchRoutingPanel): Integrate UserSettings. --- example.js | 8 +++---- lib/components/app/batch-routing-panel.js | 29 +++++++++++++---------- lib/components/form/user-settings.js | 5 ++-- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/example.js b/example.js index 1845f3ee5..48b3982bf 100644 --- a/example.js +++ b/example.js @@ -77,8 +77,8 @@ const store = createStore( combineReducers({ callTaker: createCallTakerReducer(), otp: createOtpReducer(otpConfig, initialQuery), - user: createUserReducer(), - router: connectRouter(history) + router: connectRouter(history), + user: createUserReducer(otpConfig) }), compose(applyMiddleware(...middleware)) ) @@ -92,7 +92,7 @@ class OtpRRExample extends Component { - + {/* Note: the main tag provides a way for users of screen readers to skip to the primary page content. @@ -109,7 +109,7 @@ class OtpRRExample extends Component { {otpConfig.datastoreUrl ? : null} - + {otpConfig.datastoreUrl ? : null} diff --git a/lib/components/app/batch-routing-panel.js b/lib/components/app/batch-routing-panel.js index 93d2e1975..8e62f1d8c 100644 --- a/lib/components/app/batch-routing-panel.js +++ b/lib/components/app/batch-routing-panel.js @@ -9,7 +9,7 @@ import BatchSettingsPanel from '../form/batch-settings-panel' 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 UserSettings from '../form/user-settings' import Icon from '../narrative/icon' import NarrativeItineraries from '../narrative/narrative-itineraries' import { @@ -93,6 +93,13 @@ const NarrativeContainer = styled.div` } ` +// Remove extra margins around UserSettings. +// !important is needed otherwise the OTP CSS classes are applied. +const StyledUserSettings = styled(UserSettings)` + margin-left: 0!important; + margin-right: 0!important; +` + /** * Main panel for the batch/trip comparison form. */ @@ -145,7 +152,7 @@ class BatchRoutingPanel extends Component { _toggleSettings = () => this.setState(this._updateExpanded('SETTINGS')) render () { - const {config, currentQuery, mobile} = this.props + const {activeSearch, config, currentQuery, mobile, showUserSettings} = this.props const {expanded, selectedModes} = this.state const actionText = mobile ? 'tap' : 'click' return ( @@ -175,7 +182,7 @@ class BatchRoutingPanel extends Component { {coreUtils.query.isNotDefaultQuery(currentQuery, config) && } - + - + {expanded === 'DATE_TIME' && @@ -206,21 +213,19 @@ class BatchRoutingPanel extends Component { } - {/* FIXME: Add back user settings (home, work, etc.) once connected to - the middleware persistence. - !activeSearch && showUserSettings && - - */} + {!activeSearch && showUserSettings && + + } {/* TODO: Implement mobile view */} diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 28346b84f..cbc3d8fae 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -61,6 +61,7 @@ class UserSettings extends Component { render () { const { + className, deleteLocalUserRecentPlace, deleteLocalUserSavedPlace, deleteLocalUserStop, @@ -93,7 +94,7 @@ class UserSettings extends Component { ) return ( -
    +
    order.indexOf(a.type) - order.indexOf(b.type)) return ( -
    +
    {/* Sorted locations are shown regardless of tracking. */} Date: Mon, 22 Feb 2021 17:27:27 -0500 Subject: [PATCH 020/270] refactor(StopViewer): Make tests pass --- __tests__/test-utils/mock-data/store.js | 6 ++++-- lib/components/viewers/stop-viewer.js | 2 +- lib/reducers/create-user-reducer.js | 23 ++++++++++++++++------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/__tests__/test-utils/mock-data/store.js b/__tests__/test-utils/mock-data/store.js index 90e7a696e..b0e073caf 100644 --- a/__tests__/test-utils/mock-data/store.js +++ b/__tests__/test-utils/mock-data/store.js @@ -8,7 +8,8 @@ import { Provider } from 'react-redux' import configureStore from 'redux-mock-store' import thunk from 'redux-thunk' -import {getInitialState} from '../../../lib/reducers/create-otp-reducer' +import { getInitialState } from '../../../lib/reducers/create-otp-reducer' +import { getUserInitialState } from '../../../lib/reducers/create-user-reducer' Enzyme.configure({ adapter: new EnzymeReactAdapter() }) @@ -26,7 +27,8 @@ export function getMockInitialState () { const mockInitialQuery = {} return clone({ otp: getInitialState(mockConfig, mockInitialQuery), - router: connectRouter(history) + router: connectRouter(history), + user: getUserInitialState(mockConfig) }) } diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index e7c1bcd45..c6f9cfbba 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -356,7 +356,7 @@ const mapStateToProps = (state, ownProps) => { const stopViewerConfig = getStopViewerConfig(state.otp) return { autoRefreshStopTimes: state.otp.user.autoRefreshStopTimes, - favoriteStops: state.user.localUser.favoriteStops, + favoriteStops: (state.user.localUser && state.user.localUser.favoriteStops) || [], homeTimezone: state.otp.config.homeTimezone, viewedStop: state.otp.ui.viewedStop, showUserSettings, diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 8f1f13eff..9e8bb6aa0 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -21,8 +21,9 @@ const MAX_RECENT_STORAGE = 5 * are fetched separately as soon as user login info is received * (from one of the components that uses withLoggedInUserSupport). */ -function loadUserFromLocalStoragePerConfig (config) { - const { persistence = {} } = config +function loadUserFromLocalStorage (config) { + const { locations: configLocations = null, persistence = {} } = config + const persistenceStrategy = persistence.enabled && persistence.strategy if (persistenceStrategy === 'localStorage') { // User's home and work locations @@ -40,8 +41,8 @@ function loadUserFromLocalStoragePerConfig (config) { // Filter valid locations found into locations list. const locations = [home, work].filter(p => p) // Add configurable locations to home and work locations - if (config.locations) { - locations.push(...config.locations.map(l => ({ ...l, type: 'suggested' }))) + if (configLocations) { + locations.push(...configLocations.map(l => ({ ...l, type: 'suggested' }))) } return { @@ -62,10 +63,14 @@ function loadUserFromLocalStoragePerConfig (config) { } } -function createUserReducer (config) { - const localStorageState = loadUserFromLocalStoragePerConfig(config) +/** + * Create the initial user state of otp-react-redux using the provided config, any + * and the user stored in localStorage. + */ +export function getUserInitialState (config) { + const localStorageState = loadUserFromLocalStorage(config) - const initialState = { + return { accessToken: null, itineraryExistence: null, lastPhoneSmsRequest: { @@ -79,6 +84,10 @@ function createUserReducer (config) { pathBeforeSignIn: null, ...localStorageState } +} + +function createUserReducer (config) { + const initialState = getUserInitialState(config) return (state = initialState, action) => { switch (action.type) { From cb06ae592244905d4679e0781008ad6ade80bd29 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 22 Feb 2021 18:01:28 -0500 Subject: [PATCH 021/270] docs(create-user-reducer): Fix typo --- lib/reducers/create-user-reducer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 9e8bb6aa0..d7dfbb4ce 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -17,7 +17,7 @@ const MAX_RECENT_STORAGE = 5 * - recent places in trip plan searches, * - favorite stops * - * Note: If the persistence stragegy is otp_middleware, then user settings + * Note: If the persistence strategy is otp_middleware, then user settings * are fetched separately as soon as user login info is received * (from one of the components that uses withLoggedInUserSupport). */ From ca5e7107cfed74b17e848d92822d1ef916840cc5 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 23 Feb 2021 11:11:15 -0500 Subject: [PATCH 022/270] refactor(connect-location-field): Show places according to persistence strategy. --- lib/components/form/connect-location-field.js | 52 +++++++++++++------ lib/components/form/user-settings.js | 18 ++++--- lib/reducers/create-otp-reducer.js | 4 +- lib/reducers/create-user-reducer.js | 9 ++-- lib/util/auth.js | 15 +++--- lib/util/constants.js | 3 +- lib/util/user.js | 23 ++++---- 7 files changed, 72 insertions(+), 52 deletions(-) diff --git a/lib/components/form/connect-location-field.js b/lib/components/form/connect-location-field.js index 1b3597322..dd257e9b1 100644 --- a/lib/components/form/connect-location-field.js +++ b/lib/components/form/connect-location-field.js @@ -3,9 +3,10 @@ import { connect } from 'react-redux' import * as apiActions from '../../actions/api' import * as locationActions from '../../actions/location' import Icon from '../narrative/icon' +import { PERSIST_TO_LOCAL_STORAGE, PERSIST_TO_OTP_MIDDLEWARE } from '../../util/constants' import { getActiveSearch, getShowUserSettings } from '../../util/state' import { isBlank } from '../../util/ui' -import { convertToLocationFieldLocation } from '../../util/user' +import { getPersistenceStrategy, isWork } from '../../util/user' /** * Custom icon component that renders based on the user location icon prop. @@ -19,6 +20,19 @@ const UserLocationIcon = ({ userLocation }) => { return } +/** + * Convert an entry from persisted user savedLocations into LocationField locations: + * - The icon for "Work" places is changed to 'work', + * - The name attribute is filled with the place address if available. + */ +function convertToLocationFieldLocation (place) { + return { + ...place, + icon: isWork(place) ? 'work' : place.icon, + name: place.address + } +} + /** * This higher-order component connects the target (styled) LocationField to the * redux store. @@ -43,21 +57,27 @@ export default function connectLocationField (StyledLocationField, options = {}) const { currentPosition, nearbyStops, sessionSearches } = location const activeSearch = getActiveSearch(state.otp) const query = activeSearch ? activeSearch.query : currentQuery + const { persistence } = config + const persistenceStrategy = getPersistenceStrategy(persistence) - // Clone loggedInUserLocations with changes to conform to LocationField requirements: - // - locations with blank addresses are removed. - // - "Work" location icon name is changed to 'work', - // - location.name is filled with the location address if available. - const loggedInUserLocations = loggedInUser - ? loggedInUser.savedLocations - .filter(loc => !isBlank(loc.address)) - .map(convertToLocationFieldLocation) - : [] - - // Holds saved locations unless excluded in options. - // see notes regarding persistence strategy - // refactor obtaining the locations. - const userSavedLocations = !excludeSavedLocations ? [...loggedInUserLocations, ...user.locations] : [] + // Display saved locations and recent places according to the configured persistence strategy, + // unless displaying user locations is disabled via prop. + let userSavedLocations = [] + let recentPlaces = [] + if (!excludeSavedLocations) { + if (persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE) { + // Remove locations with blank addresses, and + // modify loggedInUserLocations to conform to LocationField requirements. + userSavedLocations = loggedInUser + ? loggedInUser.savedLocations + .filter(loc => !isBlank(loc.address)) + .map(convertToLocationFieldLocation) + : [] + } else if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { + userSavedLocations = user.locations + recentPlaces = user.recentPlaces + } + } const stateToProps = { currentPosition, @@ -67,7 +87,7 @@ export default function connectLocationField (StyledLocationField, options = {}) showUserSettings: getShowUserSettings(state.otp), stopsIndex: transitIndex.stops, UserLocationIconComponent: UserLocationIcon, - userLocationsAndRecentPlaces: [...userSavedLocations, ...user.recentPlaces] + userLocationsAndRecentPlaces: [...userSavedLocations, ...recentPlaces] } // 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 diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index cbc3d8fae..cd056370b 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -12,8 +12,12 @@ import * as userActions from '../../actions/user' import { LinkWithQuery } from '../form/connected-links' import MainPanelPlace from '../user/places/main-panel-place' import PlaceShortcut from '../user/places/place-shortcut' -import { PLACES_PATH } from '../../util/constants' -import { isHome, isWork } from '../../util/user' +import { + PERSIST_TO_LOCAL_STORAGE, + PERSIST_TO_OTP_MIDDLEWARE, + PLACES_PATH +} from '../../util/constants' +import { getPersistenceStrategy, isHome, isWork } from '../../util/user' import { UnpaddedList } from './styled' const { summarizeQuery } = coreUtils.query @@ -77,7 +81,7 @@ class UserSettings extends Component { trueUser } = this.props - if (loggedInUser && persistenceStrategy === 'otp_middleware') { // use constants + if (loggedInUser && persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE) { // Add id attribute using index, that will be used to call deleteUserPlace. const loggedInUserLocations = loggedInUser.savedLocations.map((loc, index) => ({ ...loc, @@ -111,7 +115,7 @@ class UserSettings extends Component { />
    ) - } else if (persistenceStrategy === 'localStorage') { + } else if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { const { favoriteStops, storeTripHistory, recentPlaces } = localUser // Clone locations in order to prevent blank locations from seeping into the // app state/store. @@ -284,9 +288,7 @@ const RecentTrips = ({ forgetSearch, setQueryParam, tripRequests = null, user }) const mapStateToProps = (state, ownProps) => { const { config, currentQuery, location, transitIndex } = state.otp - const { language, persistence = {} } = config - console.log(config) - + const { language, persistence } = config const { localUser, localUserTripRequests, loggedInUser, loggedInUserTripRequests } = state.user return { config, @@ -296,7 +298,7 @@ const mapStateToProps = (state, ownProps) => { loggedInUser, loggedInUserTripRequests, nearbyStops: location.nearbyStops, - persistenceStrategy: persistence.enabled && persistence.strategy, + persistenceStrategy: getPersistenceStrategy(persistence), query: currentQuery, sessionSearches: location.sessionSearches, stopsIndex: transitIndex.stops, diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index cc85a6989..feaf214a2 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -5,7 +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 {FETCH_STATUS, PERSIST_TO_LOCAL_STORAGE} from '../util/constants' import {getTimestamp} from '../util/state' import {isBatchRoutingEnabled} from '../util/itinerary' @@ -42,7 +42,7 @@ function validateInitialState (initialState) { // See https://developers.arcgis.com/rest/geocode/api-reference/geocoding-free-vs-paid.htm if ( objectPath.get(config, 'persistence.enabled') && - objectPath.get(config, 'persistence.strategy') === 'localStorage' && + objectPath.get(config, 'persistence.strategy') === PERSIST_TO_LOCAL_STORAGE && objectPath.get(config, 'geocoder.type') === 'ARCGIS' ) { errors.push(new Error('Local Storage persistence and ARCGIS geocoder cannot be enabled at the same time!')) diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index d7dfbb4ce..e967f126a 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -3,6 +3,9 @@ import clone from 'clone' import update from 'immutability-helper' import coreUtils from '@opentripplanner/core-utils' +import { PERSIST_TO_LOCAL_STORAGE } from '../util/constants' +import { getPersistenceStrategy } from '../util/user' + const { getItem, removeItem, storeItem } = coreUtils.storage const MAX_RECENT_STORAGE = 5 @@ -22,10 +25,10 @@ const MAX_RECENT_STORAGE = 5 * (from one of the components that uses withLoggedInUserSupport). */ function loadUserFromLocalStorage (config) { - const { locations: configLocations = null, persistence = {} } = config + const { locations: configLocations = null, persistence } = config - const persistenceStrategy = persistence.enabled && persistence.strategy - if (persistenceStrategy === 'localStorage') { + const persistenceStrategy = getPersistenceStrategy(persistence) + if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { // User's home and work locations const home = getItem('home') const work = getItem('work') diff --git a/lib/util/auth.js b/lib/util/auth.js index 1da8e5e08..3cf333482 100644 --- a/lib/util/auth.js +++ b/lib/util/auth.js @@ -1,4 +1,5 @@ -import { ACCOUNT_PATH, PERSISTENCE_STRATEGY_OTP_MIDDLEWARE } from './constants' +import { ACCOUNT_PATH, PERSIST_TO_OTP_MIDDLEWARE } from './constants' +import { getPersistenceStrategy } from './user' /** * Custom links under the user account dropdown. @@ -20,17 +21,13 @@ export const accountLinks = [ /** * Obtains the Auth0 {domain, audience, clientId} configuration, if the following applies in config.yml: - * - persistence is defined, - * - persistence.enabled is true, - * - persistence.strategy is 'otp_middleware', + * - persistence.strategy is 'otp_middleware' (and persistence is enabled), * - persistence.auth0 is defined. * @param persistence The OTP persistence configuration from config.yml. * @returns The Auth0 configuration, or null if the conditions are not met. */ export function getAuth0Config (persistence) { - if (persistence) { - const { enabled = false, strategy = null, auth0 = null } = persistence - return (enabled && strategy === PERSISTENCE_STRATEGY_OTP_MIDDLEWARE) ? auth0 : null - } - return null + return getPersistenceStrategy(persistence) === PERSIST_TO_OTP_MIDDLEWARE + ? persistence.auth0 + : null } diff --git a/lib/util/constants.js b/lib/util/constants.js index 2d5f69964..c656fa5db 100644 --- a/lib/util/constants.js +++ b/lib/util/constants.js @@ -2,7 +2,8 @@ export const AUTH0_AUDIENCE = 'https://otp-middleware' // This should match the value expected in otp-middleware OtpUser#AUTH0_SCOPE export const AUTH0_SCOPE = 'otp-user' export const DEFAULT_APP_TITLE = 'OpenTripPlanner' -export const PERSISTENCE_STRATEGY_OTP_MIDDLEWARE = 'otp_middleware' +export const PERSIST_TO_LOCAL_STORAGE = 'localStorage' +export const PERSIST_TO_OTP_MIDDLEWARE = 'otp_middleware' export const FETCH_STATUS = { UNFETCHED: 0, diff --git a/lib/util/user.js b/lib/util/user.js index 25e1c2814..87efb6b80 100644 --- a/lib/util/user.js +++ b/lib/util/user.js @@ -85,19 +85,6 @@ export function positionHomeAndWorkFirst (userData) { userData.savedLocations = reorderedLocations } -/** - * Convert an entry from persisted user savedLocations into LocationField locations: - * - The icon for "Work" places is changed to 'work', - * - The name attribute is filled with the place address if available. - */ -export function convertToLocationFieldLocation (place) { - return { - ...place, - icon: isWork(place) ? 'work' : place.icon, - name: place.address - } -} - /** * Convert a LocationField entry to a persisted user savedLocations: * - The icon for "Work" places is changed to 'briefcase', @@ -113,3 +100,13 @@ export function convertToPlace (location) { title: formatStoredPlaceName(location) } } + +/** + * Returns the persistence.strategy string if the following apply in config.yml, null otherwise: + * - persistence is defined, + * - persistence.enabled is true, + * - persistence.strategy is defined. + */ +export function getPersistenceStrategy (persistence) { + return persistence && persistence.enabled && persistence.strategy +} From 15a58dd1b1538139f1d874b36c7a30a22c73b6d7 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 23 Feb 2021 16:34:59 -0500 Subject: [PATCH 023/270] refactor(user reducer): Move TOGGLE_TRACKING action to user reducer. --- lib/reducers/create-otp-reducer.js | 17 ----------------- lib/reducers/create-user-reducer.js | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index feaf214a2..aa949a68b 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -505,23 +505,6 @@ function createOtpReducer (config, initialQuery) { return update(state, { user: { expandAdvanced: { $set: action.payload } } }) - case 'TOGGLE_TRACKING': { - storeItem('trackRecent', action.payload) - let recentPlaces = clone(state.user.recentPlaces) - let recentSearches = clone(state.user.recentSearches) - if (!action.payload) { - // If user disables tracking, remove recent searches and locations. - recentPlaces = [] - recentSearches = [] - removeItem('recent') - removeItem('recentSearches') - } - return update(state, { user: { - trackRecent: { $set: action.payload }, - recentPlaces: { $set: recentPlaces }, - recentSearches: { $set: recentSearches } - } }) - } case 'REMEMBER_SEARCH': const searches = clone(state.user.recentSearches) const duplicateIndex = searches.findIndex(s => isEqual(s.query, action.payload.query)) diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index e967f126a..5a38531c6 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -207,7 +207,6 @@ function createUserReducer (config) { storeItem('favoriteStops', favoriteStops) return update(state, { localUser: { favoriteStops: { $set: favoriteStops } } }) } - case 'FORGET_SEARCH': { const recentSearches = clone(state.localUserTripRequests) const index = recentSearches.findIndex(l => l.id === action.payload) @@ -218,6 +217,23 @@ function createUserReducer (config) { ? update(state, { localUserTripRequests: { $splice: [[index, 1]] } }) : state } + case 'TOGGLE_TRACKING': { + storeItem('trackRecent', action.payload) + let recentPlaces = clone(state.localUser.recentPlaces) + let recentSearches = clone(state.localUser.recentSearches) + if (!action.payload) { + // If user disables tracking, remove recent searches and locations. + recentPlaces = [] + recentSearches = [] + removeItem('recent') + removeItem('recentSearches') + } + return update(state, { localUser: { + recentPlaces: { $set: recentPlaces }, + recentSearches: { $set: recentSearches }, + storeTripHistory: { $set: action.payload } + } }) + } default: return state From bf8548cf8d80bc62c777c26ee6e0b14c908f8fa7 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 23 Feb 2021 16:53:28 -0500 Subject: [PATCH 024/270] refactor(user reducer): Move REMEMBER_SEARCH to user reducer --- lib/reducers/create-otp-reducer.js | 14 -------------- lib/reducers/create-user-reducer.js | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index aa949a68b..2f4311d98 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -1,6 +1,5 @@ import clone from 'clone' import update from 'immutability-helper' -import isEqual from 'lodash.isequal' import objectPath from 'object-path' import coreUtils from '@opentripplanner/core-utils' @@ -505,19 +504,6 @@ function createOtpReducer (config, initialQuery) { return update(state, { user: { expandAdvanced: { $set: action.payload } } }) - case 'REMEMBER_SEARCH': - const searches = clone(state.user.recentSearches) - const duplicateIndex = searches.findIndex(s => isEqual(s.query, action.payload.query)) - // Overwrite duplicate search (so that new timestamp is stored). - if (duplicateIndex !== -1) searches[duplicateIndex] = action.payload - else searches.unshift(action.payload) - const sortedSearches = searches.sort((a, b) => b.timestamp - a.timestamp) - // Ensure recent searches do not extend beyond MAX_RECENT_STORAGE - if (sortedSearches.length >= MAX_RECENT_STORAGE) { - sortedSearches.splice(MAX_RECENT_STORAGE) - } - storeItem('recentSearches', sortedSearches) - return update(state, { user: { searches: { $set: sortedSearches } } }) case 'SET_AUTOPLAN': return update(state, { config: { autoPlan: { $set: action.payload.autoPlan } } diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 5a38531c6..e8c4fdcf4 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -1,6 +1,7 @@ /* eslint-disable complexity */ import clone from 'clone' import update from 'immutability-helper' +import isEqual from 'lodash.isequal' import coreUtils from '@opentripplanner/core-utils' import { PERSIST_TO_LOCAL_STORAGE } from '../util/constants' @@ -179,6 +180,7 @@ function createUserReducer (config) { ? update(state, { localUser: { favoriteStops: { $splice: [[removeIndex, 1]] } } }) : state } + case 'REMEMBER_STOP': { // Payload is stop data. We want to avoid saving other attributes that // might be contained there (like lists of patterns). @@ -207,6 +209,7 @@ function createUserReducer (config) { storeItem('favoriteStops', favoriteStops) return update(state, { localUser: { favoriteStops: { $set: favoriteStops } } }) } + case 'FORGET_SEARCH': { const recentSearches = clone(state.localUserTripRequests) const index = recentSearches.findIndex(l => l.id === action.payload) @@ -217,6 +220,22 @@ function createUserReducer (config) { ? update(state, { localUserTripRequests: { $splice: [[index, 1]] } }) : state } + + case 'REMEMBER_SEARCH': { + const searches = clone(state.localUserTripRequests) + const duplicateIndex = searches.findIndex(s => isEqual(s.query, action.payload.query)) + // Overwrite duplicate search (so that new timestamp is stored). + if (duplicateIndex !== -1) searches[duplicateIndex] = action.payload + else searches.unshift(action.payload) + const sortedSearches = searches.sort((a, b) => b.timestamp - a.timestamp) + // Ensure recent searches do not extend beyond MAX_RECENT_STORAGE + if (sortedSearches.length >= MAX_RECENT_STORAGE) { + sortedSearches.splice(MAX_RECENT_STORAGE) + } + storeItem('recentSearches', sortedSearches) + return update(state, { localUserTripRequests: { $set: sortedSearches } }) + } + case 'TOGGLE_TRACKING': { storeItem('trackRecent', action.payload) let recentPlaces = clone(state.localUser.recentPlaces) From 487466783fdf906d956929b9a76fdad4a934602b Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 23 Feb 2021 17:39:44 -0500 Subject: [PATCH 025/270] refactor(user reducer): Move FORGET_PLACE, REMEMBER_PLACE to user reducer. --- lib/actions/map.js | 43 ++++++++++----- .../map/connected-endpoints-overlay.js | 9 ++-- lib/reducers/create-otp-reducer.js | 53 ------------------- lib/reducers/create-user-reducer.js | 31 +++++++++++ 4 files changed, 66 insertions(+), 70 deletions(-) diff --git a/lib/actions/map.js b/lib/actions/map.js index 26cbcbcef..4ab38ceaa 100644 --- a/lib/actions/map.js +++ b/lib/actions/map.js @@ -19,12 +19,27 @@ import { clearActiveSearch } from './form' // Private actions const clearingLocation = createAction('CLEAR_LOCATION') const settingLocation = createAction('SET_LOCATION') +const deleteRecentPlace = createAction('DELETE_LOCAL_USER_RECENT_PLACE') +const deleteSavedPlace = createAction('DELETE_LOCAL_USER_SAVED_PLACE') // Public actions -export const forgetPlace = createAction('FORGET_PLACE') -export const rememberPlace = createAction('REMEMBER_PLACE') -export const forgetStop = createAction('FORGET_STOP') -export const rememberStop = createAction('REMEMBER_STOP') +export const rememberPlace = createAction('REMEMBER_LOCAL_USER_PLACE') +export const forgetStop = createAction('DELETE_LOCAL_USER_STOP') +export const rememberStop = createAction('REMEMBER_LOCAL_USER_STOP') + +/** + * Dispatches the action to delete a saved or recent place from localStorage. + */ +export function forgetPlace (placeId) { + return function (dispatch, getState) { + // Recent place IDs contain the string literal 'recent'. + if (placeId.indexOf('recent') !== -1) { + dispatch(deleteRecentPlace(placeId)) + } else { + dispatch(deleteSavedPlace(placeId)) + } + } +} export function clearLocation (payload) { return function (dispatch, getState) { @@ -59,13 +74,13 @@ export function setLocation (payload) { .reverse({ point: payload.location }) .then((location) => { dispatch(settingLocation({ - locationType: payload.locationType, - location + location, + locationType: payload.locationType })) }).catch(err => { dispatch(settingLocation({ - locationType: payload.locationType, - location: payload.location + location: payload.location, + locationType: payload.locationType })) console.warn(err) }) @@ -83,10 +98,10 @@ export function setLocationToCurrent (payload) { const currentPosition = getState().otp.location.currentPosition if (currentPosition.error || !currentPosition.coords) return payload.location = { + category: 'CURRENT_LOCATION', lat: currentPosition.coords.latitude, lon: currentPosition.coords.longitude, - name: '(Current Location)', - category: 'CURRENT_LOCATION' + name: '(Current Location)' } dispatch(settingLocation(payload)) } @@ -97,12 +112,12 @@ export function switchLocations () { const { from, to } = getState().otp.currentQuery // First, reverse the locations. dispatch(settingLocation({ - locationType: 'from', - location: to + location: to, + locationType: 'from' })) dispatch(settingLocation({ - locationType: 'to', - location: from + location: from, + locationType: 'to' })) // Then kick off a routing query (if the query is invalid, search will abort). dispatch(routingQuery()) diff --git a/lib/components/map/connected-endpoints-overlay.js b/lib/components/map/connected-endpoints-overlay.js index 0e85d3d80..ce4e7a465 100644 --- a/lib/components/map/connected-endpoints-overlay.js +++ b/lib/components/map/connected-endpoints-overlay.js @@ -21,10 +21,13 @@ const mapStateToProps = (state, ownProps) => { // Intermediate places doesn't trigger a re-plan, so for now default to // current query. FIXME: Determine with TriMet if this is desired behavior. const places = state.otp.currentQuery.intermediatePlaces.filter(p => p) + const { localUser, loggedInUser } = state.user + const trueUser = loggedInUser || localUser + return { fromLocation: from, intermediatePlaces: places, - locations: state.otp.user.locations, + locations: trueUser.savedLocations, showUserSettings, toLocation: to, visible: true @@ -32,10 +35,10 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { + clearLocation, forgetPlace, rememberPlace, - setLocation, - clearLocation + setLocation } export default connect(mapStateToProps, mapDispatchToProps)(EndpointsOverlay) diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 2f4311d98..4a185db9b 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -9,7 +9,6 @@ import {getTimestamp} from '../util/state' import {isBatchRoutingEnabled} from '../util/itinerary' const { isTransit, getTransitModes } = coreUtils.itinerary -const { matchLatLon } = coreUtils.map const { filterProfileOptions } = coreUtils.profile const { ensureSingleAccessMode, @@ -19,8 +18,6 @@ const { const { getItem, removeItem, storeItem } = coreUtils.storage const { getUserTimezone } = coreUtils.time -const MAX_RECENT_STORAGE = 5 - // TODO: fire planTrip action if default query is complete/error-free /** @@ -447,56 +444,6 @@ function createOtpReducer (config, initialQuery) { case 'STORE_DEFAULT_SETTINGS': storeItem('defaultQuery', action.payload) return update(state, { user: { defaults: { $set: action.payload } } }) - case 'FORGET_PLACE': { - // Payload is the place ID. - // Recent place IDs contain the string literal 'recent'. - if (action.payload.indexOf('recent') !== -1) { - const recentPlaces = clone(state.user.recentPlaces) - // Remove recent from list of recent places - const removeIndex = recentPlaces.findIndex(l => l.id === action.payload) - recentPlaces.splice(removeIndex, 1) - storeItem('recent', recentPlaces) - return removeIndex !== -1 - ? update(state, { user: { recentPlaces: { $splice: [[removeIndex, 1]] } } }) - : state - } else { - const locations = clone(state.user.locations) - const removeIndex = locations.findIndex(l => l.id === action.payload) - removeItem(action.payload) - return removeIndex !== -1 - ? update(state, { user: { locations: { $splice: [[removeIndex, 1]] } } }) - : state - } - } - case 'REMEMBER_PLACE': { - const { location, type } = action.payload - switch (type) { - case 'recent': { - const recentPlaces = clone(state.user.recentPlaces) - const index = recentPlaces.findIndex(l => matchLatLon(l, location)) - // Replace recent place if duplicate found or add to list. - if (index !== -1) recentPlaces.splice(index, 1, location) - else recentPlaces.push(location) - const sortedPlaces = recentPlaces.sort((a, b) => b.timestamp - a.timestamp) - // Only keep up to 5 recent locations - // FIXME: Check for duplicates - if (recentPlaces.length >= MAX_RECENT_STORAGE) { - sortedPlaces.splice(MAX_RECENT_STORAGE) - } - storeItem('recent', recentPlaces) - return update(state, { user: { recentPlaces: { $set: sortedPlaces } } }) - } - default: { - const locations = clone(state.user.locations) - // Determine if location type (e.g., home or work) already exists in list - const index = locations.findIndex(l => l.type === type) - if (index !== -1) locations.splice(index, 1, location) - else locations.push(location) - storeItem(type, location) - return update(state, { user: { locations: { $set: locations } } }) - } - } - } // FIXME: set up action case 'TOGGLE_ADVANCED_OPTIONS': storeItem('expandAdvanced', action.payload) diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index e8c4fdcf4..af4326595 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -7,6 +7,7 @@ import coreUtils from '@opentripplanner/core-utils' import { PERSIST_TO_LOCAL_STORAGE } from '../util/constants' import { getPersistenceStrategy } from '../util/user' +const { matchLatLon } = coreUtils.map const { getItem, removeItem, storeItem } = coreUtils.storage const MAX_RECENT_STORAGE = 5 @@ -168,6 +169,36 @@ function createUserReducer (config) { : state } + case 'REMEMBER_LOCAL_USER_PLACE': { + const { location, type } = action.payload + switch (type) { + case 'recent': { + const recentPlaces = clone(state.localUser.recentPlaces) + const index = recentPlaces.findIndex(l => matchLatLon(l, location)) + // Replace recent place if duplicate found or add to list. + if (index !== -1) recentPlaces.splice(index, 1, location) + else recentPlaces.push(location) + const sortedPlaces = recentPlaces.sort((a, b) => b.timestamp - a.timestamp) + // Only keep up to 5 recent locations + // FIXME: Check for duplicates + if (recentPlaces.length >= MAX_RECENT_STORAGE) { + sortedPlaces.splice(MAX_RECENT_STORAGE) + } + storeItem('recent', recentPlaces) + return update(state, { localUser: { recentPlaces: { $set: sortedPlaces } } }) + } + default: { + const locations = clone(state.localUser.savedLocations) + // Determine if location type (e.g., home or work) already exists in list + const index = locations.findIndex(l => l.type === type) + if (index !== -1) locations.splice(index, 1, location) + else locations.push(location) + storeItem(type, location) + return update(state, { localUser: { savedLocations: { $set: locations } } }) + } + } + } + case 'FORGET_STOP': { const stopId = action.payload const favoriteStops = clone(state.localUser.favoriteStops) From 452627e90e28743b6f111ae1cecbeb6aef338827 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 23 Feb 2021 17:40:46 -0500 Subject: [PATCH 026/270] refactor(user reducer): Remove redundant prompt to delete localStorage place --- lib/reducers/create-user-reducer.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index af4326595..0bf36c01c 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -144,8 +144,6 @@ function createUserReducer (config) { } case 'DELETE_LOCAL_USER_RECENT_PLACE': { - if (!confirm('Would you like to remove this place?')) return state - // This is used to delete the local user's recent location that matches the provided id. const placeId = action.payload const recentPlaces = clone(state.localUser.recentPlaces) From 13d811d3ddb824925d61d3a8a8fd3f35e50379df Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 24 Feb 2021 12:12:56 -0500 Subject: [PATCH 027/270] refactor(user reducer): Refactor deletion from place list. --- lib/reducers/create-user-reducer.js | 53 +++++++++++++---------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 0bf36c01c..ccfd7de94 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -12,6 +12,23 @@ const { getItem, removeItem, storeItem } = coreUtils.storage const MAX_RECENT_STORAGE = 5 +/** + * Removes a place by id from the specified localUser state and optional persistence setting. + */ +function removeLocalUserPlace (id, state, fieldName, settingName) { + const originalArray = state.localUser[fieldName] + const removeIndex = originalArray.findIndex(l => l.id === id) + // If a persistence setting is provided, create a new array without the specified element. + if (settingName) { + const newArray = clone(originalArray) + newArray.splice(removeIndex, 1) + storeItem(settingName, newArray) + } + return removeIndex !== -1 + ? update(state, { localUser: { [fieldName]: { $splice: [[removeIndex, 1]] } } }) + : state +} + /** * Load select user settings stored locally if the persistence strategy is localStorage. * Other settings not mentioned below are still loaded through createOtpReducer. @@ -101,6 +118,7 @@ function createUserReducer (config) { accessToken: { $set: action.payload } }) } + case 'SET_CURRENT_USER': { return update(state, { loggedInUser: { $set: action.payload } @@ -143,28 +161,15 @@ function createUserReducer (config) { }) } - case 'DELETE_LOCAL_USER_RECENT_PLACE': { + case 'DELETE_LOCAL_USER_RECENT_PLACE': // This is used to delete the local user's recent location that matches the provided id. - const placeId = action.payload - const recentPlaces = clone(state.localUser.recentPlaces) - // Remove recent from list of recent places - const removeIndex = recentPlaces.findIndex(l => l.id === placeId) - recentPlaces.splice(removeIndex, 1) - storeItem('recent', recentPlaces) - return removeIndex !== -1 - ? update(state, { localUser: { recentPlaces: { $splice: [[removeIndex, 1]] } } }) - : state - } + return removeLocalUserPlace(action.payload, state, 'recentPlaces', 'recent') case 'DELETE_LOCAL_USER_SAVED_PLACE': { // This is used to delete the local user's Home and Work (or other built-in) // location that matches the provided id. - const placeId = action.payload - const removeIndex = state.localUser.savedLocations.findIndex(l => l.id === placeId) - removeItem(placeId) - return removeIndex !== -1 - ? update(state, { localUser: { savedLocations: { $splice: [[removeIndex, 1]] } } }) - : state + removeItem(action.payload) + return removeLocalUserPlace(action.payload, state, 'savedLocations') } case 'REMEMBER_LOCAL_USER_PLACE': { @@ -197,18 +202,8 @@ function createUserReducer (config) { } } - case 'FORGET_STOP': { - const stopId = action.payload - const favoriteStops = clone(state.localUser.favoriteStops) - - // This is used to delete the local user's favorite stop that matches the provided id. - const removeIndex = favoriteStops.findIndex(l => l.id === stopId) - favoriteStops.splice(removeIndex, 1) - storeItem('favoriteStops', favoriteStops) - return removeIndex !== -1 - ? update(state, { localUser: { favoriteStops: { $splice: [[removeIndex, 1]] } } }) - : state - } + case 'FORGET_STOP': + return removeLocalUserPlace(action.payload, state, 'favoriteStops', 'favoriteStops') case 'REMEMBER_STOP': { // Payload is stop data. We want to avoid saving other attributes that From f6acf45c7a9e08e7782a995635f4bdf7358d3615 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 24 Feb 2021 17:13:20 -0500 Subject: [PATCH 028/270] refactor: Make Save/Forget locations from map work again. --- lib/actions/api.js | 6 +- lib/actions/map.js | 7 +- lib/actions/user.js | 68 ++++++++++++++++++- lib/components/form/connect-location-field.js | 38 +++-------- lib/components/form/user-settings.js | 8 +-- .../map/connected-endpoints-overlay.js | 16 ++++- lib/components/user/places/favorite-place.js | 6 +- lib/components/user/places/place-shortcut.js | 24 +++++-- lib/util/user.js | 44 ++++++++++-- 9 files changed, 158 insertions(+), 59 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index 5e901d58b..8951de93d 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -8,7 +8,7 @@ import queryParams from '@opentripplanner/core-utils/lib/query-params' import { createAction } from 'redux-actions' import qs from 'qs' -import { rememberPlace } from './map' +import { rememberPlace } from './user' import { getStopViewerConfig, queryIsValid } from '../util/state' import { getSecureFetchOptions } from '../util/middleware' @@ -84,7 +84,7 @@ export function routingQuery (searchId = null) { return async function (dispatch, getState) { // FIXME: batchId is searchId for now. const state = getState() - const otpState = state.otp + const { otp: otpState, user: userState } = state const isNewSearch = !searchId if (isNewSearch) searchId = randId() @@ -112,7 +112,7 @@ export function routingQuery (searchId = null) { dispatch(routingResponse({ response: json, requestId, searchId })) // If tracking is enabled, store locations and search after successful // search is completed. - if (otpState.user.trackRecent) { + if (userState.trueUser && userState.trueUser.storeTripHistory) { const { from, to } = otpState.currentQuery if (!isStoredPlace(from)) { dispatch(rememberPlace({ type: 'recent', location: formatRecentPlace(from) })) diff --git a/lib/actions/map.js b/lib/actions/map.js index 4ab38ceaa..f2912acb4 100644 --- a/lib/actions/map.js +++ b/lib/actions/map.js @@ -4,6 +4,7 @@ import { createAction } from 'redux-actions' import { routingQuery } from './api' import { clearActiveSearch } from './form' +import { deleteUserPlace } from './user' /* SET_LOCATION action creator. Updates a from or to location in the store * @@ -20,10 +21,8 @@ import { clearActiveSearch } from './form' const clearingLocation = createAction('CLEAR_LOCATION') const settingLocation = createAction('SET_LOCATION') const deleteRecentPlace = createAction('DELETE_LOCAL_USER_RECENT_PLACE') -const deleteSavedPlace = createAction('DELETE_LOCAL_USER_SAVED_PLACE') // Public actions -export const rememberPlace = createAction('REMEMBER_LOCAL_USER_PLACE') export const forgetStop = createAction('DELETE_LOCAL_USER_STOP') export const rememberStop = createAction('REMEMBER_LOCAL_USER_STOP') @@ -32,11 +31,11 @@ export const rememberStop = createAction('REMEMBER_LOCAL_USER_STOP') */ export function forgetPlace (placeId) { return function (dispatch, getState) { - // Recent place IDs contain the string literal 'recent'. + // localStorage only: Recent place IDs contain the string literal 'recent'. if (placeId.indexOf('recent') !== -1) { dispatch(deleteRecentPlace(placeId)) } else { - dispatch(deleteSavedPlace(placeId)) + dispatch(deleteUserPlace(placeId)) } } } diff --git a/lib/actions/user.js b/lib/actions/user.js index f31db421e..8324fef9b 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -7,10 +7,14 @@ import { createAction } from 'redux-actions' import { routingQuery } from './api' import { setQueryParam } from './form' import { routeTo } from './ui' -import { TRIPS_PATH } from '../util/constants' +import { + PERSIST_TO_LOCAL_STORAGE, + PERSIST_TO_OTP_MIDDLEWARE, + TRIPS_PATH +} from '../util/constants' import { secureFetch } from '../util/middleware' import { isBlank } from '../util/ui' -import { isNewUser, positionHomeAndWorkFirst } from '../util/user' +import { convertToPlace, getPersistenceStrategy, isHomeOrWork, isNewUser, positionHomeAndWorkFirst } from '../util/user' const { planParamsToQuery } = coreUtils.query const { OTP_API_DATE_FORMAT } = coreUtils.time @@ -35,6 +39,7 @@ const setitineraryExistence = createAction('SET_ITINERARY_EXISTENCE') export const deleteLocalUserRecentPlace = createAction('DELETE_LOCAL_USER_RECENT_PLACE') export const deleteLocalUserSavedPlace = createAction('DELETE_LOCAL_USER_SAVED_PLACE') export const deleteLocalUserStop = createAction('FORGET_STOP') +const rememberLocalUserPlace = createAction('REMEMBER_LOCAL_USER_PLACE') function createNewUser (auth0User) { return { @@ -501,7 +506,7 @@ export function saveUserPlace (placeToSave, placeIndex) { /** * Delete the place data at the specified index for the logged-in user. */ -export function deleteUserPlace (placeIndex) { +export function deleteLoggedInUserPlace (placeIndex) { return function (dispatch, getState) { if (!confirm('Would you like to remove this place?')) return @@ -511,3 +516,60 @@ export function deleteUserPlace (placeIndex) { dispatch(createOrUpdateUser(loggedInUser, true)) } } + +//Is there any way to do a factory interface?? +//If not, simplify the dispatch returns (remove dispatch). +/** + * Delete place data by id for the logged-in or local user + * according to the persistence strategy. + */ +export function deleteUserPlace (placeId) { + return function (dispatch, getState) { + const { otp, user } = getState() + const persistenceStrategy = getPersistenceStrategy(otp.config.persistence) + const { loggedInUser } = user + + if (persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE && loggedInUser) { + // For middleware loggedInUsers, this method should only be triggered by the + // 'Forget home' or 'Forget work' links from OTP UI's EndPointOverlay/EndPoint, + // with placeId set to 'home' or 'work'. + if (isHomeOrWork({ type: placeId })) { + // Find the index of the place in the loggedInUser.savedLocations + const placeIndex = loggedInUser.savedLocations.findIndex(loc => loc.type === placeId) + if (placeIndex > -1) { + dispatch(deleteLoggedInUserPlace(placeIndex)) + } + } + } else if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { + dispatch(deleteLocalUserSavedPlace(placeId)) + } + } +} + +/** + * Remembers a place for the logged-in or local user + * according to the persistence strategy. + */ +export function rememberPlace (placeTypeLocation) { + return function (dispatch, getState) { + const { otp, user } = getState() + const persistenceStrategy = getPersistenceStrategy(otp.config.persistence) + const { loggedInUser } = user + + if (persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE && loggedInUser) { + // For middleware loggedInUsers, this method should only be triggered by the + // 'Save as home' or 'Save as work' links from OTP UI's EndPointOverlay/EndPoint. + const { location } = placeTypeLocation + if (isHomeOrWork(location)) { + // Find the index of the place in the loggedInUser.savedLocations + const placeIndex = loggedInUser.savedLocations.findIndex(loc => loc.type === location.type) + if (placeIndex > -1) { + // Convert to loggedInUser saved place + dispatch(saveUserPlace(convertToPlace(location), placeIndex)) + } + } + } else if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { + dispatch(rememberLocalUserPlace(placeTypeLocation)) + } + } +} diff --git a/lib/components/form/connect-location-field.js b/lib/components/form/connect-location-field.js index dd257e9b1..60f10a63a 100644 --- a/lib/components/form/connect-location-field.js +++ b/lib/components/form/connect-location-field.js @@ -5,8 +5,7 @@ import * as locationActions from '../../actions/location' import Icon from '../narrative/icon' import { PERSIST_TO_LOCAL_STORAGE, PERSIST_TO_OTP_MIDDLEWARE } from '../../util/constants' import { getActiveSearch, getShowUserSettings } from '../../util/state' -import { isBlank } from '../../util/ui' -import { getPersistenceStrategy, isWork } from '../../util/user' +import { getOtpUiLocations, getPersistenceStrategy } from '../../util/user' /** * Custom icon component that renders based on the user location icon prop. @@ -20,19 +19,6 @@ const UserLocationIcon = ({ userLocation }) => { return } -/** - * Convert an entry from persisted user savedLocations into LocationField locations: - * - The icon for "Work" places is changed to 'work', - * - The name attribute is filled with the place address if available. - */ -function convertToLocationFieldLocation (place) { - return { - ...place, - icon: isWork(place) ? 'work' : place.icon, - name: place.address - } -} - /** * This higher-order component connects the target (styled) LocationField to the * redux store. @@ -52,30 +38,24 @@ export default function connectLocationField (StyledLocationField, options = {}) includeLocation = false } = options const mapStateToProps = (state, ownProps) => { - const { config, currentQuery, location, transitIndex, user } = state.otp - const { loggedInUser } = state.user + const { config, currentQuery, location, transitIndex } = state.otp const { currentPosition, nearbyStops, sessionSearches } = location const activeSearch = getActiveSearch(state.otp) const query = activeSearch ? activeSearch.query : currentQuery - const { persistence } = config - const persistenceStrategy = getPersistenceStrategy(persistence) // Display saved locations and recent places according to the configured persistence strategy, - // unless displaying user locations is disabled via prop. + // unless displaying user locations is disabled via prop (e.g. in the saved-place editor + // when the loggedInUser defines their saved locations). let userSavedLocations = [] let recentPlaces = [] if (!excludeSavedLocations) { + const { localUser, loggedInUser } = state.user + const persistenceStrategy = getPersistenceStrategy(config.persistence) if (persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE) { - // Remove locations with blank addresses, and - // modify loggedInUserLocations to conform to LocationField requirements. - userSavedLocations = loggedInUser - ? loggedInUser.savedLocations - .filter(loc => !isBlank(loc.address)) - .map(convertToLocationFieldLocation) - : [] + userSavedLocations = getOtpUiLocations(loggedInUser) } else if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { - userSavedLocations = user.locations - recentPlaces = user.recentPlaces + userSavedLocations = localUser.locations + recentPlaces = localUser.recentPlaces } } diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index cd056370b..9adf31725 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -69,7 +69,7 @@ class UserSettings extends Component { deleteLocalUserRecentPlace, deleteLocalUserSavedPlace, deleteLocalUserStop, - deleteUserPlace, + deleteLoggedInUserPlace, forgetSearch, localUser, loggedInUser, @@ -82,7 +82,7 @@ class UserSettings extends Component { } = this.props if (loggedInUser && persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE) { - // Add id attribute using index, that will be used to call deleteUserPlace. + // Add id attribute using index, that will be used to call deleteLoggedInUserPlace. const loggedInUserLocations = loggedInUser.savedLocations.map((loc, index) => ({ ...loc, id: index @@ -102,7 +102,7 @@ class UserSettings extends Component { @@ -312,7 +312,7 @@ const mapDispatchToProps = { deleteLocalUserRecentPlace: userActions.deleteLocalUserRecentPlace, deleteLocalUserSavedPlace: userActions.deleteLocalUserSavedPlace, deleteLocalUserStop: userActions.deleteLocalUserStop, - deleteUserPlace: userActions.deleteUserPlace, + deleteLoggedInUserPlace: userActions.deleteLoggedInUserPlace, forgetSearch: apiActions.forgetSearch, setLocation: mapActions.setLocation, setQueryParam: formActions.setQueryParam, diff --git a/lib/components/map/connected-endpoints-overlay.js b/lib/components/map/connected-endpoints-overlay.js index ce4e7a465..267b371d2 100644 --- a/lib/components/map/connected-endpoints-overlay.js +++ b/lib/components/map/connected-endpoints-overlay.js @@ -4,10 +4,12 @@ import { connect } from 'react-redux' import { clearLocation, forgetPlace, - rememberPlace, setLocation } from '../../actions/map' +import { rememberPlace } from '../../actions/user' +import { PERSIST_TO_LOCAL_STORAGE, PERSIST_TO_OTP_MIDDLEWARE } from '../../util/constants' import { getActiveSearch, getShowUserSettings } from '../../util/state' +import { getOtpUiLocations, getPersistenceStrategy } from '../../util/user' // connect to the redux store @@ -22,12 +24,20 @@ const mapStateToProps = (state, ownProps) => { // current query. FIXME: Determine with TriMet if this is desired behavior. const places = state.otp.currentQuery.intermediatePlaces.filter(p => p) const { localUser, loggedInUser } = state.user - const trueUser = loggedInUser || localUser + + const { persistence } = state.otp.config + const persistenceStrategy = getPersistenceStrategy(persistence) + let userSavedLocations = [] + if (persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE) { + userSavedLocations = getOtpUiLocations(loggedInUser) + } else if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { + userSavedLocations = localUser.locations + } return { fromLocation: from, intermediatePlaces: places, - locations: trueUser.savedLocations, + locations: userSavedLocations, showUserSettings, toLocation: to, visible: true diff --git a/lib/components/user/places/favorite-place.js b/lib/components/user/places/favorite-place.js index b8e587d68..c47edf0da 100644 --- a/lib/components/user/places/favorite-place.js +++ b/lib/components/user/places/favorite-place.js @@ -54,8 +54,8 @@ const StyledPlace = styled(Place)` */ class FavoritePlace extends Component { _handleDelete = () => { - const { deleteUserPlace, place } = this.props - deleteUserPlace(place.id) + const { deleteLoggedInUserPlace, place } = this.props + deleteLoggedInUserPlace(place.id) } render () { @@ -82,7 +82,7 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - deleteUserPlace: userActions.deleteUserPlace + deleteLoggedInUserPlace: userActions.deleteLoggedInUserPlace } export default connect(mapStateToProps, mapDispatchToProps)(FavoritePlace) diff --git a/lib/components/user/places/place-shortcut.js b/lib/components/user/places/place-shortcut.js index 452a4bb46..d9e289244 100644 --- a/lib/components/user/places/place-shortcut.js +++ b/lib/components/user/places/place-shortcut.js @@ -3,10 +3,23 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import * as mapActions from '../../../actions/map' -import { convertToPlace } from '../../../util/user' +import { convertToLocation, convertToPlace } from '../../../util/user' import MainPanelPlace from './main-panel-place' -const { matchLatLon } = coreUtils.map +const { formatStoredPlaceName, getDetailText, matchLatLon } = coreUtils.map + +/** + * Calls convertToPlace, and adds some more fields to the resulting place for rendering. + */ +function convertToPlaceWithDetails (location) { + const place = convertToPlace(location) + return { + ...place, + details: getDetailText(location), + id: location.id, + title: formatStoredPlaceName(location) + } +} /** * A shortcut button that sets the provided place as the 'from' or 'to' Place. @@ -39,7 +52,10 @@ class PlaceShortcut extends Component { if (place.blank) { window.alert(`Enter origin/destination in the form (or set via map click) and click the resulting marker to set as ${place.type} location.`) } else { - this._setFromOrToLocation(place) + // Convert to OTP UI location before sending events + // (to avoid issues when user clicks Forget/Save location + // multiple times subsequently) + this._setFromOrToLocation(convertToLocation(place)) } } @@ -57,7 +73,7 @@ class PlaceShortcut extends Component { const { onDelete, onView, path, place } = this.props // localStorage places (where path is not provided) need to be converted, // so the correct fields are passed to MainPanelPlace. - const convertedPlace = path ? place : convertToPlace(place) + const convertedPlace = path ? place : convertToPlaceWithDetails(place) return ( !isBlank(place.address)) + .map(convertToLocation) + : [] +} From 7d7604b48165d4cf6ef4565595b96358a0ce21f4 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 25 Feb 2021 19:16:33 -0500 Subject: [PATCH 029/270] refactor(user/places/place): Remove actions (use explicit handlers) --- lib/components/form/connect-location-field.js | 2 +- lib/components/form/user-settings.js | 38 ++--- .../map/connected-endpoints-overlay.js | 2 +- .../user/places/favorite-place-list.js | 10 +- lib/components/user/places/favorite-place.js | 15 +- .../user/places/main-panel-place.js | 21 ++- lib/components/user/places/place-shortcut.js | 32 +++- lib/components/user/places/place.js | 159 +++++++----------- lib/util/user.js | 12 +- 9 files changed, 141 insertions(+), 150 deletions(-) diff --git a/lib/components/form/connect-location-field.js b/lib/components/form/connect-location-field.js index 60f10a63a..d2069ec21 100644 --- a/lib/components/form/connect-location-field.js +++ b/lib/components/form/connect-location-field.js @@ -54,7 +54,7 @@ export default function connectLocationField (StyledLocationField, options = {}) if (persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE) { userSavedLocations = getOtpUiLocations(loggedInUser) } else if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { - userSavedLocations = localUser.locations + userSavedLocations = localUser.savedLocations recentPlaces = localUser.recentPlaces } } diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 9adf31725..797ec36ec 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -17,7 +17,8 @@ import { PERSIST_TO_OTP_MIDDLEWARE, PLACES_PATH } from '../../util/constants' -import { getPersistenceStrategy, isHome, isWork } from '../../util/user' +import { isBlank } from '../../util/ui' +import { canDeletePlace, getPersistenceStrategy, isHome, isWork } from '../../util/user' import { UnpaddedList } from './styled' const { summarizeQuery } = coreUtils.query @@ -85,6 +86,7 @@ class UserSettings extends Component { // Add id attribute using index, that will be used to call deleteLoggedInUserPlace. const loggedInUserLocations = loggedInUser.savedLocations.map((loc, index) => ({ ...loc, + blank: isBlank(loc.address), id: index })) const savedPlacesHeader = ( @@ -204,16 +206,18 @@ const Places = ({ {header &&
    {header}
    } {places.length > 0 - ? places.map((location, index) => ( - - )) - : textIfEmpty && {textIfEmpty} + ? places.map((location, index) => { + return ( + + ) + }) + : (textIfEmpty && {textIfEmpty}) } @@ -239,18 +243,14 @@ class TripRequest extends Component { const name = summarizeQuery(query, user.savedLocations) const timeInfo = moment(timestamp).fromNow() - const place = { - details: timeInfo, - icon: 'clock-o', - name, - title: `${name} (${timeInfo})` - } - return ( ) } diff --git a/lib/components/map/connected-endpoints-overlay.js b/lib/components/map/connected-endpoints-overlay.js index 267b371d2..4bcf2a2c3 100644 --- a/lib/components/map/connected-endpoints-overlay.js +++ b/lib/components/map/connected-endpoints-overlay.js @@ -31,7 +31,7 @@ const mapStateToProps = (state, ownProps) => { if (persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE) { userSavedLocations = getOtpUiLocations(loggedInUser) } else if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { - userSavedLocations = localUser.locations + userSavedLocations = localUser.savedLocations } return { diff --git a/lib/components/user/places/favorite-place-list.js b/lib/components/user/places/favorite-place-list.js index 1099ee06e..f48c32435 100644 --- a/lib/components/user/places/favorite-place-list.js +++ b/lib/components/user/places/favorite-place-list.js @@ -4,7 +4,7 @@ import { connect } from 'react-redux' import { UnpaddedList } from '../../form/styled' import { CREATE_ACCOUNT_PLACES_PATH, PLACES_PATH } from '../../../util/constants' -import FavoritePlace from './favorite-place' +import FavoritePlace, { StyledPlace } from './favorite-place' /** * Renders an editable list user's favorite locations, and lets the user add a new one. @@ -18,6 +18,7 @@ const FavoritePlaceList = ({ isCreating, loggedInUser }) => { {savedLocations.map((place, index) => ( { )} {/* For adding a new place. */} - +
    ) diff --git a/lib/components/user/places/favorite-place.js b/lib/components/user/places/favorite-place.js index c47edf0da..f4e7dab8d 100644 --- a/lib/components/user/places/favorite-place.js +++ b/lib/components/user/places/favorite-place.js @@ -3,11 +3,11 @@ import { connect } from 'react-redux' import styled from 'styled-components' import * as userActions from '../../../actions/user' +import { canDeletePlace } from '../../../util/user' import Place, { ActionButton, ActionButtonPlaceholder, EDIT_LABEL, - getActionsForPlace, PlaceButton, PlaceContent, PlaceDetail, @@ -16,7 +16,7 @@ import Place, { const FIELD_HEIGHT_PX = '60px' -const StyledPlace = styled(Place)` +export const StyledPlace = styled(Place)` align-items: stretch; display: flex; height: ${FIELD_HEIGHT_PX}; @@ -54,21 +54,24 @@ const StyledPlace = styled(Place)` */ class FavoritePlace extends Component { _handleDelete = () => { - const { deleteLoggedInUserPlace, place } = this.props - deleteLoggedInUserPlace(place.id) + const { deleteLoggedInUserPlace, index } = this.props + deleteLoggedInUserPlace(index) } render () { const { path, place } = this.props const label = place && EDIT_LABEL + const { address, icon, name, type } = place return ( ) diff --git a/lib/components/user/places/main-panel-place.js b/lib/components/user/places/main-panel-place.js index 5ad34ad69..9500bc307 100644 --- a/lib/components/user/places/main-panel-place.js +++ b/lib/components/user/places/main-panel-place.js @@ -4,7 +4,6 @@ import styled from 'styled-components' import Place, { ActionButton, EDIT_LABEL, - getActionsForPlace, PlaceDetail, PlaceName } from './place' @@ -23,31 +22,35 @@ const StyledPlace = styled(Place)` ` /** - * Wrapper for the Place component in the main panel that - * handles deleting the place. + * Wrapper for the Place component in the main panel. */ const MainPanelPlace = ({ + details, + icon, + name, onClick, onDelete, onView, path, - place + title }) => { - // Determine title and aria label. + //?? Determine aria label const isPlanAction = !!onClick const planLabel = 'Plan an itinerary using this entry' - const label = isPlanAction ? planLabel : EDIT_LABEL - const title = isPlanAction ? `${place.title || place.address}\n${planLabel}` : EDIT_LABEL + const label = isPlanAction ? `${title}\n${planLabel}` : EDIT_LABEL return ( ) } diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 08ad7677d..e8de1a764 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -5,7 +5,6 @@ import styled from 'styled-components' import { LinkContainerWithQuery } from '../../form/connected-links' import NarrativeIcon from '../../narrative/icon' -import { isHomeOrWork } from '../../../util/user' const Container = styled.li` align-items: stretch; @@ -37,92 +36,32 @@ export const ActionButton = styled(Button)` export const ActionButtonPlaceholder = styled.span`` export const EDIT_LABEL = 'Edit this place' - -/** - * Obtains the actions (e.g. delete, view) for the given place and handlers: - * - All places can be deleted, except Home and Work with empty addresses. - * - Only 'stop' locations can be 'viewed'. - */ -export function getActionsForPlace (place, onDelete, onView) { - if (!place) return [] - - const deleteAction = { - icon: 'trash-o', - onClick: onDelete, - title: 'Delete place' - } - const viewAction = { - icon: 'search', - onClick: onView, - title: 'View stop' - } - - const actions = [] - - const isFixed = isHomeOrWork(place) - if (onView && place.type === 'stop') { - actions.push(viewAction) - } - if (onDelete && !place.blank && (!isFixed || place.address)) { - actions.push(deleteAction) - } - - return actions -} +const VIEW_LABEL = 'View stop' // used for stops only. +const DELETE_LABEL = 'Delete place' /** * Renders a clickable button for editing a user's favorite place, - * and buttons for the provided actions (e.g. view, delete). + * and buttons for viewing and deleting the place if corresponding handlers are provided. */ const Place = ({ - actions, ariaLabel, buttonStyle, className, largeIcon, onClick, + onDelete, + onView, path, - place, placeDetailClassName, placeTextClassName, - title + title, + + name, + icon, + details }) => { const to = onClick ? null : path const iconSize = largeIcon && '2x' - let placeButton - if (place) { - const { address, details, icon, name, type } = place - placeButton = ( - - - {largeIcon && } - - - {!largeIcon && } - {name || address} - - - - {name && (details || address || `Set your ${type} address`)} - - - - ) - } else { - placeButton = ( - - - - Add another place - - - ) - } return ( @@ -131,35 +70,53 @@ const Place = ({ activeClassName='' to={to} > - {placeButton} + + {largeIcon && } + + + {!largeIcon && } + {name} + + + + {details} + + + {/* Action buttons. If none, render a placeholder. */} - {actions && actions.length - ? actions.map(({ icon: actionIcon, onClick: onAction, title: actionTitle }, index) => ( - - - - )) - : - } + {onView && ( + + + + )} + {onDelete && ( + + + + )} + {!onView && !onDelete && } ) } Place.propTypes = { - /** The action buttons for the place. */ - actions: PropTypes.arrayOf(PropTypes.shape({ - icon: PropTypes.string, - onClick: PropTypes.func, - title: PropTypes.string - })), /** The aria-label for the main button */ ariaLabel: PropTypes.string, /** The Bootstrap style to apply to buttons. */ @@ -170,21 +127,19 @@ Place.propTypes = { onClick: PropTypes.func, /** The path to navigate to on click. */ path: PropTypes.string, - /** The place to render. */ - place: PropTypes.shape({ - address: PropTypes.string, - details: PropTypes.string, - icon: PropTypes.string.isRequired, - name: PropTypes.string, - title: PropTypes.string, - type: PropTypes.string - }), /** CSS class name for the place details. */ placeDetailClassName: PropTypes.string, /** CSS class name for the place name. */ placeTextClassName: PropTypes.string, /** The title for the main button */ - title: PropTypes.string + title: PropTypes.string//, + + //name, + //icon, + //details + //onView + //onDelete + } export default Place diff --git a/lib/util/user.js b/lib/util/user.js index 5efaaea95..02cc72596 100644 --- a/lib/util/user.js +++ b/lib/util/user.js @@ -121,12 +121,12 @@ export function getPersistenceStrategy (persistence) { * - The name attribute is filled with the place address. */ export function convertToLocation (place) { - const { address, icon, lat, lon, type } = place + const { address, icon, lat, lon, name, type } = place return { icon: isWork(place) ? 'work' : icon, lat, lon, - name: address, + name: address || name, type } } @@ -142,3 +142,11 @@ export function getOtpUiLocations (loggedInUser) { .map(convertToLocation) : [] } + +/** + * Determines whether to allow a place to be deleted. + * @returns true if a place is not 'home' or 'work', or if it is, it should have an address or not be marked blank'. + */ +export function canDeletePlace (place, useBlankFlag) { + return !isHomeOrWork(place) || (useBlankFlag && !place.blank) || place.address +} From 2faf061e05123ca247df6ceca1f761eabb97a5a7 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 25 Feb 2021 19:23:54 -0500 Subject: [PATCH 030/270] refactor(actions/map): Fix stop action names --- lib/actions/map.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/actions/map.js b/lib/actions/map.js index f2912acb4..326017d1b 100644 --- a/lib/actions/map.js +++ b/lib/actions/map.js @@ -23,8 +23,8 @@ const settingLocation = createAction('SET_LOCATION') const deleteRecentPlace = createAction('DELETE_LOCAL_USER_RECENT_PLACE') // Public actions -export const forgetStop = createAction('DELETE_LOCAL_USER_STOP') -export const rememberStop = createAction('REMEMBER_LOCAL_USER_STOP') +export const forgetStop = createAction('FORGET_STOP') +export const rememberStop = createAction('REMEMBER_STOP') /** * Dispatches the action to delete a saved or recent place from localStorage. From 47c5daac0e86a1a7314efdd950d31e3c765e2719 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 25 Feb 2021 22:13:16 -0500 Subject: [PATCH 031/270] refactor(PlaceShortcut): Fix tooltips and save/forget home/work from map. --- lib/components/user/places/place-shortcut.js | 12 ++++++------ lib/util/user.js | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/components/user/places/place-shortcut.js b/lib/components/user/places/place-shortcut.js index a89914bf8..27978652e 100644 --- a/lib/components/user/places/place-shortcut.js +++ b/lib/components/user/places/place-shortcut.js @@ -71,19 +71,19 @@ class PlaceShortcut extends Component { const { onDelete, onView, path, place } = this.props // localStorage places (where path is not provided) need to be converted, // so the correct fields are passed to MainPanelPlace. - const convertedPlace = path ? place : convertToPlaceWithId(place) - - const { address, blank, icon, name, type } = convertedPlace - const placeTitle = formatStoredPlaceName(place) - const placeAddress = blank ? null : address + const convertedPlace = path ? place : convertToPlace(place, true) + const { address, icon, name, type } = convertedPlace let placeName + let placeTitle let details if (path) { // for middleware places placeName = name || address details = name && (address || `Set your ${type} address`) + placeTitle = `${placeName}${details && ` (${details})`}` } else { placeName = formatStoredPlaceName(place, false) + placeTitle = formatStoredPlaceName(place) details = getDetailText(place) } @@ -96,7 +96,7 @@ class PlaceShortcut extends Component { onDelete={onDelete && this._onDelete} onView={onView && this._onView} path={path} - title={placeTitle || placeAddress} + title={placeTitle} /> ) } diff --git a/lib/util/user.js b/lib/util/user.js index 02cc72596..2ad3a7cfd 100644 --- a/lib/util/user.js +++ b/lib/util/user.js @@ -89,15 +89,16 @@ export function positionHomeAndWorkFirst (userData) { /** * Convert a LocationField entry to a persisted user savedLocations: - * - id is removed. + * - id is included if requested, * - The icon for "Work" places is changed to 'briefcase', * - The address attribute is filled with the 'name' if available. */ -export function convertToPlace (location) { - const { icon, lat, lon, name, type } = location +export function convertToPlace (location, includeId) { + const { icon, id, lat, lon, name, type } = location return { address: name, icon: isWork(location) ? 'briefcase' : icon, + id: includeId ? id : undefined, lat, lon, name: formatStoredPlaceName(location, false), From d5cf4da180605949f77d561611929b4c70ff2514 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 26 Feb 2021 14:01:03 -0500 Subject: [PATCH 032/270] refactor(places): Decouple styling and rendering by place type --- lib/components/form/user-settings.js | 37 +++++--- lib/components/user/places/cached-place.js | 53 +++++++++++ .../user/places/favorite-place-list.js | 16 ++-- lib/components/user/places/favorite-place.js | 91 ------------------- .../user/places/main-panel-place.js | 61 ------------- lib/components/user/places/place-shortcut.js | 50 ++-------- lib/components/user/places/saved-place.js | 51 +++++++++++ lib/components/user/places/styled.js | 80 ++++++++++++++++ lib/util/user.js | 8 +- 9 files changed, 231 insertions(+), 216 deletions(-) create mode 100644 lib/components/user/places/cached-place.js delete mode 100644 lib/components/user/places/favorite-place.js delete mode 100644 lib/components/user/places/main-panel-place.js create mode 100644 lib/components/user/places/saved-place.js create mode 100644 lib/components/user/places/styled.js diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 797ec36ec..f98f40623 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -10,19 +10,35 @@ import * as mapActions from '../../actions/map' import * as uiActions from '../../actions/ui' import * as userActions from '../../actions/user' import { LinkWithQuery } from '../form/connected-links' -import MainPanelPlace from '../user/places/main-panel-place' +import CachedPlace from '../user/places/cached-place' +import SavedPlace from '../user/places/saved-place' import PlaceShortcut from '../user/places/place-shortcut' +import { MainPanelPlace } from '../user/places/styled' import { PERSIST_TO_LOCAL_STORAGE, - PERSIST_TO_OTP_MIDDLEWARE, - PLACES_PATH + PERSIST_TO_OTP_MIDDLEWARE } from '../../util/constants' import { isBlank } from '../../util/ui' -import { canDeletePlace, getPersistenceStrategy, isHome, isWork } from '../../util/user' +import { getPersistenceStrategy, isHome, isWork } from '../../util/user' import { UnpaddedList } from './styled' const { summarizeQuery } = coreUtils.query +/** + * Associate a styled Place component to the provided Place component for rendering. + */ +function stylePlace (BasePlaceComponent) { + return props => ( + + ) +} + +const StyledCachedPlace = stylePlace(CachedPlace) +const StyledSavedPlace = stylePlace(SavedPlace) + class UserSettings extends Component { _disableTracking = () => { const { localUser, localUserTripRequests, toggleTracking } = this.props @@ -70,7 +86,6 @@ class UserSettings extends Component { deleteLocalUserRecentPlace, deleteLocalUserSavedPlace, deleteLocalUserStop, - deleteLoggedInUserPlace, forgetSearch, localUser, loggedInUser, @@ -102,13 +117,11 @@ class UserSettings extends Component { return (
    - } - { return ( ) }) diff --git a/lib/components/user/places/cached-place.js b/lib/components/user/places/cached-place.js new file mode 100644 index 000000000..5b03b24c7 --- /dev/null +++ b/lib/components/user/places/cached-place.js @@ -0,0 +1,53 @@ +import coreUtils from '@opentripplanner/core-utils' +import React, { Component } from 'react' +import { connect } from 'react-redux' + +const { formatStoredPlaceName, getDetailText } = coreUtils.map + +/** + * Wrapper for the Place component for places cached in localStorage. + */ +class CachedPlace extends Component { + _onView = () => { + const { onView, place } = this.props + onView({ stopId: place.id }) + } + + _onDelete = () => { + const { onDelete, place } = this.props + onDelete(place.id) + } + + render () { + const { onClick, onDelete, onView, place, PlaceComponent } = this.props + const { blank, icon, type } = place + + const placeName = formatStoredPlaceName(place, false) + const placeTitle = formatStoredPlaceName(place) + const details = getDetailText(place) + const canDelete = !blank && ['stop', 'home', 'work', 'recent'].indexOf(type) !== -1 + + return ( + + ) + } +} + +// connect to redux store + +const mapStateToProps = (state, ownProps) => { + return {} +} + +const mapDispatchToProps = { +} + +export default connect(mapStateToProps, mapDispatchToProps)(CachedPlace) diff --git a/lib/components/user/places/favorite-place-list.js b/lib/components/user/places/favorite-place-list.js index f48c32435..425b96aeb 100644 --- a/lib/components/user/places/favorite-place-list.js +++ b/lib/components/user/places/favorite-place-list.js @@ -3,8 +3,10 @@ import { ControlLabel } from 'react-bootstrap' import { connect } from 'react-redux' import { UnpaddedList } from '../../form/styled' -import { CREATE_ACCOUNT_PLACES_PATH, PLACES_PATH } from '../../../util/constants' -import FavoritePlace, { StyledPlace } from './favorite-place' +import { CREATE_ACCOUNT_PLACES_PATH } from '../../../util/constants' +import { getPlaceBasePath } from '../../../util/user' +import SavedPlace from './saved-place' +import { FavoritePlace } from './styled' /** * Renders an editable list user's favorite locations, and lets the user add a new one. @@ -17,21 +19,21 @@ const FavoritePlaceList = ({ isCreating, loggedInUser }) => { Add the places you frequent often to save time planning trips: {savedLocations.map((place, index) => ( - ) )} {/* For adding a new place. */} -
    diff --git a/lib/components/user/places/favorite-place.js b/lib/components/user/places/favorite-place.js deleted file mode 100644 index f4e7dab8d..000000000 --- a/lib/components/user/places/favorite-place.js +++ /dev/null @@ -1,91 +0,0 @@ -import React, { Component } from 'react' -import { connect } from 'react-redux' -import styled from 'styled-components' - -import * as userActions from '../../../actions/user' -import { canDeletePlace } from '../../../util/user' -import Place, { - ActionButton, - ActionButtonPlaceholder, - EDIT_LABEL, - PlaceButton, - PlaceContent, - PlaceDetail, - Icon -} from './place' - -const FIELD_HEIGHT_PX = '60px' - -export const StyledPlace = styled(Place)` - align-items: stretch; - display: flex; - height: ${FIELD_HEIGHT_PX}; - margin-bottom: 10px; - - ${PlaceButton} { - align-items: center; - display: flex; - flex: 1 0 0; - overflow: hidden; - text-align: left; - text-overflow: ellipsis; - } - ${PlaceContent} { - display: inline-block; - margin-left: 10px; - } - ${PlaceDetail} { - color: #888; - display: block; - } - ${Icon} { - color: #888; - flex-shrink: 0; - } - ${ActionButton}, ${ActionButtonPlaceholder} { - margin-left: 4px; - width: ${FIELD_HEIGHT_PX}; - } -` - -/** - * Wrapper for the Place component in FavoritePlaceList that - * handles deleting the place. - */ -class FavoritePlace extends Component { - _handleDelete = () => { - const { deleteLoggedInUserPlace, index } = this.props - deleteLoggedInUserPlace(index) - } - - render () { - const { path, place } = this.props - const label = place && EDIT_LABEL - const { address, icon, name, type } = place - - return ( - - ) - } -} - -// connect to redux store - -const mapStateToProps = (state, ownProps) => { - return {} -} - -const mapDispatchToProps = { - deleteLoggedInUserPlace: userActions.deleteLoggedInUserPlace -} - -export default connect(mapStateToProps, mapDispatchToProps)(FavoritePlace) diff --git a/lib/components/user/places/main-panel-place.js b/lib/components/user/places/main-panel-place.js deleted file mode 100644 index 9500bc307..000000000 --- a/lib/components/user/places/main-panel-place.js +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react' -import styled from 'styled-components' - -import Place, { - ActionButton, - EDIT_LABEL, - PlaceDetail, - PlaceName -} from './place' - -const StyledPlace = styled(Place)` - ${PlaceName} { - margin-left: 0.25em; - } - ${PlaceDetail} { - display: block; - height: 100%; - } - ${ActionButton} { - width: 40px; - } -` - -/** - * Wrapper for the Place component in the main panel. - */ -const MainPanelPlace = ({ - details, - icon, - name, - onClick, - onDelete, - onView, - path, - title -}) => { - //?? Determine aria label - const isPlanAction = !!onClick - const planLabel = 'Plan an itinerary using this entry' - const label = isPlanAction ? `${title}\n${planLabel}` : EDIT_LABEL - - return ( - - ) -} - -export default MainPanelPlace diff --git a/lib/components/user/places/place-shortcut.js b/lib/components/user/places/place-shortcut.js index 27978652e..aea6d4c1b 100644 --- a/lib/components/user/places/place-shortcut.js +++ b/lib/components/user/places/place-shortcut.js @@ -3,21 +3,9 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import * as mapActions from '../../../actions/map' -import { convertToLocation, convertToPlace } from '../../../util/user' -import MainPanelPlace from './main-panel-place' +import { convertToLocation } from '../../../util/user' -const { formatStoredPlaceName, getDetailText, matchLatLon } = coreUtils.map - -/** - * Calls convertToPlace, and adds the id of the original location. - */ -function convertToPlaceWithId (location) { - const place = convertToPlace(location) - return { - ...place, - id: location.id - } -} +const { matchLatLon } = coreUtils.map /** * A shortcut button that sets the provided place as the 'from' or 'to' Place. @@ -68,35 +56,15 @@ class PlaceShortcut extends Component { } render () { - const { onDelete, onView, path, place } = this.props - // localStorage places (where path is not provided) need to be converted, - // so the correct fields are passed to MainPanelPlace. - const convertedPlace = path ? place : convertToPlace(place, true) - const { address, icon, name, type } = convertedPlace - - let placeName - let placeTitle - let details - if (path) { // for middleware places - placeName = name || address - details = name && (address || `Set your ${type} address`) - placeTitle = `${placeName}${details && ` (${details})`}` - } else { - placeName = formatStoredPlaceName(place, false) - placeTitle = formatStoredPlaceName(place) - details = getDetailText(place) - } - + const { index, onDelete, onView, path, place, PlaceComponent } = this.props return ( - ) } diff --git a/lib/components/user/places/saved-place.js b/lib/components/user/places/saved-place.js new file mode 100644 index 000000000..d09105328 --- /dev/null +++ b/lib/components/user/places/saved-place.js @@ -0,0 +1,51 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +import * as userActions from '../../../actions/user' +import { getPlaceBasePath, isHomeOrWork } from '../../../util/user' + +const EDIT_LABEL = 'Edit this place' + +/** + * Wrapper for the Place component for saved places in OTP Middleware. + */ +class SavedPlace extends Component { + _handleDelete = () => { + const { deleteLoggedInUserPlace, index } = this.props + deleteLoggedInUserPlace(index) + } + + render () { + const { isCreating, index, label, onClick, place, PlaceComponent } = this.props + const placeLabel = place && (label || EDIT_LABEL) + const { address, icon, name, type } = place + const canDelete = !isHomeOrWork(place) || address + //const placeTitle = `${placeName}${details && ` (${details})`}` + + return ( + + ) + } +} + +// connect to redux store + +const mapStateToProps = (state, ownProps) => { + return { + } +} + +const mapDispatchToProps = { + deleteLoggedInUserPlace: userActions.deleteLoggedInUserPlace +} + +export default connect(mapStateToProps, mapDispatchToProps)(SavedPlace) diff --git a/lib/components/user/places/styled.js b/lib/components/user/places/styled.js new file mode 100644 index 000000000..775f779e5 --- /dev/null +++ b/lib/components/user/places/styled.js @@ -0,0 +1,80 @@ +import styled from 'styled-components' + +import Place, { + ActionButton, + ActionButtonPlaceholder, + Icon, + PlaceButton, + PlaceContent, + PlaceDetail, + PlaceName +} from './place' + +// Styles and exports for favorite place components +// used in the My account page. + +const MY_PLACE_FIELD_HEIGHT_PX = '60px' + +const StyledFavoritePlace = styled(Place)` + align-items: stretch; + display: flex; + height: ${MY_PLACE_FIELD_HEIGHT_PX}; + margin-bottom: 10px; + + ${PlaceButton} { + align-items: center; + display: flex; + flex: 1 0 0; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; + } + ${PlaceContent} { + display: inline-block; + margin-left: 10px; + } + ${PlaceDetail} { + color: #888; + display: block; + } + ${Icon} { + color: #888; + flex-shrink: 0; + } + ${ActionButton}, ${ActionButtonPlaceholder} { + margin-left: 4px; + width: ${MY_PLACE_FIELD_HEIGHT_PX}; + } +` + +export const FavoritePlace = props => ( + +) + +// Styles and exports for the place component +// used in the main panel. +const StyledMainPanelPlace = styled(Place)` + ${PlaceName} { + margin-left: 0.25em; + } + ${PlaceDetail} { + display: block; + height: 100%; + } + ${ActionButton} { + width: 40px; + } +` + +export const MainPanelPlace = props => ( + +) diff --git a/lib/util/user.js b/lib/util/user.js index 2ad3a7cfd..f20c7a16f 100644 --- a/lib/util/user.js +++ b/lib/util/user.js @@ -1,5 +1,6 @@ import coreUtils from '@opentripplanner/core-utils' +import { CREATE_ACCOUNT_PLACES_PATH, PLACES_PATH } from './constants' import { isBlank } from './ui' const { formatStoredPlaceName } = coreUtils.map @@ -145,9 +146,8 @@ export function getOtpUiLocations (loggedInUser) { } /** - * Determines whether to allow a place to be deleted. - * @returns true if a place is not 'home' or 'work', or if it is, it should have an address or not be marked blank'. + * Obtains the base path for editing a place depending on whether user is creating a new account. */ -export function canDeletePlace (place, useBlankFlag) { - return !isHomeOrWork(place) || (useBlankFlag && !place.blank) || place.address +export function getPlaceBasePath (isCreating) { + return isCreating ? CREATE_ACCOUNT_PLACES_PATH : PLACES_PATH } From 6861f954467e89262574e06972c3c6bcef6b531b Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 26 Feb 2021 14:30:51 -0500 Subject: [PATCH 033/270] refactor(places): Tweak tooltips and aria-labels --- lib/components/form/user-settings.js | 1 + lib/components/user/places/cached-place.js | 3 +++ lib/components/user/places/saved-place.js | 20 ++++++++++++-------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index f98f40623..0d6d826b7 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -258,6 +258,7 @@ class TripRequest extends Component { return ( ) } From 2b44cb0812da3a13f817c3aa7a8a30f15a7e75b8 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 26 Feb 2021 15:38:01 -0500 Subject: [PATCH 034/270] refactor: Tidy up code. --- lib/actions/user.js | 2 - lib/components/form/user-settings.js | 14 ++---- lib/components/user/places/cached-place.js | 25 ++-------- .../user/places/favorite-place-list.js | 47 +++++++++---------- lib/components/user/places/place-shortcut.js | 17 ++----- lib/components/user/places/place.js | 33 ++++++------- lib/components/user/places/saved-place.js | 3 -- lib/components/user/places/styled.js | 7 +-- lib/util/user.js | 10 ++-- 9 files changed, 58 insertions(+), 100 deletions(-) diff --git a/lib/actions/user.js b/lib/actions/user.js index 8324fef9b..4d776f9cc 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -517,8 +517,6 @@ export function deleteLoggedInUserPlace (placeIndex) { } } -//Is there any way to do a factory interface?? -//If not, simplify the dispatch returns (remove dispatch). /** * Delete place data by id for the logged-in or local user * according to the persistence strategy. diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 0d6d826b7..2fa8e8f59 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -14,10 +14,7 @@ import CachedPlace from '../user/places/cached-place' import SavedPlace from '../user/places/saved-place' import PlaceShortcut from '../user/places/place-shortcut' import { MainPanelPlace } from '../user/places/styled' -import { - PERSIST_TO_LOCAL_STORAGE, - PERSIST_TO_OTP_MIDDLEWARE -} from '../../util/constants' +import { PERSIST_TO_LOCAL_STORAGE, PERSIST_TO_OTP_MIDDLEWARE } from '../../util/constants' import { isBlank } from '../../util/ui' import { getPersistenceStrategy, isHome, isWork } from '../../util/user' import { UnpaddedList } from './styled' @@ -48,7 +45,9 @@ class UserSettings extends Component { // If user has recents and does not confirm deletion, return without doing // anything. - if (hasRecents && !window.confirm('You have recent searches and/or places stored. Disabling storage of recent places/searches will remove these items. Continue?')) { + if (hasRecents && !window.confirm( + 'You have recent searches and/or places stored. Disabling storage of recent places/searches will remove these items. Continue?' + )) { return } // Disable tracking if we reach this statement. @@ -107,9 +106,7 @@ class UserSettings extends Component { const savedPlacesHeader = ( <> My saved places ( - - manage - + manage ) ) @@ -258,7 +255,6 @@ class TripRequest extends Component { return ( ) } } -// connect to redux store - -const mapStateToProps = (state, ownProps) => { - return {} -} - -const mapDispatchToProps = { -} - -export default connect(mapStateToProps, mapDispatchToProps)(CachedPlace) +export default CachedPlace diff --git a/lib/components/user/places/favorite-place-list.js b/lib/components/user/places/favorite-place-list.js index 425b96aeb..2f8f9f34b 100644 --- a/lib/components/user/places/favorite-place-list.js +++ b/lib/components/user/places/favorite-place-list.js @@ -12,33 +12,28 @@ import { FavoritePlace } from './styled' * 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 FavoritePlaceList = ({ isCreating, loggedInUser }) => { - const { savedLocations } = loggedInUser - return ( -
    - Add the places you frequent often to save time planning trips: - - {savedLocations.map((place, index) => ( - - ) - )} - - {/* For adding a new place. */} - ( +
    + Add the places you frequent often to save time planning trips: + + {loggedInUser.savedLocations.map((place, index) => ( + - -
    - ) -} + ))} + + +
    +
    +) // connect to the redux store diff --git a/lib/components/user/places/place-shortcut.js b/lib/components/user/places/place-shortcut.js index aea6d4c1b..c16a89733 100644 --- a/lib/components/user/places/place-shortcut.js +++ b/lib/components/user/places/place-shortcut.js @@ -8,8 +8,8 @@ import { convertToLocation } from '../../../util/user' const { matchLatLon } = coreUtils.map /** - * A shortcut button that sets the provided place as the 'from' or 'to' Place. - * It wraps MainPanelPlace to let the user edit, view or delete the place. + * A shortcut button that wraps the provided PlaceComponent and sets + * the provided place as the 'from' or 'to' place. */ class PlaceShortcut extends Component { /** @@ -45,25 +45,14 @@ class PlaceShortcut extends Component { } } - _onView = () => { - const { onView, place } = this.props - onView({ stopId: place.id }) - } - - _onDelete = () => { - const { onDelete, place } = this.props - onDelete(place.id) - } - render () { - const { index, onDelete, onView, path, place, PlaceComponent } = this.props + const { index, onDelete, onView, place, PlaceComponent } = this.props return ( ) diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index e8de1a764..e00804dd7 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -35,30 +35,28 @@ export const ActionButton = styled(Button)` export const ActionButtonPlaceholder = styled.span`` -export const EDIT_LABEL = 'Edit this place' const VIEW_LABEL = 'View stop' // used for stops only. const DELETE_LABEL = 'Delete place' /** - * Renders a clickable button for editing a user's favorite place, + * Renders a stylable clickable button for editing/selecting a user's favorite place, * and buttons for viewing and deleting the place if corresponding handlers are provided. */ const Place = ({ ariaLabel, buttonStyle, className, + details, + icon, largeIcon, + name, onClick, onDelete, onView, path, placeDetailClassName, placeTextClassName, - title, - - name, - icon, - details + title }) => { const to = onClick ? null : path const iconSize = largeIcon && '2x' @@ -91,6 +89,7 @@ const Place = ({ {/* Action buttons. If none, render a placeholder. */} + {!onView && !onDelete && } {onView && ( )} - {!onView && !onDelete && } ) } @@ -121,10 +119,20 @@ Place.propTypes = { ariaLabel: PropTypes.string, /** The Bootstrap style to apply to buttons. */ buttonStyle: PropTypes.string, + /** The detail text displayed for the place */ + details: PropTypes.string, + /** The font-awesome icon name for the place. */ + icon: PropTypes.string, /** Whether to render icons large. */ largeIcon: PropTypes.bool, + /** The displayed name for the place. */ + name: PropTypes.string, /** Called when the "main" button is clicked. Takes precedence over the path prop. */ onClick: PropTypes.func, + /** Determines whether the Delete button is shown. Called when the Delete button is clicked. */ + onDelete: PropTypes.func, + /** Determines whether the View button is shown. Called when the View button is clicked. */ + onView: PropTypes.func, /** The path to navigate to on click. */ path: PropTypes.string, /** CSS class name for the place details. */ @@ -132,14 +140,7 @@ Place.propTypes = { /** CSS class name for the place name. */ placeTextClassName: PropTypes.string, /** The title for the main button */ - title: PropTypes.string//, - - //name, - //icon, - //details - //onView - //onDelete - + title: PropTypes.string } export default Place diff --git a/lib/components/user/places/saved-place.js b/lib/components/user/places/saved-place.js index 4cad29462..64b558386 100644 --- a/lib/components/user/places/saved-place.js +++ b/lib/components/user/places/saved-place.js @@ -5,7 +5,6 @@ import * as userActions from '../../../actions/user' import { getPlaceBasePath, isHomeOrWork } from '../../../util/user' const EDIT_LABEL = 'Edit this place' -const PLAN_LABEL = 'Plan itinerary with this place' /** * Wrapper for the Place component for saved places in OTP Middleware. @@ -23,12 +22,10 @@ class SavedPlace extends Component { const placeName = name || address const details = name && (address || `Set your ${type} address`) const useOnClick = address && onClick - const label = useOnClick ? PLAN_LABEL : EDIT_LABEL const title = useOnClick ? `${placeName}${details && ` (${details})`}` : EDIT_LABEL return ( ( // Styles and exports for the place component // used in the main panel. + const StyledMainPanelPlace = styled(Place)` ${PlaceName} { margin-left: 0.25em; diff --git a/lib/util/user.js b/lib/util/user.js index f20c7a16f..83c0243d7 100644 --- a/lib/util/user.js +++ b/lib/util/user.js @@ -90,16 +90,14 @@ export function positionHomeAndWorkFirst (userData) { /** * Convert a LocationField entry to a persisted user savedLocations: - * - id is included if requested, * - The icon for "Work" places is changed to 'briefcase', * - The address attribute is filled with the 'name' if available. */ -export function convertToPlace (location, includeId) { - const { icon, id, lat, lon, name, type } = location +export function convertToPlace (location) { + const { icon, lat, lon, name, type } = location return { address: name, icon: isWork(location) ? 'briefcase' : icon, - id: includeId ? id : undefined, lat, lon, name: formatStoredPlaceName(location, false), @@ -134,8 +132,8 @@ export function convertToLocation (place) { } /** - * Remove entries with blank addresses, and - * convert entries from persisted user savedLocations into LocationField locations. + * Remove entries with blank addresses, and convert entries + * from persisted user savedLocations into LocationField/EndpointOverlay locations. */ export function getOtpUiLocations (loggedInUser) { return loggedInUser From d0b5f675a018b65d206c270f7cca37cfccc9d7a1 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 26 Feb 2021 17:49:04 -0500 Subject: [PATCH 035/270] refactor(user reducer): Move remaining user actions to user reducer. --- lib/actions/form.js | 7 +- lib/components/app/batch-routing-panel.js | 2 +- lib/components/app/call-taker-panel.js | 2 +- lib/components/form/user-trip-settings.js | 10 +- lib/components/viewers/stop-time-cell.js | 2 +- lib/components/viewers/stop-viewer.js | 56 ++++++----- lib/reducers/create-otp-reducer.js | 93 ++++--------------- lib/reducers/create-user-reducer.js | 107 ++++++++++++---------- lib/util/state.js | 6 +- 9 files changed, 128 insertions(+), 157 deletions(-) diff --git a/lib/actions/form.js b/lib/actions/form.js index 0e2688b74..ba5be1f6b 100644 --- a/lib/actions/form.js +++ b/lib/actions/form.js @@ -28,10 +28,11 @@ export const storeDefaultSettings = createAction('STORE_DEFAULT_SETTINGS') export function resetForm () { return function (dispatch, getState) { - const otpState = getState().otp + const { otp: otpState, user: userState } = getState() const { transitModes } = otpState.config.modes - if (otpState.user.defaults) { - dispatch(settingQueryParam(otpState.user.defaults)) + const { defaults } = userState.localUser + if (defaults) { + dispatch(settingQueryParam(defaults)) } else { // Get user overrides and apply to default query const userOverrides = coreUtils.storage.getItem('defaultQuery', {}) diff --git a/lib/components/app/batch-routing-panel.js b/lib/components/app/batch-routing-panel.js index 8e62f1d8c..d7a75e49f 100644 --- a/lib/components/app/batch-routing-panel.js +++ b/lib/components/app/batch-routing-panel.js @@ -241,7 +241,7 @@ const mapStateToProps = (state, ownProps) => { activeSearch: getActiveSearch(state.otp), config: state.otp.config, currentQuery: state.otp.currentQuery, - expandAdvanced: state.otp.user.expandAdvanced, + expandAdvanced: state.user.localUser.expandAdvanced, possibleCombinations: state.otp.config.modes.combinations, showUserSettings } diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index 055b2a8d0..5c6a8a397 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -252,7 +252,7 @@ const mapStateToProps = (state, ownProps) => { return { activeSearch: getActiveSearch(state.otp), currentQuery: state.otp.currentQuery, - expandAdvanced: state.otp.user.expandAdvanced, + expandAdvanced: state.user.localUser.expandAdvanced, mainPanelContent: state.otp.ui.mainPanelContent, modes: state.otp.config.modes, routes: state.otp.transitIndex.routes, diff --git a/lib/components/form/user-trip-settings.js b/lib/components/form/user-trip-settings.js index 130992ccc..254e59132 100644 --- a/lib/components/form/user-trip-settings.js +++ b/lib/components/form/user-trip-settings.js @@ -38,10 +38,10 @@ class UserTripSettings extends Component { const rememberIsDisabled = queryIsDefault && !defaults return ( -
    +
    : null @@ -192,8 +195,9 @@ class StopViewer extends Component { Stop ID: {stopId} @@ -204,16 +208,16 @@ class StopViewer extends Component { onToClick={this._onClickPlanTo} /> {scheduleView && } {timezoneWarning} @@ -292,17 +296,21 @@ class StopViewer extends Component { const mapStateToProps = (state, ownProps) => { const showUserSettings = getShowUserSettings(state.otp) const stopViewerConfig = getStopViewerConfig(state.otp) + const { config, ui } = state.otp + const { homeTimezone, language, persistence, transitOperators } = config + const { autoRefreshStopTimes, favoriteStops } = state.user.localUser return { - autoRefreshStopTimes: state.otp.user.autoRefreshStopTimes, - favoriteStops: (state.user.localUser && state.user.localUser.favoriteStops) || [], - homeTimezone: state.otp.config.homeTimezone, - viewedStop: state.otp.ui.viewedStop, + autoRefreshStopTimes, + enableFavoriteStops: getPersistenceStrategy(persistence) === PERSIST_TO_LOCAL_STORAGE, + favoriteStops, + homeTimezone, showUserSettings, - stopData: state.otp.transitIndex.stops[state.otp.ui.viewedStop.stopId], - stopViewerArriving: state.otp.config.language.stopViewerArriving, + stopData: state.otp.transitIndex.stops[ui.viewedStop.stopId], + stopViewerArriving: language.stopViewerArriving, stopViewerConfig, - timeFormat: getTimeFormat(state.otp.config), - transitOperators: state.otp.config.transitOperators + timeFormat: getTimeFormat(config), + transitOperators, + viewedStop: ui.viewedStop } } diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 4a185db9b..2890197a6 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -10,12 +10,8 @@ import {isBatchRoutingEnabled} from '../util/itinerary' const { isTransit, getTransitModes } = coreUtils.itinerary const { filterProfileOptions } = coreUtils.profile -const { - ensureSingleAccessMode, - getDefaultQuery, - getTripOptionsFromQuery -} = coreUtils.query -const { getItem, removeItem, storeItem } = coreUtils.storage +const { ensureSingleAccessMode, getDefaultQuery } = coreUtils.query +const { getItem } = coreUtils.storage const { getUserTimezone } = coreUtils.time // TODO: fire planTrip action if default query is complete/error-free @@ -65,10 +61,9 @@ export function getInitialState (userDefinedConfig, initialQuery) { autoPlan: false, debouncePlanTimeMs: 0, language: {}, - transitOperators: [], + onTimeThresholdSeconds: 60, realtimeEffectsDisplayThreshold: 120, routingTypes: [], - onTimeThresholdSeconds: 60, stopViewer: { numberOfDepartures: 3, // per pattern // Hide block ids unless explicitly enabled in config. @@ -77,7 +72,8 @@ export function getInitialState (userDefinedConfig, initialQuery) { // a route does not begin service again until Monday, we are showing its next // departure and it is not entirely excluded from display. timeRange: 345600 // four days in seconds - } + }, + transitOperators: [] } const config = Object.assign(defaultConfig, userDefinedConfig) @@ -101,37 +97,15 @@ export function getInitialState (userDefinedConfig, initialQuery) { config.phoneFormatOptions.countryCode = 'US' } - // Load user settings from local storage. - // TODO: Make this work with settings fetched from alternative storage system - // (e.g., OTP backend middleware containing user profile system). + // Load query-related user settings from local storage. // User overrides determine user's default mode/query parameters. const userOverrides = getItem('defaultQuery', {}) // Combine user overrides with default query to get default search settings. const defaults = Object.assign(getDefaultQuery(config), userOverrides) - // Whether to auto-refresh stop arrival times in the Stop Viewer. - const autoRefreshStopTimes = getItem('autoRefreshStopTimes', true) - // User's home and work locations - const home = getItem('home') - const work = getItem('work') - // Whether recent searches and places should be tracked in local storage. - const trackRecent = getItem('trackRecent', false) - const expandAdvanced = getItem('expandAdvanced', false) - // Recent places used in trip plan searches. - const recentPlaces = getItem('recent', []) - // List of user's favorite stops. - const favoriteStops = getItem('favoriteStops', []) - // Recent trip plan searches (excluding time/date parameters to avoid complexity). - const recentSearches = getItem('recentSearches', []) - // Filter valid locations found into locations list. - const locations = [home, work].filter(p => p) // TODO: parse and merge URL query params w/ default query - // populate query by merging any provided query params w/ the default params const currentQuery = Object.assign(defaults, initialQuery) - // Add configurable locations to home and work locations - if (config.locations) { - locations.push(...config.locations.map(l => ({ ...l, type: 'suggested' }))) - } + // Check for alternative routerId in session storage. This is generally used // for testing single GTFS feed OTP graphs that are deployed to feed-specific // routers (e.g., https://otp.server.com/otp/routers/non_default_router). @@ -168,6 +142,7 @@ export function getInitialState (userDefinedConfig, initialQuery) { } return { + activeSearchId: 0, config, currentQuery, filter: { @@ -180,31 +155,13 @@ export function getInitialState (userDefinedConfig, initialQuery) { }, location: { currentPosition: { - error: null, coords: null, + error: null, fetching: false }, - sessionSearches: [], - nearbyStops: [] - }, - user: { - autoRefreshStopTimes, - // Do not store from/to or date/time in defaults - defaults: getTripOptionsFromQuery(defaults), - expandAdvanced, - favoriteStops, - trackRecent, - locations, - recentPlaces, - recentSearches - }, - searches: {}, - transitIndex: { - stops: {}, - trips: {} + nearbyStops: [], + sessionSearches: [] }, - useRealtime: true, - activeSearchId: 0, overlay: { bikeRental: { stations: [] @@ -227,15 +184,21 @@ export function getInitialState (userDefinedConfig, initialQuery) { locations: [] } }, + searches: {}, tnc: { etaEstimates: {}, rideEstimates: {} }, + transitIndex: { + stops: {}, + trips: {} + }, ui: { + diagramLeg: null, mobileScreen: MobileScreens.WELCOME_SCREEN, - printView: window.location.href.indexOf('/print/') !== -1, - diagramLeg: null - } + printView: window.location.href.indexOf('/print/') !== -1 + }, + useRealtime: true } } @@ -438,19 +401,6 @@ function createOtpReducer (config, initialQuery) { return update(state, { activeSearchId: { $set: null } }) case 'SET_ACTIVE_SEARCH': return update(state, { activeSearchId: { $set: action.payload } }) - case 'CLEAR_DEFAULT_SETTINGS': - removeItem('defaultQuery') - return update(state, { user: { defaults: { $set: null } } }) - case 'STORE_DEFAULT_SETTINGS': - storeItem('defaultQuery', action.payload) - return update(state, { user: { defaults: { $set: action.payload } } }) - // FIXME: set up action - case 'TOGGLE_ADVANCED_OPTIONS': - storeItem('expandAdvanced', action.payload) - if (!action.payload) removeItem('expandAdvanced') - return update(state, { user: { - expandAdvanced: { $set: action.payload } - } }) case 'SET_AUTOPLAN': return update(state, { config: { autoPlan: { $set: action.payload.autoPlan } } @@ -669,9 +619,6 @@ function createOtpReducer (config, initialQuery) { } } }) - case 'TOGGLE_AUTO_REFRESH': - storeItem('autoRefreshStopTimes', action.payload) - return update(state, { user: { autoRefreshStopTimes: { $set: action.payload } } }) case 'FIND_ROUTES_RESPONSE': // If routes is undefined, initialize it w/ the full payload diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index ccfd7de94..5b271226b 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -4,10 +4,8 @@ import update from 'immutability-helper' import isEqual from 'lodash.isequal' import coreUtils from '@opentripplanner/core-utils' -import { PERSIST_TO_LOCAL_STORAGE } from '../util/constants' -import { getPersistenceStrategy } from '../util/user' - const { matchLatLon } = coreUtils.map +const { getDefaultQuery, getTripOptionsFromQuery } = coreUtils.query const { getItem, removeItem, storeItem } = coreUtils.storage const MAX_RECENT_STORAGE = 5 @@ -30,58 +28,53 @@ function removeLocalUserPlace (id, state, fieldName, settingName) { } /** - * Load select user settings stored locally if the persistence strategy is localStorage. - * Other settings not mentioned below are still loaded through createOtpReducer. - * The select user settings are: - * - Home and Work locations, - * - recent itinerary searches - * - whether recent searches and places should be tracked - * - recent places in trip plan searches, - * - favorite stops - * - * Note: If the persistence strategy is otp_middleware, then user settings + * Load user settings stored in the browser locally. The local user is always retrieved + * and plays the role of the "anonymous" or "shared" user if no middleware user is logged in. + * Note: If the persistence strategy is otp_middleware, then the middleware user settings * are fetched separately as soon as user login info is received * (from one of the components that uses withLoggedInUserSupport). */ function loadUserFromLocalStorage (config) { - const { locations: configLocations = null, persistence } = config - - const persistenceStrategy = getPersistenceStrategy(persistence) - if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { - // User's home and work locations - const home = getItem('home') - const work = getItem('work') - // Whether recent searches and places should be tracked in local storage. - const trackRecent = getItem('trackRecent', false) - const expandAdvanced = getItem('expandAdvanced', false) - // Recent places used in trip plan searches. - const recentPlaces = getItem('recent', []) - // List of user's favorite stops. - const favoriteStops = getItem('favoriteStops', []) - // Recent trip plan searches (excluding time/date parameters to avoid complexity). - const recentSearches = getItem('recentSearches', []) - // Filter valid locations found into locations list. - const locations = [home, work].filter(p => p) - // Add configurable locations to home and work locations - if (configLocations) { - locations.push(...configLocations.map(l => ({ ...l, type: 'suggested' }))) - } + const { locations: configLocations = null } = config - return { - localUser: { - expandAdvanced, // localUser only - favoriteStops, // localUser only - recentPlaces, // localUser only - savedLocations: locations, - storeTripHistory: trackRecent - }, - localUserTripRequests: recentSearches - } + // User's home and work locations + const home = getItem('home') + const work = getItem('work') + // Whether recent searches and places should be tracked in local storage. + const trackRecent = getItem('trackRecent', false) + const expandAdvanced = getItem('expandAdvanced', false) + // Recent places used in trip plan searches. + const recentPlaces = getItem('recent', []) + // List of user's favorite stops. + const favoriteStops = getItem('favoriteStops', []) + // Recent trip plan searches (excluding time/date parameters to avoid complexity). + const recentSearches = getItem('recentSearches', []) + // Filter valid locations found into locations list. + const locations = [home, work].filter(p => p) + // Add configurable locations to home and work locations + if (configLocations) { + locations.push(...configLocations.map(l => ({ ...l, type: 'suggested' }))) } + // Whether to auto-refresh stop arrival times in the Stop Viewer. + const autoRefreshStopTimes = getItem('autoRefreshStopTimes', true) + // User overrides determine user's default mode/query parameters. + const userOverrides = getItem('defaultQuery', {}) + // Combine user overrides with default query to get default search settings. + const defaults = Object.assign(getDefaultQuery(config), userOverrides) + return { - localUser: null, - localUserTripRequests: null + localUser: { + autoRefreshStopTimes, + // Do not store from/to or date/time in defaults + defaults: getTripOptionsFromQuery(defaults), + expandAdvanced, + favoriteStops, + recentPlaces, + savedLocations: locations, + storeTripHistory: trackRecent + }, + localUserTripRequests: recentSearches } } @@ -278,6 +271,26 @@ function createUserReducer (config) { } }) } + case 'TOGGLE_AUTO_REFRESH': + storeItem('autoRefreshStopTimes', action.payload) + return update(state, { localUser: { autoRefreshStopTimes: { $set: action.payload } } }) + + case 'CLEAR_DEFAULT_SETTINGS': + removeItem('defaultQuery') + return update(state, { localUser: { defaults: { $set: null } } }) + + case 'STORE_DEFAULT_SETTINGS': + storeItem('defaultQuery', action.payload) + return update(state, { localUser: { defaults: { $set: action.payload } } }) + + // FIXME: set up action + case 'TOGGLE_ADVANCED_OPTIONS': + storeItem('expandAdvanced', action.payload) + if (!action.payload) removeItem('expandAdvanced') + return update(state, { localUser: { + expandAdvanced: { $set: action.payload } + } }) + default: return state } diff --git a/lib/util/state.js b/lib/util/state.js index a3459b805..8b4af6ac8 100644 --- a/lib/util/state.js +++ b/lib/util/state.js @@ -377,7 +377,9 @@ const DEFAULT_TITLE = document.title export function getTitle (state) { // Override title can optionally be provided in config.yml - const { config, ui, user } = state.otp + const { config, ui } = state.otp + const { localUser, loggedInUser } = state.user + const user = loggedInUser || localUser let title = config.title || DEFAULT_TITLE const { mainPanelContent, viewedRoute, viewedStop } = ui switch (mainPanelContent) { @@ -392,7 +394,7 @@ export function getTitle (state) { default: const activeSearch = getActiveSearch(state.otp) if (activeSearch) { - title += ` | ${coreUtils.query.summarizeQuery(activeSearch.query, user.locations)}` + title += ` | ${coreUtils.query.summarizeQuery(activeSearch.query, user.savedLocations)}` } break } From 73c58c4fd65c95ae80d0a9ff4914042cf418b05e Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 26 Feb 2021 18:23:18 -0500 Subject: [PATCH 036/270] refactor(user reducer): Put recentSearches under localUser. --- lib/actions/api.js | 5 +++-- lib/actions/user.js | 4 ++-- lib/components/form/user-settings.js | 16 ++++++++-------- lib/reducers/create-user-reducer.js | 21 ++++++--------------- 4 files changed, 19 insertions(+), 27 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index 8951de93d..e9f0d7b22 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -111,8 +111,9 @@ export function routingQuery (searchId = null) { .then(json => { dispatch(routingResponse({ response: json, requestId, searchId })) // If tracking is enabled, store locations and search after successful - // search is completed. - if (userState.trueUser && userState.trueUser.storeTripHistory) { + // search is completed for the applicable user. + const trueUser = userState.loggedInUser || userState.localUser + if (trueUser.storeTripHistory) { const { from, to } = otpState.currentQuery if (!isStoredPlace(from)) { dispatch(rememberPlace({ type: 'recent', location: formatRecentPlace(from) })) diff --git a/lib/actions/user.js b/lib/actions/user.js index 4d776f9cc..ad0811624 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -167,7 +167,7 @@ export function createOrUpdateUser (userData, silentOnSuccess = false) { return async function (dispatch, getState) { const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(getState()) const { id } = userData // Middleware ID, NOT auth0 (or similar) id. - let requestUrl, method + let method, requestUrl // Before persisting, filter out entries from userData.savedLocations with blank addresses. userData.savedLocations = userData.savedLocations.filter( @@ -290,7 +290,7 @@ export function createOrUpdateUserMonitoredTrip ( return async function (dispatch, getState) { const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState()) const { id } = tripData - let requestUrl, method + let method, requestUrl // Determine URL and method to use. if (isNew) { diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 2fa8e8f59..7feee4799 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -38,10 +38,11 @@ const StyledSavedPlace = stylePlace(SavedPlace) class UserSettings extends Component { _disableTracking = () => { - const { localUser, localUserTripRequests, toggleTracking } = this.props - if (!localUser.storeTripHistory) return - const hasRecents = (localUser.recentPlaces && localUser.recentPlaces.length > 0) || - (localUserTripRequests && localUserTripRequests.length > 0) + const { localUser, toggleTracking } = this.props + const { recentPlaces, recentSearches, storeTripHistory } = localUser + if (!storeTripHistory) return + const hasRecents = (recentPlaces && recentPlaces.length > 0) || + (recentSearches && recentSearches.length > 0) // If user has recents and does not confirm deletion, return without doing // anything. @@ -299,12 +300,11 @@ const RecentTrips = ({ forgetSearch, setQueryParam, tripRequests = null, user }) const mapStateToProps = (state, ownProps) => { const { config, currentQuery, location, transitIndex } = state.otp const { language, persistence } = config - const { localUser, localUserTripRequests, loggedInUser, loggedInUserTripRequests } = state.user + const { localUser, loggedInUser, loggedInUserTripRequests } = state.user return { config, currentPosition: location.currentPosition, - localUser, // these users should be merged in mapStateToProps - localUserTripRequests, + localUser, //these users should be merged in mapStateToProps loggedInUser, loggedInUserTripRequests, nearbyStops: location.nearbyStops, @@ -313,7 +313,7 @@ const mapStateToProps = (state, ownProps) => { sessionSearches: location.sessionSearches, stopsIndex: transitIndex.stops, storageDisclaimer: language.storageDisclaimer, - tripRequests: loggedInUser ? loggedInUserTripRequests : localUserTripRequests, + tripRequests: loggedInUser ? loggedInUserTripRequests : localUser.recentSearches, trueUser: loggedInUser || localUser } } diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 5b271226b..59410c7a5 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -71,10 +71,10 @@ function loadUserFromLocalStorage (config) { expandAdvanced, favoriteStops, recentPlaces, + recentSearches, savedLocations: locations, storeTripHistory: trackRecent - }, - localUserTripRequests: recentSearches + } } } @@ -155,7 +155,6 @@ function createUserReducer (config) { } case 'DELETE_LOCAL_USER_RECENT_PLACE': - // This is used to delete the local user's recent location that matches the provided id. return removeLocalUserPlace(action.payload, state, 'recentPlaces', 'recent') case 'DELETE_LOCAL_USER_SAVED_PLACE': { @@ -227,19 +226,11 @@ function createUserReducer (config) { return update(state, { localUser: { favoriteStops: { $set: favoriteStops } } }) } - case 'FORGET_SEARCH': { - const recentSearches = clone(state.localUserTripRequests) - const index = recentSearches.findIndex(l => l.id === action.payload) - // Remove item from list of recent searches - recentSearches.splice(index, 1) - storeItem('recentSearches', recentSearches) - return index !== -1 - ? update(state, { localUserTripRequests: { $splice: [[index, 1]] } }) - : state - } + case 'FORGET_SEARCH': + return removeLocalUserPlace(action.payload, state, 'recentSearches', 'recentSearches') case 'REMEMBER_SEARCH': { - const searches = clone(state.localUserTripRequests) + const searches = clone(state.localUser.recentSearches) const duplicateIndex = searches.findIndex(s => isEqual(s.query, action.payload.query)) // Overwrite duplicate search (so that new timestamp is stored). if (duplicateIndex !== -1) searches[duplicateIndex] = action.payload @@ -250,7 +241,7 @@ function createUserReducer (config) { sortedSearches.splice(MAX_RECENT_STORAGE) } storeItem('recentSearches', sortedSearches) - return update(state, { localUserTripRequests: { $set: sortedSearches } }) + return update(state, { localUser: { recentSearches: { $set: sortedSearches } } }) } case 'TOGGLE_TRACKING': { From a16d39d6c61dbde748398879f7dc67dc84a4b6e9 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 26 Feb 2021 19:04:51 -0500 Subject: [PATCH 037/270] refactor(user reducer): Extract repetitive code. --- lib/reducers/create-user-reducer.js | 76 +++++++++++++++-------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 59410c7a5..3d6b6f96c 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -10,6 +10,38 @@ const { getItem, removeItem, storeItem } = coreUtils.storage const MAX_RECENT_STORAGE = 5 +/** + * Adds a place to the specified localUser state and optional persistence setting. + */ +function rememberLocalUserPlace (location, duplicateFinder, beforeSave, state, fieldName, settingName) { + let places = clone(state.localUser[fieldName]) + const duplicateIndex = places.findIndex(duplicateFinder) + // Replace recent place if duplicate found or add to beginning of list. + if (duplicateIndex !== -1) places.splice(duplicateIndex, 1, location) + else places.unshift(location) + + if (beforeSave) { + places = beforeSave(places) + } + if (settingName) { + storeItem(settingName, places) + } + return update(state, { localUser: { [fieldName]: { $set: places } } }) +} + +/** + * Sorts the given list and keeps the first MAX_RECENT_STORAGE items. + */ +function sortAndTrim (list) { + const sorted = list.sort((a, b) => b.timestamp - a.timestamp) + // Only keep up to 5 recent locations + // FIXME: Check for duplicates + if (list.length >= MAX_RECENT_STORAGE) { + sorted.splice(MAX_RECENT_STORAGE) + } + return sorted +} + /** * Removes a place by id from the specified localUser state and optional persistence setting. */ @@ -167,30 +199,11 @@ function createUserReducer (config) { case 'REMEMBER_LOCAL_USER_PLACE': { const { location, type } = action.payload switch (type) { - case 'recent': { - const recentPlaces = clone(state.localUser.recentPlaces) - const index = recentPlaces.findIndex(l => matchLatLon(l, location)) - // Replace recent place if duplicate found or add to list. - if (index !== -1) recentPlaces.splice(index, 1, location) - else recentPlaces.push(location) - const sortedPlaces = recentPlaces.sort((a, b) => b.timestamp - a.timestamp) - // Only keep up to 5 recent locations - // FIXME: Check for duplicates - if (recentPlaces.length >= MAX_RECENT_STORAGE) { - sortedPlaces.splice(MAX_RECENT_STORAGE) - } - storeItem('recent', recentPlaces) - return update(state, { localUser: { recentPlaces: { $set: sortedPlaces } } }) - } - default: { - const locations = clone(state.localUser.savedLocations) - // Determine if location type (e.g., home or work) already exists in list - const index = locations.findIndex(l => l.type === type) - if (index !== -1) locations.splice(index, 1, location) - else locations.push(location) + case 'recent': + return rememberLocalUserPlace(location, l => matchLatLon(l, location), sortAndTrim, state, 'recentPlaces', 'recent') + default: storeItem(type, location) - return update(state, { localUser: { savedLocations: { $set: locations } } }) - } + return rememberLocalUserPlace(location, l => l.type === type, null, state, 'savedLocations') } } @@ -229,20 +242,9 @@ function createUserReducer (config) { case 'FORGET_SEARCH': return removeLocalUserPlace(action.payload, state, 'recentSearches', 'recentSearches') - case 'REMEMBER_SEARCH': { - const searches = clone(state.localUser.recentSearches) - const duplicateIndex = searches.findIndex(s => isEqual(s.query, action.payload.query)) - // Overwrite duplicate search (so that new timestamp is stored). - if (duplicateIndex !== -1) searches[duplicateIndex] = action.payload - else searches.unshift(action.payload) - const sortedSearches = searches.sort((a, b) => b.timestamp - a.timestamp) - // Ensure recent searches do not extend beyond MAX_RECENT_STORAGE - if (sortedSearches.length >= MAX_RECENT_STORAGE) { - sortedSearches.splice(MAX_RECENT_STORAGE) - } - storeItem('recentSearches', sortedSearches) - return update(state, { localUser: { recentSearches: { $set: sortedSearches } } }) - } + case 'REMEMBER_SEARCH': + return rememberLocalUserPlace(location, s => isEqual(s.query, action.payload.query), + sortAndTrim, state, 'recentSearches', 'recentSearches') case 'TOGGLE_TRACKING': { storeItem('trackRecent', action.payload) From 5bcef61d19c5d181a9cfbf31cc5035d3e3641896 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 26 Feb 2021 19:56:50 -0500 Subject: [PATCH 038/270] refactor(user reducer): Fix errors --- lib/components/user/places/cached-place.js | 2 +- lib/reducers/create-user-reducer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/components/user/places/cached-place.js b/lib/components/user/places/cached-place.js index c9312727a..20c96de03 100644 --- a/lib/components/user/places/cached-place.js +++ b/lib/components/user/places/cached-place.js @@ -28,7 +28,7 @@ class CachedPlace extends Component { icon={icon} name={formatStoredPlaceName(place, false)} onClick={onClick} - onDelete={canDelete && onDelete && this._onDelete} + onDelete={(canDelete && onDelete) ? this._onDelete : null} onView={onView && this._onView} title={formatStoredPlaceName(place)} /> diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 3d6b6f96c..a6b203712 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -243,7 +243,7 @@ function createUserReducer (config) { return removeLocalUserPlace(action.payload, state, 'recentSearches', 'recentSearches') case 'REMEMBER_SEARCH': - return rememberLocalUserPlace(location, s => isEqual(s.query, action.payload.query), + return rememberLocalUserPlace(action.payload, s => isEqual(s.query, action.payload.query), sortAndTrim, state, 'recentSearches', 'recentSearches') case 'TOGGLE_TRACKING': { From 14aaf28c5da6914d3a41a687b676d712a7c06e77 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 1 Mar 2021 10:28:49 -0500 Subject: [PATCH 039/270] test(snapshots): Update snapshots --- .../viewers/__snapshots__/stop-viewer.js.snap | 22 +++++--- .../__snapshots__/create-otp-reducer.js.snap | 34 ------------- .../__snapshots__/create-user-reducer.js.snap | 51 +++++++++++++++++++ __tests__/reducers/create-user-reducer.js | 11 ++++ lib/components/form/user-settings.js | 2 +- 5 files changed, 77 insertions(+), 43 deletions(-) create mode 100644 __tests__/reducers/__snapshots__/create-user-reducer.js.snap create mode 100644 __tests__/reducers/create-user-reducer.js diff --git a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap index 1de873e4f..2d565c5d6 100644 --- a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap +++ b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap @@ -16,6 +16,7 @@ exports[`components > viewers > stop viewer should render countdown times after viewers > stop viewer should render countdown times after >
    viewers > stop viewer should render countdown times for st viewers > stop viewer should render countdown times for st >
    viewers > stop viewer should render times after midnight w 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 OTP transit index >
    viewers > stop viewer should render with TriMet transit in viewers > stop viewer should render with TriMet transit in >
    viewers > stop viewer should render with initial stop id a reducers > create-user-reducer should be able to create the initial state 1`] = ` +Object { + "accessToken": null, + "itineraryExistence": null, + "lastPhoneSmsRequest": Object { + "number": null, + "status": null, + "timestamp": 1970-01-01T00:00:00.000Z, + }, + "localUser": Object { + "autoRefreshStopTimes": true, + "defaults": Object { + "bannedRoutes": "", + "bikeSpeed": 3.58, + "companies": null, + "endTime": "09:00", + "ignoreRealtimeUpdates": false, + "intermediatePlaces": Array [], + "maxBikeDistance": 4828, + "maxBikeTime": 20, + "maxEScooterDistance": 4828, + "maxWalkDistance": 1207, + "maxWalkTime": 15, + "mode": "WALK,TRANSIT", + "numItineraries": 3, + "optimize": "QUICK", + "optimizeBike": "SAFE", + "otherThanPreferredRoutesPenalty": 900, + "preferredRoutes": "", + "routingType": "ITINERARY", + "showIntermediateStops": true, + "startTime": "07:00", + "walkSpeed": 1.34, + "watts": 250, + "wheelchair": false, + }, + "expandAdvanced": false, + "favoriteStops": Array [], + "recentPlaces": Array [], + "recentSearches": Array [], + "savedLocations": Array [], + "storeTripHistory": false, + }, + "loggedInUser": null, + "loggedInUserMonitoredTrips": null, + "loggedInUserTripRequests": null, + "pathBeforeSignIn": null, +} +`; diff --git a/__tests__/reducers/create-user-reducer.js b/__tests__/reducers/create-user-reducer.js new file mode 100644 index 000000000..a55594ec2 --- /dev/null +++ b/__tests__/reducers/create-user-reducer.js @@ -0,0 +1,11 @@ +import {getUserInitialState} from '../../lib/reducers/create-user-reducer' +import {restoreDateNowBehavior, setDefaultTestTime} from '../test-utils' + +describe('lib > reducers > create-user-reducer', () => { + afterEach(restoreDateNowBehavior) + + it('should be able to create the initial state', () => { + setDefaultTestTime() + expect(getUserInitialState({}, {})).toMatchSnapshot() + }) +}) diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 7feee4799..67d4bcc11 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -304,7 +304,7 @@ const mapStateToProps = (state, ownProps) => { return { config, currentPosition: location.currentPosition, - localUser, //these users should be merged in mapStateToProps + localUser, // these users should be merged in mapStateToProps loggedInUser, loggedInUserTripRequests, nearbyStops: location.nearbyStops, From 4518083751dc77732368ad5d8810ae881258ca6a Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 1 Mar 2021 11:24:42 -0500 Subject: [PATCH 040/270] refactor(UserSettings): Remove redundant vars, tweak comments. --- lib/actions/api.js | 8 ++--- lib/components/form/user-settings.js | 54 +++++++++++----------------- lib/reducers/create-user-reducer.js | 6 ++-- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index e9f0d7b22..6de2b953f 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -112,8 +112,8 @@ export function routingQuery (searchId = null) { dispatch(routingResponse({ response: json, requestId, searchId })) // If tracking is enabled, store locations and search after successful // search is completed for the applicable user. - const trueUser = userState.loggedInUser || userState.localUser - if (trueUser.storeTripHistory) { + const currentUser = userState.loggedInUser || userState.localUser + if (currentUser.storeTripHistory) { const { from, to } = otpState.currentQuery if (!isStoredPlace(from)) { dispatch(rememberPlace({ type: 'recent', location: formatRecentPlace(from) })) @@ -161,11 +161,11 @@ export function routingQuery (searchId = null) { // (state.user stays unpopulated until after this function is called). // const { user } = state - const storeTripHistory = user && + const loggedInUserStoreTripHistory = user && user.loggedInUser && user.loggedInUser.storeTripHistory - fetch(constructRoutingQuery(otpState, true), getOtpFetchOptions(state, storeTripHistory && i === 0)) + fetch(constructRoutingQuery(otpState, true), getOtpFetchOptions(state, loggedInUserStoreTripHistory && i === 0)) .then(getJsonAndCheckResponse) .then(json => { // FIXME: This is only performed when ignoring realtimeupdates currently, just diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 67d4bcc11..7fb8b5f48 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -15,8 +15,8 @@ import SavedPlace from '../user/places/saved-place' import PlaceShortcut from '../user/places/place-shortcut' import { MainPanelPlace } from '../user/places/styled' import { PERSIST_TO_LOCAL_STORAGE, PERSIST_TO_OTP_MIDDLEWARE } from '../../util/constants' -import { isBlank } from '../../util/ui' import { getPersistenceStrategy, isHome, isWork } from '../../util/user' + import { UnpaddedList } from './styled' const { summarizeQuery } = coreUtils.query @@ -89,47 +89,36 @@ class UserSettings extends Component { forgetSearch, localUser, loggedInUser, + loggedInUserTripRequests, persistenceStrategy, setQueryParam, setViewedStop, - storageDisclaimer, - tripRequests, - trueUser + storageDisclaimer } = this.props if (loggedInUser && persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE) { - // Add id attribute using index, that will be used to call deleteLoggedInUserPlace. - const loggedInUserLocations = loggedInUser.savedLocations.map((loc, index) => ({ - ...loc, - blank: isBlank(loc.address), - id: index - })) - const savedPlacesHeader = ( - <> - My saved places ( - manage - ) - - ) - return (
    + My saved places ( + manage + ) + )} PlaceComponent={StyledSavedPlace} - places={loggedInUserLocations} + places={loggedInUser.savedLocations} separator={false} />
    ) } else if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { - const { favoriteStops, storeTripHistory, recentPlaces } = localUser + const { favoriteStops, storeTripHistory, recentPlaces, recentSearches } = localUser // Clone locations in order to prevent blank locations from seeping into the // app state/store. const locations = this._getLocations(localUser) @@ -162,8 +151,8 @@ class UserSettings extends Component {
    @@ -273,7 +262,7 @@ class TripRequest extends Component { */ const RecentTrips = ({ forgetSearch, setQueryParam, tripRequests = null, user }) => ( // Note: tripRequests can be undefined, - // so we have to coerce it to null to make a valid render. + // so we have to coerce it to null above to make a valid render. user.storeTripHistory && tripRequests && tripRequests.length > 0 && (

    @@ -300,21 +289,20 @@ const RecentTrips = ({ forgetSearch, setQueryParam, tripRequests = null, user }) const mapStateToProps = (state, ownProps) => { const { config, currentQuery, location, transitIndex } = state.otp const { language, persistence } = config + const { currentPosition, nearbyStops, sessionSearches } = location const { localUser, loggedInUser, loggedInUserTripRequests } = state.user return { config, - currentPosition: location.currentPosition, - localUser, // these users should be merged in mapStateToProps + currentPosition, + localUser, loggedInUser, loggedInUserTripRequests, - nearbyStops: location.nearbyStops, + nearbyStops, persistenceStrategy: getPersistenceStrategy(persistence), query: currentQuery, - sessionSearches: location.sessionSearches, + sessionSearches, stopsIndex: transitIndex.stops, - storageDisclaimer: language.storageDisclaimer, - tripRequests: loggedInUser ? loggedInUserTripRequests : localUser.recentSearches, - trueUser: loggedInUser || localUser + storageDisclaimer: language.storageDisclaimer } } diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index a6b203712..5bfe85fa6 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -30,7 +30,8 @@ function rememberLocalUserPlace (location, duplicateFinder, beforeSave, state, f } /** - * Sorts the given list and keeps the first MAX_RECENT_STORAGE items. + * Sorts the given list most recent first, + * and keeps the first MAX_RECENT_STORAGE most recent items. */ function sortAndTrim (list) { const sorted = list.sort((a, b) => b.timestamp - a.timestamp) @@ -48,7 +49,8 @@ function sortAndTrim (list) { function removeLocalUserPlace (id, state, fieldName, settingName) { const originalArray = state.localUser[fieldName] const removeIndex = originalArray.findIndex(l => l.id === id) - // If a persistence setting is provided, create a new array without the specified element. + // If a persistence setting is provided, + // persist a copy of the passed array without the specified element. if (settingName) { const newArray = clone(originalArray) newArray.splice(removeIndex, 1) From 5371aa6e13d282544373542fe0411ba22390b2eb Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Tue, 24 Aug 2021 10:48:58 -0400 Subject: [PATCH 041/270] refactor: remove expandAdvanced stored setting --- lib/components/app/call-taker-panel.js | 4 ---- lib/components/form/batch-settings.js | 1 - lib/reducers/create-user-reducer.js | 12 +----------- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index 49c9318f2..df1650b48 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -1,5 +1,4 @@ import { getTimeFormat } from '@opentripplanner/core-utils/lib/time' -import { storeItem } from '@opentripplanner/core-utils/lib/storage' import React, { Component } from 'react' import { Button } from 'react-bootstrap' import { connect } from 'react-redux' @@ -72,8 +71,6 @@ class CallTakerPanel extends Component { _onHideAdvancedClick = () => { const expandAdvanced = !this.state.expandAdvanced - // FIXME move logic to action - storeItem('expandAdvanced', expandAdvanced) this.setState({expandAdvanced}) } @@ -279,7 +276,6 @@ const mapStateToProps = (state, ownProps) => { return { activeSearch: getActiveSearch(state), currentQuery: state.otp.currentQuery, - expandAdvanced: state.otp.user.expandAdvanced, groupSize: state.callTaker.fieldTrip.groupSize, mainPanelContent: state.otp.ui.mainPanelContent, maxGroupSize: getGroupSize(request), diff --git a/lib/components/form/batch-settings.js b/lib/components/form/batch-settings.js index 6e22168cc..5ba04aeb3 100644 --- a/lib/components/form/batch-settings.js +++ b/lib/components/form/batch-settings.js @@ -189,7 +189,6 @@ const mapStateToProps = (state, ownProps) => { activeSearch: getActiveSearch(state), config: state.otp.config, currentQuery: state.otp.currentQuery, - expandAdvanced: state.otp.user.expandAdvanced, possibleCombinations: state.otp.config.modes.combinations, showUserSettings } diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 5bfe85fa6..14f2f84d6 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -76,7 +76,6 @@ function loadUserFromLocalStorage (config) { const work = getItem('work') // Whether recent searches and places should be tracked in local storage. const trackRecent = getItem('trackRecent', false) - const expandAdvanced = getItem('expandAdvanced', false) // Recent places used in trip plan searches. const recentPlaces = getItem('recent', []) // List of user's favorite stops. @@ -102,7 +101,6 @@ function loadUserFromLocalStorage (config) { autoRefreshStopTimes, // Do not store from/to or date/time in defaults defaults: getTripOptionsFromQuery(defaults), - expandAdvanced, favoriteStops, recentPlaces, recentSearches, @@ -215,7 +213,7 @@ function createUserReducer (config) { case 'REMEMBER_STOP': { // Payload is stop data. We want to avoid saving other attributes that // might be contained there (like lists of patterns). - const { id, name, lat, lon } = action.payload + const { id, lat, lon, name } = action.payload const stop = { icon: 'bus', id, @@ -278,14 +276,6 @@ function createUserReducer (config) { storeItem('defaultQuery', action.payload) return update(state, { localUser: { defaults: { $set: action.payload } } }) - // FIXME: set up action - case 'TOGGLE_ADVANCED_OPTIONS': - storeItem('expandAdvanced', action.payload) - if (!action.payload) removeItem('expandAdvanced') - return update(state, { localUser: { - expandAdvanced: { $set: action.payload } - } }) - default: return state } From 509d5163174256fd19c93cc80050acb1ce952551 Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Tue, 24 Aug 2021 11:27:41 -0400 Subject: [PATCH 042/270] refactor(places): simplify component wrappers --- lib/components/app/batch-routing-panel.js | 9 ++-- lib/components/form/user-settings.js | 23 +------- lib/components/user/places/cached-place.js | 39 -------------- .../user/places/favorite-place-list.js | 5 +- lib/components/user/places/place.js | 16 ++++-- lib/components/user/places/saved-place.js | 52 ------------------- 6 files changed, 19 insertions(+), 125 deletions(-) delete mode 100644 lib/components/user/places/cached-place.js delete mode 100644 lib/components/user/places/saved-place.js diff --git a/lib/components/app/batch-routing-panel.js b/lib/components/app/batch-routing-panel.js index 70006cee1..a730c84d9 100644 --- a/lib/components/app/batch-routing-panel.js +++ b/lib/components/app/batch-routing-panel.js @@ -6,6 +6,7 @@ import * as apiActions from '../../actions/api' import * as formActions from '../../actions/form' import BatchSettings from '../form/batch-settings' import LocationField from '../form/connected-location-field' +import UserSettings from '../form/user-settings' import NarrativeItineraries from '../narrative/narrative-itineraries' import { getActiveSearch, getShowUserSettings } from '../../util/state' import ViewerContainer from '../viewers/viewer-container' @@ -33,7 +34,7 @@ const NarrativeContainer = styled.div` */ class BatchRoutingPanel extends Component { render () { - const {mobile} = this.props + const {activeSearch, mobile, showUserSettings} = this.props const actionText = mobile ? 'tap' : 'click' return ( @@ -53,11 +54,9 @@ class BatchRoutingPanel extends Component {
    - {/* FIXME: Add back user settings (home, work, etc.) once connected to - the middleware persistence. - !activeSearch && showUserSettings && + {!activeSearch && showUserSettings && - */} + } ( - - ) -} - -const StyledCachedPlace = stylePlace(CachedPlace) -const StyledSavedPlace = stylePlace(SavedPlace) - class UserSettings extends Component { _disableTracking = () => { const { localUser, toggleTracking } = this.props @@ -105,7 +88,6 @@ class UserSettings extends Component { manage ) )} - PlaceComponent={StyledSavedPlace} places={loggedInUser.savedLocations} separator={false} /> @@ -118,7 +100,7 @@ class UserSettings extends Component {
    ) } else if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { - const { favoriteStops, storeTripHistory, recentPlaces, recentSearches } = localUser + const { favoriteStops, recentPlaces, recentSearches, storeTripHistory } = localUser // Clone locations in order to prevent blank locations from seeping into the // app state/store. const locations = this._getLocations(localUser) @@ -193,7 +175,6 @@ const Places = ({ header, onDelete, onView, - PlaceComponent = StyledCachedPlace, places, separator = true, textIfEmpty @@ -213,7 +194,7 @@ const Places = ({ onDelete={onDelete} onView={onView} place={location} - PlaceComponent={PlaceComponent} + PlaceComponent={MainPanelPlace} /> ) }) diff --git a/lib/components/user/places/cached-place.js b/lib/components/user/places/cached-place.js deleted file mode 100644 index 20c96de03..000000000 --- a/lib/components/user/places/cached-place.js +++ /dev/null @@ -1,39 +0,0 @@ -import coreUtils from '@opentripplanner/core-utils' -import React, { Component } from 'react' - -const { formatStoredPlaceName, getDetailText } = coreUtils.map - -/** - * Wrapper for the Place component for places cached in localStorage. - */ -class CachedPlace extends Component { - _onView = () => { - const { onView, place } = this.props - onView({ stopId: place.id }) - } - - _onDelete = () => { - const { onDelete, place } = this.props - onDelete(place.id) - } - - render () { - const { onClick, onDelete, onView, place, PlaceComponent } = this.props - const { blank, icon, type } = place - const canDelete = !blank && ['stop', 'home', 'work', 'recent'].indexOf(type) !== -1 - - return ( - - ) - } -} - -export default CachedPlace diff --git a/lib/components/user/places/favorite-place-list.js b/lib/components/user/places/favorite-place-list.js index 2f8f9f34b..5c6088381 100644 --- a/lib/components/user/places/favorite-place-list.js +++ b/lib/components/user/places/favorite-place-list.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux' import { UnpaddedList } from '../../form/styled' import { CREATE_ACCOUNT_PLACES_PATH } from '../../../util/constants' import { getPlaceBasePath } from '../../../util/user' -import SavedPlace from './saved-place' + import { FavoritePlace } from './styled' /** @@ -17,12 +17,11 @@ const FavoritePlaceList = ({ isCreating, loggedInUser }) => ( Add the places you frequent often to save time planning trips: {loggedInUser.savedLocations.map((place, index) => ( - ))} diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index e00804dd7..462c95c5f 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -5,6 +5,7 @@ import styled from 'styled-components' import { LinkContainerWithQuery } from '../../form/connected-links' import NarrativeIcon from '../../narrative/icon' +import { getPlaceBasePath } from '../../../util/user' const Container = styled.li` align-items: stretch; @@ -37,26 +38,31 @@ export const ActionButtonPlaceholder = styled.span`` const VIEW_LABEL = 'View stop' // used for stops only. const DELETE_LABEL = 'Delete place' +const EDIT_LABEL = 'Edit this place' /** * Renders a stylable clickable button for editing/selecting a user's favorite place, * and buttons for viewing and deleting the place if corresponding handlers are provided. */ const Place = ({ + place, ariaLabel, buttonStyle, className, - details, - icon, + name = place?.name || place?.address, + details = place?.address || `Set your ${place?.type || 'other'} address`, + icon = place?.icon, + index, + isCreating, largeIcon, - name, onClick, onDelete, onView, - path, + path = `${getPlaceBasePath(isCreating)}/${index}`, placeDetailClassName, placeTextClassName, - title + title = place?.address && onClick + ? `${name}${details && ` (${details})`}` : EDIT_LABEL }) => { const to = onClick ? null : path const iconSize = largeIcon && '2x' diff --git a/lib/components/user/places/saved-place.js b/lib/components/user/places/saved-place.js deleted file mode 100644 index 64b558386..000000000 --- a/lib/components/user/places/saved-place.js +++ /dev/null @@ -1,52 +0,0 @@ -import React, { Component } from 'react' -import { connect } from 'react-redux' - -import * as userActions from '../../../actions/user' -import { getPlaceBasePath, isHomeOrWork } from '../../../util/user' - -const EDIT_LABEL = 'Edit this place' - -/** - * Wrapper for the Place component for saved places in OTP Middleware. - */ -class SavedPlace extends Component { - _handleDelete = () => { - const { deleteLoggedInUserPlace, index } = this.props - deleteLoggedInUserPlace(index) - } - - render () { - const { isCreating, index, onClick, place, PlaceComponent } = this.props - const { address, icon, name, type } = place - const canDelete = !isHomeOrWork(place) || address - const placeName = name || address - const details = name && (address || `Set your ${type} address`) - const useOnClick = address && onClick - const title = useOnClick ? `${placeName}${details && ` (${details})`}` : EDIT_LABEL - - return ( - - ) - } -} - -// connect to redux store - -const mapStateToProps = (state, ownProps) => { - return { - } -} - -const mapDispatchToProps = { - deleteLoggedInUserPlace: userActions.deleteLoggedInUserPlace -} - -export default connect(mapStateToProps, mapDispatchToProps)(SavedPlace) From 21d4699ad8ac2d9f44d338dd97eef9c6dead4dad Mon Sep 17 00:00:00 2001 From: Landon Reed Date: Fri, 10 Sep 2021 09:46:34 -0400 Subject: [PATCH 043/270] refactor: more tweaks --- .../__snapshots__/create-user-reducer.js.snap | 2 - lib/actions/user.js | 4 +- lib/components/form/user-settings.js | 188 +++++++++--------- lib/components/user/places/place.js | 2 +- lib/reducers/create-user-reducer.js | 4 - 5 files changed, 94 insertions(+), 106 deletions(-) diff --git a/__tests__/reducers/__snapshots__/create-user-reducer.js.snap b/__tests__/reducers/__snapshots__/create-user-reducer.js.snap index a4c2662c3..3aa763bd5 100644 --- a/__tests__/reducers/__snapshots__/create-user-reducer.js.snap +++ b/__tests__/reducers/__snapshots__/create-user-reducer.js.snap @@ -10,7 +10,6 @@ Object { "timestamp": 1970-01-01T00:00:00.000Z, }, "localUser": Object { - "autoRefreshStopTimes": true, "defaults": Object { "bikeSpeed": 3.58, "endTime": "09:00", @@ -33,7 +32,6 @@ Object { "watts": 250, "wheelchair": false, }, - "expandAdvanced": false, "favoriteStops": Array [], "recentPlaces": Array [], "recentSearches": Array [], diff --git a/lib/actions/user.js b/lib/actions/user.js index c63de083f..30a01a520 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -39,8 +39,8 @@ const setitineraryExistence = createAction('SET_ITINERARY_EXISTENCE') // localStorage user actions export const deleteLocalUserRecentPlace = createAction('DELETE_LOCAL_USER_RECENT_PLACE') -export const deleteLocalUserSavedPlace = createAction('DELETE_LOCAL_USER_SAVED_PLACE') -export const deleteLocalUserStop = createAction('FORGET_STOP') +const deleteLocalUserSavedPlace = createAction('DELETE_LOCAL_USER_SAVED_PLACE') +export const deleteFavoriteStop = createAction('FORGET_STOP') const rememberLocalUserPlace = createAction('REMEMBER_LOCAL_USER_PLACE') function createNewUser (auth0User) { diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index ba04f5dc2..e746d54ac 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -12,7 +12,7 @@ import * as userActions from '../../actions/user' import { LinkWithQuery } from '../form/connected-links' import PlaceShortcut from '../user/places/place-shortcut' import { MainPanelPlace } from '../user/places/styled' -import { PERSIST_TO_LOCAL_STORAGE, PERSIST_TO_OTP_MIDDLEWARE } from '../../util/constants' +import { PERSIST_TO_OTP_MIDDLEWARE } from '../../util/constants' import { getPersistenceStrategy, isHome, isWork } from '../../util/user' import { UnpaddedList } from './styled' @@ -40,6 +40,67 @@ class UserSettings extends Component { _enableTracking = () => !this.props.localUser.storeTripHistory && this.props.toggleTracking(true) + _getLocalStorageOnlyContent = () => { + const { + deleteFavoriteStop, + deleteLocalUserRecentPlace, + forgetSearch, + localUser, + setQueryParam, + setViewedStop, + storageDisclaimer + } = this.props + + const { favoriteStops, recentPlaces, recentSearches, storeTripHistory } = localUser + return ( + <> + {/* Favorite stops are shown regardless of tracking. */} + + + {storeTripHistory && } + +
    +
    +
    My preferences
    + Remember recent searches/places? + + +
    + {storageDisclaimer && +
    +
    +
    + {storageDisclaimer} +
    +
    + } + + ) + } + _getLocations = user => { const locations = [...user.savedLocations] if (!locations.find(isWork)) { @@ -47,7 +108,7 @@ class UserSettings extends Component { blank: true, icon: 'briefcase', id: 'work', - name: 'click to add', + name: 'Click to add', type: 'work' }) } @@ -56,7 +117,7 @@ class UserSettings extends Component { blank: true, icon: 'home', id: 'home', - name: 'click to add', + name: 'Click to add', type: 'home' }) } @@ -66,105 +127,38 @@ class UserSettings extends Component { render () { const { className, - deleteLocalUserRecentPlace, - deleteLocalUserSavedPlace, - deleteLocalUserStop, - forgetSearch, + deleteUserPlace, localUser, loggedInUser, - loggedInUserTripRequests, - persistenceStrategy, - setQueryParam, - setViewedStop, - storageDisclaimer + persistenceStrategy } = this.props - - if (loggedInUser && persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE) { - return ( -
    - + // Clone locations in order to prevent blank locations from seeping into the + // app state/store. + const locations = this._getLocations(localUser) + const order = ['home', 'work', 'suggested', 'stop', 'recent'] + const sortedLocations = locations + .sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)) + const isUsingOtpMiddleware = persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE + const userNotLoggedIn = isUsingOtpMiddleware && !loggedInUser + if (userNotLoggedIn) return null + return ( +
    + {/* Sorted locations are shown regardless of tracking. */} + My saved places ( manage ) - )} - places={loggedInUser.savedLocations} - separator={false} - /> - -
    - ) - } else if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { - const { favoriteStops, recentPlaces, recentSearches, storeTripHistory } = localUser - // Clone locations in order to prevent blank locations from seeping into the - // app state/store. - const locations = this._getLocations(localUser) - const order = ['home', 'work', 'suggested', 'stop', 'recent'] - const sortedLocations = locations - .sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)) - return ( -
    - {/* Sorted locations are shown regardless of tracking. */} - - - {/* Favorite stops are shown regardless of tracking. */} - - - {storeTripHistory && } - - -
    -
    -
    My preferences
    - Remember recent searches/places? - - -
    - {storageDisclaimer && -
    -
    -
    - {storageDisclaimer} -
    -
    + } -
    - ) - } - - return null + onDelete={deleteUserPlace} + places={sortedLocations} + separator={false} + /> + {!isUsingOtpMiddleware && this._getLocalStorageOnlyContent()} +
    + ) } } @@ -288,10 +282,10 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { + deleteFavoriteStop: userActions.deleteFavoriteStop, deleteLocalUserRecentPlace: userActions.deleteLocalUserRecentPlace, - deleteLocalUserSavedPlace: userActions.deleteLocalUserSavedPlace, - deleteLocalUserStop: userActions.deleteLocalUserStop, deleteLoggedInUserPlace: userActions.deleteLoggedInUserPlace, + deleteUserPlace: userActions.deleteUserPlace, forgetSearch: apiActions.forgetSearch, setLocation: mapActions.setLocation, setQueryParam: formActions.setQueryParam, diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 462c95c5f..331071052 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -100,7 +100,7 @@ const Place = ({ onView({ stopId: place.id })} title={VIEW_LABEL} > diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 14f2f84d6..4dd1b78b5 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -88,9 +88,6 @@ function loadUserFromLocalStorage (config) { if (configLocations) { locations.push(...configLocations.map(l => ({ ...l, type: 'suggested' }))) } - - // Whether to auto-refresh stop arrival times in the Stop Viewer. - const autoRefreshStopTimes = getItem('autoRefreshStopTimes', true) // User overrides determine user's default mode/query parameters. const userOverrides = getItem('defaultQuery', {}) // Combine user overrides with default query to get default search settings. @@ -98,7 +95,6 @@ function loadUserFromLocalStorage (config) { return { localUser: { - autoRefreshStopTimes, // Do not store from/to or date/time in defaults defaults: getTripOptionsFromQuery(defaults), favoriteStops, From 5202871f7f28e52de6ab7be467f411d97b2df005 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 6 Oct 2021 19:35:16 -0400 Subject: [PATCH 044/270] refactor(user/place): Format text content and restore broken functionality for localStorage. --- lib/components/form/user-settings.js | 31 +++--- .../user/places/favorite-place-row.js | 9 +- lib/components/user/places/place-shortcut.js | 28 ++++-- lib/components/user/places/place.js | 96 +++++++------------ lib/components/user/places/styled.js | 12 +-- 5 files changed, 83 insertions(+), 93 deletions(-) diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index e746d54ac..e587f37ea 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -11,7 +11,7 @@ import * as uiActions from '../../actions/ui' import * as userActions from '../../actions/user' import { LinkWithQuery } from '../form/connected-links' import PlaceShortcut from '../user/places/place-shortcut' -import { MainPanelPlace } from '../user/places/styled' +import { StyledMainPanelPlace } from '../user/places/styled' import { PERSIST_TO_OTP_MIDDLEWARE } from '../../util/constants' import { getPersistenceStrategy, isHome, isWork } from '../../util/user' @@ -56,6 +56,8 @@ class UserSettings extends Component { <> {/* Favorite stops are shown regardless of tracking. */} `Stop ID: ${location.id}`} + getMainText={location => location.address || location.name} header='Favorite stops' onDelete={deleteFavoriteStop} onView={setViewedStop} @@ -64,12 +66,16 @@ class UserSettings extends Component { /> {storeTripHistory && ''} + getMainText={location => location.address || location.name} header='Recent places' onDelete={deleteLocalUserRecentPlace} places={recentPlaces} />} `Stop ID: ${location.id}`} + getMainText={trip => location.address || location.name} setQueryParam={setQueryParam} tripRequests={recentSearches} user={localUser} @@ -108,7 +114,6 @@ class UserSettings extends Component { blank: true, icon: 'briefcase', id: 'work', - name: 'Click to add', type: 'work' }) } @@ -117,7 +122,6 @@ class UserSettings extends Component { blank: true, icon: 'home', id: 'home', - name: 'Click to add', type: 'home' }) } @@ -145,6 +149,8 @@ class UserSettings extends Component {
    {/* Sorted locations are shown regardless of tracking. */} location.address || location.name || 'Click to add'} + getMainText={location => location.type} header={isUsingOtpMiddleware && <> My saved places ( @@ -166,6 +172,8 @@ class UserSettings extends Component { * Displays a list of places with a header. */ const Places = ({ + getDetailText = location => location.name, + getMainText = location => location.type, header, onDelete, onView, @@ -183,12 +191,13 @@ const Places = ({ ? places.map((location, index) => { return ( ) }) @@ -200,7 +209,7 @@ const Places = ({ } /** - * Wraps MainPanelPlace for recent trip requests. + * Wrapper for recent trip requests. */ class TripRequest extends Component { _onSelect = () => { @@ -215,17 +224,17 @@ class TripRequest extends Component { render () { const { tripRequest, user } = this.props const { canDelete = true, query, timestamp } = tripRequest - const name = summarizeQuery(query, user.savedLocations) + const mainText = summarizeQuery(query, user.savedLocations) const timeInfo = moment(timestamp).fromNow() return ( - ) } diff --git a/lib/components/user/places/favorite-place-row.js b/lib/components/user/places/favorite-place-row.js index dea65aebe..c6bec12c3 100644 --- a/lib/components/user/places/favorite-place-row.js +++ b/lib/components/user/places/favorite-place-row.js @@ -30,6 +30,7 @@ const GreyIcon = styled(Icon)` const PlaceContent = styled.span` display: inline-block; + text-transform: capitalize; & * { color: #888; @@ -63,9 +64,9 @@ const MESSAGES = { * Renders a clickable button for editing a user's favorite place, * and lets the user delete the place. */ -const FavoritePlaceRow = ({ isFixed, onDelete, path, place }) => { +const FavoritePlaceRow = ({ detailText, icon, isFixed, mainText, onDelete, path, place }) => { if (place) { - const { address, icon, name, type } = place + const { address, icon } = place return ( @@ -75,8 +76,8 @@ const FavoritePlaceRow = ({ isFixed, onDelete, path, place }) => { > - {name && {name}} - {address || `Set your ${type} address`} + {mainText} + {detailText} diff --git a/lib/components/user/places/place-shortcut.js b/lib/components/user/places/place-shortcut.js index c16a89733..e8087ba43 100644 --- a/lib/components/user/places/place-shortcut.js +++ b/lib/components/user/places/place-shortcut.js @@ -5,6 +5,8 @@ import { connect } from 'react-redux' import * as mapActions from '../../../actions/map' import { convertToLocation } from '../../../util/user' +import { StyledMainPanelPlace } from './styled' + const { matchLatLon } = coreUtils.map /** @@ -45,15 +47,29 @@ class PlaceShortcut extends Component { } } + _onView = () => { + const { onView, place } = this.props + onView({ stopId: place.id }) + } + + _onDelete = () => { + const { onDelete, place } = this.props + onDelete(place.id) + } + render () { - const { index, onDelete, onView, place, PlaceComponent } = this.props + const { detailText, icon, mainText, onView, place, title } = this.props return ( - ) } diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 331071052..223eb014c 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -3,9 +3,9 @@ import React from 'react' import { Button } from 'react-bootstrap' import styled from 'styled-components' -import { LinkContainerWithQuery } from '../../form/connected-links' +// import { LinkContainerWithQuery } from '../../form/connected-links' import NarrativeIcon from '../../narrative/icon' -import { getPlaceBasePath } from '../../../util/user' +// import { getPlaceBasePath } from '../../../util/user' const Container = styled.li` align-items: stretch; @@ -13,6 +13,7 @@ const Container = styled.li` ` export const PlaceButton = styled(Button)` background: none; + border: none; flex: 1 0 0; overflow: hidden; text-align: left; @@ -25,95 +26,78 @@ export const PlaceDetail = styled.span`` export const PlaceContent = styled.span`` -export const PlaceName = styled.span`` +export const PlaceName = styled.span` + text-transform: capitalize; +` -export const PlaceText = styled.span`` +export const PlaceText = styled.span` + text-transform: capitalize; +` export const ActionButton = styled(Button)` background: none; + border: none; height: 100%; ` export const ActionButtonPlaceholder = styled.span`` +// FIXME_QBD: move this text elsewhere. const VIEW_LABEL = 'View stop' // used for stops only. const DELETE_LABEL = 'Delete place' -const EDIT_LABEL = 'Edit this place' +// const EDIT_LABEL = 'Edit this place' /** * Renders a stylable clickable button for editing/selecting a user's favorite place, * and buttons for viewing and deleting the place if corresponding handlers are provided. */ const Place = ({ - place, ariaLabel, - buttonStyle, className, - name = place?.name || place?.address, - details = place?.address || `Set your ${place?.type || 'other'} address`, - icon = place?.icon, - index, - isCreating, - largeIcon, + detailText, + icon, + mainText, onClick, onDelete, onView, - path = `${getPlaceBasePath(isCreating)}/${index}`, - placeDetailClassName, - placeTextClassName, - title = place?.address && onClick - ? `${name}${details && ` (${details})`}` : EDIT_LABEL + title }) => { - const to = onClick ? null : path - const iconSize = largeIcon && '2x' - return ( - - - {largeIcon && } - - - {!largeIcon && } - {name} - - - - {details} - - - - + + + + {mainText} + + + {detailText} + + + {/* Action buttons. If none, render a placeholder. */} {!onView && !onDelete && } {onView && ( onView({ stopId: place.id })} + onClick={onView} title={VIEW_LABEL} > - + )} {onDelete && ( - + )} @@ -123,28 +107,18 @@ const Place = ({ Place.propTypes = { /** The aria-label for the main button */ ariaLabel: PropTypes.string, - /** The Bootstrap style to apply to buttons. */ - buttonStyle: PropTypes.string, /** The detail text displayed for the place */ - details: PropTypes.string, + detailText: PropTypes.string, /** The font-awesome icon name for the place. */ icon: PropTypes.string, - /** Whether to render icons large. */ - largeIcon: PropTypes.bool, /** The displayed name for the place. */ - name: PropTypes.string, + mainText: PropTypes.string, /** Called when the "main" button is clicked. Takes precedence over the path prop. */ onClick: PropTypes.func, /** Determines whether the Delete button is shown. Called when the Delete button is clicked. */ onDelete: PropTypes.func, /** Determines whether the View button is shown. Called when the View button is clicked. */ onView: PropTypes.func, - /** The path to navigate to on click. */ - path: PropTypes.string, - /** CSS class name for the place details. */ - placeDetailClassName: PropTypes.string, - /** CSS class name for the place name. */ - placeTextClassName: PropTypes.string, /** The title for the main button */ title: PropTypes.string } diff --git a/lib/components/user/places/styled.js b/lib/components/user/places/styled.js index f751e5e55..a045a1a8c 100644 --- a/lib/components/user/places/styled.js +++ b/lib/components/user/places/styled.js @@ -57,7 +57,7 @@ export const FavoritePlace = props => ( // Styles and exports for the place component // used in the main panel. -const StyledMainPanelPlace = styled(Place)` +export const StyledMainPanelPlace = styled(Place)` ${PlaceName} { margin-left: 0.25em; } @@ -69,13 +69,3 @@ const StyledMainPanelPlace = styled(Place)` width: 40px; } ` - -export const MainPanelPlace = props => ( - -) From 4c7e61c5edfab76816c2b9780799297d5afcf551 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 8 Oct 2021 11:17:30 -0400 Subject: [PATCH 045/270] refactor(PlaceShortcut): Link blank places to place editor if using middleware. --- lib/components/form/user-settings.js | 4 +++- lib/components/user/places/place-shortcut.js | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index e587f37ea..d79434517 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -149,7 +149,7 @@ class UserSettings extends Component {
    {/* Sorted locations are shown regardless of tracking. */} location.address || location.name || 'Click to add'} + getDetailText={location => location.address || location.name || `Set your ${location.type} address`} getMainText={location => location.type} header={isUsingOtpMiddleware && <> @@ -172,6 +172,7 @@ class UserSettings extends Component { * Displays a list of places with a header. */ const Places = ({ + // FIXME_QBD: are these defaults still needed? getDetailText = location => location.name, getMainText = location => location.type, header, @@ -193,6 +194,7 @@ const Places = ({ { - const { place } = this.props + const { index, persistenceStrategy, place, routeTo } = this.props if (place.blank) { - window.alert(`Enter origin/destination in the form (or set via map click) and click the resulting marker to set as ${place.type} location.`) + if (persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE) { + routeTo(`/account/places/${index}`) + } else { + window.alert(`Enter origin/destination in the form (or set via map click) and click the resulting marker to set as ${place.type} location.`) + } } else { // Convert to OTP UI location before sending events // (to avoid issues when user clicks Forget/Save location @@ -78,12 +84,16 @@ class PlaceShortcut extends Component { // connect to redux store const mapStateToProps = (state, ownProps) => { + const { config, currentQuery: query } = state.otp + const { persistence } = config return { - query: state.otp.currentQuery + persistenceStrategy: getPersistenceStrategy(persistence), + query } } const mapDispatchToProps = { + routeTo: uiActions.routeTo, setLocation: mapActions.setLocation } From 8116fa3c50d7d865a562540fe4aa20c4ffbbdfce Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 8 Oct 2021 12:55:42 -0400 Subject: [PATCH 046/270] refactor(UserSettings): Tweak saved place rendering. --- lib/components/form/user-settings.js | 20 +++++++++++--------- lib/components/user/places/place-shortcut.js | 2 +- lib/util/user.js | 6 ++++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index d79434517..2a9266003 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -13,7 +13,7 @@ import { LinkWithQuery } from '../form/connected-links' import PlaceShortcut from '../user/places/place-shortcut' import { StyledMainPanelPlace } from '../user/places/styled' import { PERSIST_TO_OTP_MIDDLEWARE } from '../../util/constants' -import { getPersistenceStrategy, isHome, isWork } from '../../util/user' +import { getPersistenceStrategy, isHome, isHomeOrWork, isWork } from '../../util/user' import { UnpaddedList } from './styled' @@ -136,21 +136,23 @@ class UserSettings extends Component { loggedInUser, persistenceStrategy } = this.props - // Clone locations in order to prevent blank locations from seeping into the - // app state/store. - const locations = this._getLocations(localUser) - const order = ['home', 'work', 'suggested', 'stop', 'recent'] - const sortedLocations = locations - .sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)) const isUsingOtpMiddleware = persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE const userNotLoggedIn = isUsingOtpMiddleware && !loggedInUser if (userNotLoggedIn) return null + + // Clone locations in order to prevent blank locations from seeping into the + // app state/store. + const locations = this._getLocations(isUsingOtpMiddleware ? loggedInUser : localUser) + const order = ['home', 'work', 'suggested', 'stop', 'recent'] + const sortedLocations = isUsingOtpMiddleware + ? locations + : locations.sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)) return (
    {/* Sorted locations are shown regardless of tracking. */} location.address || location.name || `Set your ${location.type} address`} - getMainText={location => location.type} + getDetailText={location => location.address || (!isHomeOrWork(location) && location.name) || `Set your ${location.type} address`} + getMainText={location => isHomeOrWork(location) ? location.type : location.name} header={isUsingOtpMiddleware && <> My saved places ( diff --git a/lib/components/user/places/place-shortcut.js b/lib/components/user/places/place-shortcut.js index 5256b219b..d2f922e6f 100644 --- a/lib/components/user/places/place-shortcut.js +++ b/lib/components/user/places/place-shortcut.js @@ -72,7 +72,7 @@ class PlaceShortcut extends Component { icon={icon} mainText={mainText} onClick={this._onSelect} - onDelete={!place.isFixed && !place.blank && this._onDelete} + onDelete={!place.isFixed && !place.blank ? this._onDelete : null} onView={onView && this._onView} placeId={place.id} title={title} diff --git a/lib/util/user.js b/lib/util/user.js index 83c0243d7..4e24fb6b6 100644 --- a/lib/util/user.js +++ b/lib/util/user.js @@ -57,11 +57,13 @@ export const PLACE_TYPES = { // Defaults for home and work const BLANK_HOME = { ...PLACE_TYPES.home, - address: '' + address: '', + blank: true } const BLANK_WORK = { ...PLACE_TYPES.work, - address: '' + address: '', + blank: true } /** From 0170bf5824d815a901a8cdb4dc5b75b8a7571684 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 8 Oct 2021 16:00:45 -0400 Subject: [PATCH 047/270] refactor(Place): Continue harmonize place components. --- lib/actions/user.js | 23 +-- lib/components/form/user-settings.js | 2 +- .../user/places/favorite-place-list.js | 19 ++- .../user/places/favorite-place-row.js | 131 ------------------ lib/components/user/places/place-shortcut.js | 3 +- lib/components/user/places/place.js | 11 +- lib/components/user/places/styled.js | 4 + 7 files changed, 37 insertions(+), 156 deletions(-) delete mode 100644 lib/components/user/places/favorite-place-row.js diff --git a/lib/actions/user.js b/lib/actions/user.js index 30a01a520..4b8906f69 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -550,25 +550,26 @@ export function deleteLoggedInUserPlace (placeIndex) { * Delete place data by id for the logged-in or local user * according to the persistence strategy. */ -export function deleteUserPlace (placeId) { +export function deleteUserPlace (place) { return function (dispatch, getState) { const { otp, user } = getState() const persistenceStrategy = getPersistenceStrategy(otp.config.persistence) const { loggedInUser } = user if (persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE && loggedInUser) { - // For middleware loggedInUsers, this method should only be triggered by the - // 'Forget home' or 'Forget work' links from OTP UI's EndPointOverlay/EndPoint, - // with placeId set to 'home' or 'work'. - if (isHomeOrWork({ type: placeId })) { - // Find the index of the place in the loggedInUser.savedLocations - const placeIndex = loggedInUser.savedLocations.findIndex(loc => loc.type === placeId) - if (placeIndex > -1) { - dispatch(deleteLoggedInUserPlace(placeIndex)) - } + // Find the index of the place in the loggedInUser.savedLocations + + // If 'Forget home' or 'Forget work' links from OTP UI's EndPointOverlay/EndPoint + // are clicked, then place will set to 'home' or 'work'. + const placeIndex = place === 'home' || place === 'work' + ? loggedInUser.savedLocations.findIndex(loc => loc.type === place) + : loggedInUser.savedLocations.indexOf(place) + + if (placeIndex > -1) { + dispatch(deleteLoggedInUserPlace(placeIndex)) } } else if (persistenceStrategy === PERSIST_TO_LOCAL_STORAGE) { - dispatch(deleteLocalUserSavedPlace(placeId)) + dispatch(deleteLocalUserSavedPlace(place.id)) } } } diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 2a9266003..03ea44a62 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -151,7 +151,7 @@ class UserSettings extends Component {
    {/* Sorted locations are shown regardless of tracking. */} location.address || (!isHomeOrWork(location) && location.name) || `Set your ${location.type} address`} + getDetailText={location => location.address || location.name || `Set your ${location.type} address`} getMainText={location => isHomeOrWork(location) ? location.type : location.name} header={isUsingOtpMiddleware && <> diff --git a/lib/components/user/places/favorite-place-list.js b/lib/components/user/places/favorite-place-list.js index 5c6088381..f068a0cac 100644 --- a/lib/components/user/places/favorite-place-list.js +++ b/lib/components/user/places/favorite-place-list.js @@ -2,6 +2,7 @@ import React from 'react' import { ControlLabel } from 'react-bootstrap' import { connect } from 'react-redux' +import * as userActions from '../../../actions/user' import { UnpaddedList } from '../../form/styled' import { CREATE_ACCOUNT_PLACES_PATH } from '../../../util/constants' import { getPlaceBasePath } from '../../../util/user' @@ -12,22 +13,24 @@ import { FavoritePlace } from './styled' * 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 FavoritePlaceList = ({ isCreating, loggedInUser }) => ( +const FavoritePlaceList = ({ deleteUserPlace, isCreating, loggedInUser }) => (
    Add the places you frequent often to save time planning trips: {loggedInUser.savedLocations.map((place, index) => ( deleteUserPlace(place)} + path={`${getPlaceBasePath(isCreating)}/${index}`} /> ))} @@ -45,4 +48,8 @@ const mapStateToProps = (state, ownProps) => { } } -export default connect(mapStateToProps)(FavoritePlaceList) +const mapDispatchToProps = { + deleteUserPlace: userActions.deleteUserPlace +} + +export default connect(mapStateToProps, mapDispatchToProps)(FavoritePlaceList) diff --git a/lib/components/user/places/favorite-place-row.js b/lib/components/user/places/favorite-place-row.js deleted file mode 100644 index c6bec12c3..000000000 --- a/lib/components/user/places/favorite-place-row.js +++ /dev/null @@ -1,131 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import { Button } from 'react-bootstrap' -import styled, { css } from 'styled-components' - -import { LinkContainerWithQuery } from '../../form/connected-links' -import Icon from '../../narrative/icon' - -const FIELD_HEIGHT_PX = '60px' - -const Container = styled.div` - align-items: stretch; - display: flex; - height: ${FIELD_HEIGHT_PX}; - margin-bottom: 10px; -` -const PlaceButton = styled(Button)` - align-items: center; - display: flex; - flex: 1 0 0; - overflow: hidden; - text-align: left; - width: inherit; -` - -const GreyIcon = styled(Icon)` - color: #888; - margin-right: 10px; -` - -const PlaceContent = styled.span` - display: inline-block; - text-transform: capitalize; - - & * { - color: #888; - display: block; - } - - & *:first-child { - color: inherit; - } -` - -const deleteCss = css` - margin-left: 4px; - width: ${FIELD_HEIGHT_PX}; -` - -const DeleteButton = styled(Button)` - ${deleteCss} -` - -const DeletePlaceholder = styled.span` - ${deleteCss} -` - -const MESSAGES = { - DELETE: 'Delete this place', - EDIT: 'Edit this place' -} - -/** - * Renders a clickable button for editing a user's favorite place, - * and lets the user delete the place. - */ -const FavoritePlaceRow = ({ detailText, icon, isFixed, mainText, onDelete, path, place }) => { - if (place) { - const { address, icon } = place - return ( - - - - - - {mainText} - {detailText} - - - - - {/* For fixed places, show Delete only if an address has been provided. */} - {(!isFixed || address) - ? ( - - - - ) - : } - - ) - } else { - // If no place is passed, render the Add place button instead. - return ( - - - - - Add another place - - - - - ) - } -} - -FavoritePlaceRow.propTypes = { - /** Whether the place is fixed (e.g. 'Home', 'Work' are fixed.) */ - isFixed: PropTypes.bool, - /** Called when the delete button is clicked. */ - onDelete: PropTypes.func, - /** The path to navigate to on click. */ - path: PropTypes.string.isRequired, - /** The place to render. */ - place: PropTypes.shape({ - address: PropTypes.string, - icon: PropTypes.string.isRequired, - name: PropTypes.string, - type: PropTypes.string.isRequired - }) -} - -export default FavoritePlaceRow diff --git a/lib/components/user/places/place-shortcut.js b/lib/components/user/places/place-shortcut.js index d2f922e6f..764aa5fcf 100644 --- a/lib/components/user/places/place-shortcut.js +++ b/lib/components/user/places/place-shortcut.js @@ -60,7 +60,7 @@ class PlaceShortcut extends Component { _onDelete = () => { const { onDelete, place } = this.props - onDelete(place.id) + onDelete(place) } render () { @@ -74,7 +74,6 @@ class PlaceShortcut extends Component { onClick={this._onSelect} onDelete={!place.isFixed && !place.blank ? this._onDelete : null} onView={onView && this._onView} - placeId={place.id} title={title} /> ) diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 223eb014c..88225a153 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -13,7 +13,6 @@ const Container = styled.li` ` export const PlaceButton = styled(Button)` background: none; - border: none; flex: 1 0 0; overflow: hidden; text-align: left; @@ -36,7 +35,6 @@ export const PlaceText = styled.span` export const ActionButton = styled(Button)` background: none; - border: none; height: 100%; ` @@ -56,12 +54,14 @@ const Place = ({ className, detailText, icon, + largeIcon, mainText, onClick, onDelete, onView, title }) => { + const iconSize = largeIcon && '2x' return ( + {largeIcon && } - + {!largeIcon && } {mainText} @@ -88,7 +89,7 @@ const Place = ({ onClick={onView} title={VIEW_LABEL} > - + )} {onDelete && ( @@ -97,7 +98,7 @@ const Place = ({ onClick={onDelete} title={DELETE_LABEL} > - + )} diff --git a/lib/components/user/places/styled.js b/lib/components/user/places/styled.js index a045a1a8c..f79b9703c 100644 --- a/lib/components/user/places/styled.js +++ b/lib/components/user/places/styled.js @@ -58,6 +58,9 @@ export const FavoritePlace = props => ( // used in the main panel. export const StyledMainPanelPlace = styled(Place)` + ${PlaceButton} { + border: none; + } ${PlaceName} { margin-left: 0.25em; } @@ -66,6 +69,7 @@ export const StyledMainPanelPlace = styled(Place)` height: 100%; } ${ActionButton} { + border: none; width: 40px; } ` From 18021e9a58379305a30de2005238d34cac700273 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 11 Oct 2021 17:51:23 -0400 Subject: [PATCH 048/270] refactor(actions/user): Consolidate actions forgetStop/rememberStop. --- lib/actions/map.js | 4 ---- lib/actions/user.js | 3 ++- lib/components/form/user-settings.js | 8 ++++---- lib/components/viewers/stop-viewer.js | 7 ++++--- lib/reducers/create-user-reducer.js | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/actions/map.js b/lib/actions/map.js index 9312797b7..abe7780e3 100644 --- a/lib/actions/map.js +++ b/lib/actions/map.js @@ -22,10 +22,6 @@ const clearingLocation = createAction('CLEAR_LOCATION') const settingLocation = createAction('SET_LOCATION') const deleteRecentPlace = createAction('DELETE_LOCAL_USER_RECENT_PLACE') -// Public actions -export const forgetStop = createAction('FORGET_STOP') -export const rememberStop = createAction('REMEMBER_STOP') - /** * Dispatches the action to delete a saved or recent place from localStorage. */ diff --git a/lib/actions/user.js b/lib/actions/user.js index 4b8906f69..fbe00f764 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -40,7 +40,8 @@ const setitineraryExistence = createAction('SET_ITINERARY_EXISTENCE') // localStorage user actions export const deleteLocalUserRecentPlace = createAction('DELETE_LOCAL_USER_RECENT_PLACE') const deleteLocalUserSavedPlace = createAction('DELETE_LOCAL_USER_SAVED_PLACE') -export const deleteFavoriteStop = createAction('FORGET_STOP') +export const forgetStop = createAction('FORGET_STOP') +export const rememberStop = createAction('REMEMBER_STOP') const rememberLocalUserPlace = createAction('REMEMBER_LOCAL_USER_PLACE') function createNewUser (auth0User) { diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 03ea44a62..92817d89c 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -42,9 +42,9 @@ class UserSettings extends Component { _getLocalStorageOnlyContent = () => { const { - deleteFavoriteStop, deleteLocalUserRecentPlace, forgetSearch, + forgetStop, localUser, setQueryParam, setViewedStop, @@ -59,14 +59,14 @@ class UserSettings extends Component { getDetailText={location => `Stop ID: ${location.id}`} getMainText={location => location.address || location.name} header='Favorite stops' - onDelete={deleteFavoriteStop} + onDelete={forgetStop} onView={setViewedStop} places={favoriteStops} textIfEmpty='No favorite stops' /> {storeTripHistory && ''} + getDetailText={location => moment(location.timestamp).fromNow()} getMainText={location => location.address || location.name} header='Recent places' onDelete={deleteLocalUserRecentPlace} @@ -295,11 +295,11 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { - deleteFavoriteStop: userActions.deleteFavoriteStop, deleteLocalUserRecentPlace: userActions.deleteLocalUserRecentPlace, deleteLoggedInUserPlace: userActions.deleteLoggedInUserPlace, deleteUserPlace: userActions.deleteUserPlace, forgetSearch: apiActions.forgetSearch, + forgetStop: userActions.forgetStop, setLocation: mapActions.setLocation, setQueryParam: formActions.setQueryParam, setViewedStop: uiActions.setViewedStop, diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 90f828246..6e5db514a 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -11,6 +11,7 @@ import styled from 'styled-components' import * as apiActions from '../../actions/api' import * as mapActions from '../../actions/map' import * as uiActions from '../../actions/ui' +import * as userActions from '../../actions/user' import Icon from '../narrative/icon' import { PERSIST_TO_LOCAL_STORAGE } from '../../util/constants' import { getShowUserSettings, getStopViewerConfig } from '../../util/state' @@ -78,7 +79,7 @@ class StopViewer extends Component { _toggleFavorite = () => { const { forgetStop, rememberStop, stopData } = this.props - if (this._isFavorite()) forgetStop(stopData.id) + if (this._isFavorite()) forgetStop(stopData) else rememberStop(stopData) } @@ -318,8 +319,8 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = { findStop: apiActions.findStop, findStopTimesForStop: apiActions.findStopTimesForStop, - forgetStop: mapActions.forgetStop, - rememberStop: mapActions.rememberStop, + forgetStop: userActions.forgetStop, + rememberStop: userActions.rememberStop, setLocation: mapActions.setLocation, setMainPanelContent: uiActions.setMainPanelContent, toggleAutoRefresh: uiActions.toggleAutoRefresh diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 4dd1b78b5..7c09f2f0b 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -204,7 +204,7 @@ function createUserReducer (config) { } case 'FORGET_STOP': - return removeLocalUserPlace(action.payload, state, 'favoriteStops', 'favoriteStops') + return removeLocalUserPlace(action.payload.id, state, 'favoriteStops', 'favoriteStops') case 'REMEMBER_STOP': { // Payload is stop data. We want to avoid saving other attributes that From 325730aad8e52b262d5a20784b7ad23d558e3ff8 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 12 Oct 2021 17:29:06 -0400 Subject: [PATCH 049/270] refactor(UserSettings): Fix delete actions for localStorage. --- __tests__/actions/__snapshots__/api.js.snap | 4 ++-- .../viewers/__snapshots__/stop-viewer.js.snap | 16 ---------------- lib/actions/api.js | 2 +- lib/actions/user.js | 2 +- lib/components/form/user-settings.js | 2 +- lib/reducers/create-user-reducer.js | 6 +++--- 6 files changed, 8 insertions(+), 24 deletions(-) diff --git a/__tests__/actions/__snapshots__/api.js.snap b/__tests__/actions/__snapshots__/api.js.snap index f00d42d1d..f5f86bf33 100644 --- a/__tests__/actions/__snapshots__/api.js.snap +++ b/__tests__/actions/__snapshots__/api.js.snap @@ -35,7 +35,7 @@ Array [ Array [ Object { "payload": Object { - "error": [TypeError: Cannot read property 'trackRecent' of undefined], + "error": [TypeError: Cannot read property 'localUser' of undefined], "requestId": "abcd1239", "searchId": "abcd1234", "url": "http://mock-host.com:80/api/plan?fromPlace=Origin%20%2812%2C34%29%3A%3A12%2C34&toPlace=Destination%20%2834%2C12%29%3A%3A34%2C12&mode=WALK%2CTRANSIT&ignoreRealtimeUpdates=false", @@ -110,7 +110,7 @@ Array [ Array [ Object { "payload": Object { - "error": [TypeError: Cannot read property 'trackRecent' of undefined], + "error": [TypeError: Cannot read property 'localUser' of undefined], "requestId": "abcd1236", "searchId": "abcd1234", "url": "http://mock-host.com:80/api/plan?fromPlace=Origin%20%2812%2C34%29%3A%3A12%2C34&toPlace=Destination%20%2834%2C12%29%3A%3A34%2C12&mode=WALK%2CTRANSIT&ignoreRealtimeUpdates=false", diff --git a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap index 8a49a4925..231f552bf 100644 --- a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap +++ b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap @@ -15,7 +15,6 @@ exports[`components > viewers > stop viewer should render countdown times after > viewers > stop viewer should render countdown times after className="sc-jrAGrp bDAiGg" > viewers > stop viewer should render countdown times after } > viewers > stop viewer should render countdown times for st > viewers > stop viewer should render countdown times for st className="sc-jrAGrp bDAiGg" > viewers > stop viewer should render countdown times for st } > viewers > stop viewer should render times after midnight w > viewers > stop viewer should render times after midnight w className="sc-jrAGrp bDAiGg" > 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 className="sc-jrAGrp bDAiGg" > viewers > stop viewer should render with OTP transit index } > viewers > stop viewer should render with TriMet transit in > viewers > stop viewer should render with TriMet transit in className="sc-jrAGrp bDAiGg" > viewers > stop viewer should render with TriMet transit in } > viewers > stop viewer should render with initial stop id a > this.props.forgetSearch(this.props.tripRequest.id) + _onForget = () => this.props.forgetSearch(this.props.tripRequest) render () { const { tripRequest, user } = this.props diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 7c09f2f0b..6111b5f07 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -46,9 +46,9 @@ function sortAndTrim (list) { /** * Removes a place by id from the specified localUser state and optional persistence setting. */ -function removeLocalUserPlace (id, state, fieldName, settingName) { +function removeLocalUserPlace (place, state, fieldName, settingName) { const originalArray = state.localUser[fieldName] - const removeIndex = originalArray.findIndex(l => l.id === id) + const removeIndex = originalArray.findIndex(l => l.id === place.id) // If a persistence setting is provided, // persist a copy of the passed array without the specified element. if (settingName) { @@ -204,7 +204,7 @@ function createUserReducer (config) { } case 'FORGET_STOP': - return removeLocalUserPlace(action.payload.id, state, 'favoriteStops', 'favoriteStops') + return removeLocalUserPlace(action.payload, state, 'favoriteStops', 'favoriteStops') case 'REMEMBER_STOP': { // Payload is stop data. We want to avoid saving other attributes that From 40f8c8737a939dc1b9a0638e97c8afb24863762b Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 12 Oct 2021 17:47:46 -0400 Subject: [PATCH 050/270] refactor(Place button): Reinstate path prop. --- lib/components/user/places/place.js | 45 ++++++++++++++++++----------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 88225a153..58bf0bcb5 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -3,9 +3,8 @@ import React from 'react' import { Button } from 'react-bootstrap' import styled from 'styled-components' -// import { LinkContainerWithQuery } from '../../form/connected-links' +import { LinkContainerWithQuery } from '../../form/connected-links' import NarrativeIcon from '../../narrative/icon' -// import { getPlaceBasePath } from '../../../util/user' const Container = styled.li` align-items: stretch; @@ -59,27 +58,35 @@ const Place = ({ onClick, onDelete, onView, + path, title }) => { + const to = onClick ? null : path const iconSize = largeIcon && '2x' return ( - - {largeIcon && } - - - {!largeIcon && } - {mainText} - - - {detailText} - - - + + {largeIcon && } + + + {!largeIcon && } + {mainText} + + + {detailText} + + + + {/* Action buttons. If none, render a placeholder. */} {!onView && !onDelete && } @@ -112,6 +119,8 @@ Place.propTypes = { detailText: PropTypes.string, /** The font-awesome icon name for the place. */ icon: PropTypes.string, + /** Whether to render icons large. */ + largeIcon: PropTypes.bool, /** The displayed name for the place. */ mainText: PropTypes.string, /** Called when the "main" button is clicked. Takes precedence over the path prop. */ @@ -120,6 +129,8 @@ Place.propTypes = { onDelete: PropTypes.func, /** Determines whether the View button is shown. Called when the View button is clicked. */ onView: PropTypes.func, + /** The path to navigate to on click. */ + path: PropTypes.string, /** The title for the main button */ title: PropTypes.string } From e62ae57476b4c51d10e8489b8a14d1d8d93ff370 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 13 Oct 2021 11:04:09 -0400 Subject: [PATCH 051/270] refactor(Place): Fix deleting user place --- lib/actions/user.js | 5 ++++- lib/components/form/user-settings.js | 8 ++++---- lib/components/user/places/favorite-place-list.js | 2 ++ lib/components/user/places/place-shortcut.js | 3 +-- lib/components/user/places/place.js | 6 +----- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/actions/user.js b/lib/actions/user.js index 0f8e358dc..691c2fe27 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -519,10 +519,13 @@ export function planNewTripFromMonitoredTrip (monitoredTrip) { * Saves the given place data at the specified index for the logged-in user. * Note: places with blank addresses will not appear in persistence. */ -export function saveUserPlace (placeToSave, placeIndex) { +export function saveUserPlace (place, placeIndex) { return function (dispatch, getState) { const { loggedInUser } = getState().user + // Remove "blank" attribute + const { blank, ...placeToSave } = place + if (placeIndex === 'new') { loggedInUser.savedLocations.push(placeToSave) } else { diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 31dcce7e3..b42bade11 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -17,6 +17,7 @@ import { getPersistenceStrategy, isHome, isHomeOrWork, isWork } from '../../util import { UnpaddedList } from './styled' +const { toSentenceCase } = coreUtils.itinerary const { summarizeQuery } = coreUtils.query class UserSettings extends Component { @@ -152,7 +153,7 @@ class UserSettings extends Component { {/* Sorted locations are shown regardless of tracking. */} location.address || location.name || `Set your ${location.type} address`} - getMainText={location => isHomeOrWork(location) ? location.type : location.name} + getMainText={location => isHomeOrWork(location) ? toSentenceCase(location.type) : location.name} header={isUsingOtpMiddleware && <> My saved places ( @@ -174,9 +175,8 @@ class UserSettings extends Component { * Displays a list of places with a header. */ const Places = ({ - // FIXME_QBD: are these defaults still needed? - getDetailText = location => location.name, - getMainText = location => location.type, + getDetailText, + getMainText, header, onDelete, onView, diff --git a/lib/components/user/places/favorite-place-list.js b/lib/components/user/places/favorite-place-list.js index f068a0cac..da6744740 100644 --- a/lib/components/user/places/favorite-place-list.js +++ b/lib/components/user/places/favorite-place-list.js @@ -25,6 +25,7 @@ const FavoritePlaceList = ({ deleteUserPlace, isCreating, loggedInUser }) => ( mainText={place.name || place.address} onDelete={() => deleteUserPlace(place)} path={`${getPlaceBasePath(isCreating)}/${index}`} + title={'Edit this place'} /> ))} @@ -32,6 +33,7 @@ const FavoritePlaceList = ({ deleteUserPlace, isCreating, loggedInUser }) => ( icon='plus' mainText='Add another place' path={`${getPlaceBasePath(isCreating)}/new`} + title={''} />
    diff --git a/lib/components/user/places/place-shortcut.js b/lib/components/user/places/place-shortcut.js index 764aa5fcf..f8fea8949 100644 --- a/lib/components/user/places/place-shortcut.js +++ b/lib/components/user/places/place-shortcut.js @@ -64,7 +64,7 @@ class PlaceShortcut extends Component { } render () { - const { detailText, icon, mainText, onView, place, title } = this.props + const { detailText, icon, mainText, onView, place } = this.props return ( ) } diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 58bf0bcb5..289789576 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -39,17 +39,14 @@ export const ActionButton = styled(Button)` export const ActionButtonPlaceholder = styled.span`` -// FIXME_QBD: move this text elsewhere. const VIEW_LABEL = 'View stop' // used for stops only. const DELETE_LABEL = 'Delete place' -// const EDIT_LABEL = 'Edit this place' /** * Renders a stylable clickable button for editing/selecting a user's favorite place, * and buttons for viewing and deleting the place if corresponding handlers are provided. */ const Place = ({ - ariaLabel, className, detailText, icon, @@ -59,7 +56,7 @@ const Place = ({ onDelete, onView, path, - title + title = `${mainText}${detailText && ` (${detailText})`}` }) => { const to = onClick ? null : path const iconSize = largeIcon && '2x' @@ -71,7 +68,6 @@ const Place = ({ to={to} > From b7b608d2c9c8a57b9bbcbe28b81a59ef0b41a77e Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 13 Oct 2021 11:35:26 -0400 Subject: [PATCH 052/270] refactor(Tweak things): --- __tests__/reducers/create-user-reducer.js | 2 +- lib/actions/user.js | 5 ++--- lib/components/user/places/place.js | 2 -- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/__tests__/reducers/create-user-reducer.js b/__tests__/reducers/create-user-reducer.js index a55594ec2..2409f681d 100644 --- a/__tests__/reducers/create-user-reducer.js +++ b/__tests__/reducers/create-user-reducer.js @@ -6,6 +6,6 @@ describe('lib > reducers > create-user-reducer', () => { it('should be able to create the initial state', () => { setDefaultTestTime() - expect(getUserInitialState({}, {})).toMatchSnapshot() + expect(getUserInitialState({})).toMatchSnapshot() }) }) diff --git a/lib/actions/user.js b/lib/actions/user.js index 691c2fe27..772b5e5bc 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -523,7 +523,7 @@ export function saveUserPlace (place, placeIndex) { return function (dispatch, getState) { const { loggedInUser } = getState().user - // Remove "blank" attribute + // Remove "blank" attribute, it is not recognized by the middleware. const { blank, ...placeToSave } = place if (placeIndex === 'new') { @@ -551,8 +551,7 @@ export function deleteLoggedInUserPlace (placeIndex) { } /** - * Delete place data by id for the logged-in or local user - * according to the persistence strategy. + * Delete a place for the logged-in or local user according to the persistence strategy. */ export function deleteUserPlace (place) { return function (dispatch, getState) { diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 289789576..e6747d9cf 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -109,8 +109,6 @@ const Place = ({ } Place.propTypes = { - /** The aria-label for the main button */ - ariaLabel: PropTypes.string, /** The detail text displayed for the place */ detailText: PropTypes.string, /** The font-awesome icon name for the place. */ From ffbe225a27d71256ec57806f202d6e94b5410a12 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 13 Oct 2021 12:11:48 -0400 Subject: [PATCH 053/270] refactor(UserSettings): Remove unused props --- lib/components/form/user-settings.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index b42bade11..205aacde0 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -75,8 +75,6 @@ class UserSettings extends Component { />} `Stop ID: ${location.id}`} - getMainText={trip => location.address || location.name} setQueryParam={setQueryParam} tripRequests={recentSearches} user={localUser} From 32da1cf37c5ae697e9e59c9d78a6db5350e6353f Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 13 Oct 2021 12:56:26 -0400 Subject: [PATCH 054/270] refactor(UserSettings): Override default styles in BatchRoutingPanel. --- lib/components/app/batch-routing-panel.js | 2 +- lib/components/form/user-settings.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/components/app/batch-routing-panel.js b/lib/components/app/batch-routing-panel.js index a730c84d9..e66b2e7bd 100644 --- a/lib/components/app/batch-routing-panel.js +++ b/lib/components/app/batch-routing-panel.js @@ -55,7 +55,7 @@ class BatchRoutingPanel extends Component {
    {!activeSearch && showUserSettings && - + } order.indexOf(a.type) - order.indexOf(b.type)) return ( -
    +
    {/* Sorted locations are shown regardless of tracking. */} location.address || location.name || `Set your ${location.type} address`} From 56510d26ac743763341fdf83090fcc945411bada Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 13 Oct 2021 16:34:32 -0400 Subject: [PATCH 055/270] refactor(UserSettings): Include recent searches for opted-in otp-middleware users. --- __tests__/actions/__snapshots__/api.js.snap | 30 +++------------- lib/actions/api.js | 38 +++++++++++++++------ lib/components/form/user-settings.js | 20 +++++++++-- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/__tests__/actions/__snapshots__/api.js.snap b/__tests__/actions/__snapshots__/api.js.snap index f5f86bf33..88bb085fe 100644 --- a/__tests__/actions/__snapshots__/api.js.snap +++ b/__tests__/actions/__snapshots__/api.js.snap @@ -23,7 +23,7 @@ Array [ Array [ Object { "payload": Object { - "requestId": "abcd1238", + "requestId": "abcd1237", "response": Object { "fake": "response", }, @@ -32,17 +32,6 @@ Array [ "type": "ROUTING_RESPONSE", }, ], - Array [ - Object { - "payload": Object { - "error": [TypeError: Cannot read property 'localUser' of undefined], - "requestId": "abcd1239", - "searchId": "abcd1234", - "url": "http://mock-host.com:80/api/plan?fromPlace=Origin%20%2812%2C34%29%3A%3A12%2C34&toPlace=Destination%20%2834%2C12%29%3A%3A34%2C12&mode=WALK%2CTRANSIT&ignoreRealtimeUpdates=false", - }, - "type": "ROUTING_ERROR", - }, - ], Array [ [Function], ], @@ -52,7 +41,7 @@ Array [ "activeItinerary": 0, "pending": 1, "routingType": "ITINERARY", - "searchId": "abcd1237", + "searchId": "abcd1236", "updateSearchInReducer": false, }, "type": "ROUTING_REQUEST", @@ -65,8 +54,8 @@ Array [ Object { "payload": Object { "error": [Error: Received error from server], - "requestId": "abcd1240", - "searchId": "abcd1237", + "requestId": "abcd1238", + "searchId": "abcd1236", "url": "http://mock-host.com:80/api/plan?fromPlace=Origin%20%2812%2C34%29%3A%3A12%2C34&toPlace=Destination%20%2834%2C12%29%3A%3A34%2C12&mode=WALK%2CTRANSIT&ignoreRealtimeUpdates=false", }, "type": "ROUTING_ERROR", @@ -107,17 +96,6 @@ Array [ "type": "ROUTING_RESPONSE", }, ], - Array [ - Object { - "payload": Object { - "error": [TypeError: Cannot read property 'localUser' of undefined], - "requestId": "abcd1236", - "searchId": "abcd1234", - "url": "http://mock-host.com:80/api/plan?fromPlace=Origin%20%2812%2C34%29%3A%3A12%2C34&toPlace=Destination%20%2834%2C12%29%3A%3A34%2C12&mode=WALK%2CTRANSIT&ignoreRealtimeUpdates=false", - }, - "type": "ROUTING_ERROR", - }, - ], ] `; diff --git a/lib/actions/api.js b/lib/actions/api.js index 1a03dc3d9..9cd5ef4a6 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -6,11 +6,13 @@ import coreUtils from '@opentripplanner/core-utils' import { createAction } from 'redux-actions' import qs from 'qs' -import { getStopViewerConfig, queryIsValid } from '../util/state' +import { PERSIST_TO_OTP_MIDDLEWARE } from '../util/constants' import { getSecureFetchOptions } from '../util/middleware' +import { getStopViewerConfig, queryIsValid } from '../util/state' +import { getPersistenceStrategy } from '../util/user' import { ItineraryView, setItineraryView } from './ui' -import { rememberPlace } from './user' +import { fetchTripRequests, rememberPlace } from './user' if (typeof (fetch) === 'undefined') require('isomorphic-fetch') @@ -89,6 +91,9 @@ export function routingQuery (searchId = null, updateSearchInReducer = false) { return function (dispatch, getState) { // FIXME: batchId is searchId for now. const state = getState() + const { currentQuery } = state.otp + const persistenceStrategy = getPersistenceStrategy(state.otp.config.persistence) + const isUsingOtpMiddleware = persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE const isNewSearch = !searchId if (isNewSearch) searchId = randId() @@ -96,7 +101,7 @@ export function routingQuery (searchId = null, updateSearchInReducer = false) { if (!queryIsValid(state)) { console.warn( 'Query is invalid. Aborting routing query', - state.otp.currentQuery + currentQuery ) return } @@ -104,13 +109,13 @@ export function routingQuery (searchId = null, updateSearchInReducer = false) { // Reset itinerary view to default (list view). dispatch(setItineraryView(ItineraryView.LIST)) const activeItinerary = getActiveItinerary(state) - const routingType = state.otp.currentQuery.routingType + const { combinations, routingType } = currentQuery // For multiple mode combinations, gather injected params from config/query. // Otherwise, inject nothing (rely on what's in current query) and perform // one iteration. - const iterations = state.otp.currentQuery.combinations - ? state.otp.currentQuery.combinations.map( + const iterations = combinations + ? combinations.map( ({mode, params}) => ({mode, ...params}) ) : [{}] @@ -135,8 +140,8 @@ export function routingQuery (searchId = null, updateSearchInReducer = false) { })) // If tracking is enabled, store locations and search after successful // search is completed. - if (state.user.localUser?.storeTripHistory) { - const { from, to } = state.otp.currentQuery + if (!isUsingOtpMiddleware && state.user?.localUser?.storeTripHistory) { + const { from, to } = currentQuery if (!isStoredPlace(from)) { dispatch(rememberPlace({ location: formatRecentPlace(from), type: 'recent' })) } @@ -183,15 +188,26 @@ export function routingQuery (searchId = null, updateSearchInReducer = false) { // when a logged-in user refreshes the page, the trip request in the URL is not recorded again // (state.user stays unpopulated until after this function is called). // - const { user } = state - const storeTripHistory = user?.loggedInUser?.storeTripHistory + const shouldStoreTrip = i === 0 && ( + isUsingOtpMiddleware + ? state.user?.loggedInUser?.storeTripHistory + : state.user?.localUser?.storeTripHistory + ) const nonRealtimeFetch = fetch( constructRoutingQuery(state, true), - getOtpFetchOptions(state, storeTripHistory) + getOtpFetchOptions(state, shouldStoreTrip) ) .then(getJsonAndCheckResponse) .then(json => { + // If tracking is enabled, refetch recent trip requests after a few seconds, + // so that persistence of trip requests has time to complete. + if (isUsingOtpMiddleware && state.user?.loggedInUser?.storeTripHistory) { + setTimeout(() => { + dispatch(fetchTripRequests()) + }, 5000) + } + // FIXME: This is only performed when ignoring realtimeupdates currently, just // to ensure it is not repeated twice. // FIXME: We should check that the mode combination actually has diff --git a/lib/components/form/user-settings.js b/lib/components/form/user-settings.js index 864b2a863..d447fdc42 100644 --- a/lib/components/form/user-settings.js +++ b/lib/components/form/user-settings.js @@ -131,9 +131,12 @@ class UserSettings extends Component { const { className, deleteUserPlace, + forgetSearch, localUser, loggedInUser, + loggedInUserTripRequests, persistenceStrategy, + setQueryParam, style } = this.props const isUsingOtpMiddleware = persistenceStrategy === PERSIST_TO_OTP_MIDDLEWARE @@ -151,7 +154,9 @@ class UserSettings extends Component {
    {/* Sorted locations are shown regardless of tracking. */} location.address || location.name || `Set your ${location.type} address`} + getDetailText={location => + // FIXME_QBD: unify the two location models. + (isUsingOtpMiddleware ? location.address : location.name) || `Set your ${location.type} address`} getMainText={location => isHomeOrWork(location) ? toSentenceCase(location.type) : location.name} header={isUsingOtpMiddleware && <> @@ -164,7 +169,16 @@ class UserSettings extends Component { places={sortedLocations} separator={false} /> - {!isUsingOtpMiddleware && this._getLocalStorageOnlyContent()} + {isUsingOtpMiddleware + ? ( + + ) + : this._getLocalStorageOnlyContent()}
    ) } @@ -237,7 +251,7 @@ class TripRequest extends Component { icon='clock-o' mainText={mainText} onClick={this._onSelect} - onDelete={canDelete && this._onForget} + onDelete={canDelete ? this._onForget : null} /> ) } From 9661451fc7fb29c9346f31a35ceb73f1bdc7608f Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 13 Oct 2021 18:21:56 -0400 Subject: [PATCH 056/270] refactor(FavoritePlaceList): Localize component and children. --- i18n/en-US.yml | 12 ++-- i18n/fr-FR.yml | 12 ++-- lib/components/user/back-to-trip-planner.js | 3 +- .../user/places/favorite-place-list.js | 67 ++++++++++++------- lib/components/user/places/place.js | 40 +++++------ 5 files changed, 78 insertions(+), 56 deletions(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 3205d4b79..b23adf3a2 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -51,10 +51,10 @@ components: notifications: Notifications places: Favorite places terms: Terms - FavoritePlaceRow: + FavoritePlaceList: addAnotherPlace: Add another place - # deleteThisPlace and editThisPlace are aria/tooltip texts. - deleteThisPlace: Delete this place + description: "Add the places you frequent often to save time planning trips:" + # editThisPlace is a tooltip text. editThisPlace: Edit this place setAddressForPlaceType: Set your {placeType} address FavoritePlaceScreen: @@ -66,8 +66,6 @@ components: nameAlreadyUsed: You are already using this name for another place. Please enter a different name. placeNotFound: Place not found placeNotFoundDescription: Sorry, the requested place was not found. - FavoritePlacesList: - description: Add the places you frequent often to save time planning trips: FormNavigationButtons: ariaLabel: Form navigation ItinerarySummary: @@ -143,6 +141,10 @@ components: for a text message with a verification code, and enter the code below (code expires after 10 minutes)." verify: Verify + Place: + # deleteThisPlace is an aria/tooltip text. + deleteThisPlace: Delete this place + viewStop: View stop PlaceEditor: genericLocationPlaceholder: Search for location locationPlaceholder: Search for {placeName} location diff --git a/i18n/fr-FR.yml b/i18n/fr-FR.yml index 2e8776eaa..37a79104b 100644 --- a/i18n/fr-FR.yml +++ b/i18n/fr-FR.yml @@ -35,10 +35,10 @@ components: notifications: Notifications places: Lieux favoris terms: Conditions d'utilisation - FavoritePlaceRow: + FavoritePlaceList: addAnotherPlace: Ajouter un autre lieu - # deleteThisPlace and editThisPlace are aria/tooltip texts. - deleteThisPlace: Supprimer ce lieu + description: "Ajoutez les lieux que vous fréquentez souvent pour faciliter vos recherches de trajets :" + # editThisPlace is a tooltip text. editThisPlace: Modifier ce lieu setAddressForPlaceType: Entrez l'adresse de votre {placeType} FavoritePlaceScreen: @@ -50,8 +50,6 @@ components: nameAlreadyUsed: Ce nom est déjà utilisé avec un autre lieu. Veuillez saisir un nom différent. placeNotFound: Lieu introuvable placeNotFoundDescription: Le lieu recherché est introuvable. - FavoritePlacesList: - description: "Ajoutez les lieux que vous fréquentez souvent pour faciliter vos recherches de trajets :" FormNavigationButtons: ariaLabel: Navigation du formulaire ItinerarySummary: @@ -129,6 +127,10 @@ components: verificationInstructions: "Un SMS vous a été envoyé avec un code de vérification. Veuillez taper ce code ci-dessous (le code expire après 10 minutes)." verify: Verifier + Place: + # deleteThisPlace is an aria/tooltip text. + deleteThisPlace: Supprimer ce lieu + viewStop: Voir cet arrêt PlaceEditor: genericLocationPlaceholder: Adresse du lieu locationPlaceholder: Adresse de votre {placeName} diff --git a/lib/components/user/back-to-trip-planner.js b/lib/components/user/back-to-trip-planner.js index 3139e4c7a..feb5bec1d 100644 --- a/lib/components/user/back-to-trip-planner.js +++ b/lib/components/user/back-to-trip-planner.js @@ -12,8 +12,7 @@ const StyledLinkWithQuery = styled(LinkWithQuery)` const BackToTripPlanner = () => ( - {/** FIXME: handle right-to-left languages */} - + ) diff --git a/lib/components/user/places/favorite-place-list.js b/lib/components/user/places/favorite-place-list.js index da6744740..d8fa3f650 100644 --- a/lib/components/user/places/favorite-place-list.js +++ b/lib/components/user/places/favorite-place-list.js @@ -1,43 +1,62 @@ +import coreUtils from '@opentripplanner/core-utils' import React from 'react' import { ControlLabel } from 'react-bootstrap' +import { FormattedMessage, useIntl } from 'react-intl' import { connect } from 'react-redux' import * as userActions from '../../../actions/user' import { UnpaddedList } from '../../form/styled' import { CREATE_ACCOUNT_PLACES_PATH } from '../../../util/constants' -import { getPlaceBasePath } from '../../../util/user' +import { getPlaceBasePath, isHomeOrWork } from '../../../util/user' import { FavoritePlace } from './styled' +const { toSentenceCase } = coreUtils.itinerary + /** * 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 FavoritePlaceList = ({ deleteUserPlace, isCreating, loggedInUser }) => ( -
    - Add the places you frequent often to save time planning trips: - - {loggedInUser.savedLocations.map((place, index) => ( +const FavoritePlaceList = ({ deleteUserPlace, isCreating, loggedInUser }) => { + const intl = useIntl() + const basePath = getPlaceBasePath(isCreating) + return ( +
    + + + + + {loggedInUser.savedLocations.map((place, index) => ( + + )} + icon={place.icon} + key={index} + // Use toSentenceCase (and not text-transform: capitalize) + // to change the first char. of the work and home locations only (which come in lowercase). + mainText={isHomeOrWork(place) ? toSentenceCase(place.name) : (place.name || place.address)} + onDelete={() => deleteUserPlace(place)} + path={`${basePath}/${index}`} + title={intl.formatMessage({ id: 'components.FavoritePlaceList.editThisPlace' })} + /> + ))} + deleteUserPlace(place)} - path={`${getPlaceBasePath(isCreating)}/${index}`} - title={'Edit this place'} + icon='plus' + mainText={} + path={`${basePath}/new`} + title={''} /> - ))} - - - -
    -) +
    +
    + ) +} // connect to the redux store diff --git a/lib/components/user/places/place.js b/lib/components/user/places/place.js index 7f8234581..c8a897bd8 100644 --- a/lib/components/user/places/place.js +++ b/lib/components/user/places/place.js @@ -1,10 +1,11 @@ import PropTypes from 'prop-types' import React from 'react' import { Button } from 'react-bootstrap' +import { useIntl } from 'react-intl' import styled from 'styled-components' import { LinkContainerWithQuery } from '../../form/connected-links' -import Icon from '../../util/icon' +import BaseIcon from '../../util/icon' const Container = styled.li` align-items: stretch; @@ -22,13 +23,11 @@ export const PlaceDetail = styled.span`` export const PlaceContent = styled.span`` -export const PlaceName = styled.span` - text-transform: capitalize; -` +export const PlaceName = styled.span`` -export const PlaceText = styled.span` - text-transform: capitalize; -` +export const PlaceText = styled.span`` + +export const Icon = styled(BaseIcon)`` export const ActionButton = styled(Button)` background: none; @@ -37,9 +36,6 @@ export const ActionButton = styled(Button)` export const ActionButtonPlaceholder = styled.span`` -const VIEW_LABEL = 'View stop' // used for stops only. -const DELETE_LABEL = 'Delete place' - /** * Renders a stylable clickable button for editing/selecting a user's favorite place, * and buttons for viewing and deleting the place if corresponding handlers are provided. @@ -56,6 +52,9 @@ const Place = ({ path, title = `${mainText}${detailText && ` (${detailText})`}` }) => { + const intl = useIntl() + const viewStopLabel = intl.formatMessage({ id: 'components.Place.viewStop' }) + const deletePlaceLabel = intl.formatMessage({ id: 'components.Place.deleteThisPlace' }) const to = onClick ? null : path const iconSize = largeIcon && '2x' return ( @@ -69,10 +68,10 @@ const Place = ({ onClick={onClick} title={title} > - {largeIcon && } + {largeIcon && } - {!largeIcon && } + {!largeIcon && } {mainText} @@ -85,21 +84,22 @@ const Place = ({ {/* Action buttons. If none, render a placeholder. */} {!onView && !onDelete && } {onView && ( + // This button is only used for viewing stops. - + )} {onDelete && ( - + )} @@ -108,13 +108,13 @@ const Place = ({ Place.propTypes = { /** The detail text displayed for the place */ - detailText: PropTypes.string, + detailText: PropTypes.node, /** The font-awesome icon name for the place. */ icon: PropTypes.string, /** Whether to render icons large. */ largeIcon: PropTypes.bool, /** The displayed name for the place. */ - mainText: PropTypes.string, + mainText: PropTypes.node, /** Called when the "main" button is clicked. Takes precedence over the path prop. */ onClick: PropTypes.func, /** Determines whether the Delete button is shown. Called when the Delete button is clicked. */ From 44f4c337742a59a3acb49fab2bca85a48db63ce7 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 29 Nov 2021 15:21:24 +0000 Subject: [PATCH 057/270] feat(api): fetch route information from OTP-2 instance --- lib/actions/api.js | 429 +++++++++++++----- lib/components/map/connected-stops-overlay.js | 7 +- 2 files changed, 318 insertions(+), 118 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index d076d5a9b..c33d7cbb1 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -644,6 +644,9 @@ export function findRoutes(params) { findRoutesResponse, findRoutesError, { + postprocess: (payload, dispatch) => { + return dispatch(findRoutesV2()) + }, rewritePayload: (payload) => { const routes = {} payload.forEach((rte) => { @@ -656,6 +659,66 @@ export function findRoutes(params) { ) } +export function findRoutesV2() { + return createGraphQLQueryAction( + `{ + routes { + gtfsId + shortName + longName + mode + color + agency { + name + gtfsId + } + } + }`, + {}, + findRoutesResponse, + findRoutesError, + { + noThrottle: true, + // Rewrite to match OTP-1 response object + // If OTP-1 is removed, this can probably be removed as well + rewritePayload: (payload, dispatch, getState) => { + const existingRouteIds = new Map() + const state = getState() + Object.keys(state?.otp?.transitIndex?.routes).forEach((routeId) => { + // Strip out the agency part of the route ID which can sometimes differ + let strippedRouteId = routeId.split(':') + strippedRouteId = strippedRouteId[strippedRouteId.length - 1] + + existingRouteIds.set(strippedRouteId, true) + }) + + const routes = {} + payload?.data?.routes?.forEach((rte) => { + // Strip out the agency part of the route ID which can sometimes differ + let strippedRouteId = rte.gtfsId.split(':') + strippedRouteId = strippedRouteId[strippedRouteId.length - 1] + if (existingRouteIds.get(strippedRouteId)) { + return + } + + rte.agencyName = rte.agency.name + // OTP-2 includes the feed ID in the agency ID. Stripping it + // matches OTP-1 behavior + rte.agencyId = rte.agency.gtfsId.split(':')[0] + rte.id = rte.gtfsId + + // Mark this route as a v2 route so that future requests relating to it + // will be made to the v2 endpoint + rte.v2 = true + + routes[rte.gtfsId] = rte + }) + return routes + }, + serviceId: 'routes' + } + ) +} // Patterns for Route lookup query // TODO: replace with GraphQL query for route => patterns => geometry const findPatternsForRouteResponse = createAction( @@ -669,57 +732,163 @@ export const findRouteResponse = createAction('FIND_ROUTE_RESPONSE') export const findRouteError = createAction('FIND_ROUTE_ERROR') export function findRoute(params) { - return createQueryAction( - `index/routes/${params.routeId}`, - findRouteResponse, - findRouteError, - { - noThrottle: true, - postprocess: (payload, dispatch) => { - // load patterns - dispatch(findPatternsForRoute({ routeId: params.routeId })) - } + return async function (dispatch, getState) { + const state = getState() + const route = state?.otp?.transitIndex?.routes?.[params.routeId] + // This can sometimes result in extra requests, but given that requests are + // already being sprayed, this is a safe and clean solution for now + if (!route) { + await dispatch(findRoutes()) } - ) -} -export function findPatternsForRoute(params) { - return createQueryAction( - `index/routes/${params.routeId}/patterns?includeGeometry=true`, - findPatternsForRouteResponse, - findPatternsForRouteError, - { - noThrottle: true, - postprocess: (payload, dispatch) => { - // load geometry for each pattern - payload.forEach((ptn) => { - // Some OTP instances don't support includeGeometry. - // We need to manually fetch geometry in these cases. - if (!ptn.geometry) { - dispatch( - findGeometryForPattern({ - patternId: ptn.id, - routeId: params.routeId - }) - ) + if (route?.v2) { + return dispatch( + createGraphQLQueryAction( + // TODO: cleanup stops query + `{ + route(id: "${route.gtfsId}") { + gtfsId + desc + agency { + gtfsId + name + url + timezone + lang + phone + } + longName + type + color + textColor + bikesAllowed + + patterns { + id + name + patternGeometry { + points + length + } + + stops { + code + id + lat + lon + name + locationType + geometries { + geoJson + polylines { + points + length + } + } + } + } + } } - }) - }, + `, + {}, + findRouteResponse, + findRouteError, + { + noThrottle: true, + rewritePayload: (payload) => { + const { route } = payload.data + // Re-write query to achieve otp-v1 compatibility + route.id = route.gtfsId + route.agency.id = route.agency.gtfsId + route.routeBikesAllowed = route.bikesAllowed + // Ensure this isn't overwritten + route.v2 = true + + const routePatterns = {} + route.patterns.forEach((pattern) => { + routePatterns[pattern.id] = { + ...pattern, + desc: pattern.name, + geometry: pattern.patternGeometry + } + }) + route.patterns = routePatterns - rewritePayload: (payload) => { - // convert pattern array to ID-mapped object - const patterns = {} - payload.forEach((ptn) => { - patterns[ptn.id] = ptn - }) + return route + } + } + ) + ) + } - return { - patterns, - routeId: params.routeId + return dispatch( + createQueryAction( + `index/routes/${params.routeId}`, + findRouteResponse, + findRouteError, + { + noThrottle: true, + postprocess: (payload, dispatch) => { + // load patterns + dispatch(findPatternsForRoute({ routeId: params.routeId })) + } } + ) + ) + } +} + +export function findPatternsForRoute(params) { + return function (dispatch, getState) { + const state = getState() + const route = state?.otp?.transitIndex?.routes?.[params.routeId] + // If a route was fetched from GraphQL, it already has patterns + if (route && route.v2) { + if (!route.patterns) { + return dispatch(findRoute(params)) } + return } - ) + + return dispatch( + createQueryAction( + `index/routes/${params.routeId}/patterns?includeGeometry=true`, + findPatternsForRouteResponse, + findPatternsForRouteError, + { + noThrottle: true, + postprocess: (payload, dispatch) => { + // load geometry for each pattern + payload.forEach((ptn) => { + // Some OTP instances don't support includeGeometry. + // We need to manually fetch geometry in these cases. + if (!ptn.geometry) { + dispatch( + findGeometryForPattern({ + patternId: ptn.id, + routeId: params.routeId + }) + ) + } + }) + }, + + rewritePayload: (payload) => { + // convert pattern array to ID-mapped object + const patterns = {} + payload.forEach((ptn) => { + patterns[ptn.id] = ptn + }) + + return { + patterns, + routeId: params.routeId + } + } + } + ) + ) + } } // Geometry for Pattern lookup query @@ -732,20 +901,31 @@ const findGeometryForPatternError = createAction( ) export function findGeometryForPattern(params) { - return createQueryAction( - `index/patterns/${params.patternId}/geometry`, - findGeometryForPatternResponse, - findGeometryForPatternError, - { - rewritePayload: (payload) => { - return { - geometry: payload, - patternId: params.patternId, - routeId: params.routeId - } - } + return function (dispatch, getState) { + const state = getState() + const route = state?.otp?.transitIndex?.routes?.[params.routeId] + // If a route was fetched from GraphQL, it already has geometry + if (route && route.v2) { + return } - ) + + return dispatch( + createQueryAction( + `index/patterns/${params.patternId}/geometry`, + findGeometryForPatternResponse, + findGeometryForPatternError, + { + rewritePayload: (payload) => { + return { + geometry: payload, + patternId: params.patternId, + routeId: params.routeId + } + } + } + ) + ) + } } // Stops for pattern query @@ -758,21 +938,31 @@ export const findStopsForPatternError = createAction( ) export function findStopsForPattern(params) { - return createQueryAction( - `index/patterns/${params.patternId}/stops`, - findStopsForPatternResponse, - findStopsForPatternError, - { - noThrottle: true, - rewritePayload: (payload) => { - return { - patternId: params.patternId, - routeId: params.routeId, - stops: payload - } - } + return function (dispatch, getState) { + const state = getState() + const route = state?.otp?.transitIndex?.routes?.[params.routeId] + // If a route was fetched from GraphQL, it already has patterns + if (route && route.v2) { + return } - ) + return dispatch( + createQueryAction( + `index/patterns/${params.patternId}/stops`, + findStopsForPatternResponse, + findStopsForPatternError, + { + noThrottle: true, + rewritePayload: (payload) => { + return { + patternId: params.patternId, + routeId: params.routeId, + stops: payload + } + } + } + ) + ) + } } // TNC ETA estimate lookup query @@ -924,19 +1114,29 @@ const receivedVehiclePositionsError = createAction( ) export function getVehiclePositionsForRoute(routeId) { - return createQueryAction( - `index/routes/${routeId}/vehicles`, - receivedVehiclePositions, - receivedVehiclePositionsError, - { - rewritePayload: (payload) => { - return { - routeId: routeId, - vehicles: payload - } - } + return function (dispatch, getState) { + const state = getState() + const route = state?.otp?.transitIndex?.routes?.[routeId] + // If a route was fetched from GraphQL, it doens't support realtime vehicle positions + if (route && route.v2) { + return } - ) + return dispatch( + createQueryAction( + `index/routes/${routeId}/vehicles`, + receivedVehiclePositions, + receivedVehiclePositionsError, + { + rewritePayload: (payload) => { + return { + routeId: routeId, + vehicles: payload + } + } + } + ) + ) + } } const throttledUrls = {} @@ -1006,20 +1206,7 @@ function createQueryAction( return async function (dispatch, getState) { const state = getState() const { config } = state.otp - let url - if ( - options.serviceId && - config.alternateTransitIndex && - config.alternateTransitIndex.services.includes(options.serviceId) - ) { - console.log('Using alt service for ' + options.serviceId) - url = config.alternateTransitIndex.apiRoot + endpoint - } else { - const api = config.api - url = `${api?.host}${api?.port ? ':' + api.port : ''}${ - api?.path - }/${endpoint}` - } + const url = makeApiUrl(config, endpoint, options) if (!options.noThrottle) { // Don't make a request to a URL that has already seen the same request @@ -1029,7 +1216,10 @@ function createQueryAction( let payload try { - const response = await fetch(url, getOtpFetchOptions(state)) + const response = await fetch(url, { + ...getOtpFetchOptions(state), + ...options.fetchOptions + }) if (response.status >= 400) { // If a second endpoint is configured, try that before failing if (!!config.api_v2 && !options.v2) { @@ -1050,7 +1240,9 @@ function createQueryAction( } if (typeof options.rewritePayload === 'function') { - dispatch(responseAction(options.rewritePayload(payload))) + dispatch( + responseAction(options.rewritePayload(payload, dispatch, getState)) + ) } else { dispatch(responseAction(payload)) } @@ -1085,24 +1277,29 @@ function makeApiUrl(config, endpoint, options) { return url } -// TODO: Determine how we might be able to use GraphQL with the alternative -// transit index. Currently this is not easily possible because the alternative -// transit index does not have support for GraphQL and handling both Rest and -// GraphQL queries could introduce potential difficulties for maintainers. -// function createGraphQLQueryAction (query, variables, responseAction, errorAction, options) { -// const endpoint = `index/graphql` -// const fetchOptions = { -// method: 'POST', -// body: JSON.stringify({ query, variables }), -// headers: { 'Content-Type': 'application/json' } -// } -// return createQueryAction( -// endpoint, -// responseAction, -// errorAction, -// { ...options, fetchOptions } -// ) -// } +/** + * Generic helper for crafting GraphQL queries. + */ +function createGraphQLQueryAction( + query, + variables, + responseAction, + errorAction, + options +) { + const endpoint = 'index/graphql' + const fetchOptions = { + body: JSON.stringify({ query, variables }), + headers: { 'Content-Type': 'application/json' }, + method: 'POST' + } + return createQueryAction(endpoint, responseAction, errorAction, { + ...options, + fetchOptions, + // For now, all GraphQL requests are OTP-V2 + v2: true + }) +} /** * Update the browser/URL history with new parameters diff --git a/lib/components/map/connected-stops-overlay.js b/lib/components/map/connected-stops-overlay.js index 643f08c7c..30e89e3f1 100644 --- a/lib/components/map/connected-stops-overlay.js +++ b/lib/components/map/connected-stops-overlay.js @@ -1,5 +1,5 @@ -import StopsOverlay from '@opentripplanner/stops-overlay' import { connect } from 'react-redux' +import StopsOverlay from '@opentripplanner/stops-overlay' import { findStopsWithinBBox } from '../../actions/api' @@ -15,7 +15,10 @@ const mapStateToProps = (state, ownProps) => { // If a pattern is being shown, show only the pattern's stops and show them large if (viewedRoute?.patternId && state.otp.transitIndex.routes) { - stops = state.otp.transitIndex.routes[viewedRoute.routeId]?.patterns?.[viewedRoute.patternId].stops + stops = + state.otp.transitIndex.routes[viewedRoute.routeId]?.patterns?.[ + viewedRoute.patternId + ]?.stops minZoom = 2 } From d57202b84ef79958d2ab1fa22509a99706d46801 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 29 Nov 2021 18:27:04 +0000 Subject: [PATCH 058/270] refactor(api): move otp-1 query rewrite to graphql call --- lib/actions/api.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index c33d7cbb1..3e93f9cd6 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -663,7 +663,7 @@ export function findRoutesV2() { return createGraphQLQueryAction( `{ routes { - gtfsId + id: gtfsId shortName longName mode @@ -695,7 +695,7 @@ export function findRoutesV2() { const routes = {} payload?.data?.routes?.forEach((rte) => { // Strip out the agency part of the route ID which can sometimes differ - let strippedRouteId = rte.gtfsId.split(':') + let strippedRouteId = rte.id.split(':') strippedRouteId = strippedRouteId[strippedRouteId.length - 1] if (existingRouteIds.get(strippedRouteId)) { return @@ -705,13 +705,12 @@ export function findRoutesV2() { // OTP-2 includes the feed ID in the agency ID. Stripping it // matches OTP-1 behavior rte.agencyId = rte.agency.gtfsId.split(':')[0] - rte.id = rte.gtfsId // Mark this route as a v2 route so that future requests relating to it // will be made to the v2 endpoint rte.v2 = true - routes[rte.gtfsId] = rte + routes[rte.id] = rte }) return routes }, @@ -746,11 +745,11 @@ export function findRoute(params) { createGraphQLQueryAction( // TODO: cleanup stops query `{ - route(id: "${route.gtfsId}") { - gtfsId + route(id: "${route.id}") { + id: gtfsId desc agency { - gtfsId + id: gtfsId name url timezone @@ -762,6 +761,7 @@ export function findRoute(params) { color textColor bikesAllowed + routeBikesAllowed: bikesAllowed patterns { id @@ -773,7 +773,7 @@ export function findRoute(params) { stops { code - id + id: gtfsId lat lon name @@ -797,10 +797,6 @@ export function findRoute(params) { noThrottle: true, rewritePayload: (payload) => { const { route } = payload.data - // Re-write query to achieve otp-v1 compatibility - route.id = route.gtfsId - route.agency.id = route.agency.gtfsId - route.routeBikesAllowed = route.bikesAllowed // Ensure this isn't overwritten route.v2 = true From 105307c5c77318f38c64d562b80419dbe19d4a1a Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 29 Nov 2021 18:45:44 +0000 Subject: [PATCH 059/270] refactor(flex-indicator): avoid phone number getting cut off --- lib/components/narrative/default/flex-indicator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/narrative/default/flex-indicator.tsx b/lib/components/narrative/default/flex-indicator.tsx index 678495990..eed787cf6 100644 --- a/lib/components/narrative/default/flex-indicator.tsx +++ b/lib/components/narrative/default/flex-indicator.tsx @@ -67,7 +67,7 @@ const FlexIndicatorWrapper = styled.div<{ shrink: boolean }>` grid-column: 3 / span 2; grid-row: 2; text-overflow: ellipsis; - overflow-y: hidden; + overflow-y: clip; } /* Barber pole at left */ From 3b8ef6461bd0fbc955c402d87b4bb29d15a9ab89 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Tue, 30 Nov 2021 15:26:23 +0000 Subject: [PATCH 060/270] refactor(stop-viewer): support flex stops --- i18n/en-US.yml | 1 + lib/actions/api.js | 40 +++++++++++++++++++++++++++ lib/components/viewers/stop-viewer.js | 5 ++++ 3 files changed, 46 insertions(+) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 913b8de83..3302d3c95 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -436,6 +436,7 @@ components: StopViewer: displayStopId: "Stop ID: {stopId}" header: Stop Viewer + flexStop: This is a flex stop. Vehicles will drop off and pick up passengers in this flexible zone by request. You may have to call ahead for service in this area. loadingText: Loading Stop... noStopsFound: No stop times found for date. planTrip: "Plan a trip:" diff --git a/lib/actions/api.js b/lib/actions/api.js index 3e93f9cd6..2fc2cad7d 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -421,6 +421,39 @@ export function fetchStopInfo(stop) { } } } +export function fetchGraphQLOnlyStopInfo(stopId) { + if (!stopId) return + return createGraphQLQueryAction( + `{ + stop(id: "${stopId}") { + id: gtfsId + lat + lon + locationType + name + wheelchairBoarding + zoneId + geometries { + geoJson + polylines { + points + length + } + } + } + } + `, + {}, + findStopResponse, + findStopError, + { + noThrottle: true, + rewritePayload: (payload) => { + return payload.data.stop + } + } + ) +} export const findNearbyAmenitiesResponse = createAction( 'FIND_NEARBY_AMENITIES_RESPONSE' @@ -621,6 +654,13 @@ export function findStopTimesForStop(params) { findStopTimesForStopError, { noThrottle: true, + postprocess: (payload, dispatch) => { + // if the payload doesn't contain any stoptimes but does contain routes, + // try grabbing stop data from graphql + if (payload.length === 0) { + dispatch(fetchGraphQLOnlyStopInfo(stopId)) + } + }, rewritePayload: (stopTimes) => { return { stopId, diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 82311d312..9d01f0adc 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -285,6 +285,11 @@ class StopViewer extends Component {
    ) + // If geometries are available and stop times are not, it is a strong indication + // that the stop is a flex stop. + if (stopData?.geometries) { + contents = (
    ) + } } else if (scheduleView) { contents = ( Date: Wed, 1 Dec 2021 11:47:28 +0000 Subject: [PATCH 061/270] refactor(actions/api): include main route color in graphql stop response --- lib/actions/api.js | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index 2fc2cad7d..b468c16a7 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -16,6 +16,7 @@ import { getStopViewerConfig, queryIsValid } from '../util/state' import { ItineraryView, setItineraryView } from './ui' import { rememberPlace, zoomToStop } from './map' +import { setMapZoom } from './config' if (typeof fetch === 'undefined') require('isomorphic-fetch') @@ -435,10 +436,9 @@ export function fetchGraphQLOnlyStopInfo(stopId) { zoneId geometries { geoJson - polylines { - points - length - } + } + routes { + color } } } @@ -448,8 +448,15 @@ export function fetchGraphQLOnlyStopInfo(stopId) { findStopError, { noThrottle: true, + postprocess: (payload, dispatch) => { + if (payload?.data?.stop?.geometries) { + dispatch(setMapZoom(10)) + } + }, rewritePayload: (payload) => { - return payload.data.stop + const { stop } = payload.data + const color = stop.routes?.length > 0 && `#${stop.routes[0].color}` + return { ...stop, color } } } ) @@ -783,7 +790,6 @@ export function findRoute(params) { if (route?.v2) { return dispatch( createGraphQLQueryAction( - // TODO: cleanup stops query `{ route(id: "${route.id}") { id: gtfsId @@ -820,10 +826,9 @@ export function findRoute(params) { locationType geometries { geoJson - polylines { - points - length - } + } + routes { + color } } } @@ -842,6 +847,12 @@ export function findRoute(params) { const routePatterns = {} route.patterns.forEach((pattern) => { + pattern.stops = pattern.stops.map((stop) => { + const color = + stop.routes?.length > 0 && `#${stop.routes[0].color}` + if (stop.routes) delete stop.routes + return { ...stop, color } + }) routePatterns[pattern.id] = { ...pattern, desc: pattern.name, From e8baf73da879e63ca1a0bff5e5960b69d6bfeb63 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 2 Dec 2021 12:04:27 +0000 Subject: [PATCH 062/270] chore(reducers/create-otp-reducer): run autoformatter --- lib/reducers/create-otp-reducer.js | 324 +++++++++++++++++------------ 1 file changed, 190 insertions(+), 134 deletions(-) diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 914a05a2e..5dfd53c7e 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -1,22 +1,20 @@ +/* eslint-disable no-case-declarations */ import clone from 'clone' -import update from 'immutability-helper' +import coreUtils from '@opentripplanner/core-utils' import isEqual from 'lodash.isequal' import objectPath from 'object-path' -import coreUtils from '@opentripplanner/core-utils' +import update from 'immutability-helper' +import { FETCH_STATUS } from '../util/constants' +import { getTimestamp } from '../util/state' +import { isBatchRoutingEnabled } from '../util/itinerary' import { MainPanelContent, MobileScreens } from '../actions/ui' -import {FETCH_STATUS} from '../util/constants' -import {getTimestamp} from '../util/state' -import {isBatchRoutingEnabled} from '../util/itinerary' const { getTransitModes, isTransit } = coreUtils.itinerary const { matchLatLon } = coreUtils.map const { filterProfileOptions } = coreUtils.profile -const { - ensureSingleAccessMode, - getDefaultQuery, - getTripOptionsFromQuery -} = coreUtils.query +const { ensureSingleAccessMode, getDefaultQuery, getTripOptionsFromQuery } = + coreUtils.query const { getItem, removeItem, storeItem } = coreUtils.storage const { getUserTimezone } = coreUtils.time @@ -31,8 +29,8 @@ const MAX_RECENT_STORAGE = 5 * TODO: maybe it's a better idea to move all of this to a script that can do * JSON Schema validation and other stuff. */ -function validateInitialState (initialState) { - const {config} = initialState +function validateInitialState(initialState) { + const { config } = initialState const errors = [] @@ -45,17 +43,18 @@ function validateInitialState (initialState) { objectPath.get(config, 'persistence.strategy') === 'localStorage' && objectPath.get(config, 'geocoder.type') === 'ARCGIS' ) { - errors.push(new Error('Local Storage persistence and ARCGIS geocoder cannot be enabled at the same time!')) + errors.push( + new Error( + 'Local Storage persistence and ARCGIS geocoder cannot be enabled at the same time!' + ) + ) } if (errors.length > 0) { throw new Error( - errors.reduce( - (message, error) => { - return `${message}\n- ${error.message}` - }, - 'Encountered the following configuration errors:' - ) + errors.reduce((message, error) => { + return `${message}\n- ${error.message}` + }, 'Encountered the following configuration errors:') ) } } @@ -64,7 +63,7 @@ function validateInitialState (initialState) { * Create the initial state of otp-react-redux using user-provided config, any * items in localStorage and a few defaults. */ -export function getInitialState (userDefinedConfig) { +export function getInitialState(userDefinedConfig) { const defaultConfig = { autoPlan: { default: 'ONE_LOCATION_CHANGED', @@ -131,14 +130,16 @@ export function getInitialState (userDefinedConfig) { // Recent trip plan searches (excluding time/date parameters to avoid complexity). const recentSearches = getItem('recentSearches', []) // Filter valid locations found into locations list. - const locations = [home, work].filter(p => p) + const locations = [home, work].filter((p) => p) // TODO: parse and merge URL query params w/ default query // populate query by merging any provided query params w/ the default params const currentQuery = Object.assign(defaults, userDefinedConfig.initialQuery) // Add configurable locations to home and work locations if (config.locations) { - locations.push(...config.locations.map(l => ({ ...l, type: 'suggested' }))) + locations.push( + ...config.locations.map((l) => ({ ...l, type: 'suggested' })) + ) } // Check for alternative routerId in session storage. This is generally used // for testing single GTFS feed OTP graphs that are deployed to feed-specific @@ -159,7 +160,7 @@ export function getInitialState (userDefinedConfig) { // If 'TRANSIT' is included in the mode list, replace it with individual modes if (queryModes.includes('TRANSIT')) { // Isolate the non-transit modes in queryModes - queryModes = queryModes.filter(m => !isTransit(m)) + queryModes = queryModes.filter((m) => !isTransit(m)) // Add all possible transit modes queryModes = queryModes.concat(getTransitModes(config)) // Stringify and set as OTP 'mode' query param @@ -256,7 +257,7 @@ export function getInitialState (userDefinedConfig) { } } -function createOtpReducer (config) { +function createOtpReducer(config) { const initialState = getInitialState(config) // validate the initial state validateInitialState(initialState) @@ -268,35 +269,32 @@ function createOtpReducer (config) { const activeSearch = state.searches[searchId] switch (action.type) { case 'ROUTING_REQUEST': - const { - activeItinerary, - pending, - updateSearchInReducer - } = action.payload + const { activeItinerary, pending, updateSearchInReducer } = + action.payload const searchUpdate = updateSearchInReducer ? { - activeItinerary: { $set: activeItinerary }, - activeLeg: { $set: null }, - activeStep: { $set: null }, - pending: { $set: pending }, - // FIXME: get query from action payload? - query: { $set: clone(state.currentQuery) }, - // omit requests reset to make sure requests can be added to this - // search - timestamp: { $set: getTimestamp() } - } - : { - $set: { - activeItinerary, - activeLeg: null, - activeStep: null, - pending, + activeItinerary: { $set: activeItinerary }, + activeLeg: { $set: null }, + activeStep: { $set: null }, + pending: { $set: pending }, // FIXME: get query from action payload? - query: clone(state.currentQuery), - response: [], - timestamp: getTimestamp() + query: { $set: clone(state.currentQuery) }, + // omit requests reset to make sure requests can be added to this + // search + timestamp: { $set: getTimestamp() } + } + : { + $set: { + activeItinerary, + activeLeg: null, + activeStep: null, + pending, + // FIXME: get query from action payload? + query: clone(state.currentQuery), + response: [], + timestamp: getTimestamp() + } } - } return update(state, { activeSearchId: { $set: searchId }, searches: { [searchId]: searchUpdate } @@ -307,19 +305,22 @@ function createOtpReducer (config) { [searchId]: { pending: { $set: activeSearch.pending - 1 }, response: { - $push: [{ - error: action.payload.error, - requestId, - url: action.payload.url - }] + $push: [ + { + error: action.payload.error, + requestId, + url: action.payload.url + } + ] } } } }) case 'ROUTING_RESPONSE': - const response = (state.currentQuery.routingType === 'PROFILE') - ? filterProfileOptions(action.payload.response) - : action.payload.response + const response = + state.currentQuery.routingType === 'PROFILE' + ? filterProfileOptions(action.payload.response) + : action.payload.response response.requestId = requestId return update(state, { searches: { @@ -350,8 +351,8 @@ function createOtpReducer (config) { }) case 'SET_ACTIVE_ITINERARIES': const responseUpdate = {} - Object.entries(action.payload.assignedItinerariesByResponse) - .forEach(([ responseIdx, responsePlanItineraries ]) => { + Object.entries(action.payload.assignedItinerariesByResponse).forEach( + ([responseIdx, responsePlanItineraries]) => { responseUpdate[responseIdx] = { plan: { itineraries: { @@ -359,7 +360,8 @@ function createOtpReducer (config) { } } } - }) + } + ) return update(state, { searches: { [searchId]: { @@ -532,18 +534,26 @@ function createOtpReducer (config) { if (action.payload.indexOf('recent') !== -1) { const recentPlaces = clone(state.user.recentPlaces) // Remove recent from list of recent places - const removeIndex = recentPlaces.findIndex(l => l.id === action.payload) + const removeIndex = recentPlaces.findIndex( + (l) => l.id === action.payload + ) recentPlaces.splice(removeIndex, 1) storeItem('recent', recentPlaces) return removeIndex !== -1 - ? update(state, { user: { recentPlaces: { $splice: [[removeIndex, 1]] } } }) + ? update(state, { + user: { recentPlaces: { $splice: [[removeIndex, 1]] } } + }) : state } else { const locations = clone(state.user.locations) - const removeIndex = locations.findIndex(l => l.id === action.payload) + const removeIndex = locations.findIndex( + (l) => l.id === action.payload + ) removeItem(action.payload) return removeIndex !== -1 - ? update(state, { user: { locations: { $splice: [[removeIndex, 1]] } } }) + ? update(state, { + user: { locations: { $splice: [[removeIndex, 1]] } } + }) : state } } @@ -552,23 +562,29 @@ function createOtpReducer (config) { switch (type) { case 'recent': { const recentPlaces = clone(state.user.recentPlaces) - const index = recentPlaces.findIndex(l => matchLatLon(l, location)) + const index = recentPlaces.findIndex((l) => + matchLatLon(l, location) + ) // Replace recent place if duplicate found or add to list. if (index !== -1) recentPlaces.splice(index, 1, location) else recentPlaces.push(location) - const sortedPlaces = recentPlaces.sort((a, b) => b.timestamp - a.timestamp) + const sortedPlaces = recentPlaces.sort( + (a, b) => b.timestamp - a.timestamp + ) // Only keep up to 5 recent locations // FIXME: Check for duplicates if (recentPlaces.length >= MAX_RECENT_STORAGE) { sortedPlaces.splice(MAX_RECENT_STORAGE) } storeItem('recent', recentPlaces) - return update(state, { user: { recentPlaces: { $set: sortedPlaces } } }) + return update(state, { + user: { recentPlaces: { $set: sortedPlaces } } + }) } default: { const locations = clone(state.user.locations) // Determine if location type (e.g., home or work) already exists in list - const index = locations.findIndex(l => l.type === type) + const index = locations.findIndex((l) => l.type === type) if (index !== -1) locations.splice(index, 1, location) else locations.push(location) storeItem(type, location) @@ -580,11 +596,15 @@ function createOtpReducer (config) { // Payload is the stop ID. const favoriteStops = clone(state.user.favoriteStops) // Remove stop from favorites - const removeIndex = favoriteStops.findIndex(l => l.id === action.payload) + const removeIndex = favoriteStops.findIndex( + (l) => l.id === action.payload + ) favoriteStops.splice(removeIndex, 1) storeItem('favoriteStops', favoriteStops) return removeIndex !== -1 - ? update(state, { user: { favoriteStops: { $splice: [[removeIndex, 1]] } } }) + ? update(state, { + user: { favoriteStops: { $splice: [[removeIndex, 1]] } } + }) : state } case 'REMEMBER_STOP': { @@ -601,10 +621,12 @@ function createOtpReducer (config) { } const favoriteStops = clone(state.user.favoriteStops) if (favoriteStops.length >= MAX_RECENT_STORAGE) { - window.alert(`Cannot save more than ${MAX_RECENT_STORAGE} stops. Remove one before adding more.`) + window.alert( + `Cannot save more than ${MAX_RECENT_STORAGE} stops. Remove one before adding more.` + ) return state } - const index = favoriteStops.findIndex(s => s.id === stop.id) + const index = favoriteStops.findIndex((s) => s.id === stop.id) // Do nothing if duplicate stop found. if (index !== -1) { console.warn(`Stop with id ${stop.id} already exists in favorites.`) @@ -613,15 +635,19 @@ function createOtpReducer (config) { favoriteStops.unshift(stop) } storeItem('favoriteStops', favoriteStops) - return update(state, { user: { favoriteStops: { $set: favoriteStops } } }) + return update(state, { + user: { favoriteStops: { $set: favoriteStops } } + }) } // FIXME: set up action case 'TOGGLE_ADVANCED_OPTIONS': storeItem('expandAdvanced', action.payload) if (!action.payload) removeItem('expandAdvanced') - return update(state, { user: { - expandAdvanced: { $set: action.payload } - } }) + return update(state, { + user: { + expandAdvanced: { $set: action.payload } + } + }) case 'TOGGLE_TRACKING': { storeItem('trackRecent', action.payload) let recentPlaces = clone(state.user.recentPlaces) @@ -633,19 +659,25 @@ function createOtpReducer (config) { removeItem('recent') removeItem('recentSearches') } - return update(state, { user: { - recentPlaces: { $set: recentPlaces }, - recentSearches: { $set: recentSearches }, - trackRecent: { $set: action.payload } - } }) + return update(state, { + user: { + recentPlaces: { $set: recentPlaces }, + recentSearches: { $set: recentSearches }, + trackRecent: { $set: action.payload } + } + }) } case 'REMEMBER_SEARCH': const searches = clone(state.user.recentSearches) - const duplicateIndex = searches.findIndex(s => isEqual(s.query, action.payload.query)) + const duplicateIndex = searches.findIndex((s) => + isEqual(s.query, action.payload.query) + ) // Overwrite duplicate search (so that new timestamp is stored). if (duplicateIndex !== -1) searches[duplicateIndex] = action.payload else searches.unshift(action.payload) - const sortedSearches = searches.sort((a, b) => b.timestamp - a.timestamp) + const sortedSearches = searches.sort( + (a, b) => b.timestamp - a.timestamp + ) // Ensure recent searches do not extend beyond MAX_RECENT_STORAGE if (sortedSearches.length >= MAX_RECENT_STORAGE) { sortedSearches.splice(MAX_RECENT_STORAGE) @@ -654,12 +686,14 @@ function createOtpReducer (config) { return update(state, { user: { searches: { $set: sortedSearches } } }) case 'FORGET_SEARCH': { const recentSearches = clone(state.user.recentSearches) - const index = recentSearches.findIndex(l => l.id === action.payload) + const index = recentSearches.findIndex((l) => l.id === action.payload) // Remove item from list of recent searches recentSearches.splice(index, 1) storeItem('recentSearches', recentSearches) return index !== -1 - ? update(state, { user: { recentSearches: { $splice: [[index, 1]] } } }) + ? update(state, { + user: { recentSearches: { $splice: [[index, 1]] } } + }) : state } case 'SET_AUTOPLAN': @@ -686,12 +720,13 @@ function createOtpReducer (config) { case 'SET_ROUTER_ID': const routerId = action.payload // Store original path value in originalPath variable. - const originalPath = config.api.originalPath || config.api.path || '/otp/routers/default' + const originalPath = + config.api.originalPath || config.api.path || '/otp/routers/default' const path = routerId ? `/otp/routers/${routerId}` - // If routerId is null, revert to the original config's API path (or - // the standard path if that is not found). - : originalPath + : // If routerId is null, revert to the original config's API path (or + // the standard path if that is not found). + originalPath // Store routerId in session storage (persists through page reloads but // not when a new tab/window is opened). if (routerId) window.sessionStorage.setItem('routerId', routerId) @@ -742,13 +777,15 @@ function createOtpReducer (config) { }) case 'NEARBY_STOPS_RESPONSE': - const {focusStopId, stops} = action.payload + const { focusStopId, stops } = action.payload const stopLookup = {} - stops.forEach(s => { + stops.forEach((s) => { stopLookup[s.id] = s }) - const stopIds = stops.map(stop => stop.id).filter(id => focusStopId !== id) + const stopIds = stops + .map((stop) => stop.id) + .filter((id) => focusStopId !== id) if (!focusStopId) { return update(state, { @@ -762,20 +799,24 @@ function createOtpReducer (config) { // losing the child stops field we're adding here. delete stopLookup[focusStopId] return update(state, { - transitIndex: { stops: { - // We'll keep all of the main child stop objects in the stops lookup (to match how the state currently looks) - $merge: stopLookup, // going to include the parentStop - // For the parent stop, we want to add the nearby stops as a new child stops field - [focusStopId]: {$merge: {nearbyStops: stopIds}} - } } + transitIndex: { + stops: { + // We'll keep all of the main child stop objects in the stops lookup (to match how the state currently looks) + $merge: stopLookup, // going to include the parentStop + // For the parent stop, we want to add the nearby stops as a new child stops field + [focusStopId]: { $merge: { nearbyStops: stopIds } } + } + } }) } case 'FIND_NEARBY_AMENITIES_RESPONSE': const { stopId, ...amenities } = action.payload return update(state, { - transitIndex: { stops: { - [stopId]: {$merge: amenities} - } } + transitIndex: { + stops: { + [stopId]: { $merge: amenities } + } + } }) case 'STOPS_WITHIN_BBOX_RESPONSE': return update(state, { @@ -826,10 +867,12 @@ function createOtpReducer (config) { case 'SET_VIEWED_STOP': if (action.payload) { // If setting to a stop (not null), also set main panel. - return update(state, { ui: { - mainPanelContent: { $set: MainPanelContent.STOP_VIEWER }, - viewedStop: { $set: action.payload } - } }) + return update(state, { + ui: { + mainPanelContent: { $set: MainPanelContent.STOP_VIEWER }, + viewedStop: { $set: action.payload } + } + }) } else { // Otherwise, just replace viewed stop with null return update(state, { ui: { viewedStop: { $set: action.payload } } }) @@ -843,18 +886,24 @@ function createOtpReducer (config) { return update(state, { ui: { viewedTrip: { $set: null } } }) case 'SET_HOVERED_STOP': - return update(state, { ui: { highlightedStop: { $set: action.payload } } }) + return update(state, { + ui: { highlightedStop: { $set: action.payload } } + }) case 'SET_VIEWED_ROUTE': if (action.payload) { // If setting to a route (not null), also set main panel. - return update(state, { ui: { - mainPanelContent: { $set: MainPanelContent.ROUTE_VIEWER }, - viewedRoute: { $set: action.payload } - } }) + return update(state, { + ui: { + mainPanelContent: { $set: MainPanelContent.ROUTE_VIEWER }, + viewedRoute: { $set: action.payload } + } + }) } else { // Otherwise, just replace viewed route with null - return update(state, { ui: { viewedRoute: { $set: action.payload } } }) + return update(state, { + ui: { viewedRoute: { $set: action.payload } } + }) } case 'FIND_STOP_RESPONSE': return update(state, { @@ -934,7 +983,9 @@ function createOtpReducer (config) { }) case 'TOGGLE_AUTO_REFRESH': storeItem('autoRefreshStopTimes', action.payload) - return update(state, { user: { autoRefreshStopTimes: { $set: action.payload } } }) + return update(state, { + user: { autoRefreshStopTimes: { $set: action.payload } } + }) case 'FIND_ROUTES_RESPONSE': // If routes is undefined, initialize it w/ the full payload @@ -951,7 +1002,9 @@ function createOtpReducer (config) { // If routes is undefined, initialize it w/ this route only if (!state.transitIndex.routes) { return update(state, { - transitIndex: { routes: { $set: { [action.payload.id]: action.payload } } } + transitIndex: { + routes: { $set: { [action.payload.id]: action.payload } } + } }) } // Otherwise, overwrite only this route @@ -1008,19 +1061,20 @@ function createOtpReducer (config) { return update(state, { tnc: { etaEstimates: { - [action.payload.from]: fromData => { + [action.payload.from]: (fromData) => { fromData = Object.assign({}, fromData) const estimates = action.payload.estimates || [] - estimates.forEach(estimate => { + estimates.forEach((estimate) => { if (!fromData[estimate.company]) { fromData[estimate.company] = {} } - fromData[estimate.company][estimate.productId] = Object.assign( - { - estimateTimestamp: new Date() - }, - estimate - ) + fromData[estimate.company][estimate.productId] = + Object.assign( + { + estimateTimestamp: new Date() + }, + estimate + ) }) return fromData } @@ -1031,7 +1085,7 @@ function createOtpReducer (config) { return update(state, { tnc: { rideEstimates: { - [action.payload.from]: fromData => { + [action.payload.from]: (fromData) => { fromData = Object.assign({}, fromData) const { company, rideEstimate, to } = action.payload if (!rideEstimate) { @@ -1078,23 +1132,25 @@ function createOtpReducer (config) { case 'UPDATE_OVERLAY_VISIBILITY': const mapOverlays = clone(state.config.map.overlays) for (const key in action.payload) { - const overlay = mapOverlays.find(o => o.name === key) + const overlay = mapOverlays.find((o) => o.name === key) overlay.visible = action.payload[key] } - return update(state, - { config: { map: { overlays: { $set: mapOverlays } } } } - ) + return update(state, { + config: { map: { overlays: { $set: mapOverlays } } } + }) case 'UPDATE_ITINERARY_FILTER': - return update(state, - { filter: { $set: action.payload } } - ) + return update(state, { filter: { $set: action.payload } }) case 'SET_PREVIOUS_ITINERARY_VIEW': - return update(state, { ui: { previousItineraryView: { $set: action.payload } } }) + return update(state, { + ui: { previousItineraryView: { $set: action.payload } } + }) case 'UPDATE_LOCALE': - return update(state, { ui: { - locale: { $set: action.payload.locale }, - localizedMessages: { $set: action.payload.messages } - }}) + return update(state, { + ui: { + locale: { $set: action.payload.locale }, + localizedMessages: { $set: action.payload.messages } + } + }) case 'UPDATE_ROUTE_VIEWER_FILTER': return update(state, { From 3e1e3a9c9145d1b2af92876d7792d85e8f0ed0d1 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 2 Dec 2021 14:32:07 +0000 Subject: [PATCH 063/270] refactor(connected-stops-overlay): show flex stops when viewing flex rotue --- lib/actions/api.js | 4 +-- lib/components/map/connected-stops-overlay.js | 30 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index b468c16a7..cb29e7b74 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -1164,8 +1164,8 @@ export function getVehiclePositionsForRoute(routeId) { return function (dispatch, getState) { const state = getState() const route = state?.otp?.transitIndex?.routes?.[routeId] - // If a route was fetched from GraphQL, it doens't support realtime vehicle positions - if (route && route.v2) { + // If a route was fetched from GraphQL, it doesn't support realtime vehicle positions + if (route?.v2) { return } return dispatch( diff --git a/lib/components/map/connected-stops-overlay.js b/lib/components/map/connected-stops-overlay.js index 30e89e3f1..b5fa86198 100644 --- a/lib/components/map/connected-stops-overlay.js +++ b/lib/components/map/connected-stops-overlay.js @@ -9,17 +9,33 @@ import StopMarker from './connected-stop-marker' const mapStateToProps = (state, ownProps) => { const { viewedRoute } = state.otp.ui + const { routes } = state.otp.transitIndex let { stops } = state.otp.overlay.transit let minZoom = 15 - // If a pattern is being shown, show only the pattern's stops and show them large - if (viewedRoute?.patternId && state.otp.transitIndex.routes) { - stops = - state.otp.transitIndex.routes[viewedRoute.routeId]?.patterns?.[ - viewedRoute.patternId - ]?.stops - minZoom = 2 + // There are a number of cases when stops within route objects should be shown + // although there are no stops from the stops index active + if (routes) { + // All cases of stops being force-shown only apply when a route is being actively viewed + const route = routes?.[viewedRoute?.routeId] + if (route?.patterns) { + // If the pattern viewer is active, stops along that pattern should be shown + let viewedPattern = viewedRoute?.patternId + // If a flex route is being shown but the pattern viewer is not active, then the + // stops of the first pattern of the route should be shown + // This will ensure that the flex zone stops are shown. + // TODO: if the time to show flex stops changes, this is where that change + // should be made. + if (!viewedPattern && route.v2) { + viewedPattern = Object.keys(route?.patterns)?.[0] + } + + // Override the stop index so that only relevant stops are shown + stops = route.patterns?.[viewedPattern]?.stops + // Override the minimum zoom so that the stops appear even if zoomed out + minZoom = 2 + } } return { From de005fb1c5531e0d54978365734edc2dd20c4fc7 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 2 Dec 2021 15:48:21 +0000 Subject: [PATCH 064/270] refactor(trip-viewer): run autoformatter --- lib/components/viewers/trip-viewer.js | 111 +++++++++++++------------- 1 file changed, 54 insertions(+), 57 deletions(-) diff --git a/lib/components/viewers/trip-viewer.js b/lib/components/viewers/trip-viewer.js index 1affef978..8e10b2f9a 100644 --- a/lib/components/viewers/trip-viewer.js +++ b/lib/components/viewers/trip-viewer.js @@ -1,25 +1,23 @@ // FIXME: Remove this eslint rule exception. /* eslint-disable jsx-a11y/label-has-for */ +import { Button, Label } from 'react-bootstrap' +import { connect } from 'react-redux' +import { FormattedMessage, FormattedTime } from 'react-intl' import coreUtils from '@opentripplanner/core-utils' import moment from 'moment' import PropTypes from 'prop-types' import React, { Component } from 'react' -import { Button, Label } from 'react-bootstrap' -import { FormattedMessage, FormattedTime } from 'react-intl' -import { connect } from 'react-redux' -import Icon from '../util/icon' -import SpanWithSpace from '../util/span-with-space' -import { setViewedTrip } from '../../actions/ui' import { findTrip } from '../../actions/api' import { setLocation } from '../../actions/map' +import { setViewedTrip } from '../../actions/ui' +import Icon from '../util/icon' +import SpanWithSpace from '../util/span-with-space' import Strong from '../util/strong-text' import ViewStopButton from './view-stop-button' -const { - getUserTimezone -} = coreUtils.time +const { getUserTimezone } = coreUtils.time class TripViewer extends Component { static propTypes = { @@ -32,51 +30,44 @@ class TripViewer extends Component { this.props.setViewedTrip(null) } - componentDidMount () { + componentDidMount() { const { findTrip, viewedTrip } = this.props const { tripId } = viewedTrip findTrip({ tripId }) } - render () { - const { - hideBackButton, - tripData, - viewedTrip - } = this.props + render() { + const { hideBackButton, tripData, viewedTrip } = this.props return ( -
    +
    {/* Header Block */} -
    +
    {/* Back button */} {!hideBackButton && ( -
    -
    )} {/* Header Text */} -
    - +
    +
    -
    +
    {/* Basic Trip Info */} {tripData && (
    {/* Route name */}
    - {tripData.wheelchairAccessible === 1 && -
    )} {/* Stop Listing */} - {tripData && tripData.stops && tripData.stopTimes && ( + {tripData && + tripData.stops && + tripData.stopTimes && tripData.stops.map((stop, i) => { // determine whether to use special styling for first/last stop let stripMapLineClass = 'strip-map-line' if (i === 0) stripMapLineClass = 'strip-map-line-first' - else if (i === tripData.stops.length - 1) stripMapLineClass = 'strip-map-line-last' + else if (i === tripData.stops.length - 1) + stripMapLineClass = 'strip-map-line-last' // determine whether to show highlight in strip map let highlightClass - if (i === viewedTrip.fromIndex) highlightClass = 'strip-map-highlight-first' - else if (i > viewedTrip.fromIndex && i < viewedTrip.toIndex) highlightClass = 'strip-map-highlight' - else if (i === viewedTrip.toIndex) highlightClass = 'strip-map-highlight-last' + if (i === viewedTrip.fromIndex) + highlightClass = 'strip-map-highlight-first' + else if (i > viewedTrip.fromIndex && i < viewedTrip.toIndex) + highlightClass = 'strip-map-highlight' + else if (i === viewedTrip.toIndex) + highlightClass = 'strip-map-highlight-last' // Convert to unix (millisceonds) for FormattedTime // Use timezone to avoid spying on startOf in future tests @@ -128,39 +125,39 @@ class TripViewer extends Component { return (
    {/* the departure time */} -
    +
    {/* the vertical strip map */} -
    - { highlightClass &&
    } +
    + {highlightClass &&
    }
    -
    +
    + +
    {/* the stop-viewer button */} -
    +
    } + text={ + + } />
    {/* the main stop label */} -
    - {stop.name} -
    +
    {stop.name}
    -
    ) - }) - )} + })}
    ) From 231dcc9ca4714c0fdb51dbf642006a64d1d35a5c Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 2 Dec 2021 15:50:35 +0000 Subject: [PATCH 065/270] fix(trip-viewer): grab supplemental trip info from graphQL if needed --- lib/actions/api.js | 51 +++++++++++++++++++++++++++ lib/components/viewers/trip-viewer.js | 29 ++++++++++----- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index cb29e7b74..3e0c18c63 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -535,6 +535,7 @@ export function findTrip(params) { findTripResponse, findTripError, { + noThrottle: true, postprocess: (payload, dispatch) => { dispatch(findStopsForTrip({ tripId: params.tripId })) dispatch(findStopTimesForTrip({ tripId: params.tripId })) @@ -543,6 +544,56 @@ export function findTrip(params) { } ) } +export function findGraphQLTrip(params) { + // Attempt an otp-2 graphQL request for the same trip before failing + // since the otp-2 rest api doesn't return the needed fields + + // TODO: for now, this matches the OTP-1 output. In the future this should be + // reduced to only include what is needed + return createGraphQLQueryAction( + `{ + trip(id: "${params.tripId}") { + id: gtfsId + route { + id: gtfsId + agency { + id: gtfsId + name + url + timezone + lang + phone + fareUrl + } + shortName + longName + type + url + color + textColor + routeBikesAllowed: bikesAllowed + bikesAllowed + } + serviceId + tripHeadsign + directionId + blockId + shapeId + wheelchairAccessible + bikesAllowed + tripBikesAllowed: bikesAllowed + } + }`, + {}, + findTripResponse, + findTripError, + { + rewritePayload: (payload) => { + return payload.data.trip + } + } + ) +} // Stops for trip query diff --git a/lib/components/viewers/trip-viewer.js b/lib/components/viewers/trip-viewer.js index 8e10b2f9a..f4675fd40 100644 --- a/lib/components/viewers/trip-viewer.js +++ b/lib/components/viewers/trip-viewer.js @@ -8,7 +8,7 @@ import moment from 'moment' import PropTypes from 'prop-types' import React, { Component } from 'react' -import { findTrip } from '../../actions/api' +import { findGraphQLTrip, findTrip } from '../../actions/api' import { setLocation } from '../../actions/map' import { setViewedTrip } from '../../actions/ui' import Icon from '../util/icon' @@ -36,6 +36,14 @@ class TripViewer extends Component { findTrip({ tripId }) } + componentDidUpdate() { + const { findGraphQLTrip, tripData, viewedTrip } = this.props + const { tripId } = viewedTrip + if (!tripData.route) { + findGraphQLTrip({ tripId }) + } + } + render() { const { hideBackButton, tripData, viewedTrip } = this.props @@ -66,14 +74,16 @@ class TripViewer extends Component {
    {/* Route name */}
    - + {tripData.route && ( + + )}
    {/* Wheelchair/bike accessibility badges, if applicable */} @@ -173,6 +183,7 @@ const mapStateToProps = (state, ownProps) => { } const mapDispatchToProps = { + findGraphQLTrip, findTrip, setLocation, setViewedTrip From 2c8d81dc9d58b3ff23a2652d765063c188e9c69d Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 2 Dec 2021 15:52:11 +0000 Subject: [PATCH 066/270] refactor(trip-viewer): adjust for linter --- lib/components/viewers/trip-viewer.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/components/viewers/trip-viewer.js b/lib/components/viewers/trip-viewer.js index f4675fd40..ed09410a8 100644 --- a/lib/components/viewers/trip-viewer.js +++ b/lib/components/viewers/trip-viewer.js @@ -21,7 +21,10 @@ const { getUserTimezone } = coreUtils.time class TripViewer extends Component { static propTypes = { + findGraphQLTrip: findGraphQLTrip.type, + findTrip: findTrip.type, hideBackButton: PropTypes.bool, + setViewedTrip: setViewedTrip.type, tripData: PropTypes.object, viewedTrip: PropTypes.object } @@ -174,7 +177,7 @@ class TripViewer extends Component { } } -const mapStateToProps = (state, ownProps) => { +const mapStateToProps = (state) => { const viewedTrip = state.otp.ui.viewedTrip return { tripData: state.otp.transitIndex.trips[viewedTrip.tripId], From d8038ace7b477545c6d962316467d7fa17712a82 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 6 Dec 2021 11:54:41 +0100 Subject: [PATCH 067/270] chore(RouteRow): run autoformatter --- lib/components/viewers/RouteRow.js | 163 +++++++++++++++-------------- 1 file changed, 84 insertions(+), 79 deletions(-) diff --git a/lib/components/viewers/RouteRow.js b/lib/components/viewers/RouteRow.js index c9446eca2..cf49ad4f1 100644 --- a/lib/components/viewers/RouteRow.js +++ b/lib/components/viewers/RouteRow.js @@ -1,7 +1,9 @@ -import { Label, Button } from 'react-bootstrap' +// TODO: Typescript, which doesn't make sense to do in this file until common types are established +/* eslint-disable react/prop-types */ +import { Button, Label } from 'react-bootstrap' +import { VelocityTransitionGroup } from 'velocity-react' import React, { PureComponent } from 'react' import styled from 'styled-components' -import { VelocityTransitionGroup } from 'velocity-react' import { ComponentContext } from '../../util/contexts' import { getColorAndNameFromRoute, getModeFromRoute } from '../../util/viewer' @@ -9,10 +11,70 @@ import { getFormattedMode } from '../../util/i18n' import RouteDetails from './route-details' +export const StyledRouteRow = styled.div` + background-color: white; + border-bottom: 1px solid gray; +` + +export const RouteRowButton = styled(Button)` + align-items: center; + display: flex; + padding: 6px; + width: 100%; + transition: all ease-in-out 0.1s; +` + +export const RouteRowElement = styled.span`` + +export const OperatorImg = styled.img` + height: 25px; + margin-right: 8px; +` + +export const ModeIconElement = styled.span` + display: inline-block; + vertical-align: bottom; + height: 22px; +` + +const RouteNameElement = styled(Label)` + background-color: ${(props) => + props.backgroundColor === '#ffffff' || props.backgroundColor === 'white' + ? 'rgba(0,0,0,0)' + : props.backgroundColor}; + color: ${(props) => props.color}; + flex: 0 1 auto; + font-size: medium; + font-weight: 400; + margin-left: ${(props) => + props.backgroundColor === '#ffffff' || props.backgroundColor === 'white' + ? 0 + : '8px'}; + margin-top: 2px; + overflow: hidden; + text-overflow: ellipsis; +` + +export const RouteName = ({ operator, route }) => { + const { backgroundColor, color, longName } = getColorAndNameFromRoute( + operator, + route + ) + return ( + + {route.shortName} {longName} + + ) +} + export class RouteRow extends PureComponent { - static contextType = ComponentContext; + static contextType = ComponentContext - constructor (props) { + constructor(props) { super(props) // Create a ref used to scroll to this.activeRef = React.createRef() @@ -26,9 +88,9 @@ export class RouteRow extends PureComponent { // This is fired when coming back from the route details view this.activeRef.current.scrollIntoView() } - }; + } - componentDidUpdate () { + componentDidUpdate() { /* If the initial route row list is being rendered and there is an active route, scroll to it. The initialRender prop prohibits the row being scrolled to @@ -40,7 +102,13 @@ export class RouteRow extends PureComponent { } _onClick = () => { - const { findRoute, getVehiclePositionsForRoute, isActive, route, setViewedRoute } = this.props + const { + findRoute, + getVehiclePositionsForRoute, + isActive, + route, + setViewedRoute + } = this.props if (isActive) { // Deselect current route if active. setViewedRoute({ patternId: null, routeId: null }) @@ -50,19 +118,16 @@ export class RouteRow extends PureComponent { getVehiclePositionsForRoute(route.id) setViewedRoute({ routeId: route.id }) } - }; + } - render () { + render() { const { intl, isActive, operator, route } = this.props const { ModeIcon } = this.context return ( - + @@ -75,7 +140,8 @@ export class RouteRow extends PureComponent { }, { operatorName: operator.name } )} - src={operator.logo} /> + src={operator.logo} + /> )} @@ -86,11 +152,10 @@ export class RouteRow extends PureComponent { )} height={22} mode={getModeFromRoute(route)} - width={22} /> + width={22} + /> - + - props.backgroundColor === '#ffffff' || props.backgroundColor === 'white' - ? 'rgba(0,0,0,0)' - : props.backgroundColor}; - color: ${(props) => props.color}; - flex: 0 1 auto; - font-size: medium; - font-weight: 400; - margin-left: ${(props) => - props.backgroundColor === '#ffffff' || props.backgroundColor === 'white' - ? 0 - : '8px'}; - margin-top: 2px; - overflow: hidden; - text-overflow: ellipsis; -` - -export const RouteName = ({operator, route}) => { - const { backgroundColor, color, longName } = getColorAndNameFromRoute( - operator, - route - ) - return ( - - {route.shortName} {longName} - - ) -} From 9e8c1a71c74ad9a476f0f15049a5e2086f661ca3 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Mon, 6 Dec 2021 11:56:28 +0100 Subject: [PATCH 068/270] refactor: allow graphQL route queries to fail without crash --- lib/actions/api.js | 6 ++++++ lib/components/viewers/RouteRow.js | 24 +++++++++++++----------- lib/util/state.js | 2 +- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index cb29e7b74..d00cf083f 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -454,6 +454,9 @@ export function fetchGraphQLOnlyStopInfo(stopId) { } }, rewritePayload: (payload) => { + if (payload.errors) { + return findRouteError(payload.errors) + } const { stop } = payload.data const color = stop.routes?.length > 0 && `#${stop.routes[0].color}` return { ...stop, color } @@ -841,6 +844,9 @@ export function findRoute(params) { { noThrottle: true, rewritePayload: (payload) => { + if (payload.errors) { + return dispatch(findRouteError(payload.errors)) + } const { route } = payload.data // Ensure this isn't overwritten route.v2 = true diff --git a/lib/components/viewers/RouteRow.js b/lib/components/viewers/RouteRow.js index cf49ad4f1..a789cda46 100644 --- a/lib/components/viewers/RouteRow.js +++ b/lib/components/viewers/RouteRow.js @@ -144,17 +144,19 @@ export class RouteRow extends PureComponent { /> )} - - - + {route.mode && ( + + + + )} Array.from( - new Set(routes.map((route) => route.agencyName || route.agency.name)) + new Set(routes.map((route) => route.agencyName || route?.agency?.name)) ) .filter((agency) => agency !== undefined) .sort() From 4ed2535b44c3c689423fe8a905e44b6b275859e8 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 8 Dec 2021 12:47:06 +0100 Subject: [PATCH 069/270] feat(craco): clarify how craco accepts env variables BREAKING CHANGE: the old method of passing in env variables is no longer supported --- README.md | 4 ++-- craco.config.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2f6b8f2f7..59f4e7857 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ yarn start Should you want to maintain multiple configuration files, OTP-RR can be made to use a custom config file by using environment variables. Other environment variables also exist. `CUSTOM_CSS` can be used to point to a css file to inject, and `JS_CONFIG` can be used to point to a `config.js` file to override the one shipped with OTP-RR. ```bash -yarn start --env.YAML_CONFIG=/absolute/path/to/config.yml +env YAML_CONFIG=/absolute/path/to/config.yml yarn start ``` ## Deploying the UI @@ -31,7 +31,7 @@ Build the js/css bundle by running `yarn build`. The build will appear in the `d The same environment variables which affect the behavior of `yarn start` also affect `yarn build`. Running the following command builds OTP-RR with customized js and css: ```bash -yarn build --env.JS_CONFIG=my-custom-js.js env.CUSTOM_CSS=my-custom-css.css +env JS_CONFIG=my-custom-js.js CUSTOM_CSS=my-custom-css.css yarn build ``` ## Library Documentation diff --git a/craco.config.js b/craco.config.js index ebd4141fc..e7a912485 100644 --- a/craco.config.js +++ b/craco.config.js @@ -20,10 +20,10 @@ module.exports = { /** * Webpack can be passed a few environment variables to override the default * files used to run this project. The environment variables are CUSTOM_CSS, - * HTML_FILE, YAML_CONFIG, and JS_CONFIG. They must each be passed in the - * format --env.*=/path/to/file. For example: + * HTML_FILE, YAML_CONFIG, and JS_CONFIG. They must each be passed via env + * variables *=/path/to/file. For example: * - * yarn start --env.YAML_CONFIG=/absolute/path/to/config.yml + * env YAML_CONFIG=/absolute/path/to/config.yml yarn start */ webpack: { // eslint-disable-next-line complexity From df65233aedb82681fc75566272efb8a1532ab4d2 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 8 Dec 2021 14:40:52 +0100 Subject: [PATCH 070/270] improvement: maintain compatibility with non-flex configs --- lib/actions/api.js | 2 +- lib/components/viewers/styled.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index d00cf083f..c968b30ef 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -1324,7 +1324,7 @@ function makeApiUrl(config, endpoint, options) { console.log('Using alt service for ' + options.serviceId) url = config.alternateTransitIndex.apiRoot + endpoint } else { - const api = options.v2 ? config.api_v2 : config.api + const api = options.v2 && config.api_v2 ? config.api_v2 : config.api url = `${api.host}${api.port ? ':' + api.port : ''}${api.path}/${endpoint}` } return url diff --git a/lib/components/viewers/styled.js b/lib/components/viewers/styled.js index 61a25ef30..f402cb4c9 100644 --- a/lib/components/viewers/styled.js +++ b/lib/components/viewers/styled.js @@ -51,7 +51,7 @@ export const Stop = styled.a` white-space: nowrap; margin-left: 45px; /* negative margin accounts for the height of the stop blob */ - margin-top: -25px; + margin-top: -28px; &:hover { color: #23527c; From 49f0fdd5257d472b41920962e37a033912a8423e Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 8 Dec 2021 15:43:10 +0100 Subject: [PATCH 071/270] refactor(actions/api): support otp-2 only instances --- lib/actions/api.js | 25 +++++++++++++++++++++---- lib/components/viewers/stop-viewer.js | 6 +++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index c968b30ef..24073255c 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -260,13 +260,18 @@ function getJsonAndCheckResponse(res) { function getOtpFetchOptions(state, includeToken = false) { let apiBaseUrl, apiKey, token - const { api, persistence } = state.otp.config + // eslint-disable-next-line camelcase + const { api: api_v1, api_v2, persistence } = state.otp.config if (persistence && persistence.otp_middleware) { // Prettier does not understand the syntax on this line // eslint-disable-next-line prettier/prettier ({ apiBaseUrl, apiKey } = persistence.otp_middleware) } + // Use only the v2 api if only a v2 api is presented + // eslint-disable-next-line camelcase + const api = api_v1 || api_v2 + const isOtpServerSameAsMiddleware = apiBaseUrl === api.host if (isOtpServerSameAsMiddleware) { if (includeToken && state.user) { @@ -297,7 +302,8 @@ function constructRoutingQuery( config.routingTypes.find((rt) => rt.key === routingType) // Certain requests will require OTP-2. If an OTP-2 host is specified, set it to be used - const useOtp2 = !!config.api_v2 && routingMode.includes('FLEX') + const useOtp2 = + (!!config.api_v2 && routingMode.includes('FLEX')) || !config.api const api = (rt && rt.api) || (useOtp2 && config.api_v2) || config.api const planEndpoint = `${api.host}${api.port ? ':' + api.port : ''}${ @@ -697,9 +703,18 @@ export function findRoutes(params) { postprocess: (payload, dispatch) => { return dispatch(findRoutesV2()) }, - rewritePayload: (payload) => { + rewritePayload: (payload, dispatch, getState) => { + const { otp } = getState() + // eslint-disable-next-line camelcase + const { api: api_v1, api_v2 } = otp.config + // eslint-disable-next-line camelcase + const usingV2Only = !api_v1 && !!api_v2 const routes = {} payload.forEach((rte) => { + // If coming from a v2 server, + // force V2 flag to ensure follow-up requests are graphQL + rte.v2 = usingV2Only + routes[rte.id] = rte }) return routes @@ -1324,7 +1339,9 @@ function makeApiUrl(config, endpoint, options) { console.log('Using alt service for ' + options.serviceId) url = config.alternateTransitIndex.apiRoot + endpoint } else { - const api = options.v2 && config.api_v2 ? config.api_v2 : config.api + const api = + // v2 is either selected or used when there is no alternative + (options.v2 && config.api_v2) || !config.api ? config.api_v2 : config.api url = `${api.host}${api.port ? ':' + api.port : ''}${api.path}/${endpoint}` } return url diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 9d01f0adc..73508cbed 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -285,9 +285,9 @@ class StopViewer extends Component {
    ) - // If geometries are available and stop times are not, it is a strong indication - // that the stop is a flex stop. - if (stopData?.geometries) { + // If geometries are available (and are not a point) and stop times + // are not, it is a strong indication that the stop is a flex stop. + if (stopData?.geometries?.geoJson?.type !== 'Point') { contents = (
    ) } } else if (scheduleView) { From fc28866781790cff9cdca9c528cf32a5ac60ca84 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Tue, 14 Dec 2021 14:16:17 +0100 Subject: [PATCH 072/270] chore(deps): upgrade stops-overlay --- package.json | 2 +- yarn.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 9309472ca..0afe9cf68 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@opentripplanner/printable-itinerary": "^1.3.1", "@opentripplanner/route-viewer-overlay": "^1.1.1", "@opentripplanner/stop-viewer-overlay": "^1.1.1", - "@opentripplanner/stops-overlay": "^3.3.1", + "@opentripplanner/stops-overlay": "^3.4.0", "@opentripplanner/transit-vehicle-overlay": "^2.3.1", "@opentripplanner/transitive-overlay": "^1.1.2", "@opentripplanner/trip-details": "^1.6.0", diff --git a/yarn.lock b/yarn.lock index d7b1be84c..2a6c9a3b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2588,14 +2588,14 @@ "@opentripplanner/core-utils" "^4.1.0" prop-types "^15.7.2" -"@opentripplanner/stops-overlay@^3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@opentripplanner/stops-overlay/-/stops-overlay-3.3.1.tgz#02a51522bda0667e84d058562a484c917b9547f7" - integrity sha512-8BBMdd2a40jbcjMcIPo7gfTVfKRCNg7+AB5F01d+9TVRZoM1++9ffeLuiFpebD3OR1Cip6gepyz1i0O/FV55mg== +"@opentripplanner/stops-overlay@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/stops-overlay/-/stops-overlay-3.4.0.tgz#0ded677db2dead7e02a968fca4177c341b527033" + integrity sha512-cq40s9iy5t9L1fN3/cyaFjf4OzNsecCSdZC5f+N9/FL8/lLgpE8bsEh03Ggy40T6T3mY23sRPGw3Fqk+rEI62A== dependencies: - "@opentripplanner/core-utils" "^4.1.0" - "@opentripplanner/from-to-location-picker" "^1.2.1" - "@opentripplanner/zoom-based-markers" "^1.2.0" + "@opentripplanner/core-utils" "^4.5.0" + "@opentripplanner/from-to-location-picker" "^1.3.0" + "@opentripplanner/zoom-based-markers" "^1.2.1" "@opentripplanner/transit-vehicle-overlay@^2.3.1": version "2.3.1" @@ -2668,7 +2668,7 @@ lodash.memoize "^4.1.2" prop-types "^15.7.2" -"@opentripplanner/zoom-based-markers@^1.2.0": +"@opentripplanner/zoom-based-markers@^1.2.0", "@opentripplanner/zoom-based-markers@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@opentripplanner/zoom-based-markers/-/zoom-based-markers-1.2.1.tgz#a6ce157ed0169b1098d1ddad3691a712fb6b243b" integrity sha512-2MatM0+PrymcOzRsaCwmHO1nBkJfsBraGUqVkl04oUdwXQx9qKuzba2zq5CEPLacnGOF3EViZcT0kSd/wLzUpA== From 4625964adf904cc51904868c6b0c431ca004a1e2 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Tue, 14 Dec 2021 14:42:22 +0100 Subject: [PATCH 073/270] refactor(actions/api): don't crash if api not defined --- lib/actions/api.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/actions/api.js b/lib/actions/api.js index 24073255c..c8ff4a796 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -1342,6 +1342,10 @@ function makeApiUrl(config, endpoint, options) { const api = // v2 is either selected or used when there is no alternative (options.v2 && config.api_v2) || !config.api ? config.api_v2 : config.api + + // Don't crash if no api is defined (such as in the unit test env) + if (!api?.host) return null + url = `${api.host}${api.port ? ':' + api.port : ''}${api.path}/${endpoint}` } return url From 0abd4ab2118efd028de770df59b34d55b450f0f0 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 15 Dec 2021 13:42:41 +0100 Subject: [PATCH 074/270] refactor(connected-stops-overlay): update flex zone comment --- lib/components/map/connected-stops-overlay.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/components/map/connected-stops-overlay.js b/lib/components/map/connected-stops-overlay.js index b5fa86198..cacff0ea3 100644 --- a/lib/components/map/connected-stops-overlay.js +++ b/lib/components/map/connected-stops-overlay.js @@ -25,8 +25,9 @@ const mapStateToProps = (state, ownProps) => { // If a flex route is being shown but the pattern viewer is not active, then the // stops of the first pattern of the route should be shown // This will ensure that the flex zone stops are shown. - // TODO: if the time to show flex stops changes, this is where that change - // should be made. + + // Preferably, the flex stops would be rendered in a separate layer. + // However, without changes to GraphQL, getting this data is very expensive if (!viewedPattern && route.v2) { viewedPattern = Object.keys(route?.patterns)?.[0] } From cfb271c5d72be0ddbec66987edc04ad723fa216a Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 15 Dec 2021 15:29:18 +0100 Subject: [PATCH 075/270] ci: restore lint step removed in 39fd703 --- .github/workflows/node-ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index d2191a848..ed1b484eb 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -23,7 +23,11 @@ jobs: uses: bahmutov/npm-install@v1 - name: Copy example config run: cp example-config.yml config.yml - # Actual lint step temporarily removed to allow for package release + - name: Lint code + # Move everything from latest commit back to staged + run: git reset --soft HEAD^ && yarn lint + # For our info, lint all files but don't mark them as failure + # TODO: remove this once project is typescripted - name: Lint all code (ignoring errors) run: yarn lint-all || true - name: Run tests From d4a80e131339cf206cdd7ce323cde35d9bb8c6d9 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 16 Dec 2021 12:40:31 +0100 Subject: [PATCH 076/270] refactor(craco.config): remove superfluous comment --- craco.config.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/craco.config.js b/craco.config.js index c753c6648..e54f98412 100644 --- a/craco.config.js +++ b/craco.config.js @@ -31,8 +31,7 @@ module.exports = { /** * Webpack can be passed a few environment variables to override the default * files used to run this project. The environment variables are CUSTOM_CSS, - * HTML_FILE, YAML_CONFIG, and JS_CONFIG. They must each be passed via env - * variables *=/path/to/file. For example: + * HTML_FILE, YAML_CONFIG, and JS_CONFIG. For example: * * env YAML_CONFIG=/absolute/path/to/config.yml yarn start */ From ab4179f662d0be72a4382268d2ab45aa40bd1912 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Tue, 21 Dec 2021 13:06:29 +0100 Subject: [PATCH 077/270] feat(connected-route-viewer-overlay): support not rendering route inside flex zones --- example-config.yml | 4 ++++ lib/components/map/connected-route-viewer-overlay.js | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/example-config.yml b/example-config.yml index a91baec1c..c0bcbb370 100644 --- a/example-config.yml +++ b/example-config.yml @@ -405,3 +405,7 @@ dateTime: # # departure and it is not entirely excluded from display # # (defaults to 4 days/345600s if unspecified). # timeRange: 345600 +# routeViewer: +# # Whether to render routes within flex zones of a route's patterns. If set to true, +# # routes will not be rendered within flex zones. +# clipRouteRenderToPatternStops: true diff --git a/lib/components/map/connected-route-viewer-overlay.js b/lib/components/map/connected-route-viewer-overlay.js index adb99c8a4..aa229afd7 100644 --- a/lib/components/map/connected-route-viewer-overlay.js +++ b/lib/components/map/connected-route-viewer-overlay.js @@ -1,5 +1,5 @@ -import RouteViewerOverlay from '@opentripplanner/route-viewer-overlay' import { connect } from 'react-redux' +import RouteViewerOverlay from '@opentripplanner/route-viewer-overlay' // connect to the redux store @@ -14,10 +14,14 @@ const mapStateToProps = (state, ownProps) => { // If a pattern is selected, hide all other patterns if (viewedRoute?.patternId && routeData?.patterns) { - filteredPatterns = {[viewedRoute.patternId]: routeData.patterns[viewedRoute.patternId]} + filteredPatterns = { + [viewedRoute.patternId]: routeData.patterns[viewedRoute.patternId] + } } return { + clipToPatternStops: + state.otp.config.routeViewer.clipRouteRenderToPatternStops, routeData: { ...routeData, patterns: filteredPatterns } } } From 04daa4b7000472f9a5b547d37936429ef0824c3b Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Tue, 28 Dec 2021 16:25:01 +0100 Subject: [PATCH 078/270] refactor(connected-route-viewer-overlay): rename clipToPatternStops config variable --- example-config.yml | 2 +- lib/components/map/connected-route-viewer-overlay.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example-config.yml b/example-config.yml index c0bcbb370..123383bcd 100644 --- a/example-config.yml +++ b/example-config.yml @@ -408,4 +408,4 @@ dateTime: # routeViewer: # # Whether to render routes within flex zones of a route's patterns. If set to true, # # routes will not be rendered within flex zones. -# clipRouteRenderToPatternStops: true +# hideRouteShapesWithinFlexZones: true diff --git a/lib/components/map/connected-route-viewer-overlay.js b/lib/components/map/connected-route-viewer-overlay.js index aa229afd7..03cbd0370 100644 --- a/lib/components/map/connected-route-viewer-overlay.js +++ b/lib/components/map/connected-route-viewer-overlay.js @@ -21,7 +21,7 @@ const mapStateToProps = (state, ownProps) => { return { clipToPatternStops: - state.otp.config.routeViewer.clipRouteRenderToPatternStops, + state.otp.config.routeViewer.hideRouteShapesWithinFlexZones, routeData: { ...routeData, patterns: filteredPatterns } } } From 09f76f5465f479055bbee002754862223cddac37 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 28 Dec 2021 18:16:24 -0500 Subject: [PATCH 079/270] chore(i18n): Add corresponding FR message. --- i18n/en-US.yml | 2 +- i18n/fr-FR.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 3302d3c95..692ead24b 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -435,8 +435,8 @@ components: # Used in both desktop and mobile StopViewer: displayStopId: "Stop ID: {stopId}" - header: Stop Viewer flexStop: This is a flex stop. Vehicles will drop off and pick up passengers in this flexible zone by request. You may have to call ahead for service in this area. + header: Stop Viewer loadingText: Loading Stop... noStopsFound: No stop times found for date. planTrip: "Plan a trip:" diff --git a/i18n/fr-FR.yml b/i18n/fr-FR.yml index 64ca23425..15ebb83be 100644 --- a/i18n/fr-FR.yml +++ b/i18n/fr-FR.yml @@ -419,6 +419,7 @@ components: # Used in both desktop and mobile StopViewer: displayStopId: "Arrêt n°{stopId}" + flexStop: Cet arrêt fait partie d'une zone 'Flex' et est desservi à la demande. Une réservation préalable peut être exigée pour obtenir la desserte. header: Info arrêt loadingText: Chargement de l'arrêt... noStopsFound: Aucun passage n'a été trouvé pour cette date. From 6ffc02cb8262fbc542bc259d5275ddc5a62fb900 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 29 Dec 2021 11:38:06 +0100 Subject: [PATCH 080/270] refactor: address pr comments --- lib/actions/api.js | 55 +++++++++---------- lib/components/map/connected-stops-overlay.js | 6 +- lib/components/viewers/RouteRow.js | 4 +- lib/components/viewers/stop-viewer.js | 2 +- 4 files changed, 34 insertions(+), 33 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index c8ff4a796..e3bccbd71 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -798,7 +798,8 @@ export const findRouteError = createAction('FIND_ROUTE_ERROR') export function findRoute(params) { return async function (dispatch, getState) { const state = getState() - const route = state?.otp?.transitIndex?.routes?.[params.routeId] + const { routeId } = params + const route = state?.otp?.transitIndex?.routes?.[routeId] // This can sometimes result in extra requests, but given that requests are // already being sprayed, this is a safe and clean solution for now if (!route) { @@ -891,14 +892,14 @@ export function findRoute(params) { return dispatch( createQueryAction( - `index/routes/${params.routeId}`, + `index/routes/${routeId}`, findRouteResponse, findRouteError, { noThrottle: true, postprocess: (payload, dispatch) => { // load patterns - dispatch(findPatternsForRoute({ routeId: params.routeId })) + dispatch(findPatternsForRoute({ routeId })) } } ) @@ -909,7 +910,8 @@ export function findRoute(params) { export function findPatternsForRoute(params) { return function (dispatch, getState) { const state = getState() - const route = state?.otp?.transitIndex?.routes?.[params.routeId] + const { routeId } = params + const route = state?.otp?.transitIndex?.routes?.[routeId] // If a route was fetched from GraphQL, it already has patterns if (route && route.v2) { if (!route.patterns) { @@ -920,7 +922,7 @@ export function findPatternsForRoute(params) { return dispatch( createQueryAction( - `index/routes/${params.routeId}/patterns?includeGeometry=true`, + `index/routes/${routeId}/patterns?includeGeometry=true`, findPatternsForRouteResponse, findPatternsForRouteError, { @@ -934,7 +936,7 @@ export function findPatternsForRoute(params) { dispatch( findGeometryForPattern({ patternId: ptn.id, - routeId: params.routeId + routeId }) ) } @@ -950,7 +952,7 @@ export function findPatternsForRoute(params) { return { patterns, - routeId: params.routeId + routeId } } } @@ -977,19 +979,18 @@ export function findGeometryForPattern(params) { return } + const { patternId, routeId } = params return dispatch( createQueryAction( `index/patterns/${params.patternId}/geometry`, findGeometryForPatternResponse, findGeometryForPatternError, { - rewritePayload: (payload) => { - return { - geometry: payload, - patternId: params.patternId, - routeId: params.routeId - } - } + rewritePayload: (payload) => ({ + geometry: payload, + patternId, + routeId + }) } ) ) @@ -1013,6 +1014,8 @@ export function findStopsForPattern(params) { if (route && route.v2) { return } + + const { patternId, routeId } = params return dispatch( createQueryAction( `index/patterns/${params.patternId}/stops`, @@ -1020,13 +1023,11 @@ export function findStopsForPattern(params) { findStopsForPatternError, { noThrottle: true, - rewritePayload: (payload) => { - return { - patternId: params.patternId, - routeId: params.routeId, - stops: payload - } - } + rewritePayload: (payload) => ({ + patternId, + routeId, + stops: payload + }) } ) ) @@ -1195,12 +1196,10 @@ export function getVehiclePositionsForRoute(routeId) { receivedVehiclePositions, receivedVehiclePositionsError, { - rewritePayload: (payload) => { - return { - routeId: routeId, - vehicles: payload - } - } + rewritePayload: (payload) => ({ + routeId, + vehicles: payload + }) } ) ) @@ -1341,7 +1340,7 @@ function makeApiUrl(config, endpoint, options) { } else { const api = // v2 is either selected or used when there is no alternative - (options.v2 && config.api_v2) || !config.api ? config.api_v2 : config.api + (options.v2 && config.api_v2) || config.api || config.api_v2 // Don't crash if no api is defined (such as in the unit test env) if (!api?.host) return null diff --git a/lib/components/map/connected-stops-overlay.js b/lib/components/map/connected-stops-overlay.js index cacff0ea3..38d217af3 100644 --- a/lib/components/map/connected-stops-overlay.js +++ b/lib/components/map/connected-stops-overlay.js @@ -15,9 +15,11 @@ const mapStateToProps = (state, ownProps) => { let minZoom = 15 // There are a number of cases when stops within route objects should be shown - // although there are no stops from the stops index active + // although there are no stops that would otherwise be shown. In this case we need + // to override the stops index if (routes) { - // All cases of stops being force-shown only apply when a route is being actively viewed + // All cases of stops being shown even when stops are otherwise hidden + // only apply when a route is being actively viewed const route = routes?.[viewedRoute?.routeId] if (route?.patterns) { // If the pattern viewer is active, stops along that pattern should be shown diff --git a/lib/components/viewers/RouteRow.js b/lib/components/viewers/RouteRow.js index a789cda46..db9136fc2 100644 --- a/lib/components/viewers/RouteRow.js +++ b/lib/components/viewers/RouteRow.js @@ -20,8 +20,8 @@ export const RouteRowButton = styled(Button)` align-items: center; display: flex; padding: 6px; - width: 100%; transition: all ease-in-out 0.1s; + width: 100%; ` export const RouteRowElement = styled.span`` @@ -33,8 +33,8 @@ export const OperatorImg = styled.img` export const ModeIconElement = styled.span` display: inline-block; - vertical-align: bottom; height: 22px; + vertical-align: bottom; ` const RouteNameElement = styled(Label)` diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 73508cbed..2f92127b0 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -288,7 +288,7 @@ class StopViewer extends Component { // If geometries are available (and are not a point) and stop times // are not, it is a strong indication that the stop is a flex stop. if (stopData?.geometries?.geoJson?.type !== 'Point') { - contents = (
    ) + contents =
    } } else if (scheduleView) { contents = ( From 15ac1aa9a3ad1922f7207f69dec2ff17da38df07 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 30 Dec 2021 10:54:04 +0100 Subject: [PATCH 081/270] refactor(connected-route-viewer-overlay): avoid crash when routeViewer missing from config --- lib/components/map/connected-route-viewer-overlay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/map/connected-route-viewer-overlay.js b/lib/components/map/connected-route-viewer-overlay.js index 03cbd0370..a11101a22 100644 --- a/lib/components/map/connected-route-viewer-overlay.js +++ b/lib/components/map/connected-route-viewer-overlay.js @@ -21,7 +21,7 @@ const mapStateToProps = (state, ownProps) => { return { clipToPatternStops: - state.otp.config.routeViewer.hideRouteShapesWithinFlexZones, + state.otp.config?.routeViewer?.hideRouteShapesWithinFlexZones, routeData: { ...routeData, patterns: filteredPatterns } } } From f5cbd05d30f14b1b08358dd2ad37c818783a4970 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Thu, 30 Dec 2021 11:06:48 +0100 Subject: [PATCH 082/270] refactor(stop-viewer): don't show flex message during stopData load --- lib/components/viewers/stop-viewer.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 2f92127b0..1667a96f8 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -285,9 +285,13 @@ class StopViewer extends Component {
    ) - // If geometries are available (and are not a point) and stop times - // are not, it is a strong indication that the stop is a flex stop. - if (stopData?.geometries?.geoJson?.type !== 'Point') { + /* If geometries are available (and are not a point) and stop times + are not, it is a strong indication that the stop is a flex stop. + + The extra checks stopData are needed to ensure that the message is + not shown while stopData is loading + */ + if (stopData && stopData.geometries && stopData.geometries.geoJson?.type !== 'Point') { contents =
    } } else if (scheduleView) { From 9d1dd5b5b35cd24c6e0205d2d0c33c0dac9ac623 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 5 Jan 2022 12:10:49 +0100 Subject: [PATCH 083/270] chore(deps): upgrade otp-ui --- package.json | 2 +- yarn.lock | 1171 +++++++------------------------------------------- 2 files changed, 159 insertions(+), 1014 deletions(-) diff --git a/package.json b/package.json index 0d9d81e77..74415c515 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@opentripplanner/location-icon": "^1.4.0", "@opentripplanner/park-and-ride-overlay": "^1.2.2", "@opentripplanner/printable-itinerary": "^1.3.1", - "@opentripplanner/route-viewer-overlay": "^1.1.1", + "@opentripplanner/route-viewer-overlay": "^1.2.0", "@opentripplanner/stop-viewer-overlay": "^1.1.1", "@opentripplanner/stops-overlay": "^3.3.1", "@opentripplanner/transit-vehicle-overlay": "^2.3.1", diff --git a/yarn.lock b/yarn.lock index 4fdb66764..16b8684b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -419,7 +419,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.11.5", "@babel/parser@^7.12.3", "@babel/parser@^7.14.5", "@babel/parser@^7.15.0", "@babel/parser@^7.7.0", "@babel/parser@^7.7.2": +"@babel/parser@^7.11.5", "@babel/parser@^7.12.3", "@babel/parser@^7.14.5", "@babel/parser@^7.15.0", "@babel/parser@^7.7.0", "@babel/parser@^7.7.2": version "7.15.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.3.tgz#3416d9bea748052cfcb63dbcc27368105b1ed862" integrity sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA== @@ -1401,7 +1401,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.12.1", "@babel/types@^7.12.6", "@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.14.9", "@babel/types@^7.15.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": +"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.12.1", "@babel/types@^7.12.6", "@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.14.9", "@babel/types@^7.15.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": version "7.15.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.0.tgz#61af11f2286c4e9c69ca8deb5f4375a73c72dcbd" integrity sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ== @@ -2552,13 +2552,14 @@ "@opentripplanner/humanize-distance" "^1.1.0" prop-types "^15.7.2" -"@opentripplanner/route-viewer-overlay@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@opentripplanner/route-viewer-overlay/-/route-viewer-overlay-1.1.1.tgz#41735ce5c021e96a8cd679d57e2772735709e237" - integrity sha512-SMzRYuLEbSNuSh7SUr/pRKVP5CqEfapdrQpjRxDHIuPK/RXYI7uu9IhT0+iQH361qP9uMBdlbEwiV2IcZ9Fnjg== +"@opentripplanner/route-viewer-overlay@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/route-viewer-overlay/-/route-viewer-overlay-1.2.0.tgz#b1a8954fa9f3cad15f1ee06b665813e8f4539448" + integrity sha512-W0jRot7pWWll33NdXDkG/lV+hgVfybqAx1C7at0CcAHTLWk9l92HmPhbXaZExYJYL2ZY9szWvNIAehCmbxIuxQ== dependencies: "@mapbox/polyline" "^1.1.0" - "@opentripplanner/core-utils" "^4.1.0" + "@opentripplanner/core-utils" "^4.5.0" + point-in-polygon "^1.1.0" prop-types "^15.7.2" "@opentripplanner/stop-viewer-overlay@^1.1.1": @@ -3162,42 +3163,18 @@ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024" integrity sha512-bxlMKPDbY8x5h6HBwVzEOk2C8fb6SLfYQ5Jw3uBYuYF1lfWk/kbLd81la82vrIkBb0l+JdmrZaDikPrNxpS/Ew== dependencies: - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/visitor-keys" "3.10.1" - debug "^4.1.1" - glob "^7.1.6" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" - -"@types/babel__generator@*": - version "7.6.3" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.3.tgz#f456b4b2ce79137f768aa130d2423d2f0ccfaba5" - integrity sha512-/GWCmzJWqV7diQW54smJZzWbSFf4QYtF71WCKhcx6Ru/tFyQIY2eiiITcCAeuPbNSvT9YCGkVMqqvSk2Z0mXiA== - dependencies: - "@typescript-eslint/types" "4.30.0" - "@typescript-eslint/visitor-keys" "4.30.0" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" - semver "^7.3.5" - tsutils "^3.21.0" - -"@types/babel__template@*": - version "7.4.1" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" - integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== - dependencies: - eslint-visitor-keys "^1.1.0" + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": +"@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": version "7.14.2" resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43" integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== dependencies: - "@typescript-eslint/types" "4.30.0" - eslint-visitor-keys "^2.0.0" + "@babel/types" "^7.3.0" "@types/cheerio@^0.22.22": version "0.22.30" @@ -3254,8 +3231,8 @@ resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== dependencies: - "@vue/compiler-core" "3.2.6" - "@vue/shared" "3.2.6" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" "@types/html-minifier-terser@^5.0.0": version "5.1.2" @@ -3272,8 +3249,7 @@ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== dependencies: - "@vue/compiler-dom" "3.2.6" - "@vue/shared" "3.2.6" + "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^1.1.1": version "1.1.2" @@ -3308,10 +3284,10 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@vue/shared@3.2.6": - version "3.2.6" - resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.6.tgz#2c22bae88fe2b7b59fa68a9c9c4cd60bae2c1794" - integrity sha512-uwX0Qs2e6kdF+WmxwuxJxOnKs/wEkMArtYpHSm7W+VY/23Tl8syMRyjnzEeXrNCAP0/8HZxEGkHJsjPEDNRuHw== +"@types/long@^3.0.32": + version "3.0.32" + resolved "https://registry.yarnpkg.com/@types/long/-/long-3.0.32.tgz#f4e5af31e9e9b196d8e5fca8a5e2e20aa3d60b69" + integrity sha512-ZXyOOm83p7X8p3s0IYM3VeueNmHpkk/yMlP8CLeOnEcu6hIwPH7YjZBvhQkR0ZFS2DqZAxKtJ/M5fcuv3OU5BA== "@types/minimatch@*": version "3.0.5" @@ -3333,10 +3309,10 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== -"@webassemblyjs/helper-buffer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" - integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/prettier@^2.0.0", "@types/prettier@^2.1.5": version "2.3.2" @@ -3395,12 +3371,18 @@ resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.19.tgz#8f2a85e8180a43b57966b237d26a29481dacc991" integrity sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A== dependencies: - "@webassemblyjs/ast" "1.9.0" + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" -"@webassemblyjs/helper-wasm-bytecode@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" - integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== +"@types/react@17": + version "17.0.38" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd" + integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" "@types/resolve@0.0.8": version "0.0.8" @@ -3431,12 +3413,10 @@ resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== -"@webassemblyjs/leb128@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" - integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== - dependencies: - "@xtuc/long" "4.2.2" +"@types/stack-utils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== "@types/stack-utils@^2.0.0": version "2.0.1" @@ -3509,15 +3489,12 @@ dependencies: "@types/yargs-parser" "*" -"@webassemblyjs/wasm-opt@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" - integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== +"@types/yauzl@^2.9.1": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a" + integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" + "@types/node" "*" "@typescript-eslint/eslint-plugin@^4.28.2", "@typescript-eslint/eslint-plugin@^4.5.0": version "4.30.0" @@ -3625,6 +3602,30 @@ "@typescript-eslint/types" "4.30.0" eslint-visitor-keys "^2.0.0" +"@vue/compiler-core@3.2.6": + version "3.2.6" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.6.tgz#7162bb0670273f04566af0d353009187ab577915" + integrity sha512-vbwnz7+OhtLO5p5i630fTuQCL+MlUpEMTKHuX+RfetQ+3pFCkItt2JUH+9yMaBG2Hkz6av+T9mwN/acvtIwpbw== + dependencies: + "@babel/parser" "^7.15.0" + "@babel/types" "^7.15.0" + "@vue/shared" "3.2.6" + estree-walker "^2.0.2" + source-map "^0.6.1" + +"@vue/compiler-dom@3.2.6": + version "3.2.6" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.6.tgz#3764d7fe1a696e39fb2a3c9d638da0749e369b2d" + integrity sha512-+a/3oBAzFIXhHt8L5IHJOTP4a5egzvpXYyi13jR7CUYOR1S+Zzv7vBWKYBnKyJLwnrxTZnTQVjeHCgJq743XKg== + dependencies: + "@vue/compiler-core" "3.2.6" + "@vue/shared" "3.2.6" + +"@vue/shared@3.2.6": + version "3.2.6" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.6.tgz#2c22bae88fe2b7b59fa68a9c9c4cd60bae2c1794" + integrity sha512-uwX0Qs2e6kdF+WmxwuxJxOnKs/wEkMArtYpHSm7W+VY/23Tl8syMRyjnzEeXrNCAP0/8HZxEGkHJsjPEDNRuHw== + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -5896,11 +5897,6 @@ bytes@3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= - bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" @@ -6791,40 +6787,6 @@ conventional-changelog-angular@^5.0.0: compare-func "^2.0.0" q "^1.5.1" -conventional-changelog-eslint@^3.0.9: - version "3.0.9" - resolved "https://registry.yarnpkg.com/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.9.tgz#689bd0a470e02f7baafe21a495880deea18b7cdb" - integrity sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA== - dependencies: - q "^1.5.1" - -conventional-changelog-express@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/conventional-changelog-express/-/conventional-changelog-express-2.0.6.tgz#420c9d92a347b72a91544750bffa9387665a6ee8" - integrity sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ== - dependencies: - q "^1.5.1" - -conventional-changelog-jquery@^3.0.11: - version "3.0.11" - resolved "https://registry.yarnpkg.com/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.11.tgz#d142207400f51c9e5bb588596598e24bba8994bf" - integrity sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw== - dependencies: - q "^1.5.1" - -conventional-changelog-jshint@^2.0.9: - version "2.0.9" - resolved "https://registry.yarnpkg.com/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.9.tgz#f2d7f23e6acd4927a238555d92c09b50fe3852ff" - integrity sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA== - dependencies: - compare-func "^2.0.0" - q "^1.5.1" - -conventional-changelog-preset-loader@^2.3.4: - version "2.3.4" - resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" - integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== - conventional-changelog-writer@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz#1ca7880b75aa28695ad33312a1f2366f4b12659f" @@ -8831,6 +8793,11 @@ estree-walker@^1.0.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -9283,11 +9250,6 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -filter-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" - integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= - finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -9564,13 +9526,6 @@ fromentries@^1.3.2: resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a" integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg== -fs-access@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" - integrity sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o= - dependencies: - null-check "^1.0.0" - fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -9582,15 +9537,6 @@ fs-extra@8.1.0, fs-extra@^8.1.0: integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== dependencies: graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" - integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== - dependencies: - graceful-fs "^4.1.2" jsonfile "^4.0.0" universalify "^0.1.0" @@ -10776,14 +10722,6 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -11099,11 +11037,6 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= -is-plain-obj@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -12036,829 +11969,96 @@ jest-serializer@^26.6.2: resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== dependencies: - lodash.capitalize "^4.2.1" - lodash.escaperegexp "^4.1.2" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.uniqby "^4.7.0" - -istanbul-lib-coverage@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" - integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== + "@types/node" "*" + graceful-fs "^4.2.4" -istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" - integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== +jest-serializer@^27.0.6: + version "27.0.6" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.0.6.tgz#93a6c74e0132b81a2d54623251c46c498bb5bec1" + integrity sha512-PtGdVK9EGC7dsaziskfqaAPib6wTViY3G8E5wz9tLVPhHyiDNTZn/xjZ4khAw+09QkoOVpn7vF5nPSN6dtBexA== dependencies: - "@babel/core" "^7.7.5" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.0.0" - semver "^6.3.0" + "@types/node" "*" + graceful-fs "^4.2.4" -istanbul-lib-report@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" - integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== +jest-snapshot@^26.6.0, jest-snapshot@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" + integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== dependencies: - istanbul-lib-coverage "^3.0.0" - make-dir "^3.0.0" - supports-color "^7.1.0" + "@babel/types" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/babel__traverse" "^7.0.4" + "@types/prettier" "^2.0.0" + chalk "^4.0.0" + expect "^26.6.2" + graceful-fs "^4.2.4" + jest-diff "^26.6.2" + jest-get-type "^26.3.0" + jest-haste-map "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + natural-compare "^1.4.0" + pretty-format "^26.6.2" + semver "^7.3.2" -istanbul-lib-source-maps@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9" - integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg== +jest-snapshot@^27.1.0: + version "27.1.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.1.0.tgz#2a063ab90064017a7e9302528be7eaea6da12d17" + integrity sha512-eaeUBoEjuuRwmiRI51oTldUsKOohB1F6fPqWKKILuDi/CStxzp2IWekVUXbuHHoz5ik33ioJhshiHpgPFbYgcA== dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - source-map "^0.6.1" + "@babel/core" "^7.7.2" + "@babel/generator" "^7.7.2" + "@babel/parser" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" + "@babel/types" "^7.0.0" + "@jest/transform" "^27.1.0" + "@jest/types" "^27.1.0" + "@types/babel__traverse" "^7.0.4" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^27.1.0" + graceful-fs "^4.2.4" + jest-diff "^27.1.0" + jest-get-type "^27.0.6" + jest-haste-map "^27.1.0" + jest-matcher-utils "^27.1.0" + jest-message-util "^27.1.0" + jest-resolve "^27.1.0" + jest-util "^27.1.0" + natural-compare "^1.4.0" + pretty-format "^27.1.0" + semver "^7.3.2" -istanbul-reports@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" - integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" +jest-transform-stub@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz#19018b0851f7568972147a5d60074b55f0225a7d" + integrity sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg== -java-properties@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/java-properties/-/java-properties-1.0.2.tgz#ccd1fa73907438a5b5c38982269d0e771fe78211" - integrity sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ== +jest-util@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.5.0.tgz#31c63b5d6e901274d264a4fec849230aa3fa35b0" + integrity sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA== + dependencies: + "@jest/types" "^25.5.0" + chalk "^3.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + make-dir "^3.0.0" -jest-changed-files@^26.6.2: +jest-util@^26.6.0, jest-util@^26.6.2: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" - integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" + integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== dependencies: "@jest/types" "^26.6.2" - execa "^4.0.0" - throat "^5.0.0" - -jest-changed-files@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.1.0.tgz#42da6ea00f06274172745729d55f42b60a9dffe0" - integrity sha512-eRcb13TfQw0xiV2E98EmiEgs9a5uaBIqJChyl0G7jR9fCIvGjXovnDS6Zbku3joij4tXYcSK4SE1AXqOlUxjWg== - dependencies: - "@jest/types" "^27.1.0" - execa "^5.0.0" - throat "^6.0.1" - -jest-circus@26.6.0: - version "26.6.0" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-26.6.0.tgz#7d9647b2e7f921181869faae1f90a2629fd70705" - integrity sha512-L2/Y9szN6FJPWFK8kzWXwfp+FOR7xq0cUL4lIsdbIdwz3Vh6P1nrpcqOleSzr28zOtSHQNV9Z7Tl+KkuK7t5Ng== - dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^26.6.0" - "@jest/test-result" "^26.6.0" - "@jest/types" "^26.6.0" - "@types/babel__traverse" "^7.0.4" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - dedent "^0.7.0" - expect "^26.6.0" - is-generator-fn "^2.0.0" - jest-each "^26.6.0" - jest-matcher-utils "^26.6.0" - jest-message-util "^26.6.0" - jest-runner "^26.6.0" - jest-runtime "^26.6.0" - jest-snapshot "^26.6.0" - jest-util "^26.6.0" - pretty-format "^26.6.0" - stack-utils "^2.0.2" - throat "^5.0.0" - -jest-circus@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.1.0.tgz#24c280c90a625ea57da20ee231d25b1621979a57" - integrity sha512-6FWtHs3nZyZlMBhRf1wvAC5CirnflbGJAY1xssSAnERLiiXQRH+wY2ptBVtXjX4gz4AA2EwRV57b038LmifRbA== - dependencies: - "@jest/environment" "^27.1.0" - "@jest/test-result" "^27.1.0" - "@jest/types" "^27.1.0" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - dedent "^0.7.0" - expect "^27.1.0" - is-generator-fn "^2.0.0" - jest-each "^27.1.0" - jest-matcher-utils "^27.1.0" - jest-message-util "^27.1.0" - jest-runtime "^27.1.0" - jest-snapshot "^27.1.0" - jest-util "^27.1.0" - pretty-format "^27.1.0" - slash "^3.0.0" - stack-utils "^2.0.3" - throat "^6.0.1" - -jest-cli@^26.6.0: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" - integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== - dependencies: - "@jest/core" "^26.6.3" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" - chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.4" - import-local "^3.0.2" - is-ci "^2.0.0" - jest-config "^26.6.3" - jest-util "^26.6.2" - jest-validate "^26.6.2" - prompts "^2.0.1" - yargs "^15.4.1" - -jest-cli@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.1.0.tgz#118438e4d11cf6fb66cb2b2eb5778817eab3daeb" - integrity sha512-h6zPUOUu+6oLDrXz0yOWY2YXvBLk8gQinx4HbZ7SF4V3HzasQf+ncoIbKENUMwXyf54/6dBkYXvXJos+gOHYZw== - dependencies: - "@jest/core" "^27.1.0" - "@jest/test-result" "^27.1.0" - "@jest/types" "^27.1.0" - chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.4" - import-local "^3.0.2" - jest-config "^27.1.0" - jest-util "^27.1.0" - jest-validate "^27.1.0" - prompts "^2.0.1" - yargs "^16.0.3" - -jest-config@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" - integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== - dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^26.6.3" - "@jest/types" "^26.6.2" - babel-jest "^26.6.3" - chalk "^4.0.0" - deepmerge "^4.2.2" - glob "^7.1.1" - graceful-fs "^4.2.4" - jest-environment-jsdom "^26.6.2" - jest-environment-node "^26.6.2" - jest-get-type "^26.3.0" - jest-jasmine2 "^26.6.3" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" - micromatch "^4.0.2" - pretty-format "^26.6.2" - -jest-config@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.1.0.tgz#e6826e2baaa34c07c3839af86466870e339d9ada" - integrity sha512-GMo7f76vMYUA3b3xOdlcKeKQhKcBIgurjERO2hojo0eLkKPGcw7fyIoanH+m6KOP2bLad+fGnF8aWOJYxzNPeg== - dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^27.1.0" - "@jest/types" "^27.1.0" - babel-jest "^27.1.0" - chalk "^4.0.0" - deepmerge "^4.2.2" - glob "^7.1.1" - graceful-fs "^4.2.4" - is-ci "^3.0.0" - jest-circus "^27.1.0" - jest-environment-jsdom "^27.1.0" - jest-environment-node "^27.1.0" - jest-get-type "^27.0.6" - jest-jasmine2 "^27.1.0" - jest-regex-util "^27.0.6" - jest-resolve "^27.1.0" - jest-runner "^27.1.0" - jest-util "^27.1.0" - jest-validate "^27.1.0" - micromatch "^4.0.4" - pretty-format "^27.1.0" - -jest-diff@^25.2.1: - version "25.5.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" - integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== - dependencies: - chalk "^3.0.0" - diff-sequences "^25.2.6" - jest-get-type "^25.2.6" - pretty-format "^25.5.0" - -jest-diff@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" - integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== - dependencies: - chalk "^4.0.0" - diff-sequences "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" - -jest-diff@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.1.0.tgz#c7033f25add95e2218f3c7f4c3d7b634ab6b3cd2" - integrity sha512-rjfopEYl58g/SZTsQFmspBODvMSytL16I+cirnScWTLkQVXYVZfxm78DFfdIIXc05RCYuGjxJqrdyG4PIFzcJg== - dependencies: - chalk "^4.0.0" - diff-sequences "^27.0.6" - jest-get-type "^27.0.6" - pretty-format "^27.1.0" - -jest-docblock@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" - integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w== - dependencies: - detect-newline "^3.0.0" - -jest-docblock@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.0.6.tgz#cc78266acf7fe693ca462cbbda0ea4e639e4e5f3" - integrity sha512-Fid6dPcjwepTFraz0YxIMCi7dejjJ/KL9FBjPYhBp4Sv1Y9PdhImlKZqYU555BlN4TQKaTc+F2Av1z+anVyGkA== - dependencies: - detect-newline "^3.0.0" - -jest-each@^26.6.0, jest-each@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" - integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== - dependencies: - "@jest/types" "^26.6.2" - chalk "^4.0.0" - jest-get-type "^26.3.0" - jest-util "^26.6.2" - pretty-format "^26.6.2" - -jest-each@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.1.0.tgz#36ac75f7aeecb3b8da2a8e617ccb30a446df408c" - integrity sha512-K/cNvQlmDqQMRHF8CaQ0XPzCfjP5HMJc2bIJglrIqI9fjwpNqITle63IWE+wq4p+3v+iBgh7Wq0IdGpLx5xjDg== - dependencies: - "@jest/types" "^27.1.0" - chalk "^4.0.0" - jest-get-type "^27.0.6" - jest-util "^27.1.0" - pretty-format "^27.1.0" - -jest-environment-jsdom@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" - integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== - dependencies: - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - jest-mock "^26.6.2" - jest-util "^26.6.2" - jsdom "^16.4.0" - -jest-environment-jsdom@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.1.0.tgz#5fb3eb8a67e02e6cc623640388d5f90e33075f18" - integrity sha512-JbwOcOxh/HOtsj56ljeXQCUJr3ivnaIlM45F5NBezFLVYdT91N5UofB1ux2B1CATsQiudcHdgTaeuqGXJqjJYQ== - dependencies: - "@jest/environment" "^27.1.0" - "@jest/fake-timers" "^27.1.0" - "@jest/types" "^27.1.0" - "@types/node" "*" - jest-mock "^27.1.0" - jest-util "^27.1.0" - jsdom "^16.6.0" - -jest-environment-node@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" - integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== - dependencies: - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - jest-mock "^26.6.2" - jest-util "^26.6.2" - -jest-environment-node@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.1.0.tgz#feea6b765f1fd4582284d4f1007df2b0a8d15b7f" - integrity sha512-JIyJ8H3wVyM4YCXp7njbjs0dIT87yhGlrXCXhDKNIg1OjurXr6X38yocnnbXvvNyqVTqSI4M9l+YfPKueqL1lw== - dependencies: - "@jest/environment" "^27.1.0" - "@jest/fake-timers" "^27.1.0" - "@jest/types" "^27.1.0" - "@types/node" "*" - jest-mock "^27.1.0" - jest-util "^27.1.0" - -jest-get-type@^25.2.6: - version "25.2.6" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" - integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== - -jest-get-type@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" - integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== - -jest-get-type@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe" - integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg== - -jest-haste-map@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" - integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== - dependencies: - "@jest/types" "^26.6.2" - "@types/graceful-fs" "^4.1.2" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.4" - jest-regex-util "^26.0.0" - jest-serializer "^26.6.2" - jest-util "^26.6.2" - jest-worker "^26.6.2" - micromatch "^4.0.2" - sane "^4.0.3" - walker "^1.0.7" - optionalDependencies: - fsevents "^2.1.2" - -jest-haste-map@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.1.0.tgz#a39f456823bd6a74e3c86ad25f6fa870428326bf" - integrity sha512-7mz6LopSe+eA6cTFMf10OfLLqRoIPvmMyz5/OnSXnHO7hB0aDP1iIeLWCXzAcYU5eIJVpHr12Bk9yyq2fTW9vg== - dependencies: - "@jest/types" "^27.1.0" - "@types/graceful-fs" "^4.1.2" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.4" - jest-regex-util "^27.0.6" - jest-serializer "^27.0.6" - jest-util "^27.1.0" - jest-worker "^27.1.0" - micromatch "^4.0.4" - walker "^1.0.7" - optionalDependencies: - fsevents "^2.3.2" - -jest-jasmine2@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" - integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== - dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^26.6.2" - "@jest/source-map" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - expect "^26.6.2" - is-generator-fn "^2.0.0" - jest-each "^26.6.2" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-runtime "^26.6.3" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - pretty-format "^26.6.2" - throat "^5.0.0" - -jest-jasmine2@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.1.0.tgz#324a3de0b2ee20d238b2b5b844acc4571331a206" - integrity sha512-Z/NIt0wBDg3przOW2FCWtYjMn3Ip68t0SL60agD/e67jlhTyV3PIF8IzT9ecwqFbeuUSO2OT8WeJgHcalDGFzQ== - dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^27.1.0" - "@jest/source-map" "^27.0.6" - "@jest/test-result" "^27.1.0" - "@jest/types" "^27.1.0" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - expect "^27.1.0" - is-generator-fn "^2.0.0" - jest-each "^27.1.0" - jest-matcher-utils "^27.1.0" - jest-message-util "^27.1.0" - jest-runtime "^27.1.0" - jest-snapshot "^27.1.0" - jest-util "^27.1.0" - pretty-format "^27.1.0" - throat "^6.0.1" - -jest-leak-detector@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" - integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== - dependencies: - jest-get-type "^26.3.0" - pretty-format "^26.6.2" - -jest-leak-detector@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.1.0.tgz#fe7eb633c851e06280ec4dd248067fe232c00a79" - integrity sha512-oHvSkz1E80VyeTKBvZNnw576qU+cVqRXUD3/wKXh1zpaki47Qty2xeHg2HKie9Hqcd2l4XwircgNOWb/NiGqdA== - dependencies: - jest-get-type "^27.0.6" - pretty-format "^27.1.0" - -jest-matcher-utils@^26.6.0, jest-matcher-utils@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" - integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== - dependencies: - chalk "^4.0.0" - jest-diff "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" - -jest-matcher-utils@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.1.0.tgz#68afda0885db1f0b9472ce98dc4c535080785301" - integrity sha512-VmAudus2P6Yt/JVBRdTPFhUzlIN8DYJd+et5Rd9QDsO/Z82Z4iwGjo43U8Z+PTiz8CBvKvlb6Fh3oKy39hykkQ== - dependencies: - chalk "^4.0.0" - jest-diff "^27.1.0" - jest-get-type "^27.0.6" - pretty-format "^27.1.0" - -jest-message-util@^25.5.0: - version "25.5.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-25.5.0.tgz#ea11d93204cc7ae97456e1d8716251185b8880ea" - integrity sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA== - dependencies: - "@babel/code-frame" "^7.0.0" - "@jest/types" "^25.5.0" - "@types/stack-utils" "^1.0.1" - chalk "^3.0.0" - graceful-fs "^4.2.4" - micromatch "^4.0.2" - slash "^3.0.0" - stack-utils "^1.0.1" - -jest-message-util@^26.6.0, jest-message-util@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" - integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== - dependencies: - "@babel/code-frame" "^7.0.0" - "@jest/types" "^26.6.2" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.4" - micromatch "^4.0.2" - pretty-format "^26.6.2" - slash "^3.0.0" - stack-utils "^2.0.2" - -jest-message-util@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.1.0.tgz#e77692c84945d1d10ef00afdfd3d2c20bd8fb468" - integrity sha512-Eck8NFnJ5Sg36R9XguD65cf2D5+McC+NF5GIdEninoabcuoOfWrID5qJhufq5FB0DRKoiyxB61hS7MKoMD0trQ== - dependencies: - "@babel/code-frame" "^7.12.13" - "@jest/types" "^27.1.0" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.4" - micromatch "^4.0.4" - pretty-format "^27.1.0" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-mock@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" - integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== - dependencies: - "@jest/types" "^26.6.2" - "@types/node" "*" - -jest-mock@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.1.0.tgz#7ca6e4d09375c071661642d1c14c4711f3ab4b4f" - integrity sha512-iT3/Yhu7DwAg/0HvvLCqLvrTKTRMyJlrrfJYWzuLSf9RCAxBoIXN3HoymZxMnYsC3eD8ewGbUa9jUknwBenx2w== - dependencies: - "@jest/types" "^27.1.0" - "@types/node" "*" - -jest-pnp-resolver@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" - integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== - -jest-regex-util@^25.2.1: - version "25.2.6" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-25.2.6.tgz#d847d38ba15d2118d3b06390056028d0f2fd3964" - integrity sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw== - -jest-regex-util@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" - integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== - -jest-regex-util@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.0.6.tgz#02e112082935ae949ce5d13b2675db3d8c87d9c5" - integrity sha512-SUhPzBsGa1IKm8hx2F4NfTGGp+r7BXJ4CulsZ1k2kI+mGLG+lxGrs76veN2LF/aUdGosJBzKgXmNCw+BzFqBDQ== - -jest-resolve-dependencies@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" - integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== - dependencies: - "@jest/types" "^26.6.2" - jest-regex-util "^26.0.0" - jest-snapshot "^26.6.2" - -jest-resolve-dependencies@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.1.0.tgz#d32ea4a2c82f76410f6157d0ec6cde24fbff2317" - integrity sha512-Kq5XuDAELuBnrERrjFYEzu/A+i2W7l9HnPWqZEeKGEQ7m1R+6ndMbdXCVCx29Se1qwLZLgvoXwinB3SPIaitMQ== - dependencies: - "@jest/types" "^27.1.0" - jest-regex-util "^27.0.6" - jest-snapshot "^27.1.0" - -jest-resolve@26.6.0: - version "26.6.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.0.tgz#070fe7159af87b03e50f52ea5e17ee95bbee40e1" - integrity sha512-tRAz2bwraHufNp+CCmAD8ciyCpXCs1NQxB5EJAmtCFy6BN81loFEGWKzYu26Y62lAJJe4X4jg36Kf+NsQyiStQ== - dependencies: - "@jest/types" "^26.6.0" - chalk "^4.0.0" - graceful-fs "^4.2.4" - jest-pnp-resolver "^1.2.2" - jest-util "^26.6.0" - read-pkg-up "^7.0.1" - resolve "^1.17.0" - slash "^3.0.0" - -jest-resolve@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" - integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== - dependencies: - "@jest/types" "^26.6.2" - chalk "^4.0.0" - graceful-fs "^4.2.4" - jest-pnp-resolver "^1.2.2" - jest-util "^26.6.2" - read-pkg-up "^7.0.1" - resolve "^1.18.1" - slash "^3.0.0" - -jest-resolve@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.1.0.tgz#bb22303c9e240cccdda28562e3c6fbcc6a23ac86" - integrity sha512-TXvzrLyPg0vLOwcWX38ZGYeEztSEmW+cQQKqc4HKDUwun31wsBXwotRlUz4/AYU/Fq4GhbMd/ileIWZEtcdmIA== - dependencies: - "@jest/types" "^27.1.0" - chalk "^4.0.0" - escalade "^3.1.1" - graceful-fs "^4.2.4" - jest-haste-map "^27.1.0" - jest-pnp-resolver "^1.2.2" - jest-util "^27.1.0" - jest-validate "^27.1.0" - resolve "^1.20.0" - slash "^3.0.0" - -jest-runner@^26.6.0, jest-runner@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" - integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== - dependencies: - "@jest/console" "^26.6.2" - "@jest/environment" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - chalk "^4.0.0" - emittery "^0.7.1" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-config "^26.6.3" - jest-docblock "^26.0.0" - jest-haste-map "^26.6.2" - jest-leak-detector "^26.6.2" - jest-message-util "^26.6.2" - jest-resolve "^26.6.2" - jest-runtime "^26.6.3" - jest-util "^26.6.2" - jest-worker "^26.6.2" - source-map-support "^0.5.6" - throat "^5.0.0" - -jest-runner@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.1.0.tgz#1b28d114fb3b67407b8354c9385d47395e8ff83f" - integrity sha512-ZWPKr9M5w5gDplz1KsJ6iRmQaDT/yyAFLf18fKbb/+BLWsR1sCNC2wMT0H7pP3gDcBz0qZ6aJraSYUNAGSJGaw== - dependencies: - "@jest/console" "^27.1.0" - "@jest/environment" "^27.1.0" - "@jest/test-result" "^27.1.0" - "@jest/transform" "^27.1.0" - "@jest/types" "^27.1.0" - "@types/node" "*" - chalk "^4.0.0" - emittery "^0.8.1" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-docblock "^27.0.6" - jest-environment-jsdom "^27.1.0" - jest-environment-node "^27.1.0" - jest-haste-map "^27.1.0" - jest-leak-detector "^27.1.0" - jest-message-util "^27.1.0" - jest-resolve "^27.1.0" - jest-runtime "^27.1.0" - jest-util "^27.1.0" - jest-worker "^27.1.0" - source-map-support "^0.5.6" - throat "^6.0.1" - -jest-runtime@^26.6.0, jest-runtime@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" - integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== - dependencies: - "@jest/console" "^26.6.2" - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/globals" "^26.6.2" - "@jest/source-map" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/yargs" "^15.0.0" - chalk "^4.0.0" - cjs-module-lexer "^0.6.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.3" - graceful-fs "^4.2.4" - jest-config "^26.6.3" - jest-haste-map "^26.6.2" - jest-message-util "^26.6.2" - jest-mock "^26.6.2" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" - slash "^3.0.0" - strip-bom "^4.0.0" - yargs "^15.4.1" - -jest-runtime@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.1.0.tgz#1a98d984ffebc16a0b4f9eaad8ab47c00a750cf5" - integrity sha512-okiR2cpGjY0RkWmUGGado6ETpFOi9oG3yV0CioYdoktkVxy5Hv0WRLWnJFuArSYS8cHMCNcceUUMGiIfgxCO9A== - dependencies: - "@jest/console" "^27.1.0" - "@jest/environment" "^27.1.0" - "@jest/fake-timers" "^27.1.0" - "@jest/globals" "^27.1.0" - "@jest/source-map" "^27.0.6" - "@jest/test-result" "^27.1.0" - "@jest/transform" "^27.1.0" - "@jest/types" "^27.1.0" - "@types/yargs" "^16.0.0" - chalk "^4.0.0" - cjs-module-lexer "^1.0.0" - collect-v8-coverage "^1.0.0" - execa "^5.0.0" - exit "^0.1.2" - glob "^7.1.3" - graceful-fs "^4.2.4" - jest-haste-map "^27.1.0" - jest-message-util "^27.1.0" - jest-mock "^27.1.0" - jest-regex-util "^27.0.6" - jest-resolve "^27.1.0" - jest-snapshot "^27.1.0" - jest-util "^27.1.0" - jest-validate "^27.1.0" - slash "^3.0.0" - strip-bom "^4.0.0" - yargs "^16.0.3" - -jest-serializer@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" - integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== - dependencies: - "@types/node" "*" - graceful-fs "^4.2.4" - -jest-serializer@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.0.6.tgz#93a6c74e0132b81a2d54623251c46c498bb5bec1" - integrity sha512-PtGdVK9EGC7dsaziskfqaAPib6wTViY3G8E5wz9tLVPhHyiDNTZn/xjZ4khAw+09QkoOVpn7vF5nPSN6dtBexA== - dependencies: - "@types/node" "*" - graceful-fs "^4.2.4" - -jest-snapshot@^26.6.0, jest-snapshot@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" - integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== - dependencies: - "@babel/types" "^7.0.0" - "@jest/types" "^26.6.2" - "@types/babel__traverse" "^7.0.4" - "@types/prettier" "^2.0.0" - chalk "^4.0.0" - expect "^26.6.2" - graceful-fs "^4.2.4" - jest-diff "^26.6.2" - jest-get-type "^26.3.0" - jest-haste-map "^26.6.2" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-resolve "^26.6.2" - natural-compare "^1.4.0" - pretty-format "^26.6.2" - semver "^7.3.2" - -jest-snapshot@^27.1.0: - version "27.1.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.1.0.tgz#2a063ab90064017a7e9302528be7eaea6da12d17" - integrity sha512-eaeUBoEjuuRwmiRI51oTldUsKOohB1F6fPqWKKILuDi/CStxzp2IWekVUXbuHHoz5ik33ioJhshiHpgPFbYgcA== - dependencies: - "@babel/core" "^7.7.2" - "@babel/generator" "^7.7.2" - "@babel/parser" "^7.7.2" - "@babel/plugin-syntax-typescript" "^7.7.2" - "@babel/traverse" "^7.7.2" - "@babel/types" "^7.0.0" - "@jest/transform" "^27.1.0" - "@jest/types" "^27.1.0" - "@types/babel__traverse" "^7.0.4" - "@types/prettier" "^2.1.5" - babel-preset-current-node-syntax "^1.0.0" - chalk "^4.0.0" - expect "^27.1.0" - graceful-fs "^4.2.4" - jest-diff "^27.1.0" - jest-get-type "^27.0.6" - jest-haste-map "^27.1.0" - jest-matcher-utils "^27.1.0" - jest-message-util "^27.1.0" - jest-resolve "^27.1.0" - jest-util "^27.1.0" - natural-compare "^1.4.0" - pretty-format "^27.1.0" - semver "^7.3.2" - -jest-transform-stub@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz#19018b0851f7568972147a5d60074b55f0225a7d" - integrity sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg== - -jest-util@^25.5.0: - version "25.5.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.5.0.tgz#31c63b5d6e901274d264a4fec849230aa3fa35b0" - integrity sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA== - dependencies: - "@jest/types" "^25.5.0" - chalk "^3.0.0" - graceful-fs "^4.2.4" - is-ci "^2.0.0" - make-dir "^3.0.0" - -jest-util@^26.6.0, jest-util@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" - integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== - dependencies: - "@jest/types" "^26.6.2" - "@types/node" "*" - chalk "^4.0.0" - graceful-fs "^4.2.4" - is-ci "^2.0.0" - micromatch "^4.0.2" + "@types/node" "*" + chalk "^4.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + micromatch "^4.0.2" jest-util@^27.0.0, jest-util@^27.1.0: version "27.1.0" @@ -13633,11 +12833,6 @@ lodash.camelcase@^3.0.1: dependencies: lodash._createcompounder "^3.0.0" -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= - lodash.capitalize@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9" @@ -14077,23 +13272,6 @@ meow@^6.1.1: type-fest "^0.13.1" yargs-parser "^18.1.3" -meow@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306" - integrity sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA== - dependencies: - "@types/minimist" "^1.2.0" - camelcase-keys "^6.2.2" - decamelize-keys "^1.1.0" - hard-rejection "^2.1.0" - minimist-options "4.1.0" - normalize-package-data "^2.5.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.0.0" - type-fest "^0.13.1" - yargs-parser "^18.1.3" - meow@^8.0.0: version "8.1.2" resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" @@ -14464,24 +13642,6 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" -ms@^2.0.0, ms@^2.1.1, ms@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -multicast-dns-service-types@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" - integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= - -multicast-dns@^6.0.1: - version "6.2.3" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" - integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== - dependencies: - dns-packet "^1.3.1" - thunky "^1.0.2" - mute-stream@0.0.7, mute-stream@~0.0.4: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -15732,6 +14892,11 @@ pnp-webpack-plugin@1.6.4: dependencies: ts-pnp "^1.1.6" +point-in-polygon@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/point-in-polygon/-/point-in-polygon-1.1.0.tgz#b0af2616c01bdee341cbf2894df643387ca03357" + integrity sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw== + portfinder@^1.0.26: version "1.0.28" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" @@ -16843,21 +16008,6 @@ prompts@2.4.0: kleur "^3.0.3" sisteransi "^1.0.5" -promise@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/promise/-/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e" - integrity sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q== - dependencies: - asap "~2.0.6" - -prompts@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" - integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - prompts@^2.0.1: version "2.4.1" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.1.tgz#befd3b1195ba052f9fd2fde8a486c4e82ee77f61" @@ -21369,8 +20519,3 @@ yup@^0.29.3: property-expr "^2.0.2" synchronous-promise "^2.0.13" toposort "^2.0.2" - -zwitch@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" - integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== From 4d9c80ffab393628a577f6109987a944aafc8cf9 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 5 Jan 2022 12:22:09 +0100 Subject: [PATCH 084/270] chore(deps): upgrade otp-ui --- package.json | 4 ++-- yarn.lock | 34 ++++++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 0afe9cf68..8494d6c0e 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,14 @@ "dependencies": { "@auth0/auth0-react": "^1.1.0", "@opentripplanner/base-map": "^2.0.0", - "@opentripplanner/core-utils": "^4.6.0", + "@opentripplanner/core-utils": "^4.7.0", "@opentripplanner/endpoints-overlay": "^1.3.0", "@opentripplanner/from-to-location-picker": "^1.3.0", "@opentripplanner/geocoder": "^1.1.1", "@opentripplanner/humanize-distance": "^1.1.0", "@opentripplanner/icons": "^1.2.0", "@opentripplanner/itinerary-body": "^2.7.0", - "@opentripplanner/location-field": "^1.8.0", + "@opentripplanner/location-field": "^1.11.1", "@opentripplanner/location-icon": "^1.4.0", "@opentripplanner/park-and-ride-overlay": "^1.2.2", "@opentripplanner/printable-itinerary": "^1.3.1", diff --git a/yarn.lock b/yarn.lock index 2a6c9a3b2..e2c871bc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2429,7 +2429,7 @@ "@opentripplanner/core-utils" "^4.1.0" prop-types "^15.7.2" -"@opentripplanner/core-utils@^4.1.0", "@opentripplanner/core-utils@^4.2.0": +"@opentripplanner/core-utils@^4.1.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-4.4.0.tgz#3eb8f9718d32ce50e3b79d975d682559c5a7b0bf" integrity sha512-qCd+NXa0PxwcN2DFR0HX1u/76RzQwHe2r0VGcaOS4GxvQ5HEL4zPyaT+lkxAyE6AFQAvNynyofhxlnMwbxtqEA== @@ -2447,7 +2447,7 @@ prop-types "^15.7.2" qs "^6.9.1" -"@opentripplanner/core-utils@^4.5.0", "@opentripplanner/core-utils@^4.6.0": +"@opentripplanner/core-utils@^4.5.0": version "4.6.0" resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-4.6.0.tgz#f0f9924a2312b89a3f84d0a34c1a2d9f6c1ca615" integrity sha512-LQs8BrwqFXKHJOgRduKic9wEGqnNAcyrypTgKmx//Jzz6OmZyXNskrFNJwLR6HIgsps7isN/7hi3NuVFticgIA== @@ -2465,6 +2465,24 @@ prop-types "^15.7.2" qs "^6.9.1" +"@opentripplanner/core-utils@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-4.7.0.tgz#0a59f26344702e6a9b45f977c338c58d1773dc45" + integrity sha512-YqGbIG1vnZSaBsZO3ss0IZ16Tdx6Ze29iHOVO+kC3Plqu7HPv7/lhzA/3qCqlTLwNp+rkfsDlv06H/Gqr1mjHg== + dependencies: + "@mapbox/polyline" "^1.1.0" + "@opentripplanner/geocoder" "^1.1.2" + "@styled-icons/foundation" "^10.34.0" + "@turf/along" "^6.0.1" + bowser "^2.7.0" + date-fns "^2.23.0" + date-fns-tz "^1.1.4" + lodash.clonedeep "^4.5.0" + lodash.isequal "^4.5.0" + moment "^2.24.0" + prop-types "^15.7.2" + qs "^6.9.1" + "@opentripplanner/endpoints-overlay@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@opentripplanner/endpoints-overlay/-/endpoints-overlay-1.3.0.tgz#cda25f81963d28a0580d4737f82798de2a143904" @@ -2532,14 +2550,14 @@ react-resize-detector "^4.2.1" velocity-react "^1.4.3" -"@opentripplanner/location-field@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@opentripplanner/location-field/-/location-field-1.8.0.tgz#a210c9b7fbddd7a503b1077bdfbcfdd3c1dca579" - integrity sha512-0CKWy9kV/odd0mpSSCPm+F/XinYkus5xUHXsBjKcN4Fc5y+uA9mR9pfXaL7ztNARbdQMWwtPhenzsO+qae8D/Q== +"@opentripplanner/location-field@^1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@opentripplanner/location-field/-/location-field-1.11.1.tgz#78ed05d1191f2aed3a7253841627ed39c2425575" + integrity sha512-zVBSp9K693UJZrU0yLUqGsZHIBGQv0Y9VTKPYzHtBOrLjV6rnbAClRFWzl75eVXL/OIP0uf79SagAinePRwfWA== dependencies: "@conveyal/geocoder-arcgis-geojson" "^0.0.3" - "@opentripplanner/core-utils" "^4.2.0" - "@opentripplanner/geocoder" "^1.1.1" + "@opentripplanner/core-utils" "^4.5.0" + "@opentripplanner/geocoder" "^1.1.2" "@opentripplanner/humanize-distance" "^1.1.0" "@opentripplanner/location-icon" "^1.4.0" "@styled-icons/fa-solid" "^10.34.0" From 37d101343a032a8b8e75c7e31280bd5145124e4d Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 7 Jan 2022 13:08:14 +0100 Subject: [PATCH 085/270] refactor(actions/api): merge headers to support graphql calls with API key headers --- lib/actions/api.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index e3bccbd71..f7ca133ca 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -1283,9 +1283,16 @@ function createQueryAction( let payload try { + // Need to merge headers to support graphQL POST request with an api key + const mergedHeaders = { + ...getOtpFetchOptions(state)?.headers, + ...options.fetchOptions?.headers + } + const response = await fetch(url, { ...getOtpFetchOptions(state), - ...options.fetchOptions + ...options.fetchOptions, + headers: mergedHeaders }) if (response.status >= 400) { // If a second endpoint is configured, try that before failing From 80e514b7a4134c60600fd3a92b8b48474c920f66 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Fri, 7 Jan 2022 18:38:11 +0100 Subject: [PATCH 086/270] refactor(actions/api): correctly use v2 server as v1 failover --- lib/actions/api.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index f7ca133ca..168a5e749 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -383,11 +383,17 @@ export function vehicleRentalQuery( const findStopResponse = createAction('FIND_STOP_RESPONSE') const findStopError = createAction('FIND_STOP_ERROR') +// Finds a stop using the v1 endpoint, and retries using the v2 endpoint if that fails. +// We can't check for the v2 flag on a stop because we don't have the entire stop object export function findStop(params) { return createQueryAction( `index/stops/${params.stopId}`, findStopResponse, - findStopError, + () => { + // The error doesn't have to be fired since the graphql fetch + // also returns an error + return fetchGraphQLOnlyStopInfo(params.stopId) + }, { noThrottle: true, postprocess: (payload, dispatch) => { @@ -400,6 +406,9 @@ export function findStop(params) { } export function fetchStopInfo(stop) { + if (stop.v2) { + return fetchGraphQLOnlyStopInfo(stop) + } return async function (dispatch, getState) { await dispatch(findStop({ stopId: stop.stopId })) const state = getState() @@ -465,7 +474,7 @@ export function fetchGraphQLOnlyStopInfo(stopId) { } const { stop } = payload.data const color = stop.routes?.length > 0 && `#${stop.routes[0].color}` - return { ...stop, color } + return { ...stop, color, v2: true } } } ) @@ -873,7 +882,7 @@ export function findRoute(params) { const color = stop.routes?.length > 0 && `#${stop.routes[0].color}` if (stop.routes) delete stop.routes - return { ...stop, color } + return { ...stop, color, v2: true } }) routePatterns[pattern.id] = { ...pattern, @@ -1295,21 +1304,21 @@ function createQueryAction( headers: mergedHeaders }) if (response.status >= 400) { - // If a second endpoint is configured, try that before failing - if (!!config.api_v2 && !options.v2) { - return dispatch( - createQueryAction(endpoint, responseAction, errorAction, { - ...options, - v2: true - }) - ) - } const error = new Error('Received error from server') error.response = response throw error } payload = await response.json() } catch (err) { + // If a second endpoint is configured, try that before failing + if (!!config.api_v2 && !options.v2) { + return dispatch( + createQueryAction(endpoint, responseAction, errorAction, { + ...options, + v2: true + }) + ) + } return dispatch(errorAction(err)) } From 01ca5af6312adc2a4bd0a3e854c655d2fa378767 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 7 Jan 2022 15:48:19 -0500 Subject: [PATCH 087/270] fix(CallTakerPanel): Add lost narrative scrolling feature. --- lib/components/app/call-taker-panel.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index cd196bc2f..22b5733fa 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -287,10 +287,17 @@ class CallTakerPanel extends Component { )}
    + {/* FIXME: Achieve this partially scrolling layout as below + without using absolute positioning and by sharing styles with BatchRoutingPanel. */}
    From f6b9f7143137f4384601e0e4fb84dff1b8e661ed Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 11 Jan 2022 15:59:37 -0500 Subject: [PATCH 088/270] fix(call taker DateTimeOptions): Fix frozen time input. --- .../form/call-taker/date-time-options.js | 106 ++++++++++-------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/lib/components/form/call-taker/date-time-options.js b/lib/components/form/call-taker/date-time-options.js index 4a469dee5..c4dba40ef 100644 --- a/lib/components/form/call-taker/date-time-options.js +++ b/lib/components/form/call-taker/date-time-options.js @@ -1,27 +1,30 @@ +/* eslint-disable react/prop-types */ +import { injectIntl } from 'react-intl' import { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } from '@opentripplanner/core-utils/lib/time' +import { OverlayTrigger, Tooltip } from 'react-bootstrap' import moment from 'moment' import React, { Component } from 'react' -import { OverlayTrigger, Tooltip } from 'react-bootstrap' -import { injectIntl } from 'react-intl' -const departureOptions = [ - { - // Default option. - childMessageId: 'components.DateTimeOptions.now', - value: 'NOW' - }, - { - childMessageId: 'components.DateTimeOptions.departAt', - value: 'DEPART' - }, - { - childMessageId: 'components.DateTimeOptions.arriveBy', - value: 'ARRIVE' - } -] +function getDepartureOptions(intl) { + return [ + { + // Default option. + text: intl.formatMessage({ id: 'components.DateTimeOptions.now' }), + value: 'NOW' + }, + { + text: intl.formatMessage({ id: 'components.DateTimeOptions.departAt' }), + value: 'DEPART' + }, + { + text: intl.formatMessage({ id: 'components.DateTimeOptions.arriveBy' }), + value: 'ARRIVE' + } + ] +} /** * Time formats passed to moment.js used to parse the user's time input. @@ -38,14 +41,14 @@ const SUPPORTED_TIME_FORMATS = [ 'ha', 'h', 'HH:mm' -].map(format => `YYYY-MM-DDT${format}`) +].map((format) => `YYYY-MM-DDT${format}`) /** * Convert input moment object to date/time query params in OTP API format. * @param {[type]} [time=moment(] [description] * @return {[type]} [description] */ -function momentToQueryParams (time = moment()) { +function momentToQueryParams(time = moment()) { return { date: time.format(OTP_API_DATE_FORMAT), time: time.format(OTP_API_TIME_FORMAT) @@ -70,18 +73,18 @@ class DateTimeOptions extends Component { timeInput: '' } - componentDidMount () { + componentDidMount() { if (this.props.departArrive === 'NOW') { this._startAutoRefresh() } } - componentWillUnmount () { + componentWillUnmount() { this._stopAutoRefresh() } - componentDidUpdate (prevProps) { - const {date, departArrive, time} = this.props + componentDidUpdate(prevProps) { + const { date, departArrive, time } = this.props const dateTime = this.dateTimeAsMoment() const parsedTime = this.parseInputAsTime(this.state.timeInput, date) // Update time input if time changes and the parsed time does not match what @@ -100,7 +103,7 @@ class DateTimeOptions extends Component { _updateTimeInput = (time = moment()) => // If auto-updating time input (for leave now), use short 24-hr format to // avoid writing a value that is too long for the time input's width. - this.setState({timeInput: time.format('H:mm')}) + this.setState({ timeInput: time.format('H:mm') }) _startAutoRefresh = () => { const timer = window.setInterval(this._refreshDateTime, 1000) @@ -109,7 +112,7 @@ class DateTimeOptions extends Component { _stopAutoRefresh = () => { window.clearInterval(this.state.timer) - this.setState({timer: null}) + this.setState({ timer: null }) } _refreshDateTime = () => { @@ -122,8 +125,8 @@ class DateTimeOptions extends Component { } } - _setDepartArrive = evt => { - const {value: departArrive} = evt.target + _setDepartArrive = (evt) => { + const { value: departArrive } = evt.target if (departArrive === 'NOW') { const now = moment() // If setting to leave now, update date/time and start auto refresh to keep @@ -142,14 +145,15 @@ class DateTimeOptions extends Component { } } - handleDateChange = evt => this.handleDateTimeChange({ date: evt.target.value }) + handleDateChange = (evt) => + this.handleDateTimeChange({ date: evt.target.value }) /** * Handler that should be used when date or time is manually updated. This * will also update the departArrive value if need be. */ - handleDateTimeChange = params => { - const {departArrive: prevDepartArrive} = this.props + handleDateTimeChange = (params) => { + const { departArrive: prevDepartArrive } = this.props // If previously set to leave now, change to depart at when time changes. if (prevDepartArrive === 'NOW') params.departArrive = 'DEPART' this.props.setQueryParam(params) @@ -158,25 +162,28 @@ class DateTimeOptions extends Component { /** * Select input string when time input is focused by user (for quick changes). */ - handleTimeFocus = evt => evt.target.select() + handleTimeFocus = (evt) => evt.target.select() - parseInputAsTime = (timeInput, date = moment().startOf('day').format('YYYY-MM-DD')) => { + parseInputAsTime = ( + timeInput, + date = moment().startOf('day').format('YYYY-MM-DD') + ) => { return moment(date + 'T' + timeInput, SUPPORTED_TIME_FORMATS) } dateTimeAsMoment = () => moment(`${this.props.date}T${this.props.time}`) - handleTimeChange = evt => { + handleTimeChange = (evt) => { if (this.state.timer) this._stopAutoRefresh() const timeInput = evt.target.value const parsedTime = this.parseInputAsTime(timeInput) this.handleDateTimeChange({ time: parsedTime.format(OTP_API_TIME_FORMAT) }) - this.setState({timeInput}) + this.setState({ timeInput }) } - render () { - const {departArrive, intl, onKeyDown} = this.props - const {timeInput} = this.state + render() { + const { departArrive, intl, onKeyDown } = this.props + const { timeInput } = this.state const dateTime = this.dateTimeAsMoment() return ( <> @@ -186,15 +193,21 @@ class DateTimeOptions extends Component { onKeyDown={onKeyDown} value={departArrive} > - {departureOptions.map(o => )} + {getDepartureOptions(intl).map(({ text, value }) => ( + + ))} {intl.formatTime(dateTime.unix())}} - placement='bottom' + overlay={ + {intl.formatTime(dateTime)} + } + placement="bottom" trigger={['focus', 'hover']} > From 82e62f08d89160bb9a5ce2e9c5854cb7b5fc8865 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 11 Jan 2022 17:17:43 -0500 Subject: [PATCH 089/270] fix(calltaker AdvancedOptions): Fix route display in preferred/banned routes. --- lib/components/app/call-taker-panel.js | 3 +- .../form/call-taker/advanced-options.js | 147 +++++++++++------- lib/components/viewers/route-viewer.js | 102 ++++++------ package.json | 1 + yarn.lock | 69 ++++---- 5 files changed, 184 insertions(+), 138 deletions(-) diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index cd196bc2f..164b2a63d 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -15,6 +15,7 @@ import * as formActions from '../../actions/form' import { getActiveSearch, getShowUserSettings, + getSortedFilteredRoutes, hasValidLocation } from '../../util/state' import { getGroupSize } from '../../util/call-taker' @@ -313,7 +314,7 @@ const mapStateToProps = (state) => { mainPanelContent: state.otp.ui.mainPanelContent, maxGroupSize: getGroupSize(request), modes: state.otp.config.modes, - routes: state.otp.transitIndex.routes, + routes: getSortedFilteredRoutes(state), showUserSettings, timeFormat: getTimeFormat(state.otp.config) } diff --git a/lib/components/form/call-taker/advanced-options.js b/lib/components/form/call-taker/advanced-options.js index d74d809d6..0bbb19757 100644 --- a/lib/components/form/call-taker/advanced-options.js +++ b/lib/components/form/call-taker/advanced-options.js @@ -1,15 +1,17 @@ +/* eslint-disable react/prop-types */ // FIXME: Remove the following eslint rule exception. /* eslint-disable jsx-a11y/label-has-for */ -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 { FormattedMessage, injectIntl } from 'react-intl' +import { hasBike } from '@opentripplanner/core-utils/lib/itinerary' +import { SubmodeSelector } from '@opentripplanner/trip-form' +import isEmpty from 'lodash.isempty' +import React, { Component } from 'react' import Select from 'react-select' import styled from 'styled-components' -import { modeButtonButtonCss } from '../styled' import { ComponentContext } from '../../../util/contexts' +import { modeButtonButtonCss } from '../styled' export const StyledSubmodeSelector = styled(SubmodeSelector)` ${TripFormClasses.SubmodeSelector.Row} { @@ -32,73 +34,75 @@ export const StyledSubmodeSelector = styled(SubmodeSelector)` } ${TripFormClasses.SubmodeSelector} { - ${modeButtonButtonCss} + ${modeButtonButtonCss} } ` -const metersToMiles = meters => Math.round(meters * 0.000621371 * 100) / 100 -const milesToMeters = miles => miles / 0.000621371 +const metersToMiles = (meters) => Math.round(meters * 0.000621371 * 100) / 100 +const milesToMeters = (miles) => miles / 0.000621371 class AdvancedOptions extends Component { - constructor (props) { + constructor(props) { super(props) this.state = { expandAdvanced: props.expandAdvanced, routeOptions: [], - transitModes: props.modes.transitModes.map(m => m.mode) + transitModes: props.modes.transitModes.map((m) => m.mode) } } static contextType = ComponentContext - componentDidMount () { + componentDidMount() { // Fetch routes for banned/preferred routes selectors. this.props.findRoutes() } - componentDidUpdate (prevProps) { - const {routes} = this.props + componentDidUpdate(prevProps) { + const { routes } = this.props // Once routes are available, map them to the route options format. - if (routes && !prevProps.routes) { + if (!isEmpty(routes) && isEmpty(prevProps.routes)) { const routeOptions = Object.values(routes).map(this.routeToOption) - this.setState({routeOptions}) + this.setState({ routeOptions }) } } - _setBannedRoutes = options => { - const bannedRoutes = options ? options.map(o => o.value).join(',') : '' + _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(',') : '' + _setPreferredRoutes = (options) => { + const preferredRoutes = options ? options.map((o) => o.value).join(',') : '' this.props.setQueryParam({ preferredRoutes }) } - _isBannedRouteOptionDisabled = option => { + _isBannedRouteOptionDisabled = (option) => { // Disable routes that are preferred already. const preferredRoutes = this.getRouteList('preferredRoutes') - return preferredRoutes && preferredRoutes.find(o => o.value === option.value) + return ( + preferredRoutes && preferredRoutes.find((o) => o.value === option.value) + ) } - _isPreferredRouteOptionDisabled = option => { + _isPreferredRouteOptionDisabled = (option) => { // Disable routes that are banned already. const bannedRoutes = this.getRouteList('bannedRoutes') - return bannedRoutes && bannedRoutes.find(o => o.value === option.value) + return bannedRoutes && bannedRoutes.find((o) => o.value === option.value) } - getDistanceStep = distanceInMeters => { + 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' + ? `${metersToMiles(distanceInMeters) % 1}` + : '1' } - _onSubModeChange = changedMode => { + _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(',') @@ -114,33 +118,37 @@ class AdvancedOptions extends Component { allModes.splice(i, 1) } // Update transit modes in state. - this.setState({transitModes}) + 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) }) + _setMaxWalkDistance = (evt) => { + this.props.setQueryParam({ + maxWalkDistance: milesToMeters(evt.target.value) + }) } /** * Get list of routes for specified key (either 'bannedRoutes' or * 'preferredRoutes'). */ - getRouteList = key => { + 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) + 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 => ({label: id, value: id})) + return idList.map((id) => ({ label: id, value: id })) } } - routeToOption = route => { + routeToOption = (route) => { if (!route) return null - const {id, longName, shortName} = route + 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? @@ -148,17 +156,17 @@ class AdvancedOptions extends Component { const label = shortName ? `${shortName}${longName ? ` - ${longName}` : ''}` : longName - return {label, value} + return { label, value } } - render () { + render() { const { currentQuery, intl, modes, onKeyDown } = this.props const { ModeIcon } = this.context - const {maxBikeDistance, maxWalkDistance, mode} = currentQuery + const { maxBikeDistance, maxWalkDistance, mode } = currentQuery const bannedRoutes = this.getRouteList('bannedRoutes') const preferredRoutes = this.getRouteList('preferredRoutes') - const transitModes = modes.transitModes.map(modeObj => { + const transitModes = modes.transitModes.map((modeObj) => { const modeStr = modeObj.mode || modeObj return { id: modeStr, @@ -171,32 +179,45 @@ class AdvancedOptions extends Component { const unitsString = '(mi.)' return (
    -
    -
    ) } diff --git a/lib/components/viewers/route-viewer.js b/lib/components/viewers/route-viewer.js index fc9d56e8e..1f2ef3bf9 100644 --- a/lib/components/viewers/route-viewer.js +++ b/lib/components/viewers/route-viewer.js @@ -1,28 +1,33 @@ -import React, { Component } from 'react' +/* eslint-disable react/prop-types */ import { Button } from 'react-bootstrap' import { connect } from 'react-redux' -import coreUtils from '@opentripplanner/core-utils' import { FormattedMessage, injectIntl } from 'react-intl' +import coreUtils from '@opentripplanner/core-utils' import PropTypes from 'prop-types' +import React, { Component } from 'react' import { ComponentContext } from '../../util/contexts' -import { getModeFromRoute } from '../../util/viewer' -import { getVehiclePositionsForRoute, findRoutes, findRoute } from '../../actions/api' -import Icon from '../util/icon' -import { getFormattedMode } from '../../util/i18n' +import { + findRoute, + findRoutes, + getVehiclePositionsForRoute +} from '../../actions/api' import { getAgenciesFromRoutes, getModesForActiveAgencyFilter, getSortedFilteredRoutes } from '../../util/state' +import { getFormattedMode } from '../../util/i18n' +import { getModeFromRoute } from '../../util/viewer' import { setMainPanelContent, - setViewedRoute, - setRouteViewerFilter + setRouteViewerFilter, + setViewedRoute } from '../../actions/ui' +import Icon from '../util/icon' +import { RouteName, RouteRow } from './RouteRow' import RouteDetails from './route-details' -import { RouteRow, RouteName } from './RouteRow' class RouteViewer extends Component { static propTypes = { @@ -45,7 +50,7 @@ class RouteViewer extends Component { }), // Routes have many more properties, but none are guaranteed viewedRouteObject: PropTypes.shape({ id: PropTypes.string }) - }; + } state = { /** Used to track if all routes have been rendered */ @@ -61,16 +66,20 @@ class RouteViewer extends Component { _backClicked = () => this.props.viewedRoute === null ? this.props.setMainPanelContent(null) - : this.props.setViewedRoute({...this.props.viewedRoute, patternId: null}); + : this.props.setViewedRoute({ + ...this.props.viewedRoute, + patternId: null + }) - componentDidMount () { + componentDidMount() { const { findRoutes } = this.props findRoutes() } /** Used to scroll to actively viewed route on load */ - componentDidUpdate () { + componentDidUpdate() { const { routes } = this.props + console.log(routes) const { initialRender } = this.state // Wait until more than the one route is present. @@ -81,7 +90,7 @@ class RouteViewer extends Component { window.requestAnimationFrame(() => { // Setting initialRender to false ensures that routeRow will not initiate // any more scrolling - this.setState({initialRender: false}) + this.setState({ initialRender: false }) }) } } @@ -111,7 +120,7 @@ class RouteViewer extends Component { this.props.setRouteViewerFilter({ search: value }) } - render () { + render() { const { agencies, filter, @@ -142,18 +151,18 @@ class RouteViewer extends Component { ) || {} return ( -
    +
    {/* Header Block */} -
    +
    {/* Always show back button, as we don't write a route anymore */} -
    -
    -
    +
    {route && ModeIcon && ( +
    {/* Header Block */} -
    +
    {/* Back button */} {!hideBackButton && ( -
    -
    )} {/* Header Text */} -
    - +
    +
    - +
    -
    - - +
    + + - - + +
    -
    +
    {sortedRoutes.map((route) => { // Find operator based on agency_id (extracted from OTP route ID). const operator = @@ -280,10 +289,8 @@ class RouteViewer extends Component { })} {/* check modes length to differentiate between loading and over-filtered */} {modes.length > 0 && sortedRoutes.length === 0 && ( - - + + )}
    @@ -302,7 +309,8 @@ const mapStateToProps = (state, ownProps) => { routes: getSortedFilteredRoutes(state), transitOperators: state.otp.config.transitOperators, viewedRoute: state.otp.ui.viewedRoute, - viewedRouteObject: state.otp.transitIndex.routes?.[state.otp.ui.viewedRoute?.routeId] + viewedRouteObject: + state.otp.transitIndex.routes?.[state.otp.ui.viewedRoute?.routeId] } } diff --git a/package.json b/package.json index 74415c515..f2e70ecf5 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "immutable": "^3.8.1", "isomorphic-fetch": "^2.2.1", "lodash.debounce": "^4.0.8", + "lodash.isempty": "^4.4.0", "lodash.isequal": "^4.5.0", "lodash.memoize": "^4.1.2", "moment": "^2.17.1", diff --git a/yarn.lock b/yarn.lock index 16b8684b0..895f7809c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -386,6 +386,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48" integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g== +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + "@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" @@ -419,6 +424,11 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/parser@^7.1.0": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.8.tgz#61c243a3875f7d0b0962b0543a33ece6ff2f1f17" + integrity sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw== + "@babel/parser@^7.11.5", "@babel/parser@^7.12.3", "@babel/parser@^7.14.5", "@babel/parser@^7.15.0", "@babel/parser@^7.7.0", "@babel/parser@^7.7.2": version "7.15.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.3.tgz#3416d9bea748052cfcb63dbcc27368105b1ed862" @@ -1417,6 +1427,14 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" +"@babel/types@^7.3.0": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1" + integrity sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -3169,7 +3187,22 @@ "@types/babel__template" "*" "@types/babel__traverse" "*" -"@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": version "7.14.2" resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43" integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== @@ -3602,30 +3635,6 @@ "@typescript-eslint/types" "4.30.0" eslint-visitor-keys "^2.0.0" -"@vue/compiler-core@3.2.6": - version "3.2.6" - resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.6.tgz#7162bb0670273f04566af0d353009187ab577915" - integrity sha512-vbwnz7+OhtLO5p5i630fTuQCL+MlUpEMTKHuX+RfetQ+3pFCkItt2JUH+9yMaBG2Hkz6av+T9mwN/acvtIwpbw== - dependencies: - "@babel/parser" "^7.15.0" - "@babel/types" "^7.15.0" - "@vue/shared" "3.2.6" - estree-walker "^2.0.2" - source-map "^0.6.1" - -"@vue/compiler-dom@3.2.6": - version "3.2.6" - resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.6.tgz#3764d7fe1a696e39fb2a3c9d638da0749e369b2d" - integrity sha512-+a/3oBAzFIXhHt8L5IHJOTP4a5egzvpXYyi13jR7CUYOR1S+Zzv7vBWKYBnKyJLwnrxTZnTQVjeHCgJq743XKg== - dependencies: - "@vue/compiler-core" "3.2.6" - "@vue/shared" "3.2.6" - -"@vue/shared@3.2.6": - version "3.2.6" - resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.6.tgz#2c22bae88fe2b7b59fa68a9c9c4cd60bae2c1794" - integrity sha512-uwX0Qs2e6kdF+WmxwuxJxOnKs/wEkMArtYpHSm7W+VY/23Tl8syMRyjnzEeXrNCAP0/8HZxEGkHJsjPEDNRuHw== - "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -8793,11 +8802,6 @@ estree-walker@^1.0.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== -estree-walker@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" - integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== - esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -12880,6 +12884,11 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.isempty@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" + integrity sha1-b4bL7di+TsmHvpqvM8loTbGzHn4= + lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" From 51cb418c5808a377ef48c3e291929efb26a17cd1 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 11 Jan 2022 18:12:28 -0500 Subject: [PATCH 090/270] fix(CallTakerPanel): Replace abs positioning with flex layout. --- lib/components/app/call-taker-panel.js | 278 ++++++++++----------- lib/components/viewers/viewer-container.js | 14 +- 2 files changed, 144 insertions(+), 148 deletions(-) diff --git a/lib/components/app/call-taker-panel.js b/lib/components/app/call-taker-panel.js index 22b5733fa..a8e7cef23 100644 --- a/lib/components/app/call-taker-panel.js +++ b/lib/components/app/call-taker-panel.js @@ -151,157 +151,151 @@ class CallTakerPanel extends Component { zIndex: 99999 } return ( - - {/* FIXME: should this be a styled component */} -
    -
    - +
    + + {Array.isArray(intermediatePlaces) && + intermediatePlaces.map((place, i) => { + return ( + this._addPlace(result, i)} + showClearButton={!mobile} + /> + ) + })} + +
    + } /> - {Array.isArray(intermediatePlaces) && - intermediatePlaces.map((place, i) => { - return ( - this._addPlace(result, i)} - showClearButton={!mobile} - /> - ) - })} - + +
    + -
    - } - /> -
    - { + this.setState({ transitModes }) + }} /> -
    - - + Plan + +
    +
    + {groupSize !== null && maxGroupSize && ( + + + + + )} + +
    + { - this.setState({ transitModes }) - }} + routes={routes} + setQueryParam={setQueryParam} /> -
    -
    - {groupSize !== null && maxGroupSize && ( - - - - - )} - -
    - -
    -
    -
    - {!activeSearch && !showPlanTripButton && showUserSettings && ( - - )} -
    - {/* FIXME: Achieve this partially scrolling layout as below - without using absolute positioning and by sharing styles with BatchRoutingPanel. */} -
    + {!activeSearch && !showPlanTripButton && showUserSettings && ( + + )} +
    + {/* FIXME: sharing styles with BatchRoutingPanel. */} + +
    ) } diff --git a/lib/components/viewers/viewer-container.js b/lib/components/viewers/viewer-container.js index 0f7239aae..def203808 100644 --- a/lib/components/viewers/viewer-container.js +++ b/lib/components/viewers/viewer-container.js @@ -1,21 +1,23 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import React, { Component } from 'react' import { MainPanelContent } from '../../actions/ui' +import RouteViewer from './route-viewer' import StopViewer from './stop-viewer' import TripViewer from './trip-viewer' -import RouteViewer from './route-viewer' class ViewerContainer extends Component { static propTypes = { + children: PropTypes.any, className: PropTypes.string, + style: PropTypes.object, uiState: PropTypes.object } - render () { - const { className, uiState } = this.props + render() { + const { className, style, uiState } = this.props // check for main panel content if (uiState.mainPanelContent === MainPanelContent.ROUTE_VIEWER) { @@ -35,7 +37,7 @@ class ViewerContainer extends Component { // otherwise, return default content return ( -
    +
    {this.props.children}
    ) From 6df872e51caa9d562533278d8da5122236219c53 Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 12 Jan 2022 14:23:15 +0100 Subject: [PATCH 091/270] refactor(stop-viewer): address PR feedback --- lib/actions/api.js | 12 ++++-------- lib/components/viewers/stop-viewer.js | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/actions/api.js b/lib/actions/api.js index 168a5e749..c5f572f11 100644 --- a/lib/actions/api.js +++ b/lib/actions/api.js @@ -260,8 +260,7 @@ function getJsonAndCheckResponse(res) { function getOtpFetchOptions(state, includeToken = false) { let apiBaseUrl, apiKey, token - // eslint-disable-next-line camelcase - const { api: api_v1, api_v2, persistence } = state.otp.config + const { api: apiV1, api_v2: apiV2, persistence } = state.otp.config if (persistence && persistence.otp_middleware) { // Prettier does not understand the syntax on this line // eslint-disable-next-line prettier/prettier @@ -269,8 +268,7 @@ function getOtpFetchOptions(state, includeToken = false) { } // Use only the v2 api if only a v2 api is presented - // eslint-disable-next-line camelcase - const api = api_v1 || api_v2 + const api = apiV1 || apiV2 const isOtpServerSameAsMiddleware = apiBaseUrl === api.host if (isOtpServerSameAsMiddleware) { @@ -714,10 +712,8 @@ export function findRoutes(params) { }, rewritePayload: (payload, dispatch, getState) => { const { otp } = getState() - // eslint-disable-next-line camelcase - const { api: api_v1, api_v2 } = otp.config - // eslint-disable-next-line camelcase - const usingV2Only = !api_v1 && !!api_v2 + const { api: apiV1, api_v2: apiV2 } = otp.config + const usingV2Only = !apiV1 && !!apiV2 const routes = {} payload.forEach((rte) => { // If coming from a v2 server, diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index 1667a96f8..f23c07a85 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -292,7 +292,7 @@ class StopViewer extends Component { not shown while stopData is loading */ if (stopData && stopData.geometries && stopData.geometries.geoJson?.type !== 'Point') { - contents =
    + contents =
    } } else if (scheduleView) { contents = ( From 1d230898197a4ef5860349c1a9f9efa2094779ab Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 12 Jan 2022 14:23:33 +0100 Subject: [PATCH 092/270] refactor(stop-viewer): extract flex check --- lib/components/viewers/stop-viewer.js | 9 +- lib/util/viewer.js | 177 +++++++++++++++----------- 2 files changed, 112 insertions(+), 74 deletions(-) diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js index f23c07a85..e018192fe 100644 --- a/lib/components/viewers/stop-viewer.js +++ b/lib/components/viewers/stop-viewer.js @@ -21,6 +21,7 @@ import Strong from '../util/strong-text' import LiveStopTimes from './live-stop-times' import StopScheduleTable from './stop-schedule-table' +import { stopIsFlex } from '../../util/viewer' const { getUserTimezone, OTP_API_DATE_FORMAT } = coreUtils.time @@ -184,6 +185,8 @@ class StopViewer extends Component { : stopData.id } + const isFlex = stopIsFlex(stopData) + let timezoneWarning if (!inHomeTimezone) { const timezoneCode = moment().tz(homeTimezone).format('z') @@ -215,7 +218,7 @@ class StopViewer extends Component { > - + }
    @@ -291,7 +294,7 @@ class StopViewer extends Component { The extra checks stopData are needed to ensure that the message is not shown while stopData is loading */ - if (stopData && stopData.geometries && stopData.geometries.geoJson?.type !== 'Point') { + if (stopIsFlex(stopData)) { contents =
    } } else if (scheduleView) { diff --git a/lib/util/viewer.js b/lib/util/viewer.js index ecea23666..9cd715515 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ import tinycolor from 'tinycolor2' import { isBlank } from './ui' @@ -6,15 +7,15 @@ import { isBlank } from './ui' * 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) { +export function getSecondsUntilDeparture(stopTime, useSchedule) { const departureTime = useSchedule ? stopTime.scheduledDeparture : stopTime.realtimeDeparture - return (departureTime + stopTime.serviceDay) - (Date.now() / 1000) + return departureTime + stopTime.serviceDay - Date.now() / 1000 } -export function getRouteIdForPattern (pattern) { +export function getRouteIdForPattern(pattern) { const patternIdParts = pattern.id.split(':') const routeId = patternIdParts[0] + ':' + patternIdParts[1] return routeId @@ -25,7 +26,7 @@ export function getRouteIdForPattern (pattern) { * (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. */ -function excludeLastStop (stopTime) { +function excludeLastStop(stopTime) { return stopTime.stopIndex < stopTime.stopCount - 1 } @@ -41,7 +42,7 @@ function excludeLastStop (stopTime) { * @param {*} routeId The route id to show for the specified route. * @returns true if route is not null. */ -export function routeIsValid (route, routeId) { +export function routeIsValid(route, routeId) { if (!route) { console.warn(`Cannot render stop times for missing route ID: ${routeId}`) return false @@ -49,7 +50,7 @@ export function routeIsValid (route, routeId) { return true } -export function getStopName (stop) { +export function getStopName(stop) { return `${stop.name} (${getStopCodeToDisplay(stop)})` } @@ -58,7 +59,7 @@ export function getStopName (stop) { * @param {Object} stop OTP stop entity * @return {string} stop_code or cleaned stop_id */ -export function getStopCodeToDisplay (stop) { +export function getStopCodeToDisplay(stop) { if (!stop) return '' return stop.code || stop.id.split(':')[1] } @@ -68,7 +69,7 @@ export function getStopCodeToDisplay (stop) { * @param {*} pattern pattern to extract headsign out of * @returns headsign of pattern */ -export function extractHeadsignFromPattern (pattern) { +export function extractHeadsignFromPattern(pattern) { let headsign = pattern.headsign // In case stop time headsign is blank, extract headsign from the pattern 'desc' attribute // (format: '49 to ()[ from ( { @@ -110,7 +111,7 @@ export function getStopTimesByPattern (stopData) { const id = `${routeId}-${headsign}` if (!(id in stopTimesByPattern)) { - const route = stopData.routes.find(r => r.id === routeId) + const route = stopData.routes.find((r) => r.id === routeId) // in some cases, the TriMet transit index will not return all routes // that serve a stop. Perhaps it doesn't return some routes if the // route only performs a drop-off at the stop... not quite sure. So a @@ -129,7 +130,8 @@ export function getStopTimesByPattern (stopData) { // Exclude the last stop, as the stop viewer doesn't show arrival times to a terminus stop. const filteredTimes = times.filter(excludeLastStop) - stopTimesByPattern[id].times = stopTimesByPattern[id].times.concat(filteredTimes) + stopTimesByPattern[id].times = + stopTimesByPattern[id].times.concat(filteredTimes) }) } return stopTimesByPattern @@ -140,21 +142,35 @@ export function getStopTimesByPattern (stopData) { * Route model returns the mode as an integer type whereas the RouteShort model * returns the mode string. */ -export function getModeFromRoute (route) { +export function getModeFromRoute(route) { const modeLookup = { 0: 'TRAM', // - Tram, Streetcar, Light rail. - 1: 'SUBWAY', // - Subway, Metro. - 2: 'RAIL', // - Rail. Used for intercity or long-distance travel. - 3: 'BUS', // - Bus. - 4: 'FERRY', // - Ferry. - 5: 'CABLE_CAR', // - Cable tram. - 6: 'GONDOLA', // - Gondola, etc. - 7: 'FUNICULAR', // - Funicular. + 1: 'SUBWAY', + // - Funicular. // TODO: 11 and 12 are not a part of OTP as of 2019-02-14, but for now just // associate them with bus/rail. // eslint-disable-next-line sort-keys - 11: 'BUS', // - Trolleybus. - 12: 'RAIL' // - Monorail. + 11: 'BUS', + + // - Trolleybus. + 12: 'RAIL', + + // - Subway, Metro. + 2: 'RAIL', + + // - Rail. Used for intercity or long-distance travel. + 3: 'BUS', + + // - Bus. + 4: 'FERRY', + + // - Ferry. + 5: 'CABLE_CAR', + + // - Cable tram. + 6: 'GONDOLA', + // - Gondola, etc. + 7: 'FUNICULAR' // - Monorail. } return route.mode || modeLookup[route.type] } @@ -170,26 +186,28 @@ export const getModeFromStop = (stop) => { return modes[0] } if (modes.length > 1) { - const stopModeCounts = modes - .reduce((modes, cur) => { - if (!cur) { - return modes - } + const stopModeCounts = modes.reduce((modes, cur) => { + if (!cur) { + return modes + } - if (!modes[cur]) { - modes[cur] = 0 - } - modes[cur]++ + if (!modes[cur]) { + modes[cur] = 0 + } + modes[cur]++ - return modes - }, {}) + return modes + }, {}) // Get stop mode by getting most common stop mode - return stopModeCounts && Object.keys(stopModeCounts) - // Sort by mode occurrence - .sort((a, b) => { - return stopModeCounts[a] - stopModeCounts[b] - })[0] + return ( + stopModeCounts && + Object.keys(stopModeCounts) + // Sort by mode occurrence + .sort((a, b) => { + return stopModeCounts[a] - stopModeCounts[b] + })[0] + ) } } @@ -197,22 +215,22 @@ export const getModeFromStop = (stop) => { * Comparator to sort stop times by their departure times * (in chronological order - 9:13am, 9:15am, etc.) */ -export function stopTimeComparator (a, b) { +export function stopTimeComparator(a, b) { const aTime = a.serviceDay + a.scheduledDeparture const bTime = b.serviceDay + b.scheduledDeparture return aTime - bTime } /** - * Finds the stop time corresponding to the first departure - * (the closest departure past the current time). - */ -export function getFirstDepartureFromNow (stopTimes) { + * Finds the stop time corresponding to the first departure + * (the closest departure past the current time). + */ +export function getFirstDepartureFromNow(stopTimes) { // Search starting from the last stop time (largest seconds until departure). const lastStopTime = stopTimes[stopTimes.length - 1] let firstStopTime = lastStopTime - stopTimes.forEach(stopTime => { + stopTimes.forEach((stopTime) => { const firstStopTimeSeconds = getSecondsUntilDeparture(firstStopTime, true) const stopTimeSeconds = getSecondsUntilDeparture(stopTime, true) @@ -226,7 +244,7 @@ export function getFirstDepartureFromNow (stopTimes) { /** * Merges and sorts the stop time entries from the patterns in the given stopData object. */ -export function mergeAndSortStopTimes (stopData) { +export function mergeAndSortStopTimes(stopData) { const stopTimesByPattern = getStopTimesByPattern(stopData) // Merge stop times, so that we can sort them across all route patterns. @@ -235,17 +253,17 @@ export function mergeAndSortStopTimes (stopData) { 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, - headsign, - route - } - }) + 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, + headsign, + route + } + }) mergedStopTimes = mergedStopTimes.concat(filteredTimes) } }) @@ -267,7 +285,7 @@ export const REALTIME_STATUS = { * 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 ( +export function getTripStatus( isRealtime, delaySeconds, onTimeThresholdSeconds @@ -296,34 +314,35 @@ export function getTripStatus ( * * TODO: Move to @opentripplanner/core-utils once otp-rr uses otp-ui library. */ -function getContrastYIQ (routeColor) { +function getContrastYIQ(routeColor) { const textColorOptions = [ tinycolor(routeColor).darken(80).toHexString(), tinycolor(routeColor).lighten(80).toHexString() ] - return tinycolor - .mostReadable(routeColor, textColorOptions, { - includeFallbackColors: true, - level: 'AAA' - }) - .toHexString() - // Have to do this to remain compatible with former version of this function - .split('#')[1] + return ( + tinycolor + .mostReadable(routeColor, textColorOptions, { + includeFallbackColors: true, + level: 'AAA' + }) + .toHexString() + // Have to do this to remain compatible with former version of this function + .split('#')[1] + ) } /** * Uses a long name splitter to prettify a route's long name */ -function getCleanRouteLongName ({ longNameSplitter, route }) { +function getCleanRouteLongName({ longNameSplitter, route }) { let longName = '' if (route.longName) { // Attempt to split route name if splitter is defined for operator (to // remove short name value from start of long name value). const nameParts = route.longName.split(longNameSplitter) - longName = (longNameSplitter && nameParts.length > 1) - ? nameParts[1] - : route.longName + longName = + longNameSplitter && nameParts.length > 1 ? nameParts[1] : route.longName // If long name and short name are identical, set long name to be an empty // string. if (longName === route.shortName) longName = '' @@ -334,8 +353,9 @@ function getCleanRouteLongName ({ longNameSplitter, route }) { * Using an operator and route, apply heuristics to determine color and contrast color * as well as a full route name */ -export function getColorAndNameFromRoute (operator, route) { - const {defaultRouteColor, defaultRouteTextColor, longNameSplitter} = operator || {} +export function getColorAndNameFromRoute(operator, route) { + const { defaultRouteColor, defaultRouteTextColor, longNameSplitter } = + operator || {} const backgroundColor = `#${defaultRouteColor || route.color || 'ffffff'}` // NOTE: text color is not a part of short response route object, so there // is no way to determine from OTP what the text color should be if the @@ -354,3 +374,18 @@ export function getColorAndNameFromRoute (operator, route) { longName } } + +/** + * Helper method to determine if a stop being viewed is a flex stop. This is not marked by + * otp, so we must use the geometry type to determine flex status (flex stops do not use points) + * + * Extra null checks are needed to avoid mistaking a stop where data has not yet loaded for + * a flex stop. + */ +export function stopIsFlex(stopData) { + return ( + stopData && + stopData.geometries && + stopData.geometries.geoJson?.type !== 'Point' + ) +} From 0b9b183e563ab5e424b8ddf08d514c7f550f8bac Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 12 Jan 2022 14:44:18 +0100 Subject: [PATCH 093/270] feat(itinerary-body): show itinerary walking leg imagery button --- example-config.yml | 4 +++ .../line-itin/connected-itinerary-body.js | 28 ++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/example-config.yml b/example-config.yml index 123383bcd..7115a99ae 100644 --- a/example-config.yml +++ b/example-config.yml @@ -409,3 +409,7 @@ dateTime: # # Whether to render routes within flex zones of a route's patterns. If set to true, # # routes will not be rendered within flex zones. # hideRouteShapesWithinFlexZones: true + +# API key to make Mapillary API calls. These are used to show street imagery. +# mapillary: +# key: diff --git a/lib/components/narrative/line-itin/connected-itinerary-body.js b/lib/components/narrative/line-itin/connected-itinerary-body.js index 79b8f167b..3b33267af 100644 --- a/lib/components/narrative/line-itin/connected-itinerary-body.js +++ b/lib/components/narrative/line-itin/connected-itinerary-body.js @@ -1,23 +1,29 @@ +/* eslint-disable react/prop-types */ +// TODO: Typescript (otp-rr config object) +import { connect } from 'react-redux' +import { + PlaceName as PlaceNameWrapper, + PlaceRowWrapper +} from '@opentripplanner/itinerary-body/lib/styled' import isEqual from 'lodash.isequal' -import TransitLegSummary from '@opentripplanner/itinerary-body/lib/defaults/transit-leg-summary' import ItineraryBody from '@opentripplanner/itinerary-body/lib/otp-react-redux/itinerary-body' import LineColumnContent from '@opentripplanner/itinerary-body/lib/otp-react-redux/line-column-content' import PlaceName from '@opentripplanner/itinerary-body/lib/otp-react-redux/place-name' -import { PlaceName as PlaceNameWrapper, PlaceRowWrapper } from '@opentripplanner/itinerary-body/lib/styled' -import RouteDescription from '@opentripplanner/itinerary-body/lib/otp-react-redux/route-description' import React, { Component } from 'react' -import { connect } from 'react-redux' +import RouteDescription from '@opentripplanner/itinerary-body/lib/otp-react-redux/route-description' import styled from 'styled-components' +import TransitLegSummary from '@opentripplanner/itinerary-body/lib/defaults/transit-leg-summary' +import { ComponentContext } from '../../../util/contexts' import { setLegDiagram } from '../../../actions/map' import { setViewedTrip } from '../../../actions/ui' import TripDetails from '../connected-trip-details' import TripTools from '../trip-tools' -import { ComponentContext } from '../../../util/contexts' import RealtimeTimeColumn from './realtime-time-column' import TransitLegSubheader from './connected-transit-leg-subheader' +// eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {} const ItineraryBodyContainer = styled.div` @@ -37,11 +43,11 @@ class ConnectedItineraryBody extends Component { static contextType = ComponentContext /** avoid rerendering if the itinerary to display hasn't changed */ - shouldComponentUpdate (nextProps, nextState) { + shouldComponentUpdate(nextProps, nextState) { return !isEqual(this.props.itinerary, nextProps.itinerary) } - render () { + render() { const { accessibilityScoreGradationMap, config, @@ -63,6 +69,7 @@ class ConnectedItineraryBody extends Component { itinerary={itinerary} LegIcon={LegIcon} LineColumnContent={LineColumnContent} + mapillaryKey={config?.mapillary?.key} PlaceName={PlaceName} RouteDescription={RouteDescription} setActiveLeg={setActiveLeg} @@ -99,6 +106,7 @@ const mapDispatchToProps = { setViewedTrip } -export default connect(mapStateToProps, mapDispatchToProps)( - ConnectedItineraryBody -) +export default connect( + mapStateToProps, + mapDispatchToProps +)(ConnectedItineraryBody) From a3c0a0af02e89187a9a2965cda4309e74fd0291b Mon Sep 17 00:00:00 2001 From: miles-grant-ibigroup Date: Wed, 12 Jan 2022 16:32:09 +0100 Subject: [PATCH 094/270] improvement: show mapillary iframe in elevation popup --- lib/actions/map.js | 1 + lib/components/map/map.css | 21 +- lib/components/map/map.js | 67 +++- lib/components/map/mapillary-frame.tsx | 29 ++ .../narrative/default/access-leg.js | 61 ++-- .../line-itin/connected-itinerary-body.js | 5 +- lib/reducers/create-otp-reducer.js | 334 +++++++++++------- 7 files changed, 337 insertions(+), 181 deletions(-) create mode 100644 lib/components/map/mapillary-frame.tsx diff --git a/lib/actions/map.js b/lib/actions/map.js index 5db88e68f..310d66233 100644 --- a/lib/actions/map.js +++ b/lib/actions/map.js @@ -131,6 +131,7 @@ export function switchLocations() { } export const setLegDiagram = createAction('SET_LEG_DIAGRAM') +export const setMapillaryId = createAction('SET_MAPILLARY_ID') export const setElevationPoint = createAction('SET_ELEVATION_POINT') diff --git a/lib/components/map/map.css b/lib/components/map/map.css index 77652d126..04dca4aea 100644 --- a/lib/components/map/map.css +++ b/lib/components/map/map.css @@ -15,7 +15,7 @@ } .otp .link-button:focus { - outline:0; + outline: 0; } /* leg diagram */ @@ -29,8 +29,8 @@ z-index: 1000; background-color: white; background-clip: padding-box; - border: 2px solid rgba(127, 127, 127, .5); - border-Radius: 4px; + border: 2px solid rgba(127, 127, 127, 0.5); + border-radius: 4px; cursor: crosshair; } @@ -64,6 +64,21 @@ background: none; } +.otp .leg-diagram .mapillary-close-button { + background: rgba(255, 255, 255, 0.85); + border-radius: 0 0 1em; + display: block; + left: 0; + padding: 0.5em; + position: absolute; + top: 0; + z-index: 10000; +} +.otp .leg-diagram .mapillary-close-button:hover { + background: rgba(200, 200, 200, 0.85); + color: #333; +} + /*** Car Rental Map Icons ***/ .otp .car-rental-icon { diff --git a/lib/components/map/map.js b/lib/components/map/map.js index 12e01609e..4b8ca41e2 100644 --- a/lib/components/map/map.js +++ b/lib/components/map/map.js @@ -1,43 +1,56 @@ -import React, { Component } from 'react' +/* eslint-disable react/prop-types */ +// TODO: Typescript (config object) +import { Button, ButtonGroup } from 'react-bootstrap' import { connect } from 'react-redux' -import { ButtonGroup, Button } from 'react-bootstrap' +import React, { Component } from 'react' + +import { setMapillaryId } from '../../actions/map' import DefaultMap from './default-map' import LegDiagram from './leg-diagram' +import MapillaryFrame from './mapillary-frame' import StylizedMap from './stylized-map' class Map extends Component { - constructor () { + constructor() { super() this.state = { activeViewIndex: 0 } } - getComponentForView (view) { + getComponentForView(view) { // TODO: allow a 'CUSTOM' type switch (view.type) { - case 'DEFAULT': return - case 'STYLIZED': return + case 'DEFAULT': + return + case 'STYLIZED': + return } } - render () { - const { diagramLeg, mapConfig } = this.props + render() { + const { activeMapillaryImage, diagramLeg, mapConfig, setMapillaryId } = + this.props const showDiagram = diagramLeg + const showMapillary = activeMapillaryImage // Use the views defined in the config; if none defined, just show the default map const views = mapConfig.views || [{ type: 'DEFAULT' }] return ( -
    +
    {/* The map views -- only one is visible at a time */} {views.map((view, i) => { return ( -
    {this.getComponentForView(view)}
    @@ -46,15 +59,26 @@ class Map extends Component { {/* The toggle buttons -- only show if multiple views */} {views.length > 1 && ( -
    +
    {views.map((view, i) => { return (
    ) } @@ -76,9 +106,14 @@ class Map extends Component { const mapStateToProps = (state, ownProps) => { return { + activeMapillaryImage: state.otp.ui.mapillaryId, diagramLeg: state.otp.ui.diagramLeg, mapConfig: state.otp.config.map } } -export default connect(mapStateToProps)(Map) +const mapDispatchToProps = { + setMapillaryId +} + +export default connect(mapStateToProps, mapDispatchToProps)(Map) diff --git a/lib/components/map/mapillary-frame.tsx b/lib/components/map/mapillary-frame.tsx new file mode 100644 index 000000000..f4225abde --- /dev/null +++ b/lib/components/map/mapillary-frame.tsx @@ -0,0 +1,29 @@ +import { Button } from 'react-bootstrap' +import React from 'react' + +const MapillaryFrame = ({ + id, + onClose +}: { + id: string + onClose?: () => void +}): React.ReactElement => { + return ( +
    +