diff --git a/example-config.yml b/example-config.yml
index b725f59a4..63fbf78cc 100644
--- a/example-config.yml
+++ b/example-config.yml
@@ -68,6 +68,7 @@ persistence:
# otp_middleware:
# apiBaseUrl: https://otp-middleware.example.com
# apiKey: your-middleware-api-key
+# supportsPushNotifications: true # If not set, push notification settings will not be shown.
### Adding additional menu items to the main menu items. Use the separator flag
### to include a separator line if you have groups of menu items
diff --git a/i18n/en-US.yml b/i18n/en-US.yml
index 613156575..9a1a8c9a7 100644
--- a/i18n/en-US.yml
+++ b/i18n/en-US.yml
@@ -34,8 +34,14 @@ actions:
setPaymentError: "Error setting payment info:"
setRequestStatusError: "Error setting request status:"
location:
+ deniedAccessAlert: >
+ Access to your location is blocked.
+
+ To use your current location, enable location permissions from your
+ browser, and reload the page.
geolocationNotSupportedError: Geolocation not supported by your browser
unknownPositionError: Unknown error getting position
+ userDeniedPermission: User denied permission
map:
currentLocation: (Current Location)
user:
@@ -130,6 +136,7 @@ common:
walk: Walk
notifications:
email: email
+ push: push notifications
sms: SMS
places:
custom: custom
@@ -353,10 +360,8 @@ components:
description: The content you requested is not available.
header: Content not found
NotificationPrefsPane:
- description: You can receive notifications about trips you frequently take.
- noneSelect: Don't notify me
- notificationChannelPrompt: How would you like to receive notifications?
- notificationEmailDetail: "Notification emails will be sent to:"
+ noDeviceForPush: Register your device using the mobile app to access push notifications.
+ notificationChannelPrompt: "Receive notifications about your saved trips via:"
OTP2ErrorRenderer:
LOCATION_NOT_FOUND:
body: >-
@@ -408,7 +413,6 @@ components:
prompt: "Enter your phone number for SMS notifications:"
requestNewCode: Request a new code
sendVerificationText: Send verification text
- smsDetail: "SMS notifications will be sent to:"
verificationCode: "Verification code:"
verificationInstructions: >
Please check the SMS messaging app on your mobile phone for a text message
diff --git a/i18n/es.yml b/i18n/es.yml
index 99f42d57f..ab2d2ec11 100644
--- a/i18n/es.yml
+++ b/i18n/es.yml
@@ -133,6 +133,7 @@ common:
walk: Caminar
notifications:
email: correo electrónico
+ push: notificaciones push
sms: Mensaje de texto
places:
custom: personalizado
@@ -359,8 +360,9 @@ components:
header: No se encontró el contenido
NotificationPrefsPane:
description: Puede recibir notificaciones sobre los viajes que realiza con frecuencia.
+ noDeviceForPush: Regístrese con la aplicación móvil para acceder a esta configuración.
noneSelect: No enviar notificaciones
- notificationChannelPrompt: ¿Cómo desea recibir las notificaciones?
+ notificationChannelPrompt: "Recibir notificaciones para sus viajes guardados por:"
notificationEmailDetail: "Los correos electrónicos de notificación se enviarán a:"
PhoneNumberEditor:
changeNumber: Cambiar número de teléfono
@@ -375,7 +377,6 @@ components:
texto:
requestNewCode: Solicitar un nuevo código
sendVerificationText: Enviar texto de verificación
- smsDetail: "Las notificaciones por mensaje de texto se enviarán a:"
verificationCode: "Código de verificación:"
verificationInstructions: >
Por favor, compruebe en la aplicación de mensajería de texto de su
diff --git a/i18n/fr.yml b/i18n/fr.yml
index 742db4275..0cc06933a 100644
--- a/i18n/fr.yml
+++ b/i18n/fr.yml
@@ -38,8 +38,14 @@ actions:
setPaymentError: "Erreur sur les coordonnées de paiement :"
setRequestStatusError: "Erreur sur l'état de la requête :"
location:
+ deniedAccessAlert: >
+ L'accès à votre position est refusé.
+
+ Pour utiliser votre emplacement actuel, permettez-en l'accès depuis votre
+ navigateur, et ouvrez de nouveau cette page.
geolocationNotSupportedError: La géolocalisation n'est pas prise en charge par votre navigateur.
unknownPositionError: Erreur inconnue lors de la détection de votre emplacement.
+ userDeniedPermission: Refusé par l'utilisateur
map:
currentLocation: (Emplacement actuel)
user:
@@ -139,6 +145,7 @@ common:
walk: À pied
notifications:
email: e-mail
+ push: notifications push
sms: SMS
places:
custom: divers
@@ -178,7 +185,7 @@ components:
AdvancedOptions:
bannedRoutes: Choisissez les lignes à éviter…
bikeTolerance: Tolérance au vélo
- preferredRoutes: Choisissez les lignes preferées
+ preferredRoutes: Choisissez les lignes préferées
walkTolerance: Tolérance à la marche
AfterSignInScreen:
mainTitle: Redirection...
@@ -366,12 +373,8 @@ components:
description: Le contenu que vous avez demandé n'est pas disponible.
header: Contenu introuvable
NotificationPrefsPane:
- description: >-
- Vous pouvez recevoir des notifications sur les trajets que vous effectuez
- fréquemment.
- noneSelect: Ne pas me notifier
- notificationChannelPrompt: Comment voulez-vous recevoir vos notifications ?
- notificationEmailDetail: "Les courriers de notification seront envoyés à :"
+ noDeviceForPush: Inscrivez-vous avec l'application mobile pour accéder à ce paramètre.
+ notificationChannelPrompt: "Recevoir des notifications sur vos trajets par :"
OTP2ErrorRenderer:
LOCATION_NOT_FOUND:
body: >-
@@ -425,7 +428,6 @@ components:
prompt: "Entrez votre numéro de téléphone pour les SMS de notification :"
requestNewCode: Envoyer un nouveau code
sendVerificationText: Envoyer le SMS de vérification
- smsDetail: "Les SMS de notification seront envoyés au :"
verificationCode: "Code de vérification :"
verificationInstructions: >
Un SMS vous a été envoyé avec un code de vérification. Veuillez taper ce
diff --git a/i18n/i18n-exceptions.json b/i18n/i18n-exceptions.json
index db534b0e4..60f5855cd 100644
--- a/i18n/i18n-exceptions.json
+++ b/i18n/i18n-exceptions.json
@@ -1,5 +1,10 @@
{
"groups": {
+ "common.notifications.*": [
+ "email",
+ "sms",
+ "push"
+ ],
"components.OTP2ErrorRenderer.*.body": [
"LOCATION_NOT_FOUND",
"NO_STOPS_IN_RANGE",
diff --git a/i18n/ko.yml b/i18n/ko.yml
index d7528bcc5..1b339d183 100644
--- a/i18n/ko.yml
+++ b/i18n/ko.yml
@@ -119,6 +119,7 @@ common:
walk: 걷기
notifications:
email: 이메일
+ push: 푸시 알림
sms: SMS
places:
custom: 사용자 정의
@@ -324,10 +325,7 @@ components:
description: 요청한 콘텐츠를 사용할 수 없습니다.
header: 콘텐츠를 찾을 수 없음
NotificationPrefsPane:
- description: 자주 가는 트립에 대한 알림을 받을 수 있습니다.
- noneSelect: 알림 거부
- notificationChannelPrompt: 알림을 어떻게 받고 싶습니까?
- notificationEmailDetail: "알림 이메일이 다음으로 전송됩니다:"
+ notificationChannelPrompt: "저장된 여행의 알림을 받는 방법:"
PhoneNumberEditor:
changeNumber: 번호 변경
invalidCode: 확인 코드 6 자리를 입력하세요.
@@ -338,7 +336,6 @@ components:
prompt: "SMS 알림 수신을 위한 전화번호를 입력하세요:"
requestNewCode: 새 코드 요청
sendVerificationText: 확인 텍스트 전송
- smsDetail: "SMS 알림이 다음으로 전송됩니다:"
verificationCode: "확인 코드:"
verificationInstructions: |
휴대폰의 SMS 메시지 앱에서 인증 코드를 확인하고 아래에 코드를 입력하세요(코드는 10분 후에 만료됩니다).
diff --git a/i18n/vi.yml b/i18n/vi.yml
index d4f6f93a0..f7cf241bb 100644
--- a/i18n/vi.yml
+++ b/i18n/vi.yml
@@ -128,6 +128,7 @@ common:
walk: Đi bộ
notifications:
email: e-mail
+ push: thông báo đẩy
sms: tin nhắn
places:
custom: phong tục
@@ -333,12 +334,7 @@ components:
description: Nội dung bạn yêu cầu không có sẵn.
header: Không tìm thấy nội dung
NotificationPrefsPane:
- description: >-
- Bạn có thể nhận được thông báo về các chuyến đi bạn thường xuyên thực
- hiện.
- noneSelect: Đừng thông báo cho tôi
- notificationChannelPrompt: Bạn muốn nhận thông báo như thế nào?
- notificationEmailDetail: "Email thông báo sẽ được gửi đến:"
+ notificationChannelPrompt: "Nhận thông báo về các chuyến đi đã lưu bằng:"
PhoneNumberEditor:
changeNumber: Thay đổi số điện thoại
invalidCode: Vui lòng nhập 6 chữ số cho mã xác thực.
@@ -349,7 +345,6 @@ components:
prompt: "Nhập số điện thoại của bạn để nhận thông báo SMS:"
requestNewCode: Yêu cầu một mã mới
sendVerificationText: Gửi văn bản xác minh
- smsDetail: "Thông báo SMS sẽ được gửi đến:"
verificationCode: "Mã xác nhận:"
verificationInstructions: >
Vui lòng kiểm tra ứng dụng nhắn tin SMS trên điện thoại di động của bạn để
diff --git a/i18n/zh.yml b/i18n/zh.yml
index 79990f348..58e9a6d0c 100644
--- a/i18n/zh.yml
+++ b/i18n/zh.yml
@@ -119,6 +119,7 @@ common:
walk: 步行
notifications:
email: 电子邮件
+ push: 推送通知
sms: 短信
places:
custom: 习俗
@@ -325,10 +326,7 @@ components:
description: 您要求的内容不存在.
header: 未找到内容
NotificationPrefsPane:
- description: 你可以收到关于你常用行程的通知.
- noneSelect: 不要通知我
- notificationChannelPrompt: 您希望如何接收通知?
- notificationEmailDetail: "通知邮件将被发送至:"
+ notificationChannelPrompt: "如何接收已保存行程的通知:"
PhoneNumberEditor:
changeNumber: 更改电话号码
invalidCode: 请输入6位数的验证码.
@@ -339,7 +337,6 @@ components:
prompt: "输入你的电话号码以便收到短信通知:"
requestNewCode: 申请一个新的代码
sendVerificationText: 发送验证短信
- smsDetail: "短信通知将被发送到:"
verificationCode: "验证码:"
verificationInstructions: |
请检查您手机上的短信应用查看是否有验证码的短信并输入以下代码 (代码在10分钟后失效).
diff --git a/lib/actions/apiV2.js b/lib/actions/apiV2.js
index 3b9eba694..0edb46d0e 100644
--- a/lib/actions/apiV2.js
+++ b/lib/actions/apiV2.js
@@ -46,6 +46,7 @@ import { zoomToPlace } from './map'
const { generateCombinations, generateOtp2Query } = coreUtils.queryGen
const { getTripOptionsFromQuery, getUrlParams } = coreUtils.query
+const { convertGraphQLResponseToLegacy } = coreUtils.itinerary
const { randId } = coreUtils.storage
const LIGHT_GRAY = '666666'
@@ -782,47 +783,6 @@ const pickupDropoffTypeToOtp1 = (otp2Type) => {
}
}
-/**
- * Converts a leg from GraphQL format to legacy REST format.
- * @param leg OTP2 GraphQL style leg
- * @returns REST shaped leg
- */
-const processLeg = (leg) => ({
- ...leg,
- agencyBrandingUrl: leg.agency?.url,
- agencyName: leg.agency?.name,
- agencyUrl: leg.agency?.url,
- alerts: aggregateAlerts(
- leg.agency?.alerts,
- leg.route?.alerts,
- leg.to?.stop?.alerts,
- leg.from?.stop?.alerts
- ),
- alightRule: pickupDropoffTypeToOtp1(leg.dropoffType),
- boardRule: pickupDropoffTypeToOtp1(leg.pickupType),
- dropOffBookingInfo: {
- latestBookingTime: leg.dropOffBookingInfo
- },
- from: {
- ...leg.from,
- stopCode: leg.from.stop?.code,
- stopId: leg.from.stop?.gtfsId
- },
- route: leg.route?.shortName,
- routeColor: leg.route?.color,
- routeId: leg.route?.id,
- routeLongName: leg.route?.longName,
- routeShortName: leg.route?.shortName,
- routeTextColor: leg.route?.textColor,
- to: {
- ...leg.to,
- stopCode: leg.to.stop?.code,
- stopId: leg.to.stop?.gtfsId
- },
- tripHeadsign: leg.trip?.tripHeadsign,
- tripId: leg.trip?.gtfsId
-})
-
const queryParamConfig = { modeButtons: DelimitedArrayParam }
export function routingQuery(searchId = null, updateSearchInReducer) {
@@ -919,7 +879,7 @@ export function routingQuery(searchId = null, updateSearchInReducer) {
dispatch(setItineraryView(ItineraryView.LIST))
- combinations.forEach((combo) => {
+ combinations.forEach((combo, index) => {
const query = generateOtp2Query(combo)
dispatch(
createGraphQLQueryAction(
@@ -962,7 +922,7 @@ export function routingQuery(searchId = null, updateSearchInReducer) {
const withCollapsedShortNames =
response.data?.plan?.itineraries?.map((itin) => ({
...itin,
- legs: itin.legs?.map(processLeg)
+ legs: itin.legs?.map(convertGraphQLResponseToLegacy)
}))
/* It is possible for a NO_TRANSIT_CONNECTION error to be
@@ -982,6 +942,7 @@ export function routingQuery(searchId = null, updateSearchInReducer) {
}
return {
+ index,
response: {
plan: {
...response.data?.plan,
diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js
index ee77d2ca1..da34ec27b 100644
--- a/lib/actions/field-trip.js
+++ b/lib/actions/field-trip.js
@@ -687,7 +687,7 @@ function checkValidityAndCapacity(state, request) {
// iterate through itineraries to check validity and assign field trip
// groups
- response.plan.itineraries.forEach((itinerary) => {
+ response.plan.itineraries.forEach((itinerary, itinIdx) => {
let itineraryCapacity = Number.POSITIVE_INFINITY
// check each individual trip to see if there aren't any trips in this
@@ -744,12 +744,13 @@ function checkValidityAndCapacity(state, request) {
// A field trip response is guaranteed to have only one itinerary, so it
// ok to set the itinerary by response as an array with a single
// itinerary.
- assignedItinerariesByResponse[responseIdx] = [
- {
- ...itinerary,
- fieldTripGroupSize: Math.min(itineraryCapacity, remainingGroupSize)
- }
- ]
+ if (!assignedItinerariesByResponse[responseIdx]) {
+ assignedItinerariesByResponse[responseIdx] = {}
+ }
+ assignedItinerariesByResponse[responseIdx][itinIdx] = {
+ ...itinerary,
+ fieldTripGroupSize: Math.min(itineraryCapacity, remainingGroupSize)
+ }
remainingGroupSize -= itineraryCapacity
}
})
@@ -844,8 +845,12 @@ function makeFieldTripPlanRequests(request, outbound, intl) {
// I do not believe that it is worth the effort. I am instead in favor of
// re-building field trip from the ground up as a separate application.
setInterval(() => {
- const activeItineraries = getActiveItineraries(getState())
- if (activeItineraries.length >= numRequests) {
+ const searchResponse = getState().otp.searches[searchId]?.response
+ const activeItineraries =
+ searchResponse?.reduce((prev, resp) => {
+ return (prev += resp?.plan?.itineraries?.length || 0)
+ }, 0) || 0
+ if (activeItineraries >= numRequests) {
resolve()
}
}, 20)
diff --git a/lib/actions/location.tsx b/lib/actions/location.tsx
index a336efb18..c62e9b740 100644
--- a/lib/actions/location.tsx
+++ b/lib/actions/location.tsx
@@ -2,6 +2,7 @@
import { createAction } from 'redux-actions'
import { Dispatch } from 'redux'
import { IntlShape } from 'react-intl'
+import { isMobile } from '@opentripplanner/core-utils/lib/ui'
import { setLocationToCurrent } from './map'
@@ -47,9 +48,29 @@ export function getCurrentPosition(
// On error
(error) => {
console.log('error getting current position', error)
- // FIXME, analyze error code to produce better error message.
- // See https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError
- dispatch(receivedPositionError({ error }))
+ // On desktop, after user clicks "Use location" from the location fields,
+ // show an alert and explain if location is blocked.
+ // TODO: Consider moving the handling of unavailable location to the location-field component.
+ if (!isMobile() && error.code === 1) {
+ window.alert(
+ intl.formatMessage({
+ id: 'actions.location.deniedAccessAlert'
+ })
+ )
+ }
+ const newError = { ...error }
+ if (error.code === 1) {
+ // i18n for user-denied location message (error.code = 1 on secure origins).
+ if (
+ window.location.protocol === 'https:' ||
+ window.location.host.startsWith('localhost:')
+ ) {
+ newError.message = intl.formatMessage({
+ id: 'actions.location.userDeniedPermission'
+ })
+ }
+ }
+ dispatch(receivedPositionError({ error: newError }))
},
// Options
{ enableHighAccuracy: true }
diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js
index e135c16b1..0ae74b8a7 100644
--- a/lib/components/app/responsive-webapp.js
+++ b/lib/components/app/responsive-webapp.js
@@ -153,11 +153,9 @@ class ResponsiveWebapp extends Component {
}
}
- // Test location availability on load,
- // so it is reported correctly by the location fields.
- getCurrentPosition(intl)
-
if (isMobile()) {
+ // Test location availability on load
+ getCurrentPosition(intl)
// Also, watch for changes in position on mobile
navigator.geolocation.watchPosition(
// On success
diff --git a/lib/components/form/call-taker/advanced-options.js b/lib/components/form/call-taker/advanced-options.js
index 450c8c67f..dcf81ee32 100644
--- a/lib/components/form/call-taker/advanced-options.js
+++ b/lib/components/form/call-taker/advanced-options.js
@@ -2,9 +2,13 @@
// FIXME: Remove the following eslint rule exception.
/* eslint-disable jsx-a11y/label-has-for */
import * as TripFormClasses from '@opentripplanner/trip-form/lib/styled'
+import {
+ DropdownSelector,
+ SliderSelector,
+ SubmodeSelector
+} from '@opentripplanner/trip-form'
import { FormattedMessage, injectIntl } from 'react-intl'
import { hasBike } from '@opentripplanner/core-utils/lib/itinerary'
-import { SliderSelector, SubmodeSelector } from '@opentripplanner/trip-form'
import isEmpty from 'lodash.isempty'
import React, { Component, lazy, Suspense } from 'react'
import styled from 'styled-components'
@@ -155,9 +159,9 @@ class AdvancedOptions extends Component {
})
}
- _setWaklTolerance = ({ walkTolerance }) => {
+ _setWalkTolerance = ({ walkReluctance }) => {
this.props.setUrlSearch({
- walkTolerance
+ walkReluctance
})
}
@@ -224,17 +228,19 @@ class AdvancedOptions extends Component {
justifyContent: 'space-between'
}}
>
-
{hasBike(currentModes?.map((m) => m.mode).join(',') || '') ? (
diff --git a/lib/components/narrative/metro/attribute-utils.tsx b/lib/components/narrative/metro/attribute-utils.tsx
index 36a12b90b..c48afd0c8 100644
--- a/lib/components/narrative/metro/attribute-utils.tsx
+++ b/lib/components/narrative/metro/attribute-utils.tsx
@@ -9,7 +9,7 @@ export const getFirstTransitLegStop = (
itinerary.legs?.find((leg: Leg) => leg?.from?.vertexType === 'TRANSIT')?.from
?.name
-export const getFlexAttirbutes = (
+export const getFlexAttributes = (
itinerary: Itinerary
): {
isCallAhead: boolean
diff --git a/lib/components/narrative/metro/metro-itinerary.tsx b/lib/components/narrative/metro/metro-itinerary.tsx
index e86a593e3..cbd0f7194 100644
--- a/lib/components/narrative/metro/metro-itinerary.tsx
+++ b/lib/components/narrative/metro/metro-itinerary.tsx
@@ -28,7 +28,7 @@ import ItineraryBody from '../line-itin/connected-itinerary-body'
import NarrativeItinerary from '../narrative-itinerary'
import SimpleRealtimeAnnotation from '../simple-realtime-annotation'
-import { getFirstTransitLegStop, getFlexAttirbutes } from './attribute-utils'
+import { getFlexAttributes } from './attribute-utils'
import DepartureTimesList, {
SetActiveItineraryHandler
} from './departure-times-list'
@@ -202,23 +202,17 @@ class MetroItinerary extends NarrativeItinerary {
static ModesAndRoutes = MetroItineraryRoutes
_onMouseEnter = () => {
- const { active, index, setVisibleItinerary, visibleItinerary } = this.props
+ const { active, index, setVisibleItinerary, visible } = this.props
// Set this itinerary as visible if not already visible.
- const visibleNotSet =
- visibleItinerary === null || visibleItinerary === undefined
- const isVisible =
- visibleItinerary === index || (active === index && visibleNotSet)
+ const isVisible = visible || active
if (typeof setVisibleItinerary === 'function' && !isVisible) {
setVisibleItinerary({ index })
}
}
_onMouseLeave = () => {
- const { index, setVisibleItinerary, visibleItinerary } = this.props
- if (
- typeof setVisibleItinerary === 'function' &&
- visibleItinerary === index
- ) {
+ const { setVisibleItinerary, visible } = this.props
+ if (typeof setVisibleItinerary === 'function' && visible) {
setVisibleItinerary({ index: null })
}
}
@@ -266,7 +260,7 @@ class MetroItinerary extends NarrativeItinerary {
const { SvgIcon } = this.context
const { isCallAhead, isContinuousDropoff, isFlexItinerary, phone } =
- getFlexAttirbutes(itinerary)
+ getFlexAttributes(itinerary)
const { fareCurrency, transitFare } = getFare(itinerary, defaultFareType)
diff --git a/lib/components/narrative/narrative-itineraries.js b/lib/components/narrative/narrative-itineraries.js
index 30200a5cf..4fa470537 100644
--- a/lib/components/narrative/narrative-itineraries.js
+++ b/lib/components/narrative/narrative-itineraries.js
@@ -43,6 +43,15 @@ import Loading from './loading'
import NarrativeItinerariesErrors from './narrative-itineraries-errors'
import NarrativeItinerariesHeader from './narrative-itineraries-header'
+/** Creates a start time object for the given itinerary. */
+function makeStartTime(itinerary) {
+ return {
+ itinerary,
+ legs: itinerary.legs,
+ realtime: firstTransitLegIsRealtime(itinerary)
+ }
+}
+
function doMergeItineraries(itineraries) {
const mergedItineraries = itineraries
.reduce((prev, cur, curIndex) => {
@@ -60,46 +69,51 @@ function doMergeItineraries(itineraries) {
// Only process itineraries less than 24 hours in the future
differenceInDays(updatedItinerary.startTime, Date.now()) < 1
) {
- const duplicateItin = updatedItineraries[duplicateIndex]
+ const duplicateFoundItin = updatedItineraries[duplicateIndex]
// TODO: MERGE ROUTE NAMES
- // Add only new start time to existing itinerary
+ // Add only new start time to existing itinerary.
+ // The existing itinerary is the earliest between
+ // this itinerary (updatedItinerary) and duplicateItin.
+ // This is because alternate routes are only added to the first non-duplicate itinerary,
+ // and we show alternate routes for the first (i.e. earliest) non-duplicate itinerary found.
+ let duplicateItin = duplicateFoundItin
+ let itinCopyToAdd = updatedItinerary
+ if (duplicateFoundItin.startTime > updatedItinerary.startTime) {
+ duplicateItin = updatedItinerary
+ duplicateItin.startTimes = duplicateFoundItin.allStartTimes
+ updatedItineraries[duplicateIndex] = updatedItinerary
+ itinCopyToAdd = duplicateFoundItin
+ }
+
if (!duplicateItin.allStartTimes) {
- duplicateItin.allStartTimes = [
- {
- itinerary: duplicateItin,
- legs: duplicateItin.legs,
- realtime: firstTransitLegIsRealtime(duplicateItin)
- }
- ]
+ duplicateItin.allStartTimes = [makeStartTime(duplicateItin)]
}
// Only add new time if it doesn't already exist. It would be better to use
// the uniqueness feature of Set, but unfortunately objects are never equal
if (
!duplicateItin.allStartTimes.find(
- (time) => getFirstLegStartTime(time.legs) === cur.startTime
+ (time) =>
+ getFirstLegStartTime(time.legs) === itinCopyToAdd.startTime
)
) {
- duplicateItin.allStartTimes.push({
- itinerary: updatedItinerary,
- legs: cur.legs,
- realtime: firstTransitLegIsRealtime(cur)
- })
+ duplicateItin.allStartTimes.push(makeStartTime(itinCopyToAdd))
}
// Some legs will be the same, but have a different route
// This map catches those and stores the alternate routes so they can be displayed
duplicateItin.legs = duplicateItin.legs.map((leg, index) => {
const newLeg = clone(leg)
- if (leg?.routeId !== cur.legs[index]?.routeId) {
+ const curLeg = itinCopyToAdd.legs[index]
+ const curLegRouteId = curLeg?.routeId
+ if (curLegRouteId && leg?.routeId && leg?.routeId !== curLegRouteId) {
if (!newLeg.alternateRoutes) {
newLeg.alternateRoutes = {}
}
- const { routeId } = cur.legs?.[index]
- newLeg.alternateRoutes[routeId] = {
+ newLeg.alternateRoutes[curLegRouteId] = {
// We save the entire leg to the alternateRoutes object so in
// the future, we can draw the leg on the map as an alternate route
- ...cur.legs?.[index]
+ ...curLeg
}
}
return newLeg
@@ -546,7 +560,7 @@ class NarrativeItineraries extends Component {
}
const reduceErrorsFromResponse = (acc, cur) => {
- const { routingErrors } = cur?.plan
+ const { routingErrors } = cur?.plan || {}
if (routingErrors) {
routingErrors.forEach((routingError) => {
const { code, inputField } = routingError
diff --git a/lib/components/user/monitored-trip/trip-basics-pane.tsx b/lib/components/user/monitored-trip/trip-basics-pane.tsx
index 3ab97bf9e..b2aa897cc 100644
--- a/lib/components/user/monitored-trip/trip-basics-pane.tsx
+++ b/lib/components/user/monitored-trip/trip-basics-pane.tsx
@@ -18,9 +18,9 @@ import styled from 'styled-components'
import type { IntlShape, WrappedComponentProps } from 'react-intl'
import * as userActions from '../../../actions/user'
+import { FieldSet } from '../styled'
import { getErrorStates } from '../../../util/ui'
import { getFormattedDayOfWeekPlural } from '../../../util/monitored-trip'
-import { labelStyle } from '../styled'
import FormattedDayOfWeek from '../../util/formatted-day-of-week'
import FormattedDayOfWeekCompact from '../../util/formatted-day-of-week-compact'
import FormattedValidationError from '../../util/formatted-validation-error'
@@ -65,12 +65,7 @@ const ALL_DAYS = [
] as const
// Styles.
-const AvailableDays = styled.fieldset`
- /* Format