diff --git a/example-config.yml b/example-config.yml index 0312aa3ac..79a242954 100644 --- a/example-config.yml +++ b/example-config.yml @@ -436,6 +436,8 @@ itinerary: advancedSettingsPanel: # Show button in advanced panel that allows users to save and return saveAndReturnButton: true +# Prevent users from selecting a single day for saving trips. +disableSingleItineraryDays: false # The transitOperators key is a list of transit operators that can be used to # order transit agencies when sorting by route. Also, this can optionally diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 1b13a37da..5df0782de 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -534,6 +534,7 @@ components: SavedTripScreen: itineraryLoaded: Itinerary loaded itineraryLoading: Loading itinerary + selectAtLeastOneDay: Please select at least one day to monitor. tooManyTrips: > You already have reached the maximum of five saved trips. Please remove unused trips from your saved trips, and try again. diff --git a/lib/components/user/monitored-trip/saved-trip-screen.js b/lib/components/user/monitored-trip/saved-trip-screen.js index 14ab18f8b..d0b7f1c1f 100644 --- a/lib/components/user/monitored-trip/saved-trip-screen.js +++ b/lib/components/user/monitored-trip/saved-trip-screen.js @@ -149,8 +149,14 @@ class SavedTripScreen extends Component { } render() { - const { isCreating, itinerary, loggedInUser, monitoredTrips, pending } = - this.props + const { + disableSingleItineraryDays, + isCreating, + itinerary, + loggedInUser, + monitoredTrips, + pending + } = this.props const isAwaiting = !monitoredTrips || (isCreating && pending) let screenContents @@ -176,7 +182,32 @@ class SavedTripScreen extends Component { // Text constant is used to allow format.js command line tool to keep track of // which IDs are in the code. .notOneOf(otherTripNames, 'trip-name-already-used') - const validationSchema = yup.object(clonedSchemaShape) + const validationSchema = yup + .object(clonedSchemaShape) + // If disableSingleItineraryDays is true, test to see if at least one day checkbox is checked + .test('dayofweek', 'Please select one day', function (obj) { + if ( + obj.monday || + obj.tuesday || + obj.wednesday || + obj.thursday || + obj.friday || + obj.saturday || + obj.sunday || + !disableSingleItineraryDays + ) { + return true + } + + /* Hack: because the selected days values are not grouped, we need to assign this error to one of the + checkboxes so that form validates correctly. Monday makes sure the focus is on the first checkbox. */ + + return new yup.ValidationError( + 'Please select at least one day to monitor', + obj.monday, + 'monday' + ) + }) screenContents = ( { const pending = activeSearch ? Boolean(activeSearch.pending) : false const itineraries = getActiveItineraries(state) || [] const tripId = ownProps.match.params.id + const { disableSingleItineraryDays } = state.otp.config return { activeSearchId: state.otp.activeSearchId, + disableSingleItineraryDays, homeTimezone: state.otp.config.homeTimezone, isCreating: tripId === 'new', itinerary: itineraries[activeItinerary], diff --git a/lib/components/user/monitored-trip/trip-basics-pane.tsx b/lib/components/user/monitored-trip/trip-basics-pane.tsx index 9d98eedd1..1fe2e9ac7 100644 --- a/lib/components/user/monitored-trip/trip-basics-pane.tsx +++ b/lib/components/user/monitored-trip/trip-basics-pane.tsx @@ -9,7 +9,7 @@ import { Radio } from 'react-bootstrap' import { Field, FormikProps } from 'formik' -import { FormattedMessage, injectIntl } from 'react-intl' +import { FormattedMessage, injectIntl, useIntl } from 'react-intl' import { Prompt } from 'react-router' // @ts-expect-error FormikErrorFocus does not support TypeScript yet. import FormikErrorFocus from 'formik-error-focus' @@ -46,6 +46,7 @@ type TripBasicsProps = WrappedComponentProps & intl: IntlShape ) => void clearItineraryExistence: () => void + disableSingleItineraryDays?: boolean isCreating: boolean itineraryExistence?: ItineraryExistence } @@ -132,6 +133,97 @@ function isDisabled(day: string, itineraryExistence?: ItineraryExistence) { return itineraryExistence && !itineraryExistence[day]?.valid } +const RenderAvailableDays = ({ + errorCheckingTrip, + errorSelectingDays, + finalItineraryExistence, + isCreating, + monitoredTrip +}: { + errorCheckingTrip: boolean + errorSelectingDays?: 'error' | null + finalItineraryExistence: ItineraryExistence | undefined + isCreating: boolean + monitoredTrip: MonitoredTrip +}) => { + const intl = useIntl() + const baseColor = getBaseColor() + return ( + <> + {errorCheckingTrip && ( + <> + {/* FIXME: Temporary solution until itinerary existence check is fixed. */} +
+ + + )} + + {ALL_DAYS.map((day) => { + const isDayDisabled = isDisabled(day, finalItineraryExistence) + const labelClass = isDayDisabled ? 'disabled-day' : '' + const notAvailableText = isDayDisabled + ? intl.formatMessage( + { + id: 'components.TripBasicsPane.tripNotAvailableOnDay' + }, + { + repeatedDay: getFormattedDayOfWeekPlural(day, intl) + } + ) + : '' + + return ( + + + + + {notAvailableText} + + ) + })} + + + {finalItineraryExistence ? ( + + ) : ( + + } + now={100} + /> + )} + + + {errorSelectingDays && ( + + )} + + + ) +} + /** * This component shows summary information for a trip * and lets the user edit the trip name and day. @@ -220,6 +312,7 @@ class TripBasicsPane extends Component { const { canceled, dirty, + disableSingleItineraryDays, errors, intl, isCreating, @@ -257,6 +350,9 @@ class TripBasicsPane extends Component { const errorCheckingTrip = ALL_DAYS.every((day) => isDisabled(day, finalItineraryExistence) ) + /* Hack: because the selected days checkboxes are not grouped, we need to assign this error to one of the + checkboxes so that the FormikErrorFocus works. */ + const selectOneDayError = errorStates.monday return (
{/* TODO: This component does not block navigation on reload or using the back button. @@ -286,104 +382,53 @@ class TripBasicsPane extends Component { )} - - - - - - - - {errorCheckingTrip && ( + {disableSingleItineraryDays ? ( + + + + + + + ) : ( + + + + + + + + {!isOneTime && ( <> - {/* FIXME: Temporary solution until itinerary existence check is fixed. */} -
- + )} -
- {!isOneTime && ( - <> - - {ALL_DAYS.map((day) => { - const isDayDisabled = isDisabled( - day, - finalItineraryExistence - ) - const labelClass = isDayDisabled ? 'disabled-day' : '' - const notAvailableText = isDayDisabled - ? intl.formatMessage( - { - id: 'components.TripBasicsPane.tripNotAvailableOnDay' - }, - { - repeatedDay: getFormattedDayOfWeekPlural(day, intl) - } - ) - : '' - - const baseColor = getBaseColor() - return ( - - - - - - {notAvailableText} - - - ) - })} - - - {finalItineraryExistence ? ( - - ) : ( - - } - now={100} - /> - )} - - - )} - - - - - {/* Scroll to the trip name/days fields if submitting and there is an error on these fields. */} - -
+ + + + + )} + + {/* Scroll to the trip name/days fields if submitting and there is an error on these fields. */} +
) } @@ -394,7 +439,9 @@ class TripBasicsPane extends Component { const mapStateToProps = (state: AppReduxState) => { const { itineraryExistence } = state.user + const { disableSingleItineraryDays } = state.otp.config return { + disableSingleItineraryDays, itineraryExistence } } diff --git a/lib/components/util/formatted-validation-error.js b/lib/components/util/formatted-validation-error.js index 2bf08ecb2..9b836207f 100644 --- a/lib/components/util/formatted-validation-error.js +++ b/lib/components/util/formatted-validation-error.js @@ -28,6 +28,10 @@ export default function FormattedValidationError({ type }) { return ( ) + case 'select-at-least-one-day': + return ( + + ) default: return null } diff --git a/lib/util/config-types.ts b/lib/util/config-types.ts index 3f71126cb..80f9cf10f 100644 --- a/lib/util/config-types.ts +++ b/lib/util/config-types.ts @@ -382,6 +382,7 @@ export interface AppConfig { co2?: CO2Config companies?: Company[] dateTime?: DateTimeConfig + disableSingleItineraryDays?: boolean elevationProfile?: boolean extraMenuItems?: AppMenuItemConfig[] geocoder: GeocoderConfig