Skip to content

Commit

Permalink
refactor(exception service): support trip editing for ex service
Browse files Browse the repository at this point in the history
  • Loading branch information
philip-cline committed Nov 10, 2023
1 parent a36bea3 commit 8125f09
Show file tree
Hide file tree
Showing 11 changed files with 94 additions and 20 deletions.
2 changes: 2 additions & 0 deletions i18n/english.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,8 @@ components:
save: Save
Validation:
agencyRequired: Field must be populated for feeds with more than one agency.
conflictingServiceId: Service ID already exists in a standard calendar!
customServiceId: Custom service ID
dateServiceIdCombinationDuplicate: Date (%exceptionDate%) and Service ID (%serviceId%) combination cannot appear more than once for all exceptions.
idMustBeUnique: Identifier must be unique.
idRequired: Identifier is required if more than one agency exists.
Expand Down
2 changes: 2 additions & 0 deletions i18n/german.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1473,6 +1473,8 @@ components:
save: Speichern
Validation:
agencyRequired: Field must be populated for feeds with more than one agency.
conflictingServiceId: Service ID already exists in a standard calendar!
customServiceId: Custom service ID
dateServiceIdCombinationDuplicate: Date (%exceptionDate%) and Service ID (%serviceId%) combination cannot appear more than once for all exceptions.
idMustBeUnique: Identifier must be unique.
idRequired: Identifier is required if more than one agency exists.
Expand Down
2 changes: 2 additions & 0 deletions i18n/polish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,8 @@ components:
save: Save
Validation:
agencyRequired: Field must be populated for feeds with more than one agency.
conflictingServiceId: Service ID already exists in a standard calendar!
customServiceId: Custom service ID
dateServiceIdCombinationDuplicate: Date (%exceptionDate%) and Service ID (%serviceId%) combination cannot appear more than once for all exceptions.
idMustBeUnique: Identifier must be unique.
idRequired: Identifier is required if more than one agency exists.
Expand Down
4 changes: 3 additions & 1 deletion lib/editor/actions/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,9 @@ export function fetchBaseGtfs ({
# (avoid duplicate dates).
dates
# Fetch exemplar for display of exception based service in calendar select
exemplar
exemplar
# Fetch custom_schedule for proper service_id in exception based services
custom_schedule
}
stops (limit: -1) {
id
Expand Down
21 changes: 21 additions & 0 deletions lib/editor/components/ScheduleExceptionForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ export default class ScheduleExceptionForm extends Component<Props> {
})
}

_onCustomScheduleChange = (evt: SyntheticInputEvent<HTMLInputElement>) => {
const {activeComponent, activeEntity, updateActiveGtfsEntity} = this.props
updateActiveGtfsEntity({
component: activeComponent,
entity: activeEntity,
props: {custom_schedule: evt.target.value}
})
}

