Merge branch 'feature/request-group-environments'

This commit is contained in:
Gregory Schier 2016-04-16 09:33:30 -07:00
commit 07a2ea374a
29 changed files with 266 additions and 179 deletions

View File

@ -3,9 +3,6 @@ jest.unmock('../global');
jest.unmock('../../constants/global');
jest.unmock('../../constants/actionTypes');
jest.unmock('../../validators/requestGroup');
jest.unmock('jsonschema');
jest.unmock('redux-thunk');
jest.unmock('redux-mock-store');
describe('RequestGroup Actions', () => {
it('should add valid group', () => {

View File

@ -5,9 +5,6 @@ jest.unmock('../../constants/actionTypes');
jest.unmock('../../validators/request');
jest.unmock('../../reducers/requests');
jest.unmock('../../reducers/global');
jest.unmock('jsonschema');
jest.unmock('redux-thunk');
jest.unmock('redux-mock-store');
// Jest seems to barf when this isn't here
jest.unmock('request');

View File

@ -1,6 +1,6 @@
import * as types from '../constants/actionTypes'
import * as methods from '../constants/global'
import makeRequest from '../lib/request.electron'
import makeRequest from '../lib/request'
import {loadStart, loadStop} from './global'
import {showModal} from './modals'
import {REQUEST_RENAME} from '../constants/modals'
@ -93,7 +93,7 @@ export function sendRequest (request) {
if (err) {
console.error(err);
}
dispatch(setResponse(request.id, response));
dispatch(loadStop());
});

View File

@ -8,7 +8,9 @@ class RequestBodyEditor extends Component {
render () {
const {request, onChange, className} = this.props;
const mode = request.contentType || 'application/json';
// console.log(request);
// const mode = request.contentType || 'application/json';
const mode = 'application/json';
return (
<Editor

View File

@ -138,7 +138,7 @@ class Sidebar extends Component {
return (
<aside className="sidebar bg-dark grid--v">
<header className="header">
<h1><WorkspaceDropdown /></h1>
<WorkspaceDropdown />
</header>
<div className="grid--v grid--start grid__cell">
<ul

View File

@ -49,6 +49,7 @@ const BASE_CODEMIRROR_OPTIONS = {
foldGutter: true,
height: 'auto',
lineWrapping: false,
tabSize: 4,
gutters: [
'CodeMirror-linenumbers',
'CodeMirror-foldgutter',
@ -127,7 +128,7 @@ class Editor extends Component {
options = Object.assign({}, options);
// Strip of charset if there is one
options.mode = options.mode.split(';')[0];
options.mode = options.mode ? options.mode.split(';')[0] : 'text/plain';
if (options.mode === 'json') {
options.mode = 'application/json';

View File

@ -148,7 +148,7 @@ class KeyValueEditor extends Component {
{pairs.map((pair, i) => {
return (
<div key={i} className="grid__cell grid__cell--no-flex grid">
<div className="form-control form-control--underlined grid__cell">
<div className="form-control form-control--underlined form-control--wide grid__cell">
<input
type="text"
placeholder={this.props.namePlaceholder || 'Name'}
@ -159,7 +159,8 @@ class KeyValueEditor extends Component {
onBlur={e => {this._focusedPair = -1}}
onKeyDown={this._keyDown.bind(this)}/>
</div>
<div className="form-control form-control--underlined grid__cell">
<div>&nbsp;&nbsp;&nbsp;</div>
<div className="form-control form-control--underlined form-control--wide grid__cell">
<input
type="text"
placeholder={this.props.valuePlaceholder || 'Value'}
@ -170,6 +171,7 @@ class KeyValueEditor extends Component {
onBlur={e => {this._focusedPair = -1}}
onKeyDown={this._keyDown.bind(this)}/>
</div>
<div>&nbsp;&nbsp;&nbsp;</div>
<div className="grid--v">
<button className="btn btn--compact"
tabIndex="-1"
@ -182,16 +184,18 @@ class KeyValueEditor extends Component {
})}
{maxPairs === undefined || pairs.length < maxPairs ? (
<div className="grid__cell grid__cell--no-flex grid">
<div className="form-control form-control--underlined grid__cell">
<div className="form-control form-control--underlined form-control--wide grid__cell">
<input type="text"
placeholder={this.props.namePlaceholder || 'Name'}
onFocus={e => {this._focusedField = NAME; this._addPair()}}/>
</div>
<div className="form-control form-control--underlined grid__cell">
<div>&nbsp;&nbsp;&nbsp;</div>
<div className="form-control form-control--underlined form-control--wide grid__cell">
<input type="text"
placeholder={this.props.valuePlaceholder || 'Value'}
onFocus={e => {this._focusedField = VALUE; this._addPair()}}/>
</div>
<div>&nbsp;&nbsp;&nbsp;</div>
<div className="grid--v">
<button className="btn btn--compact" disabled={true} tabIndex="-1">
<i className="fa fa-blank"></i>

View File

@ -1,36 +1,6 @@
import React, {Component, PropTypes} from 'react';
import classnames from 'classnames';
const ModalHeader = (props) => (
<div className="modal__header bg-light">
<div className="grid">
<div className="grid__cell pad">
<div className={props.className}>
{props.children}
</div>
</div>
<div className="grid--v">
<button className="btn btn--compact txt-lg" data-close-modal="true">
<i className="fa fa-times"></i>
</button>
</div>
</div>
</div>
);
const ModalBody = (props) => (
<div className={classnames('modal__body', 'grid__cell', 'scrollable', props.className)}>
{props.children}
</div>
);
const ModalFooter = (props) => (
<div className="modal__footer">
<div className={props.className}>
{props.children}
</div>
</div>
);
import React, {Component, PropTypes} from 'react'
import classnames from 'classnames'
import * as ModalActions from '../../actions/modals'
class Modal extends Component {
_handleClick (e) {
@ -70,7 +40,7 @@ class Modal extends Component {
}
componentDidMount () {
this.refs.modal.focus();
this.refs.modal && this.refs.modal.focus();
}
render () {
@ -80,7 +50,8 @@ class Modal extends Component {
className={classnames('modal', 'grid', 'grid--center', this.props.className)}
onKeyDown={this._keyDown.bind(this)}
onClick={this._handleClick.bind(this)}>
<div className={classnames('modal__content', 'grid--v', 'bg-super-light', {tall: this.props.tall})}>
<div
className={classnames('modal__content', 'grid--v', 'bg-super-light', {tall: this.props.tall})}>
{this.props.children}
</div>
</div>
@ -93,4 +64,5 @@ Modal.propTypes = {
tall: PropTypes.bool
};
export {Modal, ModalHeader, ModalBody, ModalFooter};
export default Modal;

View File

@ -0,0 +1,10 @@
import React from 'react';
import classnames from 'classnames';
const ModalBody = (props) => (
<div className={classnames('modal__body', 'grid__cell', 'scrollable', props.className)}>
{props.children}
</div>
);
export default ModalBody;

View File

@ -0,0 +1,11 @@
import React from 'react';
const ModalFooter = (props) => (
<div className="modal__footer">
<div className={props.className}>
{props.children}
</div>
</div>
);
export default ModalFooter;

View File

@ -0,0 +1,20 @@
import React from 'react';
const ModalHeader = (props) => (
<div className="modal__header bg-light">
<div className="grid">
<div className="grid__cell pad">
<div className={props.className}>
{props.children}
</div>
</div>
<div className="grid--v">
<button className="btn btn--compact txt-lg" data-close-modal="true">
<i className="fa fa-times"></i>
</button>
</div>
</div>
</div>
);
export default ModalHeader;

View File

@ -1,5 +1,8 @@
import React, {Component, PropTypes} from 'react';
import {Modal, ModalHeader, ModalBody, ModalFooter} from './Modal'
import React, {Component, PropTypes} from 'react'
import Modal from './Modal'
import ModalBody from './ModalBody'
import ModalHeader from './ModalHeader'
import ModalFooter from './ModalFooter'
class PromptModal extends Component {
_onSubmit (e) {
@ -29,9 +32,9 @@ class PromptModal extends Component {
}
render () {
const {onClose, submitName, headerName} = this.props;
const {onClose, submitName, headerName, ...extraProps} = this.props;
return (
<Modal ref="modal" onClose={onClose}>
<Modal ref="modal" onClose={onClose} {...extraProps}>
<ModalHeader>{headerName}</ModalHeader>
<ModalBody className="wide">
<form onSubmit={this._onSubmit.bind(this)} className="wide pad">

View File

@ -20,6 +20,11 @@ class RequestGroupActionsDropdown extends Component {
<i className="fa fa-edit"></i> Rename
</button>
</li>
<li>
<button>
<i className="fa fa-code"></i> Environment
</button>
</li>
<li>
<button onClick={e => actions.deleteRequestGroup(requestGroup.id)}>
<i className="fa fa-trash-o"></i> Delete

View File

@ -13,7 +13,9 @@ class WorkspaceDropdown extends Component {
<Dropdown right={true} {...other} className="block">
<button className="btn header__content">
<div className="grid grid--center">
<div className="grid__cell">Insomnia</div>
<div className="grid__cell">
<h1 className="no-pad">Insomnia</h1>
</div>
<div className="no-wrap">
{loading ? <i className="fa fa-refresh fa-spin txt-lg"></i> : ''}&nbsp;
<i className="fa fa-caret-down txt-lg"></i>

View File

@ -1,35 +0,0 @@
import React, {Component, PropTypes} from 'react'
import {Modal, ModalHeader, ModalBody, ModalFooter} from '../base/Modal'
import Editor from '../base/Editor'
import * as modalIds from '../../constants/modals'
class EnvironmentEditModal extends Component {
render () {
const editorOptions = {
mode: 'application/json',
placeholder: '{ "base_url": "https://website.com/api" }',
theme: 'neat'
};
return (
<Modal {...this.props}>
<ModalHeader>Edit Environments</ModalHeader>
<ModalBody className="grid--v wide">
<Editor value={undefined}
options={editorOptions}/>
</ModalBody>
<ModalFooter className="text-right">
<button className="btn">Done</button>
</ModalFooter>
</Modal>
);
}
}
EnvironmentEditModal.propTypes = {};
EnvironmentEditModal.defaultProps = {
id: modalIds.ENVIRONMENT_EDITOR
};
export default EnvironmentEditModal;

View File

@ -7,14 +7,10 @@ import * as RequestGroupActions from '../../actions/requestGroups'
import * as RequestActions from '../../actions/requests'
import * as modalIds from '../../constants/modals'
import PromptModal from '../base/PromptModal'
import EnvironmentEditModal from './EnvironmentEditModal'
class Modals extends Component {
class Prompts extends Component {
constructor (props) {
super(props);
this._modals = [
EnvironmentEditModal
];
this._prompts = {};
this._prompts[modalIds.REQUEST_RENAME] = {
header: 'Rename Request',
@ -37,54 +33,33 @@ class Modals extends Component {
const {modals, actions} = this.props;
return (
<div>
<div ref="prompts">
{Object.keys(this._prompts).map(id => {
const promptDef = this._prompts[id];
const modal = modals.find(m => m.id === id);
<div ref="prompts">
{Object.keys(this._prompts).map(id => {
const promptDef = this._prompts[id];
const modal = modals.find(m => m.id === id);
if (!modal) {
return null;
}
if (!modal) {
return null;
}
return (
<PromptModal
key={id}
headerName={promptDef.header}
submitName={promptDef.submit}
defaultValue={modal.data.defaultValue}
onClose={() => actions.hideModal(modal.id)}
onSubmit={value => promptDef.onSubmit(modal, value)}
/>
)
})}
</div>
<div ref="modals">
{this._modals.map(c => {
const id = c.defaultProps.id;
const isVisible = modals.find(m => m.id === id);
const modal = React.createElement(c, {
key: id,
onClose: () => actions.hideModal(id),
tall: true
});
return isVisible ? modal : null;
})}
</div>
<div ref="toasts">
{/*<div className="toast grid">
<div className="toast__message">Request deleted</div>
<button className="btn toast__action">
Undo
</button>
</div> */}
</div>
return (
<PromptModal
key={id}
id={id}
headerName={promptDef.header}
submitName={promptDef.submit}
defaultValue={modal.data.defaultValue}
onClose={() => actions.hideModal(modal.id)}
onSubmit={value => promptDef.onSubmit(modal, value)}
/>
)
})}
</div>
);
}
}
Modals.propTypes = {
Prompts.propTypes = {
actions: PropTypes.shape({
hideModal: PropTypes.func.isRequired,
updateRequestGroup: PropTypes.func.isRequired,
@ -114,5 +89,5 @@ function mapDispatchToProps (dispatch) {
export default connect(
mapStateToProps,
mapDispatchToProps
)(Modals);
)(Prompts);

View File

@ -0,0 +1,65 @@
import React, {Component, PropTypes} from 'react'
import Modal from '../base/Modal'
import ModalBody from '../base/ModalBody'
import ModalHeader from '../base/ModalHeader'
import ModalFooter from '../base/ModalFooter'
import Editor from '../base/Editor'
import KeyValueEditor from '../base/KeyValueEditor'
import * as modalIds from '../../constants/modals'
class RequestGroupEnvironmentEditModal extends Component {
constructor (props) {
super(props);
this.state = {
pairs: []
}
}
_saveChanges () {
this.props.onChange(this.state.pairs);
}
_keyValueChange (pairs) {
this.setState({pairs});
}
render () {
const editorOptions = {
mode: 'application/json',
placeholder: '{ "array": [1, 2, 3, 4] }',
theme: 'neat'
};
return (
<Modal {...this.props}>
<ModalHeader>Environment Variables</ModalHeader>
<ModalBody className="grid--v wide pad">
<div>
<KeyValueEditor onChange={this._keyValueChange.bind(this)}
pairs={this.state.pairs}
namePlaceholder="BASE_URL"
valuePlaceholder="https://api.insomnia.com/v1"/>
</div>
{/*
<h3>Hello</h3>
<Editor value={undefined} options={editorOptions}/>
*/}
</ModalBody>
<ModalFooter className="text-right">
<button className="btn" onClick={this._saveChanges.bind(this)}>Done</button>
</ModalFooter>
</Modal>
);
}
}
RequestGroupEnvironmentEditModal.propTypes = {
// requestGroup: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired
};
RequestGroupEnvironmentEditModal.defaultProps = {
id: modalIds.ENVIRONMENT_EDITOR
};
export default RequestGroupEnvironmentEditModal;

View File

@ -4,17 +4,19 @@ import {bindActionCreators} from 'redux'
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'
import Editor from '../components/base/Editor'
import Modals from '../components/modals/ModalContainer'
import Prompts from '../components/modals/Prompts'
import KeyValueEditor from '../components/base/KeyValueEditor'
import RequestBodyEditor from '../components/RequestBodyEditor'
import RequestAuthEditor from '../components/RequestAuthEditor'
import RequestUrlBar from '../components/RequestUrlBar'
import Sidebar from '../components/Sidebar'
import RequestGroupEnvironmentEditModal from '../components/modals/RequestGroupEnvironmentEditModal'
import * as GlobalActions from '../actions/global'
import * as RequestGroupActions from '../actions/requestGroups'
import * as RequestActions from '../actions/requests'
import * as ResponseActions from '../actions/responses'
import * as ModalActions from '../actions/modals'
// Don't inject component styles (use our own)
Tabs.setUseDefaultStyles(false);
@ -59,7 +61,7 @@ class App extends Component {
request={activeRequest}/>
</TabPanel>
<TabPanel className="grid__cell grid__cell--scroll--v">
<div className="wide">
<div className="wide pad">
<KeyValueEditor
pairs={activeRequest.params}
onChange={params => actions.updateRequest({id: activeRequest.id, params})}
@ -67,7 +69,7 @@ class App extends Component {
</div>
</TabPanel>
<TabPanel className="grid__cell grid__cell--scroll--v">
<div className="wide">
<div className="wide pad">
<RequestAuthEditor
request={activeRequest}
onChange={authentication => actions.updateRequest({id: activeRequest.id, authentication})}
@ -75,7 +77,7 @@ class App extends Component {
</div>
</TabPanel>
<TabPanel className="grid__cell grid__cell--scroll--v">
<div className="wide">
<div className="wide pad">
<KeyValueEditor
pairs={activeRequest.headers}
onChange={headers => actions.updateRequest({id: activeRequest.id, headers})}
@ -144,13 +146,19 @@ class App extends Component {
}
render () {
const {actions, requests, responses, requestGroups, tabs} = this.props;
const {actions, requests, responses, requestGroups, tabs, modals} = this.props;
const activeRequest = requests.all.find(r => r.id === requests.active);
const activeResponse = responses[activeRequest && activeRequest.id];
return (
<div className="grid bg-super-dark tall">
<Modals />
<Prompts />
{!modals.find(m => m.id === RequestGroupEnvironmentEditModal.defaultProps.id) ? null : (
<RequestGroupEnvironmentEditModal
onClose={() => actions.hideModal(RequestGroupEnvironmentEditModal.defaultProps.id)}
onChange={v => console.log(v)}
/>
)}
<Sidebar
activateRequest={actions.activateRequest}
changeFilter={actions.changeFilter}
@ -192,7 +200,8 @@ App.propTypes = {
active: PropTypes.string // "required" but can be null
}).isRequired,
responses: PropTypes.object.isRequired,
tabs: PropTypes.object.isRequired
tabs: PropTypes.object.isRequired,
modals: PropTypes.array.isRequired
};
function mapStateToProps (state) {
@ -201,7 +210,8 @@ function mapStateToProps (state) {
requests: state.requests,
requestGroups: state.requestGroups,
responses: state.responses,
tabs: state.tabs
tabs: state.tabs,
modals: state.modals
};
}
@ -210,6 +220,7 @@ function mapDispatchToProps (dispatch) {
actions: Object.assign(
{},
bindActionCreators(GlobalActions, dispatch),
bindActionCreators(ModalActions, dispatch),
bindActionCreators(RequestGroupActions, dispatch),
bindActionCreators(RequestActions, dispatch),
bindActionCreators(ResponseActions, dispatch)

View File

@ -34,7 +34,7 @@
& > button {
font-size: $font-size-md;
text-align: left;
padding: 10px $padding-md;
padding: 10px $padding-md 10px $padding-sm;
width: 100%;
display: block;
color: $font-light-bg !important;
@ -50,7 +50,7 @@
i.fa {
display: inline-block;
width: 2em;
text-align: left;
text-align: center;
}
}
}

View File

@ -9,6 +9,7 @@
position: relative;
display: flex;
flex: 1 1 100%;
min-height: 5rem;
.editor {
box-sizing: border-box;
@ -45,7 +46,7 @@
.CodeMirror-lines {
// Scroll past the end
padding-bottom: 100px;
//padding-bottom: 50px;
}
.CodeMirror-placeholder {

View File

@ -47,6 +47,7 @@
&.form-control--underlined {
textarea,
input {
border-radius: 0;
border-top: 0;
border-right: 0;
border-left: 0;

View File

@ -20,6 +20,7 @@
.modal__content {
border-radius: $radius-md;
box-sizing: border-box;
box-shadow: 0 0 2rem 0 rgba(0, 0, 0, 0.5);
width: $modal-width;
max-width: 100%;
max-height: 100%;

View File

@ -24,6 +24,22 @@ h2 {
font-size: $font-size-xl;
}
h3 {
font-size: $font-size-lg;
}
h1, h2, h3 {
padding-top: 1.5em;
}
hr {
width: 100%;
height: 1px;
background-color: $hl-md;
border: 0;
margin: $padding-md 0;
}
.text-center {
text-align: center;
}
@ -58,6 +74,10 @@ i.fa {
white-space: nowrap;
}
.no-pad {
padding: 0 !important;
}
.wide {
width: 100%;
}

View File

@ -1,7 +1,7 @@
import swig from 'swig'
import nunjucks from 'nunjucks'
export default function (template, context) {
return swig.render(template, {
locals: context
})
nunjucks.configure({autoescape: false});
export default function (template, context = {}) {
return nunjucks.renderString(template, context);
}

View File

@ -1,6 +1,5 @@
jest.unmock('../request');
jest.unmock('../../constants/global');
jest.unmock('jsonschema');
import validate from '../request';

View File

@ -3,9 +3,19 @@ import {Validator} from 'jsonschema';
const validator = new Validator();
const environmentsSchema = {
id: '/RequestGroupEnvironment',
id: '/Environment',
type: 'object',
properties: {}
properties: {
id: {type: 'string'},
name: {type: 'string'},
data: {type: 'object'}
},
required: [
'data',
'id',
'name'
],
additionalProperties: false
};
const requestGroupSchema = {
@ -16,7 +26,7 @@ const requestGroupSchema = {
created: {type: 'number', minimum: 1000000000000, maximum: 10000000000000},
modified: {type: 'number', minimum: 1000000000000, maximum: 10000000000000},
name: {type: 'string', minLength: 1},
environment: {type: 'object'}
environment: {ref: '/Environment'}
},
required: [
'id',
@ -28,6 +38,8 @@ const requestGroupSchema = {
additionalProperties: false
};
validator.addSchema(environmentsSchema);
export default function (requestGroup) {
return validator.validate(requestGroup, requestGroupSchema);
}

View File

@ -3,6 +3,7 @@
"private": true,
"version": "3.0.0",
"description": "Insomnia App",
"main": "electron.js",
"repository": {
"type": "git",
"url": "git@bitbucket.org:gschier/insomnia.git"
@ -23,7 +24,6 @@
"redux-logger": "^2.6.1",
"redux-thunk": "^2.0.1",
"request": "latest",
"swig": "^1.4.2",
"webpack": "^1.12.14"
},
"devDependencies": {
@ -63,9 +63,21 @@
},
"jest": {
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/redux-thunk",
"<rootDir>/node_modules/redux-mock-store",
"<rootDir>/node_modules/nunjucks",
"<rootDir>/node_modules/jsonschema",
"<rootDir>/node_modules/react",
"<rootDir>/node_modules/react-dom",
"<rootDir>/node_modules/react-addons-test-utils"
],
"testFileExtensions": [
"test.js"
],
"moduleFileExtensions": [
"js",
"electron.js",
"chrome.js"
]
}
}

View File

@ -1,7 +1,8 @@
var path = require('path');
var webpack = require('webpack');
var webpackTargetElectronRenderer = require('webpack-target-electron-renderer');
module.exports = {
var config = {
target: 'web',
devtool: 'source-map',
context: path.join(__dirname, '../app'),
@ -52,7 +53,21 @@ module.exports = {
]
},
resolve: {
extensions: ['', '.js', '.jsx'],
extensions: ['', '.js', '.jsx', '.electron.js', '.chrome.js'],
packageMains: ['webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main']
}
},
plugins: [
new webpack.DefinePlugin({
__DEV__: true,
'process.env': {
NODE_ENV: JSON.stringify('development')
}
}),
new webpack.ExternalsPlugin('commonjs', [
'request'
])
]
};
config.target = webpackTargetElectronRenderer(config);
module.exports = config;

View File

@ -1,7 +1,6 @@
var path = require('path');
var webpack = require('webpack');
var base = require('./base.config');
var webpackTargetElectronRenderer = require('webpack-target-electron-renderer');
base.entry = [
'webpack-hot-middleware/client?path=http://localhost:3333/__webpack_hmr',
@ -21,23 +20,10 @@ for (var i = 0; i < base.module.loaders.length; i++) {
}
}
base.plugins = [
base.plugins = base.plugins.concat([
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin(),
new webpack.DefinePlugin({
__DEV__: true,
'process.env': {
NODE_ENV: JSON.stringify('development')
}
}),
new webpack.ExternalsPlugin('commonjs', [
'request'
])
];
base.target = webpackTargetElectronRenderer(base);
base.resolve.extensions.push('electron.js');
new webpack.NoErrorsPlugin()
]);
module.exports = base;