Skip to content

Commit

Permalink
Merge pull request #1280 from opentripplanner/disable-sinlge-itinerar…
Browse files Browse the repository at this point in the history
…y-days

Configure the ability to save trip for only one day
  • Loading branch information
amy-corson-ibigroup authored Sep 30, 2024
2 parents ca8995a + 4c3c5d5 commit 8c85cb8
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 99 deletions.
2 changes: 2 additions & 0 deletions example-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions i18n/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions i18n/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ components:
SavedTripScreen:
itineraryLoaded: Trajet chargé
itineraryLoading: Chargement du trajet
selectAtLeastOneDay: Veuillez choisir au moins un jour pour le suivi.
tooManyTrips: >
Vous avez déjà atteint le nombre maximum de 5 trajets enregistrés.
Veuillez supprimer les trajets enregistrés qui sont inutilisés, puis
Expand Down
39 changes: 36 additions & 3 deletions lib/components/user/monitored-trip/saved-trip-screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = (
<Formik
Expand Down Expand Up @@ -236,8 +267,10 @@ const mapStateToProps = (state, ownProps) => {
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],
Expand Down
239 changes: 143 additions & 96 deletions lib/components/user/monitored-trip/trip-basics-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -46,6 +46,7 @@ type TripBasicsProps = WrappedComponentProps &
intl: IntlShape
) => void
clearItineraryExistence: () => void
disableSingleItineraryDays?: boolean
isCreating: boolean
itineraryExistence?: ItineraryExistence
}
Expand Down Expand Up @@ -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
isCreating: boolean
monitoredTrip: MonitoredTrip
}) => {
const intl = useIntl()
const baseColor = getBaseColor()
return (
<>
{errorCheckingTrip && (
<>
{/* FIXME: Temporary solution until itinerary existence check is fixed. */}
<br />
<FormattedMessage id="actions.user.itineraryExistenceCheckFailed" />
</>
)}
<AvailableDays>
{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 (
<MonitoredDayCircle
baseColor={baseColor}
key={day}
monitored={!isDayDisabled && monitoredTrip[day]}
title={notAvailableText}
>
<Field
// Let users save an existing trip, even though it may not be available on some days.
// TODO: improve checking trip availability.
disabled={isDayDisabled && isCreating}
id={day}
name={day}
type="checkbox"
/>
<Ban aria-hidden />
<label htmlFor={day}>
<InvisibleA11yLabel>
<FormattedDayOfWeek day={day} />
</InvisibleA11yLabel>
<span aria-hidden className={labelClass}>
{/* The abbreviated text is visual only. Screen readers should read out the full day. */}
<FormattedDayOfWeekCompact day={day} />
</span>
</label>
<InvisibleA11yLabel>{notAvailableText}</InvisibleA11yLabel>
</MonitoredDayCircle>
)
})}
</AvailableDays>
<HelpBlock role="status">
{finalItineraryExistence ? (
<FormattedMessage id="components.TripBasicsPane.tripIsAvailableOnDaysIndicated" />
) : (
<ProgressBar
active
label={
<FormattedMessage id="components.TripBasicsPane.checkingItineraryExistence" />
}
now={100}
/>
)}
</HelpBlock>
<HelpBlock role="alert">
{errorSelectingDays && (
<FormattedValidationError type="select-at-least-one-day" />
)}
</HelpBlock>
</>
)
}

