diff --git a/lib/components/app/batch-routing-panel.tsx b/lib/components/app/batch-routing-panel.tsx index 5c6a69c73..6ca1f6da2 100644 --- a/lib/components/app/batch-routing-panel.tsx +++ b/lib/components/app/batch-routing-panel.tsx @@ -3,12 +3,14 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group' import { FormattedMessage, injectIntl, IntlShape } from 'react-intl' import React, { Component, FormEvent } from 'react' +import * as apiActions from '../../actions/api' import { advancedPanelClassName, mainPanelClassName, transitionDuration, TransitionStyles } from '../form/styled' +import { alertUserTripPlan } from '../form/util' import { getActiveSearch, getShowUserSettings } from '../../util/state' import { getPersistenceMode } from '../../util/user' import AdvancedSettingsPanel from '../form/advanced-settings-panel' @@ -22,9 +24,11 @@ import ViewerContainer from '../viewers/viewer-container' interface Props { activeSearch: any + currentQuery: any intl: IntlShape mainPanelContent: number mobile?: boolean + routingQuery: () => void showUserSettings: boolean } @@ -75,7 +79,13 @@ class BatchRoutingPanel extends Component { handleSubmit = (e: FormEvent) => e.preventDefault() handlePlanTripClick = () => { - this.setState({ planTripClicked: true }) + const { currentQuery, intl, routingQuery } = this.props + alertUserTripPlan( + intl, + currentQuery, + () => this.setState({ planTripClicked: true }), + routingQuery + ) } render() { @@ -128,6 +138,7 @@ class BatchRoutingPanel extends Component { > { (state.user.loggedInUser?.hasConsentedToTerms || getPersistenceMode(state.otp.config.persistence).isLocalStorage) const { mainPanelContent } = state.otp.ui + const currentQuery = state.otp.currentQuery return { activeSearch: getActiveSearch(state), + currentQuery, mainPanelContent, showUserSettings } } -export default connect(mapStateToProps)(injectIntl(BatchRoutingPanel)) +const mapDispatchToProps = { + routingQuery: apiActions.routingQuery +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(injectIntl(BatchRoutingPanel)) diff --git a/lib/components/form/advanced-settings-panel.tsx b/lib/components/form/advanced-settings-panel.tsx index d39e03ec9..a55484b66 100644 --- a/lib/components/form/advanced-settings-panel.tsx +++ b/lib/components/form/advanced-settings-panel.tsx @@ -30,7 +30,8 @@ import { onSettingsUpdate, pipe, populateSettingWithIcon, - setModeButton + setModeButton, + tripPlannerValidationErrors } from './util' import { setModeButtonEnabled } from './batch-settings' import { styledCheckboxCss } from './styled' @@ -111,8 +112,11 @@ const DtSelectorContainer = styled.div` ` const AdvancedSettingsPanel = ({ + autoPlan, closeAdvancedSettings, + currentQuery, enabledModeButtons, + handlePlanTrip, innerRef, modeButtonOptions, modeSettingDefinitions, @@ -121,8 +125,11 @@ const AdvancedSettingsPanel = ({ setCloseAdvancedSettingsWithDelay, setQueryParam }: { + autoPlan: boolean closeAdvancedSettings: () => void + currentQuery: any enabledModeButtons: string[] + handlePlanTrip: () => void innerRef: RefObject modeButtonOptions: ModeButtonDefinition[] modeSettingDefinitions: ModeSetting[] @@ -177,6 +184,15 @@ const AdvancedSettingsPanel = ({ ) ) + + const tripFormErrors = tripPlannerValidationErrors(currentQuery, intl) + + const closePanel = useCallback(() => { + // Only autoplan if there are no validation errors + tripFormErrors.length === 0 && autoPlan && handlePlanTrip() + closeAdvancedSettings() + }, [autoPlan, closeAdvancedSettings, handlePlanTrip, tripFormErrors.length]) + const handleModeButtonToggle = setModeButton( enabledModeButtons, onSettingsUpdate(setQueryParam) @@ -189,8 +205,8 @@ const AdvancedSettingsPanel = ({ const onSaveAndReturnClick = useCallback(async () => { await setCloseAdvancedSettingsWithDelay() setClosingBySave(true) - closeAdvancedSettings() - }, [closeAdvancedSettings, setCloseAdvancedSettingsWithDelay]) + closePanel() + }, [closePanel, setCloseAdvancedSettingsWithDelay]) return ( @@ -199,7 +215,7 @@ const AdvancedSettingsPanel = ({ aria-label={closeButtonText} id="close-advanced-settings-button" onClick={() => { - closeAdvancedSettings() + closePanel() }} title={closeButtonText} > @@ -263,9 +279,12 @@ const mapStateToProps = (state: AppReduxState) => { state.otp.modeSettingDefinitions || [], modes?.initialState?.modeSettingValues || {} ) + + const { autoPlan } = state.otp.config const saveAndReturnButton = state.otp.config?.advancedSettingsPanel?.saveAndReturnButton return { + autoPlan: autoPlan !== false, currentQuery: state.otp.currentQuery, // TODO: Duplicated in apiv2.js enabledModeButtons: diff --git a/lib/components/form/batch-settings.tsx b/lib/components/form/batch-settings.tsx index 23bd13a6c..47700fdeb 100644 --- a/lib/components/form/batch-settings.tsx +++ b/lib/components/form/batch-settings.tsx @@ -5,9 +5,8 @@ import { ModeButtonDefinition } from '@opentripplanner/types' import { Search } from '@styled-icons/fa-solid/Search' import { SyncAlt } from '@styled-icons/fa-solid/SyncAlt' import { useIntl } from 'react-intl' -import React, { useCallback, useContext, useState } from 'react' +import React, { useContext, useState } from 'react' -import * as apiActions from '../../actions/api' import * as formActions from '../../actions/form' import { ComponentContext } from '../../util/contexts' import { getActiveSearch, hasValidLocation } from '../../util/state' @@ -16,7 +15,6 @@ import { StyledIconWrapper } from '../util/styledIcon' import { addModeButtonIcon, - alertUserTripPlan, modesQueryParamConfig, onSettingsUpdate, pipe, @@ -39,7 +37,6 @@ type Props = { modeButtonOptions: ModeButtonDefinition[] onPlanTripClick: () => void openAdvancedSettings: () => void - routingQuery: any setQueryParam: (evt: any) => void spacedOutModeSelector?: boolean } @@ -64,7 +61,6 @@ function BatchSettings({ modeButtonOptions, onPlanTripClick, openAdvancedSettings, - routingQuery, setQueryParam, spacedOutModeSelector }: Props) { @@ -80,10 +76,6 @@ function BatchSettings({ pipe(addModeButtonIcon(ModeIcon), setModeButtonEnabled(enabledModeButtons)) ) - const _planTrip = useCallback(() => { - alertUserTripPlan(intl, currentQuery, onPlanTripClick, routingQuery) - }, [currentQuery, intl, onPlanTripClick, routingQuery]) - const baseColor = getBaseColor() const accentColor = getDarkenedBaseColor() @@ -109,7 +101,7 @@ function BatchSettings({ /> { } const mapDispatchToProps = { - routingQuery: apiActions.routingQuery, setQueryParam: formActions.setQueryParam } diff --git a/lib/components/form/util.tsx b/lib/components/form/util.tsx index 85dd3107d..c973af17c 100644 --- a/lib/components/form/util.tsx +++ b/lib/components/form/util.tsx @@ -72,13 +72,10 @@ export const setModeButton = ) } -export const alertUserTripPlan = ( - intl: IntlShape, +export const tripPlannerValidationErrors = ( currentQuery: any, - onPlanTripClick: () => void, - routingQuery: () => any -): void => { - // Check for any validation issues in query. + intl: IntlShape +): string[] => { const issues: string[] = [] if (!hasValidLocation(currentQuery, 'from')) { issues.push(intl.formatMessage({ id: 'components.BatchSettings.origin' })) @@ -88,6 +85,17 @@ export const alertUserTripPlan = ( intl.formatMessage({ id: 'components.BatchSettings.destination' }) ) } + return issues +} + +export const alertUserTripPlan = ( + intl: IntlShape, + currentQuery: any, + onPlanTripClick: () => void, + routingQuery: () => any +): void => { + // Check for any validation issues in query and alert user. + const issues = tripPlannerValidationErrors(currentQuery, intl) onPlanTripClick() if (issues.length > 0) { // TODO: replace with less obtrusive validation. diff --git a/lib/components/mobile/batch-search-screen.tsx b/lib/components/mobile/batch-search-screen.tsx index c24b4dacb..5be2cc8c7 100644 --- a/lib/components/mobile/batch-search-screen.tsx +++ b/lib/components/mobile/batch-search-screen.tsx @@ -4,6 +4,7 @@ import { injectIntl, IntlShape } from 'react-intl' import React, { Component } from 'react' import styled from 'styled-components' +import * as apiActions from '../../actions/api' import * as uiActions from '../../actions/ui' import { advancedPanelClassName, @@ -11,6 +12,7 @@ import { transitionDuration, TransitionStyles } from '../form/styled' +import { alertUserTripPlan } from '../form/util' import { MobileScreens } from '../../actions/ui-constants' import AdvancedSettingsPanel from '../form/advanced-settings-panel' import BatchSettings from '../form/batch-settings' @@ -43,8 +45,10 @@ const MobileSearchSettings = styled.div<{ ` interface Props { + currentQuery: any intl: IntlShape map: React.ReactElement + routingQuery: any setMobileScreen: (screen: number) => void } @@ -63,7 +67,10 @@ class BatchSearchScreen extends Component { _advancedSettingRef = React.createRef() handlePlanTripClick = () => { - this.setState({ planTripClicked: true }) + const { currentQuery, intl, routingQuery } = this.props + alertUserTripPlan(intl, currentQuery, routingQuery, () => + this.setState({ planTripClicked: true }) + ) } openAdvancedSettings = () => { @@ -159,6 +166,7 @@ class BatchSearchScreen extends Component { > { // connect to the redux store +const mapStateToProps = (state: any) => { + const currentQuery = state.otp.currentQuery + return { + currentQuery + } +} + const mapDispatchToProps = { + routingQuery: apiActions.routingQuery, setMobileScreen: uiActions.setMobileScreen } -export default connect(null, mapDispatchToProps)(injectIntl(BatchSearchScreen)) +export default connect( + mapStateToProps, + mapDispatchToProps +)(injectIntl(BatchSearchScreen)) diff --git a/percy/percy.test.js b/percy/percy.test.js index 37433032d..957f07e43 100644 --- a/percy/percy.test.js +++ b/percy/percy.test.js @@ -42,6 +42,12 @@ async function loadPath(otpPath) { return page } +const openEditIfNeeded = async (page, isMobile) => { + if (isMobile) { + await page.click('button.edit-search-button') + } +} + beforeAll(async () => { try { // Launch OTP-RR web server @@ -114,9 +120,7 @@ async function executeTest(page, isMobile, isCallTaker) { if (!isCallTaker) { // Edit trip params [mobile-specific] - if (isMobile) { - await page.click('button.edit-search-button') - } + await openEditIfNeeded(page, isMobile) // Change the modes: Activate Transit and remove Bike. await page.click('label[title="Transit"]') @@ -148,6 +152,8 @@ async function executeTest(page, isMobile, isCallTaker) { await page.waitForTimeout(500) // Delete both origin and destination + await openEditIfNeeded(page, isMobile) + await page.click('.from-form-control') await page.waitForTimeout(300) // Click the clear button next to it