From e9d64ebb2309432c37f5104099cf47f33bde6246 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 26 Apr 2016 00:29:24 -0700 Subject: [PATCH] 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 --- app/components/RequestBodyEditor.js | 21 +- app/components/RequestPane.js | 132 +++++++++ app/components/RequestUrlBar.js | 33 ++- app/components/ResponsePane.js | 90 ++++++ app/components/Sidebar.js | 187 +++++-------- app/components/SidebarRequestGroupRow.js | 79 ++++++ app/components/SidebarRequestRow.js | 47 ++++ app/containers/App.js | 264 +++++------------- app/containers/Prompts.js | 26 +- app/containers/RequestActionsDropdown.js | 2 +- app/containers/RequestGroupActionsDropdown.js | 2 - app/containers/WorkspaceDropdown.js | 103 +++++-- app/css/components/dropdown.scss | 3 +- app/css/components/tabs.scss | 29 +- app/css/constants/dimensions.scss | 4 + app/css/layout/base.scss | 7 +- app/database/index.js | 139 ++++++--- app/database/util.js | 2 +- app/index.js | 74 +---- app/lib/constants.js | 3 +- app/lib/import.js | 56 ++-- app/lib/{request.js => network.js} | 17 +- app/redux/create.js | 11 +- app/redux/initstore.js | 48 ++++ app/redux/modules/entities.js | 69 +++++ app/redux/modules/requestGroups.js | 44 +-- app/redux/modules/requests.js | 73 +---- app/redux/modules/responses.js | 21 +- app/redux/modules/workspaces.js | 37 +-- app/redux/reducer.js | 28 +- package.json | 1 + webpack/webpack.config.development.js | 1 - 32 files changed, 954 insertions(+), 699 deletions(-) create mode 100644 app/components/RequestPane.js create mode 100644 app/components/ResponsePane.js create mode 100644 app/components/SidebarRequestGroupRow.js create mode 100644 app/components/SidebarRequestRow.js rename app/lib/{request.js => network.js} (82%) create mode 100644 app/redux/initstore.js create mode 100644 app/redux/modules/entities.js diff --git a/app/components/RequestBodyEditor.js b/app/components/RequestBodyEditor.js index 436175103..a3e716593 100644 --- a/app/components/RequestBodyEditor.js +++ b/app/components/RequestBodyEditor.js @@ -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 ( @@ -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; diff --git a/app/components/RequestPane.js b/app/components/RequestPane.js new file mode 100644 index 000000000..444c70410 --- /dev/null +++ b/app/components/RequestPane.js @@ -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 ( +
+
+
+
+ ) + } + + return ( +
+
+
+ sendRequest(request)} + onUrlChange={updateRequestUrl} + onMethodChange={updateRequestMethod} + url={request.url} + method={request.method} + /> +
+ + + + + + +
    + {/*
  • */} +
  • +
  • +
  • +
