From 9e1794c5e5eb6743a95a46aae595eb4106cebb59 Mon Sep 17 00:00:00 2001
From: Landon Reed
Date: Thu, 9 Aug 2018 08:49:53 -0400
Subject: [PATCH 01/18] refactor(user): do not throw error if invalid user
app_metadata encountered
---
lib/common/user/UserPermissions.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/common/user/UserPermissions.js b/lib/common/user/UserPermissions.js
index ba8407bb1..fece53b85 100644
--- a/lib/common/user/UserPermissions.js
+++ b/lib/common/user/UserPermissions.js
@@ -72,7 +72,7 @@ export default class UserPermissions {
}
}
} else {
- throw new Error('User app_metadata is misconfigured.')
+ console.warn('User does not have permissions for this application\'s client ID.', datatoolsApps)
}
}
From b6d6d5abadf408e95eeedbb09354454bbfed1280 Mon Sep 17 00:00:00 2001
From: Landon Reed
Date: Thu, 9 Aug 2018 08:51:34 -0400
Subject: [PATCH 02/18] refactor(user-admin): clean up user admin UI
Uses appropriate bootstrap components for x button. Changes icon from search -> refresh because a
user was having trouble figuring out how to view new users.
---
lib/admin/components/UserList.js | 94 ++++++++++++++++++++------------
1 file changed, 60 insertions(+), 34 deletions(-)
diff --git a/lib/admin/components/UserList.js b/lib/admin/components/UserList.js
index 4cf43abe0..af82200b1 100644
--- a/lib/admin/components/UserList.js
+++ b/lib/admin/components/UserList.js
@@ -1,34 +1,43 @@
+// @flow
+
import Icon from '@conveyal/woonerf/components/icon'
-import React, {PropTypes, Component} from 'react'
-import ReactDOM from 'react-dom'
+import React, {Component} from 'react'
import {Panel, Row, Col, Button, ButtonGroup, InputGroup, FormControl, ListGroup, ListGroupItem} from 'react-bootstrap'
import {getComponentMessages, getMessage} from '../../common/util/config'
import CreateUser from './CreateUser'
import UserRow from './UserRow'
-export default class UserList extends Component {
- static propTypes = {
- createUser: PropTypes.func,
- deleteUser: PropTypes.func,
- fetchProjectFeeds: PropTypes.func,
- isFetching: PropTypes.bool,
- page: PropTypes.number,
- perPage: PropTypes.number,
- projects: PropTypes.array,
- saveUser: PropTypes.func,
- setPage: PropTypes.func,
- setUserPermission: PropTypes.func,
- token: PropTypes.string,
- userCount: PropTypes.number,
- users: PropTypes.array,
- userSearch: PropTypes.func
- }
+type Props = {
+ createUser: () => void,
+ creatingUser: boolean,
+ deleteUser: () => void,
+ fetchProjectFeeds: () => void,
+ isFetching: boolean,
+ organizations: Array,
+ page: number,
+ perPage: number,
+ projects: Array,
+ saveUser: () => void,
+ setPage: number => void,
+ setUserPermission: () => void,
+ token: string,
+ userCount: number,
+ users: Array,
+ userSearch: string => void
+}
+
+type State = {
+ searchText: string
+}
- state = {}
+export default class UserList extends Component {
+ state = {
+ searchText: ''
+ }
_clearSearch = () => {
- ReactDOM.findDOMNode(this.refs.searchInput).value = ''
+ this.setState({searchText: ''})
this.props.userSearch('')
}
@@ -40,12 +49,19 @@ export default class UserList extends Component {
this.props.setPage(this.props.page + 1)
}
- _onSearchKeyUp = e => {
- if (e.keyCode === 13) this.userSearch()
+ _onSearchTextChange = (e: any) => this.setState({searchText: e.target.value})
+
+ _onSearchTextKeyUp = (e: any) => {
+ switch (e.keyCode) {
+ case 13: // ENTER
+ return this._userSearch()
+ default:
+ break
+ }
}
- userSearch = () => {
- this.props.userSearch(ReactDOM.findDOMNode(this.refs.searchInput).value)
+ _userSearch = () => {
+ this.props.userSearch(this.state.searchText || '')
}
render () {
@@ -92,7 +108,11 @@ export default class UserList extends Component {
{userCount > 0
- ? {getMessage(messages, 'showing')} {minUserIndex } - {maxUserIndex} {getMessage(messages, 'of')} {userCount}
+ ?
+ {getMessage(messages, 'showing')}{' '}
+ {minUserIndex } - {maxUserIndex}{' '}
+ {getMessage(messages, 'of')} {userCount}
+
: (No results to show)
}
@@ -102,21 +122,27 @@ export default class UserList extends Component {
-
-
+ onKeyUp={this._onSearchTextKeyUp}
+ onChange={this._onSearchTextChange} />
+
+
+
+
-
+
-
-
+ onClick={this._userSearch}>
+
{/* TODO: add filter for organization */}
{/* isApplicationAdmin &&
From 562bf4da60055a7b2fc2d8c52e3bf6933f924e66 Mon Sep 17 00:00:00 2001
From: Landon Reed
Date: Fri, 17 Aug 2018 14:13:52 -0400
Subject: [PATCH 03/18] test(flow): add flow typing for components
---
__tests__/end-to-end.js | 4 +-
lib/common/components/EditableTextField.js | 102 +++++----
lib/common/user/Auth0Manager.js | 2 +-
lib/common/util/date-time.js | 26 ++-
lib/common/util/map-keys.js | 9 +-
lib/editor/actions/map/stopStrategies.js | 6 +-
lib/editor/components/ColorField.js | 32 ++-
lib/editor/components/CreateSnapshotModal.js | 41 +++-
.../components/EditorFeedSourcePanel.js | 67 +++---
lib/editor/components/EditorHelpModal.js | 78 ++++---
lib/editor/components/EditorInput.js | 87 ++++----
lib/editor/components/EditorSidebar.js | 22 +-
lib/editor/components/EntityDetails.js | 80 ++++---
lib/editor/components/EntityDetailsHeader.js | 55 +++--
lib/editor/components/EntityList.js | 89 ++++----
lib/editor/components/EntityListButtons.js | 47 ++--
.../components/EntityListSecondaryActions.js | 41 ++--
lib/editor/components/ExceptionDate.js | 29 ++-
lib/editor/components/FareRuleSelections.js | 36 +--
lib/editor/components/FareRulesForm.js | 63 ++++--
lib/editor/components/FeedInfoPanel.js | 59 +++--
lib/editor/components/GtfsEditor.js | 100 ++++++---
lib/editor/components/HourMinuteInput.js | 11 +-
lib/editor/components/MinuteSecondInput.js | 85 ++++---
.../components/ScheduleExceptionForm.js | 60 ++---
.../components/VirtualizedEntitySelect.js | 42 ++--
lib/editor/components/ZoneSelect.js | 70 ++++--
lib/editor/components/map/AddableStop.js | 22 +-
.../components/map/AddableStopsLayer.js | 38 ++--
lib/editor/components/map/ControlPoint.js | 68 +++---
.../components/map/ControlPointsLayer.js | 45 ++--
.../components/map/DirectionIconsLayer.js | 86 ++++---
lib/editor/components/map/EditorMap.js | 209 ++++++++++++------
.../components/map/EditorMapLayersControl.js | 82 ++++---
.../components/map/PatternStopMarker.js | 53 +++--
.../components/map/PatternStopsLayer.js | 67 ++++--
lib/editor/components/map/PatternsLayer.js | 95 +++++---
lib/editor/components/map/RouteLayer.js | 78 -------
lib/editor/components/map/StopsLayer.js | 134 ++++++-----
.../components/map/pattern-debug-lines.js | 31 ++-
.../pattern/CalculateDefaultTimesForm.js | 43 ++--
.../components/pattern/EditSchedulePanel.js | 35 +--
lib/editor/components/pattern/EditSettings.js | 52 +++--
.../components/pattern/EditShapePanel.js | 50 ++++-
.../components/pattern/PatternStopButtons.js | 43 ++--
.../components/pattern/PatternStopCard.js | 100 +++++----
.../pattern/PatternStopContainer.js | 57 +++--
.../components/pattern/PatternStopsPanel.js | 51 +++--
.../components/pattern/TripPatternList.js | 91 ++++----
.../pattern/TripPatternListControls.js | 7 +-
.../components/pattern/TripPatternViewer.js | 22 +-
.../components/timetable/CalendarSelect.js | 52 +++--
.../components/timetable/EditableCell.js | 125 ++++++-----
lib/editor/components/timetable/HeaderCell.js | 34 ++-
.../components/timetable/PatternSelect.js | 43 ++--
.../components/timetable/RouteSelect.js | 36 ++-
lib/editor/components/timetable/Timetable.js | 73 ++++--
.../components/timetable/TimetableEditor.js | 23 +-
.../components/timetable/TimetableGrid.js | 103 +++++----
.../components/timetable/TimetableHeader.js | 61 ++---
lib/editor/containers/ActiveGtfsEditor.js | 2 +-
lib/editor/reducers/data.js | 31 ++-
lib/editor/reducers/index.js | 16 +-
lib/editor/reducers/mapState.js | 8 +-
lib/editor/reducers/settings.js | 6 +-
lib/editor/reducers/timetable.js | 12 +-
lib/editor/selectors/index.js | 95 ++++----
lib/editor/util/gtfs.js | 1 +
lib/editor/util/index.js | 9 +-
lib/editor/util/map.js | 31 ++-
lib/editor/util/timetable.js | 1 +
lib/editor/util/ui.js | 10 +
lib/editor/util/validation.js | 11 +-
lib/gtfs/components/GtfsMap.js | 47 ++--
lib/gtfs/components/PatternGeoJson.js | 32 +--
lib/gtfs/components/StopMarker.js | 26 ++-
lib/gtfs/reducers/filter.js | 12 +-
lib/gtfsplus/components/GtfsPlusEditor.js | 104 +++++----
lib/gtfsplus/components/GtfsPlusField.js | 74 ++++---
.../components/GtfsPlusFieldHeader.js | 27 +--
lib/gtfsplus/components/GtfsPlusTable.js | 75 ++++---
.../containers/ActiveGtfsPlusEditor.js | 30 +--
lib/manager/components/DeploymentsPanel.js | 52 +++--
lib/manager/components/FeedSourceDropdown.js | 51 +++--
lib/manager/components/FeedSourcePanel.js | 51 +++--
lib/manager/components/FeedSourceTable.js | 143 +++++++-----
lib/manager/components/FeedSourceViewer.js | 8 +-
lib/manager/components/ManagerHeader.js | 61 ++---
lib/manager/components/NotesViewer.js | 68 +++---
lib/manager/components/ProjectViewer.js | 22 +-
lib/manager/components/ProjectViewerHeader.js | 44 ++--
.../components/version/FeedVersionDetails.js | 45 ++--
.../version/FeedVersionNavigator.js | 4 +-
.../components/version/FeedVersionReport.js | 4 +-
.../components/version/FeedVersionViewer.js | 2 +-
.../containers/ActiveFeedSourceViewer.js | 2 +-
.../containers/ActiveFeedVersionNavigator.js | 2 +-
lib/public/components/PublicHeader.js | 22 +-
lib/public/components/UserAccount.js | 98 ++++----
.../components/direction-icon.js | 35 ---
lib/style.css | 19 ++
lib/types.js | 102 ++++++---
package.json | 3 +
103 files changed, 2969 insertions(+), 1981 deletions(-)
delete mode 100644 lib/editor/components/map/RouteLayer.js
delete mode 100644 lib/scenario-editor/components/direction-icon.js
diff --git a/__tests__/end-to-end.js b/__tests__/end-to-end.js
index 6edeb29a8..a48388606 100644
--- a/__tests__/end-to-end.js
+++ b/__tests__/end-to-end.js
@@ -477,6 +477,8 @@ describe('end-to-end', () => {
log.info('Chromium closed.')
})
+ /// Begin tests
+
makeTest('should load the page', async () => {
await goto('http://localhost:9966')
await expectSelectorToContainHtml('h1', 'Conveyal Datatools')
@@ -485,7 +487,7 @@ describe('end-to-end', () => {
makeTest('should login', async () => {
await goto('http://localhost:9966')
- await click('[data-test-id="header-log-in-button"]')
+ await waitForAndClick('[data-test-id="header-log-in-button"]')
await waitForSelector('button[class="auth0-lock-submit"]')
await page.type('input[class="auth0-lock-input"][name="email"]', config.username)
await page.type('input[class="auth0-lock-input"][name="password"]', config.password)
diff --git a/lib/common/components/EditableTextField.js b/lib/common/components/EditableTextField.js
index ca4763d60..bb0789807 100644
--- a/lib/common/components/EditableTextField.js
+++ b/lib/common/components/EditableTextField.js
@@ -1,26 +1,36 @@
+// @flow
+
import Icon from '@conveyal/woonerf/components/icon'
-import React, {Component, PropTypes} from 'react'
-import ReactDOM from 'react-dom'
+import React, {Component} from 'react'
import { Form, FormControl, InputGroup, FormGroup, Button } from 'react-bootstrap'
import { Link } from 'react-router'
-export default class EditableTextField extends Component {
- static propTypes = {
- rejectEmptyValue: PropTypes.bool,
- disabled: PropTypes.bool,
- inline: PropTypes.bool,
- hideEditButton: PropTypes.bool,
- isEditing: PropTypes.bool,
- link: PropTypes.string,
- maxLength: PropTypes.number,
- min: PropTypes.number,
- placeholder: PropTypes.string,
- step: PropTypes.number,
- tabIndex: PropTypes.number,
- type: PropTypes.string,
- value: PropTypes.string,
- onChange: PropTypes.func
- }
+type Props = {
+ rejectEmptyValue?: boolean,
+ disabled?: boolean,
+ inline?: boolean,
+ hideEditButton?: boolean,
+ isEditing?: boolean,
+ link?: string,
+ maxLength?: number,
+ min?: number,
+ placeholder?: string,
+ step?: number,
+ style: {[string]: number | string},
+ tabIndex?: number,
+ type: string,
+ value: string,
+ onChange: string => void
+}
+
+type State = {
+ isEditing: boolean,
+ initialValue: string,
+ value: string
+}
+
+export default class EditableTextField extends Component {
+ input = {}
static defaultProps = {
rejectEmptyValue: false,
@@ -30,22 +40,31 @@ export default class EditableTextField extends Component {
state = {
isEditing: this.props.isEditing || false,
+ initialValue: this.props.value,
value: this.props.value
}
- componentWillReceiveProps (nextProps) {
- if (this.state.value !== nextProps.value) this.setState({ value: nextProps.value })
+ componentWillReceiveProps (nextProps: Props) {
+ if (this.state.value !== nextProps.value) {
+ // Update value if externally changed.
+ this.setState({ value: nextProps.value })
+ }
+ if (this.state.initialValue !== nextProps.value) {
+ // Update initial value if externally changed.
+ this.setState({ initialValue: nextProps.value })
+ }
}
edit = () => this.setState({isEditing: true})
- save = () => {
+ _save = (evt?: SyntheticEvent) => {
+ if (evt) evt.preventDefault()
const {onChange, rejectEmptyValue} = this.props
- const value = ReactDOM.findDOMNode(this.input).value
+ const {initialValue, value} = this.state
// Do not allow save if there is no input value.
if (rejectEmptyValue && !value) return window.alert('Must provide a valid input.')
// If there was no change in the value, cancel editing.
- if (value === this.state.value) return this.cancel()
+ if (value === initialValue) return this.cancel()
// Otherwise, call onChange function from props and store value in state.
onChange && onChange(value)
this.setState({
@@ -56,34 +75,35 @@ export default class EditableTextField extends Component {
cancel () {
const {rejectEmptyValue} = this.props
- const value = ReactDOM.findDOMNode(this.input).value
+ const {initialValue} = this.state
// Do not allow cancel if there is no input value
- if (rejectEmptyValue && !value) return window.alert('Must provide a valid input.')
- else this.setState({isEditing: false})
+ if (rejectEmptyValue && !initialValue) return window.alert('Must provide a valid input.')
+ else this.setState({isEditing: false, value: initialValue})
}
- handleKeyDown = (e) => {
+ handleKeyDown = (e: SyntheticKeyboardEvent) => {
switch (e.keyCode) {
case 9: // [Enter]
case 13: // [Tab]
- if (this.state.isEditing) return this.save()
+ e.preventDefault()
+ if (this.state.isEditing) {
+ this._save(e)
+ }
break
case 27: // [Esc]
+ e.preventDefault()
return this.cancel()
default:
break
}
}
- _ref = input => {
- this.input = input
- // Auto-focus on text input when input is rendered (instead of disallowed
- // autofocus prop).
- this.input && ReactDOM.findDOMNode(this.input).focus()
+ _onInputChange = (e: SyntheticInputEvent) => {
+ this.setState({value: e.target.value})
}
// select entire text string on input focus
- _onInputFocus = (e) => e.target.select()
+ _onInputFocus = (e: SyntheticInputEvent) => e.target.select()
render () {
const {
@@ -104,7 +124,7 @@ export default class EditableTextField extends Component {
value
} = this.state
// trim length of display text to fit content
- const displayValue = maxLength !== null && value && value.length > maxLength
+ const displayValue = typeof maxLength === 'number' && value && value.length > maxLength
? `${value.substr(0, maxLength)}...`
: value || '(none)'
if (inline) {
@@ -119,17 +139,18 @@ export default class EditableTextField extends Component {
+ value={value} />
+ onClick={this._save}>
@@ -150,8 +171,7 @@ export default class EditableTextField extends Component {
data-test-id='editable-text-field-edit-button'
disabled={disabled}
onClick={this.edit}
- tabIndex={tabIndex}
- >
+ tabIndex={tabIndex}>
}
diff --git a/lib/common/user/Auth0Manager.js b/lib/common/user/Auth0Manager.js
index 402fb79df..df190f371 100644
--- a/lib/common/user/Auth0Manager.js
+++ b/lib/common/user/Auth0Manager.js
@@ -366,7 +366,7 @@ function renewAuth () {
nonce,
postMessageDataType: 'auth0:silent-authentication',
redirectUri: window.location.origin + '/api/auth0-silent-callback',
- scope: 'openid email',
+ scope: 'app_metadata profile email openid user_metadata',
usePostMessage: true
}, (err, authResult) => {
if (err) {
diff --git a/lib/common/util/date-time.js b/lib/common/util/date-time.js
index 66603ef65..4d19cc8de 100644
--- a/lib/common/util/date-time.js
+++ b/lib/common/util/date-time.js
@@ -14,13 +14,16 @@ export function fromNow (value: number | string): string {
return moment(value).fromNow()
}
-export function convertSecondsToString (seconds: number): string {
+/**
+ * Converts seconds to an hour:minute string.
+ */
+export function convertSecondsToHHMMString (seconds: number): string {
const hours = Math.floor(seconds / 60 / 60)
const minutes = Math.floor(seconds / 60) % 60
return seconds ? `${hours}:${minutes < 10 ? '0' + minutes : minutes}` : '00:00'
}
-export function convertStringToSeconds (string: string): number {
+export function convertHHMMStringToSeconds (string: string): number {
const hourMinute = string.split(':')
if (!isNaN(hourMinute[0]) && !isNaN(hourMinute[1])) {
// If both hours and minutes are present
@@ -36,3 +39,22 @@ export function convertStringToSeconds (string: string): number {
return 0
}
}
+
+export function convertSecondsToMMSSString (seconds: number) {
+ const minutes = Math.floor(seconds / 60)
+ const sec = seconds % 60
+ return seconds ? `${minutes}:${sec < 10 ? '0' + sec : sec}` : '00:00'
+}
+
+export function convertMMSSStringToSeconds (string: string) {
+ const minuteSecond = string.split(':')
+ if (!isNaN(minuteSecond[0]) && !isNaN(minuteSecond[1])) {
+ return (Math.abs(+minuteSecond[0]) * 60) + Math.abs(+minuteSecond[1])
+ } else if (isNaN(minuteSecond[0])) {
+ return Math.abs(+minuteSecond[1])
+ } else if (isNaN(minuteSecond[1])) {
+ return Math.abs(+minuteSecond[0] * 60)
+ } else {
+ return 0
+ }
+}
diff --git a/lib/common/util/map-keys.js b/lib/common/util/map-keys.js
index 29bc70fe0..294a46b85 100644
--- a/lib/common/util/map-keys.js
+++ b/lib/common/util/map-keys.js
@@ -8,12 +8,12 @@ import snakeCase from 'lodash/snakeCase'
* Converts the keys for an object (or array of objects) using string mapping
* function passed in. Operates on object recursively.
*/
-function mapObjectKeys (object: any, keyMapper: string => string): any {
+function mapObjectKeys (object: Object, keyMapper: string => string): Object {
const convertedObject = {}
const convertedArray = []
forEach(
object,
- (value, key) => {
+ (value: Object, key: string) => {
if (isPlainObject(value) || Array.isArray(value)) {
// If plain object or an array, recursively update keys of any values
// that are also objects.
@@ -23,6 +23,7 @@ function mapObjectKeys (object: any, keyMapper: string => string): any {
else convertedObject[keyMapper(key)] = value
}
)
+ // $FlowFixMe
if (Array.isArray(object)) return convertedArray
else return convertedObject
}
@@ -31,7 +32,7 @@ function mapObjectKeys (object: any, keyMapper: string => string): any {
* Converts the keys for an object or array of objects to camelCase. The function
* always recursively converts keys.
*/
-export function camelCaseKeys (object: any): any {
+export function camelCaseKeys (object: Object): Object {
return mapObjectKeys(object, camelCase)
}
@@ -39,6 +40,6 @@ export function camelCaseKeys (object: any): any {
* Converts the keys for an object or array of objects to snake_case. The function
* always recursively converts keys.
*/
-export function snakeCaseKeys (object: any): any {
+export function snakeCaseKeys (object: Object): Object {
return mapObjectKeys(object, snakeCase)
}
diff --git a/lib/editor/actions/map/stopStrategies.js b/lib/editor/actions/map/stopStrategies.js
index 3fdeff134..97d7f09e4 100644
--- a/lib/editor/actions/map/stopStrategies.js
+++ b/lib/editor/actions/map/stopStrategies.js
@@ -194,7 +194,7 @@ export function addStopAtInterval (latlng: LatLng, activePattern: Pattern, contr
}
}
-export function addStopToPattern (pattern: Pattern, stop: GtfsStop, index: number) {
+export function addStopToPattern (pattern: Pattern, stop: GtfsStop, index?: number) {
return async function (dispatch: dispatchFn, getState: getStateFn) {
dispatch(addingStopToPattern(pattern, stop, index))
const {data, editSettings} = getState().editor
@@ -208,7 +208,7 @@ export function addStopToPattern (pattern: Pattern, stop: GtfsStop, index: numbe
const stopSequence = missingIndex ? patternStops.length : index
const newStop = stopToPatternStop(stop, stopSequence)
- if (missingIndex || index === patternStops.length) {
+ if (typeof index === 'undefined' || index === null || index === patternStops.length) {
// Push pattern stop to cloned list.
patternStops.push(newStop)
if (hasShapePoints) {
@@ -423,6 +423,7 @@ function extendPatternToPoint (pattern, endPoint, newEndPoint, stop = null, spli
pointType: POINT_TYPE.ANCHOR,
distance: initialDistance + splitDistance
}
+ // $FlowFixMe
clonedControlPoints.push(controlPoint)
// Update previous distance
previousDistance = splitDistance
@@ -444,6 +445,7 @@ function extendPatternToPoint (pattern, endPoint, newEndPoint, stop = null, spli
distance: initialDistance + distanceAdded,
stopId: stop.stop_id
}
+ // $FlowFixMe
clonedControlPoints.push(controlPoint)
}
// Return updated pattern geometry and control points
diff --git a/lib/editor/components/ColorField.js b/lib/editor/components/ColorField.js
index 6fb675cd3..8e7ab4836 100644
--- a/lib/editor/components/ColorField.js
+++ b/lib/editor/components/ColorField.js
@@ -1,23 +1,31 @@
-import React, {PropTypes, Component} from 'react'
+// @flow
+
+import React, {Component} from 'react'
import {FormGroup} from 'react-bootstrap'
import SketchPicker from 'react-color/lib/components/sketch/Sketch'
import ClickOutside from '../../common/components/ClickOutside'
-export default class ColorField extends Component {
- static propTypes = {
- field: PropTypes.object,
- formProps: PropTypes.object,
- label: PropTypes.any,
- onChange: PropTypes.func,
- value: PropTypes.string
- }
+type Props = {
+ field: any,
+ formProps: any,
+ label: any,
+ onChange: any => void,
+ value: ?string
+}
+
+type State = {
+ open: boolean,
+ color: {r: string, g: string, b: string, a: string}
+}
+export default class ColorField extends Component {
state = {
+ open: false,
color: {r: '241', g: '112', b: '19', a: '1'} // default color
}
- _handleClick = (e) => {
+ _handleClick = (e: SyntheticInputEvent) => {
e.preventDefault()
this.setState({ open: !this.state.open })
}
@@ -28,7 +36,7 @@ export default class ColorField extends Component {
render () {
const {formProps, label, value} = this.props
- const hexColor = value !== null ? `#${value}` : '#000000'
+ const hexColor = value ? `#${value}` : '#000000'
const colorStyle = {
width: '36px',
height: '20px',
@@ -69,7 +77,7 @@ export default class ColorField extends Component {
onClickOutside={this._handleClose}>
+ onChange={this.props.onChange} />
: null
}
diff --git a/lib/editor/components/CreateSnapshotModal.js b/lib/editor/components/CreateSnapshotModal.js
index b0c8b44cc..2976974e2 100644
--- a/lib/editor/components/CreateSnapshotModal.js
+++ b/lib/editor/components/CreateSnapshotModal.js
@@ -1,14 +1,27 @@
-import React, {Component, PropTypes} from 'react'
-import ReactDOM from 'react-dom'
+// @flow
+
+import React, {Component} from 'react'
import { Modal, Button, FormGroup, FormControl, ControlLabel } from 'react-bootstrap'
-export default class CreateSnapshotModal extends Component {
- static propTypes = {
- onOkClicked: PropTypes.func
- }
+type Props = {
+ onOkClicked: (string, ?string) => void
+}
+
+type State = {
+ comment: ?string,
+ name: ?string,
+ showModal: boolean
+}
+export default class CreateSnapshotModal extends Component {
state = {
- showModal: false
+ showModal: false,
+ name: null,
+ comment: null
+ }
+
+ _onChange = (e: SyntheticInputEvent) => {
+ this.setState({[e.target.name]: e.target.value})
}
close = () => {
@@ -24,8 +37,8 @@ export default class CreateSnapshotModal extends Component {
}
ok = () => {
- const name = ReactDOM.findDOMNode(this.refs.name).value
- const comment = ReactDOM.findDOMNode(this.refs.comment).value
+ const {comment, name} = this.state
+ if (!name) return window.alert('Must give snapshot a valid name!')
this.props.onOkClicked(name, comment)
this.close()
}
@@ -42,9 +55,10 @@ export default class CreateSnapshotModal extends Component {
Name
@@ -52,8 +66,11 @@ export default class CreateSnapshotModal extends Component {
Comment
+ />
diff --git a/lib/editor/components/EditorFeedSourcePanel.js b/lib/editor/components/EditorFeedSourcePanel.js
index 9023d6035..1cb16dba0 100644
--- a/lib/editor/components/EditorFeedSourcePanel.js
+++ b/lib/editor/components/EditorFeedSourcePanel.js
@@ -1,5 +1,7 @@
+// @flow
+
import Icon from '@conveyal/woonerf/components/icon'
-import React, {Component, PropTypes} from 'react'
+import React, {Component} from 'react'
import {Panel, Row, Col, ButtonGroup, Button, Glyphicon, ListGroup, ListGroupItem} from 'react-bootstrap'
import {LinkContainer} from 'react-router-bootstrap'
import moment from 'moment'
@@ -9,41 +11,35 @@ import ConfirmModal from '../../common/components/ConfirmModal'
import {getComponentMessages, getMessage, getConfigProperty} from '../../common/util/config'
import {isEditingDisabled} from '../../manager/util'
-export default class EditorFeedSourcePanel extends Component {
- static propTypes = {
- feedSource: PropTypes.object.isRequired,
-
- exportSnapshotAsVersion: PropTypes.func.isRequired,
- getSnapshots: PropTypes.func.isRequired,
- restoreSnapshot: PropTypes.func.isRequired,
- deleteSnapshot: PropTypes.func.isRequired,
- loadFeedVersionForEditing: PropTypes.func.isRequired
- }
+import type {Feed, Project, Snapshot} from '../../types'
+import type {UserState} from '../../manager/reducers/user'
+
+type Props = {
+ createSnapshot: (Feed, string, ?string) => void,
+ downloadSnapshot: (Feed, Snapshot) => void,
+ feedSource: Feed,
+ exportSnapshotAsVersion: (Feed, string) => void,
+ getSnapshots: Feed => void,
+ restoreSnapshot: (Feed, Snapshot) => void,
+ deleteSnapshot: (Feed, Snapshot) => void,
+ project: Project,
+ user: UserState
+}
+export default class EditorFeedSourcePanel extends Component {
messages = getComponentMessages('EditorFeedSourcePanel')
componentWillMount () {
this.props.getSnapshots(this.props.feedSource)
}
- _onCreateSnapshot = (name, comment) => {
+ _onCreateSnapshot = (name: string, comment: ?string) => {
this.props.createSnapshot(this.props.feedSource, name, comment)
}
_openModal = () => this.refs.snapshotModal.open()
- _sortBySnapshotTime = (a, b) => b.snapshotTime - a.snapshotTime
-
- _onLoadVersion = () => {
- const {feedSource, loadFeedVersionForEditing} = this.props
- const version = feedSource.feedVersions[feedSource.feedVersions.length - 1]
- const {id: feedVersionId, feedSourceId} = version
- this.refs.confirmModal.open({
- title: getMessage(this.messages, 'load'),
- body: getMessage(this.messages, 'confirmLoad'),
- onConfirm: () => loadFeedVersionForEditing({feedSourceId, feedVersionId})
- })
- }
+ _sortBySnapshotTime = (a: Snapshot, b: Snapshot) => b.snapshotTime - a.snapshotTime
render () {
const {
@@ -51,7 +47,7 @@ export default class EditorFeedSourcePanel extends Component {
project,
user
} = this.props
- const disabled = !user.permissions.hasFeedPermission(
+ const disabled = !user.permissions || !user.permissions.hasFeedPermission(
project.organizationId,
project.id,
feedSource.id,
@@ -125,13 +121,20 @@ export default class EditorFeedSourcePanel extends Component {
}
}
-class SnapshotItem extends Component {
- static propTypes = {
- modal: PropTypes.object.isRequired,
- snapshot: PropTypes.object.isRequired,
- feedSource: PropTypes.object.isRequired
- }
+type ItemProps = {
+ createSnapshot: (Feed, string, ?string) => void,
+ disabled: boolean,
+ downloadSnapshot: (Feed, Snapshot) => void,
+ feedSource: Feed,
+ exportSnapshotAsVersion: (Feed, string) => void,
+ getSnapshots: Feed => void,
+ restoreSnapshot: (Feed, Snapshot) => void,
+ deleteSnapshot: (Feed, Snapshot) => void,
+ modal: any,
+ snapshot: Snapshot
+}
+class SnapshotItem extends Component {
messages = getComponentMessages('EditorFeedSourcePanel')
_onClickDownload = () => {
@@ -164,7 +167,7 @@ class SnapshotItem extends Component {
render () {
const {disabled, snapshot} = this.props
- const dateFormat = getConfigProperty('application.date_format')
+ const dateFormat = getConfigProperty('application.date_format') || ''
const timeFormat = 'h:MMa'
const formattedTime = moment(snapshot.snapshotTime)
.format(`${dateFormat}, ${timeFormat}`)
diff --git a/lib/editor/components/EditorHelpModal.js b/lib/editor/components/EditorHelpModal.js
index 5905f83c7..46fb3859e 100644
--- a/lib/editor/components/EditorHelpModal.js
+++ b/lib/editor/components/EditorHelpModal.js
@@ -1,15 +1,33 @@
+// @flow
+
import Icon from '@conveyal/woonerf/components/icon'
-import React, {Component, PropTypes} from 'react'
+import React, {Component} from 'react'
import {Modal, Button, ButtonToolbar, Checkbox} from 'react-bootstrap'
import {LinkContainer} from 'react-router-bootstrap'
import {getConfigProperty} from '../../common/util/config'
-export default class EditorHelpModal extends Component {
- static propTypes = {
- show: PropTypes.bool
- }
+import type {Feed} from '../../types'
+import type {EditorStatus} from '../reducers/data'
+
+type Props = {
+ createSnapshot: (Feed, string, ?string) => void,
+ feedSource: Feed,
+ isNewFeed: boolean,
+ show: boolean,
+ status: EditorStatus,
+ hideTutorial: boolean,
+ loadFeedVersionForEditing: ({feedSourceId: string, feedVersionId: string}) => void,
+ onComponentMount: any => void,
+ setTutorialHidden: boolean => void
+}
+type State = {
+ showModal: boolean,
+ hideTutorial: boolean
+}
+
+export default class EditorHelpModal extends Component {
state = {
showModal: this.props.show,
hideTutorial: this.props.hideTutorial
@@ -25,6 +43,7 @@ export default class EditorHelpModal extends Component {
_onClickLoad = () => {
const {feedSource, loadFeedVersionForEditing} = this.props
const {latestVersionId: feedVersionId, id: feedSourceId} = feedSource
+ if (!feedVersionId) return console.warn('Cannot load null version ID.')
loadFeedVersionForEditing({feedSourceId, feedVersionId})
}
@@ -50,10 +69,9 @@ export default class EditorHelpModal extends Component {
render () {
const {feedSource, isNewFeed, show, status} = this.props
- if (!show) {
- return null
- }
+ if (!show) return null
const {Body, Footer, Header, Title} = Modal
+ const docsUrl: ?string = getConfigProperty('application.docs_url')
return (
Snapshot created successfully!
:
- There is no feed loaded in the editor. To begin editing you can either
- start from scratch or import an existing version (if a version exists).
+ There is no feed loaded in the editor. To begin editing you
+ can either start from scratch or import an existing version
+ (if a version exists).
}
{status.snapshotFinished
@@ -104,37 +123,16 @@ export default class EditorHelpModal extends Component {
}
- : For instructions on using the editor, view the{' '}
-
- documentation
- .
-
+ : docsUrl
+ ? For instructions on using the editor, view the{' '}
+
+ documentation
+ .
+
+ : null
}
- {/*
- -
-
-
- First slide label
- Nulla vitae elit libero, a pharetra augue mollis interdum.
-
-
- -
-
-
- Second slide label
- Lorem ipsum dolor sit amet, consectetur adipiscing elit.
-
-
- -
-
-
- Third slide label
- Praesent commodo cursus magna, vel scelerisque nisl consectetur.
-
-
- */}