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 { class RequestBodyEditor extends Component {
render () { render () {
const {request, onChange, className} = this.props; const {body, contentType, requestId, onChange, className} = this.props;
const mode = request.contentType || 'text/plain';
return ( return (
<Editor <Editor
value={request.body} value={body}
className={className} className={className}
debounceMillis={400}
onChange={onChange} onChange={onChange}
uniquenessKey={request._id} uniquenessKey={requestId}
options={{ options={{
mode: mode, mode: contentType,
placeholder: 'request body here...' placeholder: 'request body here...'
}} }}
/> />
@ -22,10 +22,13 @@ class RequestBodyEditor extends Component {
} }
RequestBodyEditor.propTypes = { RequestBodyEditor.propTypes = {
request: PropTypes.shape({ // Functions
body: PropTypes.string.isRequired onChange: PropTypes.func.isRequired,
}).isRequired,
onChange: PropTypes.func.isRequired // Other
requestId: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
contentType: PropTypes.string.isRequired
}; };
export default RequestBodyEditor; 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'; import {METHODS} from '../lib/constants';
class UrlInput extends Component { class UrlInput extends Component {
_handleFormSubmit (e) {
e.preventDefault();
this.props.sendRequest();
}
render () { render () {
const {sendRequest, onUrlChange, onMethodChange, request} = this.props; const {onUrlChange, onMethodChange, uniquenessKey, url, method} = this.props;
return ( return (
<div className="tall grid grid--center wide bg-super-light"> <div className="tall grid grid--center wide bg-super-light">
<Dropdown className="tall"> <Dropdown className="tall">
<button className="pad tall txt-md"> <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> </button>
<ul> <ul>
{METHODS.map((method) => ( {METHODS.map(m => (
<li key={method}> <li key={m}>
<button onClick={onMethodChange.bind(null, method)}> <button onClick={onMethodChange.bind(null, m)}>
{method} {m}
</button> </button>
</li> </li>
))} ))}
</ul> </ul>
</Dropdown> </Dropdown>
<form className="tall grid__cell form-control form-control--wide" <form className="tall grid__cell form-control form-control--wide"
onSubmit={e => {e.preventDefault(); sendRequest(request)}}> onSubmit={this._handleFormSubmit.bind(this)}>
<DebouncingInput <DebouncingInput
type="text" type="text"
className="txt-md" className="txt-md"
placeholder="http://echo.insomnia.rest/status/200" placeholder="http://echo.insomnia.rest/status/200"
value={request.url} value={url}
debounceMillis={1000} debounceMillis={1000}
uniquenessKey={request._id} uniquenessKey={uniquenessKey}
onChange={onUrlChange}/> onChange={onUrlChange}/>
</form> </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 Send
</button>&nbsp;&nbsp; </button>&nbsp;&nbsp;
</div> </div>
@ -45,10 +51,9 @@ UrlInput.propTypes = {
sendRequest: PropTypes.func.isRequired, sendRequest: PropTypes.func.isRequired,
onUrlChange: PropTypes.func.isRequired, onUrlChange: PropTypes.func.isRequired,
onMethodChange: PropTypes.func.isRequired, onMethodChange: PropTypes.func.isRequired,
request: PropTypes.shape({ uniquenessKey: PropTypes.string.isRequired,
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
method: PropTypes.string.isRequired method: PropTypes.string.isRequired
}).isRequired
}; };
export default UrlInput; 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 React, {Component, PropTypes} from 'react'
import classnames from 'classnames'
import WorkspaceDropdown from './../containers/WorkspaceDropdown' import WorkspaceDropdown from './../containers/WorkspaceDropdown'
import RequestActionsDropdown from './../containers/RequestActionsDropdown'
import RequestGroupActionsDropdown from './../containers/RequestGroupActionsDropdown'
import DebouncingInput from './base/DebouncingInput' import DebouncingInput from './base/DebouncingInput'
import MethodTag from './MethodTag' import SidebarRequestGroupRow from './SidebarRequestGroupRow'
import * as db from '../database' import SidebarRequestRow from './SidebarRequestRow'
class Sidebar extends Component { class Sidebar extends Component {
onFilterChange (value) { onFilterChange (value) {
this.props.changeFilter(value); this.props.changeFilter(value);
} }
renderRequestGroupRow (requestGroup = null) { _filterChildren (filter, children, extra = null) {
const { return children.filter(child => {
activeFilter, if (child.doc.type !== 'Request') {
activeRequest,
addRequestToRequestGroup,
toggleRequestGroup,
requests
} = this.props;
let filteredRequests = requests.filter(
r => {
// TODO: Move this to a lib file
if (!activeFilter) {
return true; return true;
} }
const requestGroupName = requestGroup ? requestGroup.name : ''; const request = child.doc;
const toMatch = `${requestGroupName}${r.method}${r.name}`.toLowerCase();
const matchTokens = activeFilter.toLowerCase().split(' '); const otherMatches = extra || '';
const toMatch = `${request.method}${request.name}${otherMatches}`.toLowerCase();
const matchTokens = filter.toLowerCase().split(' ');
for (let i = 0; i < matchTokens.length; i++) { for (let i = 0; i < matchTokens.length; i++) {
let token = `${matchTokens[i]}`; let token = `${matchTokens[i]}`;
if (toMatch.indexOf(token) === -1) { if (toMatch.indexOf(token) === -1) {
// Filter failed. Don't render children
return false; return false;
} }
} }
return true; return true;
} })
);
if (!requestGroup) {
filteredRequests = filteredRequests.filter(r => !r.parent);
return filteredRequests.map(request => this.renderRequestRow(request));
} }
// Grab all of the children for this request group _renderChildren (children, requestGroup) {
filteredRequests = filteredRequests.filter(r => r.parent === requestGroup._id); const {filter} = this.props;
// Don't show folder if it was not in the filter const filteredChildren = this._filterChildren(
if (activeFilter && !filteredRequests.length) { 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; return null;
} }
})
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>
);
}
renderRequestRow (request = null, requestGroup = null) {
const {activeRequest, activateRequest} = this.props;
const isActive = request && activeRequest && request._id === activeRequest._id;
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>
);
} }
render () { render () {
const {activeFilter, requestGroups} = this.props; const {filter, children} = this.props;
return ( return (
<section className="sidebar bg-dark grid--v section section--bordered"> <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"> <div className="grid--v grid--start grid__cell section__body">
<ul <ul
className="grid--v grid--start grid__cell sidebar__scroll hover-scrollbars sidebar__request-list"> className="grid--v grid--start grid__cell sidebar__scroll hover-scrollbars sidebar__request-list">
{this.renderRequestGroupRow(null)} {this._renderChildren(children)}
{requestGroups.map(requestGroup => this.renderRequestGroupRow(requestGroup))}
</ul> </ul>
<div className="grid grid--center"> <div className="grid grid--center">
<div className="grid__cell form-control form-control--underlined"> <div className="grid__cell form-control form-control--underlined">
@ -145,7 +95,7 @@ class Sidebar extends Component {
type="text" type="text"
placeholder="Filter Items" placeholder="Filter Items"
debounceMillis={300} debounceMillis={300}
value={activeFilter} value={filter}
onChange={this.onFilterChange.bind(this)}/> onChange={this.onFilterChange.bind(this)}/>
</div> </div>
</div> </div>
@ -156,14 +106,19 @@ class Sidebar extends Component {
} }
Sidebar.propTypes = { Sidebar.propTypes = {
// Functions
activateRequest: PropTypes.func.isRequired, activateRequest: PropTypes.func.isRequired,
toggleRequestGroup: PropTypes.func.isRequired,
addRequestToRequestGroup: PropTypes.func.isRequired, addRequestToRequestGroup: PropTypes.func.isRequired,
changeFilter: PropTypes.func.isRequired, changeFilter: PropTypes.func.isRequired,
toggleRequestGroup: PropTypes.func.isRequired,
activeFilter: PropTypes.string, // Other
requests: PropTypes.array.isRequired, children: PropTypes.array.isRequired,
requestGroups: PropTypes.array.isRequired, workspaceId: PropTypes.string.isRequired,
activeRequest: PropTypes.object
// Optional
filter: PropTypes.string,
activeRequestId: PropTypes.string
}; };
export default Sidebar; 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 React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {bindActionCreators} from 'redux' import {bindActionCreators} from 'redux'
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'
import Editor from '../components/base/Editor'
import Prompts from './Prompts' 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 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 GlobalActions from '../redux/modules/global'
import * as RequestGroupActions from '../redux/modules/requestGroups' import * as RequestGroupActions from '../redux/modules/requestGroups'
import * as RequestActions from '../redux/modules/requests' import * as RequestActions from '../redux/modules/requests'
import * as ModalActions from '../redux/modules/modals' import * as ModalActions from '../redux/modules/modals'
import * as TabActions from '../redux/modules/tabs'
import * as db from '../database' import * as db from '../database'
// Don't inject component styles (use our own)
Tabs.setUseDefaultStyles(false);
class App extends Component { class App extends Component {
constructor (props) { constructor (props) {
super(props); super(props);
@ -35,183 +24,74 @@ class App extends Component {
} }
} }
_renderRequestPanel (actions, activeRequest, tabs) { _generateSidebarTree (parentId, entities) {
if (!activeRequest) { const children = entities.filter(e => e.parentId === parentId);
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 ( if (children.length > 0) {
<section className="grid__cell section section--bordered"> return children.map(c => ({
<div className="grid--v wide"> doc: c,
<div className="header section__header"> children: this._generateSidebarTree(c._id, entities)
<RequestUrlBar }));
sendRequest={actions.requests.send} } else {
onUrlChange={url => {db.update(activeRequest, {url})}} return children;
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 () { render () {
const {actions, requests, responses, requestGroups, tabs, modals} = this.props; const {actions, modals, workspaces, requests, entities} = this.props;
const activeRequest = requests.all.find(r => r._id === requests.active);
const activeResponse = activeRequest ? responses[activeRequest._id] : undefined; // 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 ( return (
<div className="grid bg-super-dark tall"> <div className="grid bg-super-dark tall">
<Sidebar <Sidebar
activateRequest={actions.requests.activate} workspaceId={workspace._id}
activateRequest={r => db.update(workspace, {activeRequestId: r._id})}
changeFilter={actions.requests.changeFilter} 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})} toggleRequestGroup={requestGroup => db.update(requestGroup, {collapsed: !requestGroup.collapsed})}
activeRequest={activeRequest} activeRequestId={activeRequest ? activeRequest._id : null}
activeFilter={requests.filter} filter={requests.filter}
requestGroups={requestGroups.all} children={children}
requests={requests.all}/> />
<div className="grid wide grid--collapse"> <div className="grid wide grid--collapse">
{this._renderRequestPanel(actions, activeRequest, tabs)} <RequestPane
{this._renderResponsePanel(actions, activeResponse, tabs)} 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> </div>
<Prompts /> <Prompts />
{modals.map(m => { {modals.map(m => {
if (m.id === EnvironmentEditModal.defaultProps.id) { if (m.id === EnvironmentEditModal.defaultProps.id) {
return ( return (
@ -234,43 +114,36 @@ class App extends Component {
App.propTypes = { App.propTypes = {
actions: PropTypes.shape({ actions: PropTypes.shape({
requests: PropTypes.shape({ requests: PropTypes.shape({
activate: PropTypes.func.isRequired,
update: PropTypes.func.isRequired,
remove: PropTypes.func.isRequired,
send: PropTypes.func.isRequired, send: PropTypes.func.isRequired,
changeFilter: PropTypes.func.isRequired changeFilter: PropTypes.func.isRequired
}), }),
requestGroups: PropTypes.shape({ requestGroups: PropTypes.shape({
remove: PropTypes.func.isRequired,
update: PropTypes.func.isRequired,
toggle: PropTypes.func.isRequired toggle: PropTypes.func.isRequired
}), }),
modals: PropTypes.shape({ modals: PropTypes.shape({
hide: PropTypes.func.isRequired hide: PropTypes.func.isRequired
}),
tabs: PropTypes.shape({
select: PropTypes.func.isRequired
}) })
}).isRequired, }).isRequired,
requestGroups: PropTypes.shape({ entities: PropTypes.shape({
all: PropTypes.array.isRequired requests: PropTypes.object.isRequired,
requestGroups: PropTypes.object.isRequired,
responses: PropTypes.object.isRequired
}).isRequired,
workspaces: PropTypes.shape({
activeId: PropTypes.string
}).isRequired, }).isRequired,
requests: PropTypes.shape({ requests: PropTypes.shape({
all: PropTypes.array.isRequired, filter: PropTypes.string.isRequired
active: PropTypes.string // "required" but can be null
}).isRequired, }).isRequired,
responses: PropTypes.object.isRequired,
tabs: PropTypes.object.isRequired,
modals: PropTypes.array.isRequired modals: PropTypes.array.isRequired
}; };
function mapStateToProps (state) { function mapStateToProps (state) {
return { return {
actions: state.actions, actions: state.actions,
workspaces: state.workspaces,
requests: state.requests, requests: state.requests,
requestGroups: state.requestGroups, entities: state.entities,
responses: state.responses,
tabs: state.tabs,
modals: state.modals modals: state.modals
}; };
} }
@ -279,7 +152,6 @@ function mapDispatchToProps (dispatch) {
return { return {
actions: { actions: {
global: bindActionCreators(GlobalActions, dispatch), global: bindActionCreators(GlobalActions, dispatch),
tabs: bindActionCreators(TabActions, dispatch),
modals: bindActionCreators(ModalActions, dispatch), modals: bindActionCreators(ModalActions, dispatch),
requestGroups: bindActionCreators(RequestGroupActions, dispatch), requestGroups: bindActionCreators(RequestGroupActions, dispatch),
requests: bindActionCreators(RequestActions, dispatch) requests: bindActionCreators(RequestActions, dispatch)

View File

@ -3,12 +3,14 @@ import {connect} from 'react-redux'
import {bindActionCreators} from 'redux' import {bindActionCreators} from 'redux'
import * as ModalActions from '../redux/modules/modals' 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 PromptModal from '../components/base/PromptModal'
import * as db from '../database' 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 { class Prompts extends Component {
constructor (props) { constructor (props) {
@ -29,6 +31,14 @@ class Prompts extends Component {
db.update(modal.data.requestGroup, {name}) 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 () { render () {
@ -65,12 +75,6 @@ Prompts.propTypes = {
actions: PropTypes.shape({ actions: PropTypes.shape({
modals: PropTypes.shape({ modals: PropTypes.shape({
hide: PropTypes.func.isRequired hide: PropTypes.func.isRequired
}),
requestGroups: PropTypes.shape({
update: PropTypes.func.isRequired
}),
requests: PropTypes.shape({
update: PropTypes.func.isRequired
}) })
}), }),
modals: PropTypes.array.isRequired modals: PropTypes.array.isRequired
@ -86,9 +90,7 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return { return {
actions: { actions: {
requests: bindActionCreators(RequestActions, dispatch), modals: bindActionCreators(ModalActions, dispatch)
modals: bindActionCreators(ModalActions, dispatch),
requestGroups: bindActionCreators(RequestGroupActions, dispatch)
} }
} }
} }

View File

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

View File

@ -38,8 +38,6 @@ class RequestGroupActionsDropdown extends Component {
RequestGroupActionsDropdown.propTypes = { RequestGroupActionsDropdown.propTypes = {
actions: PropTypes.shape({ actions: PropTypes.shape({
update: PropTypes.func.isRequired,
remove: PropTypes.func.isRequired,
showUpdateNamePrompt: PropTypes.func.isRequired, showUpdateNamePrompt: PropTypes.func.isRequired,
showEnvironmentEditModal: 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 Dropdown from '../components/base/Dropdown'
import DropdownDivider from '../components/base/DropdownDivider' import DropdownDivider from '../components/base/DropdownDivider'
import * as RequestGroupActions from '../redux/modules/requestGroups' import * as RequestGroupActions from '../redux/modules/requestGroups'
import * as WorkspaceActions from '../redux/modules/workspaces'
import * as db from '../database' import * as db from '../database'
import importData from '../lib/import' import importData from '../lib/import'
@ -20,24 +21,45 @@ class WorkspaceDropdown extends Component {
}] }]
}; };
// 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 => { electron.remote.dialog.showOpenDialog(options, paths => {
paths.map(path => { paths.map(path => {
fs.readFile(path, 'utf8', (err, data) => { 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 () { 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 ( return (
<Dropdown right={true} {...other} className="block"> <Dropdown right={true} {...other} className="block">
<button className="btn header__content"> <button className="btn header__content">
<div className="grid grid--center"> <div className="grid grid--center">
<div className="grid__cell"> <div className="grid__cell">
<h1 className="no-pad">Insomnia</h1> <h1 className="no-pad">{workspace.name}</h1>
</div> </div>
<div className="no-wrap"> <div className="no-wrap">
{loading ? <i className="fa fa-refresh fa-spin txt-lg"></i> : ''}&nbsp; {loading ? <i className="fa fa-refresh fa-spin txt-lg"></i> : ''}&nbsp;
@ -50,45 +72,49 @@ class WorkspaceDropdown extends Component {
<DropdownDivider name="Current Workspace"/> <DropdownDivider name="Current Workspace"/>
<li> <li>
<button onClick={e => db.requestCreate()}> <button onClick={e => db.requestCreate({parentId: workspace._id})}>
<i className="fa fa-plus-circle"></i> New Request <i className="fa fa-plus-circle"></i> New Request
</button> </button>
</li> </li>
<li> <li>
<button onClick={e => db.requestGroupCreate()}> <button onClick={e => db.requestGroupCreate({parentId: workspace._id})}>
<i className="fa fa-folder"></i> New Request Group <i className="fa fa-folder"></i> New Request Group
</button> </button>
</li> </li>
<li> {/*<li>
<button onClick={e => actions.showEnvironmentEditModal()}> <button onClick={e => actions.requestGroups.showEnvironmentEditModal()}>
<i className="fa fa-code"></i> Manage Environments <i className="fa fa-code"></i> Manage Environments
</button> </button>
</li> </li>*/}
<li> <li>
<button onClick={e => this._importDialog()}> <button onClick={e => this._importDialog()}>
<i className="fa fa-share-square-o"></i> Import/Export <i className="fa fa-share-square-o"></i> Import/Export
</button> </button>
</li> </li>
<li> <li>
<button> <button onClick={e => actions.workspaces.showUpdateNamePrompt(workspace)}>
<i className="fa fa-empty"></i> Delete <strong>Sendwithus</strong> <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> </button>
</li> </li>
<DropdownDivider name="Workspaces"/> <DropdownDivider name="Workspaces"/>
<li> {allWorkspaces.map(w => {
<button> return w._id === workspace._id ? null : (
<i className="fa fa-random"></i> Switch to <strong>Sendwithus Track</strong> <li key={w._id}>
<button onClick={() => actions.workspaces.activate(w)}>
<i className="fa fa-random"></i> Switch to <strong>{w.name}</strong>
</button> </button>
</li> </li>
)
})}
<li> <li>
<button> <button onClick={e => this._workspaceCreate()}>
<i className="fa fa-random"></i> Switch to <strong>Default</strong>
</button>
</li>
<li>
<button>
<i className="fa fa-blank"></i> Create Workspace <i className="fa fa-blank"></i> Create Workspace
</button> </button>
</li> </li>
@ -105,13 +131,27 @@ class WorkspaceDropdown extends Component {
WorkspaceDropdown.propTypes = { WorkspaceDropdown.propTypes = {
loading: PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired,
workspaces: PropTypes.shape({
activeId: PropTypes.string
}),
entities: PropTypes.shape({
workspaces: PropTypes.object.isRequired
}).isRequired,
actions: PropTypes.shape({ actions: PropTypes.shape({
requestGroups: PropTypes.shape({
showEnvironmentEditModal: PropTypes.func.isRequired showEnvironmentEditModal: PropTypes.func.isRequired
}),
workspaces: PropTypes.shape({
activate: PropTypes.func.isRequired,
showUpdateNamePrompt: PropTypes.func.isRequired
})
}) })
}; };
function mapStateToProps (state) { function mapStateToProps (state) {
return { return {
workspaces: state.workspaces,
entities: state.entities,
actions: state.actions, actions: state.actions,
loading: state.global.loading loading: state.global.loading
}; };
@ -119,7 +159,10 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return { return {
actions: bindActionCreators(RequestGroupActions, dispatch) actions: {
requestGroups: bindActionCreators(RequestGroupActions, dispatch),
workspaces: bindActionCreators(WorkspaceActions, dispatch)
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,11 @@
import * as methods from '../lib/constants'; import * as methods from '../lib/constants';
import {generateId} from './util' 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 // We have to include the web version of PouchDB in app.html because
// the NodeJS version defaults to LevelDB which is hard (impossible?) // the NodeJS version defaults to LevelDB which is hard (impossible?)
// to get working in Electron apps // to get working in Electron apps
@ -10,19 +15,47 @@ let db = new PouchDB('insomnia.db', {adapter: 'websql'});
// For browser console debugging // For browser console debugging
global.db = db; 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', since: 'now',
live: true, live: true,
include_docs: true, include_docs: true,
return_docs: false return_docs: false
}).on('change', function (res) {
Object.keys(changeListeners).map(id => changeListeners[id](res))
}).on('complete', function (info) { }).on('complete', function (info) {
console.log('complete', info); console.log('complete', info);
}).on('error', function (err) { }).on('error', function (err) {
console.log('error', 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) { export function get (id) {
@ -40,6 +73,7 @@ export function update (doc, patch = {}) {
return db.put(updatedDoc).catch(e => { return db.put(updatedDoc).catch(e => {
if (e.status === 409) { if (e.status === 409) {
console.warn('Retrying document update for', updatedDoc); console.warn('Retrying document update for', updatedDoc);
get(doc._id).then(dbDoc => { get(doc._id).then(dbDoc => {
update(dbDoc, patch); 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) { 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 = {}) { function modelCreate (type, idPrefix, defaults, patch = {}) {
const model = Object.assign( const baseDefaults = {
parentId: null
};
const doc = Object.assign(
baseDefaults,
defaults, defaults,
patch, patch,
@ -70,33 +121,30 @@ function modelCreate (type, idPrefix, defaults, patch = {}) {
} }
); );
update(model); return update(doc).then(() => doc);
return model;
} }
// ~~~~~~~ // // ~~~~~~~ //
// REQUEST // // REQUEST //
// ~~~~~~~ // // ~~~~~~~ //
export function requestCreate (patch = {}) { export function requestCreate (patch = {}) {
return modelCreate('Request', 'req', { return modelCreate(TYPE_REQUEST, 'req', {
url: '', url: '',
name: 'New Request', name: 'New Request',
method: methods.METHOD_GET, method: methods.METHOD_GET,
activated: Date.now(),
body: '', body: '',
params: [], params: [],
contentType: 'text/plain', contentType: 'text/plain',
headers: [], headers: [],
authentication: {}, authentication: {}
parent: null
}, patch); }, patch);
} }
export function requestCopy (originalRequest) { export function requestCopy (request) {
const name = `${originalRequest.name} (Copy)`; const name = `${request.name} (Copy)`;
return requestCreate(Object.assign({}, originalRequest, {name})); return requestCreate(Object.assign({}, request, {name}));
} }
@ -105,22 +153,19 @@ export function requestCopy (originalRequest) {
// ~~~~~~~~~~~~~ // // ~~~~~~~~~~~~~ //
export function requestGroupCreate (patch = {}) { export function requestGroupCreate (patch = {}) {
return modelCreate('RequestGroup', 'grp', { return modelCreate(TYPE_REQUEST_GROUP, 'grp', {
collapsed: false, collapsed: false,
name: 'New Request Group', name: 'New Request Group',
environment: {}, environment: {}
parent: null
}, patch); }, patch);
} }
// ~~~~~~~~ // // ~~~~~~~~ //
// RESPONSE // // RESPONSE //
// ~~~~~~~~ // // ~~~~~~~~ //
export function responseCreate (patch = {}) { export function responseCreate (patch = {}) {
return modelCreate('Response', 'rsp', { return modelCreate(TYPE_RESPONSE, 'res', {
requestId: null,
statusCode: 0, statusCode: 0,
statusMessage: '', statusMessage: '',
contentType: 'text/plain', contentType: 'text/plain',
@ -131,32 +176,44 @@ export function responseCreate (patch = {}) {
}, 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 // // WORKSPACE //
// ~~~~~~~~~ // // ~~~~~~~~~ //
export function workspaceCreate (patch = {}) { export function workspaceCreate (patch = {}) {
return modelCreate('Workspace', 'wsp', { return modelCreate(TYPE_WORKSPACE, 'wrk', {
name: 'New Request Group', name: 'New Workspace',
activeRequestId: null,
environments: [] environments: []
}, patch); }, 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) { export function generateId (prefix) {
let id = `${prefix}/${Date.now()}/`; 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)]; id += CHARS[Math.floor(Math.random() * CHARS.length)];
} }

View File

@ -1,75 +1,31 @@
import React from 'react' import React from 'react'
import {render} from 'react-dom' import {render} from 'react-dom'
import {Provider} from 'react-redux' import {Provider} from 'react-redux'
import {bindActionCreators} from 'redux' import {Tabs} from 'react-tabs'
import createStore from './redux/create' import createStore from './redux/create'
import App from './containers/App' 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 // Global CSS
import './css/index.scss' import './css/index.scss'
import './css/lib/chrome/platform_app.css' import './css/lib/chrome/platform_app.css'
import './css/lib/fontawesome/css/font-awesome.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 export const store = createStore();
console.log('-- Init Insomnia --');
const actionFns = { console.log('-- Loading App --');
RequestGroup: bindActionCreators(RequestGroupActions, store.dispatch),
Request: bindActionCreators(RequestActions, store.dispatch),
Response: bindActionCreators(ResponseActions, store.dispatch)
};
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);
});
}
function restoreDB() {
db.allDocs().then(response => {
response.rows.map(row => refreshDoc(row.doc));
})
}
watchDB();
restoreDB();
initDB()
.then(() => initStore(store.dispatch))
.then(() => {
console.log('-- Rendering App --');
render( render(
<Provider store={store}><App /></Provider>, <Provider store={store}><App /></Provider>,
document.getElementById('root') document.getElementById('root')
); );
});

View File

@ -17,6 +17,7 @@ export const METHODS = [
METHOD_HEAD 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_GROUP_RENAME = 'requestgroup.update.name';
export const MODAL_REQUEST_RENAME = 'request.update.name';
export const MODAL_ENVIRONMENT_EDITOR = 'environment.edit'; export const MODAL_ENVIRONMENT_EDITOR = 'environment.edit';

View File

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

View File

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

View File

@ -3,27 +3,22 @@ import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger' import createLogger from 'redux-logger'
import rootReducer from './reducer' import rootReducer from './reducer'
const loggerMiddleware = createLogger({
collapsed: true
});
export default function configureStore (initialState) { export default function configureStore (initialState) {
const store = createStore( const store = createStore(
rootReducer, rootReducer,
initialState, initialState,
applyMiddleware( applyMiddleware(
thunkMiddleware, thunkMiddleware,
loggerMiddleware createLogger({collapsed: true})
) )
); );
if (module.hot) { if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('./reducer', () => { module.hot.accept('./reducer', () => {
const nextReducer = require('./reducer').default; const nextReducer = require('./reducer.js').default;
store.replaceReducer(nextReducer); 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 {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_TOGGLE = 'request-groups/toggle';
export const REQUEST_GROUP_DELETE = 'requestgroups/delete';
export const REQUEST_GROUP_TOGGLE = 'requestgroups/toggle';
// ~~~~~~~~ // // ~~~~~~~~ //
// REDUCERS // // REDUCERS //
// ~~~~~~~~ // // ~~~~~~~~ //
function allReducer (state = [], action) { // Nothing yet...
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
});
// ~~~~~~~ // // ~~~~~~~ //
// ACTIONS // // 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) { export function toggle (requestGroup) {
return {type: REQUEST_GROUP_TOGGLE, requestGroup} return {type: REQUEST_GROUP_TOGGLE, requestGroup}
} }

View File

@ -1,86 +1,35 @@
import {combineReducers} from 'redux' import * as network from '../../lib/network'
import makeRequest from '../../lib/request'
import {loadStart, loadStop} from './global' import {loadStart, loadStop} from './global'
import {show} from './modals' import {show} from './modals'
import {MODAL_REQUEST_RENAME} from '../../lib/constants' 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'; export const REQUEST_CHANGE_FILTER = 'requests/filter';
const initialState = {
filter: ''
};
// ~~~~~~~~ // // ~~~~~~~~ //
// REDUCERS // // REDUCERS //
// ~~~~~~~~ // // ~~~~~~~~ //
function allReducer (state = [], action) { export default function (state = initialState, action) {
switch (action.type) { 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) {
switch (action.type) {
case REQUEST_CHANGE_FILTER: case REQUEST_CHANGE_FILTER:
return action.filter; const filter = action.filter;
return Object.assign({}, state, {filter});
default: default:
return state; return state;
} }
} }
export default combineReducers({
all: allReducer,
filter: filterReducer,
active: activeReducer
});
// ~~~~~~~ // // ~~~~~~~ //
// ACTIONS // // 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) { export function changeFilter (filter) {
return {type: REQUEST_CHANGE_FILTER, filter}; return {type: REQUEST_CHANGE_FILTER, filter};
} }
@ -89,7 +38,7 @@ export function send (request) {
return dispatch => { return dispatch => {
dispatch(loadStart()); dispatch(loadStart());
makeRequest(request, () => { network.send(request, () => {
dispatch(loadStop()); dispatch(loadStop());
}); });
} }

View File

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

View File

@ -1,39 +1,26 @@
import {combineReducers} from 'redux' 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'; export const WORKSPACE_ACTIVATE = 'workspaces/activate';
// ~~~~~~~~ // // ~~~~~~~~ //
// REDUCERS // // REDUCERS //
// ~~~~~~~~ // // ~~~~~~~~ //
function allReducer (state = [], action) {
switch (action.type) {
default:
return state;
}
}
function activeReducer (state = null, action) { function activeReducer (state = null, action) {
switch (action.type) { switch (action.type) {
default:
return state;
} case WORKSPACE_ACTIVATE:
} return action.workspace._id;
function filterReducer (state = '', action) {
switch (action.type) {
default: default:
return state; return state;
} }
} }
export default combineReducers({ export default combineReducers({
all: allReducer, activeId: activeReducer
filter: filterReducer,
active: activeReducer
}); });
@ -41,15 +28,11 @@ export default combineReducers({
// ACTIONS // // ACTIONS //
// ~~~~~~~ // // ~~~~~~~ //
export function remove (request) { export function activate (workspace) {
return {type: WORKSPACE_DELETE, request}; return {type: WORKSPACE_ACTIVATE, workspace};
} }
export function update (request) { export function showUpdateNamePrompt (workspace) {
return {type: WORKSPACE_UPDATE, request}; 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 {combineReducers} from 'redux'
import workspacesReducer from './modules/workspaces' import workspaces from './modules/workspaces'
import requestsReducer from './modules/requests' import requestGroups from './modules/requestGroups'
import tabsReducer from './modules/tabs' import requests from './modules/requests'
import globalReducer from './modules/global' import responses from './modules/responses'
import modalsReducer from './modules/modals' import global from './modules/global'
import requestGroupsReducer from './modules/requestGroups' import modals from './modules/modals'
import responsesReducer from './modules/responses' import entities from './modules/entities'
export default combineReducers({ export default combineReducers({
workspaces: workspacesReducer, workspaces,
requestGroups: requestGroupsReducer, responses,
requests: requestsReducer, requests,
responses: responsesReducer, requestGroups,
modals: modalsReducer, modals,
global: globalReducer, global,
tabs: tabsReducer entities
}); });

View File

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

View File

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