This commit is contained in:
Gregory Schier 2016-03-19 21:00:40 -07:00
parent 3ebb3ed00d
commit 1452c39b67
31 changed files with 555 additions and 277 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["es2015", "react"]
}

View File

@ -0,0 +1,22 @@
// import {expect, unmock, it, describe} from 'jest';
jest.unmock('../components/Sidebar');
import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';
import Sidebar from '../components/Sidebar';
describe('Sidebar', () => {
it('changes the text after click', () => {
// Render a checkbox with label in the document
const sidebar = TestUtils.renderIntoDocument(
<div><Sidebar /></div>
);
const sidebarNode = ReactDOM.findDOMNode(sidebar);
// Verify that it's Off by default
expect(sidebarNode.textContent).toEqual('Off');
});
});

23
app/actions/global.js Normal file
View File

@ -0,0 +1,23 @@
import * as types from '../constants/actionTypes';
import {LOCALSTORAGE_KEY} from "../constants/global";
export function restoreState () {
return (dispatch) => {
setTimeout(() => {
let state = undefined;
try {
state = JSON.parse(localStorage[LOCALSTORAGE_KEY]);
} catch (e) { }
dispatch({type: types.GLOBAL_STATE_RESTORED, state});
}, 300);
}
}
export function loadStart () {
return {type: types.GLOBAL_LOAD_START};
}
export function loadStop () {
return {type: types.GLOBAL_LOAD_STOP};
}

View File

@ -1,69 +0,0 @@
import * as types from '../constants/ActionTypes'
export function addTodo (text) {
return (dispatch, getState) => {
dispatch(loadStart());
setTimeout(() => {
let id = Date.now();
let completed = false;
localStorage['todos'] = JSON.stringify(
[...JSON.parse(localStorage['todos'] || '[]'), {
text: text,
id: id,
completed: completed
}]
);
dispatch({type: types.ADD_TODO, text, id, completed});
dispatch(loadStop());
}, 300);
};
}
export function loadTodos () {
return (dispatch) => {
dispatch(loadStart());
setTimeout(() => {
let todos = JSON.parse(localStorage['todos'] || '[]');
dispatch({type: types.LOAD_TODOS, todos})
}, 300);
}
}
export function loadStart () {
return {type: types.LOAD_START};
}
export function loadStop () {
return {type: types.LOAD_STOP};
}
export function deleteTodo (id) {
return (dispatch) => {
dispatch(loadStart());
setTimeout(() => {
localStorage['todos'] = JSON.stringify(
JSON.parse(localStorage['todos'] || '[]').filter(t => t.id !== id)
);
dispatch({type: types.DELETE_TODO, id});
dispatch(loadStop());
}, 300);
}
}
export function editTodo (id, text) {
return {type: types.EDIT_TODO, id, text};
}
export function completeTodo (id) {
return {type: types.COMPLETE_TODO, id};
}
export function completeAll () {
return {type: types.COMPLETE_ALL};
}
export function clearCompleted () {
return {type: types.CLEAR_COMPLETED};
}

62
app/actions/requests.js Normal file
View File

@ -0,0 +1,62 @@
import * as types from '../constants/actionTypes'
import * as methods from '../constants/global'
import {loadStart} from "./global";
import {loadStop} from "./global";
function defaultRequest () {
return {
id: null,
_mode: 'json',
created: 0,
modified: 0,
name: '',
method: methods.METHOD_GET,
body: '',
params: [],
headers: [],
authentication: {}
}
}
/**
* Build a new request from a subset of fields
* @param request values to override defaults with
* @returns {*}
*/
function buildRequest (request) {
// Build the required fields
const id = request.id || `req_${Date.now()}`;
const created = request.created || Date.now();
const modified = request.modified || Date.now();
// Create the request
return Object.assign({}, defaultRequest(), request, {
id, created, modified
});
}
export function addRequest (name = 'My Request') {
return (dispatch) => {
dispatch(loadStart());
const request = buildRequest({name});
setTimeout(() => {
dispatch({type: types.REQUEST_ADD, request});
dispatch(loadStop());
}, 500);
};
}
export function updateRequest (requestPatch) {
return (dispatch) => {
dispatch(loadStart());
const newRequest = Object.assign({}, requestPatch, {modified: Date.now()});
const request = buildRequest(newRequest);
setTimeout(() => {
dispatch({type: types.REQUEST_UPDATE, request});
dispatch(loadStop());
}, 500);
};
}