+
+
+ + + + + + +
+ + + + +
+ +
+
+ +
+
+ + +
+ + +
+
+
+
+
+
+ ) + } +} + +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; diff --git a/app/components/RequestUrlBar.js b/app/components/RequestUrlBar.js index c21f616bc..fb07b1e4d 100644 --- a/app/components/RequestUrlBar.js +++ b/app/components/RequestUrlBar.js @@ -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 (
    - {METHODS.map((method) => ( -
  • -
  • ))}
{e.preventDefault(); sendRequest(request)}}> + onSubmit={this._handleFormSubmit.bind(this)}> -   
@@ -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; diff --git a/app/components/ResponsePane.js b/app/components/ResponsePane.js new file mode 100644 index 000000000..87cf42a31 --- /dev/null +++ b/app/components/ResponsePane.js @@ -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 ( +
+
+
+
+ ) + } + + return ( +
+
+
+ {!response ? null : ( +
+ + + +
+ )} +
+ + + + + + +
    +
  • +
  • +
  • +
+
+
+ +
+ + + + +
+
+ {!response ? null : response.headers.map((h, i) => ( +
+
{h.name}
+
{h.value}
+
+ ))} +
+
+
+
+
+
+ ) + } +} + +ResponsePane.propTypes = { + response: PropTypes.object +}; + +export default ResponsePane; diff --git a/app/components/Sidebar.js b/app/components/Sidebar.js index 61e3ebc40..4190c2afe 100644 --- a/app/components/Sidebar.js +++ b/app/components/Sidebar.js @@ -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 ( -
  • -
    -
    - -
    -
    - - -
    -
    -
      - {expanded && !filteredRequests.length ? this.renderRequestRow() : null} - {!expanded ? null : filteredRequests.map(request => this.renderRequestRow(request, requestGroup))} -
    -
  • - ); + 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 ( -
  • -
    -
    - {request ? ( - - ) : ( - - )} -
    - {request ? ( - - ) : null} -
    -
  • - ); + 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 ( + + ) + } else if (child.doc.type === 'RequestGroup') { + const requestGroup = child.doc; + const isActive = !!child.children.find(c => c.doc._id === this.props.activeRequestId); + + return ( + + {this._renderChildren(child.children, requestGroup)} + + ) + } else { + console.error('Unknown child type', child.doc.type); + return null; + } + }) } render () { - const {activeFilter, requestGroups} = this.props; + const {filter, children} = this.props; return (
    @@ -136,8 +87,7 @@ class Sidebar extends Component {
      - {this.renderRequestGroupRow(null)} - {requestGroups.map(requestGroup => this.renderRequestGroupRow(requestGroup))} + {this._renderChildren(children)}
    @@ -145,7 +95,7 @@ class Sidebar extends Component { type="text" placeholder="Filter Items" debounceMillis={300} - value={activeFilter} + value={filter} onChange={this.onFilterChange.bind(this)}/>
    @@ -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; diff --git a/app/components/SidebarRequestGroupRow.js b/app/components/SidebarRequestGroupRow.js new file mode 100644 index 000000000..b14d473e8 --- /dev/null +++ b/app/components/SidebarRequestGroupRow.js @@ -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 ( +
  • +
    +
    + +
    +
    + + +
    +
    +
      + {!expanded || children.length > 0 ? null : ( + {}} + isActive={false} + request={null} + /> + )} + {expanded ? children : null} +
    +
  • + ); + } +} + +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; diff --git a/app/components/SidebarRequestRow.js b/app/components/SidebarRequestRow.js new file mode 100644 index 000000000..20481e953 --- /dev/null +++ b/app/components/SidebarRequestRow.js @@ -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 ( +
  • +
    +
    + {request ? ( + + ) : ( + + )} +
    + {request ? ( + + ) : null} +
    +
  • + ); + } +} + +SidebarRequestRow.propTypes = { + // Functions + activateRequest: PropTypes.func.isRequired, + + // Other + isActive: PropTypes.bool.isRequired, + + // Optional + requestGroup: PropTypes.object, + request: PropTypes.object +}; + +export default SidebarRequestRow; diff --git a/app/containers/App.js b/app/containers/App.js index ec14081ab..481ff275f 100644 --- a/app/containers/App.js +++ b/app/containers/App.js @@ -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 ( -
    -
    -
    -
    - ) + _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 ( -
    -
    -
    - {db.update(activeRequest, {url})}} - onMethodChange={method => {db.update(activeRequest, {method})}} - request={activeRequest} - /> -
    - actions.tabs.select('request', i)} - selectedIndex={tabs.request || 0}> - - - - - - - - - - - - {db.update(activeRequest, {body})}} - request={activeRequest}/> - - -
    - {db.update(activeRequest, {params})}} - /> -
    -
    - -
    - {db.update(activeRequest, {authentication})}} - /> -
    -
    - -
    - {db.update(activeRequest, {headers})}} - /> -
    -
    -
    -
    -
    - ) - } - - _renderResponsePanel (actions, activeResponse, tabs) { - if (!activeResponse) { - return ( -
    -
    -
    -
    - ) - } - - return ( -
    -
    -
    - {!activeResponse ? null : ( -
    - - - -
    - )} -
    - actions.tabs.select('response', i)} - selectedIndex={tabs.response || 0}> - - - - - - - - - - - - -
    -
    - {!activeResponse ? null : activeResponse.headers.map((h, i) => ( -
    -
    {h.name}
    -
    {h.value}
    -
    - ))} -
    -
    -
    -
    -
    -
    - ) } 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 (
    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} + />
    - {this._renderRequestPanel(actions, activeRequest, tabs)} - {this._renderResponsePanel(actions, activeResponse, tabs)} + 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})} + /> +
    + + {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) diff --git a/app/containers/Prompts.js b/app/containers/Prompts.js index c5dabbd45..83ad938e2 100644 --- a/app/containers/Prompts.js +++ b/app/containers/Prompts.js @@ -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) } } } diff --git a/app/containers/RequestActionsDropdown.js b/app/containers/RequestActionsDropdown.js index 6b9a48b5d..d5bba2c80 100644 --- a/app/containers/RequestActionsDropdown.js +++ b/app/containers/RequestActionsDropdown.js @@ -17,7 +17,7 @@ class RequestActionsDropdown extends Component {
    • diff --git a/app/containers/RequestGroupActionsDropdown.js b/app/containers/RequestGroupActionsDropdown.js index 1ae002b37..a950282fd 100644 --- a/app/containers/RequestGroupActionsDropdown.js +++ b/app/containers/RequestGroupActionsDropdown.js @@ -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 }), diff --git a/app/containers/WorkspaceDropdown.js b/app/containers/WorkspaceDropdown.js index 53bfac449..ec76236d8 100644 --- a/app/containers/WorkspaceDropdown.js +++ b/app/containers/WorkspaceDropdown.js @@ -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 (
        - - - + + +
      • -
      • -
      • -
      • - -
      • + */}
      • - +
      • +
      • +
      • - - + + + {allWorkspaces.map(w => { + return w._id === workspace._id ? null : ( +
      • + +
      • + ) + })}
      • - -
      • -
      • - -
      • -
      • -
      • - +
      • @@ -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) + } } } diff --git a/app/css/components/dropdown.scss b/app/css/components/dropdown.scss index 0efabba94..3de31edd0 100644 --- a/app/css/components/dropdown.scss +++ b/app/css/components/dropdown.scss @@ -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; diff --git a/app/css/components/tabs.scss b/app/css/components/tabs.scss index 5ff98ae6c..e559f0e29 100644 --- a/app/css/components/tabs.scss +++ b/app/css/components/tabs.scss @@ -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; } } diff --git a/app/css/constants/dimensions.scss b/app/css/constants/dimensions.scss index bf01b3f3e..b0ae578f4 100644 --- a/app/css/constants/dimensions.scss +++ b/app/css/constants/dimensions.scss @@ -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; } diff --git a/app/css/layout/base.scss b/app/css/layout/base.scss index e202d427e..4413fedbc 100644 --- a/app/css/layout/base.scss +++ b/app/css/layout/base.scss @@ -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; } diff --git a/app/database/index.js b/app/database/index.js index 6cd770b81..fc2e4b8e1 100644 --- a/app/database/index.js +++ b/app/database/index.js @@ -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); +// } diff --git a/app/database/util.js b/app/database/util.js index dd6adfb84..927882231 100644 --- a/app/database/util.js +++ b/app/database/util.js @@ -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)]; } diff --git a/app/index.js b/app/index.js index 4420815f9..484af0ac2 100644 --- a/app/index.js +++ b/app/index.js @@ -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( + , + document.getElementById('root') + ); }); -} - -function restoreDB() { - db.allDocs().then(response => { - response.rows.map(row => refreshDoc(row.doc)); - }) -} - -watchDB(); -restoreDB(); - -render( - , - document.getElementById('root') -); - diff --git a/app/lib/constants.js b/app/lib/constants.js index 906cb4ca1..e2abb939f 100644 --- a/app/lib/constants.js +++ b/app/lib/constants.js @@ -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'; diff --git a/app/lib/import.js b/app/lib/import.js index 7d1f9ecff..d70496f7a 100644 --- a/app/lib/import.js +++ b/app/lib/import.js @@ -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) ); } diff --git a/app/lib/request.js b/app/lib/network.js similarity index 82% rename from app/lib/request.js rename to app/lib/network.js index ff46c9eeb..11d11e22d 100644 --- a/app/lib/request.js +++ b/app/lib/network.js @@ -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) } } diff --git a/app/redux/create.js b/app/redux/create.js index f2c74bc14..264c6c81e 100644 --- a/app/redux/create.js +++ b/app/redux/create.js @@ -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; } diff --git a/app/redux/initstore.js b/app/redux/initstore.js new file mode 100644 index 000000000..0ad9fc654 --- /dev/null +++ b/app/redux/initstore.js @@ -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)); + }); +} diff --git a/app/redux/modules/entities.js b/app/redux/modules/entities.js new file mode 100644 index 000000000..de5b5e830 --- /dev/null +++ b/app/redux/modules/entities.js @@ -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); +} diff --git a/app/redux/modules/requestGroups.js b/app/redux/modules/requestGroups.js index e78920778..e9c0338e6 100644 --- a/app/redux/modules/requestGroups.js +++ b/app/redux/modules/requestGroups.js @@ -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} } diff --git a/app/redux/modules/requests.js b/app/redux/modules/requests.js index a7b18f2a1..9d720f01c 100644 --- a/app/redux/modules/requests.js +++ b/app/redux/modules/requests.js @@ -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()); }); } diff --git a/app/redux/modules/responses.js b/app/redux/modules/responses.js index 4bfe8a2e2..f3375a892 100644 --- a/app/redux/modules/responses.js +++ b/app/redux/modules/responses.js @@ -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... diff --git a/app/redux/modules/workspaces.js b/app/redux/modules/workspaces.js index 0574331c9..c451b2a9a 100644 --- a/app/redux/modules/workspaces.js +++ b/app/redux/modules/workspaces.js @@ -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}; -} - diff --git a/app/redux/reducer.js b/app/redux/reducer.js index 99e495648..49a1d2782 100644 --- a/app/redux/reducer.js +++ b/app/redux/reducer.js @@ -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 }); diff --git a/package.json b/package.json index fe69425e3..548246a96 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/webpack/webpack.config.development.js b/webpack/webpack.config.development.js index 3f6c5ac50..d18a62220 100644 --- a/webpack/webpack.config.development.js +++ b/webpack/webpack.config.development.js @@ -1,4 +1,3 @@ -import path from 'path' import webpack from 'webpack' import baseConfig from './webpack.config.base'