diff --git a/i18n/english.yml b/i18n/english.yml index 182450331..ccda84b77 100644 --- a/i18n/english.yml +++ b/i18n/english.yml @@ -58,6 +58,9 @@ components: deployments: Deployments projects: Projects root: Explore + CalendarSelect: + exceptionBasedCalendar: (Exception based calendar) + selectCalendar: " Select calendar..." ConfirmModal: cancel: Cancel ok: OK @@ -327,6 +330,9 @@ components: createStop: Right-click a location on map to create a new stop editSchedules: Edit schedules name: Name + ExceptionCalendarSelector: + selectCalendar: Select calendar... + unnamedDefault: "[unnamed]" ExceptionDate: addRange: Add range dateRemoved: ⓘ Date has been removed. Date entered is already included in an existing range or single date! @@ -1165,6 +1171,24 @@ components: patterns: "%num% Patterns" stops: "%num% Stops" trips: "%num% Trips" + ScheduleExceptionForm: + addDate: Add date + customServiceID: Custom service ID + dates: Dates + exceptionBasedService: Exception Based Service + exceptionName: Exception name* + noDatesSpecified: No dates specified + noService: No Service + onTheseDates: On these dates* + ranges: Ranges + runFollowing: "Run the following schedule:" + selectCalendar: Select calendar to run* + selectCalendarToAdd: "Select calendars to add (optional):" + selectCalendarToRemove: "Select calendars to remove (optional):" + selectExemplar: -- Select exception type -- + swapAddOrRemove: Swap, add, or remove + thanksgivingDay: Thanksgiving Day + unnamedDefault: "[unnamed]" SelectFileModal: ok: OK cancel: Cancel @@ -1469,6 +1493,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. diff --git a/i18n/german.yml b/i18n/german.yml index 788a40925..12d8c2201 100644 --- a/i18n/german.yml +++ b/i18n/german.yml @@ -58,6 +58,9 @@ components: deployments: Deployments projects: Projekte root: Erkunden + CalendarSelect: + exceptionBasedCalendar: (Exception based calendar) + selectCalendar: " Select calendar..." ConfirmModal: cancel: Abbrechen ok: OK @@ -336,6 +339,9 @@ components: auf eine Örtlickeit in der Karte editSchedules: Abfahrzeiten erstellen name: Name + ExceptionCalendarSelector: + selectCalendar: Select calendar... + unnamedDefault: "[unnamed]" ExceptionDate: addRange: Add range dateRemoved: ⓘ Date has been removed. Date entered is already included in an existing range or single date! @@ -1167,6 +1173,24 @@ components: patterns: "%num% Patterns" stops: "%num% Stops" trips: "%num% Trips" + ScheduleExceptionForm: + addDate: " Add date" + customServiceID: Custom service ID + dates: Dates + exceptionBasedService: Exception Based Service + exceptionName: Exception name* + noDatesSpecified: No dates specified + noService: No Service + onTheseDates: On these dates* + ranges: Ranges + runFollowing: "Run the following schedule:" + selectCalendar: Select calendar to run* + selectCalendarToAdd: "Select calendars to add (optional):" + selectCalendarToRemove: "Select calendars to remove (optional):" + selectExemplar: -- Select exception type -- + swapAddOrRemove: Swap, add, or remove + thanksgivingDay: Thanksgiving Day + unnamedDefault: "[unnamed]" SelectFileModal: cancel: Abbrechen ok: OK @@ -1473,6 +1497,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. diff --git a/i18n/polish.yml b/i18n/polish.yml index f47e6518f..ca78d076d 100644 --- a/i18n/polish.yml +++ b/i18n/polish.yml @@ -58,6 +58,9 @@ components: deployments: Wdrożenia projects: Projekty root: Eksploruj + CalendarSelect: + exceptionBasedCalendar: (Exception based calendar) + selectCalendar: " Select calendar..." ConfirmModal: cancel: Cancel ok: OK @@ -333,6 +336,9 @@ components: createStop: Right-click a location on map to create a new stop editSchedules: Edit schedules name: Name + ExceptionCalendarSelector: + selectCalendar: Select calendar... + unnamedDefault: "[unnamed]" ExceptionDate: addRange: Add range dateRemoved: ⓘ Date has been removed. Date entered is already included in an existing range or single date! @@ -1153,6 +1159,24 @@ components: patterns: "%num% Patterns" stops: "%num% Stops" trips: "%num% Trips" + ScheduleExceptionForm: + addDate: " Add date" + customServiceID: Custom service ID + dates: Dates + exceptionBasedService: Exception Based Service + exceptionName: Exception name* + noDatesSpecified: No dates specified + noService: No Service + onTheseDates: On these dates* + ranges: Ranges + runFollowing: "Run the following schedule:" + selectCalendar: Select calendar to run* + selectCalendarToAdd: "Select calendars to add (optional):" + selectCalendarToRemove: "Select calendars to remove (optional):" + selectExemplar: -- Select exception type -- + swapAddOrRemove: Swap, add, or remove + thanksgivingDay: Thanksgiving Day + unnamedDefault: "[unnamed]" SelectFileModal: cancel: Cancel ok: OK @@ -1450,6 +1474,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. diff --git a/lib/editor/actions/editor.js b/lib/editor/actions/editor.js index 589feab5d..b83e5ad06 100644 --- a/lib/editor/actions/editor.js +++ b/lib/editor/actions/editor.js @@ -513,6 +513,10 @@ export function fetchBaseGtfs ({ # Fetch dates to ensure we can do validation in the UI # (avoid duplicate dates). dates + # Fetch exemplar for display of exception based service in calendar select + exemplar + # Fetch custom_schedule for proper service_id in exception based services + custom_schedule } stops (limit: -1) { id diff --git a/lib/editor/components/ScheduleExceptionForm.js b/lib/editor/components/ScheduleExceptionForm.js index d15a615fa..541f06a9c 100644 --- a/lib/editor/components/ScheduleExceptionForm.js +++ b/lib/editor/components/ScheduleExceptionForm.js @@ -17,6 +17,7 @@ import toSentenceCase from '../../common/util/text' import {getRangesForDates} from '../../common/util/exceptions' import {EXCEPTION_EXEMPLARS} from '../util' import {getTableById} from '../util/gtfs' +import {getComponentMessages} from '../../common/util/config' import type {ServiceCalendar, ScheduleException} from '../../types' import type {EditorTables} from '../../types/reducers' import type {EditorValidationIssue} from '../util/validation' @@ -37,6 +38,8 @@ type SelectOption = { } export default class ScheduleExceptionForm extends Component { + messages = getComponentMessages('ScheduleExceptionForm') + _onAddDate = () => { const {activeComponent, activeEntity, updateActiveGtfsEntity} = this.props const dates = [...activeEntity.dates] @@ -87,12 +90,23 @@ export default class ScheduleExceptionForm extends Component { }) } + _onCustomScheduleChange = (evt: SyntheticInputEvent) => { + 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: - return 'Swap, add, or remove' + return this.messages('swapAddOrRemove') case EXCEPTION_EXEMPLARS.NO_SERVICE: - return toSentenceCase(exemplar.replace('_', ' ')) + return this.messages('noService') + case EXCEPTION_EXEMPLARS.EXCEPTION_SERVICE: + return this.messages('exceptionBasedService') default: return toSentenceCase(exemplar) } @@ -147,11 +161,11 @@ export default class ScheduleExceptionForm extends Component { validationState={this._checkValidation('name')} > - Exception name* + {this.messages('exceptionName')} { validationState={this._checkValidation('exemplar')} > - Run the following schedule: + {this.messages('runFollowing')} {Object.keys(EXCEPTION_EXEMPLARS) .map(exemplar => { @@ -186,7 +200,7 @@ export default class ScheduleExceptionForm extends Component { {exemplar === EXCEPTION_EXEMPLARS.CUSTOM ? { ? (
{ ) : null } + {exemplar === EXCEPTION_EXEMPLARS.EXCEPTION_SERVICE && + + + {this.messages('customServiceID')} + + + + } { validationState={this._checkValidation('dates')} > - On these dates* + {this.messages('onTheseDates')} { maintainContainerHeight typeName={null} > - {datesAndRangesBothActive &&
Ranges
} + {datesAndRangesBothActive &&
{this.messages('ranges')}
} {ranges && ranges.map((range, index) => { const startDateIndex = dates.findIndex(d => d === range.startDate) const endDateIndex = dates.findIndex(d => d === range.endDate) @@ -242,7 +268,7 @@ export default class ScheduleExceptionForm extends Component { {...this.props} />) })} - {datesAndRangesBothActive &&
Dates
} + {datesAndRangesBothActive &&
{this.messages('dates')}
} {parsedDates && parsedDates.map((date, index) => { const dateIndex = dates.findIndex(d => d === date) return ( @@ -256,7 +282,7 @@ export default class ScheduleExceptionForm extends Component { })}
{(!parsedDates || parsedDates.length === 0) && (!ranges || ranges.length === 0) && -
No dates specified
+
{this.messages('noDatesSpecified')}
}
@@ -265,7 +291,7 @@ export default class ScheduleExceptionForm extends Component { disabled={this.props.validationErrors.find(el => el.field.includes('dates-'))} // Any dates validation issue blocks adding a new date onClick={this._onAddDate} > - Add date + {this.messages('addDate')}
@@ -283,6 +309,8 @@ type SelectorProps = { } class ExceptionCalendarSelector extends Component { + messages = getComponentMessages('ExceptionCalendarSelector') + _onChange = (input: ?Array) => { const {id, onChange} = this.props onChange && onChange(input, id) @@ -295,7 +323,7 @@ class ExceptionCalendarSelector extends Component { }) _getCalendarName = calendar => { - let name = '[unnamed]' + let name = this.messages('unnamedDefault') if (calendar) { name = calendar.description ? calendar.description : '' name += calendar.id ? ` (${calendar.id})` : '' @@ -309,7 +337,7 @@ class ExceptionCalendarSelector extends Component { {label} Select calendar...} + placeholder={{this.messages('selectCalendar')}} valueRenderer={this._optionRenderer} optionRenderer={this._optionRenderer} disabled={!activePattern || entityIsNew(activePattern)} diff --git a/lib/editor/components/timetable/TimetableHeader.js b/lib/editor/components/timetable/TimetableHeader.js index f688eca04..1b0ae34d7 100644 --- a/lib/editor/components/timetable/TimetableHeader.js +++ b/lib/editor/components/timetable/TimetableHeader.js @@ -22,9 +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 {getActiveCalendar, getExceptionBasedCalendars} from '../../util/timetable' +import {getComponentMessages} from '../../../common/util/config' import type {TripValidationIssues} from '../../selectors/timetable' -import type {Feed, GtfsRoute, Pattern, ServiceCalendar, 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' @@ -69,7 +70,9 @@ export default class TimetableHeader extends Component { _onClickUndoButton = (e: SyntheticInputEvent) => { const {activePattern, activeScheduleId, feedSource, fetchTripsForCalendar, tableData} = this.props const calendars: Array = getTableById(tableData, 'calendar') - const activeCalendar = calendars.find(c => c.service_id === activeScheduleId) + const scheduleExceptions: Array = getTableById(tableData, 'scheduleexception') + const exceptionBasedCalendars: Array = 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}`) @@ -117,7 +120,8 @@ export default class TimetableHeader extends Component { } = this.props const {edited, hideDepartureTimes, selected, trips, useSecondsInOffset} = timetable const calendars: Array = getTableById(tableData, 'calendar') - const activeCalendar = calendars.find(c => c.service_id === activeScheduleId) + const exceptionBasedCalendars = getExceptionBasedCalendars(getTableById(tableData, 'scheduleexception')) + const activeCalendar = getActiveCalendar(calendars, exceptionBasedCalendars, activeScheduleId) const headerStyle = { backgroundColor: 'white' } @@ -370,14 +374,16 @@ export default class TimetableHeader extends Component { + /> {/* Edit timetable buttons */} diff --git a/lib/editor/containers/ActiveTimetableEditor.js b/lib/editor/containers/ActiveTimetableEditor.js index b70129bcc..3e5a86056 100644 --- a/lib/editor/containers/ActiveTimetableEditor.js +++ b/lib/editor/containers/ActiveTimetableEditor.js @@ -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 && ex.custom_schedule[0] === activeScheduleId) const timetableStatus = state.editor.timetable.status return { activePatternId, diff --git a/lib/editor/util/index.js b/lib/editor/util/index.js index 4f9f4d29b..1670d9b66 100644 --- a/lib/editor/util/index.js +++ b/lib/editor/util/index.js @@ -30,7 +30,8 @@ export const EXCEPTION_EXEMPLARS = { SUNDAY: 6, NO_SERVICE: 7, CUSTOM: 8, - SWAP: 9 + SWAP: 9, + EXCEPTION_SERVICE: 10 } export const ROUTE_STATUS_CODES = { diff --git a/lib/editor/util/timetable.js b/lib/editor/util/timetable.js index 7fc1bd5d2..e184fc206 100644 --- a/lib/editor/util/timetable.js +++ b/lib/editor/util/timetable.js @@ -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. @@ -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, exceptionBasedCalendars: Array, activeScheduleId: string): ?ServiceCalendar | ?ScheduleExceptionCalendar { + 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): Array { + 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 diff --git a/lib/editor/util/validation.js b/lib/editor/util/validation.js index 806f1d111..a94e23b6a 100644 --- a/lib/editor/util/validation.js +++ b/lib/editor/util/validation.js @@ -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, @@ -332,6 +334,19 @@ 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 = clone(getTableById(tableData, 'calendar')) + + // Check if service ID exists elsewhere + // $FlowFixMe: Array#some confuses flow it seems. + 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) diff --git a/lib/types/index.js b/lib/types/index.js index 2942cbf29..82d7f134b 100644 --- a/lib/types/index.js +++ b/lib/types/index.js @@ -864,6 +864,20 @@ export type ScheduleException = {| removed_service: ?Array |} +export type ScheduleExceptionCalendar = {| + added_service: ?Array, + custom_schedule: ?string, // An exception based calendar must have only one service_id in custom_schedule. + dates: Array, + exemplar: number, + feedId: string, + id: number, + isCreating: boolean, + name: string, + removed_service: ?Array, + service_id: string, + +|} + export type ScheduleExceptionDateRange = { endDate: ExceptionDate, startDate: ExceptionDate diff --git a/lib/types/reducers.js b/lib/types/reducers.js index 2a72674f1..17a74223e 100644 --- a/lib/types/reducers.js +++ b/lib/types/reducers.js @@ -144,6 +144,7 @@ export type EditorTables = { tripPatterns?: Array }>, schedule_exceptions: Array<{ + exemplar: number, id: number, name: string }>,