├── .flowconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── __mocks__ ├── auth0-js.js └── auth0-lock.js ├── __tests__ ├── end-to-end.js └── test-utils │ └── setup.js ├── configurations ├── default │ ├── env.yml.tmp │ └── settings.yml ├── end-to-end │ ├── env.yml.tmp │ ├── test-gtfs-to-fetch.zip │ └── test-gtfs-to-upload.zip └── test │ ├── env.yml │ └── settings.yml ├── docs ├── dev │ ├── api_interaction.md │ ├── deployment.md │ ├── development.md │ └── migration.md ├── img │ ├── add-fare-zone.png │ ├── auth0-token-generator.png │ ├── create-project.png │ ├── create-user.png │ ├── edit-calendars.png │ ├── edit-fare-rules.png │ ├── edit-fares.png │ ├── edit-frequencies.png │ ├── edit-patterns.png │ ├── edit-routes.png │ ├── edit-stops.png │ ├── edit-timetables.png │ ├── feed-profile.png │ ├── feed-version-navigator.png │ ├── password-reset-logged-in.png │ ├── password-reset-logged-out.png │ ├── pattern-add-stop.png │ ├── pattern-insert-stop.png │ ├── pattern-shape-panel.png │ ├── pattern-stop-order.png │ ├── pattern-stop-toolbar.png │ ├── project-profile.png │ ├── public-portal.png │ ├── quick-access-toolbar.png │ ├── schedule-exception.png │ ├── schedule-toolbar.png │ ├── select-trips.png │ ├── timetable-selector.png │ ├── user-admin.png │ ├── user-profile.png │ └── view-all-stops.png ├── index.md ├── style.css └── user │ ├── appendix-gtfs-warnings.md │ ├── editor │ ├── calendars.md │ ├── fares.md │ ├── introduction.md │ ├── patterns.md │ ├── routes.md │ ├── schedules.md │ └── stops.md │ ├── introduction.md │ ├── managing-projects-feeds.md │ └── managing-users.md ├── flow-typed └── npm │ ├── @conveyal │ └── lonlat_v1.x.x.js │ ├── babel-polyfill_v6.x.x.js │ ├── common-tags_v1.4.x.js │ ├── flow-bin_v0.x.x.js │ ├── isomorphic-fetch_v2.x.x.js │ ├── jest_v22.x.x.js │ ├── js-yaml_v3.x.x.js │ ├── lodash.throttle_v4.x.x.js │ ├── lodash.tolower_v4.x.x.js │ ├── lodash.upperfirst_v4.x.x.js │ ├── lodash_v4.x.x.js │ ├── moment_v2.3.x.js │ ├── nock_v9.x.x.js │ ├── numeral_v2.x.x.js │ ├── object-path_v0.11.x.js │ ├── polyline_v0.2.x.js │ ├── prop-types_v15.x.x.js │ ├── puppeteer_v1.2.x.js │ ├── qs_v6.x.x.js │ ├── react-color_v2.x.x.js │ ├── react-dnd-html5-backend_v2.x.x.js │ ├── react-dnd_v2.x.x.js │ ├── react-redux_v5.x.x.js │ ├── redux-actions_v2.x.x.js │ ├── redux_v3.x.x.js │ ├── reselect_v3.x.x.js │ ├── turf-point_v2.x.x.js │ ├── turf-polygon_v1.x.x.js │ ├── uuid_v3.x.x.js │ └── validator_v7.x.x.js ├── gtfs.yml ├── gtfsplus.yml ├── i18n ├── english.yml ├── espanol.yml └── francais.yml ├── index.html ├── lib ├── admin │ ├── actions │ │ ├── admin.js │ │ └── organizations.js │ ├── components │ │ ├── CreateUser.js │ │ ├── OrganizationList.js │ │ ├── OrganizationSettings.js │ │ ├── ProjectSettings.js │ │ ├── UserAdmin.js │ │ ├── UserList.js │ │ ├── UserRow.js │ │ ├── UserSettings.js │ │ └── permissions.js │ ├── containers │ │ └── ActiveUserAdmin.js │ └── reducers │ │ ├── index.js │ │ ├── organizations.js │ │ └── users.js ├── alerts │ ├── actions │ │ ├── activeAlert.js │ │ ├── alerts.js │ │ └── visibilityFilter.js │ ├── components │ │ ├── AffectedEntity.js │ │ ├── AffectedServices.js │ │ ├── AgencySelector.js │ │ ├── AlertEditor.js │ │ ├── AlertPreview.js │ │ ├── AlertsList.js │ │ ├── AlertsViewer.js │ │ ├── CreateAlert.js │ │ ├── ModeSelector.js │ │ ├── RouteSelector.js │ │ └── StopSelector.js │ ├── containers │ │ ├── ActiveAlertEditor.js │ │ ├── MainAlertsViewer.js │ │ └── VisibleAlertsList.js │ ├── reducers │ │ ├── active.js │ │ ├── alerts.js │ │ └── index.js │ ├── selectors │ │ └── index.js │ └── util │ │ └── index.js ├── assets │ ├── application_icon.png │ └── application_logo.png ├── common │ ├── actions │ │ └── index.js │ ├── components │ │ ├── ClickOutside.js │ │ ├── ConfirmModal.js │ │ ├── EditableTextField.js │ │ ├── InfoModal.js │ │ ├── JobMonitor.js │ │ ├── LanguageSelect.js │ │ ├── Loading.js │ │ ├── Login.js │ │ ├── ManagerPage.js │ │ ├── MapModal.js │ │ ├── OptionButton.js │ │ ├── PageNotFound.js │ │ ├── SelectFileModal.js │ │ ├── Sidebar.js │ │ ├── SidebarNavItem.js │ │ ├── SidebarPopover.js │ │ ├── StatusMessage.js │ │ ├── StatusModal.js │ │ ├── TimezoneSelect.js │ │ ├── Title.js │ │ └── UserButtons.js │ ├── constants │ │ └── index.js │ ├── containers │ │ ├── ActiveSidebar.js │ │ ├── ActiveSidebarNavItem.js │ │ ├── App.js │ │ ├── CurrentStatusMessage.js │ │ ├── CurrentStatusModal.js │ │ ├── Login.js │ │ ├── PageContent.js │ │ ├── StarButton.js │ │ └── WatchButton.js │ ├── user │ │ ├── Auth0Manager.js │ │ ├── UserPermissions.js │ │ ├── UserSubscriptions.js │ │ └── __tests__ │ │ │ ├── Auth0Manager.js.hold │ │ │ └── __snapshots__ │ │ │ └── Auth0Manager.js.snap.hold │ └── util │ │ ├── __tests__ │ │ ├── config.js │ │ └── gtfs.js │ │ ├── analytics.js │ │ ├── config.js │ │ ├── date-time.js │ │ ├── file-download.js │ │ ├── geo.js │ │ ├── gtfs.js │ │ ├── json.js │ │ ├── map-keys.js │ │ ├── maps.js │ │ ├── modules.js │ │ ├── permissions.js │ │ ├── timezones.js │ │ ├── to-sentence-case.js │ │ ├── upload-file.js │ │ ├── user.js │ │ └── util.js ├── editor │ ├── actions │ │ ├── active.js │ │ ├── editor.js │ │ ├── map │ │ │ ├── index.js │ │ │ └── stopStrategies.js │ │ ├── snapshots.js │ │ ├── trip.js │ │ └── tripPattern.js │ ├── components │ │ ├── ColorField.js │ │ ├── CreateSnapshotModal.js │ │ ├── EditorFeedSourcePanel.js │ │ ├── EditorHelpModal.js │ │ ├── EditorInput.js │ │ ├── EditorSidebar.js │ │ ├── EntityDetails.js │ │ ├── EntityDetailsHeader.js │ │ ├── EntityList.js │ │ ├── EntityListButtons.js │ │ ├── EntityListSecondaryActions.js │ │ ├── ExceptionDate.js │ │ ├── FareRuleSelections.js │ │ ├── FareRulesForm.js │ │ ├── FeedInfoPanel.js │ │ ├── GtfsEditor.js │ │ ├── HourMinuteInput.js │ │ ├── MinuteSecondInput.js │ │ ├── ScheduleExceptionForm.js │ │ ├── VirtualizedEntitySelect.js │ │ ├── ZoneSelect.js │ │ ├── map │ │ │ ├── AddableStop.js │ │ │ ├── AddableStopsLayer.js │ │ │ ├── ControlPoint.js │ │ │ ├── ControlPointsLayer.js │ │ │ ├── DirectionIconsLayer.js │ │ │ ├── EditorMap.js │ │ │ ├── EditorMapLayersControl.js │ │ │ ├── PatternStopMarker.js │ │ │ ├── PatternStopsLayer.js │ │ │ ├── PatternsLayer.js │ │ │ ├── StopsLayer.js │ │ │ └── pattern-debug-lines.js │ │ ├── pattern │ │ │ ├── CalculateDefaultTimesForm.js │ │ │ ├── EditSchedulePanel.js │ │ │ ├── EditSettings.js │ │ │ ├── EditShapePanel.js │ │ │ ├── PatternStopButtons.js │ │ │ ├── PatternStopCard.js │ │ │ ├── PatternStopContainer.js │ │ │ ├── PatternStopsPanel.js │ │ │ ├── TripPatternList.js │ │ │ ├── TripPatternListControls.js │ │ │ └── TripPatternViewer.js │ │ └── timetable │ │ │ ├── CalendarSelect.js │ │ │ ├── EditableCell.js │ │ │ ├── HeaderCell.js │ │ │ ├── PatternSelect.js │ │ │ ├── RouteSelect.js │ │ │ ├── Timetable.js │ │ │ ├── TimetableEditor.js │ │ │ ├── TimetableGrid.js │ │ │ ├── TimetableHeader.js │ │ │ └── TimetableHelpModal.js │ ├── constants │ │ └── index.js │ ├── containers │ │ ├── ActiveEditorFeedSourcePanel.js │ │ ├── ActiveEntityList.js │ │ ├── ActiveFeedInfoPanel.js │ │ ├── ActiveGtfsEditor.js │ │ ├── ActiveTimetableEditor.js │ │ └── ActiveTripPatternList.js │ ├── reducers │ │ ├── data.js │ │ ├── index.js │ │ ├── mapState.js │ │ ├── settings.js │ │ └── timetable.js │ ├── selectors │ │ ├── index.js │ │ └── timetable.js │ └── util │ │ ├── __tests__ │ │ ├── fixtures │ │ │ ├── mapzen-response-delete-middle-control-point.json │ │ │ ├── mapzen-response-update-first-stop.json │ │ │ ├── mapzen-response-update-last-stop.json │ │ │ ├── mapzen-response-update-middle-control-point.json │ │ │ ├── test-control-points-with-extra-point-at-end.json │ │ │ ├── test-control-points.json │ │ │ └── test-pattern-shape.json │ │ ├── gtfs.js │ │ ├── map.js_hold │ │ └── validation.js │ │ ├── gtfs.js │ │ ├── index.js │ │ ├── map.js │ │ ├── objects.js │ │ ├── timetable.js │ │ ├── types.js │ │ ├── ui.js │ │ └── validation.js ├── gtfs │ ├── actions │ │ ├── filter.js │ │ ├── general.js │ │ ├── patterns.js │ │ ├── routes.js │ │ ├── shapes.js │ │ └── timetables.js │ ├── components │ │ ├── GtfsFilter.js │ │ ├── GtfsMap.js │ │ ├── PatternGeoJson.js │ │ ├── ShowAllRoutesOnMapFilter.js │ │ ├── StopMarker.js │ │ ├── TransferPerformance.js │ │ ├── gtfs-search.js │ │ └── gtfsmapsearch.js │ ├── containers │ │ ├── ActiveGtfsMap.js │ │ ├── GlobalGtfsFilter.js │ │ └── ShowAllRoutesOnMapFilter.js │ ├── reducers │ │ ├── filter.js │ │ ├── index.js │ │ ├── patterns.js │ │ ├── routes.js │ │ ├── shapes.js │ │ ├── stops.js │ │ ├── timetables.js │ │ └── validation.js │ ├── selectors │ │ └── index.js │ └── util │ │ ├── __tests__ │ │ └── stats.js │ │ ├── graphql.js │ │ ├── index.js │ │ └── stats.js ├── gtfsplus │ ├── actions │ │ └── gtfsplus.js │ ├── components │ │ ├── GtfsPlusEditor.js │ │ ├── GtfsPlusField.js │ │ ├── GtfsPlusFieldHeader.js │ │ ├── GtfsPlusTable.js │ │ └── GtfsPlusVersionSummary.js │ ├── containers │ │ ├── ActiveGtfsPlusEditor.js │ │ └── ActiveGtfsPlusVersionSummary.js │ ├── reducers │ │ ├── gtfsplus.js │ │ └── index.js │ ├── selectors │ │ └── index.js │ └── util │ │ └── index.js ├── index.css ├── main.js ├── manager │ ├── actions │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── user.js.snap.hold │ │ │ └── user.js.hold │ │ ├── deployments.js │ │ ├── feeds.js │ │ ├── languages.js │ │ ├── notes.js │ │ ├── projects.js │ │ ├── status.js │ │ ├── ui.js │ │ ├── user.js │ │ ├── versions.js │ │ └── visibilityFilter.js │ ├── components │ │ ├── CollapsiblePanel.js │ │ ├── CreateFeedSource.js │ │ ├── CreateProject.js │ │ ├── DeploymentConfirmModal.js │ │ ├── DeploymentPreviewButton.js │ │ ├── DeploymentSettings.js │ │ ├── DeploymentVersionsTable.js │ │ ├── DeploymentViewer.js │ │ ├── DeploymentsPanel.js │ │ ├── ExternalPropertiesTable.js │ │ ├── FeedSourceDropdown.js │ │ ├── FeedSourcePanel.js │ │ ├── FeedSourceSettings.js │ │ ├── FeedSourceTable.js │ │ ├── FeedSourceViewer.js │ │ ├── HomeProjectDropdown.js │ │ ├── ManagerHeader.js │ │ ├── NotesViewer.js │ │ ├── ProjectFeedListToolbar.js │ │ ├── ProjectSettings.js │ │ ├── ProjectSettingsForm.js │ │ ├── ProjectViewer.js │ │ ├── ProjectsList.js │ │ ├── RecentActivityBlock.js │ │ ├── ThirdPartySyncButton.js │ │ ├── UserAccountInfoPanel.js │ │ ├── UserHomePage.js │ │ ├── reporter │ │ │ ├── components │ │ │ │ ├── DateTimeFilter.js │ │ │ │ ├── PatternLayout.js │ │ │ │ ├── RouteLayout.js │ │ │ │ ├── StopLayout.js │ │ │ │ ├── TimetableLayout.js │ │ │ │ └── TripsPerHourChart.js │ │ │ └── containers │ │ │ │ ├── ActiveDateTimeFilter.js │ │ │ │ ├── Patterns.js │ │ │ │ ├── Routes.js │ │ │ │ ├── Stops.js │ │ │ │ └── Timetables.js │ │ ├── validation │ │ │ ├── GtfsValidationViewer.js │ │ │ ├── ServicePerModeChart.js │ │ │ ├── TripsChart.js │ │ │ └── ValidationErrorItem.js │ │ └── version │ │ │ ├── FeedVersionAccessibility.js │ │ │ ├── FeedVersionDetails.js │ │ │ ├── FeedVersionMap.js │ │ │ ├── FeedVersionNavigator.js │ │ │ ├── FeedVersionReport.js │ │ │ ├── FeedVersionTabs.js │ │ │ ├── FeedVersionViewer.js │ │ │ ├── VersionButtonToolbar.js │ │ │ └── VersionDateLabel.js │ ├── containers │ │ ├── ActiveDeploymentViewer.js │ │ ├── ActiveFeedSourceViewer.js │ │ ├── ActiveFeedVersionNavigator.js │ │ ├── ActiveProjectViewer.js │ │ ├── ActiveProjectsList.js │ │ ├── ActiveUserHomePage.js │ │ └── CreateProject.js │ ├── reducers │ │ ├── index.js │ │ ├── languages.js │ │ ├── projects.js │ │ ├── status.js │ │ ├── ui.js │ │ └── user.js │ ├── selectors │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── index.js.snap │ │ │ └── index.js │ │ └── index.js │ └── util │ │ ├── deployment.js │ │ ├── index.js │ │ └── version.js ├── mock-data.js ├── public │ ├── components │ │ ├── FeedsMap.js │ │ ├── PublicFeedSourceViewer.js │ │ ├── PublicFeedsViewer.js │ │ ├── PublicHeader.js │ │ ├── PublicPage.js │ │ ├── RegionSearch.js │ │ ├── SignupPage.js │ │ └── UserAccount.js │ └── containers │ │ ├── ActivePublicFeedSourceViewer.js │ │ ├── ActivePublicFeedsViewer.js │ │ ├── ActivePublicHeader.js │ │ ├── ActiveSignupPage.js │ │ └── ActiveUserAccount.js ├── scenario-editor │ ├── components │ │ └── StopLayer.js │ └── utils │ │ ├── reverse.js │ │ └── valhalla.js ├── signs │ ├── actions │ │ ├── activeSign.js │ │ ├── signs.js │ │ └── visibilityFilter.js │ ├── components │ │ ├── AffectedEntity.js │ │ ├── CreateSign.js │ │ ├── DisplaySelector.js │ │ ├── SignEditor.js │ │ ├── SignPreview.js │ │ ├── SignsList.js │ │ └── SignsViewer.js │ ├── containers │ │ ├── ActiveSignEditor.js │ │ ├── MainSignsViewer.js │ │ └── VisibleSignsList.js │ ├── reducers │ │ ├── active.js │ │ ├── index.js │ │ └── signs.js │ ├── selectors │ │ └── index.js │ ├── style.css │ └── util │ │ └── index.js ├── style.css └── types │ ├── actions.js │ ├── index.js │ └── reducers.js ├── mkdocs.yml ├── package.json ├── scripts ├── lint-messages.js ├── load.py ├── loadLegacy.py ├── package.json ├── seedData.js ├── updateAppMetadata.js └── yarn.lock └── yarn.lock /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/config-chain/.* 3 | .*/node_modules/fbjs/flow/.* 4 | .*/node_modules/immutable/.* 5 | .*/node_modules/mapbox.js/docs/examples/.* 6 | .*/node_modules/nock/node_modules/changelog/examples/.* 7 | .*/node_modules/npmconf/.* 8 | .*/node_modules/react-leaflet/src/.* 9 | .*/node_modules/reqwest/.* 10 | .*/node_modules/module-deps/test/invalid_pkg/package.json 11 | .*/node_modules/immutable/dist/immutable.js.flow 12 | 13 | [include] 14 | 15 | [libs] 16 | 17 | [options] 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | dist 4 | *.log 5 | coverage* 6 | tmp/ 7 | .tags 8 | 9 | # Optional npm cache directory 10 | .npm 11 | 12 | # Optional REPL history 13 | .node_repl_history 14 | 15 | # Configurations 16 | configurations/* 17 | !configurations/default 18 | !configurations/test 19 | !configurations/end-to-end 20 | dist 21 | assets 22 | 23 | # Secret config files 24 | env.yml 25 | env.yml-original 26 | .env 27 | !configurations/test/env.yml 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | notifications: 3 | email: false 4 | slack: conveyal:WQxmWiu8PdmujwLw4ziW72Gc 5 | node_js: 6 | - '8' 7 | cache: 8 | yarn: true 9 | before_install: 10 | - npm i -g codecov 11 | # Use updated python to avoid SSL insecure warnings: 12 | # https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings 13 | - python --version 14 | - pyenv versions 15 | - pyenv global 2.7.14 16 | - pip install --user mkdocs 17 | script: 18 | - yarn run lint 19 | - yarn run lint-messages 20 | - yarn run flow 21 | - yarn run cover-client 22 | - codecov 23 | - yarn run build -- --minify 24 | - mkdocs build 25 | 26 | # If sudo is disabled, CI runs on container based infrastructure (allows caching &c.) 27 | sudo: false 28 | 29 | # Push results to codecov.io 30 | after_success: 31 | - bash <(curl -s https://codecov.io/bash) 32 | - yarn run semantic-release 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Conveyal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # datatools-ui 2 | 3 | The core application for Conveyal's transit data tools suite. 4 | 5 | ## Configuration 6 | 7 | This repository serves as the front end UI for the Data Manager application. It must be run in conjunction with [datatools-server](https://github.com/conveyal/datatools-server) 8 | 9 | ## Documentation 10 | 11 | View the [latest release documentation](http://conveyal-data-tools.readthedocs.org/en/latest/) at ReadTheDocs for more info on deployment and development as well as a user guide. 12 | 13 | Note: `dev` branch docs can be found [here](http://conveyal-data-tools.readthedocs.org/en/dev/). 14 | -------------------------------------------------------------------------------- /__mocks__/auth0-js.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | 3 | module.exports = { 4 | WebAuth: class WebAuth { 5 | constructor ({domain, clientID}) { 6 | if (!domain) { 7 | throw new Error('Domain required') 8 | } 9 | if (!clientID) { 10 | throw new Error('Client ID required') 11 | } 12 | } 13 | 14 | renewAuth ( 15 | { 16 | audience, 17 | nonce, 18 | postMessageDataType, 19 | redirectUri, 20 | scope, 21 | usePostMessage 22 | }, 23 | callback 24 | ) { 25 | return callback(null, { 26 | accessToken: jwt.sign( 27 | { 28 | nonce 29 | }, 30 | 'signingKey' 31 | ), 32 | idToken: jwt.sign( 33 | { 34 | nonce 35 | }, 36 | 'signingKey' 37 | ) 38 | }) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /__mocks__/auth0-lock.js: -------------------------------------------------------------------------------- 1 | // TODO: remove. There was an import issue, so this is a temporary hack. 2 | // perhaps a later version of Auth0 will have negate the need for this file. 3 | module.exports = function () {} 4 | -------------------------------------------------------------------------------- /__tests__/test-utils/setup.js: -------------------------------------------------------------------------------- 1 | window.localStorage = { 2 | getItem: () => null 3 | } 4 | -------------------------------------------------------------------------------- /configurations/default/env.yml.tmp: -------------------------------------------------------------------------------- 1 | AUTH0_CLIENT_ID: your-auth0-client-id 2 | AUTH0_DOMAIN: your-auth0-domain 3 | MAPZEN_TURN_BY_TURN_KEY: test-turn-key 4 | MAPBOX_ACCESS_TOKEN: test-access-token 5 | MAPBOX_MAP_ID: mapbox.streets 6 | MAPBOX_ATTRIBUTION: © Mapbox © OpenStreetMap Improve this map 7 | # R5_URL: http://localhost:8080 8 | GRAPH_HOPPER_KEY: graph-hopper-routing-key 9 | -------------------------------------------------------------------------------- /configurations/default/settings.yml: -------------------------------------------------------------------------------- 1 | application: 2 | active_project: project-id 3 | changelog_url: 'https://changelog.example.com' 4 | data: 5 | gtfs_s3_bucket: bucket-name 6 | use_s3_storage: false 7 | date_format: MMM Do YYYY 8 | docs_url: 'http://docs.example.com' 9 | logo: 'http://gtfs-assets-dev.conveyal.com/data_manager.png' 10 | notifications_enabled: false 11 | profileRefreshTime: -1 12 | support_email: support@example.com 13 | title: Data Manager 14 | entries: 15 | - 'lib/main.js:dist/index.js' 16 | - 'lib/index.css:dist/index.css' 17 | extensions: 18 | transitfeeds: 19 | enabled: false 20 | transitland: 21 | enabled: false 22 | modules: 23 | editor: 24 | enabled: true 25 | enterprise: 26 | enabled: true 27 | user_admin: 28 | enabled: true 29 | validator: 30 | enabled: true 31 | -------------------------------------------------------------------------------- /configurations/end-to-end/env.yml.tmp: -------------------------------------------------------------------------------- 1 | username: user@email.com 2 | password: password 3 | -------------------------------------------------------------------------------- /configurations/end-to-end/test-gtfs-to-fetch.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/configurations/end-to-end/test-gtfs-to-fetch.zip -------------------------------------------------------------------------------- /configurations/end-to-end/test-gtfs-to-upload.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/configurations/end-to-end/test-gtfs-to-upload.zip -------------------------------------------------------------------------------- /configurations/test/env.yml: -------------------------------------------------------------------------------- 1 | AUTH0_CLIENT_ID: test 2 | AUTH0_DOMAIN: test.domain.com 3 | MAPZEN_TURN_BY_TURN_KEY: test 4 | MAPBOX_ACCESS_TOKEN: test 5 | MAPBOX_MAP_ID: test 6 | MAPBOX_ATTRIBUTION: © Mapbox © OpenStreetMap Improve this map 7 | -------------------------------------------------------------------------------- /configurations/test/settings.yml: -------------------------------------------------------------------------------- 1 | application: 2 | active_project: project-id 3 | changelog_url: 'https://changelog.example.com' 4 | data: 5 | gtfs_s3_bucket: bucket-name 6 | use_s3_storage: false 7 | date_format: MMM Do YYYY 8 | docs_url: 'http://docs.example.com' 9 | logo: 'http://example.com/data_manager.png' 10 | notifications_enabled: false 11 | profileRefreshTime: -1 12 | support_email: support@example.com 13 | title: Data Manager 14 | entries: 15 | - 'lib/main.js:dist/index.js' 16 | - 'lib/index.css:dist/index.css' 17 | extensions: 18 | transitfeeds: 19 | enabled: false 20 | transitland: 21 | enabled: false 22 | modules: 23 | editor: 24 | enabled: true 25 | enterprise: 26 | enabled: true 27 | user_admin: 28 | enabled: true 29 | validator: 30 | enabled: true 31 | -------------------------------------------------------------------------------- /docs/dev/migration.md: -------------------------------------------------------------------------------- 1 | # Migration 2 | 3 | ## Migrating manager application data 4 | datatools-server offers a way to migrate application data (e.g., due to either breaking application schema changes or server changes). **Note:** this process requires temporarily exposing a `GET` request that exposes the entirety of the manager database. 5 | 6 | 1. Set the config setting `modules:dump:enabled` to `true`. 7 | 2. Restart the application. 8 | 3. Download copy of application data to local json file `curl localhost:4000/dump > db_backup.json`. 9 | 4. Change dump config setting back to `false`. 10 | 5. (optional) If looking to reload into the existing server, delete the manager mapdb (`.db` and `.dbp`) files in `application:data:mapdb` 11 | 6. Follow instructions in [`/scripts/load.py`](https://github.com/conveyal/datatools-ui/blob/master/scripts/load.py) to upload the json data to the new server. 12 | -------------------------------------------------------------------------------- /docs/img/add-fare-zone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/add-fare-zone.png -------------------------------------------------------------------------------- /docs/img/auth0-token-generator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/auth0-token-generator.png -------------------------------------------------------------------------------- /docs/img/create-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/create-project.png -------------------------------------------------------------------------------- /docs/img/create-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/create-user.png -------------------------------------------------------------------------------- /docs/img/edit-calendars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/edit-calendars.png -------------------------------------------------------------------------------- /docs/img/edit-fare-rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/edit-fare-rules.png -------------------------------------------------------------------------------- /docs/img/edit-fares.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/edit-fares.png -------------------------------------------------------------------------------- /docs/img/edit-frequencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/edit-frequencies.png -------------------------------------------------------------------------------- /docs/img/edit-patterns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/edit-patterns.png -------------------------------------------------------------------------------- /docs/img/edit-routes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/edit-routes.png -------------------------------------------------------------------------------- /docs/img/edit-stops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/edit-stops.png -------------------------------------------------------------------------------- /docs/img/edit-timetables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/edit-timetables.png -------------------------------------------------------------------------------- /docs/img/feed-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/feed-profile.png -------------------------------------------------------------------------------- /docs/img/feed-version-navigator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/feed-version-navigator.png -------------------------------------------------------------------------------- /docs/img/password-reset-logged-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/password-reset-logged-in.png -------------------------------------------------------------------------------- /docs/img/password-reset-logged-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/password-reset-logged-out.png -------------------------------------------------------------------------------- /docs/img/pattern-add-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/pattern-add-stop.png -------------------------------------------------------------------------------- /docs/img/pattern-insert-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/pattern-insert-stop.png -------------------------------------------------------------------------------- /docs/img/pattern-shape-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/pattern-shape-panel.png -------------------------------------------------------------------------------- /docs/img/pattern-stop-order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/pattern-stop-order.png -------------------------------------------------------------------------------- /docs/img/pattern-stop-toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/pattern-stop-toolbar.png -------------------------------------------------------------------------------- /docs/img/project-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/project-profile.png -------------------------------------------------------------------------------- /docs/img/public-portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/public-portal.png -------------------------------------------------------------------------------- /docs/img/quick-access-toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/quick-access-toolbar.png -------------------------------------------------------------------------------- /docs/img/schedule-exception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/schedule-exception.png -------------------------------------------------------------------------------- /docs/img/schedule-toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/schedule-toolbar.png -------------------------------------------------------------------------------- /docs/img/select-trips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/select-trips.png -------------------------------------------------------------------------------- /docs/img/timetable-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/timetable-selector.png -------------------------------------------------------------------------------- /docs/img/user-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/user-admin.png -------------------------------------------------------------------------------- /docs/img/user-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/user-profile.png -------------------------------------------------------------------------------- /docs/img/view-all-stops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/docs/img/view-all-stops.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Conveyal Transit Data Tools 2 | 3 | The Conveyal Transit Data Tools suite provides web-based tools for creating, managing, evaluating, and publishing transit data, specifically data stored in the General Transit Feed Specification (GTFS) format. 4 | 5 | ![screenshot](img/public-portal.png) 6 | 7 | The following documentation is available (see full table of contents at left): 8 | 9 | - **User Documentation** covering basic use and operation of the tools 10 | - **Developer Documentation** covering installation/deployment of the software and project development 11 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | img[alt=screenshot] { width: 100%; } 2 | 3 | /*Ignore first heading in page in TOC list*/ 4 | li.toctree-l3:first-child { 5 | display: none; 6 | } 7 | 8 | .img-center { 9 | width: 300px; 10 | margin-left: auto; 11 | margin-right: auto; 12 | } 13 | -------------------------------------------------------------------------------- /docs/user/editor/fares.md: -------------------------------------------------------------------------------- 1 | # Fares 2 | 3 | ## Editing fares 4 | 5 | To begin editing fares, click the fare ticket button on the lefthand navigation bar. 6 | 7 | ![screenshot](../../img/edit-fares.png) 8 | 9 | Choose a fare from the list to begin editing. To create a new fare, click `+ New fare`. **Note:** as with all newly created items (except patterns), the new fare will not be saved until the save icon (💾) is clicked. 10 | 11 | ## Fare attributes 12 | 13 | Fare attributes describe the basic information about a fare. Full details on fare attributes can be found at the [GTFS specification reference](https://developers.google.com/transit/gtfs/reference/fare_attributes-file). 14 | 15 | ## Fare rules 16 | 17 | To edit fare rules, you must first create and save a fare with attributes. After choosing a fare, click the `Fare rules` tab and define one or more rules for this fare using the following types: 18 | 19 | 1. **Route** - applies to any itinerary that includes the route 20 | 2. **From/to zone** - applies to any itinerary that travels from the origin zone to the destination zone 21 | 3. **Contains zone** - applies to any itinerary that passes through *each* `contains` zone 22 | 23 | **Note:** fare rules can be tricky, see the [GTFS specification reference](https://developers.google.com/transit/gtfs/reference/fare_rules-file) for more information on how fare rules apply. 24 |
25 | ![screenshot](../../img/edit-fare-rules.png) 26 |
27 | 28 | ## Creating fare zones 29 | 30 | To create a fare zone for use in fare rules, you must first select a stop that you would like to include in the zone. Click in the `zone_id` dropdown and begin typing the new `zone_id`. Click `Create new zone: [zone_id]` and then save the stop. Repeat for as many zones as needed. 31 |
32 | ![screenshot](../../img/add-fare-zone.png) 33 |
34 | 35 | Once created and assigned to one or more stop, fare zones can be used when defining fare rules for **From/to zone** or **Contains zone**. 36 | -------------------------------------------------------------------------------- /docs/user/editor/stops.md: -------------------------------------------------------------------------------- 1 | # Stops 2 | 3 | ## Editing stops 4 | 5 | To begin editing stops, click the map marker icon button on the lefthand navigation bar. 6 | 7 | ![screenshot](../../img/edit-stops.png) 8 | 9 | ## Selecting a stop 10 | 11 | Choose a stop from the list or search by stop name in the dropdown. 12 | 13 | You can also **zoom into the map** while the stop list is visible and once you're close enough you'll begin to see stops displayed. Click one to begin editing its details. 14 | 15 | ## Creating a stop: right-click on map 16 | 17 | To create a new stop, **right-click on the map** in the location you would like to place the stop. **Note:** as with all newly created items (except patterns), the new stop will not be saved until the save icon (💾) is clicked. 18 | 19 | ## Moving a stop 20 | 21 | To move a selected stop simply **click and drag the stop to the new location**. Or, if already you know the latitude and longitude coordinates, you can copy these into the text fields. After moving the stop, click save to keep the changes. 22 | 23 | 27 | 28 | ## View all stops for feed 29 | 30 | To view all stops for a feed, hover over the map layers icon (in the top, lefthand corner of the map) and turn on the `Stop locations` layer. When you do, you'll see all of the stops (which appear as grey circles) for the feed even at wide zoom levels. This layer can be viewed whether or not the stop list is visible, so it can be helpful for users who would like to view stop locations alongside routes or trip patterns. 31 | 32 | ![screenshot](../../img/view-all-stops.png) 33 | 34 | Clicking on a stop shown in this layer will select the stop for editing, but be careful—it can be tricky to select the right stop from very far away! 35 | 36 | 37 | 40 | -------------------------------------------------------------------------------- /docs/user/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Conceptual Overview 4 | 5 | The GTFS Data Manager enables exchange and coordination of data creation, updates, validation and deployment of GTFS data feeds for transit schedules. 6 | 7 | The platform allows GTFS producers (transit operators, local governments, etc.) to share existing feeds or utilize the build function in GTFS Editor to create and maintain feeds. GTFS creators can use the built in validator to check for potential issues. 8 | 9 | ## Data Manager Concepts 10 | 11 | ### Projects 12 | 13 | Projects are collections of feed sources and deployments. 14 | 15 | ### Feed Sources 16 | 17 | Feed sources define the locations or upstream sources of GTFS feeds. These can be any combination of: 18 | 19 | 1. **Manually Uploaded** - Manually collected/managed feeds provided directly by an external source. 20 | 2. **Fetched Automatically** - Public available feeds that can be fetch from a URL 21 | 3. **Produced In House** - Internally managed/created feeds produced by GTFS Editor 22 | 23 | ### Feed Versions 24 | 25 | Feed Versions store specific instances of a GTFS feed for a given feed source as published over time. Each Feed Version has an associated GTFS file that is stored within the Data Manager, can be downloaded by users, and for which detailed information such as validation results is available. 26 | 27 | ### Snapshots 28 | 29 | Internally managed GTFS data sets are pulled from the GTFS Editor using “snapshots” created in the editor interface. These snapshots are static versions of the data set, or save points, that can be exported, or used as starting point for future edits. The Data manager only imports snapshotted versions of feeds. This allows users to ensure the correct version of data is being imported and to retrieve and review data in the future. 30 | 31 | *Note: See section 2.6 in GTFS Editor User Manual for more information on snapshots.* 32 | -------------------------------------------------------------------------------- /flow-typed/npm/@conveyal/lonlat_v1.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 801d87d0bccd96d1e1a9c0fd6a351c79 2 | // flow-typed version: <>/@conveyal/lonlat_v^1.3.0/flow_v0.37.0 3 | 4 | declare module '@conveyal/lonlat' { 5 | declare type coordinatesInput = [number, number] 6 | declare type objectInput = { 7 | lat?: number, 8 | latitude?: number, 9 | lon?: number, 10 | lng?: number, 11 | longitude?: number 12 | } 13 | declare type pointInput = {x: number, y: number} 14 | declare type standardizedLonLat = { 15 | lat: number, 16 | lon: number 17 | } 18 | 19 | declare export default function normalize(mixed): standardizedLonLat 20 | 21 | declare export function isEqual(mixed, mixed, ?number): boolean 22 | declare export function print(mixed): string 23 | declare export function toCoordinates(mixed): [number, number] 24 | declare export function toLeaflet(mixed): {lat: number, lng: number} 25 | } 26 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-polyfill_v6.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 28eccd914ac7bd65de204cb1d8d37cfe 2 | // flow-typed version: 7b122e75af/babel-polyfill_v6.x.x/flow_>=v0.30.x 3 | 4 | declare module 'babel-polyfill' {} 5 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6a5610678d4b01e13bbfbbc62bdaf583 2 | // flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /flow-typed/npm/isomorphic-fetch_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 47370d221401bec823c43c3598266e26 2 | // flow-typed version: ec28077c25/isomorphic-fetch_v2.x.x/flow_>=v0.25.x 3 | 4 | 5 | declare module 'isomorphic-fetch' { 6 | declare module.exports: (input: string | Request | URL, init?: RequestOptions) => Promise; 7 | } 8 | -------------------------------------------------------------------------------- /flow-typed/npm/lodash.throttle_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: b1bcad4f459d3e23e1460c8b17a4feea 2 | // flow-typed version: <>/lodash.throttle_v^4.1.1/flow_v0.37.0 3 | 4 | declare module 'lodash.throttle' { 5 | declare export default function throttle((...T) => void, number): (...T) => void 6 | } 7 | -------------------------------------------------------------------------------- /flow-typed/npm/lodash.tolower_v4.x.x.js: -------------------------------------------------------------------------------- 1 | declare module 'lodash.tolower' { 2 | declare export default (string) => string 3 | } 4 | -------------------------------------------------------------------------------- /flow-typed/npm/lodash.upperfirst_v4.x.x.js: -------------------------------------------------------------------------------- 1 | declare module 'lodash.upperfirst' { 2 | declare export default (string) => string 3 | } 4 | -------------------------------------------------------------------------------- /flow-typed/npm/object-path_v0.11.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 8e08c7b448e19eef73737b79fa31c8ae 2 | // flow-typed version: <>/object-path_v0.11.4/flow_v0.53.1 3 | 4 | declare module 'object-path' { 5 | declare module.exports: { 6 | ensureExists(obj: ?Object | Array, string | Array, ?any): ?any, 7 | get(obj: ?Object | Array, string | Array): ?any, 8 | set(obj: ?Object | Array, string, any): ?any 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /flow-typed/npm/polyline_v0.2.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: e8af478fdd21bcfff8ff15754086c5b2 2 | // flow-typed version: <>/polyline_v^0.2.0/flow_v0.53.1 3 | 4 | type Coordinate = [number, number] 5 | 6 | declare module 'polyline' { 7 | declare module.exports: { 8 | decode(string): Coordinate[], 9 | encode(Coordinate[]): string 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /flow-typed/npm/prop-types_v15.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: d9a983bb1ac458a256c31c139047bdbb 2 | // flow-typed version: 927687984d/prop-types_v15.x.x/flow_>=v0.41.x 3 | 4 | type $npm$propTypes$ReactPropsCheckType = ( 5 | props: any, 6 | propName: string, 7 | componentName: string, 8 | href?: string) => ?Error; 9 | 10 | declare module 'prop-types' { 11 | declare var array: React$PropType$Primitive>; 12 | declare var bool: React$PropType$Primitive; 13 | declare var func: React$PropType$Primitive; 14 | declare var number: React$PropType$Primitive; 15 | declare var object: React$PropType$Primitive; 16 | declare var string: React$PropType$Primitive; 17 | declare var symbol: React$PropType$Primitive; 18 | declare var any: React$PropType$Primitive; 19 | declare var arrayOf: React$PropType$ArrayOf; 20 | declare var element: React$PropType$Primitive; /* TODO */ 21 | declare var instanceOf: React$PropType$InstanceOf; 22 | declare var node: React$PropType$Primitive; /* TODO */ 23 | declare var objectOf: React$PropType$ObjectOf; 24 | declare var oneOf: React$PropType$OneOf; 25 | declare var oneOfType: React$PropType$OneOfType; 26 | declare var shape: React$PropType$Shape; 27 | 28 | declare function checkPropTypes( 29 | propTypes: $Subtype<{[_: $Keys]: $npm$propTypes$ReactPropsCheckType}>, 30 | values: V, 31 | location: string, 32 | componentName: string, 33 | getStack: ?(() => ?string) 34 | ) : void; 35 | } 36 | -------------------------------------------------------------------------------- /flow-typed/npm/qs_v6.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: c83e1b4bfe8580e9003b7e0a111276b9 2 | // flow-typed version: <>/qs_v^6.2.1/flow_v0.53.1 3 | 4 | declare module 'qs' { 5 | declare module.exports: { 6 | stringify(Object): string 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /flow-typed/npm/react-dnd-html5-backend_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: b517cd8873674b6ac09e799503836168 2 | // flow-typed version: 30815cf324/react-dnd-html5-backend_v2.x.x/flow_>=v0.25.x 3 | 4 | declare type $npm$reactDnd$NativeTypes$FILE = "__NATIVE_FILE__"; 5 | declare type $npm$reactDnd$NativeTypes$URL = "__NATIVE_URL__"; 6 | declare type $npm$reactDnd$NativeTypes$TEXT = "__NATIVE_TEXT__"; 7 | declare type $npm$reactDnd$NativeTypes = 8 | | $npm$reactDnd$NativeTypes$FILE 9 | | $npm$reactDnd$NativeTypes$URL 10 | | $npm$reactDnd$NativeTypes$TEXT; 11 | 12 | declare module "react-dnd-html5-backend" { 13 | declare module.exports: { 14 | getEmptyImage(): Image, 15 | NativeTypes: { 16 | FILE: $npm$reactDnd$NativeTypes$FILE, 17 | URL: $npm$reactDnd$NativeTypes$URL, 18 | TEXT: $npm$reactDnd$NativeTypes$TEXT 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /flow-typed/npm/turf-point_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3457aa4eecf0797ededf7acbdec325e0 2 | // flow-typed version: da30fe6876/turf-point_v2.x.x/flow_>=v0.25.x 3 | 4 | // @flow 5 | 6 | type $npm$Turf$Point$Point = { 7 | type: "Point", 8 | coordinates: [number, number], 9 | bbox?: Array, 10 | crs?: { type: string, properties: mixed } 11 | }; 12 | 13 | type $npm$Turf$Destination$FeaturePoint = { 14 | type: "Feature", 15 | geometry: $npm$Turf$Point$Point, 16 | properties: ?{ [key: string]: ?Properties }, 17 | bbox?: Array, 18 | crs?: { type: string, properties: mixed } 19 | }; 20 | 21 | declare module "turf-point" { 22 | declare module.exports: ( 23 | coordinates: [number, number], 24 | properties?: Properties 25 | ) => $npm$Turf$Destination$FeaturePoint; 26 | } 27 | -------------------------------------------------------------------------------- /flow-typed/npm/turf-polygon_v1.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: f745a75864c8e3f18e215f5102636b3d 2 | // flow-typed version: da30fe6876/turf-polygon_v1.x.x/flow_>=v0.25.x 3 | 4 | // @flow 5 | 6 | type $npm$turfPolygon$LineRing = Array<[number, number]>; 7 | 8 | type $npm$turfPolygon$Polygon = { 9 | type: "Polygon", 10 | coordinates: Array<$npm$turfPolygon$LineRing>, 11 | bbox?: Array, 12 | crs?: { type: string, properties: mixed } 13 | }; 14 | 15 | type $npm$turfPolygon$FeaturePolygon = { 16 | type: "Feature", 17 | geometry: $npm$turfPolygon$Polygon, 18 | properties: ?{ [key: string]: ?Properties }, 19 | bbox?: Array, 20 | crs?: { type: string, properties: mixed } 21 | }; 22 | 23 | declare module "turf-polygon" { 24 | declare module.exports: ( 25 | Array>, 26 | properties?: Properties 27 | ) => $npm$turfPolygon$FeaturePolygon; 28 | } 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Data Tools 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/admin/components/permissions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default [ 4 | /* { 5 | type: 'administer-project', 6 | name: 'Administer Project', 7 | feedSpecific: false 8 | }, */ 9 | { 10 | type: 'manage-feed', 11 | name: 'Manage Feed Configuration', 12 | feedSpecific: true 13 | }, 14 | { 15 | type: 'edit-gtfs', 16 | name: 'Edit GTFS Feeds', 17 | feedSpecific: true 18 | }, 19 | { 20 | type: 'approve-gtfs', 21 | name: 'Approve GTFS Feeds', 22 | feedSpecific: true 23 | }, 24 | { 25 | type: 'edit-alert', 26 | name: 'Edit GTFS-RT Alerts', 27 | feedSpecific: true 28 | }, 29 | { 30 | type: 'approve-alert', 31 | name: 'Approve GTFS-RT Alerts', 32 | feedSpecific: true 33 | }, 34 | { 35 | type: 'edit-etid', 36 | name: 'Edit eTID Configurations', 37 | feedSpecific: true 38 | }, 39 | { 40 | type: 'approve-etid', 41 | name: 'Approve eTID Configurations', 42 | feedSpecific: true 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /lib/admin/containers/ActiveUserAdmin.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import UserAdmin from '../components/UserAdmin' 6 | import { 7 | createUser, 8 | deleteUser, 9 | fetchUsers, 10 | setUserPage, 11 | setUserQueryString 12 | } from '../actions/admin' 13 | import { 14 | createOrganization, 15 | deleteOrganization, 16 | fetchOrganizations, 17 | updateOrganization 18 | } from '../actions/organizations' 19 | import {updateUserData} from '../../manager/actions/user' 20 | import {fetchProjects} from '../../manager/actions/projects' 21 | import {fetchProjectFeeds} from '../../manager/actions/feeds' 22 | 23 | import type {AppState, RouterProps} from '../../types/reducers' 24 | 25 | export type Props = RouterProps 26 | 27 | const mapStateToProps = (state: AppState, ownProps: Props) => { 28 | return { 29 | projects: state.projects.all, 30 | user: state.user, 31 | users: state.admin.users, 32 | organizations: state.admin.organizations, 33 | activeComponent: ownProps.routeParams.subpage 34 | } 35 | } 36 | 37 | const mapDispatchToProps = { 38 | createOrganization, 39 | createUser, 40 | deleteOrganization, 41 | deleteUser, 42 | fetchOrganizations, 43 | fetchProjectFeeds, 44 | fetchProjects, 45 | fetchUsers, 46 | setUserPage, 47 | setUserQueryString, 48 | updateOrganization, 49 | updateUserData 50 | } 51 | 52 | const ActiveUserAdmin = connect( 53 | mapStateToProps, 54 | mapDispatchToProps 55 | )(UserAdmin) 56 | 57 | export default ActiveUserAdmin 58 | -------------------------------------------------------------------------------- /lib/admin/reducers/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { combineReducers } from 'redux' 4 | 5 | import users from './users' 6 | import organizations from './organizations' 7 | 8 | export default combineReducers({ 9 | users, 10 | organizations 11 | }) 12 | -------------------------------------------------------------------------------- /lib/admin/reducers/organizations.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import update from 'react-addons-update' 4 | 5 | import type {Action} from '../../types/actions' 6 | import type {OrganizationsState} from '../../types/reducers' 7 | 8 | export const defaultState = { 9 | isFetching: false, 10 | data: null, 11 | userQueryString: null 12 | } 13 | 14 | const organizations = ( 15 | state: OrganizationsState = defaultState, 16 | action: Action 17 | ): OrganizationsState => { 18 | switch (action.type) { 19 | case 'REQUESTING_ORGANIZATIONS': 20 | return update(state, {isFetching: { $set: true }}) 21 | case 'RECEIVE_ORGANIZATIONS': 22 | return update(state, { 23 | isFetching: { $set: false }, 24 | data: { $set: action.payload } 25 | }) 26 | case 'CREATED_ORGANIZATION': 27 | if (state.data) { 28 | return update(state, {data: { $push: [action.payload] }}) 29 | } 30 | return state 31 | default: 32 | return state 33 | } 34 | } 35 | 36 | export default organizations 37 | -------------------------------------------------------------------------------- /lib/admin/reducers/users.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import update from 'react-addons-update' 4 | 5 | import type {Action} from '../../types/actions' 6 | import type {AdminUsersState} from '../../types/reducers' 7 | 8 | export const defaultState = { 9 | isFetching: false, 10 | data: null, 11 | userCount: 0, 12 | page: 0, 13 | perPage: 10, 14 | userQueryString: null 15 | } 16 | 17 | const users = (state: AdminUsersState = defaultState, action: Action): AdminUsersState => { 18 | switch (action.type) { 19 | case 'REQUESTING_USERS': 20 | return update(state, {isFetching: { $set: true }}) 21 | case 'RECEIVE_USERS': 22 | const {totalUserCount, users} = action.payload 23 | return update(state, { 24 | isFetching: { $set: false }, 25 | data: { $set: users }, 26 | userCount: { $set: totalUserCount } 27 | }) 28 | case 'CREATED_USER': 29 | if (state.data) { 30 | return update(state, {data: { $push: [action.payload] }}) 31 | } 32 | return state 33 | case 'SET_USER_PAGE': 34 | return update(state, {page: { $set: action.payload }}) 35 | case 'SET_USER_QUERY_STRING': 36 | return update(state, {userQueryString: { $set: action.payload }}) 37 | default: 38 | return state 39 | } 40 | } 41 | 42 | export default users 43 | -------------------------------------------------------------------------------- /lib/alerts/actions/activeAlert.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {createAction, type ActionType} from 'redux-actions' 4 | 5 | import type {AlertEntity} from '../../types' 6 | import type {dispatchFn, getStateFn} from '../../types/reducers' 7 | 8 | export const deleteActiveEntity = createAction( 9 | 'DELETE_ACTIVE_ALERT_AFFECTED_ENTITY', 10 | (payload: any) => payload 11 | ) 12 | const newEntity = createAction( 13 | 'ADD_ACTIVE_ALERT_AFFECTED_ENTITY', 14 | (payload: { 15 | agency: any, 16 | id: number, 17 | type: string, 18 | [string]: any 19 | }) => payload 20 | ) 21 | export const setActiveProperty = createAction( 22 | 'SET_ACTIVE_ALERT_PROPERTY', 23 | (payload: { [string]: any }) => payload 24 | ) 25 | export const setActivePublished = createAction( 26 | 'SET_ACTIVE_ALERT_PUBLISHED', 27 | (payload: boolean) => payload 28 | ) 29 | export const updateActiveEntity = createAction( 30 | 'UPDATE_ACTIVE_ALERT_ENTITY', 31 | (payload: AlertEntity) => payload 32 | ) 33 | 34 | export type ActiveAlertActions = ActionType | 35 | ActionType | 36 | ActionType | 37 | ActionType | 38 | ActionType 39 | 40 | // Initialize next entity ID at 0 (increments as new entities are added). 41 | let nextEntityId = 0 42 | 43 | export function addActiveEntity ( 44 | field: string = 'AGENCY', 45 | value: any = null, 46 | agency: any = null, 47 | newEntityId: ?number = 0 48 | ) { 49 | return function (dispatch: dispatchFn, getState: getStateFn) { 50 | nextEntityId++ 51 | return dispatch(newEntity({ 52 | id: newEntityId || nextEntityId, 53 | type: field, 54 | agency, 55 | [field.toLowerCase()]: value 56 | })) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/alerts/actions/visibilityFilter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {createAction, type ActionType} from 'redux-actions' 4 | 5 | import {createVoidPayloadAction} from '../../common/actions' 6 | 7 | export const setAlertAgencyFilter = createAction( 8 | 'SET_ALERT_AGENCY_FILTER', 9 | (payload: string) => payload 10 | ) 11 | export const setAlertSort = createAction( 12 | 'SET_ALERT_SORT', 13 | (payload: { 14 | direction: string, 15 | type: string 16 | }) => payload 17 | ) 18 | export const setVisibilityFilter = createVoidPayloadAction('SET_ALERT_VISIBILITY_FILTER') 19 | export const setVisibilitySearchText = createAction( 20 | 'SET_ALERT_VISIBILITY_SEARCH_TEXT', 21 | (payload: string) => payload 22 | ) 23 | 24 | export type AlertVisibilityFilterActions = ActionType | 25 | ActionType | 26 | ActionType | 27 | ActionType 28 | -------------------------------------------------------------------------------- /lib/alerts/components/AgencySelector.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import {FormControl} from 'react-bootstrap' 5 | 6 | import * as activeAlertActions from '../actions/activeAlert' 7 | import {getFeed, getFeedId} from '../../common/util/modules' 8 | 9 | import type {AlertEntity, Feed} from '../../types' 10 | 11 | type Props = { 12 | entity: AlertEntity, 13 | feeds: Array, 14 | updateActiveEntity: typeof activeAlertActions.updateActiveEntity 15 | } 16 | 17 | export default class AgencySelector extends Component { 18 | _onSelect = (evt: SyntheticInputEvent) => { 19 | const {entity, feeds, updateActiveEntity} = this.props 20 | const feed = getFeed(feeds, evt.target.value) 21 | updateActiveEntity(entity, 'AGENCY', feed) 22 | } 23 | 24 | render () { 25 | const {feeds, entity} = this.props 26 | return ( 27 |
28 | 32 | {feeds.map((feed) => ( 33 | 38 | ))} 39 | 40 |
41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/alerts/components/CreateAlert.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Icon from '@conveyal/woonerf/components/icon' 4 | import React, {Component} from 'react' 5 | import { Button } from 'react-bootstrap' 6 | 7 | import * as alertActions from '../actions/alerts' 8 | 9 | type Props = { 10 | createAlert: typeof alertActions.createAlert, 11 | disabled: boolean, 12 | fetched: boolean 13 | } 14 | 15 | export default class CreateAlert extends Component { 16 | render () { 17 | const { 18 | createAlert, 19 | disabled, 20 | fetched 21 | } = this.props 22 | const createDisabled = disabled != null 23 | ? disabled || !fetched 24 | : false 25 | return ( 26 | 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/alerts/components/ModeSelector.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import { FormControl } from 'react-bootstrap' 5 | 6 | import * as activeAlertActions from '../actions/activeAlert' 7 | import {modes} from '../util' 8 | 9 | import type {AlertEntity} from '../../types' 10 | 11 | type Props = { 12 | entity: AlertEntity, 13 | updateActiveEntity: typeof activeAlertActions.updateActiveEntity 14 | } 15 | 16 | export default class ModeSelector extends Component { 17 | _onChange = (evt: SyntheticInputEvent) => 18 | this.props.updateActiveEntity(this.props.entity, 'MODE', this.getMode(evt.target.value)) 19 | 20 | getMode = (routeType: string) => modes.find((mode) => mode.gtfsType === +routeType) 21 | 22 | render () { 23 | const {entity} = this.props 24 | return ( 25 |
26 | 30 | {modes.map((mode, i) => ( 31 | 32 | ))} 33 | 34 |
35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/alerts/components/RouteSelector.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | 5 | import * as activeAlertActions from '../actions/activeAlert' 6 | import {getRouteNameAlerts} from '../../editor/util/gtfs' 7 | import GtfsSearch from '../../gtfs/components/gtfs-search' 8 | 9 | import type {GtfsOption} from '../../gtfs/components/gtfs-search' 10 | import type {AlertEntity, Feed, GtfsRoute, GtfsStop} from '../../types' 11 | 12 | type Props = { 13 | clearable?: boolean, 14 | entity: AlertEntity, 15 | feeds: Array, 16 | filterByStop?: GtfsStop, 17 | minimumInput?: number, 18 | route?: GtfsRoute, 19 | updateActiveEntity: typeof activeAlertActions.updateActiveEntity 20 | } 21 | 22 | export default class RouteSelector extends Component { 23 | _onChange = (value: GtfsOption) => { 24 | const {entity, filterByStop, updateActiveEntity} = this.props 25 | if (value) { 26 | updateActiveEntity(entity, 'ROUTE', value.route, value.agency) 27 | } else if (value == null) { 28 | if (filterByStop) { 29 | updateActiveEntity(entity, 'ROUTE', null, entity.agency) 30 | } else { 31 | updateActiveEntity(entity, 'ROUTE', null, null) 32 | } 33 | } 34 | } 35 | 36 | render () { 37 | const {route, feeds, minimumInput, filterByStop, clearable, entity} = this.props 38 | const {agency: feed} = entity 39 | const agencyName = feed ? feed.name : 'Unknown agency' 40 | const routeName = route 41 | ? getRouteNameAlerts(route) || '[no name]' 42 | : '[route not found!]' 43 | const value = route 44 | ? { 45 | route, 46 | value: route.route_id, 47 | label: `${routeName} (${agencyName})`, 48 | agency: feed 49 | } 50 | : '' 51 | return ( 52 | 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/alerts/components/StopSelector.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | 5 | import * as activeAlertActions from '../actions/activeAlert' 6 | import GtfsSearch from '../../gtfs/components/gtfs-search' 7 | 8 | import type {GtfsOption} from '../../gtfs/components/gtfs-search' 9 | import type {AlertEntity, Feed, GtfsRoute, GtfsStop} from '../../types' 10 | 11 | type Props = { 12 | clearable?: boolean, 13 | entity: AlertEntity, 14 | feeds: Array, 15 | filterByRoute?: GtfsRoute, 16 | minimumInput?: number, 17 | stop: ?GtfsStop, 18 | updateActiveEntity: typeof activeAlertActions.updateActiveEntity 19 | } 20 | 21 | export default class StopSelector extends Component { 22 | _onChange = (value: GtfsOption) => { 23 | const {entity, updateActiveEntity} = this.props 24 | if (value) updateActiveEntity(entity, 'STOP', value.stop, value.agency) 25 | else updateActiveEntity(entity, 'STOP', null, null) 26 | } 27 | 28 | render () { 29 | const { 30 | stop, 31 | feeds, 32 | minimumInput, 33 | filterByRoute, 34 | clearable, 35 | entity 36 | } = this.props 37 | const {agency: feed} = entity 38 | const agencyName = feed ? feed.name : 'Unknown agency' 39 | return ( 40 | 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/alerts/containers/ActiveAlertEditor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import { 6 | createAlert, 7 | deleteAlert, 8 | onAlertEditorMount, 9 | saveAlert, 10 | setActiveAlert 11 | } from '../actions/alerts' 12 | import { 13 | setActiveProperty, 14 | setActivePublished, 15 | addActiveEntity, 16 | deleteActiveEntity, 17 | updateActiveEntity 18 | } from '../actions/activeAlert' 19 | import AlertEditor from '../components/AlertEditor' 20 | import { getFeedsForPermission } from '../../common/util/permissions' 21 | import {getActiveFeeds} from '../../gtfs/selectors' 22 | import {getActiveProject} from '../../manager/selectors' 23 | 24 | import type {AppState, RouterProps} from '../../types/reducers' 25 | 26 | export type Props = RouterProps 27 | 28 | const mapStateToProps = (state: AppState, ownProps: Props) => { 29 | return { 30 | activeFeeds: getActiveFeeds(state), 31 | alert: state.alerts.active, 32 | editableFeeds: getFeedsForPermission(getActiveProject(state), state.user, 'edit-alert'), 33 | permissionFilter: state.gtfs.filter.permissionFilter, 34 | project: getActiveProject(state), 35 | publishableFeeds: getFeedsForPermission(getActiveProject(state), state.user, 'approve-alert'), 36 | user: state.user 37 | } 38 | } 39 | 40 | const mapDispatchToProps = { 41 | addActiveEntity, 42 | createAlert, 43 | deleteActiveEntity, 44 | deleteAlert, 45 | onAlertEditorMount, 46 | saveAlert, 47 | setActiveAlert, 48 | setActiveProperty, 49 | setActivePublished, 50 | updateActiveEntity 51 | } 52 | 53 | const ActiveAlertEditor = connect( 54 | mapStateToProps, 55 | mapDispatchToProps 56 | )(AlertEditor) 57 | 58 | export default ActiveAlertEditor 59 | -------------------------------------------------------------------------------- /lib/alerts/containers/MainAlertsViewer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import {createAlert, fetchRtdAlerts, onAlertsViewerMount} from '../actions/alerts' 6 | import AlertsViewer from '../components/AlertsViewer' 7 | import {getActiveAndLoadedFeeds} from '../../gtfs/selectors' 8 | import {getActiveProject} from '../../manager/selectors' 9 | 10 | import type {AppState, RouterProps} from '../../types/reducers' 11 | 12 | export type Props = RouterProps 13 | 14 | const mapStateToProps = (state: AppState, ownProps: Props) => { 15 | return { 16 | activeFeeds: getActiveAndLoadedFeeds(state), 17 | alerts: state.alerts.all, 18 | fetched: state.alerts.fetched, 19 | isFetching: state.alerts.isFetching, 20 | permissionFilter: state.gtfs.filter.permissionFilter, 21 | project: getActiveProject(state), 22 | user: state.user 23 | } 24 | } 25 | 26 | const mapDispatchToProps = { 27 | createAlert, 28 | fetchRtdAlerts, 29 | onAlertsViewerMount 30 | } 31 | 32 | const MainAlertsViewer = connect( 33 | mapStateToProps, 34 | mapDispatchToProps 35 | )(AlertsViewer) 36 | 37 | export default MainAlertsViewer 38 | -------------------------------------------------------------------------------- /lib/alerts/containers/VisibleAlertsList.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import { 6 | editAlert, 7 | deleteAlert 8 | } from '../actions/alerts' 9 | import { 10 | setAlertAgencyFilter, 11 | setAlertSort, 12 | setVisibilityFilter, 13 | setVisibilitySearchText 14 | } from '../actions/visibilityFilter' 15 | import {getFeedsForPermission} from '../../common/util/permissions' 16 | import AlertsList from '../components/AlertsList' 17 | import {getActiveProject} from '../../manager/selectors' 18 | import {getVisibleAlerts} from '../selectors' 19 | 20 | import type {AppState} from '../../types/reducers' 21 | 22 | export type Props = {} 23 | 24 | const mapStateToProps = (state: AppState, ownProps: Props) => { 25 | const activeProject = getActiveProject(state) 26 | return { 27 | alerts: getVisibleAlerts(state), 28 | editableFeeds: getFeedsForPermission(activeProject, state.user, 'edit-alert'), 29 | feeds: activeProject && activeProject.feedSources ? activeProject.feedSources : [], 30 | fetched: state.alerts.fetched, 31 | filterCounts: state.alerts.counts, 32 | isFetching: state.alerts.isFetching, 33 | publishableFeeds: getFeedsForPermission(activeProject, state.user, 'approve-alert'), 34 | visibilityFilter: state.alerts.filter 35 | } 36 | } 37 | 38 | const mapDispatchToProps = { 39 | deleteAlert, 40 | editAlert, 41 | setAlertAgencyFilter, 42 | setAlertSort, 43 | setVisibilityFilter, 44 | setVisibilitySearchText 45 | } 46 | 47 | const VisibleAlertsList = connect(mapStateToProps, mapDispatchToProps)(AlertsList) 48 | 49 | export default VisibleAlertsList 50 | -------------------------------------------------------------------------------- /lib/alerts/reducers/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import active from './active' 4 | import alerts from './alerts' 5 | 6 | export default alerts.merge(active) 7 | -------------------------------------------------------------------------------- /lib/alerts/selectors/index.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | import { getFeedId } from '../../common/util/modules' 4 | import { filterAlertsByCategory } from '../util' 5 | 6 | export const getVisibleAlerts = createSelector( 7 | [state => state.alerts.all, state => state.alerts.filter], 8 | (alerts, visibilityFilter) => { 9 | if (!alerts) return [] 10 | 11 | // filter alerts by the search text string 12 | let visibleAlerts = alerts.filter(alert => 13 | alert.title.toLowerCase().indexOf((visibilityFilter.searchText || '').toLowerCase()) !== -1) 14 | 15 | if (visibilityFilter.feedId && visibilityFilter.feedId !== 'ALL') { 16 | // console.log('filtering alerts by feedId' + visibilityFilter.feedId) 17 | visibleAlerts = visibleAlerts.filter(alert => alert.affectedEntities.findIndex(ent => getFeedId(ent.agency) === visibilityFilter.feedId) !== -1) 18 | } 19 | 20 | if (visibilityFilter.sort) { 21 | // console.log('sorting alerts by ' + visibilityFilter.sort.type + ' direction: ' + visibilityFilter.sort.direction) 22 | visibleAlerts = visibleAlerts.sort((a, b) => { 23 | var aValue = visibilityFilter.sort.type === 'title' ? a[visibilityFilter.sort.type].toUpperCase() : a[visibilityFilter.sort.type] 24 | var bValue = visibilityFilter.sort.type === 'title' ? b[visibilityFilter.sort.type].toUpperCase() : b[visibilityFilter.sort.type] 25 | if (aValue < bValue) return visibilityFilter.sort.direction === 'asc' ? -1 : 1 26 | if (aValue > bValue) return visibilityFilter.sort.direction === 'asc' ? 1 : -1 27 | return 0 28 | }) 29 | } else { 30 | // sort by id 31 | visibleAlerts.sort((a, b) => a.id - b.id) 32 | } 33 | return filterAlertsByCategory(visibleAlerts, visibilityFilter.filter) 34 | } 35 | ) 36 | -------------------------------------------------------------------------------- /lib/assets/application_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/lib/assets/application_icon.png -------------------------------------------------------------------------------- /lib/assets/application_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalogueglobal/datatools-ui/952a304c401b19a44f77730c225125d2a76d6918/lib/assets/application_logo.png -------------------------------------------------------------------------------- /lib/common/components/ClickOutside.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from 'react' 4 | 5 | type Props = { 6 | children: React.Node, 7 | onClickOutside: (MouseEvent | KeyboardEvent) => void 8 | } 9 | 10 | /** 11 | * Wrapper component that detects click or key press (ESC) and calls 12 | * onClickOutside function in response. 13 | */ 14 | export default class ClickOutside extends React.Component { 15 | container = null 16 | 17 | componentDidMount () { 18 | document.addEventListener('click', this.handle, true) 19 | document.addEventListener('keydown', this.handleKeyDown, true) 20 | } 21 | 22 | componentWillUnmount () { 23 | document.removeEventListener('click', this.handle, true) 24 | document.removeEventListener('keydown', this.handleKeyDown, true) 25 | } 26 | 27 | handle = (e: MouseEvent) => { 28 | const {onClickOutside} = this.props 29 | const el = this.container 30 | // $FlowFixMe 31 | if (!el.contains(e.target)) onClickOutside(e) 32 | } 33 | 34 | handleKeyDown = (e: KeyboardEvent) => { 35 | const {onClickOutside} = this.props 36 | // Handle ESC key press 37 | if (e.keyCode === 27) onClickOutside(e) 38 | } 39 | 40 | render () { 41 | const {children, onClickOutside, ...props} = this.props 42 | return
{ this.container = ref }}> 45 | {children} 46 |
47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/common/components/InfoModal.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import { Modal, Button } from 'react-bootstrap' 5 | 6 | type Props = { 7 | body?: string, 8 | title?: string 9 | } 10 | 11 | type State = { 12 | body: string, 13 | showModal: boolean, 14 | title: string 15 | } 16 | 17 | export default class InfoModal extends React.Component { 18 | state = { 19 | body: '', 20 | showModal: false, 21 | title: '' 22 | } 23 | 24 | close () { 25 | this.setState({ 26 | showModal: false 27 | }) 28 | } 29 | 30 | open (props: Props) { 31 | this.setState({ 32 | showModal: true, 33 | title: props.title, 34 | body: props.body 35 | }) 36 | } 37 | 38 | ok = () => { 39 | this.close() 40 | } 41 | 42 | render () { 43 | const {Body, Footer, Header, Title} = Modal 44 | return ( 45 | 46 |
47 | {this.state.title} 48 |
49 | 50 | 51 |

{this.state.body}

52 | 53 | 54 |
55 | 59 |
60 |
61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/common/components/LanguageSelect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import { shallowEqual } from 'react-pure-render' 5 | import Select from 'react-select' 6 | import ISO6391 from 'iso-639-1' 7 | 8 | import {getComponentMessages} from '../util/config' 9 | 10 | type Props = { 11 | clearable?: boolean, 12 | minimumInput?: number, 13 | onChange?: string => void, 14 | placeholder?: string, 15 | tabIndex?: number, 16 | value: ?string 17 | } 18 | 19 | type State = { 20 | value: ?string 21 | } 22 | 23 | export default class LanguageSelect extends Component { 24 | messages = getComponentMessages('LanguageSelect') 25 | 26 | static defaultProps = { 27 | minimumInput: 1 28 | } 29 | 30 | componentWillMount () { 31 | this.setState({ 32 | value: this.props.value 33 | }) 34 | } 35 | 36 | componentWillReceiveProps (nextProps: Props) { 37 | if (!shallowEqual(nextProps.value, this.props.value)) { 38 | this.setState({value: nextProps.value}) 39 | } 40 | } 41 | 42 | _onChange = (value: string) => { 43 | const {onChange} = this.props 44 | this.setState({value}) 45 | onChange && onChange(value) 46 | } 47 | 48 | _getOptions = () => ISO6391.getAllCodes().map(code => ({value: code, label: ISO6391.getName(code)})) 49 | 50 | render () { 51 | const {clearable, tabIndex, placeholder, minimumInput} = this.props 52 | return ( 53 | 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/common/components/Title.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {Component} from 'react' 4 | 5 | type Props = { 6 | children: string 7 | } 8 | 9 | export default class Title extends Component { 10 | oldTitle = '' 11 | 12 | componentWillMount () { 13 | this.oldTitle = document.title 14 | document.title = this.props.children 15 | } 16 | 17 | componentWillUnmount () { 18 | document.title = this.oldTitle 19 | } 20 | 21 | componentWillReceiveProps (nextProps: Props) { 22 | if (nextProps.children !== this.props.children) { 23 | document.title = nextProps.children 24 | } 25 | } 26 | 27 | shouldComponentUpdate (nextProps: Props) { 28 | // Never update the component when the title changes. 29 | return false 30 | } 31 | 32 | render () { 33 | return null 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/common/components/UserButtons.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { Component } from 'react' 4 | import { Button } from 'react-bootstrap' 5 | import { LinkContainer } from 'react-router-bootstrap' 6 | import Icon from '@conveyal/woonerf/components/icon' 7 | 8 | import type {ManagerUserState} from '../../types/reducers' 9 | 10 | type Props = { 11 | logout: () => any, 12 | user: ManagerUserState 13 | } 14 | 15 | /** 16 | * A common component containing buttons for standard user actions: "My 17 | * Account", "Site Admin", and "Logout" 18 | */ 19 | export default class UserButtons extends Component { 20 | render () { 21 | const { logout, user } = this.props 22 | const buttonStyle = { margin: 2 } 23 | const isSiteAdmin = user.permissions && user.permissions.isApplicationAdmin() && 24 | user.permissions.canAdministerAnOrganization() 25 | return ( 26 |
27 | 28 | 31 | 32 | {isSiteAdmin && ( 33 | 34 | 41 | 42 | )} 43 | 50 |
51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/common/constants/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const SECURE: string = 'secure/' 3 | export const API_PREFIX: string = `/api/manager/` 4 | export const SECURE_API_PREFIX: string = `${API_PREFIX}${SECURE}` 5 | export const GTFS_GRAPHQL_PREFIX: string = `${SECURE_API_PREFIX}gtfs/graphql` 6 | export const EDITOR_PREFIX: string = `/api/editor/` 7 | export const SECURE_EDITOR_PREFIX: string = `${EDITOR_PREFIX}${SECURE}` 8 | -------------------------------------------------------------------------------- /lib/common/containers/ActiveSidebar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import Sidebar from '../components/Sidebar' 6 | import {logout, revokeToken} from '../../manager/actions/user' 7 | import { 8 | fetchAppInfo, 9 | removeRetiredJob, 10 | startJobMonitor, 11 | setJobMonitorVisible 12 | } from '../../manager/actions/status' 13 | import {setSidebarExpanded, setTutorialHidden} from '../../manager/actions/ui' 14 | 15 | import type {AppState} from '../../types/reducers' 16 | 17 | export type Props = {} 18 | 19 | const mapStateToProps = (state: AppState, ownProps: Props) => { 20 | return { 21 | appInfo: state.status.appInfo, 22 | expanded: state.ui.sidebarExpanded, 23 | hideTutorial: state.ui.hideTutorial, 24 | jobMonitor: state.status.jobMonitor, 25 | languages: state.languages ? state.languages : ['English', 'Español', 'Français'], 26 | projects: state.projects ? state.projects : null, 27 | user: state.user, 28 | userPicture: state.user.profile ? state.user.profile.picture : null 29 | } 30 | } 31 | 32 | const mapDispatchToProps = { 33 | fetchAppInfo, 34 | logout, 35 | removeRetiredJob, 36 | revokeToken, 37 | setJobMonitorVisible, 38 | setSidebarExpanded, 39 | setTutorialHidden, 40 | startJobMonitor 41 | } 42 | 43 | const ActiveSidebar = connect(mapStateToProps, mapDispatchToProps)(Sidebar) 44 | 45 | export default ActiveSidebar 46 | -------------------------------------------------------------------------------- /lib/common/containers/ActiveSidebarNavItem.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import SidebarNavItem from '../components/SidebarNavItem' 6 | 7 | import type {AppState} from '../../types/reducers' 8 | 9 | export type Props = { 10 | 'data-test-id'?: string, 11 | active?: boolean, 12 | icon: string, 13 | label: string, 14 | link?: string 15 | } 16 | 17 | const mapStateToProps = (state: AppState, ownProps: Props) => { 18 | return { 19 | expanded: state.ui.sidebarExpanded 20 | } 21 | } 22 | 23 | const mapDispatchToProps = {} 24 | 25 | var ActiveSidebarNavItem = connect( 26 | mapStateToProps, 27 | mapDispatchToProps 28 | )(SidebarNavItem) 29 | 30 | export default ActiveSidebarNavItem 31 | -------------------------------------------------------------------------------- /lib/common/containers/CurrentStatusMessage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux' 4 | 5 | import StatusMessage from '../components/StatusMessage' 6 | 7 | import type {AppState} from '../../types/reducers' 8 | 9 | export type Props = {} 10 | 11 | const mapStateToProps = (state: AppState, ownProps: Props) => { 12 | return { 13 | message: state.status.message, 14 | sidebarExpanded: state.ui.sidebarExpanded 15 | } 16 | } 17 | 18 | var CurrentStatusMessage = connect( 19 | mapStateToProps 20 | )(StatusMessage) 21 | 22 | export default CurrentStatusMessage 23 | -------------------------------------------------------------------------------- /lib/common/containers/CurrentStatusModal.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | 5 | import StatusModal from '../components/StatusModal' 6 | import {clearStatusModal} from '../../manager/actions/status' 7 | import {removeEditorLock} from '../../editor/actions/editor' 8 | 9 | import type {AppState} from '../../types/reducers' 10 | 11 | export type Props = {} 12 | 13 | const mapStateToProps = (state: AppState, ownProps: Props) => { 14 | return { 15 | ...state.status.modal 16 | } 17 | } 18 | const mapDispatchToProps = { 19 | clearStatusModal, 20 | removeEditorLock 21 | } 22 | 23 | var CurrentStatusModal = connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(StatusModal) 27 | 28 | export default CurrentStatusModal 29 | -------------------------------------------------------------------------------- /lib/common/containers/Login.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {connect} from 'react-redux' 4 | import {push} from 'react-router-redux/lib/actions' 5 | 6 | import Login from '../components/Login' 7 | import {receiveTokenAndProfile} from '../../manager/actions/user' 8 | 9 | import type {AppState, RouterProps} from '../../types/reducers' 10 | 11 | export type Props = $Shape & { 12 | onHide?: () => void, 13 | redirectOnSuccess?: string 14 | } 15 | 16 | const mapStateToProps = (state: AppState, ownProps: Props) => { 17 | const {redirectOnSuccess} = state.user 18 | return { 19 | // If redirect URL is null, we want to default to application home. 20 | redirectOnSuccess: redirectOnSuccess || window.location.path || '/home' 21 | } 22 | } 23 | 24 | const mapDispatchToProps = { 25 | push, 26 | receiveTokenAndProfile, 27 | // Default onHide action is to go back to root page 28 | onHide: () => push('/') 29 | } 30 | 31 | // this was done because there was an error I couldn't figure out in StatusModal 32 | const connected: any = connect(mapStateToProps, mapDispatchToProps)(Login) 33 | export default connected 34 | -------------------------------------------------------------------------------- /lib/common/containers/PageContent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as React from 'react' 4 | import {connect} from 'react-redux' 5 | 6 | import type {AppState} from '../../types/reducers' 7 | 8 | type ContainerProps = { 9 | children: React.Node 10 | } 11 | 12 | type Props = ContainerProps & { 13 | sidebarExpanded: boolean 14 | } 15 | 16 | class Content extends React.Component { 17 | render () { 18 | return ( 19 |
28 | {this.props.children} 29 |
30 | ) 31 | } 32 | } 33 | 34 | const mapStateToProps = (state: AppState, ownProps: ContainerProps) => { 35 | return { 36 | sidebarExpanded: state.ui.sidebarExpanded 37 | } 38 | } 39 | 40 | const mapDispatchToProps = {} 41 | 42 | var PageContent = connect( 43 | mapStateToProps, 44 | mapDispatchToProps 45 | )(Content) 46 | 47 | export default PageContent 48 | -------------------------------------------------------------------------------- /lib/common/containers/StarButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Icon from '@conveyal/woonerf/components/icon' 4 | import React, {Component} from 'react' 5 | import {Button} from 'react-bootstrap' 6 | import {connect} from 'react-redux' 7 | 8 | import {getComponentMessages} from '../util/config' 9 | // $FlowFixMe FIXME action no longer present in user actions 10 | import {updateStar} from '../../manager/actions/user' 11 | 12 | import type {dispatchFn, ManagerUserState} from '../../types/reducers' 13 | 14 | type Props = { 15 | dispatch: dispatchFn, 16 | isStarred: boolean, 17 | target: string, 18 | user: ManagerUserState 19 | } 20 | 21 | class StarButton extends Component { 22 | messages = getComponentMessages('StarButton') 23 | 24 | _onClick = () => { 25 | const {dispatch, isStarred, user, target} = this.props 26 | dispatch(updateStar(user.profile, target, !isStarred)) 27 | } 28 | 29 | render () { 30 | const {isStarred} = this.props 31 | 32 | return ( 33 | 40 | ) 41 | } 42 | } 43 | 44 | export default connect()(StarButton) 45 | -------------------------------------------------------------------------------- /lib/common/containers/WatchButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import {Button, Glyphicon, MenuItem} from 'react-bootstrap' 5 | import {connect} from 'react-redux' 6 | 7 | import {updateTargetForSubscription} from '../../manager/actions/user' 8 | import {getComponentMessages, getConfigProperty} from '../util/config' 9 | 10 | import type {AppState, ManagerUserState} from '../../types/reducers' 11 | 12 | type ContainerProps = { 13 | componentClass?: string, 14 | isWatching: ?boolean, 15 | subscriptionType: string, 16 | target: string, 17 | user: ManagerUserState 18 | } 19 | 20 | type Props = ContainerProps & { 21 | updateTarget: typeof updateTargetForSubscription 22 | } 23 | 24 | class WatchButton extends Component { 25 | messages = getComponentMessages('WatchButton') 26 | 27 | _onToggleWatch = () => { 28 | const {updateTarget, user, target, subscriptionType} = this.props 29 | if (user.profile) updateTarget(user.profile, target, subscriptionType) 30 | else console.warn('User profile not found. Cannot update subscription', user) 31 | } 32 | 33 | _getLabel = () => { 34 | const {isWatching} = this.props 35 | return ( 36 | 37 | {' '} 38 | {this.messages(isWatching ? 'unwatch' : 'watch')} 39 | 40 | ) 41 | } 42 | 43 | render () { 44 | const {componentClass} = this.props 45 | // Do not render watch button if notifications are not enabled. 46 | if (!getConfigProperty('application.notifications_enabled')) return null 47 | switch (componentClass) { 48 | case 'menuItem': 49 | return ( 50 | 52 | {this._getLabel()} 53 | 54 | ) 55 | default: 56 | return ( 57 | 61 | ) 62 | } 63 | } 64 | } 65 | 66 | const mapDispatchToProps = { 67 | updateTarget: updateTargetForSubscription 68 | } 69 | 70 | const mapStateToProps = (state: AppState, ownProps: ContainerProps) => ({}) 71 | 72 | export default connect( 73 | mapStateToProps, 74 | mapDispatchToProps 75 | )(WatchButton) 76 | -------------------------------------------------------------------------------- /lib/common/user/UserSubscriptions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {DatatoolsApps} from './UserPermissions' 4 | 5 | export type Subscription = { 6 | target: Array, 7 | type: string 8 | } 9 | 10 | export default class UserSubscriptions { 11 | subscriptionLookup: {[string]: Subscription} = {} 12 | 13 | constructor (datatoolsApps: DatatoolsApps) { 14 | const clientId = process.env.AUTH0_CLIENT_ID 15 | if (!clientId) throw new Error('Auth0 client ID must be set in config') 16 | // If missing datatoolsApp, construct an empty subscriptions object. 17 | if (!datatoolsApps) return 18 | else if (!Array.isArray(datatoolsApps)) { 19 | console.warn('User app_metadata is misconfigured.', datatoolsApps) 20 | return 21 | } 22 | const datatoolsJson = datatoolsApps.find(dt => dt.client_id === clientId) 23 | if (datatoolsJson && datatoolsJson.subscriptions) { 24 | for (const subscription of datatoolsJson.subscriptions) { 25 | this.subscriptionLookup[subscription.type] = subscription 26 | } 27 | } 28 | } 29 | hasSubscription (subscriptionType: string) { 30 | return this.subscriptionLookup[subscriptionType] !== null 31 | } 32 | 33 | getSubscription (subscriptionType: string) { 34 | return this.subscriptionLookup[subscriptionType] 35 | } 36 | 37 | hasProjectSubscription (projectId: string, subscriptionType: string) { 38 | if (!this.hasSubscription(subscriptionType)) return null 39 | const subscription = this.getSubscription(subscriptionType) 40 | return subscription ? subscription.target.indexOf(projectId) !== -1 : false 41 | } 42 | 43 | hasFeedSubscription (projectId: string, feedId: string, subscriptionType: string) { 44 | if (!this.hasSubscription(subscriptionType)) return null 45 | else if (this.hasProjectSubscription(projectId, subscriptionType)) return true 46 | const subscription = this.getSubscription(subscriptionType) 47 | return subscription ? subscription.target.indexOf(feedId) !== -1 : false 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/common/user/__tests__/Auth0Manager.js.hold: -------------------------------------------------------------------------------- 1 | import auth0 from '../Auth0Manager' 2 | 3 | describe('common > user > Auth0Manager >', () => { 4 | it('should login with lock', (done) => { 5 | // setup localStorage 6 | const storage = {} 7 | window.localStorage = { 8 | getItem: (k) => storage[k], 9 | setItem: (k, v) => { storage[k] = v } 10 | } 11 | 12 | // setup mock action 13 | const receiveTokenAndProfile = jest.fn() 14 | 15 | // set auth0 lock options to generate test response 16 | auth0.lockOptions = { 17 | fakeAuthenticatedToken: 'fake-token', 18 | getProfileSuccess: true, 19 | getProfileResult: { 20 | app_metadata: { 21 | datatools: [] 22 | } 23 | } 24 | } 25 | 26 | auth0.loginWithLock({ 27 | receiveTokenAndProfile 28 | }) 29 | 30 | // do a timeout because the promise needs to execute 31 | setTimeout(() => { 32 | expect(receiveTokenAndProfile.mock.calls).toMatchSnapshot() 33 | done() 34 | }, 10) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /lib/common/user/__tests__/__snapshots__/Auth0Manager.js.snap.hold: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`common > user > Auth0Manager > should login with lock 1`] = ` 4 | Array [ 5 | Array [ 6 | Object { 7 | "profile": Object { 8 | "app_metadata": Object { 9 | "datatools": Array [], 10 | }, 11 | }, 12 | "token": "fake-token", 13 | }, 14 | ], 15 | ] 16 | `; 17 | -------------------------------------------------------------------------------- /lib/common/util/__tests__/config.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {getComponentMessages} from '../config' 4 | 5 | describe('lib > common > util > config', () => { 6 | describe('> getComponentMessages', () => { 7 | let oldConfig 8 | 9 | afterEach(() => { 10 | window.DT_CONFIG = oldConfig 11 | }) 12 | 13 | beforeEach(() => { 14 | oldConfig = window.DT_CONFIG 15 | window.DT_CONFIG = { 16 | messages: { 17 | active: { 18 | components: { 19 | Breadcrumbs: { 20 | deployments: 'Deployments', 21 | projects: 'Projects', 22 | root: 'Explore' 23 | } 24 | } 25 | } 26 | } 27 | } 28 | }) 29 | 30 | it('should return message properly', () => { 31 | expect(getComponentMessages('Breadcrumbs')('root')).toEqual('Explore') 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /lib/common/util/__tests__/gtfs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import {secondsAfterMidnightToHHMM} from '../gtfs' 4 | 5 | describe('lib > common > util > gtfs', () => { 6 | describe('> secondsAfterMidnightToHHMM', () => { 7 | describe('valid times', () => { 8 | it('should parse value 0', () => { 9 | expect(secondsAfterMidnightToHHMM(0)).toEqual('00:00:00') 10 | }) 11 | 12 | it('should parse a value in the day', () => { 13 | expect(secondsAfterMidnightToHHMM(12345)).toEqual('03:25:45') 14 | }) 15 | 16 | it('should parse a value after midnight', () => { 17 | expect(secondsAfterMidnightToHHMM(99999)).toEqual('27:46:39') 18 | }) 19 | 20 | it('should parse a value 2 days in the future', () => { 21 | expect(secondsAfterMidnightToHHMM(222222)).toEqual('61:43:42') 22 | }) 23 | 24 | it('should parse a value below 0', () => { 25 | expect(secondsAfterMidnightToHHMM(-1)).toEqual('23:59:59 (previous day)') 26 | }) 27 | }) 28 | 29 | describe('invalid times', () => { 30 | it('should not parse null', () => { 31 | expect(secondsAfterMidnightToHHMM(null)).toEqual('') 32 | }) 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /lib/common/util/analytics.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import ReactGA from 'react-ga' 4 | 5 | // Check if Google Analytics is enabled for the application. 6 | const hasAnalytics: boolean = 7 | process.env.NODE_ENV !== 'dev' && 8 | process.env.NODE_ENV !== 'test' && 9 | !!process.env.GOOGLE_ANALYTICS_TRACKING_ID 10 | if (!hasAnalytics) console.warn('Google Analytics not enabled.') 11 | else ReactGA.initialize(process.env.GOOGLE_ANALYTICS_TRACKING_ID) 12 | 13 | /** 14 | * Log page views in Google Analytics (if enabled). 15 | */ 16 | export function logPageView (): void { 17 | if (hasAnalytics) { 18 | const page = `${window.location.pathname}${window.location.search}` 19 | ReactGA.set({page}) 20 | ReactGA.pageview(page) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/common/util/date-time.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import moment from 'moment' 4 | 5 | import {getConfigProperty} from './config' 6 | 7 | export function formatTimestamp (value: number | string, includeTime: boolean = true): string { 8 | const dateFormat = getConfigProperty('application.date_format') || 'MMM Do YYYY' 9 | const timeFormat = getConfigProperty('application.time_format') || 'h:MMa' 10 | return moment(value).format(`${dateFormat}${includeTime ? `, ${timeFormat}` : ''}`) 11 | } 12 | 13 | export function fromNow (value: number | string): string { 14 | return moment(value).fromNow() 15 | } 16 | 17 | /** 18 | * Converts seconds to an hour:minute string. 19 | */ 20 | export function convertSecondsToHHMMString (seconds: number): string { 21 | const hours = Math.floor(seconds / 60 / 60) 22 | const minutes = Math.floor(seconds / 60) % 60 23 | return seconds ? `${hours}:${minutes < 10 ? '0' + minutes : minutes}` : '00:00' 24 | } 25 | 26 | export function convertHHMMStringToSeconds (string: string): number { 27 | const hourMinute = string.split(':') 28 | if (!isNaN(hourMinute[0]) && !isNaN(hourMinute[1])) { 29 | // If both hours and minutes are present 30 | return (Math.abs(+hourMinute[0]) * 60 * 60) + (Math.abs(+hourMinute[1]) * 60) 31 | } else if (isNaN(hourMinute[0])) { 32 | // If less than one hour 33 | return Math.abs(+hourMinute[1]) * 60 34 | } else if (isNaN(hourMinute[1])) { 35 | // If minutes are not present 36 | return Math.abs(+hourMinute[0]) * 60 * 60 37 | } else { 38 | // If no input 39 | return 0 40 | } 41 | } 42 | 43 | export function convertSecondsToMMSSString (seconds: number) { 44 | const minutes = Math.floor(seconds / 60) 45 | const sec = seconds % 60 46 | return seconds ? `${minutes}:${sec < 10 ? '0' + sec : sec}` : '00:00' 47 | } 48 | 49 | export function convertMMSSStringToSeconds (string: string) { 50 | const minuteSecond = string.split(':') 51 | if (!isNaN(minuteSecond[0]) && !isNaN(minuteSecond[1])) { 52 | return (Math.abs(+minuteSecond[0]) * 60) + Math.abs(+minuteSecond[1]) 53 | } else if (isNaN(minuteSecond[0])) { 54 | return Math.abs(+minuteSecond[1]) 55 | } else if (isNaN(minuteSecond[1])) { 56 | return Math.abs(+minuteSecond[0] * 60) 57 | } else { 58 | return 0 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/common/util/file-download.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import {saveAs} from 'file-saver' 3 | 4 | /** 5 | * This downloads a file using file-saver. Previously a custom method was used 6 | * (essentially the link.click simulation found here 7 | * https://stackoverflow.com/a/14966131/915811). However, that method no longer 8 | * works with the latest version of Chrome. 9 | */ 10 | export default function fileDownload (data: any, filename: string, type: string): void { 11 | const blob: Blob = new window.Blob([data], {type}) 12 | saveAs(blob, filename) 13 | } 14 | -------------------------------------------------------------------------------- /lib/common/util/geo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {Bounds, Feed, FeedWithValidation} from '../../types' 4 | 5 | export function getFeedsBounds (feeds: Array): ?Bounds { 6 | const feedsWithBounds: Array = ((feeds.filter( 7 | (feed: Feed) => feed.latestValidation && feed.latestValidation.bounds 8 | ): any): Array) 9 | if (feedsWithBounds.length === 1) { 10 | return feedsWithBounds[0].latestValidation.bounds 11 | } else if (feedsWithBounds.length === 0) { 12 | return null 13 | } else { 14 | const bounds: Bounds = feedsWithBounds[0].latestValidation.bounds 15 | feedsWithBounds.forEach((feed: FeedWithValidation) => { 16 | const curFeedBounds: Bounds = feed.latestValidation.bounds 17 | if (curFeedBounds.east > bounds.east) { 18 | bounds.east = curFeedBounds.east 19 | } 20 | if (curFeedBounds.north > bounds.north) { 21 | bounds.north = curFeedBounds.north 22 | } 23 | if (curFeedBounds.south < bounds.south) { 24 | bounds.south = curFeedBounds.south 25 | } 26 | if (curFeedBounds.west < bounds.west) { 27 | bounds.west = curFeedBounds.west 28 | } 29 | }) 30 | return bounds 31 | } 32 | } 33 | 34 | export function convertToArrayBounds (bounds: ?Bounds) { 35 | if (!bounds) throw new Error('Must provide valid bounds ({north, south, east, west})') 36 | else return [[bounds.north, bounds.east], [bounds.south, bounds.west]] 37 | } 38 | -------------------------------------------------------------------------------- /lib/common/util/gtfs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import moment from 'moment' 4 | 5 | /** 6 | * @param {number} seconds Seconds after midnight 7 | * @return {string} A blank string if not a valid value, 8 | * or a string in the format HH:mm:ss where HH can be greater than 23 9 | */ 10 | export function secondsAfterMidnightToHHMM (seconds: ?(number | string)): string { 11 | if (typeof seconds === 'number') { 12 | const formattedValue = moment() 13 | .startOf('day') 14 | .seconds(seconds) 15 | .format('HH:mm:ss') 16 | if (seconds >= 86400) { 17 | // Replace hours part if seconds are greater than 24 hours (by default 18 | // moment.js does not handle times greater than 24h). 19 | const parts = formattedValue.split(':') 20 | parts[0] = '' + (parseInt(parts[0], 10) + 24 * Math.floor(seconds / 86400)) 21 | return parts.join(':') 22 | } else if (seconds < 0) { 23 | // probably an extreme edge case, but it's technically possible 24 | return `${formattedValue} (previous day)` 25 | } else { 26 | return formattedValue 27 | } 28 | } 29 | // If handling time format and value is not a number, return empty string. 30 | return '' 31 | } 32 | 33 | /** 34 | * Shorthand helper function to convert seconds value to human-readable text. 35 | */ 36 | export function humanizeSeconds (seconds: number): string { 37 | return moment.duration(seconds, 'seconds').humanize() 38 | } 39 | -------------------------------------------------------------------------------- /lib/common/util/json.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * Check if a string is valid JSON. 5 | */ 6 | export function isValidJSON (str: string): boolean { 7 | try { 8 | JSON.parse(str) 9 | } catch (e) { 10 | return false 11 | } 12 | return true 13 | } 14 | -------------------------------------------------------------------------------- /lib/common/util/map-keys.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import forEach from 'lodash/forEach' 3 | import camelCase from 'lodash/camelCase' 4 | import isPlainObject from 'lodash/isPlainObject' 5 | import snakeCase from 'lodash/snakeCase' 6 | 7 | /** 8 | * Converts the keys for an object (or array of objects) using string mapping 9 | * function passed in. Operates on object recursively. 10 | */ 11 | function mapObjectKeys (object: Object, keyMapper: string => string): Object { 12 | const convertedObject = {} 13 | const convertedArray = [] 14 | forEach( 15 | object, 16 | (value: Object, key: string) => { 17 | if (isPlainObject(value) || Array.isArray(value)) { 18 | // If plain object or an array, recursively update keys of any values 19 | // that are also objects. 20 | value = mapObjectKeys(value, keyMapper) 21 | } 22 | if (Array.isArray(object)) convertedArray.push(value) 23 | else convertedObject[keyMapper(key)] = value 24 | } 25 | ) 26 | // $FlowFixMe 27 | if (Array.isArray(object)) return convertedArray 28 | else return convertedObject 29 | } 30 | 31 | /** 32 | * Converts the keys for an object or array of objects to camelCase. The function 33 | * always recursively converts keys. 34 | */ 35 | export function camelCaseKeys (object: Object): Object { 36 | return mapObjectKeys(object, camelCase) 37 | } 38 | 39 | /** 40 | * Converts the keys for an object or array of objects to snake_case. The function 41 | * always recursively converts keys. 42 | */ 43 | export function snakeCaseKeys (object: Object): Object { 44 | return mapObjectKeys(object, snakeCase) 45 | } 46 | -------------------------------------------------------------------------------- /lib/common/util/maps.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Browser } from 'leaflet' 4 | 5 | export function defaultTileURL (mapId: ?string): string { 6 | const MAPBOX_MAP_ID = mapId || process.env.MAPBOX_MAP_ID 7 | const MAPBOX_ACCESS_TOKEN = process.env.MAPBOX_ACCESS_TOKEN 8 | if (!MAPBOX_MAP_ID || !MAPBOX_ACCESS_TOKEN) { 9 | throw new Error('Mapbox ID and token not defined') 10 | } 11 | return `https://api.tiles.mapbox.com/v4/${MAPBOX_MAP_ID}/{z}/{x}/{y}${Browser.retina ? '@2x' : ''}.png?access_token=${MAPBOX_ACCESS_TOKEN}` 12 | } 13 | -------------------------------------------------------------------------------- /lib/common/util/modules.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import objectPath from 'object-path' 4 | 5 | import {getConfigProperty} from './config' 6 | 7 | import type {Feed} from '../../types' 8 | 9 | export function getFeed (feeds: ?Array, id: string): ?Feed { 10 | // console.log(feeds, id) 11 | // TODO: move use_extension to extension enabled?? 12 | const useMtc = getConfigProperty('modules.gtfsapi.use_extension') === 'mtc' 13 | const feed = feeds 14 | ? feeds.find( 15 | feed => 16 | useMtc 17 | ? objectPath.get(feed, 'externalProperties.MTC.AgencyId') === id 18 | : feed.id === id 19 | ) 20 | : null 21 | return feed 22 | } 23 | 24 | export function getFeedId (feed: ?Feed): ?string { 25 | const useMtc = getConfigProperty('modules.gtfsapi.use_extension') === 'mtc' 26 | return !feed 27 | ? null 28 | : useMtc ? objectPath.get(feed, 'externalProperties.MTC.AgencyId') : feed.id 29 | } 30 | 31 | function getRtdApi (): ?string { 32 | if (getConfigProperty('modules.alerts.use_extension') === 'mtc') { 33 | return getConfigProperty('extensions.mtc.rtd_api') 34 | } 35 | return null 36 | } 37 | 38 | export function getAlertsUrl (): string { 39 | const rtdApi = getRtdApi() 40 | return rtdApi ? rtdApi + '/ServiceAlert' : '/api/manager/secure/alerts' 41 | } 42 | 43 | export function getSignConfigUrl (): string { 44 | const rtdApi = getRtdApi() 45 | return rtdApi 46 | ? rtdApi + '/DisplayConfiguration' 47 | : '/api/manager/secure/displays' 48 | } 49 | 50 | export function getDisplaysUrl (): string { 51 | const rtdApi = getRtdApi() 52 | return rtdApi 53 | ? rtdApi + '/Display' 54 | : '/api/manager/secure/displays' 55 | } 56 | -------------------------------------------------------------------------------- /lib/common/util/permissions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {AlertEntity, Feed, Project} from '../../types' 4 | import type {ManagerUserState} from '../../types/reducers' 5 | 6 | export function getFeedsForPermission ( 7 | project: ?Project, 8 | user: ManagerUserState, 9 | permission: string 10 | ): Array { 11 | if (project && project.feedSources) { 12 | const {id, organizationId} = project 13 | return project.feedSources.filter( 14 | feed => 15 | user.permissions && user.permissions.hasFeedPermission( 16 | organizationId, 17 | id, 18 | feed.id, 19 | permission 20 | ) !== null 21 | ) 22 | } 23 | return [] 24 | } 25 | 26 | // ensure list of feeds contains all agency IDs for set of entities 27 | export function checkEntitiesForFeeds ( 28 | entities: Array, 29 | feeds: Array 30 | ): boolean { 31 | const publishableIds: Array = feeds.map(f => f.id) 32 | const entityIds: Array = entities 33 | ? entities.map(entity => entity.agency ? entity.agency.id : '') 34 | : [] 35 | for (var i = 0; i < entityIds.length; i++) { 36 | if (publishableIds.indexOf(entityIds[i]) === -1) return false 37 | } 38 | return true 39 | } 40 | -------------------------------------------------------------------------------- /lib/common/util/to-sentence-case.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import toLower from 'lodash/toLower' 4 | import upperFirst from 'lodash/upperFirst' 5 | 6 | export default function toSentenceCase (s: string): string { 7 | return upperFirst(toLower(s)) 8 | } 9 | -------------------------------------------------------------------------------- /lib/common/util/upload-file.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import fetch from 'isomorphic-fetch' 4 | 5 | import {getHeaders} from './util' 6 | 7 | export function uploadFile ({ 8 | file, token, url 9 | }: { 10 | file: File, token: string, url: string 11 | }) { 12 | return fetch(url, { 13 | method: 'post', 14 | headers: getHeaders(token, true, 'application/zip'), 15 | body: file 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /lib/common/util/user.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import objectPath from 'object-path' 4 | 5 | import type {Profile} from '../../types' 6 | 7 | export const getUserMetadataProperty = ( 8 | profile: ?Profile, 9 | propertyString: string 10 | ) => { 11 | const CLIENT_ID = process.env.AUTH0_CLIENT_ID 12 | const datatools = objectPath.get(profile, 'user_metadata.datatools') 13 | const application = 14 | datatools && datatools.find(d => d.client_id === CLIENT_ID) 15 | return objectPath.get(application, propertyString) 16 | } 17 | -------------------------------------------------------------------------------- /lib/editor/components/ColorField.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import {FormGroup} from 'react-bootstrap' 5 | import SketchPicker from 'react-color/lib/components/sketch/Sketch' 6 | 7 | import ClickOutside from '../../common/components/ClickOutside' 8 | 9 | type Props = { 10 | field: any, 11 | formProps: any, 12 | label: any, 13 | onChange: any => void, 14 | value: ?string 15 | } 16 | 17 | type State = { 18 | color: {a: string, b: string, g: string, r: string}, 19 | open: boolean 20 | } 21 | 22 | export default class ColorField extends Component { 23 | state = { 24 | open: false, 25 | color: {r: '241', g: '112', b: '19', a: '1'} // default color 26 | } 27 | 28 | _handleClick = (e: SyntheticInputEvent) => { 29 | e.preventDefault() 30 | this.setState({ open: !this.state.open }) 31 | } 32 | 33 | _handleClose = () => this.setState({ open: !this.state.open }) 34 | 35 | render () { 36 | const {formProps, label, value} = this.props 37 | const hexColor = value ? `#${value}` : '#000000' 38 | const colorStyle = { 39 | width: '36px', 40 | height: '20px', 41 | borderRadius: '2px', 42 | background: hexColor 43 | } 44 | const styles = { 45 | swatch: { 46 | padding: '5px', 47 | marginRight: '30px', 48 | background: '#fff', 49 | borderRadius: '4px', 50 | display: 'inline-block', 51 | cursor: 'pointer' 52 | }, 53 | popover: { 54 | position: 'absolute', 55 | zIndex: '200' 56 | }, 57 | cover: { 58 | position: 'fixed', 59 | top: '0', 60 | right: '0', 61 | bottom: '0', 62 | left: '0' 63 | } 64 | } 65 | return ( 66 | 67 | {label} 68 | 73 | {this.state.open 74 | ? 76 | 79 | 80 | : null 81 | } 82 | 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/editor/components/EditorSidebar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | 5 | import ActiveSidebarNavItem from '../../common/containers/ActiveSidebarNavItem' 6 | import ActiveSidebar from '../../common/containers/ActiveSidebar' 7 | import { GTFS_ICONS } from '../util/ui' 8 | 9 | import type {Feed} from '../../types' 10 | import type {GtfsIcon} from '../util/ui' 11 | 12 | type Props = { 13 | activeComponent: string, 14 | editingIsDisabled: boolean, 15 | feedSource: Feed 16 | } 17 | 18 | export default class EditorSidebar extends Component { 19 | isActive (item: GtfsIcon, component: string) { 20 | return component === item.id || (component === 'scheduleexception' && item.id === 'calendar') 21 | } 22 | 23 | render () { 24 | const {activeComponent, editingIsDisabled, feedSource} = this.props 25 | 26 | return ( 27 | 28 | 35 | {GTFS_ICONS.map(item => { 36 | return item.hideSidebar 37 | ? null 38 | : 50 | })} 51 | 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/editor/components/HourMinuteInput.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import {FormControl} from 'react-bootstrap' 5 | 6 | import { 7 | convertSecondsToHHMMString, 8 | convertHHMMStringToSeconds 9 | } from '../../common/util/date-time' 10 | 11 | type Props = { 12 | onChange: (number) => any, 13 | seconds: number, 14 | style: {[string]: string | number} 15 | } 16 | 17 | type State = { 18 | string?: string 19 | } 20 | 21 | export default class HourMinuteInput extends Component { 22 | state = {} 23 | 24 | _onChange = (evt: SyntheticInputEvent) => { 25 | const {onChange} = this.props 26 | const {value} = evt.target 27 | const seconds = convertHHMMStringToSeconds(value) 28 | this.setState({string: value}) 29 | onChange && onChange(seconds) 30 | } 31 | 32 | componentWillReceiveProps (nextProps: Props) { 33 | this.setState({string: convertSecondsToHHMMString(nextProps.seconds)}) 34 | } 35 | 36 | render () { 37 | const {seconds, style, ...otherProps} = this.props 38 | const {string} = this.state 39 | return ( 40 | 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/editor/components/MinuteSecondInput.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import {FormControl} from 'react-bootstrap' 5 | 6 | import { 7 | convertSecondsToMMSSString, 8 | convertMMSSStringToSeconds 9 | } from '../../common/util/date-time' 10 | 11 | type Props = { 12 | onChange: number => void, 13 | seconds: number, 14 | style?: {[string]: string | number} 15 | } 16 | 17 | type State = { 18 | seconds: number, 19 | string: string 20 | } 21 | 22 | const _getState = (seconds: number) => ({ 23 | seconds: typeof seconds === 'undefined' ? 0 : seconds, 24 | string: convertSecondsToMMSSString(seconds) 25 | }) 26 | 27 | export default class MinuteSecondInput extends Component { 28 | componentWillMount () { 29 | this.setState(_getState(this.props.seconds)) 30 | } 31 | 32 | componentWillReceiveProps (nextProps: Props) { 33 | if (typeof nextProps.seconds !== 'undefined' && this.state.seconds !== nextProps.seconds) { 34 | this.setState(_getState(nextProps.seconds)) 35 | } 36 | } 37 | 38 | _onChange = (evt: SyntheticInputEvent) => { 39 | const {onChange} = this.props 40 | const {value} = evt.target 41 | const seconds = convertMMSSStringToSeconds(value) 42 | if (seconds === this.state.seconds) { 43 | this.setState({string: value}) 44 | } else { 45 | this.setState({seconds, string: value}) 46 | onChange && onChange(seconds) 47 | } 48 | } 49 | 50 | render () { 51 | const {seconds, string} = this.state 52 | return ( 53 | 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/editor/components/ZoneSelect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, {Component} from 'react' 4 | import Select from 'react-select' 5 | 6 | import type {ZoneOption} from '../../types' 7 | 8 | type Props = { 9 | addCreateOption?: boolean, 10 | onChange: ?ZoneOption => void, 11 | options: Array, 12 | placeholder: string, 13 | value: ?ZoneOption 14 | } 15 | 16 | type State = { 17 | value: any 18 | } 19 | 20 | export default class ZoneSelect extends Component { 21 | static defaultProps = { 22 | placeholder: 'Select zone ID...' 23 | } 24 | 25 | state = { 26 | value: null 27 | } 28 | 29 | /** 30 | * NOTE: this method adds "Create new zone" custom option when using 31 | * ZoneSelect to edit stop entities. 32 | */ 33 | _filterZoneOptions = (options: Array, filter: string, values: Array) => { 34 | // Filter options on already selected values 35 | const valueKeys = values && values.map(i => i.value) 36 | let filteredOptions: Array = valueKeys 37 | ? options.filter(option => valueKeys.indexOf(option.value) === -1) 38 | : options 39 | if (filter !== undefined && filter != null && filter.length > 0) { 40 | // Filter options on labels 41 | filteredOptions = filteredOptions 42 | .filter(option => RegExp(filter, 'ig').test(option.label)) 43 | } 44 | // Append Addition option 45 | if (filteredOptions.length === 0) { 46 | filteredOptions.push({ 47 | label: Create new zone: {filter}, 48 | value: filter, 49 | create: true 50 | }) 51 | } 52 | return filteredOptions 53 | } 54 | 55 | _onChange = (option: ZoneOption) => { 56 | const value = option ? option.value : null 57 | this.setState({value}) 58 | } 59 | 60 | render () { 61 | const {addCreateOption, onChange, placeholder, value, options} = this.props 62 | return ( 63 |