Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configure the ability to save trip for only one day #1280

Merged
merged 6 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
amy-corson-ibigroup marked this conversation as resolved.
Show resolved Hide resolved
elevationProfile?: boolean
extraMenuItems?: AppMenuItemConfig[]
geocoder: GeocoderConfig
Expand Down
Loading