40
app/components/App.js Normal file
View File

@ -0,0 +1,40 @@
import React, {PropTypes, Component} from 'react'
import Sidebar from './Sidebar'
import RequestPane from './RequestPane'
import ResponsePane from './ResponsePane'
class App extends Component {
renderEditor () {
const {updateRequest, requests} = this.props;
return (
<div className="grid">
<RequestPane
updateRequest={updateRequest}
request={requests.active}/>
<ResponsePane request={requests.active}/>
</div>
)
}
render () {
const {addRequest, loading, requests} = this.props;
return (
<div className="grid bg-dark">
<Sidebar
addRequest={addRequest}
loading={loading}
requests={requests}/>
{requests.active ? this.renderEditor() : <div></div>}
</div>
)
}
}
App.propTypes = {
addRequest: PropTypes.func.isRequired,
updateRequest: PropTypes.func.isRequired,
requests: PropTypes.object.isRequired,
loading: PropTypes.bool.isRequired
};
export default App;

View File

@ -50,32 +50,20 @@ class Editor extends Component {
componentDidMount () { componentDidMount () {
var textareaNode = this.refs.textarea; var textareaNode = this.refs.textarea;
let options = Object.assign({
theme: 'monokai',
scrollPastEnd: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
cursorScrollMargin: 60,
extraKeys: {
"Ctrl-Q": function (cm) {
cm.foldCode(cm.getCursor());
}
},
scrollbarStyle: 'overlay'
}, this.props.options || {});
if (options.mode === 'application/json') { this.codeMirror = CodeMirror.fromTextArea(textareaNode);
options.mode = 'application/ld+json';
}
this.codeMirror = CodeMirror.fromTextArea(textareaNode, options);
this.codeMirror.on('change', this.codemirrorValueChanged.bind(this)); this.codeMirror.on('change', this.codemirrorValueChanged.bind(this));
this.codeMirror.on('focus', this.focusChanged.bind(this, true)); this.codeMirror.on('focus', this.focusChanged.bind(this, true));
this.codeMirror.on('blur', this.focusChanged.bind(this, false)); this.codeMirror.on('blur', this.focusChanged.bind(this, false));
this._currentCodemirrorValue = this.props.defaultValue || this.props.value || ''; this._currentCodemirrorValue = this.props.defaultValue || this.props.value || '';
this.codemirrorSetOptions(this.props.options);
this.codeMirror.setValue(this._currentCodemirrorValue); this.codeMirror.setValue(this._currentCodemirrorValue);
} }
componentDidUpdate () {
this.codemirrorSetOptions(this.props.options);
}
componentWillUnmount () { componentWillUnmount () {
// todo: is there a lighter-weight way to remove the cm instance? // todo: is there a lighter-weight way to remove the cm instance?
if (this.codeMirror) { if (this.codeMirror) {
@ -96,10 +84,6 @@ class Editor extends Component {
} }
} }
getCodeMirror () {
return this.codeMirror;
}
focus () { focus () {
if (this.codeMirror) { if (this.codeMirror) {
this.codeMirror.focus(); this.codeMirror.focus();
@ -113,18 +97,51 @@ class Editor extends Component {
this.props.onFocusChange && this.props.onFocusChange(focused); this.props.onFocusChange && this.props.onFocusChange(focused);
} }
codemirrorValueChanged (doc, change) { codemirrorSetOptions (options) {
options = Object.assign({
theme: 'monokai',
scrollPastEnd: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
cursorScrollMargin: 60,
extraKeys: {
"Ctrl-Q": function (cm) {
cm.foldCode(cm.getCursor());
}
},
scrollbarStyle: 'overlay'
}, options || {});
if (options.mode === 'json') {
options.mode = 'application/json';
}
if (options.mode === 'application/json') {
options.mode = 'application/ld+json';
}
Object.keys(options).map(key => {
this.codeMirror.setOption(key, options[key]);
});
}
codemirrorValueChanged (doc) {
clearTimeout(this._timeout);
this._timeout = setTimeout(() => {
var newValue = doc.getValue(); var newValue = doc.getValue();
this._currentCodemirrorValue = newValue; this._currentCodemirrorValue = newValue;
this.props.onChange && this.props.onChange(newValue); this.props.onChange && this.props.onChange(newValue);
}, (this.props.debounceMillis || 0));
} }
render () { render () {
return ( return (
<div className={`editor ${this.props.className || ''}`}>
<textarea name={this.props.path} <textarea name={this.props.path}
ref='textarea' ref='textarea'
defaultValue={this.props.value} defaultValue={this.props.value}
autoComplete='off'></textarea> autoComplete='off'></textarea>
</div>
); );
} }
} }
@ -135,7 +152,8 @@ Editor.propTypes = {
options: PropTypes.object, options: PropTypes.object,
path: PropTypes.string, path: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
className: PropTypes.any className: PropTypes.any,
debounceMillis: PropTypes.number
}; };
export default Editor; export default Editor;

View File

@ -1,9 +1,21 @@
import React from 'react' import React, {Component, PropTypes} from 'react'
import Editor from '../components/Editor' import Editor from '../components/Editor'
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs';
const RequestPane = (props) => ( // Don't inject component styles (use our own)
Tabs.setUseDefaultStyles(false);
class RequestPane extends Component {
shouldComponentUpdate (nextProps, nextState) {
return nextProps.request !== this.props.request
}
render () {
const {request, updateRequest} = this.props;
return (
<section id="request" className="pane col grid-v"> <section id="request" className="pane col grid-v">
<header className="pane-header pane-header-no-padding bg-super-light"> <header className="pane__header bg-super-light">
<div className="form-control url-input"> <div className="form-control url-input">
<div className="grid"> <div className="grid">
<button className="btn method-dropdown"> <button className="btn method-dropdown">
@ -16,21 +28,41 @@ const RequestPane = (props) => (
</div> </div>
</div> </div>
</header> </header>
<div className="pane-body grid-v"> <div className="pane__body">
<div className="bg-dark pane-tabs"> <Tabs selectedIndex={1} className="grid-v">
{['Query Params', 'Body', 'Headers', 'Basic Auth'].map((name => <TabList className="grid pane__header">
<button key={name} className={'btn ' + (name === 'Body' ? 'bg-dark' : 'bg-dark')}> <Tab>
{name} <button className="btn">Params</button>
</button> </Tab>
))} <Tab>
</div> <button className="btn">Body</button>
<Editor value={localStorage['json']} </Tab>
onChange={(v) => {localStorage['json'] = v}} <Tab>
options={{mode: 'application/json', lineNumbers: true}}/> <button className="btn">Basic Auth</button>
</Tab>
<Tab>
<button className="btn">Headers</button>
</Tab>
</TabList>
<TabPanel className="col">Params</TabPanel>
<TabPanel className="col">
<Editor value={request.body}
onChange={(body) => updateRequest(Object.assign({}, request, {body}) )}
debounceMillis={500}
options={{mode: request._mode, lineNumbers: true}}/>
</TabPanel>
<TabPanel className="col">Basic Auth</TabPanel>
<TabPanel className="col">Headers</TabPanel>
</Tabs>
</div> </div>
</section> </section>
); );
}
}
RequestPane.propTypes = {}; RequestPane.propTypes = {
updateRequest: PropTypes.func.isRequired,
request: PropTypes.object.isRequired
};
export default RequestPane; export default RequestPane;

View File

@ -1,20 +1,22 @@
import React from 'react' import React, {PropTypes} from 'react'
import Editor from '../components/Editor' import Editor from '../components/Editor'
const ResponsePane = (props) => ( const ResponsePane = (props) => (
<section id="response" className="pane col grid-v"> <section id="response" className="pane col grid-v">
<header className="pane-header header-no-padding text-center bg-super-light"> <header className="pane__header text-center bg-super-light">
<div> <div className="pane__header__content">
<div className="tag success"><strong>200</strong> SUCCESS</div> <div className="tag success"><strong>200</strong> SUCCESS</div>
<div className="tag"><strong>GET</strong> https://google.com</div> <div className="tag"><strong>GET</strong> https://google.com</div>
</div> </div>
</header> </header>
<div className="pane-body"> <div className="pane__body">
<Editor value={'{}'} options={{mode: 'application/json', lineNumbers: true}}></Editor> <Editor value={'{}'} options={{mode: 'application/json'}}></Editor>
</div> </div>
</section> </section>
); );
ResponsePane.propTypes = {}; ResponsePane.propTypes = {
request: PropTypes.object.isRequired
};
export default ResponsePane; export default ResponsePane;

View File

@ -1,22 +1,43 @@
import React from 'react' import React, {PropTypes} from 'react'
const Sidebar = (props) => ( const Sidebar = (props) => (
<aside id="sidebar" className="pane"> <aside id="sidebar" className="pane">
<header className="pane-header pane-header-clickable bg-primary"> <header className="pane__header bg-primary">
<h2><a href="#">Insomnia</a></h2> <h2>
<a href="#" className="pane__header__content">
{props.loading ? <i className="fa fa-refresh fa-spin pull-right"></i> : ''}
Insomnia
</a>
</h2>
</header> </header>
<div className="pane-body"> <div className="pane__body">
<ul className="sidebar-items"> <ul className="sidebar-items">
{[0, 1, 2, 3, 4].map((i) => { <li className="grid">
return <li key={i} className={'sidebar-item ' + (i === 0 ? 'active': '')}> <div className="form-control col">
<a href="#">Item 1</a> <input type="text" placeholder="Filter Requests"/>
</li>; </div>
<button className="btn" onClick={(e) => props.addRequest()}>
<i className="fa fa-plus-circle"></i>
</button>
</li>
</ul>
<ul className="sidebar-items">
{props.requests.all.map((request) => {
const isActive = request.id === props.requests.active.id;
return (
<li key={request.id} className={'sidebar-item ' + (isActive ? 'active': '')}>
<a href="#">{request.name}</a>
</li>
);
})} })}
</ul> </ul>
</div> </div>
</aside> </aside>
); );
Sidebar.propTypes = {}; Sidebar.propTypes = {
requests: PropTypes.object.isRequired,
loading: PropTypes.bool.isRequired
};
export default Sidebar; export default Sidebar;

View File

@ -1 +1,10 @@
export const ADD_REQUEST = 'ADD_REQUEST'; // Global
export const GLOBAL_LOAD_START = 'GLOBAL.LOAD_START';
export const GLOBAL_LOAD_STOP = 'GLOBAL.LOAD_STOP';
export const GLOBAL_STATE_SAVED = 'GLOBAL.STATE_SAVED';
export const GLOBAL_STATE_RESTORED = 'GLOBAL.STATE_RESTORED';
// Requests
export const REQUEST_ADD = 'REQUEST.ADD';
export const REQUEST_UPDATE = 'REQUEST.UPDATE';
export const REQUESTS_LOADED = 'REQUESTS.LOADED';

11
app/constants/global.js Normal file
View File

@ -0,0 +1,11 @@
// Global
export const LOCALSTORAGE_DEBOUNCE_MILLIS = 1000;
export const LOCALSTORAGE_KEY = 'insomnia';
// HTTP Methods
export const METHOD_GET = 'GET';
export const METHOD_PUT = 'PUT';
export const METHOD_POST = 'POST';
export const METHOD_DELETE = 'DELETE';
export const METHOD_OPTIONS = 'OPTIONS';
export const METHOD_HEAD = 'HEAD';

View File

@ -1,42 +0,0 @@
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import Sidebar from '../components/Sidebar'
import RequestPane from '../components/RequestPane'
import ResponsePane from '../components/ResponsePane'
class App extends Component {
render () {
//const { global, todos, actions } = this.props;
return (
<div className="grid bg-dark">
<Sidebar />
<RequestPane />
<ResponsePane />
</div>
)
};
}
App.propTypes = {
//todos: PropTypes.array.isRequired,
//global: PropTypes.object.isRequired,
//actions: PropTypes.object.isRequired
};
function mapStateToProps (state) {
return {
//todos: state.todos,
//global: state.global
}
}
function mapDispatchToProps (dispatch) {
return {
//actions: bindActionCreators(TodoActions, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App)

View File

@ -0,0 +1,45 @@
import React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import App from '../components/App.js';
import * as RequestActions from '../actions/requests'
import * as GlobalActions from '../actions/global'
class AppWrapper extends Component {
componentDidMount() {
this.props.actions.restoreState();
}
render() {
const {actions, requests, loading, initialized} = this.props;
if (!initialized) {
return <div>Loading...</div>
} else {
return <App
addRequest={actions.addRequest}
updateRequest={actions.updateRequest}
requests={requests}
loading={loading}/>
}
}
}
function mapStateToProps (state) {
return state;
}
function mapDispatchToProps (dispatch) {
return {
actions: Object.assign(
{},
bindActionCreators(GlobalActions, dispatch),
bindActionCreators(RequestActions, dispatch)
)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(AppWrapper);

View File

@ -25,6 +25,10 @@ h2 {
text-align: center; text-align: center;
} }
.pull-right {
float: right;
}
strong { strong {
font-weight: 600; font-weight: 600;
} }

View File

@ -1,26 +1,31 @@
@import '../constants/colors'; @import '../constants/colors';
.CodeMirror { .editor {
height: 100%;
.CodeMirror {
height: 100% !important; height: 100% !important;
width: 100%; width: 100%;
font-size: 13px !important; font-size: 12px !important;
font-family: "Source Code Pro", monospace; font-family: "Source Code Pro", monospace;
padding: 5px 0; padding: 5px 0;
box-sizing: border-box; box-sizing: border-box;
} }
.CodeMirror, .CodeMirror,
.cm-s-seti.CodeMirror, // Hack because seti theme is dumb .cm-s-seti.CodeMirror, // Hack because seti theme is dumb
.CodeMirror-gutters, .CodeMirror-gutters,
.CodeMirror-scrollbar-filler, .CodeMirror-scrollbar-filler,
.CodeMirror-gutter-filler { .CodeMirror-gutter-filler {
background-color: $bg-dark !important; background-color: $bg-dark !important;
} }
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { .CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
background: lighten($bg-dark, 10%); background: lighten($bg-dark, 10%);
}
.CodeMirror-linenumber, .CodeMirror-guttermarker-subtle {
color: #555555 !important;
}
} }
.CodeMirror-linenumber, .CodeMirror-guttermarker-subtle {
color: #555555 !important;
}

View File

@ -1,7 +1,7 @@
@import '../constants/colors'; @import '../constants/colors';
#response { #response {
.pane-header { .pane__header {
.tag { // HACK: Find some way to center this vertically .tag { // HACK: Find some way to center this vertically
margin-top: -4px; margin-top: -4px;

View File

@ -0,0 +1,9 @@
.ReactTabs {
.ReactTabs__Tab {
opacity: 0.5;
}
.ReactTabs__Tab--selected {
opacity: 1;
}
}

View File

@ -28,6 +28,14 @@ $font-dark-bg: #dddddd;
&, a, textarea, input, .btn { &, a, textarea, input, .btn {
color: $font-dark-bg; color: $font-dark-bg;
} }
.btn:hover {
background: lighten($bg-dark, 4%);
}
.btn:active {
background: lighten($bg-dark, 6%);
}
} }
.bg-light { .bg-light {

View File

@ -13,6 +13,7 @@
} }
.col { .col {
display: block;
flex: 1 1 auto; flex: 1 1 auto;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -33,7 +34,14 @@
} }
.row { .row {
height: 100%; display: block;
flex: 1 1 auto; flex: 1 1 auto;
height: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
.un-grid {
display: initial;
height: auto;
width: auto;
}

View File

@ -19,3 +19,4 @@
@import 'components/request'; @import 'components/request';
@import 'components/response'; @import 'components/response';
@import 'components/tag'; @import 'components/tag';
@import 'components/tabs';

View File

@ -2,7 +2,6 @@
@import '../constants/colors'; @import '../constants/colors';
.form-control { .form-control {
width: 100%;
outline: none; outline: none;
border: 0; border: 0;
height: $form-height; height: $form-height;

View File

@ -2,29 +2,22 @@
@import '../constants/dimensions'; @import '../constants/dimensions';
.pane { .pane {
.pane-header { .pane__header {
z-index: 1; z-index: 1;
position: relative; position: relative;
border-right: 2px solid rgba(0,0,0,0.05); border-right: 2px solid rgba(0, 0, 0, 0.04);
height: $header-height;
&.pane-header-clickable a, &:not(.pane-header-clickable) > *:first-child { .pane__header__content {
display: block; display: block;
height: $header-height; height: $header-height;
box-sizing: border-box; box-sizing: border-box;
padding: $default-padding; padding: $default-padding;
} }
&:not(.pane-header_clickable).pane-header-no-padding > *:first-child {
padding: 0;
} }
&.pane-header-clickable.header-no-padding a { .pane__body {
padding: 0;
}
}
.pane-body {
height: 100%; height: 100%;
border-right: 2px solid rgba(0,0,0,0.1); border-right: 1px solid rgba(0, 0, 0, 0.1);
} }
} }

View File

@ -1,8 +1,9 @@
import React from 'react' import React from 'react'
import { render } from 'react-dom' import {render} from 'react-dom'
import { Provider } from 'react-redux' import {Provider} from 'react-redux'
import App from './containers/App'
import configureStore from './stores/configureStore' import configureStore from './stores/configureStore'
import AppWrapper from './containers/AppWrapper'
// Global CSS // Global CSS
import './css/index.scss' import './css/index.scss'
@ -12,6 +13,6 @@ import './css/lib/fontawesome/css/font-awesome.css'
const store = configureStore(); const store = configureStore();
render( render(
<Provider store={store}><App /></Provider>, <Provider store={store}><AppWrapper /></Provider>,
document.getElementById('root') document.getElementById('root')
); );

View File

@ -0,0 +1,13 @@
import {LOCALSTORAGE_DEBOUNCE_MILLIS} from "../constants/global";
let timeout = null;
export default (store) => next => action => {
let result = next(action);
clearTimeout(timeout);
timeout = setTimeout(() => {
localStorage['insomnia'] = JSON.stringify(store.getState(), null, 2);
}, LOCALSTORAGE_DEBOUNCE_MILLIS);
return result;
};

29
app/reducers/global.js Normal file
View File

@ -0,0 +1,29 @@
import * as types from '../constants/actionTypes';
import requestsReducer from './requests'
import settingsReducer from './settings'
const initialState = {
initialized: false,
loading: false
};
export default function (state = initialState, action) {
switch (action.type) {
case types.GLOBAL_STATE_SAVED:
return state;
case types.GLOBAL_STATE_RESTORED:
return Object.assign({}, state, action.state, {initialized: true});
case types.GLOBAL_LOAD_START:
return Object.assign({}, state, {loading: true});
case types.GLOBAL_LOAD_STOP:
return Object.assign({}, state, {loading: false});
default:
// Send everything else to the child reducers
const requests = requestsReducer(state.requests, action);
const settings = settingsReducer(state.settings, action);
return Object.assign({}, state, {
settings,
requests
});
}
};

View File

@ -1,8 +0,0 @@
import { combineReducers } from 'redux'
import requests from './requests'
const rootReducer = combineReducers({
requests
});
export default rootReducer;

View File

@ -1,22 +1,33 @@
import * as types from '../constants/ActionTypes'; import * as types from "../constants/actionTypes";
function request (state, action) { const initialState = {
all: [],
active: null
};
function requestsReducer (state = [], action) {
switch (action.type) { switch (action.type) {
case types.ADD_REQUEST: case types.REQUEST_ADD:
return { return [...state, action.request];
foo: 'bar' case types.REQUEST_UPDATE:
}; return state.map(request => request.id === action.request.id ? action.request : request);
default: default:
return state; return state;
} }
} }
export default function requests (state = [], action) { export default function (state = initialState, action) {
let all, active;
switch (action.type) { switch (action.type) {
case types.ADD_REQUEST: case types.REQUEST_ADD:
return [...state, request(undefined, action)]; all = requestsReducer(state.all, action);
active = state.active || action.request;
return Object.assign({}, state, {all, active});
case types.REQUEST_UPDATE:
all = requestsReducer(state.all, action);
active = state.active.id === action.request.id ? action.request : state.active;
return Object.assign({}, state, {all, active});
default: default:
return state; return state
} }
} }

11
app/reducers/settings.js Normal file
View File

@ -0,0 +1,11 @@
import * as types from '../constants/actionTypes';
const initialState = {
};
export default function (state = initialState, action) {
switch (action.type) {
default:
return state
}
}

View File

@ -1,9 +1,12 @@
import { createStore, applyMiddleware } from 'redux' import {createStore, applyMiddleware} from 'redux'
import thunkMiddleware from 'redux-thunk' import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger' import createLogger from 'redux-logger'
import rootReducer from '../reducers' import rootReducer from '../reducers/global'
import localStorageMiddleware from '../middleware/localstorage'
const loggerMiddleware = createLogger(); const loggerMiddleware = createLogger({
collapsed: true
});
export default function configureStore (initialState) { export default function configureStore (initialState) {
const store = createStore( const store = createStore(
@ -11,14 +14,15 @@ export default function configureStore (initialState) {
initialState, initialState,
applyMiddleware( applyMiddleware(
thunkMiddleware, thunkMiddleware,
localStorageMiddleware,
loggerMiddleware loggerMiddleware
) )
); );
if (module.hot) { if (module.hot) {
// Enable Webpack hot module replacement for reducers // Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => { module.hot.accept('../reducers/global', () => {
const nextReducer = require('../reducers').default; const nextReducer = require('../reducers/global').default;
store.replaceReducer(nextReducer); store.replaceReducer(nextReducer);
}) })
} }

View File

@ -7,12 +7,12 @@
"url": "git@bitbucket.org:gschier/insomnia.git" "url": "git@bitbucket.org:gschier/insomnia.git"
}, },
"dependencies": { "dependencies": {
"babel-polyfill": "^6.7.2",
"classnames": "^2.2.3", "classnames": "^2.2.3",
"codemirror": "^5.12.0", "codemirror": "^5.12.0",
"react": "^0.14.7", "react": "^0.14.7",
"react-dom": "^0.14.7", "react-dom": "^0.14.7",
"react-redux": "^4.4.1", "react-redux": "^4.4.1",
"react-tabs": "^0.5.3",
"redux": "^3.3.1", "redux": "^3.3.1",
"redux-logger": "^2.6.1", "redux-logger": "^2.6.1",
"redux-thunk": "^2.0.1", "redux-thunk": "^2.0.1",
@ -20,12 +20,17 @@
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.7.2", "babel-core": "^6.7.2",
"babel-jest": "^9.0.3",
"babel-loader": "^6.2.4", "babel-loader": "^6.2.4",
"babel-polyfill": "^6.7.2",
"babel-preset-es2015": "^6.6.0", "babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0", "babel-preset-react": "^6.5.0",
"css-loader": "^0.23.1", "css-loader": "^0.23.1",
"file-loader": "^0.8.5", "file-loader": "^0.8.5",
"jest": "^0.1.40",
"jest-cli": "^0.9.2",
"node-sass": "^3.4.2", "node-sass": "^3.4.2",
"react-addons-test-utils": "^0.14.7",
"react-hot-loader": "^1.3.0", "react-hot-loader": "^1.3.0",
"sass-loader": "^3.2.0", "sass-loader": "^3.2.0",
"style-loader": "^0.13.0", "style-loader": "^0.13.0",
@ -34,6 +39,14 @@
}, },
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"test": "jest",
"build": "webpack --config webpack/prod.config.js # --optimize-minimize" "build": "webpack --config webpack/prod.config.js # --optimize-minimize"
},
"jest": {
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/react",
"<rootDir>/node_modules/react-dom",
"<rootDir>/node_modules/react-addons-test-utils"
]
} }
} }