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} /> + + - + - {/* 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} /> @@ -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 } - {/* - - 900x500 - -

First slide label

-

Nulla vitae elit libero, a pharetra augue mollis interdum.

- -
- - 900x500 - -

Second slide label

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

- -
- - 900x500 - -

Third slide label

-

Praesent commodo cursus magna, vel scelerisque nisl consectetur.

- -
-
*/}