_renderExceptionExemplars (exemplar: string, value: number): string {
switch (value) {
case EXCEPTION_EXEMPLARS.SWAP:
Expand Down Expand Up @@ -214,6 +223,18 @@ export default class ScheduleExceptionForm extends Component<Props> {
)
: null
}
{exemplar === EXCEPTION_EXEMPLARS.EXCEPTION_SERVICE &&
<FormGroup className={`col-xs-12`} validationState={this._checkValidation('Custom service ID')}>
<ControlLabel>
<small>Custom service ID</small>
</ControlLabel>
<FormControl
value={activeEntity.custom_schedule || ''} // If custom_shcedule not provided yet, provide empty string to override FormControl state.
placeholder='Thanksgiving-custom-1'
onChange={this._onCustomScheduleChange}
/>
</FormGroup>
}
<FormGroup
className={`col-xs-12`}
controlId={`exception-dates`}
Expand Down
4 changes: 2 additions & 2 deletions lib/editor/components/timetable/CalendarSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ export default class CalendarSelect extends Component<Props> {
// TODO: trip counts (total v pattern)
const exceptionBasedCalendarOptions = exceptionBasedCalendars && exceptionBasedCalendars.map(exception => ({
label: exception.name || exception.id,
value: exception.name,
value: exception.service_id,
type: 'exception-based',
service_id: exception.name
service_id: exception.service_id // For an exception based schedule the custom schedule should only have one entry.
}))

// $FlowFixMe: we need two types of options
Expand Down
21 changes: 8 additions & 13 deletions lib/editor/components/timetable/TimetableHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import * as tripActions from '../../actions/trip'
import OptionButton from '../../../common/components/OptionButton'
import HourMinuteInput from '../HourMinuteInput'
import {getTableById} from '../../util/gtfs'
import { getComponentMessages } from '../../../common/util/config'
import { EXCEPTION_EXEMPLARS } from '../../util'
import {getActiveCalendar, getExceptionBasedCalendars} from '../../util/timetable'
import {getComponentMessages} from '../../../common/util/config'
import type {TripValidationIssues} from '../../selectors/timetable'
import type {Feed, GtfsRoute, Pattern, ServiceCalendar, ScheduleException, TripCounts} from '../../../types'
import type {Feed, GtfsRoute, Pattern, ServiceCalendar, ScheduleException, ScheduleExceptionCalendar, TripCounts} from '../../../types'
import type {EditorTables, TimetableState} from '../../../types/reducers'

import PatternSelect from './PatternSelect'
Expand Down Expand Up @@ -70,7 +70,9 @@ export default class TimetableHeader extends Component<Props> {
_onClickUndoButton = (e: SyntheticInputEvent<HTMLInputElement>) => {
const {activePattern, activeScheduleId, feedSource, fetchTripsForCalendar, tableData} = this.props
const calendars: Array<ServiceCalendar> = getTableById(tableData, 'calendar')
const activeCalendar = calendars.find(c => c.service_id === activeScheduleId)
const scheduleExceptions: Array<ScheduleException> = getTableById(tableData, 'scheduleexception')
const exceptionBasedCalendars: Array<ScheduleExceptionCalendar> = getExceptionBasedCalendars(scheduleExceptions)
const activeCalendar = getActiveCalendar(calendars, exceptionBasedCalendars, activeScheduleId)
if (activeCalendar) {
fetchTripsForCalendar(feedSource.id, activePattern, activeCalendar.service_id)
} else console.warn(`Could not locate calendar with service_id=${activeScheduleId}`)
Expand Down Expand Up @@ -118,15 +120,8 @@ export default class TimetableHeader extends Component<Props> {
} = this.props
const {edited, hideDepartureTimes, selected, trips, useSecondsInOffset} = timetable
const calendars: Array<ServiceCalendar> = getTableById(tableData, 'calendar')
const scheduleExceptions: Array<ScheduleException> = getTableById(tableData, 'scheduleexception')
const exceptionBasedCalendars = scheduleExceptions && scheduleExceptions.reduce((calendars, exception) => {
if (exception.exemplar === EXCEPTION_EXEMPLARS.EXCEPTION_SERVICE) {
calendars.push({...exception, service_id: exception.name})
}
return calendars
}, [])

const activeCalendar = calendars.find(c => c.service_id === activeScheduleId) || exceptionBasedCalendars.find(c => c.name === activeScheduleId)
const exceptionBasedCalendars = getExceptionBasedCalendars(getTableById(tableData, 'scheduleexception'))
const activeCalendar = getActiveCalendar(calendars, exceptionBasedCalendars, activeScheduleId)
const headerStyle = {
backgroundColor: 'white'
}
Expand Down
3 changes: 1 addition & 2 deletions lib/editor/containers/ActiveTimetableEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ const mapStateToProps = (state: AppState, ownProps: Props) => {
const columns = getTimetableColumns(state)
const tripValidationErrors = getTripValidationErrors(state)
const {subEntity: activePattern} = active
const activeSchedule = getTableById(tables, 'calendar')
.find(c => c.service_id === activeScheduleId)
const activeSchedule = getTableById(tables, 'calendar').find(c => c.service_id === activeScheduleId) || getTableById(tables, 'scheduleexception').find(ex => ex.custom_schedule[0] === activeScheduleId)
const timetableStatus = state.editor.timetable.status
return {
activePatternId,
Expand Down
25 changes: 24 additions & 1 deletion lib/editor/util/timetable.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import moment from 'moment'

import type {TimetableColumn} from '../../types'
import type {ScheduleException, ScheduleExceptionCalendar, ServiceCalendar, TimetableColumn} from '../../types'

import { EXCEPTION_EXEMPLARS } from '.'

/**
* This object defines the timetable editor keyboard shorcuts.
Expand Down Expand Up @@ -48,6 +50,27 @@ export function parseTime (timeString: string) {
)
}

// Helper function to find and return the active calendar, be it traditional or exception-based.
export function getActiveCalendar (calendars: Array<ServiceCalendar>, exceptionBasedCalendars: Array<ScheduleExceptionCalendar>, activeScheduleId: string) {
const activeCalendarFilter = c => c.service_id === activeScheduleId
return calendars.find(activeCalendarFilter) || exceptionBasedCalendars.find(activeCalendarFilter)
}

// Function to filter schedule exceptions into exemplars for exception-based service and to modify the custom_schedule and
// service_id in the format that the Timetable comoponents require.
export function getExceptionBasedCalendars (scheduleExceptions: Array<ScheduleException>): Array<ScheduleExceptionCalendar> {
const exceptionBasedCalendars = scheduleExceptions && scheduleExceptions.reduce((calendars, exception) => {
if (exception.exemplar === EXCEPTION_EXEMPLARS.EXCEPTION_SERVICE) {
if (exception.custom_schedule && exception.custom_schedule[0]) {
calendars.push({...exception, custom_schedule: exception.custom_schedule[0], service_id: exception.custom_schedule[0]})
}
}
return calendars
}, [])
// $FlowFixMe: reduce confuses flow about custom_schedule type
return exceptionBasedCalendars
}

export const LEFT_COLUMN_WIDTH = 30
export const ROW_HEIGHT = 25
export const OVERSCAN_COLUMN_COUNT = 10
Expand Down
16 changes: 15 additions & 1 deletion lib/editor/util/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import clone from 'lodash/cloneDeep'
import moment from 'moment'
import validator from 'validator'

import type {Entity, GtfsSpecField, ScheduleException} from '../../types'
import type {Entity, GtfsSpecField, ScheduleException, ServiceCalendar} from '../../types'
import type {EditorTables} from '../../types/reducers'
import { getComponentMessages } from '../../common/util/config'

import {getTableById} from './gtfs'

import { EXCEPTION_EXEMPLARS } from '.'

export type EditorValidationIssue = {
field: string,
invalid: boolean,
Expand Down Expand Up @@ -332,6 +334,18 @@ export function validate (
}
}

// Special validation for exception based calendars
if (entity && entity.exemplar && entity.exemplar === EXCEPTION_EXEMPLARS.EXCEPTION_SERVICE) {
if (!entity.custom_schedule) valErrors.push(emptyFieldValidationIssue(messages('customServiceId')))
const calendars: Array<ServiceCalendar> = clone(getTableById(tableData, 'calendar'))

// Check if service ID exists elsewhere
if (entity.custom_schedule && calendars.some(c => c.service_id === entity.custom_schedule)) {
const reason = messages('conflictingServiceId')
valErrors.push(validationIssue(reason, messages('customServiceId')))
}
}

for (let i = 0; i < scheduleExceptions.length; i++) {
const scheduleException = scheduleExceptions[i]
const serviceIds = getExceptionServiceIds(scheduleException)
Expand Down
14 changes: 14 additions & 0 deletions lib/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,20 @@ export type ScheduleException = {|
removed_service: ?Array<string>
|}

export type ScheduleExceptionCalendar = {|
added_service: ?Array<string>,
custom_schedule: ?string, // An exception based calendar must have only one service_id in custom_schedule.
dates: Array<ExceptionDate>,
exemplar: number,
feedId: string,
id: number,
isCreating: boolean,
name: string,
removed_service: ?Array<string>,
service_id: string,

|}

export type ScheduleExceptionDateRange = {
endDate: ExceptionDate,
startDate: ExceptionDate
Expand Down

0 comments on commit 8125f09

Please sign in to comment.