diff --git a/app/actions/requests.js b/app/actions/requests.js index a60d1e8d0..6d93685e2 100644 --- a/app/actions/requests.js +++ b/app/actions/requests.js @@ -37,7 +37,7 @@ function buildRequest (request) { } export function addRequest (name = 'My Request') { - return (dispatch, getState) => { + return (dispatch) => { dispatch(loadStart()); const request = buildRequest({name}); dispatch({type: types.REQUEST_ADD, request}); diff --git a/app/components/RequestBodyEditor.js b/app/components/RequestBodyEditor.js new file mode 100644 index 000000000..229c7a6d2 --- /dev/null +++ b/app/components/RequestBodyEditor.js @@ -0,0 +1,30 @@ +import React, {Component, PropTypes} from 'react'; +import CodeEditor from './base/Editor' + +class RequestBodyEditor extends Component { + shouldComponentUpdate (nextProps) { + return this.props.request !== nextProps.request; + } + + render () { + const {request, onChange, className} = this.props; + return ( + + ) + } +} + +RequestBodyEditor.propTypes = { + request: PropTypes.shape({ + body: PropTypes.string.isRequired, + _mode: PropTypes.string.isRequired + }).isRequired, + onChange: PropTypes.func.isRequired +}; + +export default RequestBodyEditor; diff --git a/app/components/Sidebar.js b/app/components/Sidebar.js index 25a957152..063e6c615 100644 --- a/app/components/Sidebar.js +++ b/app/components/Sidebar.js @@ -1,53 +1,56 @@ import React, {PropTypes} from 'react' -import Dropdown from '../components/Dropdown' +import Dropdown from './base/Dropdown' const Sidebar = (props) => ( ); Sidebar.propTypes = { activateRequest: PropTypes.func.isRequired, + addRequest: PropTypes.func.isRequired, requests: PropTypes.array.isRequired, activeRequest: PropTypes.object, loading: PropTypes.bool.isRequired diff --git a/app/components/UrlInput.js b/app/components/UrlInput.js index b9165531d..16332dca2 100644 --- a/app/components/UrlInput.js +++ b/app/components/UrlInput.js @@ -1,6 +1,6 @@ import React, {Component, PropTypes} from 'react' -import Input from '../components/Input'; -import Dropdown from '../components/Dropdown'; +import Input from './base/Input'; +import Dropdown from './base/Dropdown'; import {METHODS} from '../constants/global'; class UrlInput extends Component { diff --git a/app/components/Dropdown.js b/app/components/base/Dropdown.js similarity index 50% rename from app/components/Dropdown.js rename to app/components/base/Dropdown.js index 37f2511cb..988d4fd09 100644 --- a/app/components/Dropdown.js +++ b/app/components/base/Dropdown.js @@ -3,34 +3,39 @@ import React, {Component, PropTypes} from 'react'; class Dropdown extends Component { constructor () { super(); - this.state = {open: false}; + this.state = { + open: false + }; } componentDidMount () { // Capture clicks outside the component and close the dropdown // TODO: Remove this listener when component unmounts - document.addEventListener('click', (e) => { - if (!this.refs.container.contains(e.target)) { - e.preventDefault(); - this.setState({open: false}); - } - }); + document.addEventListener('click', this._clickEvenCallback.bind(this)); } - handleClick (e) { + _clickEvenCallback (e) { + if (!this.refs.container.contains(e.target)) { + e.preventDefault(); + this.setState({open: false}); + } + } + + _handleClick (e) { e.preventDefault(); this.setState({open: !this.state.open}); } render () { - const {initialValue, value} = this.props; + const classes = ['dropdown']; + + this.state.open && classes.push('dropdown--open'); + this.props.right && classes.push('dropdown--right'); + return (
+ className={classes.join(' ')} + onClick={this._handleClick.bind(this)}> {this.props.children}
) diff --git a/app/components/CodeEditor.js b/app/components/base/Editor.js similarity index 65% rename from app/components/CodeEditor.js rename to app/components/base/Editor.js index af48bcb2a..8dff585a6 100644 --- a/app/components/CodeEditor.js +++ b/app/components/base/Editor.js @@ -3,7 +3,7 @@ import {getDOMNode} from 'react-dom'; import CodeMirror from 'codemirror'; // Modes -import 'codemirror/mode/css/css'; +import '../../../node_modules/codemirror/mode/css/css'; import 'codemirror/mode/htmlmixed/htmlmixed'; import 'codemirror/mode/javascript/javascript'; @@ -20,6 +20,17 @@ import 'codemirror/addon/fold/xml-fold'; import 'codemirror/addon/fold/foldgutter'; import 'codemirror/addon/fold/foldgutter.css'; +// TODO: Figure out how to lint (json-lint doesn't build in webpack environment) +// import 'codemirror/addon/lint/lint'; +// import 'codemirror/addon/lint/json-lint'; +// import 'codemirror/addon/lint/html-lint'; +// import 'codemirror/addon/lint/lint.css'; +// import * as jsonlint from 'jsonlint'; +// import * as htmlhint from 'htmlhint'; + +// window.jsonlint = jsonlint; +// window.htmlhint = htmlhint; + // CSS Themes import 'codemirror/theme/material.css' import 'codemirror/theme/railscasts.css' @@ -32,7 +43,7 @@ import 'codemirror/theme/seti.css' import 'codemirror/theme/monokai.css' // App styles -import '../css/components/editor.scss'; +import '../../css/components/editor.scss'; const DEFAULT_DEBOUNCE_MILLIS = 500; @@ -40,7 +51,12 @@ const BASE_CODEMIRROR_OPTIONS = { theme: 'monokai', lineNumbers: true, foldGutter: true, - gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + // lint: true, + gutters: [ + 'CodeMirror-linenumbers', + 'CodeMirror-foldgutter', + 'CodeMirror-lint-markers' + ], cursorScrollMargin: 80, extraKeys: { "Ctrl-Q": function (cm) { @@ -60,13 +76,11 @@ class Editor extends Component { var textareaNode = this.refs.textarea; this.codeMirror = CodeMirror.fromTextArea(textareaNode, BASE_CODEMIRROR_OPTIONS); - this.codeMirror.on('change', this.codemirrorValueChanged.bind(this)); - this.codeMirror.on('focus', this.focusChanged.bind(this, true)); - this.codeMirror.on('blur', this.focusChanged.bind(this, false)); - this.codeMirror.on('paste', this.codemirrorValueChanged.bind(this)); + this.codeMirror.on('change', this._codemirrorValueChanged.bind(this)); + this.codeMirror.on('paste', this._codemirrorValueChanged.bind(this)); this._currentCodemirrorValue = this.props.defaultValue || this.props.value || ''; - this.codemirrorSetValue(this._currentCodemirrorValue); - this.codemirrorSetOptions(this.props.options); + this._codemirrorSetValue(this._currentCodemirrorValue); + this._codemirrorSetOptions(this.props.options); } componentWillUnmount () { @@ -77,35 +91,48 @@ class Editor extends Component { } componentWillReceiveProps (nextProps) { - if (this.codeMirror && nextProps.value !== undefined && this._currentCodemirrorValue !== nextProps.value) { - this.codemirrorSetValue(nextProps.value); + // Don't update if no CodeMirror instance + if (!this.codeMirror) { + return; } + // Don't update if no value passed + if (nextProps.value === undefined) { + return; + } + + // Don't update if same value passed again + if (this._currentCodemirrorValue === nextProps.value) { + return; + } + + // Set the new value + this._codemirrorSetValue(nextProps.value); + // Reset any options that may have changed - this.codemirrorSetOptions(nextProps.options); + this._codemirrorSetOptions(nextProps.options); } + /** + * Focus the cursor to the editor + */ focus () { if (this.codeMirror) { this.codeMirror.focus(); } } - focusChanged (focused) { - this.setState({isFocused: focused}); - this.props.onFocusChange && this.props.onFocusChange(focused); - } - /** - * Set options on the CodeMirror editor while also sanitizing them + * Sets options on the CodeMirror editor while also sanitizing them * @param options */ - codemirrorSetOptions (options) { + _codemirrorSetOptions (options) { if (options.mode === 'json') { options.mode = 'application/json'; } if (options.mode === 'application/json') { + // ld+json looks better because keys are a different color options.mode = 'application/ld+json'; } @@ -118,7 +145,7 @@ class Editor extends Component { * Wrapper function to add extra behaviour to our onChange event * @param doc CodeMirror document */ - codemirrorValueChanged (doc) { + _codemirrorValueChanged (doc) { // Update our cached value var newValue = doc.getValue(); this._currentCodemirrorValue = newValue; @@ -138,10 +165,10 @@ class Editor extends Component { } /** - * Set the CodeMirror value without triggering the onChange event + * Sets the CodeMirror value without triggering the onChange event * @param code the code to set in the editor */ - codemirrorSetValue (code) { + _codemirrorSetValue (code) { this._ignoreNextChange = true; this.codeMirror.setValue(code); } @@ -149,10 +176,12 @@ class Editor extends Component { render () { return (
- +
); } diff --git a/app/components/Input.js b/app/components/base/Input.js similarity index 100% rename from app/components/Input.js rename to app/components/base/Input.js diff --git a/app/containers/App.js b/app/containers/App.js index 608dfad80..9fc2108cd 100644 --- a/app/containers/App.js +++ b/app/containers/App.js @@ -3,7 +3,8 @@ import {connect} from 'react-redux' import {bindActionCreators} from 'redux' import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'; -import CodeEditor from '../components/CodeEditor' +import CodeEditor from '../components/base/Editor' +import RequestBodyEditor from '../components/RequestBodyEditor' import UrlInput from '../components/UrlInput' import Sidebar from '../components/Sidebar' @@ -14,8 +15,7 @@ import * as GlobalActions from '../actions/global' Tabs.setUseDefaultStyles(false); class App extends Component { - renderPageBody() { - const {actions, activeRequest} = this.props; + renderPageBody (actions, activeRequest) { if (!activeRequest) { return
; @@ -24,15 +24,16 @@ class App extends Component { const updateRequestBody = actions.updateRequestBody.bind(null, activeRequest.id); const updateRequestUrl = actions.updateRequestUrl.bind(null, activeRequest.id); const updateRequestMethod = actions.updateRequestMethod.bind(null, activeRequest.id); - + return (
- +
@@ -43,10 +44,11 @@ class App extends Component { - + Params Basic Auth @@ -57,8 +59,8 @@ class App extends Component {
-
200 SUCCESS
-
GET https://google.com
+
200 SUCCESS
+
GET https://google.com
@@ -71,19 +73,21 @@ class App extends Component {
) } + render () { - const {actions, loading, activeRequest, allRequests} = this.props; + const {actions, loading, requests} = this.props; + const activeRequest = requests.all.find(r => r.id === requests.active); return (
+ loading={loading} + requests={requests.all}/>
- {this.renderPageBody()} + {this.renderPageBody(actions, activeRequest)}
) @@ -91,16 +95,23 @@ class App extends Component { } App.propTypes = { - allRequests: PropTypes.array.isRequired, - activeRequest: PropTypes.object, + actions: PropTypes.shape({ + activateRequest: PropTypes.func.isRequired, + updateRequestBody: PropTypes.func.isRequired, + updateRequestUrl: PropTypes.func.isRequired, + updateRequestMethod: PropTypes.func.isRequired + }).isRequired, + requests: PropTypes.shape({ + all: PropTypes.array.isRequired, + active: PropTypes.string // "required" but can be null + }).isRequired, loading: PropTypes.bool.isRequired }; function mapStateToProps (state) { return { actions: state.actions, - allRequests: state.requests.all, - activeRequest: state.requests.all.find(r => r.id === state.requests.active), + requests: state.requests, loading: state.loading }; } diff --git a/app/css/components/editor.scss b/app/css/components/editor.scss index ffd2c7df0..ed4407665 100644 --- a/app/css/components/editor.scss +++ b/app/css/components/editor.scss @@ -32,6 +32,9 @@ box-sizing: border-box; margin-bottom: 0; padding-bottom: 0; + + /* Normal Overrides */ + cursor: text; // Show text cursor everywhere (not just in .Codemirror-lines) } .CodeMirror-lines { diff --git a/app/reducers/requests.js b/app/reducers/requests.js index 31a3e5ebd..e52835376 100644 --- a/app/reducers/requests.js +++ b/app/reducers/requests.js @@ -37,8 +37,8 @@ export default function (state = initialState, action) { switch (action.type) { case types.REQUEST_ADD: all = requestsReducer(state.all, action); - return Object.assign({}, state, {all, active}); active = action.request.id; + return Object.assign({}, state, {all, active}); case types.REQUEST_UPDATE: all = requestsReducer(state.all, action); return Object.assign({}, state, {all}); diff --git a/package.json b/package.json index e3341133c..aeefd225b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "dependencies": { "classnames": "^2.2.3", "codemirror": "^5.12.0", + "htmlhint": "^0.9.12", + "jsonlint": "^1.6.2", "jsonschema": "^1.1.0", "react": "^0.14.7", "react-dom": "^0.14.7", diff --git a/webpack/base.config.js b/webpack/base.config.js index 192b87c3d..414616147 100644 --- a/webpack/base.config.js +++ b/webpack/base.config.js @@ -2,6 +2,8 @@ var path = require('path'); var webpack = require('webpack'); module.exports = { + target: 'web', + devtool: 'source-map', context: path.join(__dirname, '../app'), entry: [ './index.js', diff --git a/webpack/dev.config.js b/webpack/dev.config.js index a27675f22..6ecf6f384 100644 --- a/webpack/dev.config.js +++ b/webpack/dev.config.js @@ -7,6 +7,8 @@ base.entry = [ 'webpack/hot/only-dev-server' ].concat(base.entry); + +base.devtool = 'eval'; // Fastest form of source maps base.debug = true; base.devtool = 'inline-source-maps'; base.output.path = path.join(base.output.path, '/dev');