Workspaces (#7)

* Got a hacky workspace implementation running

* Removed some hax with reducer composition

* Moved some more around

* Moved files back out

* Started on entities reducer

* Split up some components

* Moved nested modules back out of workspaces

* Started on new Sidebar tree stuff

* Better store stuff

* Some more tweaks

* Removed workspace update action

* Re-implemented filtering in the Sidbare

* Switch to get the newest response
This commit is contained in:
Gregory Schier 2016-04-26 00:29:24 -07:00
parent 97f579f5e5
commit e9d64ebb23
32 changed files with 954 additions and 699 deletions

View File

@ -3,17 +3,17 @@ import Editor from './base/Editor'
class RequestBodyEditor extends Component {
render () {
const {request, onChange, className} = this.props;
const mode = request.contentType || 'text/plain';
const {body, contentType, requestId, onChange, className} = this.props;
return (
<Editor
value={request.body}
value={body}
className={className}
debounceMillis={400}
onChange={onChange}
uniquenessKey={request._id}
uniquenessKey={requestId}
options={{
mode: mode,
mode: contentType,
placeholder: 'request body here...'
}}
/>
@ -22,10 +22,13 @@ class RequestBodyEditor extends Component {
}
RequestBodyEditor.propTypes = {
request: PropTypes.shape({
body: PropTypes.string.isRequired
}).isRequired,
onChange: PropTypes.func.isRequired
// Functions
onChange: PropTypes.func.isRequired,
// Other
requestId: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
contentType: PropTypes.string.isRequired
};
export default RequestBodyEditor;

View File

@ -0,0 +1,132 @@
import React, {Component, PropTypes} from 'react'
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'
import KeyValueEditor from '../components/base/KeyValueEditor'
import Dropdown from '../components/base/Dropdown'
import RequestBodyEditor from '../components/RequestBodyEditor'
import RequestAuthEditor from '../components/RequestAuthEditor'
import RequestUrlBar from '../components/RequestUrlBar'
class RequestPane extends Component {
render () {
const {
request,
sendRequest,
updateRequestUrl,
updateRequestMethod,
updateRequestBody,
updateRequestParams,
updateRequestAuthentication,
updateRequestHeaders
} = this.props;
if (!request) {
return (
<section className="grid__cell section section--bordered grid--v grid--start">
<header className="header bg-super-light section__header"></header>
<div className="section__body grid__cell"></div>
</section>
)
}
return (
<section className="grid__cell section section--bordered">
<div className="grid--v wide">
<div className="header section__header">
<RequestUrlBar
uniquenessKey={request._id}
sendRequest={() => sendRequest(request)}
onUrlChange={updateRequestUrl}
onMethodChange={updateRequestMethod}
url={request.url}
method={request.method}
/>
</div>
<Tabs className="grid__cell grid--v section__body">
<TabList className="grid grid--start">
<Tab className="no-wrap grid grid--center">
<button>JSON</button>
<Dropdown>
<button><i className="fa fa-caret-down"></i></button>
<ul>
{/*<li><button><i className="fa fa-picture-o"></i> File Upload</button></li>*/}
<li><button><i className="fa fa-bars"></i> Form Data</button></li>
<li><button><i className="fa fa-code"></i> XML</button></li>
<li><button><i className="fa fa-file-text"></i> Plain Text</button></li>
</ul>
</Dropdown>
</Tab>
<Tab>
<button className="no-wrap">
Params {request.params.length ? `(${request.params.length})` : ''}
</button>
</Tab>
<Tab>
<button className="no-wrap">
Headers {request.headers.length ? `(${request.headers.length})` : ''}
</button>
</Tab>
</TabList>
<TabPanel className="grid__cell editor-wrapper">
<RequestBodyEditor
onChange={updateRequestBody}
requestId={request._id}
contentType={request.contentType}
body={request.body}
/>
</TabPanel>
<TabPanel className="grid__cell grid__cell--scroll--v">
<div>
<KeyValueEditor
className="pad"
namePlaceholder="name"
valuePlaceholder="value"
uniquenessKey={request._id}
pairs={request.params}
onChange={updateRequestParams}
/>
</div>
</TabPanel>
<TabPanel className="grid__cell grid__cell--scroll--v">
<div>
<div className="pad">
<label>Basic Authentication</label>
<RequestAuthEditor
request={request}
onChange={updateRequestAuthentication}
/>
<br/>
<label>Other Headers</label>
<KeyValueEditor
namePlaceholder="My-Header"
valuePlaceholder="Value"
uniquenessKey={request._id}
pairs={request.headers}
onChange={updateRequestHeaders}
/>
</div>
</div>
</TabPanel>
</Tabs>
</div>
</section>
)
}
}
RequestPane.propTypes = {
// Functions
sendRequest: PropTypes.func.isRequired,
updateRequestUrl: PropTypes.func.isRequired,
updateRequestMethod: PropTypes.func.isRequired,
updateRequestBody: PropTypes.func.isRequired,
updateRequestParams: PropTypes.func.isRequired,
updateRequestAuthentication: PropTypes.func.isRequired,
updateRequestHeaders: PropTypes.func.isRequired,
// Other
request: PropTypes.object
};
export default RequestPane;

View File

@ -4,36 +4,42 @@ import Dropdown from './base/Dropdown';
import {METHODS} from '../lib/constants';
class UrlInput extends Component {
_handleFormSubmit (e) {
e.preventDefault();
this.props.sendRequest();
}
render () {
const {sendRequest, onUrlChange, onMethodChange, request} = this.props;
const {onUrlChange, onMethodChange, uniquenessKey, url, method} = this.props;
return (
<div className="tall grid grid--center wide bg-super-light">
<Dropdown className="tall">
<button className="pad tall txt-md">
{request.method}&nbsp;<i className="fa fa-caret-down"></i>
{method}&nbsp;
<i className="fa fa-caret-down"></i>
</button>
<ul>
{METHODS.map((method) => (
<li key={method}>
<button onClick={onMethodChange.bind(null, method)}>
{method}
{METHODS.map(m => (
<li key={m}>
<button onClick={onMethodChange.bind(null, m)}>
{m}
</button>
</li>
))}
</ul>
</Dropdown>
<form className="tall grid__cell form-control form-control--wide"
onSubmit={e => {e.preventDefault(); sendRequest(request)}}>
onSubmit={this._handleFormSubmit.bind(this)}>
<DebouncingInput
type="text"
className="txt-md"
placeholder="http://echo.insomnia.rest/status/200"
value={request.url}
value={url}
debounceMillis={1000}
uniquenessKey={request._id}
uniquenessKey={uniquenessKey}
onChange={onUrlChange}/>
</form>
<button className="btn btn--compact txt-lg" onClick={sendRequest.bind(null, request)}>
<button className="btn btn--compact txt-lg" onClick={this._handleFormSubmit.bind(this)}>
Send
</button>&nbsp;&nbsp;
</div>
@ -45,10 +51,9 @@ UrlInput.propTypes = {
sendRequest: PropTypes.func.isRequired,
onUrlChange: PropTypes.func.isRequired,
onMethodChange: PropTypes.func.isRequired,
request: PropTypes.shape({
url: PropTypes.string.isRequired,
method: PropTypes.string.isRequired
}).isRequired
uniquenessKey: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
method: PropTypes.string.isRequired
};
export default UrlInput;

View File

@ -0,0 +1,90 @@
import React, {Component, PropTypes} from 'react'
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'
import Dropdown from '../components/base/Dropdown'
import Editor from '../components/base/Editor'
import StatusTag from '../components/StatusTag'
import SizeTag from '../components/SizeTag'
import TimeTag from '../components/TimeTag'
class ResponsePane extends Component {
render () {
const {
response
} = this.props;
if (!response) {
return (
<section className="grid__cell section grid--v grid--start">
<header className="header bg-light section__header"></header>
<div className="section__body grid__cell"></div>
</section>
)
}
return (
<section className="grid__cell section">
<div className="grid--v wide">
<header
className="grid grid--center header text-center bg-super-light txt-sm section__header">
{!response ? null : (
<div>
<StatusTag
statusCode={response.statusCode}
statusMessage={response.statusMessage}
/>
<TimeTag milliseconds={response.millis}/>
<SizeTag bytes={response.bytes}/>
</div>
)}
</header>
<Tabs className="grid__cell grid--v section__body">
<TabList className="grid grid--start">
<Tab className="no-wrap grid grid--center">
<button>Preview</button>
<Dropdown>
<button><i className="fa fa-caret-down"></i></button>
<ul>
<li><button><i className="fa fa-eye"></i> Preview</button></li>
<li><button><i className="fa fa-code"></i> Source</button></li>
<li><button><i className="fa fa-file"></i> Raw</button></li>
</ul>
</Dropdown>
</Tab>
<Tab><button>Headers</button></Tab>
</TabList>
<TabPanel className="grid__cell editor-wrapper">
<Editor
value={response && response.body || ''}
prettify={true}
options={{
mode: response && response.contentType || 'text/plain',
readOnly: true,
placeholder: 'nothing yet...'
}}
/>
</TabPanel>
<TabPanel className="grid__cell grid__cell--scroll--v">
<div className="wide">
<div className="grid--v grid--start pad">
{!response ? null : response.headers.map((h, i) => (
<div className="grid grid__cell grid__cell--no-flex selectable" key={i}>
<div className="grid__cell">{h.name}</div>
<div className="grid__cell">{h.value}</div>
</div>
))}
</div>
</div>
</TabPanel>
</Tabs>
</div>
</section>
)
}
}
ResponsePane.propTypes = {
response: PropTypes.object
};
export default ResponsePane;

View File

@ -1,132 +1,83 @@
import React, {Component, PropTypes} from 'react'
import classnames from 'classnames'
import WorkspaceDropdown from './../containers/WorkspaceDropdown'
import RequestActionsDropdown from './../containers/RequestActionsDropdown'
import RequestGroupActionsDropdown from './../containers/RequestGroupActionsDropdown'
import DebouncingInput from './base/DebouncingInput'
import MethodTag from './MethodTag'
import * as db from '../database'
import SidebarRequestGroupRow from './SidebarRequestGroupRow'
import SidebarRequestRow from './SidebarRequestRow'
class Sidebar extends Component {
onFilterChange (value) {
this.props.changeFilter(value);
}
renderRequestGroupRow (requestGroup = null) {
const {
activeFilter,
activeRequest,
addRequestToRequestGroup,
toggleRequestGroup,
requests
} = this.props;
let filteredRequests = requests.filter(
r => {
// TODO: Move this to a lib file
if (!activeFilter) {
return true;
}
const requestGroupName = requestGroup ? requestGroup.name : '';
const toMatch = `${requestGroupName}${r.method}${r.name}`.toLowerCase();
const matchTokens = activeFilter.toLowerCase().split(' ');
for (let i = 0; i < matchTokens.length; i++) {
let token = `${matchTokens[i]}`;
if (toMatch.indexOf(token) === -1) {
return false;
}
}
_filterChildren (filter, children, extra = null) {
return children.filter(child => {
if (child.doc.type !== 'Request') {
return true;
}
);
if (!requestGroup) {
filteredRequests = filteredRequests.filter(r => !r.parent);
return filteredRequests.map(request => this.renderRequestRow(request));
}
const request = child.doc;
// Grab all of the children for this request group
filteredRequests = filteredRequests.filter(r => r.parent === requestGroup._id);
const otherMatches = extra || '';
const toMatch = `${request.method}${request.name}${otherMatches}`.toLowerCase();
const matchTokens = filter.toLowerCase().split(' ');
// Don't show folder if it was not in the filter
if (activeFilter && !filteredRequests.length) {
return null;
}
for (let i = 0; i < matchTokens.length; i++) {
let token = `${matchTokens[i]}`;
if (toMatch.indexOf(token) === -1) {
// Filter failed. Don't render children
return false;
}
}
const isActive = activeRequest && filteredRequests.find(r => r._id == activeRequest._id);
let folderIconClass = 'fa-folder';
let expanded = !requestGroup.collapsed;
folderIconClass += !expanded ? '' : '-open';
folderIconClass += isActive ? '' : '-o';
const sidebarItemClassNames = classnames(
'sidebar__item',
'sidebar__item--bordered',
{'sidebar__item--active': isActive}
);
return (
<li key={requestGroup._id}>
<div className={sidebarItemClassNames}>
<div className="sidebar__item__row sidebar__item__row--heading">
<button onClick={e => toggleRequestGroup(requestGroup)}>
<i className={'fa ' + folderIconClass}></i>
&nbsp;&nbsp;&nbsp;{requestGroup.name}
</button>
</div>
<div className="sidebar__item__btn grid">
<button onClick={(e) => addRequestToRequestGroup(requestGroup)}>
<i className="fa fa-plus-circle"></i>
</button>
<RequestGroupActionsDropdown
requestGroup={requestGroup}
right={true}
className="tall"/>
</div>
</div>
<ul>
{expanded && !filteredRequests.length ? this.renderRequestRow() : null}
{!expanded ? null : filteredRequests.map(request => this.renderRequestRow(request, requestGroup))}
</ul>
</li>
);
return true;
})
}
renderRequestRow (request = null, requestGroup = null) {
const {activeRequest, activateRequest} = this.props;
const isActive = request && activeRequest && request._id === activeRequest._id;
_renderChildren (children, requestGroup) {
const {filter} = this.props;
return (
<li key={request ? request._id : 'none'}>
<div className={'sidebar__item ' + (isActive ? 'sidebar__item--active' : '')}>
<div className="sidebar__item__row">
{request ? (
<button onClick={() => {activateRequest(request)}}>
<MethodTag method={request.method}/> {request.name}
</button>
) : (
<button className="italic">No Requests</button>
)}
</div>
{request ? (
<RequestActionsDropdown
className="sidebar__item__btn"
right={true}
request={request}
requestGroup={requestGroup}
/>
) : null}
</div>
</li>
);
const filteredChildren = this._filterChildren(
filter,
children,
requestGroup && requestGroup.name
).sort((a, b) => a.doc._id > b.doc._id ? -1 : 1);
return filteredChildren.map(child => {
if (child.doc.type === 'Request') {
return (
<SidebarRequestRow
key={child.doc._id}
activateRequest={this.props.activateRequest}
isActive={child.doc._id === this.props.activeRequestId}
request={child.doc}
/>
)
} else if (child.doc.type === 'RequestGroup') {
const requestGroup = child.doc;
const isActive = !!child.children.find(c => c.doc._id === this.props.activeRequestId);
return (
<SidebarRequestGroupRow
key={requestGroup._id}
isActive={isActive}
hideIfNoChildren={filter}
toggleRequestGroup={this.props.toggleRequestGroup}
addRequestToRequestGroup={this.props.addRequestToRequestGroup}
numChildren={child.children.length}
requestGroup={requestGroup}
>
{this._renderChildren(child.children, requestGroup)}
</SidebarRequestGroupRow>
)
} else {
console.error('Unknown child type', child.doc.type);
return null;
}
})
}
render () {
const {activeFilter, requestGroups} = this.props;
const {filter, children} = this.props;
return (
<section className="sidebar bg-dark grid--v section section--bordered">
@ -136,8 +87,7 @@ class Sidebar extends Component {
<div className="grid--v grid--start grid__cell section__body">
<ul
className="grid--v grid--start grid__cell sidebar__scroll hover-scrollbars sidebar__request-list">
{this.renderRequestGroupRow(null)}
{requestGroups.map(requestGroup => this.renderRequestGroupRow(requestGroup))}
{this._renderChildren(children)}
</ul>
<div className="grid grid--center">
<div className="grid__cell form-control form-control--underlined">
@ -145,7 +95,7 @@ class Sidebar extends Component {
type="text"
placeholder="Filter Items"
debounceMillis={300}
value={activeFilter}
value={filter}
onChange={this.onFilterChange.bind(this)}/>
</div>
</div>
@ -156,14 +106,19 @@ class Sidebar extends Component {
}
Sidebar.propTypes = {
// Functions
activateRequest: PropTypes.func.isRequired,
toggleRequestGroup: PropTypes.func.isRequired,
addRequestToRequestGroup: PropTypes.func.isRequired,
changeFilter: PropTypes.func.isRequired,
toggleRequestGroup: PropTypes.func.isRequired,
activeFilter: PropTypes.string,
requests: PropTypes.array.isRequired,
requestGroups: PropTypes.array.isRequired,
activeRequest: PropTypes.object
// Other
children: PropTypes.array.isRequired,
workspaceId: PropTypes.string.isRequired,
// Optional
filter: PropTypes.string,
activeRequestId: PropTypes.string
};
export default Sidebar;

View File

@ -0,0 +1,79 @@
import React, {Component, PropTypes} from 'react'
import classnames from 'classnames'
import RequestGroupActionsDropdown from './../containers/RequestGroupActionsDropdown'
import SidebarRequestRow from './SidebarRequestRow'
class SidebarRequestGroupRow extends Component {
render () {
const {
children,
hideIfNoChildren,
requestGroup,
isActive,
toggleRequestGroup,
addRequestToRequestGroup
} = this.props;
// If we are supposed to have children, but aren't passed any, we are probably
// filtering so don't render anything
if (hideIfNoChildren && children.length === 0) {
return null;
}
let folderIconClass = 'fa-folder';
let expanded = !requestGroup.collapsed;
folderIconClass += !expanded ? '' : '-open';
folderIconClass += isActive ? '' : '-o';
const sidebarItemClassNames = classnames(
'sidebar__item',
'sidebar__item--bordered',
{'sidebar__item--active': isActive}
);
return (
<li key={requestGroup._id}>
<div className={sidebarItemClassNames}>
<div className="sidebar__item__row sidebar__item__row--heading">
<button onClick={e => toggleRequestGroup(requestGroup)}>
<i className={'fa ' + folderIconClass}></i>
&nbsp;&nbsp;&nbsp;{requestGroup.name}
</button>
</div>
<div className="sidebar__item__btn grid">
<button onClick={(e) => addRequestToRequestGroup(requestGroup)}>
<i className="fa fa-plus-circle"></i>
</button>
<RequestGroupActionsDropdown
requestGroup={requestGroup}
right={true}
className="tall"/>
</div>
</div>
<ul>
{!expanded || children.length > 0 ? null : (
<SidebarRequestRow
activateRequest={() => {}}
isActive={false}
request={null}
/>
)}
{expanded ? children : null}
</ul>
</li>
);
}
}
SidebarRequestGroupRow.propTypes = {
// Functions
toggleRequestGroup: PropTypes.func.isRequired,
addRequestToRequestGroup: PropTypes.func.isRequired,
// Other
isActive: PropTypes.bool.isRequired,
hideIfNoChildren: PropTypes.number.isRequired,
requestGroup: PropTypes.object.isRequired
};
export default SidebarRequestGroupRow;

View File

@ -0,0 +1,47 @@
import React, {Component, PropTypes} from 'react'
import RequestActionsDropdown from './../containers/RequestActionsDropdown'
import MethodTag from './MethodTag'
class SidebarRequestRow extends Component {
render () {
const {request, requestGroup, isActive, activateRequest} = this.props;
return (
<li key={request ? request._id : 'none'}>
<div className={'sidebar__item ' + (isActive ? 'sidebar__item--active' : '')}>
<div className="sidebar__item__row">
{request ? (
<button onClick={() => {activateRequest(request)}}>
<MethodTag method={request.method}/> {request.name}
</button>
) : (
<button className="italic">No Requests</button>
)}
</div>
{request ? (
<RequestActionsDropdown
className="sidebar__item__btn"
right={true}
request={request}
requestGroup={requestGroup}
/>
) : null}
</div>
</li>
);
}
}
SidebarRequestRow.propTypes = {
// Functions
activateRequest: PropTypes.func.isRequired,
// Other
isActive: PropTypes.bool.isRequired,
// Optional
requestGroup: PropTypes.object,
request: PropTypes.object
};
export default SidebarRequestRow;

View File

@ -1,31 +1,20 @@
import React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'
import Editor from '../components/base/Editor'
import Prompts from './Prompts'
import KeyValueEditor from '../components/base/KeyValueEditor'
import RequestBodyEditor from '../components/RequestBodyEditor'
import RequestAuthEditor from '../components/RequestAuthEditor'
import RequestUrlBar from '../components/RequestUrlBar'
import StatusTag from '../components/StatusTag'
import SizeTag from '../components/SizeTag'
import TimeTag from '../components/TimeTag'
import Sidebar from '../components/Sidebar'
import EnvironmentEditModal from '../components/EnvironmentEditModal'
import RequestPane from '../components/RequestPane'
import ResponsePane from '../components/ResponsePane'
import Sidebar from '../components/Sidebar'
import * as GlobalActions from '../redux/modules/global'
import * as RequestGroupActions from '../redux/modules/requestGroups'
import * as RequestActions from '../redux/modules/requests'
import * as ModalActions from '../redux/modules/modals'
import * as TabActions from '../redux/modules/tabs'
import * as db from '../database'
// Don't inject component styles (use our own)
Tabs.setUseDefaultStyles(false);
class App extends Component {
constructor (props) {
super(props);
@ -35,183 +24,74 @@ class App extends Component {
}
}
_renderRequestPanel (actions, activeRequest, tabs) {
if (!activeRequest) {
return (
<section className="grid__cell section section--bordered grid--v grid--start">
<header className="header bg-super-light section__header"></header>
<div className="section__body grid__cell"></div>
</section>
)
_generateSidebarTree (parentId, entities) {
const children = entities.filter(e => e.parentId === parentId);
if (children.length > 0) {
return children.map(c => ({
doc: c,
children: this._generateSidebarTree(c._id, entities)
}));
} else {
return children;
}
return (
<section className="grid__cell section section--bordered">
<div className="grid--v wide">
<div className="header section__header">
<RequestUrlBar
sendRequest={actions.requests.send}
onUrlChange={url => {db.update(activeRequest, {url})}}
onMethodChange={method => {db.update(activeRequest, {method})}}
request={activeRequest}
/>
</div>
<Tabs className="grid__cell grid--v section__body"
onSelect={i => actions.tabs.select('request', i)}
selectedIndex={tabs.request || 0}>
<TabList className="grid grid--start">
<Tab><button>Body</button></Tab>
<Tab>
<button className="no-wrap">
Params {activeRequest.params.length ? `(${activeRequest.params.length})` : ''}
</button>
</Tab>
<Tab><button>Auth</button></Tab>
<Tab>
<button className="no-wrap">
Headers {activeRequest.headers.length ? `(${activeRequest.headers.length})` : ''}
</button>
</Tab>
</TabList>
<TabPanel className="grid__cell editor-wrapper">
<RequestBodyEditor
onChange={body => {db.update(activeRequest, {body})}}
request={activeRequest}/>
</TabPanel>
<TabPanel className="grid__cell grid__cell--scroll--v">
<div>
<KeyValueEditor
className="pad"
namePlaceholder="name"
valuePlaceholder="value"
uniquenessKey={activeRequest._id}
pairs={activeRequest.params}
onChange={params => {db.update(activeRequest, {params})}}
/>
</div>
</TabPanel>
<TabPanel className="grid__cell grid__cell--scroll--v">
<div>
<RequestAuthEditor
className="pad"
request={activeRequest}
onChange={authentication => {db.update(activeRequest, {authentication})}}
/>
</div>
</TabPanel>
<TabPanel className="grid__cell grid__cell--scroll--v">
<div>
<KeyValueEditor
className="pad"
namePlaceholder="My-Header"
valuePlaceholder="Value"
uniquenessKey={activeRequest._id}
pairs={activeRequest.headers}
onChange={headers => {db.update(activeRequest, {headers})}}
/>
</div>
</TabPanel>
</Tabs>
</div>
</section>
)
}
_renderResponsePanel (actions, activeResponse, tabs) {
if (!activeResponse) {
return (
<section className="grid__cell section grid--v grid--start">
<header className="header bg-light section__header"></header>
<div className="section__body grid__cell"></div>
</section>
)
}
return (
<section className="grid__cell section">
<div className="grid--v wide">
<header
className="grid grid--center header text-center bg-super-light txt-sm section__header">
{!activeResponse ? null : (
<div>
<StatusTag
statusCode={activeResponse.statusCode}
statusMessage={activeResponse.statusMessage}
/>
<TimeTag milliseconds={activeResponse.millis}/>
<SizeTag bytes={activeResponse.bytes}/>
</div>
)}
</header>
<Tabs className="grid__cell grid--v section__body"
onSelect={i => actions.tabs.select('response', i)}
selectedIndex={tabs.response || 0}>
<TabList className="grid grid--start">
<Tab><button>Preview</button></Tab>
<Tab><button>Raw</button></Tab>
<Tab><button>Headers</button></Tab>
</TabList>
<TabPanel className="grid__cell editor-wrapper">
<Editor
value={activeResponse && activeResponse.body || ''}
prettify={true}
options={{
mode: activeResponse && activeResponse.contentType || 'text/plain',
readOnly: true,
placeholder: 'nothing yet...'
}}
/>
</TabPanel>
<TabPanel className="grid__cell editor-wrapper">
<Editor
value={activeResponse && activeResponse.body || ''}
options={{
lineWrapping: true,
mode: 'text/plain',
readOnly: true,
placeholder: 'nothing yet...'
}}
/>
</TabPanel>
<TabPanel className="grid__cell grid__cell--scroll--v">
<div className="wide">
<div className="grid--v grid--start pad">
{!activeResponse ? null : activeResponse.headers.map((h, i) => (
<div className="grid grid__cell grid__cell--no-flex selectable" key={i}>
<div className="grid__cell">{h.name}</div>
<div className="grid__cell">{h.value}</div>
</div>
))}
</div>
</div>
</TabPanel>
</Tabs>
</div>
</section>
)
}
render () {
const {actions, requests, responses, requestGroups, tabs, modals} = this.props;
const activeRequest = requests.all.find(r => r._id === requests.active);
const activeResponse = activeRequest ? responses[activeRequest._id] : undefined;
const {actions, modals, workspaces, requests, entities} = this.props;
// TODO: Factor this out into a selector
let workspace = entities.workspaces[workspaces.activeId];
if (!workspace) {
workspace = entities.workspaces[Object.keys(entities.workspaces)[0]];
}
const activeRequestId = workspace.activeRequestId;
const activeRequest = activeRequestId ? entities.requests[activeRequestId] : null;
const responses = Object.keys(entities.responses).map(id => entities.responses[id]);
const allRequests = Object.keys(entities.requests).map(id => entities.requests[id]);
const allRequestGroups = Object.keys(entities.requestGroups).map(id => entities.requestGroups[id]);
const activeResponse = responses.sort(
(a, b) => a._id > b._id ? -1 : 1
).find(r => r.parentId === activeRequestId);
const children = this._generateSidebarTree(
workspace._id,
allRequests.concat(allRequestGroups)
);
return (
<div className="grid bg-super-dark tall">
<Sidebar
activateRequest={actions.requests.activate}
workspaceId={workspace._id}
activateRequest={r => db.update(workspace, {activeRequestId: r._id})}
changeFilter={actions.requests.changeFilter}
addRequestToRequestGroup={requestGroup => db.requestCreate({parent: requestGroup._id})}
addRequestToRequestGroup={requestGroup => db.requestCreate({parentId: requestGroup._id})}
toggleRequestGroup={requestGroup => db.update(requestGroup, {collapsed: !requestGroup.collapsed})}
activeRequest={activeRequest}
activeFilter={requests.filter}
requestGroups={requestGroups.all}
requests={requests.all}/>
activeRequestId={activeRequest ? activeRequest._id : null}
filter={requests.filter}
children={children}
/>
<div className="grid wide grid--collapse">
{this._renderRequestPanel(actions, activeRequest, tabs)}
{this._renderResponsePanel(actions, activeResponse, tabs)}
<RequestPane
request={activeRequest}
sendRequest={actions.requests.send}
updateRequestBody={body => db.update(activeRequest, {body})}
updateRequestUrl={url => db.update(activeRequest, {url})}
updateRequestMethod={method => db.update(activeRequest, {method})}
updateRequestParams={params => db.update(activeRequest, {params})}
updateRequestAuthentication={authentication => db.update(activeRequest, {authentication})}
updateRequestHeaders={headers => db.update(activeRequest, {headers})}
/>
<ResponsePane
response={activeResponse}
/>
</div>
<Prompts />
{modals.map(m => {
if (m.id === EnvironmentEditModal.defaultProps.id) {
return (
@ -234,43 +114,36 @@ class App extends Component {
App.propTypes = {
actions: PropTypes.shape({
requests: PropTypes.shape({
activate: PropTypes.func.isRequired,
update: PropTypes.func.isRequired,
remove: PropTypes.func.isRequired,
send: PropTypes.func.isRequired,
changeFilter: PropTypes.func.isRequired
}),
requestGroups: PropTypes.shape({
remove: PropTypes.func.isRequired,
update: PropTypes.func.isRequired,
toggle: PropTypes.func.isRequired
}),
modals: PropTypes.shape({
hide: PropTypes.func.isRequired
}),
tabs: PropTypes.shape({
select: PropTypes.func.isRequired
})
}).isRequired,
requestGroups: PropTypes.shape({
all: PropTypes.array.isRequired
entities: PropTypes.shape({
requests: PropTypes.object.isRequired,
requestGroups: PropTypes.object.isRequired,
responses: PropTypes.object.isRequired
}).isRequired,
workspaces: PropTypes.shape({
activeId: PropTypes.string
}).isRequired,
requests: PropTypes.shape({
all: PropTypes.array.isRequired,
active: PropTypes.string // "required" but can be null
filter: PropTypes.string.isRequired
}).isRequired,
responses: PropTypes.object.isRequired,
tabs: PropTypes.object.isRequired,
modals: PropTypes.array.isRequired
};
function mapStateToProps (state) {
return {
actions: state.actions,
workspaces: state.workspaces,
requests: state.requests,
requestGroups: state.requestGroups,
responses: state.responses,
tabs: state.tabs,
entities: state.entities,
modals: state.modals
};
}
@ -279,7 +152,6 @@ function mapDispatchToProps (dispatch) {
return {
actions: {
global: bindActionCreators(GlobalActions, dispatch),
tabs: bindActionCreators(TabActions, dispatch),
modals: bindActionCreators(ModalActions, dispatch),
requestGroups: bindActionCreators(RequestGroupActions, dispatch),
requests: bindActionCreators(RequestActions, dispatch)

View File

@ -3,12 +3,14 @@ import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import * as ModalActions from '../redux/modules/modals'
import * as RequestGroupActions from '../redux/modules/requestGroups'
import * as RequestActions from '../redux/modules/requests'
import PromptModal from '../components/base/PromptModal'
import * as db from '../database'
import {MODAL_REQUEST_RENAME, MODAL_REQUEST_GROUP_RENAME} from '../lib/constants';
import {
MODAL_REQUEST_RENAME,
MODAL_REQUEST_GROUP_RENAME,
MODAL_WORKSPACE_RENAME
} from '../lib/constants';
class Prompts extends Component {
constructor (props) {
@ -29,6 +31,14 @@ class Prompts extends Component {
db.update(modal.data.requestGroup, {name})
}
};
this._prompts[MODAL_WORKSPACE_RENAME] = {
header: 'Rename Workspace',
submit: 'Rename',
onSubmit: (modal, name) => {
db.update(modal.data.workspace, {name})
}
};
}
render () {
@ -65,12 +75,6 @@ Prompts.propTypes = {
actions: PropTypes.shape({
modals: PropTypes.shape({
hide: PropTypes.func.isRequired
}),
requestGroups: PropTypes.shape({
update: PropTypes.func.isRequired
}),
requests: PropTypes.shape({
update: PropTypes.func.isRequired
})
}),
modals: PropTypes.array.isRequired
@ -86,9 +90,7 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return {
actions: {
requests: bindActionCreators(RequestActions, dispatch),
modals: bindActionCreators(ModalActions, dispatch),
requestGroups: bindActionCreators(RequestGroupActions, dispatch)
modals: bindActionCreators(ModalActions, dispatch)
}
}
}

View File

@ -17,7 +17,7 @@ class RequestActionsDropdown extends Component {
<ul>
<li>
<button onClick={e => db.requestCopy(request)}>
<i className="fa fa-copy"></i> Duplicate
<i className="fa fa-copy"></i> Clone
</button>
</li>
<li>

View File

@ -38,8 +38,6 @@ class RequestGroupActionsDropdown extends Component {
RequestGroupActionsDropdown.propTypes = {
actions: PropTypes.shape({
update: PropTypes.func.isRequired,
remove: PropTypes.func.isRequired,
showUpdateNamePrompt: PropTypes.func.isRequired,
showEnvironmentEditModal: PropTypes.func.isRequired
}),

View File

@ -8,6 +8,7 @@ import {connect} from 'react-redux'
import Dropdown from '../components/base/Dropdown'
import DropdownDivider from '../components/base/DropdownDivider'
import * as RequestGroupActions from '../redux/modules/requestGroups'
import * as WorkspaceActions from '../redux/modules/workspaces'
import * as db from '../database'
import importData from '../lib/import'
@ -19,25 +20,46 @@ class WorkspaceDropdown extends Component {
name: 'Insomnia Imports', extensions: ['json']
}]
};
// TODO: Factor this out into a selector
const {entities, workspaces} = this.props;
let workspace = entities.workspaces[workspaces.activeId];
if (!workspace) {
workspace = entities.workspaces[Object.keys(entities.workspaces)[0]];
}
electron.remote.dialog.showOpenDialog(options, paths => {
paths.map(path => {
fs.readFile(path, 'utf8', (err, data) => {
err || importData(data);
err || importData(workspace, data);
})
})
});
}
_workspaceCreate () {
db.workspaceCreate({name: 'New Workspace'}).then(workspace => {
this.props.actions.workspaces.activate(workspace);
});
}
render () {
const {actions, loading, ...other} = this.props;
const {actions, loading, workspaces, entities, ...other} = this.props;
const allWorkspaces = Object.keys(entities.workspaces).map(id => entities.workspaces[id]);
// TODO: Factor this out into a selector
let workspace = entities.workspaces[workspaces.activeId];
if (!workspace) {
workspace = entities.workspaces[Object.keys(entities.workspaces)[0]];
}
return (
<Dropdown right={true} {...other} className="block">
<button className="btn header__content">
<div className="grid grid--center">
<div className="grid__cell">
<h1 className="no-pad">Insomnia</h1>
<h1 className="no-pad">{workspace.name}</h1>
</div>
<div className="no-wrap">
{loading ? <i className="fa fa-refresh fa-spin txt-lg"></i> : ''}&nbsp;
@ -46,54 +68,58 @@ class WorkspaceDropdown extends Component {
</div>
</button>
<ul>
<DropdownDivider name="Current Workspace" />
<DropdownDivider name="Current Workspace"/>
<li>
<button onClick={e => db.requestCreate()}>
<button onClick={e => db.requestCreate({parentId: workspace._id})}>
<i className="fa fa-plus-circle"></i> New Request
</button>
</li>
<li>
<button onClick={e => db.requestGroupCreate()}>
<button onClick={e => db.requestGroupCreate({parentId: workspace._id})}>
<i className="fa fa-folder"></i> New Request Group
</button>
</li>
<li>
<button onClick={e => actions.showEnvironmentEditModal()}>
{/*<li>
<button onClick={e => actions.requestGroups.showEnvironmentEditModal()}>
<i className="fa fa-code"></i> Manage Environments
</button>
</li>
</li>*/}
<li>
<button onClick={e => this._importDialog()}>
<i className="fa fa-share-square-o"></i> Import/Export
</button>
</li>
<li>
<button>
<i className="fa fa-empty"></i> Delete <strong>Sendwithus</strong>
<button onClick={e => actions.workspaces.showUpdateNamePrompt(workspace)}>
<i className="fa fa-empty"></i> Rename <strong>{workspace.name}</strong>
</button>
</li>
<li>
<button onClick={e => db.remove(workspace)}>
<i className="fa fa-empty"></i> Delete <strong>{workspace.name}</strong>
</button>
</li>
<DropdownDivider name="Workspaces" />
<DropdownDivider name="Workspaces"/>
{allWorkspaces.map(w => {
return w._id === workspace._id ? null : (
<li key={w._id}>
<button onClick={() => actions.workspaces.activate(w)}>
<i className="fa fa-random"></i> Switch to <strong>{w.name}</strong>
</button>
</li>
)
})}
<li>
<button>
<i className="fa fa-random"></i> Switch to <strong>Sendwithus Track</strong>
</button>
</li>
<li>
<button>
<i className="fa fa-random"></i> Switch to <strong>Default</strong>
</button>
</li>
<li>
<button>
<button onClick={e => this._workspaceCreate()}>
<i className="fa fa-blank"></i> Create Workspace
</button>
</li>
<DropdownDivider name="Insomnia" />
<DropdownDivider name="Insomnia"/>
<li><button><i className="fa fa-cog"></i> Settings</button></li>
<li><button><i className="fa fa-blank"></i> Open New Window</button></li>
@ -105,13 +131,27 @@ class WorkspaceDropdown extends Component {
WorkspaceDropdown.propTypes = {
loading: PropTypes.bool.isRequired,
workspaces: PropTypes.shape({
activeId: PropTypes.string
}),
entities: PropTypes.shape({
workspaces: PropTypes.object.isRequired
}).isRequired,
actions: PropTypes.shape({
showEnvironmentEditModal: PropTypes.func.isRequired
requestGroups: PropTypes.shape({
showEnvironmentEditModal: PropTypes.func.isRequired
}),
workspaces: PropTypes.shape({
activate: PropTypes.func.isRequired,
showUpdateNamePrompt: PropTypes.func.isRequired
})
})
};
function mapStateToProps (state) {
return {
workspaces: state.workspaces,
entities: state.entities,
actions: state.actions,
loading: state.global.loading
};
@ -119,7 +159,10 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return {
actions: bindActionCreators(RequestGroupActions, dispatch)
actions: {
requestGroups: bindActionCreators(RequestGroupActions, dispatch),
workspaces: bindActionCreators(WorkspaceActions, dispatch)
}
}
}

View File

@ -19,7 +19,6 @@
margin-bottom: 3px;
border-radius: $radius-md;
background: $bg-super-light;
padding: $padding-xs;
overflow: hidden;
li {
@ -29,7 +28,7 @@
padding: $padding-sm $padding-md $padding-sm $padding-sm;
width: 100%;
display: block;
color: $font-super-light-bg;
color: $font-super-light-bg !important;
&:hover {
background: $hl-sm;

View File

@ -9,6 +9,7 @@ $border-color: $hl-md;
align-items: flex-start;
align-content: flex-start;
height: $line-height-sm;
line-height: $line-height-sm;
&::after {
width: 100%;
@ -21,12 +22,16 @@ $border-color: $hl-md;
.ReactTabs__Tab {
align-self: flex-start;
padding: $padding-sm / 4 $padding-md;
height: $line-height-sm;
box-sizing: border-box;
border: 1px solid transparent;
border-bottom: 1px solid $border-color;
border-top: 0 !important;
* {
color: $hl-xxl;
}
&:first-child {
border-left-color: transparent;
}
@ -35,26 +40,32 @@ $border-color: $hl-md;
outline: 0;
}
button {
color: $hl-xxl;
position: relative;
& > * {
height: 100%;
width: 100%;
border-left: 1px solid transparent;
border-right: 1px solid transparent;
padding-left: $padding-md / 4;
padding-right: $padding-md / 4;
&:first-child {
padding-left: $padding-md;
}
&:last-child {
padding-right: $padding-md;
}
}
}
.ReactTabs__Tab--selected {
border: 1px solid $border-color;
border-bottom-color: transparent;
button {
* {
color: inherit;
border: 0 !important;
}
button:hover {
& > button:hover {
background: transparent;
}
}

View File

@ -37,6 +37,10 @@ $modal-width: 50rem;
$breakpoint-md: 790px;
$breakpoint-sm: 580px;
.txt-xs {
font-size: $font-size-xs;
}
.txt-sm {
font-size: $font-size-sm;
}

View File

@ -29,7 +29,7 @@ h3 {
}
h1, h2, h3 {
padding-top: 1.5em;
padding-bottom: 1em;
}
hr {
@ -40,6 +40,11 @@ hr {
margin: $padding-md 0;
}
label {
color: $hl-xxl;
font-size: 0.9em;
}
.monospace {
font-family: monospace;
}

View File

@ -2,6 +2,11 @@
import * as methods from '../lib/constants';
import {generateId} from './util'
export const TYPE_WORKSPACE = 'Workspace';
export const TYPE_REQUEST_GROUP = 'RequestGroup';
export const TYPE_REQUEST = 'Request';
export const TYPE_RESPONSE = 'Response';
// We have to include the web version of PouchDB in app.html because
// the NodeJS version defaults to LevelDB which is hard (impossible?)
// to get working in Electron apps
@ -10,19 +15,47 @@ let db = new PouchDB('insomnia.db', {adapter: 'websql'});
// For browser console debugging
global.db = db;
export let changes = db.changes({
let changeListeners = {};
export function onChange (id, callback) {
console.log(`-- Added DB Listener ${id} -- `);
changeListeners[id] = callback;
}
export function offChange (id) {
console.log(`-- Removed DB Listener ${id} -- `);
delete changeListeners[id];
}
export function allDocs () {
return db.allDocs({include_docs: true});
}
db.changes({
since: 'now',
live: true,
include_docs: true,
return_docs: false
}).on('change', function (res) {
Object.keys(changeListeners).map(id => changeListeners[id](res))
}).on('complete', function (info) {
console.log('complete', info);
}).on('error', function (err) {
console.log('error', err);
});
export function allDocs () {
return db.allDocs({include_docs: true});
/**
* Initialize the database. This should be called once on app start.
* @returns {Promise}
*/
export function initDB () {
console.log('-- Initializing Database --');
return Promise.all([
db.createIndex({index: {fields: ['parentId']}}),
db.createIndex({index: {fields: ['type']}})
]).catch(err => {
console.error('Failed to PouchDB Indexes', err);
});
}
export function get (id) {
@ -40,6 +73,7 @@ export function update (doc, patch = {}) {
return db.put(updatedDoc).catch(e => {
if (e.status === 409) {
console.warn('Retrying document update for', updatedDoc);
get(doc._id).then(dbDoc => {
update(dbDoc, patch);
});
@ -47,8 +81,20 @@ export function update (doc, patch = {}) {
});
}
export function getChildren (doc) {
const parentId = doc._id;
return db.find({selector: {parentId}});
}
export function removeChildren (doc) {
return getChildren(doc).then(res => res.docs.map(remove));
}
export function remove (doc) {
return update(doc, {_deleted: true});
return Promise.all([
update(doc, {_deleted: true}),
removeChildren(doc)
]);
}
// ~~~~~~~~~~~~~~~~~~~ //
@ -56,7 +102,12 @@ export function remove (doc) {
// ~~~~~~~~~~~~~~~~~~~ //
function modelCreate (type, idPrefix, defaults, patch = {}) {
const model = Object.assign(
const baseDefaults = {
parentId: null
};
const doc = Object.assign(
baseDefaults,
defaults,
patch,
@ -70,33 +121,30 @@ function modelCreate (type, idPrefix, defaults, patch = {}) {
}
);
update(model);
return model;
return update(doc).then(() => doc);
}
// ~~~~~~~ //
// REQUEST //
// ~~~~~~~ //
export function requestCreate (patch = {}) {
return modelCreate('Request', 'req', {
return modelCreate(TYPE_REQUEST, 'req', {
url: '',
name: 'New Request',
method: methods.METHOD_GET,
activated: Date.now(),
body: '',
params: [],
contentType: 'text/plain',
headers: [],
authentication: {},
parent: null
authentication: {}
}, patch);
}
export function requestCopy (originalRequest) {
const name = `${originalRequest.name} (Copy)`;
return requestCreate(Object.assign({}, originalRequest, {name}));
export function requestCopy (request) {
const name = `${request.name} (Copy)`;
return requestCreate(Object.assign({}, request, {name}));
}
@ -105,22 +153,19 @@ export function requestCopy (originalRequest) {
// ~~~~~~~~~~~~~ //
export function requestGroupCreate (patch = {}) {
return modelCreate('RequestGroup', 'grp', {
return modelCreate(TYPE_REQUEST_GROUP, 'grp', {
collapsed: false,
name: 'New Request Group',
environment: {},
parent: null
environment: {}
}, patch);
}
// ~~~~~~~~ //
// RESPONSE //
// ~~~~~~~~ //
export function responseCreate (patch = {}) {
return modelCreate('Response', 'rsp', {
requestId: null,
return modelCreate(TYPE_RESPONSE, 'res', {
statusCode: 0,
statusMessage: '',
contentType: 'text/plain',
@ -131,32 +176,44 @@ export function responseCreate (patch = {}) {
}, patch);
}
db.createIndex({
index: {fields: ['requestId']}
}).catch(err => {
console.error('Failed to create index', err);
}).then(() => {
console.log('-- Indexes Updated --');
});
export function responseGetForRequest (request) {
return db.find({
selector: {
requestId: request._id
},
sort: [{requestId: 'desc'}],
limit: 1
})
}
// ~~~~~~~~~ //
// WORKSPACE //
// ~~~~~~~~~ //
export function workspaceCreate (patch = {}) {
return modelCreate('Workspace', 'wsp', {
name: 'New Request Group',
return modelCreate(TYPE_WORKSPACE, 'wrk', {
name: 'New Workspace',
activeRequestId: null,
environments: []
}, patch);
}
export function workspaceAll () {
return db.find({
selector: {type: 'Workspace'}
}).then(res => {
if (res.docs.length) {
return res;
} else {
// No workspaces? Create first one and try again
// TODO: Replace this with UI flow maybe?
console.log('-- Creating First Workspace --');
return workspaceCreate({name: 'Insomnia'}).then(() => {
return workspaceAll();
})
}
})
}
// ~~~~~~~~ //
// SETTINGS //
// ~~~~~~~~ //
// TODO: This
// export function settingsCreate (patch = {}) {
// return modelCreate('Settings', 'set', {
// editorLineWrapping: false,
// editorLineNumbers: true
// }, patch);
// }

View File

@ -5,7 +5,7 @@ const CHARS = '23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ'.split('')
export function generateId (prefix) {
let id = `${prefix}/${Date.now()}/`;
for (let i = 0; i < 10; i++) {
for (let i = 0; i < 13; i++) {
id += CHARS[Math.floor(Math.random() * CHARS.length)];
}

View File

@ -1,75 +1,31 @@
import React from 'react'
import {render} from 'react-dom'
import {Provider} from 'react-redux'
import {bindActionCreators} from 'redux'
import {Tabs} from 'react-tabs'
import createStore from './redux/create'
import App from './containers/App'
import * as RequestGroupActions from './redux/modules/requestGroups'
import * as RequestActions from './redux/modules/requests'
import * as ResponseActions from './redux/modules/responses'
import * as db from './database'
// Global CSS
import './css/index.scss'
import './css/lib/chrome/platform_app.css'
import './css/lib/fontawesome/css/font-awesome.css'
import {initStore} from './redux/initstore'
import {initDB} from './database'
const store = createStore();
// Don't inject component styles (use our own)
Tabs.setUseDefaultStyles(false);
// Dispatch the initial load of data
console.log('-- Init Insomnia --');
export const store = createStore();
const actionFns = {
RequestGroup: bindActionCreators(RequestGroupActions, store.dispatch),
Request: bindActionCreators(RequestActions, store.dispatch),
Response: bindActionCreators(ResponseActions, store.dispatch)
};
console.log('-- Loading App --');
function refreshDoc (doc) {
const fns = actionFns[doc.type];
if (fns) {
fns[doc._deleted ? 'remove' : 'update'](doc);
} else if (doc.hasOwnProperty('type')) {
console.warn('Unknown change', doc.type, doc);
} else {
// Probably a design doc update or something...
}
}
function watchDB () {
console.log('-- Watching PouchDB --');
let buffer = [];
let timeout = null;
// Debounce and buffer changes if they happen in quick succession
db.changes.on('change', (response) => {
const doc = response.doc;
buffer.push(doc);
clearTimeout(timeout);
timeout = setTimeout(() => {
buffer.map(refreshDoc);
buffer = [];
}, 50);
initDB()
.then(() => initStore(store.dispatch))
.then(() => {
console.log('-- Rendering App --');
render(
<Provider store={store}><App /></Provider>,
document.getElementById('root')
);
});
}
function restoreDB() {
db.allDocs().then(response => {
response.rows.map(row => refreshDoc(row.doc));
})
}
watchDB();
restoreDB();
render(
<Provider store={store}><App /></Provider>,
document.getElementById('root')
);

View File

@ -17,6 +17,7 @@ export const METHODS = [
METHOD_HEAD
];
export const MODAL_REQUEST_RENAME = 'request.update.name';
export const MODAL_WORKSPACE_RENAME = 'workspace.update.name';
export const MODAL_REQUEST_GROUP_RENAME = 'requestgroup.update.name';
export const MODAL_REQUEST_RENAME = 'request.update.name';
export const MODAL_ENVIRONMENT_EDITOR = 'environment.edit';

View File

@ -4,27 +4,30 @@ const TYPE_REQUEST = 'request';
const TYPE_REQUEST_GROUP = 'request_group';
const FORMAT_MAP = {
'json': 'application/json'
// TODO: Fill these out
};
function importRequestGroup (iRequestGroup, exportFormat) {
function importRequestGroup (iRequestGroup, parentId, exportFormat) {
if (exportFormat === 1) {
const requestGroup = db.requestGroupCreate({
db.requestGroupCreate({
parentId,
collapsed: true,
name: iRequestGroup.name,
environment: (iRequestGroup.environments || {}).base || {}
}).then(requestGroup => {
// Sometimes (maybe all the time, I can't remember) requests will be nested
if (iRequestGroup.hasOwnProperty('requests')) {
// Let's process them oldest to newest
iRequestGroup.requests.reverse();
iRequestGroup.requests.map(
r => importRequest(r, requestGroup._id, exportFormat)
);
}
});
// Sometimes (maybe all the time, I can't remember) requests will be nested
if (iRequestGroup.hasOwnProperty('requests')) {
// Let's process them oldest to newest
iRequestGroup.requests.reverse();
iRequestGroup.requests.map(
r => importRequest(r, requestGroup._id, exportFormat)
);
}
}
}
function importRequest (iRequest, parent, exportFormat) {
function importRequest (iRequest, parentId, exportFormat) {
if (exportFormat === 1) {
let auth = {};
if (iRequest.authentication.username) {
@ -33,8 +36,10 @@ function importRequest (iRequest, parent, exportFormat) {
password: iRequest.authentication.password
}
}
db.requestCreate({
parentId,
activated: 0, // Don't activate imported requests
name: iRequest.name,
url: iRequest.url,
method: iRequest.method,
@ -42,31 +47,32 @@ function importRequest (iRequest, parent, exportFormat) {
headers: iRequest.headers || [],
params: iRequest.params || [],
contentType: FORMAT_MAP[iRequest.__insomnia.format] || 'text/plain',
authentication: auth,
parent: parent
authentication: auth
});
}
}
export default function (txt, callback) {
export default function (workspace, txt) {
let data;
try {
data = JSON.parse(txt);
} catch (e) {
return callback(new Error('Invalid Insomnia export'));
// TODO: Handle these errors
return;
}
if (!data.hasOwnProperty('_type') || !data.hasOwnProperty('items')) {
return callback(new Error('Invalid Insomnia export'));
// TODO: Handle these errors
return;
}
data.items.filter(i => i._type === TYPE_REQUEST_GROUP).map(
rg => importRequestGroup(rg, data.__export_format)
data.items.reverse().filter(i => i._type === TYPE_REQUEST_GROUP).map(
rg => importRequestGroup(rg, workspace._id, data.__export_format)
);
data.items.filter(i => i._type === TYPE_REQUEST).map(
r => importRequest(r, data.__export_format)
data.items.reverse().filter(i => i._type === TYPE_REQUEST).map(
r => importRequest(r, workspace._id, data.__export_format)
);
}

View File

@ -2,11 +2,11 @@ import networkRequest from 'request'
import render from './render'
import * as db from '../database'
function makeRequest (unrenderedRequest, callback, context = {}) {
function actuallySend (unrenderedRequest, callback, context = {}) {
// SNEAKY HACK: Render nested object by converting it to JSON then rendering
const template = JSON.stringify(unrenderedRequest);
const request = JSON.parse(render(template, context));
const config = {
method: request.method,
body: request.body,
@ -35,6 +35,7 @@ function makeRequest (unrenderedRequest, callback, context = {}) {
}
}
// TODO: this needs to account for existing URL params
config.url += request.params.map((p, i) => {
const name = encodeURIComponent(p.name);
const value = encodeURIComponent(p.value);
@ -47,7 +48,7 @@ function makeRequest (unrenderedRequest, callback, context = {}) {
console.error('Request Failed', err, response);
} else {
db.responseCreate({
requestId: request._id,
parentId: request._id,
statusCode: response.statusCode,
statusMessage: response.statusMessage,
contentType: response.headers['content-type'],
@ -65,12 +66,12 @@ function makeRequest (unrenderedRequest, callback, context = {}) {
});
}
export default function (request, callback) {
if (request.parent) {
db.get(request.parent).then(
requestGroup => makeRequest(request, callback, requestGroup.environment)
export function send (request, callback) {
if (request.parentId) {
db.get(request.parentId).then(
requestGroup => actuallySend(request, callback, requestGroup.environment)
);
} else {
makeRequest(request, callback)
actuallySend(request, callback)
}
}

View File

@ -3,27 +3,22 @@ import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import rootReducer from './reducer'
const loggerMiddleware = createLogger({
collapsed: true
});
export default function configureStore (initialState) {
const store = createStore(
rootReducer,
initialState,
applyMiddleware(
thunkMiddleware,
loggerMiddleware
createLogger({collapsed: true})
)
);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('./reducer', () => {
const nextReducer = require('./reducer').default;
const nextReducer = require('./reducer.js').default;
store.replaceReducer(nextReducer);
})
}
return store
return store;
}

48
app/redux/initstore.js Normal file
View File

@ -0,0 +1,48 @@
import {bindActionCreators} from 'redux'
import * as entitiesActions from './modules/entities'
import * as db from '../database'
const CHANGE_ID = 'store.listener';
export function initStore (dispatch) {
db.offChange(CHANGE_ID);
// New stuff...
const entities = bindActionCreators(entitiesActions, dispatch);
const docChanged = doc => {
if (!doc.hasOwnProperty('type')) {
return;
}
// New stuff...
entities[doc._deleted ? 'remove' : 'update'](doc);
};
console.log('-- Restoring Store --');
const start = Date.now();
return db.workspaceAll().then(res => {
const restoreChildren = (doc) => {
docChanged(doc);
return db.getChildren(doc).then(res => {
// Done condition
if (!res.docs.length) {
return;
}
return Promise.all(
res.docs.map(doc => restoreChildren(doc))
);
})
};
return res.docs.map(restoreChildren)
}).then(() => {
console.log(`Restore took ${(Date.now() - start) / 1000} s`);
}).then(() => {
db.onChange(CHANGE_ID, res => docChanged(res.doc));
});
}

View File

@ -0,0 +1,69 @@
import {combineReducers} from 'redux'
import {TYPE_WORKSPACE, TYPE_REQUEST_GROUP, TYPE_REQUEST, TYPE_RESPONSE} from '../../database/index'
import * as workspaceFns from './workspaces'
const ENTITY_UPDATE = 'entities/update';
const ENTITY_REMOVE = 'entities/remove';
// ~~~~~~~~ //
// REDUCERS //
// ~~~~~~~~ //
function genericEntityReducer (referenceName) {
return function (state = {}, action) {
const doc = action[referenceName];
if (!doc) {
return state;
}
switch (action.type) {
case ENTITY_UPDATE:
return {...state, [doc._id]: doc};
case ENTITY_REMOVE:
const newState = Object.assign({}, state);
delete newState[action[referenceName]._id];
return newState;
default:
return state;
}
}
}
export default combineReducers({
workspaces: genericEntityReducer('workspace'),
requestGroups: genericEntityReducer('requestGroup'),
requests: genericEntityReducer('request'),
responses: genericEntityReducer('response')
})
// ~~~~~~~ //
// ACTIONS //
// ~~~~~~~ //
const updateFns = {
[TYPE_WORKSPACE]: workspace => ({type: ENTITY_UPDATE, workspace}),
[TYPE_REQUEST_GROUP]: requestGroup => ({type: ENTITY_UPDATE, requestGroup}),
[TYPE_RESPONSE]: response => ({type: ENTITY_UPDATE, response}),
[TYPE_REQUEST]: request => ({type: ENTITY_UPDATE, request})
};
const removeFns = {
[TYPE_WORKSPACE]: workspace => ({type: ENTITY_REMOVE, workspace}),
[TYPE_REQUEST_GROUP]: requestGroup => ({type: ENTITY_REMOVE, requestGroup}),
[TYPE_RESPONSE]: response => ({type: ENTITY_UPDATE, response}),
[TYPE_REQUEST]: request => ({type: ENTITY_REMOVE, request})
};
export function update (doc) {
return updateFns[doc.type](doc);
}
export function remove (doc) {
return removeFns[doc.type](doc);
}

View File

@ -1,58 +1,20 @@
import {combineReducers} from 'redux'
import {show} from './modals'
import {MODAL_ENVIRONMENT_EDITOR, MODAL_REQUEST_GROUP_RENAME} from '../../lib/constants';
import {MODAL_ENVIRONMENT_EDITOR, MODAL_REQUEST_GROUP_RENAME} from '../../lib/constants'
export const REQUEST_GROUP_UPDATE = 'requestgroups/update';
export const REQUEST_GROUP_DELETE = 'requestgroups/delete';
export const REQUEST_GROUP_TOGGLE = 'requestgroups/toggle';
export const REQUEST_GROUP_TOGGLE = 'request-groups/toggle';
// ~~~~~~~~ //
// REDUCERS //
// ~~~~~~~~ //
function allReducer (state = [], action) {
switch (action.type) {
case REQUEST_GROUP_UPDATE:
const i = state.findIndex(r => r._id === action.requestGroup._id);
if (i === -1) {
return [action.requestGroup, ...state];
} else {
return [...state.slice(0, i), action.requestGroup, ...state.slice(i + 1)]
}
case REQUEST_GROUP_TOGGLE:
return state.map(
rg => rg._id === action._id ? Object.assign({}, rg, {collapsed: !rg.collapsed}) : rg
);
case REQUEST_GROUP_DELETE:
return state.filter(rg => rg._id !== action.requestGroup._id);
default:
return state;
}
}
export default combineReducers({
all: allReducer
});
// Nothing yet...
// ~~~~~~~ //
// ACTIONS //
// ~~~~~~~ //
export function update (requestGroup) {
return {type: REQUEST_GROUP_UPDATE, requestGroup};
}
export function remove (requestGroup) {
return {type: REQUEST_GROUP_DELETE, requestGroup};
}
export function toggle (requestGroup) {
return {type: REQUEST_GROUP_TOGGLE, requestGroup}
}

View File

@ -1,86 +1,35 @@
import {combineReducers} from 'redux'
import makeRequest from '../../lib/request'
import * as network from '../../lib/network'
import {loadStart, loadStop} from './global'
import {show} from './modals'
import {MODAL_REQUEST_RENAME} from '../../lib/constants'
export const REQUEST_UPDATE = 'requests/update';
export const REQUEST_DELETE = 'requests/delete';
export const REQUEST_ACTIVATE = 'requests/activate';
export const REQUEST_CHANGE_FILTER = 'requests/filter';
const initialState = {
filter: ''
};
// ~~~~~~~~ //
// REDUCERS //
// ~~~~~~~~ //
function allReducer (state = [], action) {
switch (action.type) {
case REQUEST_DELETE:
return state.filter(r => r._id !== action.request._id);
case REQUEST_UPDATE:
const i = state.findIndex(r => r._id === action.request._id);
if (i === -1) {
return [action.request, ...state];
} else {
return [...state.slice(0, i), action.request, ...state.slice(i + 1)]
}
default:
return state;
}
}
function activeReducer (state = null, action) {
switch (action.type) {
case REQUEST_ACTIVATE:
return action.request._id;
case REQUEST_DELETE:
return state === action._id ? null : state;
default:
return state;
}
}
function filterReducer (state = '', action) {
export default function (state = initialState, action) {
switch (action.type) {
case REQUEST_CHANGE_FILTER:
return action.filter;
const filter = action.filter;
return Object.assign({}, state, {filter});
default:
return state;
}
}
export default combineReducers({
all: allReducer,
filter: filterReducer,
active: activeReducer
});
// ~~~~~~~ //
// ACTIONS //
// ~~~~~~~ //
export function remove (request) {
return {type: REQUEST_DELETE, request};
}
export function update (request) {
return {type: REQUEST_UPDATE, request};
}
export function activate (request) {
return {type: REQUEST_ACTIVATE, request};
}
export function changeFilter (filter) {
return {type: REQUEST_CHANGE_FILTER, filter};
}
@ -89,7 +38,7 @@ export function send (request) {
return dispatch => {
dispatch(loadStart());
makeRequest(request, () => {
network.send(request, () => {
dispatch(loadStop());
});
}

View File

@ -1,29 +1,12 @@
const RESPONSE_UPDATE = 'responses/update';
const initialState = {};
// ~~~~~~~~ //
// REDUCERS //
// ~~~~~~~~ //
export default function (state = initialState, action) {
switch (action.type) {
case RESPONSE_UPDATE:
return Object.assign({}, state, {
[action.response.requestId]: action.response
});
default:
return state;
}
}
// None yet
// ~~~~~~~ //
// ACTIONS //
// ~~~~~~~ //
export function update (response) {
return {type: RESPONSE_UPDATE, response};
}
// None yet...

View File

@ -1,39 +1,26 @@
import {combineReducers} from 'redux'
import {MODAL_WORKSPACE_RENAME} from '../../lib/constants'
import {show} from './modals'
export const WORKSPACE_UPDATE = 'workspaces/update';
export const WORKSPACE_DELETE = 'workspaces/delete';
export const WORKSPACE_ACTIVATE = 'workspaces/activate';
// ~~~~~~~~ //
// REDUCERS //
// ~~~~~~~~ //
function allReducer (state = [], action) {
switch (action.type) {
default:
return state;
}
}
function activeReducer (state = null, action) {
switch (action.type) {
default:
return state;
}
}
case WORKSPACE_ACTIVATE:
return action.workspace._id;
function filterReducer (state = '', action) {
switch (action.type) {
default:
return state;
}
}
export default combineReducers({
all: allReducer,
filter: filterReducer,
active: activeReducer
activeId: activeReducer
});
@ -41,15 +28,11 @@ export default combineReducers({
// ACTIONS //
// ~~~~~~~ //
export function remove (request) {
return {type: WORKSPACE_DELETE, request};
export function activate (workspace) {
return {type: WORKSPACE_ACTIVATE, workspace};
}
export function update (request) {
return {type: WORKSPACE_UPDATE, request};
export function showUpdateNamePrompt (workspace) {
const defaultValue = workspace.name;
return show(MODAL_WORKSPACE_RENAME, {defaultValue, workspace});
}
export function activate (request) {
return {type: WORKSPACE_ACTIVATE, request};
}

View File

@ -1,19 +1,19 @@
import {combineReducers} from 'redux'
import workspacesReducer from './modules/workspaces'
import requestsReducer from './modules/requests'
import tabsReducer from './modules/tabs'
import globalReducer from './modules/global'
import modalsReducer from './modules/modals'
import requestGroupsReducer from './modules/requestGroups'
import responsesReducer from './modules/responses'
import workspaces from './modules/workspaces'
import requestGroups from './modules/requestGroups'
import requests from './modules/requests'
import responses from './modules/responses'
import global from './modules/global'
import modals from './modules/modals'
import entities from './modules/entities'
export default combineReducers({
workspaces: workspacesReducer,
requestGroups: requestGroupsReducer,
requests: requestsReducer,
responses: responsesReducer,
modals: modalsReducer,
global: globalReducer,
tabs: tabsReducer
workspaces,
responses,
requests,
requestGroups,
modals,
global,
entities
});

View File

@ -19,6 +19,7 @@
"react-tabs": "^0.5.3",
"redux": "^3.3.1",
"redux-logger": "^2.6.1",
"redux-shortcuts": "0.0.1",
"redux-thunk": "^2.0.1"
},
"devDependencies": {

View File

@ -1,4 +1,3 @@
import path from 'path'
import webpack from 'webpack'
import baseConfig from './webpack.config.base'