diff --git a/app/actions/requestgroups.js b/app/actions/requestgroups.js index d39c57ed6..4e287d797 100644 --- a/app/actions/requestgroups.js +++ b/app/actions/requestgroups.js @@ -20,6 +20,6 @@ export function showUpdateNamePrompt (requestGroup) { return modals.show(REQUEST_GROUP_RENAME, {defaultValue, requestGroup}); } -export function showEnvironmentEditModal () { - return modals.show(ENVIRONMENT_EDITOR); +export function showEnvironmentEditModal (requestGroup) { + return modals.show(ENVIRONMENT_EDITOR, {requestGroup}); } diff --git a/app/components/RequestAuthEditor.js b/app/components/RequestAuthEditor.js index f62b3c9a1..8afbc25ac 100644 --- a/app/components/RequestAuthEditor.js +++ b/app/components/RequestAuthEditor.js @@ -13,6 +13,7 @@ class RequestAuthEditor extends Component { return ( this._updatePair(i, {name: e.target.value})} onFocus={e => {this._focusedPair = i; this._focusedField = NAME}} onBlur={e => {this._focusedPair = -1}} @@ -165,7 +171,7 @@ class KeyValueEditor extends Component { type="text" placeholder={this.props.valuePlaceholder || 'Value'} ref={`${i}.${VALUE}`} - value={pair.value} + defaultValue={pair.value} onChange={e => this._updatePair(i, {value: e.target.value})} onFocus={e => {this._focusedPair = i; this._focusedField = VALUE}} onBlur={e => {this._focusedPair = -1}} @@ -210,6 +216,7 @@ class KeyValueEditor extends Component { KeyValueEditor.propTypes = { onChange: PropTypes.func.isRequired, + uniquenessKey: PropTypes.string.isRequired, pairs: PropTypes.array.isRequired, maxPairs: PropTypes.number, namePlaceholder: PropTypes.string, diff --git a/app/components/dropdowns/RequestGroupActionsDropdown.js b/app/components/dropdowns/RequestGroupActionsDropdown.js index eac72f174..097d8047b 100644 --- a/app/components/dropdowns/RequestGroupActionsDropdown.js +++ b/app/components/dropdowns/RequestGroupActionsDropdown.js @@ -21,7 +21,7 @@ class RequestGroupActionsDropdown extends Component {
  • -
  • @@ -40,7 +40,8 @@ RequestGroupActionsDropdown.propTypes = { actions: PropTypes.shape({ update: PropTypes.func.isRequired, remove: PropTypes.func.isRequired, - showUpdateNamePrompt: PropTypes.func.isRequired + showUpdateNamePrompt: PropTypes.func.isRequired, + showEnvironmentEditModal: PropTypes.func.isRequired }), requestGroup: PropTypes.object }; diff --git a/app/components/dropdowns/WorkspaceDropdown.js b/app/components/dropdowns/WorkspaceDropdown.js index 1ebbc3837..ad889a604 100644 --- a/app/components/dropdowns/WorkspaceDropdown.js +++ b/app/components/dropdowns/WorkspaceDropdown.js @@ -1,11 +1,31 @@ +import fs from 'fs' import React, {Component, PropTypes} from 'react' import {bindActionCreators} from 'redux' import {connect} from 'react-redux' import Dropdown from '../base/Dropdown' import * as RequestGroupActions from '../../actions/requestGroups' import * as db from '../../database' +import importData from '../../lib/import' class WorkspaceDropdown extends Component { + _importDialog () { + const dialog = require('electron').remote.dialog; + const options = { + properties: ['openFile'], + filters: [{ + name: 'Insomnia Imports', extensions: ['json'] + }] + }; + + dialog.showOpenDialog(options, paths => { + paths.map(path => { + fs.readFile(path, 'utf8', (err, data) => { + err || importData(data); + }) + }) + }); + } + render () { const {actions, loading, ...other} = this.props; @@ -23,16 +43,26 @@ class WorkspaceDropdown extends Component { diff --git a/app/components/modals/RequestGroupEnvironmentEditModal.js b/app/components/modals/EnvironmentEditModal.js similarity index 63% rename from app/components/modals/RequestGroupEnvironmentEditModal.js rename to app/components/modals/EnvironmentEditModal.js index 4bcd37810..8f7d0f8dc 100644 --- a/app/components/modals/RequestGroupEnvironmentEditModal.js +++ b/app/components/modals/EnvironmentEditModal.js @@ -7,22 +7,39 @@ import Editor from '../base/Editor' import KeyValueEditor from '../base/KeyValueEditor' import * as modalIds from '../../constants/modals' -class RequestGroupEnvironmentEditModal extends Component { +class EnvironmentEditModal extends Component { constructor (props) { super(props); this.state = { - pairs: [] + pairs: this._mapDataToPairs(props.requestGroup.environment) } } _saveChanges () { - this.props.onChange(this.state.pairs); + this.props.onChange(this._mapPairsToData(this.state.pairs)); + this.props.onClose(); } _keyValueChange (pairs) { this.setState({pairs}); } + _mapPairsToData (pairs) { + return pairs.reduce((prev, curr) => { + return Object.assign({}, prev, {[curr.name]: curr.value}); + }, {}); + } + + _mapDataToPairs (data) { + return Object.keys(data).map(key => ({name: key, value: data[key]})); + } + + componentWillReceiveProps (nextProps) { + this.setState({ + pairs: this._mapDataToPairs(nextProps.requestGroup.environment) + }) + } + render () { const editorOptions = { mode: 'application/json', @@ -36,6 +53,7 @@ class RequestGroupEnvironmentEditModal extends Component {
    @@ -53,13 +71,15 @@ class RequestGroupEnvironmentEditModal extends Component { } } -RequestGroupEnvironmentEditModal.propTypes = { - // requestGroup: PropTypes.object.isRequired, +EnvironmentEditModal.propTypes = { + requestGroup: PropTypes.shape({ + environment: PropTypes.object.isRequired + }), onChange: PropTypes.func.isRequired }; -RequestGroupEnvironmentEditModal.defaultProps = { +EnvironmentEditModal.defaultProps = { id: modalIds.ENVIRONMENT_EDITOR }; -export default RequestGroupEnvironmentEditModal; +export default EnvironmentEditModal; diff --git a/app/containers/App.js b/app/containers/App.js index 022b84cbb..b9d747c29 100644 --- a/app/containers/App.js +++ b/app/containers/App.js @@ -10,11 +10,12 @@ 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 EnvironmentEditModal from '../components/modals/EnvironmentEditModal' import * as GlobalActions from '../actions/global' import * as RequestGroupActions from '../actions/requestGroups' import * as RequestActions from '../actions/requests' +import * as ModalActions from '../actions/modals' import * as db from '../database' @@ -63,6 +64,7 @@ class App extends Component {
    {db.update(activeRequest, {params})}} /> @@ -79,6 +81,7 @@ class App extends Component {
    {db.update(activeRequest, {headers})}} /> @@ -153,17 +156,25 @@ class App extends Component { return (
    - {!modals.find(m => m.id === RequestGroupEnvironmentEditModal.defaultProps.id) ? null : ( - actions.hideModal(RequestGroupEnvironmentEditModal.defaultProps.id)} - onChange={v => console.log(v)} - /> - )} + {modals.map(m => { + if (m.id === EnvironmentEditModal.defaultProps.id) { + return ( + actions.modals.hide(m.id)} + onChange={rg => db.update(m.data.requestGroup, {environment: rg.environment})} + /> + ) + } else { + return null; + } + })} db.requestCreate({parent: requestGroup._id})} - toggleRequestGroup={requestGroup => db.requestGroupToggle(requestGroup)} + toggleRequestGroup={requestGroup => db.update(requestGroup, {collapsed: !requestGroup.collapsed})} activeRequest={activeRequest} activeFilter={requests.filter} requestGroups={requestGroups.all} @@ -193,6 +204,9 @@ App.propTypes = { update: PropTypes.func.isRequired, toggle: PropTypes.func.isRequired }), + modals: PropTypes.shape({ + hide: PropTypes.func.isRequired + }), global: PropTypes.shape({ selectTab: PropTypes.func.isRequired }) @@ -224,6 +238,7 @@ function mapDispatchToProps (dispatch) { return { actions: { global: bindActionCreators(GlobalActions, dispatch), + modals: bindActionCreators(ModalActions, dispatch), requestGroups: bindActionCreators(RequestGroupActions, dispatch), requests: bindActionCreators(RequestActions, dispatch) } diff --git a/app/database/index.js b/app/database/index.js index 04ce32fbe..0878e9963 100644 --- a/app/database/index.js +++ b/app/database/index.js @@ -1,5 +1,6 @@ import PouchDB from 'pouchdb'; import * as methods from '../constants/global'; +import {generateId} from './util' let db = new PouchDB('insomnia.db'); @@ -20,6 +21,10 @@ export function allDocs () { return db.allDocs({include_docs: true}); } +export function get (id) { + return db.get(id); +} + export function update (doc, patch = {}) { const updatedDoc = Object.assign( {}, @@ -31,7 +36,7 @@ export function update (doc, patch = {}) { return db.put(updatedDoc).catch(e => { if (e.status === 409) { console.warn('Retrying document update for', updatedDoc); - db.get(doc._id).then(dbDoc => { + get(doc._id).then(dbDoc => { update(dbDoc, patch); }); } @@ -47,7 +52,7 @@ export function remove (doc) { // ~~~~~~~ // export function requestCreate (patch = {}) { - update(Object.assign( + const request = Object.assign( // Defaults { url: '', @@ -66,13 +71,17 @@ export function requestCreate (patch = {}) { // Required Generated Fields { - _id: `rq_${Date.now()}`, + _id: generateId('req'), _rev: undefined, type: 'Request', created: Date.now(), modified: Date.now() } - )); + ); + + update(request); + + return request; } export function requestDuplicate (request) { @@ -84,7 +93,7 @@ export function requestDuplicate (request) { // ~~~~~~~~~~~~~ // export function requestGroupCreate (patch = {}) { - update(Object.assign( + const requestGroup = Object.assign( // Default Fields { collapsed: false, @@ -98,17 +107,17 @@ export function requestGroupCreate (patch = {}) { // Required Generated Fields { - _id: `rg_${Date.now()}`, + _id: generateId('grp'), _rev: undefined, type: 'RequestGroup', created: Date.now(), modified: Date.now() } - )); -} - -export function requestGroupToggle (requestGroup, patch = {}) { - return update(requestGroup, patch); + ); + + update(requestGroup); + + return requestGroup; } export {changes}; diff --git a/app/database/util.js b/app/database/util.js new file mode 100644 index 000000000..01ebf3069 --- /dev/null +++ b/app/database/util.js @@ -0,0 +1,11 @@ +const CHARS = '023456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ'.split(''); + +export function generateId (prefix) { + let id = `${prefix}_${Date.now()}-`; + + for (let i = 0; i < 10; i++) { + id += CHARS[Math.floor(Math.random() * CHARS.length)]; + } + + return id; +} diff --git a/app/lib/import.js b/app/lib/import.js new file mode 100644 index 000000000..ea972b887 --- /dev/null +++ b/app/lib/import.js @@ -0,0 +1,70 @@ +import * as db from '../database' + +const TYPE_REQUEST = 'request'; +const TYPE_REQUEST_GROUP = 'request_group'; +const FORMAT_MAP = { + 'json': 'application/json' +}; + +function importRequestGroup (iRequestGroup, exportFormat) { + if (exportFormat === 1) { + const requestGroup = db.requestGroupCreate({ + name: iRequestGroup.name, + environment: (iRequestGroup.environments || {}).base || {} + }); + + // Sometimes (maybe all the time, I can't remember) requests will be nested + if (iRequestGroup.hasOwnProperty('requests')) { + iRequestGroup.requests.map( + r => importRequest(r, requestGroup._id, exportFormat) + ); + } + } +} + +function importRequest (iRequest, parent, exportFormat) { + if (exportFormat === 1) { + let auth = {}; + if (iRequest.authentication.username) { + auth = { + username: iRequest.authentication.username, + password: iRequest.authentication.password + } + } + + db.requestCreate({ + name: iRequest.name, + url: iRequest.url, + method: iRequest.method, + body: iRequest.body, + headers: iRequest.headers || [], + params: iRequest.params || [], + contentType: FORMAT_MAP[iRequest.__insomnia.format] || 'text/plain', + authentication: auth, + parent: parent + }); + } +} + +export default function (txt, callback) { + let data; + + try { + data = JSON.parse(txt); + } catch (e) { + return callback(new Error('Invalid Insomnia export')); + } + + + if (!data.hasOwnProperty('_type') || !data.hasOwnProperty('items')) { + return callback(new Error('Invalid Insomnia export')); + } + + data.items.filter(i => i._type === TYPE_REQUEST_GROUP).map( + rg => importRequestGroup(rg, data.__export_format) + ); + + data.items.filter(i => i._type === TYPE_REQUEST).map( + r => importRequest(r, data.__export_format) + ); +} diff --git a/app/lib/request.js b/app/lib/request.js index 36759639d..46eba7a37 100644 --- a/app/lib/request.js +++ b/app/lib/request.js @@ -1,14 +1,20 @@ import networkRequest from 'request' import render from './render' +import * as db from '../database' -export default function (request, callback) { +function makeRequest (request, callback) { const config = { - url: request.url, method: request.method, body: request.body, headers: {} }; + if (request.url.indexOf('://') === -1) { + config.url = request.url; + } else { + config.url = `https://${request.url}`; + } + if (request.authentication.username) { config.auth = { user: request.authentication.username, @@ -23,7 +29,7 @@ export default function (request, callback) { config.headers[header.name] = header.value; } } - + // TODO: this is just a POC. It breaks in a lot of cases config.url += request.params.map((p, i) => { const name = encodeURIComponent(p.name); @@ -31,12 +37,8 @@ export default function (request, callback) { return `${i === 0 ? '?' : '&'}${name}=${value}`; }).join(''); - // SNEAKY HACK: Render nested object by converting it to JSON then rendering - const context = {template_id: 'tem_WWq2w9uJNR6Pqk8APkvsS3'}; - const template = JSON.stringify(config); - const renderedConfig = JSON.parse(render(template, context)); - networkRequest(renderedConfig, function (err, response) { + networkRequest(config, function (err, response) { if (err) { return callback(err); } else { @@ -52,3 +54,17 @@ export default function (request, callback) { } }); } + +export default function (originalRequest, callback) { + // SNEAKY HACK: Render nested object by converting it to JSON then rendering + const template = JSON.stringify(originalRequest); + const request = JSON.parse(render(template, context)); + + if (request.parent) { + db.get(request.parent).then( + requestGroup => makeRequest(config, callback, requestGroup.environment) + ); + } else { + makeRequest(request, callback) + } +} diff --git a/package.json b/package.json index 4b9b688c5..d2ea66f99 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "babel-preset-react": "^6.5.0", "concurrently": "^2.0.0", "css-loader": "^0.23.1", - "electron-prebuilt": "^0.37.2", "express": "latest", "file-loader": "^0.8.5", "jest": "^0.1.40", diff --git a/webpack/dev.electron.config.js b/webpack/dev.config.js similarity index 100% rename from webpack/dev.electron.config.js rename to webpack/dev.config.js diff --git a/webpack/server.js b/webpack/server.js index c04a41965..6c2481e84 100644 --- a/webpack/server.js +++ b/webpack/server.js @@ -2,7 +2,7 @@ const express = require('express'); const webpack = require('webpack'); -const config = require('./dev.electron.config.js'); +const config = require('./dev.config.js'); const app = express(); const compiler = webpack(config);