Refactor and a bunch of fixes

This commit is contained in:
Gregory Schier 2016-03-20 16:20:00 -07:00
parent 43214dd1d3
commit 09baf21d3a
13 changed files with 177 additions and 70 deletions

View File

@ -31,6 +31,7 @@ describe('Requests Actions', () => {
modified: 1000000000000,
name: 'Test Request',
method: 'GET',
url: '',
body: '',
headers: [],
params: [],
@ -61,6 +62,7 @@ describe('Requests Actions', () => {
_mode: 'json',
created: 1000000000000,
modified: 1000000000000,
url: '',
name: 'Test Request',
method: 'GET',
body: '',
@ -73,8 +75,9 @@ describe('Requests Actions', () => {
{type: types.GLOBAL_LOAD_START},
{
type: types.REQUEST_UPDATE,
request: {
patch: {
method: 'POST',
id: 'rq_1000000000000',
modified: 1000000000000
}
},
@ -84,7 +87,7 @@ describe('Requests Actions', () => {
const store = mockStore({});
store.dispatch(addRequest('Test Request'));
jest.runAllTimers();
store.dispatch(updateRequest({method: 'POST'}));
store.dispatch(updateRequest({id: 'rq_1000000000000', method: 'POST'}));
jest.runAllTimers();
const actions = store.getActions();

View File

@ -9,6 +9,7 @@ function defaultRequest () {
_mode: 'json',
created: 0,
modified: 0,
url: '',
name: '',
method: methods.METHOD_GET,
body: '',
@ -30,7 +31,7 @@ function buildRequest (request) {
const modified = request.modified || Date.now();
// Create the request
return Object.assign({}, defaultRequest(), request, {
return Object.assign(defaultRequest(), request, {
id, created, modified
});
}
@ -51,21 +52,34 @@ export function addRequest (name = 'My Request') {
}
export function updateRequest (requestPatch) {
if (!requestPatch.id) {
throw new Error('Cannot update request without id');
}
return (dispatch) => {
dispatch(loadStart());
const request = Object.assign({}, requestPatch, {modified: Date.now()});
dispatch({type: types.REQUEST_UPDATE, request});
const modified = Date.now();
const patch = Object.assign({}, requestPatch, {modified});
dispatch({type: types.REQUEST_UPDATE, patch});
return new Promise((resolve) => {
setTimeout(() => {
dispatch(loadStop());
resolve();
}, 500);
}, 800);
});
};
}
export function activateRequest (request) {
return {type: types.REQUEST_ACTIVATE, request: request};
export function updateRequestUrl (id, url) {
return updateRequest({id, url});
}
export function updateRequestBody (id, body) {
return updateRequest({id, body});
}
export function activateRequest (id) {
return {type: types.REQUEST_ACTIVATE, id};
}

View File

@ -70,6 +70,7 @@ class Editor extends Component {
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._currentCodemirrorValue = this.props.defaultValue || this.props.value || '';
this.codemirrorSetValue(this._currentCodemirrorValue);
this.codemirrorSetOptions(this.props.options);

51
app/components/Input.js Normal file
View File

@ -0,0 +1,51 @@
import React, {Component, PropTypes} from 'react';
const DEFAULT_DEBOUNCE_MILLIS = 300;
class Input extends Component {
valueChanged (e) {
if (!this.props.onChange) {
return;
}
// Surround in closure because callback may change before debounce
clearTimeout(this._timeout);
((value, cb, debounceMillis = DEFAULT_DEBOUNCE_MILLIS) => {
this._timeout = setTimeout(() => cb(value), debounceMillis);
})(e.target.value, this.props.onChange, this.props.debounceMillis);
}
updateValueFromProps() {
this.refs.input.value = this.props.initialValue || this.props.value || '';
}
componentDidMount () {
this.updateValueFromProps()
}
componentDidUpdate () {
this.updateValueFromProps()
}
render () {
const {initialValue, value} = this.props;
return (
<input
ref="input"
type="text"
initialValue={initialValue || value}
onChange={this.valueChanged.bind(this)}
placeholder="https://google.com"
/>
)
}
}
Input.propTypes = {
onChange: PropTypes.func.isRequired,
initialValue: PropTypes.string,
debounceMillis: PropTypes.number,
value: PropTypes.string
};
export default Input;

View File

@ -1,5 +1,6 @@
import React, {Component, PropTypes} from 'react'
import Editor from '../components/Editor'
import CodeEditor from '../components/CodeEditor'
import UrlInput from '../components/UrlInput'
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs';
// Don't inject component styles (use our own)
@ -11,21 +12,14 @@ class RequestPane extends Component {
}
render () {
const {request, updateRequest} = this.props;
const {request, updateRequestBody, updateRequestUrl} = this.props;
return (
<section id="request" className="pane col grid-v">
<header className="pane__header bg-super-light">
<div className="form-control url-input">
<div className="grid">
<button className="btn method-dropdown">
POST&nbsp;&nbsp;<i className="fa fa-caret-down"></i>
</button>
<input type="text" placeholder="https://google.com"/>
<button className="btn send-request-button">
<i className="fa fa-repeat txt-xl"></i>
</button>
</div>
<UrlInput onUrlChange={updateRequestUrl}
urlValue={request.url}/>
</div>
</header>
<div className="pane__body">
@ -46,8 +40,8 @@ class RequestPane extends Component {
</TabList>
<TabPanel className="col">Params</TabPanel>
<TabPanel className="col">
<Editor value={request.body}
onChange={(body) => updateRequest(Object.assign({}, request, {body}) )}
<CodeEditor value={request.body}
onChange={updateRequestBody}
options={{mode: request._mode, lineNumbers: true}}/>
</TabPanel>
<TabPanel className="col">Basic Auth</TabPanel>
@ -60,7 +54,8 @@ class RequestPane extends Component {
}
RequestPane.propTypes = {
updateRequest: PropTypes.func.isRequired,
updateRequestUrl: PropTypes.func.isRequired,
updateRequestBody: PropTypes.func.isRequired,
request: PropTypes.object.isRequired
};

View File

@ -1,5 +1,5 @@
import React, {PropTypes} from 'react'
import Editor from '../components/Editor'
import CodeEditor from '../components/CodeEditor'
const ResponsePane = (props) => (
<section id="response" className="pane col grid-v">
@ -10,7 +10,7 @@ const ResponsePane = (props) => (
</div>
</header>
<div className="pane__body">
<Editor value={'{}'} options={{mode: 'application/json'}}></Editor>
<CodeEditor value={'{}'} options={{mode: 'application/json'}}></CodeEditor>
</div>
</section>
);

View File

@ -22,11 +22,11 @@ const Sidebar = (props) => (
</li>
</ul>
<ul className="sidebar-items">
{props.requests.all.map((request) => {
const isActive = request.id === props.requests.active.id;
{props.requests.map((request) => {
const isActive = request.id === props.activeRequest.id;
return (
<li key={request.id} className={'sidebar-item ' + (isActive ? 'active': '')}>
<a href="#" onClick={() => {props.activateRequest(request)}}>{request.name}</a>
<a href="#" onClick={() => {props.activateRequest(request.id)}}>{request.name}</a>
</li>
);
})}
@ -37,7 +37,8 @@ const Sidebar = (props) => (
Sidebar.propTypes = {
activateRequest: PropTypes.func.isRequired,
requests: PropTypes.object.isRequired,
requests: PropTypes.array.isRequired,
activeRequest: PropTypes.object,
loading: PropTypes.bool.isRequired
};

View File

@ -0,0 +1,30 @@
import React, {Component, PropTypes} from 'react'
import Input from '../components/Input';
class UrlInput extends Component {
render () {
const {onUrlChange, urlValue} = this.props;
return (
<div className="grid">
<button className="btn method-dropdown">
POST&nbsp;&nbsp;<i className="fa fa-caret-down"></i>
</button>
<Input type="text"
placeholder="https://google.com"
initialValue={urlValue}
onChange={onUrlChange}/>
<button className="btn send-request-button">
<i className="fa fa-repeat txt-xl"></i>
</button>
</div>
)
}
}
UrlInput.propTypes = {
onUrlChange: PropTypes.func.isRequired,
urlValue: PropTypes.string.isRequired
};
export default UrlInput;

View File

@ -16,46 +16,52 @@ class App extends Component {
}
renderRequestPane () {
const {actions, requests} = this.props;
const {actions, activeRequest} = this.props;
return (
<RequestPane
updateRequest={actions.updateRequest}
request={requests.active}/>
updateRequestBody={actions.updateRequestBody.bind(null, activeRequest.id)}
updateRequestUrl={actions.updateRequestUrl.bind(null, activeRequest.id)}
request={activeRequest}
/>
)
}
renderResponsePane () {
const {requests} = this.props;
const {activeRequest} = this.props;
return (
<ResponsePane request={requests.active}/>
<ResponsePane request={activeRequest}/>
)
}
render () {
const {actions, loading, requests} = this.props;
const {actions, loading, activeRequest, allRequests} = this.props;
return (
<div className="grid bg-dark">
<Sidebar
activateRequest={actions.activateRequest}
addRequest={actions.addRequest}
loading={loading}
requests={requests}/>
{requests.active ? this.renderRequestPane() : <div></div>}
{requests.active ? this.renderResponsePane() : <div></div>}
activeRequest={activeRequest}
requests={allRequests}
/>
{activeRequest ? this.renderRequestPane() : <div></div>}
{activeRequest ? this.renderResponsePane() : <div></div>}
</div>
)
}
}
App.propTypes = {
requests: PropTypes.object.isRequired,
allRequests: PropTypes.array.isRequired,
activeRequest: PropTypes.object,
loading: PropTypes.bool.isRequired
};
function mapStateToProps (state) {
return {
actions: state.actions,
requests: state.requests,
allRequests: state.requests.all,
activeRequest: state.requests.all.find(r => r.id === state.requests.activeId),
loading: state.loading
};
}

View File

@ -11,7 +11,7 @@ describe('Requests Reducer', () => {
beforeEach(() => {
initialState = {
all: [],
active: null
activeId: null
};
request = {
@ -49,7 +49,7 @@ describe('Requests Reducer', () => {
})
).toEqual({
all: [request],
active: request
activeId: request.id
});
});
@ -59,42 +59,43 @@ describe('Requests Reducer', () => {
request: request
});
const newRequest = Object.assign(
{}, request, {name: 'New Name'}
);
const patch = {
id: request.id,
name: 'New Name'
};
expect(reducer(state, {
type: types.REQUEST_UPDATE,
request: newRequest
patch: patch
})).toEqual({
all: [newRequest],
active: newRequest
all: [Object.assign({}, request, patch)],
activeId: request.id
});
});
it('should not update unknown request', () => {
expect(reducer(initialState, {
type: types.REQUEST_UPDATE,
request: request
patch: {id: 'req_1234567890123'}
})).toEqual(initialState);
});
it('should activate request', () => {
initialState.all = [request];
initialState.active = null;
initialState.activeId = null;
expect(reducer(initialState, {
type: types.REQUEST_ACTIVATE,
request: request
id: request.id
})).toEqual({
all: [request],
active: request
activeId: request.id
});
});
it('should not activate invalid request', () => {
initialState.all = [request];
initialState.active = null;
initialState.activeId = null;
expect(reducer(initialState, {
type: types.REQUEST_ACTIVATE

View File

@ -2,7 +2,7 @@ import * as types from "../constants/actionTypes";
const initialState = {
all: [],
active: null
activeId: null
};
function requestsReducer (state = [], action) {
@ -11,8 +11,8 @@ function requestsReducer (state = [], action) {
return [...state, action.request];
case types.REQUEST_UPDATE:
return state.map(request => {
if (request.id === action.request.id) {
return Object.assign({}, request, action.request);
if (request.id === action.patch.id) {
return Object.assign({}, request, action.patch);
} else {
return request;
}});
@ -22,20 +22,22 @@ function requestsReducer (state = [], action) {
}
export default function (state = initialState, action) {
let all, active;
let all, activeId;
switch (action.type) {
case types.REQUEST_ADD:
all = requestsReducer(state.all, action);
active = state.active || action.request;
return Object.assign({}, state, {all, active});
activeId = state.activeId || action.request.id;
return Object.assign({}, state, {all, activeId});
case types.REQUEST_UPDATE:
all = requestsReducer(state.all, action);
active = state.active;
active = active && active.id === action.request.id ? action.request : active;
return Object.assign({}, state, {all, active});
return Object.assign({}, state, {all});
case types.REQUEST_ACTIVATE:
active = action.request;
return active ? Object.assign({}, state, {active}) : state;
if (!state.all.find(r => r.id === action.id)) {
// Don't set if the request doesn't exist
return state;
} else {
return Object.assign({}, state, {activeId: action.id});
}
default:
return state
}

View File

@ -53,7 +53,8 @@ const requestSchema = {
modified: {type: 'number', minimum: 1000000000000, maximum: 10000000000000},
name: {type: 'string', minLength: 1},
method: {enum: METHODS},
body: {type: 'string', minLength: 0},
url: {type: 'string'},
body: {type: 'string'},
authentication: {
oneOf: [
{$ref: '/BasicAuthentication'},
@ -66,6 +67,7 @@ const requestSchema = {
required: [
'_mode',
'id',
'url',
'created',
'modified',
'name',

View File

@ -13,6 +13,7 @@ describe('RequestSchema', () => {
id: 'rq_1234567890123',
created: Date.now(),
modified: Date.now(),
url: 'https://google.com',
name: 'My Request',
method: 'GET',
body: '{"foo": "bar"}',