/**
* This component shows summary information for a trip
* and lets the user edit the trip name and day.
Expand Down Expand Up @@ -220,6 +312,7 @@ class TripBasicsPane extends Component<TripBasicsProps, State> {
const {
canceled,
dirty,
disableSingleItineraryDays,
errors,
intl,
isCreating,
Expand Down Expand Up @@ -257,6 +350,9 @@ class TripBasicsPane extends Component<TripBasicsProps, State> {
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 (
<div>
{/* TODO: This component does not block navigation on reload or using the back button.
Expand Down Expand Up @@ -286,104 +382,53 @@ class TripBasicsPane extends Component<TripBasicsProps, State> {
)}
</HelpBlock>
</FormGroup>

<FormGroup>
<ControlLabel>
<FormattedMessage id="components.TripBasicsPane.tripDaysPrompt" />
</ControlLabel>
<Radio
checked={!isOneTime}
// FIXME: Temporary solution until itinerary existence check is fixed.
disabled={errorCheckingTrip}
onChange={this._handleRecurringTrip}
>
<FormattedMessage id="components.TripBasicsPane.recurringEachWeek" />
{errorCheckingTrip && (
{disableSingleItineraryDays ? (
<FormGroup validationState={selectOneDayError}>
<ControlLabel>
<FormattedMessage id="components.TripBasicsPane.tripDaysPrompt" />
</ControlLabel>
<RenderAvailableDays
errorCheckingTrip={errorCheckingTrip}
errorSelectingDays={selectOneDayError}
finalItineraryExistence={finalItineraryExistence}
isCreating={isCreating}
monitoredTrip={monitoredTrip}
/>
</FormGroup>
) : (
<FormGroup>
<ControlLabel>
<FormattedMessage id="components.TripBasicsPane.tripDaysPrompt" />
</ControlLabel>
<Radio
checked={!isOneTime}
// FIXME: Temporary solution until itinerary existence check is fixed.
disabled={errorCheckingTrip}
onChange={this._handleRecurringTrip}
>
<FormattedMessage id="components.TripBasicsPane.recurringEachWeek" />
</Radio>
{!isOneTime && (
<>
{/* FIXME: Temporary solution until itinerary existence check is fixed. */}
<br />
<FormattedMessage id="actions.user.itineraryExistenceCheckFailed" />
<RenderAvailableDays
errorCheckingTrip={errorCheckingTrip}
finalItineraryExistence={finalItineraryExistence}
isCreating={isCreating}
monitoredTrip={monitoredTrip}
/>
</>
)}
</Radio>
{!isOneTime && (
<>
<AvailableDays>
{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 (
<MonitoredDayCircle
baseColor={baseColor}
key={day}
monitored={!isDayDisabled && monitoredTrip[day]}
title={notAvailableText}
>
<Field
// Let users save an existing trip, even though it may not be available on some days.
// TODO: improve checking trip availability.
disabled={isDayDisabled && isCreating}
id={day}
name={day}
type="checkbox"
/>
<Ban aria-hidden />
<label htmlFor={day}>
<InvisibleA11yLabel>
<FormattedDayOfWeek day={day} />
</InvisibleA11yLabel>
<span aria-hidden className={labelClass}>
{/* The abbreviated text is visual only. Screen readers should read out the full day. */}
<FormattedDayOfWeekCompact day={day} />
</span>
</label>
<InvisibleA11yLabel>
{notAvailableText}
</InvisibleA11yLabel>
</MonitoredDayCircle>
)
})}
</AvailableDays>
<HelpBlock role="status">
{finalItineraryExistence ? (
<FormattedMessage id="components.TripBasicsPane.tripIsAvailableOnDaysIndicated" />
) : (
<ProgressBar
active
label={
<FormattedMessage id="components.TripBasicsPane.checkingItineraryExistence" />
}
now={100}
/>
)}
</HelpBlock>
</>
)}
<Radio checked={isOneTime} onChange={this._handleOneTimeTrip}>
<FormattedMessage
id="components.TripBasicsPane.onlyOnDate"
values={{ date: itinerary.startTime }}
/>
</Radio>

{/* Scroll to the trip name/days fields if submitting and there is an error on these fields. */}
<FormikErrorFocus align="middle" duration={200} />
</FormGroup>
<Radio checked={isOneTime} onChange={this._handleOneTimeTrip}>
<FormattedMessage
id="components.TripBasicsPane.onlyOnDate"
values={{ date: itinerary.startTime }}
/>
</Radio>
</FormGroup>
)}

{/* Scroll to the trip name/days fields if submitting and there is an error on these fields. */}
<FormikErrorFocus align="middle" duration={200} />
</div>
)
}
Expand All @@ -394,7 +439,9 @@ class TripBasicsPane extends Component<TripBasicsProps, State> {

const mapStateToProps = (state: AppReduxState) => {
const { itineraryExistence } = state.user
const { disableSingleItineraryDays } = state.otp.config
return {
disableSingleItineraryDays,
itineraryExistence
}
}
Expand Down
4 changes: 4 additions & 0 deletions lib/components/util/formatted-validation-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export default function FormattedValidationError({ type }) {
return (
<FormattedMessage id="components.SavedTripScreen.tripNameRequired" />
)
case 'select-at-least-one-day':
return (
<FormattedMessage id="components.SavedTripScreen.selectAtLeastOneDay" />
)
default:
return null
}
Expand Down
1 change: 1 addition & 0 deletions lib/util/config-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ export interface AppConfig {
co2?: CO2Config
companies?: Company[]
dateTime?: DateTimeConfig
disableSingleItineraryDays?: boolean
elevationProfile?: boolean
extraMenuItems?: AppMenuItemConfig[]
geocoder: GeocoderConfig
Expand Down

0 comments on commit 8c85cb8

Please sign in to comment.