├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
36 | {feed.name}
37 |
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 |
33 | {fetched
34 | ? 'New Alert'
35 | : Fetching alerts
36 | }
37 |
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 | {mode.name}
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 |
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 |
65 | )
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/lib/common/components/Loading.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import Icon from '@conveyal/woonerf/components/icon'
4 | import React, {Component} from 'react'
5 | import { Row, Col } from 'react-bootstrap'
6 |
7 | type Props = {
8 | style?: {[string]: string | number}
9 | }
10 |
11 | export default class Loading extends Component {
12 | render () {
13 | const {style} = this.props
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/common/components/Login.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {Component} from 'react'
4 |
5 | import * as userActions from '../../manager/actions/user'
6 | import auth0 from '../user/Auth0Manager'
7 |
8 | import type {Props as ContainerProps} from '../containers/Login'
9 |
10 | type Props = ContainerProps & {
11 | onHide: () => void,
12 | push: string => void,
13 | receiveTokenAndProfile: typeof userActions.receiveTokenAndProfile,
14 | redirectOnSuccess: string
15 | }
16 |
17 | export default class Auth0 extends Component {
18 | componentDidMount () {
19 | // Show lock when component mounts. NOTE: render method returns null.
20 | auth0.loginWithLock(this.props)
21 | }
22 |
23 | componentWillUnmount () {
24 | // Hide lock when component unmounts
25 | auth0.hideLock()
26 | }
27 |
28 | render () {
29 | return null
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/common/components/OptionButton.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import * as React from 'react'
4 | import {Button, OverlayTrigger} from 'react-bootstrap'
5 |
6 | type Props = {
7 | active?: boolean,
8 | children: React.Node,
9 | onClick?: any => any,
10 | onDeselect?: any => any,
11 | tooltip?: React.Node,
12 | value: string | number | boolean
13 | }
14 |
15 | export default class OptionButton extends React.Component {
16 | _onClick = () => {
17 | const {active, onClick, onDeselect, value} = this.props
18 | if (!active) {
19 | onClick && onClick(value)
20 | } else {
21 | onDeselect && onDeselect(value)
22 | }
23 | }
24 |
25 | render () {
26 | const {children, tooltip, ...other} = this.props
27 | const button = (
28 |
32 | {children}
33 |
34 | )
35 | if (tooltip) {
36 | return (
37 |
40 | {button}
41 |
42 | )
43 | } else {
44 | return button
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/common/components/PageNotFound.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import * as React from 'react'
4 | import {connect} from 'react-redux'
5 | import {Grid, Row, Col} from 'react-bootstrap'
6 |
7 | import ManagerPage from './ManagerPage'
8 | import PublicPage from '../../public/components/PublicPage'
9 | import {Link} from 'react-router'
10 |
11 | import type {AppState, ManagerUserState, RouterProps} from '../../types/reducers'
12 |
13 | class PageNotFound extends React.Component<{user: ManagerUserState}> {
14 | render () {
15 | const {user} = this.props
16 | // Return content in public wrapper or manager app wrapper depending on
17 | // whether user logged in.
18 | const component = user.profile ? ManagerPage : PublicPage
19 | return React.createElement(component, {ref: 'page'},
20 |
21 |
22 |
23 | Page Not Found.
24 |
25 | Go to{' '}
26 | home page
27 |
28 |
29 |
30 |
31 | )
32 | }
33 | }
34 |
35 | const mapStateToProps = (state: AppState, ownProps: RouterProps) => {
36 | return {
37 | user: state.user
38 | }
39 | }
40 |
41 | const mapDispatchToProps = {}
42 |
43 | export default connect(
44 | mapStateToProps,
45 | mapDispatchToProps
46 | )(PageNotFound)
47 |
--------------------------------------------------------------------------------
/lib/common/components/StatusMessage.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React from 'react'
4 | import { Button } from 'react-bootstrap'
5 |
6 | import type {Props as ContainerProps} from '../containers/CurrentStatusMessage'
7 |
8 | type Props = ContainerProps & {
9 | message: ?string,
10 | sidebarExpanded: boolean
11 | }
12 |
13 | type State = {visible: boolean}
14 |
15 | export default class StatusMessage extends React.Component {
16 | state = {
17 | visible: true
18 | }
19 |
20 | clear = () => {
21 | this.setState({
22 | visible: false
23 | })
24 | }
25 |
26 | componentWillReceiveProps (newProps: Props) {
27 | this.setState({
28 | visible: true
29 | })
30 | }
31 |
32 | render () {
33 | const { message, sidebarExpanded } = this.props
34 | const styles = {
35 | position: 'fixed',
36 | left: sidebarExpanded ? '140px' : '60px',
37 | bottom: '0px',
38 | height: '60px',
39 | zIndex: 1000
40 | }
41 |
42 | return (
43 |
44 | {message && this.state.visible
45 | ?
49 | {message}
50 |
51 | : null
52 | }
53 |
54 | )
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lib/common/components/TimezoneSelect.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, {Component} from 'react'
4 | import { shallowEqual } from 'react-pure-render'
5 | import Select from 'react-select'
6 |
7 | import {getComponentMessages} from '../util/config'
8 | import timezones from '../util/timezones'
9 |
10 | type Option = {label: string, value: string}
11 |
12 | type Props = {
13 | clearable?: boolean,
14 | onChange: Option => void,
15 | placeholder?: string,
16 | value: ?string
17 | }
18 |
19 | type State = {value: ?(string | Option)}
20 |
21 | export default class TimezoneSelect extends Component {
22 | messages = getComponentMessages('TimezoneSelect')
23 |
24 | componentWillMount () {
25 | this.setState({
26 | value: this.props.value
27 | })
28 | }
29 |
30 | componentWillReceiveProps (nextProps: Props) {
31 | if (!shallowEqual(nextProps.value, this.props.value)) {
32 | this.setState({value: nextProps.value})
33 | }
34 | }
35 |
36 | onChange = (value: Option) => {
37 | const {onChange} = this.props
38 | this.setState({value})
39 | onChange && onChange(value)
40 | }
41 |
42 | render () {
43 | const {clearable, placeholder} = this.props
44 | const options = timezones.map(tz => ({value: tz, label: tz}))
45 | return (
46 |
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 |
29 | My account
30 |
31 |
32 | {isSiteAdmin && (
33 |
34 |
39 | Site Admin
40 |
41 |
42 | )}
43 |
48 | Logout
49 |
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 |
35 | {isStarred
36 | ? {this.messages('unstar')}
37 | : {this.messages('star')}
38 | }
39 |
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 |
59 | {this._getLabel()}
60 |
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 |
71 |
72 |
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 |
72 | )
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/editor/components/pattern/TripPatternViewer.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, {Component} from 'react'
4 |
5 | import EditableTextField from '../../../common/components/EditableTextField'
6 | import EditShapePanel from './EditShapePanel'
7 | import EditSchedulePanel from './EditSchedulePanel'
8 | import CalculateDefaultTimesForm from './CalculateDefaultTimesForm'
9 | import PatternStopsPanel from './PatternStopsPanel'
10 |
11 | import type {Props} from './TripPatternList'
12 |
13 | export default class TripPatternViewer extends Component {
14 | savePatternName = (name: string) => {
15 | const {activePattern, saveActiveGtfsEntity, updateActiveGtfsEntity} = this.props
16 | updateActiveGtfsEntity({
17 | component: 'trippattern',
18 | entity: activePattern,
19 | props: {name}
20 | })
21 | saveActiveGtfsEntity('trippattern')
22 | }
23 |
24 | render () {
25 | const {
26 | activeEntity,
27 | activePattern,
28 | activePatternId,
29 | activePatternTripCount,
30 | deleteAllTripsForPattern,
31 | feedSource,
32 | saveActiveGtfsEntity,
33 | setActiveEntity,
34 | showConfirmModal,
35 | updateActiveGtfsEntity
36 | } = this.props
37 | if (!activePattern) return null
38 | return (
39 |
40 |
44 |
45 |
56 |
57 |
58 |
59 | )
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/editor/components/timetable/TimetableHelpModal.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, {Component} from 'react'
4 | import {Modal, Button} from 'react-bootstrap'
5 |
6 | import {getComponentMessages} from '../../../common/util/config'
7 | import {SHORTCUTS} from '../../util/timetable'
8 |
9 | type Props = {
10 | onClose: () => void,
11 | show: boolean
12 | }
13 |
14 | type State = {
15 | showModal: boolean
16 | }
17 |
18 | export default class TimetableHelpModal extends Component {
19 | messages = getComponentMessages('TimetableHelpModal')
20 |
21 | componentWillMount () {
22 | this.setState({
23 | showModal: this.props.show
24 | })
25 | }
26 |
27 | _close = () => {
28 | const {onClose} = this.props
29 | onClose && onClose()
30 | }
31 |
32 | render () {
33 | const {Body, Footer, Header, Title} = Modal
34 | return (
35 |
38 |
39 | {this.messages('title')}
40 |
41 |
42 | {Object.keys(SHORTCUTS).map(key => {
43 | return (
44 |
45 |
{this.messages(`shortcuts.${key}.title`)}
46 |
47 | {SHORTCUTS[key].map((item, i) => {
48 | const keys = item.split(':')
49 | const extraKeys = keys.length === 3
50 | ? {' '}{keys[1]} {keys[2]}
51 | : ''
52 | return (
53 |
54 | {keys[0]} {extraKeys}:{' '}
55 | {this.messages(`shortcuts.${key}.desc.${i}`)}
56 |
57 | )
58 | })}
59 |
60 |
61 | )
62 | })}
63 |
64 |
65 |
66 | Press ? to view shortcuts
67 |
68 | Close
69 |
70 |
71 | )
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/editor/constants/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export const ENTITY = {
4 | // TODO: use these constants for component names
5 | STOP: 'stop',
6 | ROUTE: 'route',
7 | TRIP_PATTERN: 'trippattern',
8 |
9 | // For constructing new entities (before saving to server)
10 | NEW_ID: -2
11 | }
12 |
13 | export const POINT_TYPE = Object.freeze({
14 | DEFAULT: 0,
15 | ANCHOR: 1,
16 | STOP: 2
17 | })
18 |
--------------------------------------------------------------------------------
/lib/editor/containers/ActiveEditorFeedSourcePanel.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 | import {
5 | fetchSnapshots,
6 | restoreSnapshot,
7 | deleteSnapshot,
8 | downloadSnapshot,
9 | createSnapshot
10 | } from '../actions/snapshots.js'
11 | import {createFeedVersionFromSnapshot} from '../../manager/actions/versions'
12 |
13 | import EditorFeedSourcePanel from '../components/EditorFeedSourcePanel'
14 |
15 | import type {Feed, Project} from '../../types'
16 | import type {AppState} from '../../types/reducers'
17 |
18 | export type Props = {
19 | feedSource: Feed,
20 | project: Project
21 | }
22 |
23 | const mapStateToProps = (state: AppState, ownProps: Props) => {
24 | const {user} = state
25 | return {
26 | user
27 | }
28 | }
29 |
30 | const mapDispatchToProps = {
31 | createFeedVersionFromSnapshot,
32 | createSnapshot,
33 | deleteSnapshot,
34 | downloadSnapshot,
35 | fetchSnapshots,
36 | restoreSnapshot
37 | }
38 |
39 | const ActiveEditorFeedSourcePanel = connect(
40 | mapStateToProps,
41 | mapDispatchToProps
42 | )(EditorFeedSourcePanel)
43 |
44 | export default ActiveEditorFeedSourcePanel
45 |
--------------------------------------------------------------------------------
/lib/editor/containers/ActiveEntityList.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import {
6 | deleteGtfsEntity,
7 | enterTimetableEditor,
8 | setActiveEntity,
9 | updateActiveGtfsEntity
10 | } from '../actions/active'
11 | import {cloneGtfsEntity, newGtfsEntity, updateEntitySort} from '../actions/editor'
12 | import {getTableById} from '../util/gtfs'
13 | import EntityList from '../components/EntityList'
14 | import {findProjectByFeedSource} from '../../manager/util'
15 | import {getActiveEntityList} from '../selectors'
16 |
17 | import type {AppState} from '../../types/reducers'
18 |
19 | export type Props = {
20 | activeComponent: string,
21 | activeEntityId: number,
22 | feedSourceId: string,
23 | showConfirmModal: any,
24 | width: number
25 | }
26 |
27 | const mapStateToProps = (state: AppState, ownProps: Props) => {
28 | const {activeComponent, feedSourceId} = ownProps
29 | const {sort, tables} = state.editor.data
30 |
31 | // Simplify active entity properties so that EntityList is not re-rendered when
32 | // any fields on active entity are edited
33 | const routes = getTableById(tables, 'route')
34 | const hasRoutes = routes && routes.length > 0
35 | const list = getActiveEntityList(state)
36 | const activeEntity = list.find(entity => entity.isActive)
37 | const entities = activeComponent && getTableById(tables, activeComponent)
38 | const project = findProjectByFeedSource(state.projects.all, feedSourceId)
39 | const feedSource = project && project.feedSources && project.feedSources.find(fs => fs.id === feedSourceId)
40 |
41 | return {
42 | activeEntity,
43 | entities,
44 | feedSource,
45 | hasRoutes,
46 | list,
47 | sort
48 | }
49 | }
50 |
51 | const mapDispatchToProps = {
52 | cloneGtfsEntity,
53 | deleteGtfsEntity,
54 | enterTimetableEditor,
55 | newGtfsEntity,
56 | setActiveEntity,
57 | updateActiveGtfsEntity,
58 | updateEntitySort
59 | }
60 |
61 | const ActiveEntityList = connect(mapStateToProps, mapDispatchToProps)(EntityList)
62 |
63 | export default ActiveEntityList
64 |
--------------------------------------------------------------------------------
/lib/editor/containers/ActiveFeedInfoPanel.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import {setActiveEntity} from '../actions/active'
6 | import {createSnapshot, fetchSnapshots, restoreSnapshot} from '../actions/snapshots'
7 | import {displayRoutesShapefile} from '../actions/map'
8 | import FeedInfoPanel from '../components/FeedInfoPanel'
9 | import {findProjectByFeedSource} from '../../manager/util'
10 | import {getTableById} from '../util/gtfs'
11 |
12 | import type {AppState} from '../../types/reducers'
13 |
14 | export type Props = {
15 | feedSourceId: string,
16 | showConfirmModal: any
17 | }
18 |
19 | const mapStateToProps = (state: AppState, ownProps: Props) => {
20 | const {feedSourceId} = ownProps
21 | const project = findProjectByFeedSource(state.projects.all, feedSourceId)
22 | const feedSource = project && project.feedSources && project.feedSources.find(fs => fs.id === feedSourceId)
23 | const feedInfo = getTableById(state.editor.data.tables, 'feedinfo')[0]
24 | return {
25 | feedInfo,
26 | feedSourceId,
27 | feedSource,
28 | project
29 | }
30 | }
31 |
32 | const mapDispatchToProps = {
33 | createSnapshot,
34 | displayRoutesShapefile,
35 | fetchSnapshots,
36 | restoreSnapshot,
37 | setActiveEntity
38 | }
39 |
40 | const ActiveFeedInfoPanel = connect(mapStateToProps, mapDispatchToProps)(FeedInfoPanel)
41 |
42 | export default ActiveFeedInfoPanel
43 |
--------------------------------------------------------------------------------
/lib/editor/reducers/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { combineReducers } from 'redux'
4 | import {handleActions} from 'redux-actions'
5 | import undoable, {includeAction} from 'redux-undo'
6 |
7 | import data from './data'
8 | import * as settings from './settings'
9 | import * as mapState from './mapState'
10 | import timetable from './timetable'
11 |
12 | export default combineReducers({
13 | data,
14 | editSettings: undoable(
15 | handleActions(settings.reducers, settings.defaultState),
16 | { undoType: 'UNDO_TRIP_PATTERN_EDITS',
17 | filter: includeAction(['UPDATE_PATTERN_GEOMETRY']),
18 | clearHistoryType: [
19 | 'TOGGLE_PATTERN_EDITING',
20 | 'SAVED_TRIP_PATTERN'
21 | ],
22 | initialState: settings.defaultState
23 | }
24 | ),
25 | mapState: handleActions(mapState.reducers, mapState.defaultState),
26 | timetable
27 | })
28 |
--------------------------------------------------------------------------------
/lib/editor/reducers/mapState.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import update from 'react-addons-update'
4 | import {latLngBounds} from 'leaflet'
5 | import type {ActionType} from 'redux-actions'
6 |
7 | import {receivedRoutesShapefile, updateMapSetting} from '../actions/map'
8 | import {receiveFeedSource} from '../../manager/actions/feeds'
9 | import { getFeedBounds } from '../util/map'
10 |
11 | import type {MapState} from '../../types/reducers'
12 |
13 | export const defaultState = {
14 | zoom: null,
15 | bounds: latLngBounds([[60, 60], [-60, -20]]), // entire globe
16 | routesGeojson: null,
17 | target: null
18 | }
19 |
20 | export const reducers = {
21 | 'RECEIVE_FEEDSOURCE' (
22 | state: MapState,
23 | action: ActionType
24 | ): MapState {
25 | if (action.payload) {
26 | return update(state, {
27 | bounds: {$set: latLngBounds(getFeedBounds(action.payload))},
28 | target: {$set: action.payload && action.payload.id}
29 | })
30 | } else {
31 | return state
32 | }
33 | },
34 | 'RECEIVED_ROUTES_SHAPEFILE' (
35 | state: MapState,
36 | action: ActionType
37 | ) {
38 | return update(state, {
39 | routesGeojson: {$set: action.payload.geojson}
40 | })
41 | },
42 | 'UPDATE_MAP_SETTING' (
43 | state: MapState,
44 | action: ActionType
45 | ) {
46 | const updatedState = {}
47 | for (const key in action.payload) {
48 | if (key === 'bounds' && !action.payload[key]) {
49 | // do nothing. Setting bounds to null would cause an error for Leaflet
50 | } else {
51 | updatedState[key] = {$set: action.payload[key]}
52 | }
53 | }
54 | if (!('target' in action.payload)) {
55 | // If no target present in payload, set to null (no target to focus on)
56 | updatedState.target = {$set: null}
57 | }
58 | return update(state, updatedState)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/lib/editor/reducers/settings.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import update from 'react-addons-update'
4 | import type {ActionType} from 'redux-actions'
5 |
6 | import {settingActiveGtfsEntity, updateEditSetting} from '../actions/active'
7 | import {
8 | controlPointDragOrEnd,
9 | updatePatternGeometry,
10 | updateTempPatternGeometry
11 | } from '../actions/map'
12 | import { CLICK_OPTIONS } from '../util'
13 |
14 | import type {EditSettingsState} from '../../types/reducers'
15 |
16 | export const defaultState = {
17 | addStops: false,
18 | afterIntersection: true,
19 | controlPoints: null,
20 | currentDragId: null,
21 | distanceFromIntersection: 5,
22 | editGeometry: false,
23 | followStreets: true,
24 | hideInactiveSegments: false,
25 | intersectionStep: 2,
26 | onMapClick: CLICK_OPTIONS[0],
27 | patternSegments: null,
28 | shapePoints: null,
29 | showStops: true,
30 | showTooltips: true,
31 | hideStopHandles: true,
32 | stopInterval: 400
33 | }
34 |
35 | export const reducers = {
36 | 'CONTROL_POINT_DRAG_START_OR_END' (
37 | state: EditSettingsState,
38 | action: ActionType
39 | ): EditSettingsState {
40 | return update(state, {
41 | currentDragId: {$set: action.payload}
42 | })
43 | },
44 | 'SETTING_ACTIVE_GTFS_ENTITY' (
45 | state: EditSettingsState,
46 | action: ActionType
47 | ): EditSettingsState {
48 | return defaultState
49 | },
50 | 'UPDATE_TEMP_PATTERN_GEOMETRY' (
51 | state: EditSettingsState,
52 | action: ActionType
53 | ): EditSettingsState {
54 | return update(state, {
55 | controlPoints: {$set: action.payload.controlPoints},
56 | patternSegments: {$set: action.payload.patternSegments}
57 | })
58 | },
59 | 'UPDATE_PATTERN_GEOMETRY' (
60 | state: EditSettingsState,
61 | action: ActionType
62 | ): EditSettingsState {
63 | return update(state, {
64 | controlPoints: {$set: action.payload.controlPoints},
65 | patternSegments: {$set: action.payload.patternSegments}
66 | })
67 | },
68 | 'UPDATE_EDIT_SETTING' (
69 | state: EditSettingsState,
70 | action: ActionType
71 | ): EditSettingsState {
72 | const {setting, value} = action.payload
73 | return update(state, { [setting]: {$set: value} })
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/editor/util/__tests__/fixtures/mapzen-response-delete-middle-control-point.json:
--------------------------------------------------------------------------------
1 | {
2 | "trip": {
3 | "language": "en-US",
4 | "summary": {
5 | "max_lon": -77.922523,
6 | "max_lat": 42.096169,
7 | "time": 27,
8 | "length": 0.428,
9 | "min_lat": 42.09235,
10 | "min_lon": -77.923058
11 | },
12 | "locations": [{
13 | "lon": -77.922523,
14 | "lat": 42.09235,
15 | "type": "break"
16 | },
17 | {
18 | "lon": -77.923058,
19 | "lat": 42.096169,
20 | "type": "break"
21 | }
22 | ],
23 | "units": "kilometers",
24 | "legs": [{
25 | "shape": "{rbhoAvl_ssC_gAbGgNz@gIl@oNjAcVjBqeA|I}^fD",
26 | "summary": {
27 | "max_lon": -77.922523,
28 | "max_lat": 42.096169,
29 | "time": 27,
30 | "length": 0.428,
31 | "min_lat": 42.09235,
32 | "min_lon": -77.923058
33 | },
34 | "maneuvers": [{
35 | "travel_mode": "drive",
36 | "begin_shape_index": 0,
37 | "length": 0.428,
38 | "time": 27,
39 | "type": 1,
40 | "end_shape_index": 7,
41 | "instruction": "Drive north on Stannards Road/NY 19.",
42 | "verbal_pre_transition_instruction": "Drive north on Stannards Road, New York 19 for 400 meters.",
43 | "travel_type": "bus",
44 | "street_names": [
45 | "Stannards Road",
46 | "NY 19"
47 | ]
48 | },
49 | {
50 | "travel_type": "bus",
51 | "travel_mode": "drive",
52 | "begin_shape_index": 7,
53 | "time": 0,
54 | "type": 4,
55 | "end_shape_index": 7,
56 | "instruction": "You have arrived at your destination.",
57 | "length": 0,
58 | "verbal_transition_alert_instruction": "You will arrive at your destination.",
59 | "verbal_pre_transition_instruction": "You have arrived at your destination."
60 | }
61 | ]
62 | }],
63 | "status_message": "Found route between points",
64 | "status": 0
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/editor/util/__tests__/fixtures/mapzen-response-update-last-stop.json:
--------------------------------------------------------------------------------
1 | {
2 | "trip": {
3 | "language": "en-US",
4 | "summary": {
5 | "max_lon": -77.923058,
6 | "max_lat": 42.097988,
7 | "time": 12,
8 | "length": 0.209,
9 | "min_lat": 42.096169,
10 | "min_lon": -77.923637
11 | },
12 | "locations": [{
13 | "lon": -77.923058,
14 | "lat": 42.096169,
15 | "type": "break"
16 | },
17 | {
18 | "side_of_street": "right",
19 | "lon": -77.922623,
20 | "lat": 42.098282,
21 | "type": "break"
22 | }
23 | ],
24 | "units": "kilometers",
25 | "legs": [{
26 | "shape": "oajhoAbn`ssCqRzAu^rFuUbFs_@xMeFxB",
27 | "summary": {
28 | "max_lon": -77.923058,
29 | "max_lat": 42.097988,
30 | "time": 12,
31 | "length": 0.209,
32 | "min_lat": 42.096169,
33 | "min_lon": -77.923637
34 | },
35 | "maneuvers": [{
36 | "travel_mode": "drive",
37 | "begin_shape_index": 0,
38 | "length": 0.209,
39 | "time": 12,
40 | "type": 1,
41 | "end_shape_index": 5,
42 | "instruction": "Drive north on Stannards Road/NY 19.",
43 | "verbal_pre_transition_instruction": "Drive north on Stannards Road, New York 19 for 200 meters.",
44 | "travel_type": "bus",
45 | "street_names": [
46 | "Stannards Road",
47 | "NY 19"
48 | ]
49 | },
50 | {
51 | "travel_type": "bus",
52 | "travel_mode": "drive",
53 | "begin_shape_index": 5,
54 | "time": 0,
55 | "type": 5,
56 | "end_shape_index": 5,
57 | "instruction": "Your destination is on the right.",
58 | "length": 0,
59 | "verbal_transition_alert_instruction": "Your destination will be on the right.",
60 | "verbal_pre_transition_instruction": "Your destination is on the right."
61 | }
62 | ]
63 | }],
64 | "status_message": "Found route between points",
65 | "status": 0
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/lib/editor/util/__tests__/fixtures/test-control-points-with-extra-point-at-end.json:
--------------------------------------------------------------------------------
1 | [{
2 | "distance": 0,
3 | "id": "2zu5",
4 | "permanent": true,
5 | "point": {
6 | "type": "Feature",
7 | "properties": {},
8 | "geometry": {
9 | "type": "Point",
10 | "coordinates": [-122.00743318, 37.06121322]
11 | }
12 | },
13 | "stopId": "aad0f374-0f29-4300-ae05-6affbe0f1562",
14 | "defaultTravelTime": 0,
15 | "defaultDwellTime": 0,
16 | "timepoint": null,
17 | "shapeDistTraveled": 0,
18 | "hidden": true
19 | },
20 | {
21 | "distance": 170.7245980700509,
22 | "id": "25p4",
23 | "permanent": true,
24 | "point": {
25 | "type": "Feature",
26 | "properties": {},
27 | "geometry": {
28 | "type": "Point",
29 | "coordinates": [-122.00872656844231, 37.060454715231224]
30 | }
31 | }
32 | },
33 | {
34 | "distance": 341.4491961401018,
35 | "id": "cy46",
36 | "permanent": true,
37 | "point": {
38 | "type": "Feature",
39 | "properties": {},
40 | "geometry": {
41 | "type": "Point",
42 | "coordinates": [-122.00981702504365, 37.059191439360475]
43 | }
44 | },
45 | "stopId": "ec6a3479-9795-4480-9884-1c25851a163e",
46 | "defaultTravelTime": 0,
47 | "defaultDwellTime": 0,
48 | "timepoint": null,
49 | "shapeDistTraveled": 341.4491961401018,
50 | "hidden": true
51 | },
52 | {
53 | "distance": 358.0806866950734,
54 | "id": "4s05",
55 | "permanent": true,
56 | "point": {
57 | "type": "Feature",
58 | "properties": {},
59 | "geometry": {
60 | "type": "Point",
61 | "coordinates": [-122.009919, 37.059066]
62 | }
63 | }
64 | }
65 | ]
66 |
--------------------------------------------------------------------------------
/lib/editor/util/__tests__/fixtures/test-control-points.json:
--------------------------------------------------------------------------------
1 | [{
2 | "distance": 0,
3 | "id": "bv67",
4 | "permanent": true,
5 | "point": {
6 | "type": "Feature",
7 | "properties": {},
8 | "geometry": {
9 | "type": "Point",
10 | "coordinates": [-77.922203, 42.086407]
11 | }
12 | },
13 | "stopId": "56c69e57-b007-4367-94ed-2c224662a2af",
14 | "defaultTravelTime": 0,
15 | "defaultDwellTime": 0,
16 | "timepoint": null,
17 | "shapeDistTraveled": 0,
18 | "hidden": true
19 | }, {
20 | "distance": 345.8645390523855,
21 | "id": "y1e5",
22 | "permanent": true,
23 | "point": {
24 | "type": "Feature",
25 | "properties": {},
26 | "geometry": {
27 | "type": "Point",
28 | "coordinates": [-77.92215494185281, 42.08925407008939]
29 | }
30 | }
31 | }, {
32 | "distance": 691.729078104771,
33 | "id": "fyz2",
34 | "permanent": true,
35 | "point": {
36 | "type": "Feature",
37 | "properties": {},
38 | "geometry": {
39 | "type": "Point",
40 | "coordinates": [-77.92252574804179, 42.09235127309673]
41 | }
42 | },
43 | "stopId": "a9c9cd55-ca3f-4ce7-9a93-96f2f4eaf089",
44 | "defaultTravelTime": 120,
45 | "defaultDwellTime": 0,
46 | "timepoint": null,
47 | "shapeDistTraveled": 691.729078104771,
48 | "hidden": true
49 | }, {
50 | "distance": 905.1384059344052,
51 | "id": "0cj3",
52 | "permanent": true,
53 | "point": {
54 | "type": "Feature",
55 | "properties": {},
56 | "geometry": {
57 | "type": "Point",
58 | "coordinates": [-77.92276015989871, 42.09426187381858]
59 | }
60 | }
61 | }, {
62 | "distance": 1118.5477337640395,
63 | "id": "1gwo",
64 | "permanent": true,
65 | "point": {
66 | "type": "Feature",
67 | "properties": {},
68 | "geometry": {
69 | "type": "Point",
70 | "coordinates": [-77.92305462145765, 42.096168018547736]
71 | }
72 | },
73 | "stopId": "5b6afe4d-f4f2-43f3-810e-91749420af53",
74 | "defaultTravelTime": 240,
75 | "defaultDwellTime": 0,
76 | "timepoint": null,
77 | "shapeDistTraveled": 1118.5477337640395,
78 | "hidden": true
79 | }]
80 |
--------------------------------------------------------------------------------
/lib/editor/util/__tests__/fixtures/test-pattern-shape.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "LineString",
3 | "coordinates": [
4 | [-77.922203, 42.086407],
5 | [-77.922196, 42.086498],
6 | [-77.922173, 42.086498],
7 | [-77.92183700000001, 42.086513000000004],
8 | [-77.92188300000001, 42.086956],
9 | [-77.921929, 42.087368],
10 | [-77.922043, 42.088386],
11 | [-77.922112, 42.088844],
12 | [-77.922135, 42.089099],
13 | [-77.92231799999999, 42.090522],
14 | [-77.92235600000001, 42.090847000000004],
15 | [-77.922432, 42.091491000000005],
16 | [-77.922509, 42.092201],
17 | [-77.922654, 42.093502],
18 | [-77.922684, 42.093745999999996],
19 | [-77.922707, 42.09391],
20 | [-77.92274499999999, 42.094158],
21 | [-77.922799, 42.094528000000004],
22 | [-77.922974, 42.095657],
23 | [-77.923104, 42.096481]
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/lib/editor/util/__tests__/gtfs.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {getEntityName} from '../gtfs'
4 |
5 | describe('editor > util > gtfs', () => {
6 | describe('getEntityName', () => {
7 | describe('routes', () => {
8 | it('should get name for route with no names', () => {
9 | // cast input to any flow type cause I'm lazy and don't want to
10 | // add all variables when they aren't needed
11 | expect(getEntityName(({
12 | route_id: '1'
13 | }: any))).toEqual('[no name]')
14 | })
15 |
16 | it('should get name for route with just short name', () => {
17 | // cast input to any flow type cause I'm lazy and don't want to
18 | // add all variables when they aren't needed
19 | expect(getEntityName(({
20 | route_id: '1',
21 | route_short_name: 'short name'
22 | }: any))).toEqual('short name')
23 | })
24 |
25 | it('should get name for route with just long name', () => {
26 | // cast input to any flow type cause I'm lazy and don't want to
27 | // add all variables when they aren't needed
28 | expect(getEntityName(({
29 | route_id: '1',
30 | route_long_name: 'long name'
31 | }: any))).toEqual('long name')
32 | })
33 |
34 | it('should get name for route with both short and long name', () => {
35 | // cast input to any flow type cause I'm lazy and don't want to
36 | // add all variables when they aren't needed
37 | expect(getEntityName(({
38 | route_id: '1',
39 | route_long_name: 'long name',
40 | route_short_name: 'short name'
41 | }: any))).toEqual('short name - long name')
42 | })
43 | })
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/lib/editor/util/types.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export const FIELD_PROPS = [
4 | {
5 | inputType: 'DROPDOWN',
6 | props: {
7 | componentClass: 'select'
8 | }
9 | },
10 | {
11 | inputType: 'NUMBER',
12 | props: {
13 | type: 'number'
14 | }
15 | },
16 | {
17 | inputType: 'POSITIVE_INT',
18 | props: {
19 | min: 0,
20 | step: 1,
21 | type: 'number'
22 | }
23 | },
24 | {
25 | inputType: 'POSITIVE_NUM',
26 | props: {
27 | min: 0,
28 | type: 'number'
29 | }
30 | },
31 | {
32 | inputType: 'TIME',
33 | props: {
34 | placeholder: 'HH:MM:SS'
35 | }
36 | },
37 | {
38 | inputType: 'LATITUDE',
39 | props: {
40 | type: 'number'
41 | }
42 | },
43 | {
44 | inputType: 'LONGITUDE',
45 | props: {
46 | type: 'number'
47 | }
48 | }
49 | ]
50 |
--------------------------------------------------------------------------------
/lib/editor/util/ui.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export type GtfsIcon = {
4 | addable: boolean,
5 | hideSidebar?: boolean,
6 | icon: string,
7 | id: string,
8 | label: string,
9 | tableName: string,
10 | title: string
11 | }
12 |
13 | export const GTFS_ICONS = [
14 | {
15 | id: 'feedinfo',
16 | tableName: 'feedinfo',
17 | icon: 'info',
18 | addable: false,
19 | title: 'Edit feed info',
20 | label: 'Feed Info'
21 | },
22 | {
23 | id: 'agency',
24 | tableName: 'agency',
25 | icon: 'building',
26 | addable: true,
27 | title: 'Edit agencies',
28 | label: 'Agencies'
29 | },
30 | {
31 | id: 'route',
32 | tableName: 'routes',
33 | icon: 'bus',
34 | addable: true,
35 | title: 'Edit routes',
36 | label: 'Routes'
37 | },
38 | {
39 | id: 'stop',
40 | tableName: 'stops',
41 | icon: 'map-marker',
42 | addable: true,
43 | title: 'Edit stops',
44 | label: 'Stops'
45 | },
46 | {
47 | id: 'calendar',
48 | tableName: 'calendar',
49 | icon: 'calendar',
50 | addable: true,
51 | title: 'Edit calendars',
52 | label: 'Calendars'
53 | },
54 | {
55 | id: 'scheduleexception',
56 | tableName: 'scheduleexception',
57 | icon: 'ban',
58 | addable: true,
59 | hideSidebar: true,
60 | title: 'Edit schedule exceptions',
61 | label: 'Schedule Exceptions'
62 | },
63 | {
64 | id: 'fare',
65 | tableName: 'fare',
66 | icon: 'ticket',
67 | addable: true,
68 | title: 'Edit fares',
69 | label: 'Fares'
70 | }
71 | ]
72 |
--------------------------------------------------------------------------------
/lib/gtfs/actions/shapes.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {createAction, type ActionType} from 'redux-actions'
4 |
5 | import {createVoidPayloadAction, secureFetch} from '../../common/actions'
6 | import {compose, shapes} from '../../gtfs/util/graphql'
7 | import {updateRoutesOnMapDisplay} from './filter'
8 |
9 | import type {dispatchFn, getStateFn} from '../../types/reducers'
10 |
11 | export const errorFetchingShapes = createVoidPayloadAction(
12 | 'FETCH_GRAPHQL_SHAPES_REJECTED'
13 | )
14 | export const fetchingShapes = createVoidPayloadAction('FETCH_GRAPHQL_SHAPES')
15 | export const receiveShapes = createAction(
16 | 'FETCH_GRAPHQL_SHAPES_FULFILLED',
17 | (payload: Array<[number, number]>) => payload
18 | )
19 |
20 | export type GtfsShapesActions = ActionType |
21 | ActionType |
22 | ActionType
23 |
24 | export function toggleShowAllRoutesOnMap (namespace: string) {
25 | return function (dispatch: dispatchFn, getState: getStateFn) {
26 | let state = getState()
27 | dispatch(updateRoutesOnMapDisplay(!state.gtfs.filter.showAllRoutesOnMap))
28 | state = getState()
29 | if (
30 | state.gtfs.filter.showAllRoutesOnMap &&
31 | !state.gtfs.shapes.fetchStatus.fetched &&
32 | !state.gtfs.shapes.fetchStatus.fetching
33 | ) {
34 | dispatch(fetchingShapes())
35 | return dispatch(secureFetch(compose(shapes, {namespace})))
36 | .then(response => response.json())
37 | .then(json => {
38 | const {patterns} = json.data.feed
39 | const shapes = patterns.map(pattern => {
40 | return pattern.shape.map(point => [
41 | point.shape_pt_lat,
42 | point.shape_pt_lon
43 | ])
44 | })
45 | dispatch(receiveShapes(shapes))
46 | })
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/gtfs/components/ShowAllRoutesOnMapFilter.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, {Component} from 'react'
4 | import {Checkbox} from 'react-bootstrap'
5 |
6 | import * as shapesActions from '../actions/shapes'
7 |
8 | import type {Props as ContainerProps} from '../containers/ShowAllRoutesOnMapFilter'
9 |
10 | type Props = ContainerProps & {
11 | showAllRoutesOnMap: boolean,
12 | toggleShowAllRoutesOnMap: typeof shapesActions.toggleShowAllRoutesOnMap
13 | }
14 |
15 | export default class ShowAllRoutesOnMapFilter extends Component {
16 | render () {
17 | const {
18 | showAllRoutesOnMap,
19 | toggleShowAllRoutesOnMap
20 | } = this.props
21 |
22 | return (
23 |
27 | Show all routes
28 |
29 | )
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/gtfs/components/StopMarker.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import Icon from '@conveyal/woonerf/components/icon'
4 | import React, {Component} from 'react'
5 | import { Marker, Popup } from 'react-leaflet'
6 | import { Button } from 'react-bootstrap'
7 | import { divIcon } from 'leaflet'
8 |
9 | import TransferPerformance from './TransferPerformance'
10 |
11 | import type {GtfsStop, StopWithFeed} from '../../types'
12 |
13 | type Props = {
14 | newEntityId: ?number,
15 | onStopClick: ?(GtfsStop, any, ?number) => void,
16 | popupAction: ?string,
17 | renderTransferPerformance: ?boolean,
18 | routes: Array,
19 | stop: StopWithFeed
20 | }
21 |
22 | export default class StopMarker extends Component {
23 | _onClick = () => {
24 | const {newEntityId, onStopClick, stop} = this.props
25 | const {feed} = stop
26 | onStopClick && onStopClick(stop, feed, newEntityId)
27 | }
28 |
29 | render () {
30 | const {stop, renderTransferPerformance, onStopClick, popupAction, routes} = this.props
31 | if (!stop) return null
32 | const busIcon = divIcon({
33 | html: `
34 |
35 |
36 | `,
37 | className: '',
38 | iconSize: [24, 24]
39 | })
40 | return (
41 |
45 |
46 |
47 |
{stop.stop_name} ({stop.stop_id})
48 | {renderTransferPerformance &&
}
49 | {onStopClick && (
50 |
54 | {popupAction} stop
55 |
56 | )}
57 |
58 |
59 |
60 | )
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib/gtfs/components/TransferPerformance.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, {Component} from 'react'
4 | import { ControlLabel, FormControl } from 'react-bootstrap'
5 | import moment from 'moment'
6 |
7 | type Props = {
8 | routes: any,
9 | stop: any
10 | }
11 |
12 | type State = {
13 | index: number
14 | }
15 |
16 | export default class TransferPerformance extends Component {
17 | state = {
18 | index: 0
19 | }
20 |
21 | renderTransferPerformanceResult (transferPerformance: ?any) {
22 | if (!transferPerformance) {
23 | return No transfers found
24 | }
25 | return (
26 |
27 | Typical case: {moment.duration(transferPerformance.typicalCase, 'seconds').humanize()}
28 | Best case: {moment.duration(transferPerformance.bestCase, 'seconds').humanize()}
29 | Worst case: {moment.duration(transferPerformance.worstCase, 'seconds').humanize()}
30 |
31 | )
32 | }
33 |
34 | _onChangeSelect = (evt: any) => {
35 | const index = +evt.target.value
36 | this.setState({index})
37 | }
38 |
39 | render () {
40 | const { stop, routes } = this.props
41 | return stop.transferPerformance && stop.transferPerformance.length
42 | ?
43 | Transfer performance
44 |
48 | {stop.transferPerformance
49 | .map((summary, index) => {
50 | const fromRoute = routes.find(r => r.route_id === summary.fromRoute)
51 | const toRoute = routes.find(r => r.route_id === summary.toRoute)
52 | return (
53 |
56 | {fromRoute.route_short_name} to {toRoute.route_short_name}
57 |
58 | )
59 | })
60 | }
61 |
62 | {this.renderTransferPerformanceResult(stop.transferPerformance[this.state.index])}
63 |
64 | : No transfers found
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/gtfs/containers/GlobalGtfsFilter.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { connect } from 'react-redux'
4 |
5 | import GtfsFilter from '../components/GtfsFilter'
6 | import {
7 | addActiveFeed,
8 | removeActiveFeed,
9 | addAllActiveFeeds,
10 | removeAllActiveFeeds
11 | } from '../actions/filter'
12 | import {getActiveProject} from '../../manager/selectors'
13 | import {getActiveFeeds, getPublishedFeeds, getActiveAndLoadedFeeds, getAllFeeds} from '../../gtfs/selectors'
14 |
15 | import type {AppState} from '../../types/reducers'
16 |
17 | export type Props = {}
18 |
19 | const mapStateToProps = (state: AppState, ownProps: Props) => {
20 | return {
21 | activeFeeds: getActiveFeeds(state),
22 | allFeeds: getAllFeeds(state),
23 | activeAndLoadedFeeds: getActiveAndLoadedFeeds(state),
24 | loadedFeeds: getPublishedFeeds(state),
25 | user: state.user,
26 | project: getActiveProject(state)
27 | }
28 | }
29 |
30 | const mapDispatchToProps = {
31 | addActiveFeed,
32 | addAllActiveFeeds,
33 | removeActiveFeed,
34 | removeAllActiveFeeds
35 | }
36 |
37 | const GlobalGtfsFilter = connect(mapStateToProps, mapDispatchToProps)(GtfsFilter)
38 |
39 | export default GlobalGtfsFilter
40 |
--------------------------------------------------------------------------------
/lib/gtfs/containers/ShowAllRoutesOnMapFilter.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import {toggleShowAllRoutesOnMap} from '../actions/shapes'
6 | import ShowAllRoutesOnMapFilter from '../components/ShowAllRoutesOnMapFilter'
7 |
8 | import type {AppState} from '../../types/reducers'
9 |
10 | export type Props = {}
11 |
12 | const mapStateToProps = (state: AppState, ownProps: Props) => {
13 | return {
14 | showAllRoutesOnMap: state.gtfs.filter.showAllRoutesOnMap
15 | }
16 | }
17 |
18 | const mapDispatchToProps = {
19 | toggleShowAllRoutesOnMap
20 | }
21 |
22 | export default connect(
23 | mapStateToProps,
24 | mapDispatchToProps
25 | )(ShowAllRoutesOnMapFilter)
26 |
--------------------------------------------------------------------------------
/lib/gtfs/reducers/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { combineReducers } from 'redux'
4 |
5 | import filter from './filter'
6 | import patterns from './patterns'
7 | import routes from './routes'
8 | import shapes from './shapes'
9 | import stops from './stops'
10 | import timetables from './timetables'
11 | import validation from './validation'
12 |
13 | export default combineReducers({
14 | filter,
15 | patterns,
16 | routes,
17 | shapes,
18 | stops,
19 | timetables,
20 | validation
21 | })
22 |
--------------------------------------------------------------------------------
/lib/gtfs/reducers/shapes.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import type {Action} from '../../types/actions'
4 | import type {ShapesState} from '../../types/reducers'
5 |
6 | export const defaultState = {
7 | fetchStatus: {
8 | fetched: false,
9 | fetching: false,
10 | error: false
11 | },
12 | data: []
13 | }
14 |
15 | export default function reducer (
16 | state: ShapesState = defaultState,
17 | action: Action
18 | ): ShapesState {
19 | switch (action.type) {
20 | case 'SET_ACTIVE_FEEDVERSION':
21 | return defaultState
22 | case 'FETCH_GRAPHQL_SHAPES':
23 | return {
24 | fetchStatus: {
25 | fetched: false,
26 | fetching: true,
27 | error: false
28 | },
29 | data: []
30 | }
31 | case 'FETCH_GRAPHQL_SHAPES_REJECTED':
32 | return {
33 | fetchStatus: {
34 | fetched: false,
35 | fetching: false,
36 | error: true
37 | },
38 | data: []
39 | }
40 | case 'FETCH_GRAPHQL_SHAPES_FULFILLED':
41 | return {
42 | fetchStatus: {
43 | fetched: true,
44 | fetching: false,
45 | error: false
46 | },
47 | data: action.payload
48 | }
49 | default:
50 | return state
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lib/gtfs/reducers/stops.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import type {Action} from '../../types/actions'
4 | import type {StopsState} from '../../types/reducers'
5 |
6 | export const defaultState = {
7 | routeFilter: null,
8 | patternFilter: null,
9 | fetchStatus: {
10 | fetched: false,
11 | fetching: false,
12 | error: false
13 | },
14 | data: []
15 | }
16 |
17 | export default function reducer (
18 | state: StopsState = defaultState,
19 | action: Action
20 | ): StopsState {
21 | switch (action.type) {
22 | case 'SET_ACTIVE_FEEDVERSION':
23 | return defaultState
24 | case 'FETCH_GRAPHQL_STOPS':
25 | return {
26 | ...state,
27 | fetchStatus: {
28 | fetched: false,
29 | fetching: true,
30 | error: false
31 | },
32 | data: []
33 | }
34 | case 'FETCH_GRAPHQL_STOPS_REJECTED':
35 | return {
36 | ...state,
37 | fetchStatus: {
38 | fetched: false,
39 | fetching: false,
40 | error: true
41 | },
42 | data: []
43 | }
44 | case 'RECEIVED_GTFS_ELEMENTS':
45 | return {
46 | ...state,
47 | fetchStatus: {
48 | fetched: true,
49 | fetching: false,
50 | error: false
51 | },
52 | data: action.payload.stops
53 | }
54 | default:
55 | return state
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/gtfs/reducers/timetables.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import type {Action} from '../../types/actions'
4 | import type {TimetablesState} from '../../types/reducers'
5 |
6 | export const defaultState = {
7 | fetchStatus: {
8 | fetched: false,
9 | fetching: false,
10 | error: false
11 | },
12 | data: null
13 | }
14 |
15 | export default function reducer (
16 | state: TimetablesState = defaultState,
17 | action: Action
18 | ): TimetablesState {
19 | switch (action.type) {
20 | case 'SET_ACTIVE_FEEDVERSION':
21 | return defaultState
22 | case 'FETCH_GRAPHQL_TIMETABLES':
23 | return {
24 | fetchStatus: {
25 | fetched: false,
26 | fetching: true,
27 | error: false
28 | },
29 | data: null
30 | }
31 | case 'FETCH_GRAPHQL_TIMETABLES_REJECTED':
32 | return {
33 | fetchStatus: {
34 | fetched: false,
35 | fetching: false,
36 | error: true
37 | },
38 | data: null
39 | }
40 | case 'FETCH_GRAPHQL_TIMETABLES_FULFILLED':
41 | const {data} = action.payload
42 |
43 | return {
44 | fetchStatus: {
45 | fetched: true,
46 | fetching: false,
47 | error: false
48 | },
49 | data
50 | }
51 | default:
52 | return state
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/gtfs/reducers/validation.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import update from 'react-addons-update'
4 |
5 | import {getEntityGraphQLRoot, getEntityIdField} from '../../gtfs/util'
6 |
7 | import type {Action} from '../../types/actions'
8 | import type {ValidationState} from '../../types/reducers'
9 |
10 | export const defaultState = {
11 | fetchStatus: {
12 | fetched: false,
13 | fetching: false,
14 | error: false
15 | },
16 | data: {}
17 | }
18 |
19 | export default function reducer (
20 | state: ValidationState = defaultState,
21 | action: Action
22 | ): ValidationState {
23 | switch (action.type) {
24 | case 'SET_ACTIVE_FEEDVERSION':
25 | return defaultState
26 | case 'FETCH_GRAPHQL_ROUTES':
27 | return {
28 | fetchStatus: {
29 | fetched: false,
30 | fetching: true,
31 | error: false
32 | },
33 | data: {}
34 | }
35 | case 'FETCH_GRAPHQL_ROUTES_REJECTED':
36 | return update(state, {
37 | fetchStatus: {
38 | $set: {
39 | fetched: false,
40 | fetching: false,
41 | error: true
42 | }
43 | }
44 | })
45 | case 'RECEIVE_GTFS_ENTITIES':
46 | const {data, editor, component} = action.payload
47 | if (editor) {
48 | // Ignore entity fetches for the editor
49 | return state
50 | }
51 | const entities = data.feed[getEntityGraphQLRoot(component)]
52 | const lookup = {}
53 | entities.forEach(entity => {
54 | const id = entity[getEntityIdField(component)]
55 | lookup[`${component}:${id}`] = entity
56 | })
57 | return update(state, {
58 | data: {$merge: lookup}
59 | })
60 | default:
61 | return state
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/gtfs/selectors/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { createSelector } from 'reselect'
4 |
5 | import {getFeedsForPermission} from '../../common/util/permissions'
6 | import {getActiveProject} from '../../manager/selectors'
7 |
8 | import type {AppState} from '../../types/reducers'
9 |
10 | const getPermissionFilter = (state: AppState) => state.gtfs.filter.permissionFilter
11 |
12 | const getUser = (state: AppState) => state.user
13 |
14 | export const getAllFeeds: AppState => any = createSelector(
15 | [ getUser, getPermissionFilter, getActiveProject ],
16 | (user, filter, project) => {
17 | return getFeedsForPermission(project, user, filter)
18 | }
19 | )
20 |
21 | export const getPublishedFeeds = createSelector(
22 | [ getActiveProject ],
23 | (activeProject) => {
24 | return activeProject && activeProject.feedSources
25 | ? activeProject.feedSources.filter(feedSource => feedSource.publishedVersionId)
26 | : []
27 | }
28 | )
29 |
30 | export const getActiveFeeds: AppState => any = createSelector(
31 | [ getAllFeeds, state => state.gtfs.filter.activeFeeds ],
32 | (all, active) => {
33 | return all.filter((feed, index) => active && active[feed.id])
34 | }
35 | )
36 |
37 | export const getActiveAndLoadedFeeds: AppState => any = createSelector(
38 | [ getActiveFeeds, getPublishedFeeds ],
39 | (active, published) => {
40 | return active.filter(f => f && published.findIndex(feed => feed.id === f.id) !== -1)
41 | }
42 | )
43 |
--------------------------------------------------------------------------------
/lib/gtfs/util/__tests__/stats.js:
--------------------------------------------------------------------------------
1 | /* globals describe, expect, it */
2 |
3 | import * as stats from '../stats'
4 |
5 | describe('gtfs > util > stats', () => {
6 | it('formatSpeed should work', () => {
7 | expect(stats.formatSpeed(123)).toEqual('275')
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/lib/gtfs/util/stats.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export function formatHeadway (seconds: number): string {
4 | if (seconds > 0) {
5 | return '' + Math.round(seconds / 60)
6 | } else if (seconds === 0) {
7 | return '0'
8 | } else {
9 | return 'N/A'
10 | }
11 | }
12 |
13 | export function formatSpeed (metersPerSecond: number): string {
14 | return metersPerSecond >= 0 ? '' + Math.round(metersPerSecond * 2.236936) : 'N/A'
15 | }
16 |
--------------------------------------------------------------------------------
/lib/gtfsplus/components/GtfsPlusFieldHeader.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, {Component} from 'react'
4 | import {Glyphicon} from 'react-bootstrap'
5 |
6 | import type {GtfsPlusField, GtfsPlusTable} from '../../types'
7 |
8 | type Props = {
9 | field: GtfsPlusField,
10 | showHelpClicked: (string, ?string) => void,
11 | table: GtfsPlusTable
12 | }
13 |
14 | export default class GtfsPlusFieldHeader extends Component {
15 | _onClick = () => {
16 | this.props.showHelpClicked(this.props.table.id, this.props.field.name)
17 | }
18 |
19 | render () {
20 | const {field} = this.props
21 | return (
22 |
24 | {field.name}{field.required ? ' *' : ''}
25 |
30 |
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/gtfsplus/containers/ActiveGtfsPlusEditor.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import GtfsPlusEditor from '../components/GtfsPlusEditor'
6 | import {fetchFeedSourceAndProject} from '../../manager/actions/feeds'
7 | import {
8 | addGtfsPlusRow,
9 | updateGtfsPlusField,
10 | deleteGtfsPlusRow,
11 | uploadGtfsPlusFeed,
12 | downloadGtfsPlusFeed,
13 | loadGtfsEntities,
14 | receiveGtfsEntities,
15 | setActiveTable,
16 | setCurrentPage,
17 | setVisibilityFilter
18 | } from '../actions/gtfsplus'
19 | import {getVisibleRows, getFilteredPageCount} from '../selectors'
20 |
21 | import type {AppState, RouterProps} from '../../types/reducers'
22 |
23 | export type Props = RouterProps
24 |
25 | const mapStateToProps = (state: AppState, ownProps: Props) => {
26 | const {
27 | activeTableId,
28 | currentPage,
29 | gtfsEntityLookup,
30 | recordsPerPage,
31 | tableData,
32 | validation,
33 | visibility
34 | } = state.gtfsplus
35 | const {feedSourceId, feedVersionId} = ownProps.routeParams
36 | const {user} = state
37 | // find the containing project
38 | const project = state.projects.all
39 | ? state.projects.all.find(p => p.feedSources &&
40 | p.feedSources.findIndex(fs => fs.id === feedSourceId) !== -1
41 | )
42 | : null
43 | const feedSource = project && project.feedSources
44 | ? project.feedSources.find(fs => fs.id === feedSourceId)
45 | : null
46 |
47 | return {
48 | activeTableId,
49 | currentPage,
50 | feedSource,
51 | feedSourceId,
52 | feedVersionId,
53 | gtfsEntityLookup,
54 | pageCount: getFilteredPageCount(state),
55 | project,
56 | recordsPerPage,
57 | tableData,
58 | user,
59 | validation,
60 | visibility,
61 | visibleRows: getVisibleRows(state)
62 | }
63 | }
64 |
65 | const mapDispatchToProps = {
66 | addGtfsPlusRow,
67 | deleteGtfsPlusRow,
68 | downloadGtfsPlusFeed,
69 | fetchFeedSourceAndProject,
70 | loadGtfsEntities,
71 | receiveGtfsEntities,
72 | setActiveTable,
73 | setCurrentPage,
74 | setVisibilityFilter,
75 | updateGtfsPlusField,
76 | uploadGtfsPlusFeed
77 | }
78 |
79 | const ActiveGtfsPlusEditor = connect(
80 | mapStateToProps,
81 | mapDispatchToProps
82 | )(GtfsPlusEditor)
83 |
84 | export default ActiveGtfsPlusEditor
85 |
--------------------------------------------------------------------------------
/lib/gtfsplus/containers/ActiveGtfsPlusVersionSummary.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import GtfsPlusVersionSummary from '../components/GtfsPlusVersionSummary'
6 |
7 | import {downloadGtfsPlusFeed, publishGtfsPlusFeed} from '../actions/gtfsplus'
8 |
9 | import type {FeedVersion} from '../../types'
10 | import type {AppState} from '../../types/reducers'
11 |
12 | export type Props = {
13 | version: FeedVersion
14 | }
15 |
16 | const mapStateToProps = (state: AppState, ownProps: Props) => {
17 | return {
18 | gtfsplus: state.gtfsplus,
19 | user: state.user
20 | }
21 | }
22 |
23 | const mapDispatchToProps = {
24 | downloadGtfsPlusFeed,
25 | publishGtfsPlusFeed
26 | }
27 |
28 | const ActiveGtfsPlusVersionSummary = connect(
29 | mapStateToProps,
30 | mapDispatchToProps
31 | )(GtfsPlusVersionSummary)
32 |
33 | export default ActiveGtfsPlusVersionSummary
34 |
--------------------------------------------------------------------------------
/lib/gtfsplus/reducers/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import gtfsplus from './gtfsplus'
4 |
5 | export default { gtfsplus }
6 |
--------------------------------------------------------------------------------
/lib/gtfsplus/selectors/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { createSelector } from 'reselect'
4 |
5 | import type {AppState, ValidationIssue} from '../../types/reducers'
6 |
7 | const getActiveTable = createSelector(
8 | [
9 | (state: AppState) => state.gtfsplus.activeTableId,
10 | (state: AppState) => state.gtfsplus.tableData
11 | ],
12 | (tableId, tableData): ?any => {
13 | return tableData && tableData[tableId]
14 | }
15 | )
16 |
17 | const getTableValidation = (state: AppState): Array => {
18 | return state.gtfsplus.validation[state.gtfsplus.activeTableId] || []
19 | }
20 |
21 | export const getFilteredRows = createSelector(
22 | [
23 | (state: AppState) => state.gtfsplus.visibility,
24 | getActiveTable,
25 | getTableValidation
26 | ],
27 | (visibility, table, validation): Array => {
28 | switch (visibility) {
29 | case 'all':
30 | return table
31 | case 'validation':
32 | return table
33 | ? table.filter(record => (validation.find(v => v.rowIndex === record.origRowIndex)))
34 | : []
35 | default:
36 | return []
37 | }
38 | }
39 | )
40 |
41 | export const getFilteredPageCount = createSelector(
42 | [
43 | (state: AppState) => state.gtfsplus.recordsPerPage,
44 | getFilteredRows
45 | ],
46 | (recordsPerPage, rows): number => {
47 | const numRows = rows ? rows.length : 0
48 | return Math.ceil(numRows / recordsPerPage)
49 | }
50 | )
51 |
52 | export const getVisibleRows = createSelector(
53 | [
54 | (state: AppState) => state.gtfsplus.currentPage,
55 | (state: AppState) => state.gtfsplus.recordsPerPage,
56 | getFilteredRows
57 | ],
58 | (currentPage, recordsPerPage, rows): Array => {
59 | return rows
60 | ? rows.slice((currentPage - 1) * recordsPerPage,
61 | Math.min(currentPage * recordsPerPage, rows.length))
62 | : []
63 | }
64 | )
65 |
--------------------------------------------------------------------------------
/lib/gtfsplus/util/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {getGtfsPlusSpec} from '../../common/util/config'
4 |
5 | export function constructNewGtfsPlusRow (tableId: string): { [string]: any } {
6 | const table = getGtfsPlusSpec()
7 | .find(t => t.id === tableId)
8 | if (typeof table === 'undefined') {
9 | throw new Error(`Could not find table '${tableId}' in GTFS+ spec`)
10 | }
11 | const rowData = {}
12 | for (const field of table.fields) {
13 | rowData[field.name] = null
14 | }
15 | return rowData
16 | }
17 |
--------------------------------------------------------------------------------
/lib/index.css:
--------------------------------------------------------------------------------
1 |
2 | @import url(node_modules/font-awesome/css/font-awesome.css);
3 | /**
4 | * Note: this css file must be imported so that marker icons URLs will work
5 | * properly with react-leaflet. See https://github.com/PaulLeCam/react-leaflet/issues/453
6 | */
7 | @import url(https://unpkg.com/leaflet@1.3.4/dist/leaflet.css);
8 |
9 | @import url(node_modules/bootstrap/dist/css/bootstrap.min.css);
10 | @import url(node_modules/react-bootstrap-table/dist/react-bootstrap-table.min.css);
11 | @import url(node_modules/react-bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css);
12 |
13 | @import url(node_modules/react-select/dist/react-select.css);
14 |
15 | @import url(node_modules/react-virtualized/styles.css);
16 | @import url(node_modules/react-virtualized-select/styles.css);
17 | @import url(node_modules/rc-slider/assets/index.css);
18 | @import url(node_modules/react-toggle/style.css);
19 |
20 | @import url(lib/style.css);
21 |
--------------------------------------------------------------------------------
/lib/main.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import mount from '@conveyal/woonerf/mount'
3 |
4 | import admin from './admin/reducers'
5 | import alerts from './alerts/reducers'
6 | import App from './common/containers/App'
7 | import editor from './editor/reducers'
8 | import gtfs from './gtfs/reducers'
9 | import * as gtfsPlusReducers from './gtfsplus/reducers'
10 | import * as managerReducers from './manager/reducers'
11 | import signs from './signs/reducers'
12 |
13 | mount({
14 | app: App,
15 | reducers: {
16 | ...managerReducers,
17 | admin,
18 | alerts,
19 | signs,
20 | ...gtfsPlusReducers,
21 | editor,
22 | gtfs
23 | }
24 | })
25 |
--------------------------------------------------------------------------------
/lib/manager/actions/__tests__/__snapshots__/user.js.snap.hold:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`manager > actions > user > checkLogin should work with expired token 1`] = `
4 | Array [
5 | Object {
6 | "payload": Object {
7 | "permissions": UserPermissions {
8 | "appPermissionLookup": Object {},
9 | "orgPermissionLookup": Object {},
10 | "organizationLookup": Object {},
11 | "projectLookup": Object {},
12 | },
13 | "profile": Object {
14 | "app_metadata": Object {
15 | "datatools": Array [],
16 | },
17 | },
18 | },
19 | "type": "USER_LOGGED_IN",
20 | },
21 | ]
22 | `;
23 |
24 | exports[`manager > actions > user > checkLogin should work with no token saved in localStorage 1`] = `
25 | Array [
26 | Object {
27 | "type": "CHECKING_EXISTING_LOGIN",
28 | },
29 | Object {
30 | "type": "USER_LOGGED_OUT",
31 | },
32 | ]
33 | `;
34 |
35 | exports[`manager > actions > user > checkLogin should work with unexpired token 1`] = `
36 | Object {
37 | "payload": Object {
38 | "permissions": UserPermissions {
39 | "appPermissionLookup": Object {},
40 | "orgPermissionLookup": Object {},
41 | "organizationLookup": Object {},
42 | "projectLookup": Object {},
43 | },
44 | "profile": Object {
45 | "app_metadata": Object {
46 | "datatools": Array [],
47 | },
48 | },
49 | },
50 | "type": "USER_LOGGED_IN",
51 | }
52 | `;
53 |
54 | exports[`manager > actions > user > receiveTokenAndProfile should receive token and profile 1`] = `
55 | Object {
56 | "payload": Object {
57 | "permissions": UserPermissions {
58 | "appPermissionLookup": Object {},
59 | "orgPermissionLookup": Object {},
60 | "organizationLookup": Object {},
61 | "projectLookup": Object {},
62 | },
63 | "profile": Object {
64 | "app_metadata": Object {
65 | "datatools": Array [],
66 | },
67 | },
68 | "token": "fake-token",
69 | },
70 | "type": "USER_LOGGED_IN",
71 | }
72 | `;
73 |
--------------------------------------------------------------------------------
/lib/manager/actions/languages.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {createAction, type ActionType} from 'redux-actions'
4 |
5 | export const setActiveLanguage = createAction(
6 | 'SET_ACTIVE_LANGUAGE',
7 | (payload: string) => payload
8 | )
9 |
10 | export type LanguageActions = ActionType
11 |
--------------------------------------------------------------------------------
/lib/manager/actions/ui.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {createAction, type ActionType} from 'redux-actions'
4 |
5 | import { updateUserMetadata } from './user'
6 |
7 | import type {dispatchFn, getStateFn} from '../../types/reducers'
8 |
9 | const settingSidebarExpanded = createAction(
10 | 'SETTING_SIDEBAR_EXPANDED',
11 | (payload: boolean) => payload
12 | )
13 | const settingTutorialVisibility = createAction(
14 | 'SETTING_TUTORIAL_VISIBILITY',
15 | (payload: boolean) => payload
16 | )
17 |
18 | export type UiActions = ActionType |
19 | ActionType
20 |
21 | export function setSidebarExpanded (value: boolean) {
22 | return function (dispatch: dispatchFn, getState: getStateFn) {
23 | dispatch(settingSidebarExpanded(value))
24 | dispatch(updateUserMetadata(getState().user.profile, {sidebarExpanded: value}))
25 | }
26 | }
27 |
28 | export function setTutorialHidden (value: boolean) {
29 | return function (dispatch: dispatchFn, getState: getStateFn) {
30 | dispatch(settingTutorialVisibility(value))
31 | dispatch(updateUserMetadata(getState().user.profile, {hideTutorial: value}))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/manager/actions/visibilityFilter.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {createAction, type ActionType} from 'redux-actions'
4 |
5 | export const setVisibilitySearchText = createAction(
6 | 'SET_PROJECT_VISIBILITY_SEARCH_TEXT',
7 | (payload: null | string) => payload
8 | )
9 | export const setVisibilityFilter = createAction(
10 | 'SET_PROJECT_VISIBILITY_FILTER',
11 | (payload: string) => payload
12 | )
13 |
14 | export type VisibilityFilterActions = ActionType |
15 | ActionType
16 |
--------------------------------------------------------------------------------
/lib/manager/components/CreateProject.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, {Component} from 'react'
4 | import {Col, Grid, Row} from 'react-bootstrap'
5 |
6 | import {createProject} from '../actions/projects'
7 | import ManagerPage from '../../common/components/ManagerPage'
8 | import ProjectSettingsForm from './ProjectSettingsForm'
9 |
10 | type Props = {
11 | createProject: typeof createProject
12 | }
13 |
14 | /**
15 | * A component to facilitate the creation of a new project.
16 | */
17 | export default class CreateProject extends Component {
18 | _saveProject = (projectId: string, data: Object) => {
19 | return this.props.createProject(data)
20 | }
21 |
22 | render () {
23 | return (
24 |
27 |
28 |
29 |
30 | Create New Project
31 |
37 |
38 |
39 |
40 |
41 | )
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/manager/components/ThirdPartySyncButton.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import Icon from '@conveyal/woonerf/components/icon'
4 | import React, {Component} from 'react'
5 | import { DropdownButton, MenuItem, Glyphicon } from 'react-bootstrap'
6 |
7 | import { isExtensionEnabled } from '../../common/util/config'
8 |
9 | type Props = {
10 | projectEditDisabled: boolean,
11 | thirdPartySync: string => void
12 | }
13 |
14 | export default class ThirdPartySyncButton extends Component {
15 | _onClickMTC = (evt: SyntheticMouseEvent) =>
16 | this.props.thirdPartySync('MTC')
17 |
18 | _onClickTransitLand = (evt: SyntheticMouseEvent) =>
19 | this.props.thirdPartySync('TRANSITLAND')
20 |
21 | _onClickTransitFeeds = (evt: SyntheticMouseEvent) =>
22 | this.props.thirdPartySync('TRANSITFEEDS')
23 |
24 | render () {
25 | const {projectEditDisabled} = this.props
26 | return (
27 | Sync}>
32 | {isExtensionEnabled('transitland')
33 | ?
38 | transit.land
39 |
40 | : null
41 | }
42 | {isExtensionEnabled('transitfeeds')
43 | ?
48 | transitfeeds.com
49 |
50 | : null
51 | }
52 | {isExtensionEnabled('mtc')
53 | ?
58 | MTC
59 |
60 | : null
61 | }
62 |
63 | )
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/lib/manager/components/UserAccountInfoPanel.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, {Component} from 'react'
4 | import { Panel, Badge, Row, Col } from 'react-bootstrap'
5 |
6 | import * as userActions from '../actions/user'
7 | import UserButtons from '../../common/components/UserButtons'
8 |
9 | import type {ManagerUserState} from '../../types/reducers'
10 |
11 | type Props = {
12 | logout: typeof userActions.logout,
13 | user: ManagerUserState
14 | }
15 |
16 | export default class UserAccountInfoPanel extends Component {
17 | render () {
18 | const {
19 | user,
20 | logout
21 | } = this.props
22 | const {profile, permissions} = user
23 | if (!profile || !permissions) {
24 | console.warn('Cannot find user profile/permissions in app state', user)
25 | return null
26 | }
27 | return (
28 |
29 |
30 |
31 |
35 |
36 |
37 |
38 | Hello, {profile.nickname}.
39 |
40 | {profile.email}
41 |
42 |
43 | {permissions.isApplicationAdmin()
44 | ? 'Application admin'
45 | : permissions.canAdministerAnOrganization()
46 | ? 'Organization admin'
47 | : 'Standard user'
48 | }
49 |
50 | {/* TODO: fetch organization for user and show badge here */}
51 | {' '}
52 | {/* userOrganization &&
53 |
54 | user.permissions.getOrganizationId()
55 |
56 | */}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | )
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/lib/manager/components/reporter/containers/ActiveDateTimeFilter.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import DateTimeFilter from '../components/DateTimeFilter'
6 | import {updateDateTimeFilter} from '../../../../gtfs/actions/filter'
7 |
8 | import type {FeedVersion} from '../../../../types'
9 | import type {AppState} from '../../../../types/reducers'
10 |
11 | export type Props = {
12 | hideDateTimeField?: boolean,
13 | onChange?: (any) => void,
14 | version: FeedVersion
15 | }
16 |
17 | const mapStateToProps = (state: AppState, ownProps: Props) => {
18 | return {
19 | dateTime: state.gtfs.filter.dateTimeFilter
20 | }
21 | }
22 |
23 | const mapDispatchToProps = {
24 | updateDateTimeFilter
25 | }
26 |
27 | const ActiveDateTimeFilter = connect(
28 | mapStateToProps,
29 | mapDispatchToProps
30 | )(DateTimeFilter)
31 |
32 | export default ActiveDateTimeFilter
33 |
--------------------------------------------------------------------------------
/lib/manager/components/reporter/containers/Patterns.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import PatternLayout from '../components/PatternLayout'
6 | import {updatePatternFilter} from '../../../../gtfs/actions/filter'
7 | import {
8 | patternDateTimeFilterChange,
9 | patternRouteFilterChange
10 | } from '../../../../gtfs/actions/patterns'
11 | import {fetchRoutes} from '../../../../gtfs/actions/routes'
12 | import {timetablePatternFilterChange} from '../../../../gtfs/actions/timetables'
13 | import {getPatternData} from '../../../selectors'
14 |
15 | import type {FeedVersion} from '../../../../types'
16 | import type {AppState} from '../../../../types/reducers'
17 |
18 | export type Props = {
19 | selectTab: string => void,
20 | version: FeedVersion
21 | }
22 |
23 | const mapStateToProps = (state: AppState, ownProps: Props) => {
24 | return {
25 | fetchStatus: state.gtfs.patterns.fetchStatus,
26 | routes: state.gtfs.routes.allRoutes,
27 | routeFilter: state.gtfs.filter.routeFilter,
28 | patternData: getPatternData(state)
29 | }
30 | }
31 |
32 | const mapDispatchToProps = {
33 | fetchRoutes,
34 | patternDateTimeFilterChange,
35 | patternRouteFilterChange,
36 | timetablePatternFilterChange,
37 | updatePatternFilter
38 | }
39 |
40 | const Patterns = connect(
41 | mapStateToProps,
42 | mapDispatchToProps
43 | )(PatternLayout)
44 |
45 | export default Patterns
46 |
--------------------------------------------------------------------------------
/lib/manager/components/reporter/containers/Routes.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import RouteLayout from '../components/RouteLayout'
6 | import {fetchRoutes, fetchRouteDetails, routeOffsetChange} from '../../../../gtfs/actions/routes'
7 | import {patternRouteFilterChange} from '../../../../gtfs/actions/patterns'
8 | import {getRouteData} from '../../../selectors'
9 |
10 | import type {FeedVersion} from '../../../../types'
11 | import type {AppState} from '../../../../types/reducers'
12 |
13 | export type Props = {
14 | selectTab: string => void,
15 | version: FeedVersion
16 | }
17 |
18 | const mapStateToProps = (state: AppState, ownProps: Props) => {
19 | const {namespace} = ownProps.version
20 | const {gtfs} = state
21 | const {filter, routes} = gtfs
22 | const {routeOffset} = filter
23 | const {data, fetchStatus} = routes.routeDetails
24 |
25 | return {
26 | fetchStatus,
27 | namespace,
28 | allRoutes: state.gtfs.routes.allRoutes,
29 | numRoutes: data ? data.numRoutes : 0,
30 | routeData: getRouteData(state),
31 | routeOffset
32 | }
33 | }
34 |
35 | const mapDispatchToProps = {
36 | fetchRouteDetails,
37 | fetchRoutes,
38 | patternRouteFilterChange,
39 | routeOffsetChange
40 | }
41 |
42 | const Routes = connect(
43 | mapStateToProps,
44 | mapDispatchToProps
45 | )(RouteLayout)
46 |
47 | export default Routes
48 |
--------------------------------------------------------------------------------
/lib/manager/components/reporter/containers/Stops.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import StopLayout from '../components/StopLayout'
6 | import {updatePatternFilter} from '../../../../gtfs/actions/filter'
7 | import {
8 | patternDateTimeFilterChange,
9 | patternRouteFilterChange
10 | } from '../../../../gtfs/actions/patterns'
11 | import {fetchRoutes} from '../../../../gtfs/actions/routes'
12 | import {getFilteredStops} from '../../../selectors'
13 |
14 | import type {FeedVersion} from '../../../../types'
15 | import type {AppState} from '../../../../types/reducers'
16 |
17 | export type Props = {
18 | selectTab: (string) => void,
19 | tableOptions: any,
20 | version: FeedVersion
21 | }
22 |
23 | const mapStateToProps = (state: AppState, ownProps: Props) => {
24 | const {gtfs} = state
25 | const {filter, patterns, routes} = gtfs
26 | const {patternFilter, routeFilter} = filter
27 |
28 | return {
29 | patternFilter,
30 | patterns: patterns.data.patterns,
31 | routeFilter,
32 | routes: routes.allRoutes,
33 | stops: getFilteredStops(state)
34 | }
35 | }
36 |
37 | const mapDispatchToProps = {
38 | fetchRoutes,
39 | patternDateTimeFilterChange,
40 | patternRouteFilterChange,
41 | updatePatternFilter
42 | }
43 |
44 | export default connect(
45 | mapStateToProps,
46 | mapDispatchToProps
47 | )(StopLayout)
48 |
--------------------------------------------------------------------------------
/lib/manager/components/reporter/containers/Timetables.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import TimetableLayout from '../components/TimetableLayout'
6 | import {fetchRoutes} from '../../../../gtfs/actions/routes'
7 | import {
8 | fetchTimetablesWithFilters,
9 | timetableDateTimeFilterChange,
10 | timetablePatternFilterChange,
11 | timetableRouteFilterChange,
12 | timetableShowArrivalToggle,
13 | timetableTimepointToggle
14 | } from '../../../../gtfs/actions/timetables'
15 | import {getTimetableData} from '../../../selectors'
16 |
17 | import type {FeedVersion} from '../../../../types'
18 | import type {AppState} from '../../../../types/reducers'
19 |
20 | export type Props = {
21 | selectTab: string => void,
22 | tableOptions: any,
23 | version: FeedVersion
24 | }
25 |
26 | const mapStateToProps = (state: AppState, ownProps: Props) => {
27 | const {gtfs} = state
28 | const {
29 | filter,
30 | patterns,
31 | routes,
32 | timetables
33 | } = gtfs
34 |
35 | return {
36 | filter,
37 | patterns,
38 | routes: routes.allRoutes,
39 | timetables,
40 | timetableTableData: getTimetableData(state)
41 | }
42 | }
43 |
44 | const mapDispatchToProps = {
45 | fetchRoutes,
46 | fetchTimetablesWithFilters,
47 | timetableDateTimeFilterChange,
48 | timetablePatternFilterChange,
49 | timetableRouteFilterChange,
50 | timetableShowArrivalToggle,
51 | timetableTimepointToggle
52 | }
53 |
54 | export default connect(mapStateToProps, mapDispatchToProps)(TimetableLayout)
55 |
--------------------------------------------------------------------------------
/lib/manager/components/version/VersionDateLabel.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, {Component} from 'react'
4 | import {Label} from 'react-bootstrap'
5 | import moment from 'moment'
6 |
7 | import type {FeedVersion} from '../../../types'
8 |
9 | type Props = {
10 | version: FeedVersion
11 | }
12 |
13 | export default class VersionDateLabel extends Component {
14 | render () {
15 | const {version} = this.props
16 | const {validationSummary: summary} = version
17 | if (!summary) return null
18 | const now = +moment().startOf('day')
19 | const start = +moment(summary.startDate)
20 | const end = +moment(summary.endDate)
21 | const future = start > now
22 | const expired = end < now
23 | const bsStyle = future ? 'info' : expired ? 'danger' : 'success'
24 | const text = future ? 'future' : expired ? 'expired' : 'active'
25 | return (
26 |
28 | {text}
29 |
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/manager/containers/ActiveDeploymentViewer.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import {
6 | addFeedVersion,
7 | deleteFeedVersion,
8 | deployToTarget,
9 | downloadDeployment,
10 | updateDeployment,
11 | updateVersionForFeedSource
12 | } from '../actions/deployments'
13 | import DeploymentViewer from '../components/DeploymentViewer'
14 |
15 | import type {Deployment, Feed, Project} from '../../types'
16 | import type {AppState} from '../../types/reducers'
17 |
18 | export type Props = {
19 | deployment: Deployment,
20 | deployments: Array,
21 | feedSources: Array,
22 | project: Project
23 | }
24 |
25 | const mapStateToProps = (state: AppState, ownProps: Props) => {
26 | const user = state.user
27 | return {
28 | user
29 | }
30 | }
31 |
32 | const mapDispatchToProps = {
33 | addFeedVersion,
34 | deleteFeedVersion,
35 | deployToTarget,
36 | downloadDeployment,
37 | updateDeployment,
38 | updateVersionForFeedSource
39 | }
40 |
41 | const ActiveDeploymentViewer = connect(
42 | mapStateToProps,
43 | mapDispatchToProps
44 | )(DeploymentViewer)
45 |
46 | export default ActiveDeploymentViewer
47 |
--------------------------------------------------------------------------------
/lib/manager/containers/ActiveProjectViewer.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import {
6 | setVisibilitySearchText,
7 | setVisibilityFilter
8 | } from '../actions/visibilityFilter'
9 | import {
10 | deleteProject,
11 | deployPublic,
12 | downloadFeedForProject,
13 | fetchFeedsForProject,
14 | onProjectViewerMount,
15 | thirdPartySync,
16 | updateProject
17 | } from '../actions/projects'
18 | import {
19 | createFeedSource,
20 | deleteFeedSource,
21 | runFetchFeed,
22 | updateFeedSource
23 | } from '../actions/feeds'
24 | import {
25 | fetchProjectDeployments,
26 | createDeployment,
27 | createDeploymentFromFeedSource,
28 | saveDeployment,
29 | deleteDeployment,
30 | updateDeployment
31 | } from '../actions/deployments'
32 | import {uploadFeed} from '../actions/versions'
33 | import ProjectViewer from '../components/ProjectViewer'
34 |
35 | import type {AppState, RouterProps} from '../../types/reducers'
36 |
37 | export type Props = RouterProps
38 |
39 | const mapStateToProps = (state: AppState, ownProps: Props) => {
40 | const {user} = state
41 | const {all, filter: visibilityFilter, isFetching} = state.projects
42 | const {
43 | projectId,
44 | subpage: activeComponent,
45 | subsubpage: activeSubComponent
46 | } = ownProps.routeParams
47 | const project = all ? all.find(p => p.id === projectId) : null
48 | return {
49 | project,
50 | projectId,
51 | visibilityFilter,
52 | activeComponent,
53 | activeSubComponent,
54 | user,
55 | isFetching
56 | }
57 | }
58 |
59 | const mapDispatchToProps = {
60 | createDeployment,
61 | createDeploymentFromFeedSource,
62 | createFeedSource,
63 | deleteDeployment,
64 | deleteFeedSource,
65 | deleteProject,
66 | deployPublic,
67 | downloadFeedForProject,
68 | fetchFeedsForProject,
69 | fetchProjectDeployments,
70 | onProjectViewerMount,
71 | runFetchFeed,
72 | saveDeployment,
73 | setVisibilityFilter,
74 | setVisibilitySearchText,
75 | thirdPartySync,
76 | updateDeployment,
77 | updateFeedSource,
78 | updateProject,
79 | uploadFeed
80 | }
81 |
82 | const ActiveProjectViewer = connect(mapStateToProps, mapDispatchToProps)(
83 | ProjectViewer
84 | )
85 |
86 | export default ActiveProjectViewer
87 |
--------------------------------------------------------------------------------
/lib/manager/containers/ActiveProjectsList.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import {fetchProjects, updateProject} from '../actions/projects'
6 | import {setVisibilitySearchText} from '../actions/visibilityFilter'
7 | import ProjectsList from '../components/ProjectsList'
8 | import {getProjects} from '../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 | projects: getProjects(state),
17 | user: state.user,
18 | visibilitySearchText: state.projects.filter.searchText
19 | }
20 | }
21 |
22 | const mapDispatchToProps = {
23 | fetchProjects,
24 | setVisibilitySearchText,
25 | updateProject
26 | }
27 |
28 | const ActiveProjectsList = connect(
29 | mapStateToProps,
30 | mapDispatchToProps
31 | )(ProjectsList)
32 |
33 | export default ActiveProjectsList
34 |
--------------------------------------------------------------------------------
/lib/manager/containers/ActiveUserHomePage.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import {logout, onUserHomeMount} from '../actions/user'
6 | import {fetchProjectFeeds} from '../actions/feeds'
7 | import {setVisibilitySearchText, setVisibilityFilter} from '../actions/visibilityFilter'
8 | import UserHomePage from '../components/UserHomePage'
9 |
10 | import type {AppState, RouterProps} from '../../types/reducers'
11 |
12 | export type Props = RouterProps
13 |
14 | const mapStateToProps = (state: AppState, ownProps: Props) => {
15 | const {projects, user} = state
16 | return {
17 | user,
18 | projects: projects.all
19 | ? projects.all.filter(p => p.isCreating ||
20 | (user.permissions && user.permissions.isApplicationAdmin()) ||
21 | (user.permissions && user.permissions.hasProject(p.id, p.organizationId)))
22 | : [],
23 | project: ownProps.routeParams.projectId && projects.all
24 | ? projects.all.find(p => p.id === ownProps.routeParams.projectId)
25 | : null,
26 | projectId: ownProps.routeParams.projectId,
27 | visibilityFilter: projects.filter
28 | }
29 | }
30 |
31 | const mapDispatchToProps = {
32 | fetchProjectFeeds,
33 | logout,
34 | onUserHomeMount,
35 | setVisibilityFilter,
36 | setVisibilitySearchText
37 | }
38 |
39 | const ActiveUserHomePage = connect(
40 | mapStateToProps,
41 | mapDispatchToProps
42 | )(UserHomePage)
43 |
44 | export default ActiveUserHomePage
45 |
--------------------------------------------------------------------------------
/lib/manager/containers/CreateProject.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { connect } from 'react-redux'
4 |
5 | import { createProject } from '../actions/projects'
6 | import CreateProject from '../components/CreateProject'
7 |
8 | const mapStateToProps = (state, ownProps) => {
9 | return {
10 | user: state.user
11 | }
12 | }
13 |
14 | const mapDispatchToProps = {
15 | createProject
16 | }
17 |
18 | export default connect(
19 | mapStateToProps,
20 | mapDispatchToProps
21 | )(CreateProject)
22 |
--------------------------------------------------------------------------------
/lib/manager/reducers/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import languages from './languages'
4 | import projects from './projects'
5 | import status from './status'
6 | import ui from './ui'
7 | import user from './user'
8 |
9 | export default {
10 | languages,
11 | projects,
12 | status,
13 | ui,
14 | user
15 | }
16 |
--------------------------------------------------------------------------------
/lib/manager/reducers/languages.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import update from 'react-addons-update'
4 | import { getConfigProperty } from '../../common/util/config'
5 |
6 | import type {Action} from '../../types/actions'
7 | import type {LanguagesState} from '../../types/reducers'
8 |
9 | export const defaultState = {
10 | all: getConfigProperty('messages.all'),
11 | // set active default to english
12 | active: getConfigProperty('messages.active')
13 | }
14 |
15 | const languages = (state: LanguagesState = defaultState, action: Action): LanguagesState => {
16 | let languageIndex
17 | switch (action.type) {
18 | case 'SET_ACTIVE_LANGUAGE':
19 | const language = action.payload
20 | languageIndex = state.all.findIndex(l => l.id === language)
21 | window.DT_CONFIG.messages.active = state.all[languageIndex]
22 | window.localStorage.setItem('lang', language)
23 | return update(state, {active: { $set: state.all[languageIndex] }})
24 | default:
25 | return state
26 | }
27 | }
28 |
29 | export default languages
30 |
--------------------------------------------------------------------------------
/lib/manager/reducers/ui.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import update from 'react-addons-update'
4 | import { getUserMetadataProperty } from '../../common/util/user'
5 |
6 | import type {UiState} from '../../types/reducers'
7 |
8 | export const defaultState = {
9 | sidebarExpanded: true,
10 | hideTutorial: false
11 | }
12 |
13 | const ui = (state: UiState = defaultState, action: any): UiState => {
14 | switch (action.type) {
15 | case 'USER_LOGGED_IN':
16 | const hideTutorial = getUserMetadataProperty(action.profile, 'hideTutorial')
17 | const sidebarExpanded = getUserMetadataProperty(action.profile, 'sidebarExpanded')
18 | return update(state, {
19 | sidebarExpanded: { $set: sidebarExpanded },
20 | hideTutorial: { $set: hideTutorial }
21 | })
22 | case 'SETTING_TUTORIAL_VISIBILITY':
23 | return update(state, { hideTutorial: { $set: action.value } })
24 | case 'SETTING_SIDEBAR_EXPANDED':
25 | return update(state, { sidebarExpanded: { $set: action.payload } })
26 | default:
27 | return state
28 | }
29 | }
30 |
31 | export default ui
32 |
--------------------------------------------------------------------------------
/lib/manager/reducers/user.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import UserPermissions from '../../common/user/UserPermissions'
4 | import UserSubscriptions from '../../common/user/UserSubscriptions'
5 |
6 | import type {Action} from '../../types/actions'
7 | import type {ManagerUserState} from '../../types/reducers'
8 |
9 | export const defaultState = {
10 | isCheckingLogin: true,
11 | token: null,
12 | profile: null,
13 | permissions: null,
14 | recentActivity: null,
15 | redirectOnSuccess: null,
16 | subscriptions: null
17 | }
18 |
19 | const user = (state: ManagerUserState = defaultState, action: Action): ManagerUserState => {
20 | switch (action.type) {
21 | case 'SET_REDIRECT_ON_LOGIN':
22 | return {...state, redirectOnSuccess: action.payload}
23 | case 'CHECKING_EXISTING_LOGIN':
24 | return {...state, isCheckingLogin: true}
25 | case 'USER_LOGGED_IN':
26 | return {
27 | ...state,
28 | isCheckingLogin: false,
29 | token: action.payload.token,
30 | profile: action.payload.profile,
31 | permissions: new UserPermissions(action.payload.profile.app_metadata.datatools),
32 | subscriptions: new UserSubscriptions(action.payload.profile.app_metadata.datatools)
33 | }
34 | case 'USER_PROFILE_UPDATED':
35 | return {
36 | ...state,
37 | profile: action.payload,
38 | permissions: new UserPermissions(action.payload.app_metadata.datatools),
39 | subscriptions: new UserSubscriptions(action.payload.app_metadata.datatools)
40 | }
41 | case 'REVOKE_USER_TOKEN':
42 | return {...state, token: null}
43 | case 'USER_LOGGED_OUT':
44 | return {
45 | ...state,
46 | isCheckingLogin: false,
47 | token: null,
48 | profile: null,
49 | permissions: null,
50 | subscriptions: null
51 | }
52 | case 'CREATED_PUBLIC_USER':
53 | return {
54 | ...state,
55 | profile: action.payload.profile,
56 | permissions: new UserPermissions(action.payload.profile.app_metadata.datatools),
57 | subscriptions: new UserSubscriptions(action.payload.profile.app_metadata.datatools)
58 | }
59 | case 'RECEIVE_USER_RECENT_ACTIVITY':
60 | return {...state, recentActivity: action.payload}
61 | default:
62 | return state
63 | }
64 | }
65 |
66 | export default user
67 |
--------------------------------------------------------------------------------
/lib/manager/util/version.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import moment from 'moment'
4 |
5 | import type {FeedVersion} from '../../types'
6 |
7 | export function getTableFatalExceptions (version: FeedVersion): Array {
8 | const tableFatalExceptions = []
9 | for (const key in version.feedLoadResult) {
10 | if (version.feedLoadResult.hasOwnProperty(key)) {
11 | const loadItem = version.feedLoadResult[key]
12 | if (loadItem && loadItem.fatalException) {
13 | tableFatalExceptions.push({
14 | tableName: key,
15 | ...loadItem
16 | })
17 | break
18 | }
19 | }
20 | }
21 | return tableFatalExceptions
22 | }
23 |
24 | /**
25 | * Get string containing comma-separated feed source names from versions list.
26 | */
27 | export function getFeedNames (versions: Array): string {
28 | return versions.map(version => version.feedSource.name).join(', ')
29 | }
30 |
31 | export function versionHasExpired (version: FeedVersion): boolean {
32 | return moment(version.validationResult.endDate).isBefore(moment())
33 | }
34 |
35 | export function versionHasNotBegun (version: FeedVersion): boolean {
36 | return moment(version.validationResult.startDate).isAfter(moment())
37 | }
38 |
--------------------------------------------------------------------------------
/lib/public/components/PublicPage.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, {Component, type Node} from 'react'
4 |
5 | import CurrentStatusMessage from '../../common/containers/CurrentStatusMessage'
6 | import ConfirmModal from '../../common/components/ConfirmModal'
7 | import SelectFileModal from '../../common/components/SelectFileModal'
8 | import Title from '../../common/components/Title'
9 | import ActivePublicHeader from '../containers/ActivePublicHeader'
10 | import { getConfigProperty } from '../../common/util/config'
11 |
12 | type Props = {
13 | children?: Node
14 | }
15 |
16 | export default class PublicPage extends Component {
17 | showConfirmModal (props: any) {
18 | this.refs.confirmModal.open(props)
19 | }
20 |
21 | showSelectFileModal (props: any) {
22 | this.refs.selectFileModal.open(props)
23 | }
24 |
25 | render () {
26 | const appTitle = getConfigProperty('application.title') || 'Data Tools'
27 | return (
28 |
29 |
{appTitle}
30 |
31 | {this.props.children}
32 |
33 |
34 |
35 |
36 | )
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/public/containers/ActivePublicFeedSourceViewer.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { connect } from 'react-redux'
4 |
5 | import PublicFeedSourceViewer from '../components/PublicFeedSourceViewer'
6 | import {
7 | fetchFeedSourceAndProject,
8 | updateFeedSource,
9 | runFetchFeed
10 | } from '../../manager/actions/feeds'
11 | import { fetchFeedVersions, uploadFeed } from '../../manager/actions/versions'
12 |
13 | import type {AppState, RouterProps} from '../../types/reducers'
14 |
15 | export type Props = RouterProps
16 |
17 | const mapStateToProps = (state: AppState, ownProps: Props) => {
18 | const feedSourceId = ownProps.routeParams.feedSourceId
19 | const user = state.user
20 | // find the containing project
21 | const project = state.projects.all
22 | ? state.projects.all.find(p => {
23 | if (!p.feedSources) return false
24 | return (p.feedSources.findIndex(fs => fs.id === feedSourceId) !== -1)
25 | })
26 | : null
27 |
28 | let feedSource
29 | if (project && project.feedSources) {
30 | feedSource = project.feedSources.find(fs => fs.id === feedSourceId)
31 | }
32 |
33 | return {
34 | feedSource,
35 | project,
36 | user
37 | }
38 | }
39 |
40 | const mapDispatchToProps = {
41 | fetchFeedSourceAndProject,
42 | fetchFeedVersions,
43 | runFetchFeed,
44 | updateFeedSource,
45 | uploadFeed
46 | }
47 |
48 | const ActivePublicFeedSourceViewer = connect(
49 | mapStateToProps,
50 | mapDispatchToProps
51 | )(PublicFeedSourceViewer)
52 |
53 | export default ActivePublicFeedSourceViewer
54 |
--------------------------------------------------------------------------------
/lib/public/containers/ActivePublicFeedsViewer.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { connect } from 'react-redux'
4 |
5 | import PublicFeedsViewer from '../components/PublicFeedsViewer'
6 | import { setVisibilitySearchText } from '../../manager/actions/visibilityFilter'
7 | import { fetchProjectsWithPublicFeeds } from '../../manager/actions/projects'
8 |
9 | import type {AppState, RouterProps} from '../../types/reducers'
10 |
11 | export type Props = RouterProps
12 |
13 | const mapStateToProps = (state: AppState, ownProps: Props) => {
14 | return {
15 | visibilitySearchText: state.projects.filter.searchText,
16 | projects: state.projects.all
17 | }
18 | }
19 |
20 | const mapDispatchToProps = {
21 | fetchProjectsWithPublicFeeds,
22 | setVisibilitySearchText
23 | }
24 |
25 | const ActivePublicFeedsViewer = connect(
26 | mapStateToProps,
27 | mapDispatchToProps
28 | )(PublicFeedsViewer)
29 |
30 | export default ActivePublicFeedsViewer
31 |
--------------------------------------------------------------------------------
/lib/public/containers/ActivePublicHeader.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { connect } from 'react-redux'
4 |
5 | import PublicHeader from '../components/PublicHeader'
6 |
7 | import { logout } from '../../manager/actions/user'
8 | import { getConfigProperty } from '../../common/util/config'
9 |
10 | import type {AppState} from '../../types/reducers'
11 |
12 | export type Props = {}
13 |
14 | const mapStateToProps = (state: AppState, ownProps: Props) => {
15 | return {
16 | title: getConfigProperty('application.title'),
17 | username: state.user.profile ? state.user.profile.email : null,
18 | userPicture: state.user.profile ? state.user.profile.picture : null
19 | }
20 | }
21 |
22 | const mapDispatchToProps = {
23 | logout
24 | }
25 |
26 | const ActivePublicHeader = connect(
27 | mapStateToProps,
28 | mapDispatchToProps
29 | )(PublicHeader)
30 |
31 | export default ActivePublicHeader
32 |
--------------------------------------------------------------------------------
/lib/public/containers/ActiveSignupPage.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { connect } from 'react-redux'
4 |
5 | import SignupPage from '../components/SignupPage'
6 | import { createPublicUser } from '../../manager/actions/user'
7 |
8 | import type {AppState, RouterProps} from '../../types/reducers'
9 |
10 | export type Props = RouterProps
11 |
12 | const mapStateToProps = (state: AppState, ownProps: Props) => ({})
13 |
14 | const mapDispatchToProps = {
15 | createPublicUser
16 | }
17 |
18 | const ActiveUserAccount = connect(
19 | mapStateToProps,
20 | mapDispatchToProps
21 | )(SignupPage)
22 |
23 | export default ActiveUserAccount
24 |
--------------------------------------------------------------------------------
/lib/public/containers/ActiveUserAccount.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import UserAccount from '../components/UserAccount'
6 | import {setVisibilitySearchText} from '../../manager/actions/visibilityFilter'
7 | import {fetchProjects} from '../../manager/actions/projects'
8 | import {fetchProjectDeployments} from '../../manager/actions/deployments'
9 | import {fetchProjectFeeds} from '../../manager/actions/feeds'
10 | import {
11 | updateUserData,
12 | updateTargetForSubscription,
13 | unsubscribeAll
14 | } from '../../manager/actions/user'
15 |
16 | import type {AppState, RouterProps} from '../../types/reducers'
17 |
18 | export type Props = RouterProps
19 |
20 | const mapStateToProps = (state: AppState, ownProps: Props) => {
21 | return {
22 | visibilitySearchText: state.projects.filter.searchText,
23 | projects: state.projects.all,
24 | user: state.user,
25 | activeComponent: ownProps.routeParams.subpage,
26 | projectId: ownProps.routeParams.projectId
27 | }
28 | }
29 |
30 | const mapDispatchToProps = {
31 | fetchProjectDeployments,
32 | fetchProjectFeeds,
33 | fetchProjects,
34 | setVisibilitySearchText,
35 | unsubscribeAll,
36 | updateTargetForSubscription,
37 | updateUserData
38 | }
39 |
40 | const ActiveUserAccount = connect(
41 | mapStateToProps,
42 | mapDispatchToProps
43 | )(UserAccount)
44 |
45 | export default ActiveUserAccount
46 |
--------------------------------------------------------------------------------
/lib/signs/actions/activeSign.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {createAction, type ActionType} from 'redux-actions'
4 |
5 | const addAffectedEntity = createAction(
6 | 'ADD_ACTIVE_SIGN_AFFECTED_ENTITY',
7 | (payload: {
8 | agency?: any,
9 | id: number,
10 | type: string
11 | }) => payload
12 | )
13 | export const deleteActiveEntity = createAction(
14 | 'DELETE_ACTIVE_SIGN_AFFECTED_ENTITY',
15 | (payload: any) => payload
16 | )
17 | export const toggleConfigForDisplay = createAction(
18 | 'TOGGLE_CONFIG_FOR_DISPLAY',
19 | (payload: {
20 | configId: string,
21 | configType: string,
22 | display: string
23 | }) => payload
24 | )
25 | export const updateActiveEntity = createAction(
26 | 'UPDATE_ACTIVE_SIGN_ENTITY',
27 | (payload: {
28 | agency: string,
29 | entity: any,
30 | field: string,
31 | value: any
32 | }) => payload
33 | )
34 | export const updateActiveSignProperty = createAction(
35 | 'UPDATE_ACTIVE_SIGN_PROPERTY',
36 | (payload: { key: string, value: any }) => payload
37 | )
38 |
39 | export type ActiveSignActions = ActionType |
40 | ActionType |
41 | ActionType |
42 | ActionType |
43 | ActionType
44 |
45 | let nextEntityId = 0
46 | export function addActiveEntity (
47 | field: string = 'AGENCY',
48 | value: ?any = null,
49 | agency: ?string = null,
50 | newEntityId: ?number = 0
51 | ) {
52 | nextEntityId++
53 | const newEntity = {}
54 | newEntity.id = newEntityId || nextEntityId
55 | newEntity.type = field
56 |
57 | // set agency of new entity
58 | if (agency) {
59 | newEntity.agency = agency
60 | }
61 | newEntity[field.toLowerCase()] = value
62 | return addAffectedEntity(newEntity)
63 | }
64 |
--------------------------------------------------------------------------------
/lib/signs/actions/visibilityFilter.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {createAction, type ActionType} from 'redux-actions'
4 |
5 | export const setVisibilityFilter = createAction(
6 | 'SET_SIGN_VISIBILITY_FILTER',
7 | (payload: string) => payload
8 | )
9 | export const setVisibilitySearchText = createAction(
10 | 'SET_SIGN_VISIBILITY_SEARCH_TEXT',
11 | (payload: string) => payload
12 | )
13 |
14 | export type SignVisibilityFilterActions = ActionType |
15 | ActionType
16 |
--------------------------------------------------------------------------------
/lib/signs/components/CreateSign.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 signsActions from '../actions/signs'
8 |
9 | type Props = {
10 | createSign: typeof signsActions.createSign,
11 | disabled: boolean,
12 | fetched: boolean
13 | }
14 |
15 | export default class CreateAlert extends Component {
16 | render () {
17 | const {
18 | createSign,
19 | disabled,
20 | fetched
21 | } = this.props
22 | const createDisabled = disabled != null
23 | ? disabled || !fetched
24 | : false
25 | return (
26 |
32 | {fetched
33 | ? 'New Configuration'
34 | : Fetching configurations
35 | }
36 |
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/signs/containers/ActiveSignEditor.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import {
6 | createDisplay,
7 | createSign,
8 | deleteSign,
9 | fetchRtdSigns,
10 | saveSign
11 | } from '../actions/signs'
12 | import {
13 | addActiveEntity,
14 | deleteActiveEntity,
15 | toggleConfigForDisplay,
16 | updateActiveEntity,
17 | updateActiveSignProperty
18 | } from '../actions/activeSign'
19 | import SignEditor from '../components/SignEditor'
20 | import {getFeedsForPermission} from '../../common/util/permissions'
21 | import {getActiveFeeds} from '../../gtfs/selectors'
22 | import {fetchProjects} from '../../manager/actions/projects'
23 | import {getActiveProject} from '../../manager/selectors'
24 |
25 | import type {AppState, RouterProps} from '../../types/reducers'
26 |
27 | export type Props = RouterProps
28 |
29 | const mapStateToProps = (state: AppState, ownProps: Props) => {
30 | return {
31 | sign: state.signs.active,
32 | activeFeeds: getActiveFeeds(state),
33 | project: getActiveProject(state),
34 | user: state.user,
35 | editableFeeds: getFeedsForPermission(getActiveProject(state), state.user, 'edit-etid'),
36 | permissionFilter: state.gtfs.filter.permissionFilter,
37 | publishableFeeds: getFeedsForPermission(getActiveProject(state), state.user, 'approve-etid')
38 | }
39 | }
40 |
41 | const mapDispatchToProps = {
42 | addActiveEntity,
43 | createDisplay,
44 | createSign,
45 | deleteActiveEntity,
46 | deleteSign,
47 | fetchProjects,
48 | fetchRtdSigns,
49 | saveSign,
50 | toggleConfigForDisplay,
51 | updateActiveEntity,
52 | updateActiveSignProperty
53 | }
54 |
55 | const ActiveSignEditor = connect(
56 | mapStateToProps,
57 | mapDispatchToProps
58 | )(SignEditor)
59 |
60 | export default ActiveSignEditor
61 |
--------------------------------------------------------------------------------
/lib/signs/containers/MainSignsViewer.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import SignsViewer from '../components/SignsViewer'
6 | import {createSign, fetchRtdSigns} from '../actions/signs'
7 | import {updatePermissionFilter} from '../../gtfs/actions/filter'
8 | import {getAllFeeds, getActiveFeeds} from '../../gtfs/selectors'
9 | import {fetchProjects} from '../../manager/actions/projects'
10 | import {getActiveProject} from '../../manager/selectors'
11 |
12 | import type {AppState, RouterProps} from '../../types/reducers'
13 |
14 | export type Props = RouterProps
15 |
16 | const mapStateToProps = (state: AppState, ownProps: Props) => {
17 | return {
18 | activeFeeds: getActiveFeeds(state),
19 | allFeeds: getAllFeeds(state),
20 | fetched: state.signs.fetched,
21 | permissionFilter: state.gtfs.filter.permissionFilter,
22 | project: getActiveProject(state),
23 | signs: state.signs.all,
24 | user: state.user
25 | }
26 | }
27 |
28 | const mapDispatchToProps = {
29 | createSign,
30 | fetchProjects,
31 | fetchRtdSigns,
32 | updatePermissionFilter
33 | }
34 |
35 | const MainSignsViewer = connect(
36 | mapStateToProps,
37 | mapDispatchToProps
38 | )(SignsViewer)
39 |
40 | export default MainSignsViewer
41 |
--------------------------------------------------------------------------------
/lib/signs/containers/VisibleSignsList.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import {connect} from 'react-redux'
4 |
5 | import {editSign, deleteSign} from '../actions/signs'
6 | import {setVisibilitySearchText, setVisibilityFilter} from '../actions/visibilityFilter'
7 | import {getFeedsForPermission} from '../../common/util/permissions'
8 | import SignsList from '../components/SignsList'
9 | import {getActiveProject} from '../../manager/selectors'
10 | import {getVisibleSigns} from '../selectors'
11 |
12 | import type {AppState} from '../../types/reducers'
13 |
14 | export type Props = {}
15 |
16 | const mapStateToProps = (state: AppState, ownProps: Props) => {
17 | const activeProject = getActiveProject(state)
18 | return {
19 | isFetching: state.signs.isFetching,
20 | signs: getVisibleSigns(state),
21 | visibilityFilter: state.signs.filter,
22 | editableFeeds: getFeedsForPermission(activeProject, state.user, 'edit-etid'),
23 | publishableFeeds: getFeedsForPermission(activeProject, state.user, 'approve-etid'),
24 | filterCounts: state.signs.counts
25 | }
26 | }
27 |
28 | const mapDispatchToProps = {
29 | editSign,
30 | deleteSign,
31 | setVisibilitySearchText,
32 | setVisibilityFilter
33 | }
34 |
35 | const VisibleSignsList = connect(mapStateToProps, mapDispatchToProps)(SignsList)
36 |
37 | export default VisibleSignsList
38 |
--------------------------------------------------------------------------------
/lib/signs/reducers/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import active from './active'
4 | import signs from './signs'
5 |
6 | export default signs.merge(active)
7 |
--------------------------------------------------------------------------------
/lib/signs/selectors/index.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect'
2 |
3 | import {filterSignsByCategory} from '../util'
4 |
5 | export const getVisibleSigns = createSelector(
6 | [state => state.signs.all, state => state.signs.filter],
7 | (signs, visibilityFilter) => {
8 | if (!signs) return []
9 | const visibleSigns = signs.filter(sign =>
10 | sign.title.toLowerCase().indexOf((visibilityFilter.searchText || '').toLowerCase()) !== -1)
11 | return filterSignsByCategory(visibleSigns, visibilityFilter.filter)
12 | }
13 | )
14 |
--------------------------------------------------------------------------------
/lib/signs/style.css:
--------------------------------------------------------------------------------
1 | .leaflet-top,
2 | .leaflet-bottom {
3 | z-index: 0;
4 | }
5 |
6 | .editable-cell:focus {
7 | background-color: '#FFC0CB'
8 | }
9 |
--------------------------------------------------------------------------------
/lib/signs/util/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import type {Sign} from '../../types'
4 |
5 | export const FILTERS = ['ALL', 'PUBLISHED', 'DRAFT']
6 |
7 | export function filterSignsByCategory (signs: Array, filter: string) {
8 | switch (filter) {
9 | case 'ALL':
10 | return signs
11 | case 'PUBLISHED':
12 | return signs.filter(sign => sign.published)
13 | case 'DRAFT':
14 | return signs.filter(sign => !sign.published)
15 | default:
16 | return signs
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Transit Data Tools Docs
2 | site_url: http://conveyal-data-tools.readthedocs.io
3 | repo_url: https://github.com/conveyal/datatools-manager
4 | docs_dir: docs
5 | site_dir: target/mkdocs
6 | theme: readthedocs
7 | extra_css: [style.css]
8 |
9 | pages:
10 | - Home: 'index.md'
11 | - User Guide:
12 | - Data Manager:
13 | - 'Introduction': 'user/introduction.md'
14 | - 'Managing Projects & Feeds': 'user/managing-projects-feeds.md'
15 | - 'Managing Users': 'user/managing-users.md'
16 | - 'Appendix: GTFS Validation Warnings': 'user/appendix-gtfs-warnings.md'
17 | - 'GTFS Editor':
18 | - Introduction: 'user/editor/introduction.md'
19 | - Routes: 'user/editor/routes.md'
20 | - Patterns: 'user/editor/patterns.md'
21 | - Stops: 'user/editor/stops.md'
22 | - Schedules: 'user/editor/schedules.md'
23 | - Calendars: 'user/editor/calendars.md'
24 | - Fares: 'user/editor/fares.md'
25 | - Development Guide:
26 | - Deployment: dev/deployment.md
27 | - Development: dev/development.md
28 | - Migration: dev/migration.md
29 |
--------------------------------------------------------------------------------
/scripts/load.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # load the database to a fresh server
3 | # usage: load.py dump.json http://localhost:9000
4 | # validation: curl -X POST http://localhost:9000/validateAll (add force=true query param to force validation)
5 | # if validating multiple large feeds, you may need to run application with more GBs, e.g. java -Xmx6G -jar target/datatools.jar
6 |
7 | from sys import argv
8 | import urllib2
9 |
10 | server = argv[2]
11 | # strip trailing slash to normalize url
12 | server = server if not server.endswith('/') else server[:-1]
13 |
14 | # TODO: don't load everything into RAM when loading
15 | inf = open(argv[1])
16 | dump = inf.read()
17 |
18 | print dump[0:79]
19 |
20 | req = urllib2.Request(server + '/load', dump, {'Content-Type': 'application/json', 'Content-Length': len(dump)})
21 | opener = urllib2.build_opener()
22 |
23 | try:
24 | opener.open(req)
25 | except urllib2.URLError, e:
26 | print e.reason
27 | print e.read()
28 |
--------------------------------------------------------------------------------
/scripts/loadLegacy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # load the database to a fresh server
3 | # usage: loadLegacy.py dump.json http://localhost:9000
4 |
5 | from sys import argv
6 | import urllib2
7 |
8 | server = argv[2]
9 | # strip trailing slash to normalize url
10 | server = server if not server.endswith('/') else server[:-1]
11 |
12 | # TODO: don't load everything into RAM when loading
13 | inf = open(argv[1])
14 | dump = inf.read()
15 |
16 | print dump[0:79]
17 |
18 | req = urllib2.Request(server + '/loadLegacy', dump, {'Content-Type': 'application/json', 'Content-Length': len(dump)})
19 | opener = urllib2.build_opener()
20 |
21 | try:
22 | opener.open(req)
23 | except urllib2.URLError, e:
24 | print e.code
25 | print e.read()
26 |
--------------------------------------------------------------------------------
/scripts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "auth0": "^2.7.0",
4 | "make-runnable": "^1.3.6",
5 | "request-promise-native": "^1.0.5"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------