This commit is contained in:
Gregory Schier 2016-04-04 00:15:30 -07:00
parent c39b1824c1
commit b4c48fb411
32 changed files with 505 additions and 342 deletions

View File

@ -27,14 +27,16 @@ describe('Requests Actions', () => {
type: types.REQUEST_ADD,
request: {
id: 'rq_1000000000000',
_mode: 'json',
created: 1000000000000,
modified: 1000000000000,
name: 'Test Request',
name: 'My Request',
method: 'GET',
url: '',
body: '',
headers: [],
headers: [{
name: 'Content-Type',
value: 'application/json'
}],
params: [],
authentication: {}
}
@ -43,7 +45,7 @@ describe('Requests Actions', () => {
];
const store = mockStore();
store.dispatch(addRequest('Test Request'));
store.dispatch(addRequest());
jest.runAllTimers();
const actions = store.getActions();

View File

@ -4,11 +4,13 @@ import {loadStart} from "./global";
import {loadStop} from "./global";
const defaultRequestGroup = {
id: null,
created: 0,
modified: 0,
name: '',
environment: {}
id: null,
created: 0,
modified: 0,
collapsed: false,
name: '',
environment: {},
children: []
};
/**
@ -43,6 +45,20 @@ export function addRequestGroup (name = 'My Group') {
};
}
export function addChildRequest (id, requestId) {
return (dispatch) => {
dispatch(loadStart());
dispatch({type: types.REQUEST_GROUP_ADD_CHILD_REQUEST, id, requestId});
return new Promise((resolve) => {
setTimeout(() => {
dispatch(loadStop());
resolve();
}, 500);
});
};
}
export function updateRequestGroup (requestGroupPatch) {
if (!requestGroupPatch.id) {
throw new Error('Cannot update RequestGroup without id');

View File

@ -5,7 +5,6 @@ import {loadStop} from "./global";
const defaultRequest = {
id: null,
_mode: 'json',
created: 0,
modified: 0,
url: '',
@ -13,15 +12,13 @@ const defaultRequest = {
method: methods.METHOD_GET,
body: '',
params: [],
headers: [],
headers: [{
name: 'Content-Type',
value: 'application/json'
}],
authentication: {}
};
/**
* Build a new request from a subset of fields
* @param request values to override defaults with
* @returns {*}
*/
function buildRequest (request) {
// Build the required fields
const id = request.id || `rq_${Date.now()}`;
@ -34,12 +31,18 @@ function buildRequest (request) {
});
}
export function addRequest (name = 'My Request') {
export function addRequest (requestGroupId = null) {
return (dispatch) => {
dispatch(loadStart());
const request = buildRequest({name});
const request = buildRequest({name: 'My Request'});
dispatch({type: types.REQUEST_ADD, request});
if (requestGroupId) {
const id = requestGroupId;
const requestId = request.id;
dispatch({type: types.REQUEST_GROUP_ADD_CHILD_REQUEST, requestId, id});
}
return new Promise((resolve) => {
setTimeout(() => {
dispatch(loadStop());
@ -103,3 +106,7 @@ export function activateRequest (id) {
export function changeFilter (filter) {
return {type: types.REQUEST_CHANGE_FILTER, filter};
}
export function sendRequest (id) {
}

View File

@ -0,0 +1,27 @@
import React, {Component, PropTypes} from 'react'
import * as constants from '../constants/global';
class MethodTag extends Component {
render () {
const {method} = this.props;
let methodName;
if (method === constants.METHOD_DELETE || method === constants.METHOD_OPTIONS) {
methodName = method.slice(0, 3);
} else {
methodName = method.slice(0, 4);
}
return (
<div className={'tag tag--small method-' + method}>
{methodName}
</div>
);
}
}
MethodTag.propTypes = {
method: PropTypes.string.isRequired
};
export default MethodTag;

View File

@ -8,13 +8,16 @@ class RequestBodyEditor extends Component {
render () {
const {request, onChange, className} = this.props;
const contentTypeHeader = request.headers.find(h => h.name.toLowerCase() === 'content-type');
const mode = contentTypeHeader ? contentTypeHeader.value : 'text/plain';
return (
<CodeEditor
value={request.body}
className={className}
onChange={onChange}
options={{
mode: request._mode,
mode: mode,
placeholder: 'request body here...'
}}
/>
@ -24,8 +27,7 @@ class RequestBodyEditor extends Component {
RequestBodyEditor.propTypes = {
request: PropTypes.shape({
body: PropTypes.string.isRequired,
_mode: PropTypes.string.isRequired
body: PropTypes.string.isRequired
}).isRequired,
onChange: PropTypes.func.isRequired
};

View File

@ -12,10 +12,10 @@ class UrlInput extends Component {
return (
<div className="grid form-control form-control--left form-control--right">
<Dropdown>
<button className="btn txt-lg">
<button className="btn txt-md">
{request.method}&nbsp;&nbsp;<i className="fa fa-caret-down"></i>
</button>
<ul className="bg-super-light">
<ul>
{METHODS.map((method) => (
<li key={method}>
<button onClick={onMethodChange.bind(null, method)}>
@ -26,12 +26,12 @@ class UrlInput extends Component {
</ul>
</Dropdown>
<Input type="text"
className="txt-lg"
className="txt-md"
placeholder="http://echo.insomnia.rest/status/200"
initialValue={request.url}
onChange={onUrlChange}/>
<button className="btn">
<i className="fa fa-repeat txt-xl"></i>
<i className="fa fa-repeat txt-lg"></i>
</button>
</div>
)

View File

@ -1,7 +1,9 @@
import React, {Component, PropTypes} from 'react'
import Dropdown from './base/Dropdown'
import WorkspaceDropdown from './dropdowns/WorkspaceDropdown'
import DebouncingInput from './base/DebouncingInput'
import RequestActionsDropdown from './dropdowns/RequestActionsDropdown'
import MethodTag from './MethodTag'
import Dropdown from './base/Dropdown'
class Sidebar extends Component {
onFilterChange (value) {
@ -9,80 +11,97 @@ class Sidebar extends Component {
}
renderRequestGroupRow (requestGroup) {
const {activeFilter, addRequest, requests} = this.props;
const {activeFilter, activeRequest, addRequest, toggleRequestGroup, requests, requestGroups} = this.props;
const filteredRequests = requests.filter(
r => !requestGroup || requestGroup.requests.find(r.id)
).filter(
r => r.name.toLowerCase().indexOf(activeFilter.toLowerCase()) >= 0
let filteredRequests = requests.filter(
r => r.name.toLowerCase().indexOf(activeFilter.toLowerCase()) != -1
);
if (requestGroup) {
if (!requestGroup) {
// Grab all requests that are not children of request groups
filteredRequests = filteredRequests.filter(r => {
return !requestGroups.find(rg => {
return rg.children.find(c => c.id === r.id)
})
});
return filteredRequests.map(request => this.renderRequestRow(request));
} else {
// Grab all of the children for this request group
filteredRequests = filteredRequests.filter(
r => requestGroup.children.find(c => c.id === r.id)
);
const isActive = activeRequest && filteredRequests.find(r => r.id == activeRequest.id);
let folderIconClass = 'fa-folder';
folderIconClass += requestGroup.collapsed ? '' : '-open';
folderIconClass += isActive ? '' : '-o';
return (
<li>
<div className="grid">
<button className="sidebar__item col text-left">
<i className="fa fa-folder-open-o"></i>&nbsp;&nbsp;&nbsp;Request Group
</button>
<button className="sidebar__item-btn" onClick={(e) => addRequest()}>
<i className="fa fa-plus-circle"></i>
</button>
<li key={requestGroup.id}>
<div
className={'sidebar__item sidebar__item--bordered ' + (isActive ? 'sidebar__item--active' : '')}>
<div className="sidebar__item__row">
<button onClick={e => toggleRequestGroup(requestGroup.id)}>
<i className={'fa ' + folderIconClass}></i>
&nbsp;&nbsp;&nbsp;{requestGroup.name}
</button>
</div>
<div className="sidebar__item__btn">
<button onClick={(e) => addRequest(requestGroup.id)}>
<i className="fa fa-plus-circle"></i>
</button>
<Dropdown right={true} className="tall">
<button>
<i className="fa fa-caret-down"></i>
</button>
<ul>
<li><button>Hello</button></li>
<li><button>Hello</button></li>
<li><button>Hello</button></li>
</ul>
</Dropdown>
</div>
</div>
<ul>
{filteredRequests.map(request => this.renderRequestRow(request))}
{requestGroup.collapsed ? null : filteredRequests.map(request => this.renderRequestRow(request))}
</ul>
</li>
)
} else {
return (
filteredRequests.map(request => this.renderRequestRow(request))
)
}
}
renderRequestRow (request) {
const {activeRequest, activateRequest} = this.props;
const isActive = activeRequest && request.id === activeRequest.id;
const className = ['grid'].concat(isActive ? ['active'] : '');
return (
<li key={request.id} className={className.join(' ')}>
<div className="grid">
<button className='sidebar__item col' onClick={() => {activateRequest(request.id)}}>
{request.name}
</button>
<RequestActionsDropdown className="sidebar__item-btn" right={true} request={request}/>
<li key={request.id}>
<div className={'sidebar__item ' + (isActive ? 'sidebar__item--active' : '')}>
<div className="sidebar__item__row">
<button onClick={() => {activateRequest(request.id)}}>
<MethodTag method={request.method}/> {request.name}
</button>
</div>
<RequestActionsDropdown
className="sidebar__item__btn"
right={true}
request={request}
/>
</div>
</li>
);
}
render () {
const {activeFilter, loading, addRequest, requestGroups} = this.props;
const {activeFilter, requestGroups} = this.props;
return (
<aside className="sidebar pane">
<div className="grid-v">
<header className="pane__header bg-primary">
<h1>
<Dropdown right={true}>
<button className="pane__header__content">
<i className="fa fa-angle-down pull-right"></i>
{loading ? <i className="fa fa-refresh fa-spin pull-right"></i> : ''}
Insomnia
</button>
<ul className="bg-super-light">
<li>
<button onClick={(e) => addRequest()}>
<i className="fa fa-plus-circle"></i> Add Request
</button>
</li>
<li><button><i className="fa fa-share-square-o"></i> Import/Export</button></li>
<li><button><i className="fa fa-empty"></i> Toggle Sidebar</button></li>
<li><button><i className="fa fa-empty"></i> Delete Workspace</button></li>
</ul>
</Dropdown>
</h1>
<header className="pane__header bg-dark">
<h1><WorkspaceDropdown /></h1>
</header>
<div className="pane__body hide-scrollbars bg-dark">
<div className="stock-height form-control form-control--outlined col">
@ -106,13 +125,12 @@ class Sidebar extends Component {
Sidebar.propTypes = {
activateRequest: PropTypes.func.isRequired,
addRequest: 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,
loading: PropTypes.bool.isRequired
activeRequest: PropTypes.object
};
export default Sidebar;

View File

@ -31,7 +31,7 @@ class Dropdown extends Component {
}
render () {
const classes = ['dropdown'];
const classes = ['dropdown'].concat(this.props.className || []);
this.state.open && classes.push('dropdown--open');
this.props.right && classes.push('dropdown--right');

View File

@ -6,15 +6,18 @@ import * as RequestActions from '../../actions/requests'
class RequestActionsDropdown extends Component {
render () {
const {actions, request, ...other} = this.props;
const {actions, request, buttonClassName, ...other} = this.props;
return (
<Dropdown right={this.props.right || false} {...other}>
<button className="pane__header__content">
<i className="fa fa-angle-down pull-right"></i>
<Dropdown {...other}>
<button className={buttonClassName}>
<i className="fa fa-gear"></i>
</button>
<ul className="bg-super-light">
<li><button onClick={actions.deleteRequest.bind(null, request.id)}>Delete</button></li>
<ul>
<li><button>Duplicate</button></li>
<li><button>Rename</button></li>
<li><button>Export</button></li>
<li><button onClick={e => actions.deleteRequest(request.id)}>Delete</button></li>
</ul>
</Dropdown>
)
@ -25,7 +28,8 @@ RequestActionsDropdown.propTypes = {
request: PropTypes.object.isRequired,
actions: PropTypes.shape({
deleteRequest: PropTypes.func.isRequired
})
}),
buttonClassName: PropTypes.string
};
function mapStateToProps (state) {

View File

@ -0,0 +1,69 @@
import React, {Component, PropTypes} from 'react'
import {bindActionCreators} from 'redux'
import {connect} from 'react-redux'
import Dropdown from '../base/Dropdown'
import * as RequestActions from '../../actions/requests'
import * as RequestGroupActions from '../../actions/requestGroups'
class WorkspaceDropdown extends Component {
render () {
const {actions, loading, ...other} = this.props;
return (
<Dropdown right={true} {...other} className="block">
<button className="pane__header__content">
<div className="grid">
<div className="col">
Insomnia
</div>
<div className="no-wrap">
{loading ? <i className="fa fa-refresh fa-spin txt-lg"></i> : ''}&nbsp;
<i className="fa fa-caret-down txt-lg"></i>
</div>
</div>
</button>
<ul>
<li><button onClick={e => actions.addRequest()}>
<i className="fa fa-plus-circle"></i> Add Request
</button></li>
<li><button onClick={e => actions.addRequestGroup()}>
<i className="fa fa-plus-circle"></i> Add Request Group
</button></li>
<li><button><i className="fa fa-share-square-o"></i> Import/Export</button></li>
<li><button><i className="fa fa-empty"></i> Toggle Sidebar</button></li>
<li><button><i className="fa fa-empty"></i> Delete Workspace</button></li>
</ul>
</Dropdown>
)
}
}
WorkspaceDropdown.propTypes = {
loading: PropTypes.bool.isRequired,
actions: PropTypes.shape({
addRequest: PropTypes.func.isRequired,
addRequestGroup: PropTypes.func.isRequired
})
};
function mapStateToProps (state) {
return {
actions: state.actions,
loading: state.loading
};
}
function mapDispatchToProps (dispatch) {
return {
actions: Object.assign(
{},
bindActionCreators(RequestActions, dispatch),
bindActionCreators(RequestGroupActions, dispatch)
)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(WorkspaceDropdown);

View File

@ -16,3 +16,4 @@ export const REQUEST_GROUP_ADD = 'REQUEST_GROUP.ADD';
export const REQUEST_GROUP_UPDATE = 'REQUEST_GROUP.UPDATE';
export const REQUEST_GROUP_DELETE = 'REQUEST_GROUP.DELETE';
export const REQUEST_GROUP_TOGGLE = 'REQUEST_GROUP.TOGGLE';
export const REQUEST_GROUP_ADD_CHILD_REQUEST = 'REQUEST_GROUP.ADD_CHILD_REQUEST';

View File

@ -9,6 +9,7 @@ export const METHOD_POST = 'POST';
export const METHOD_DELETE = 'DELETE';
export const METHOD_OPTIONS = 'OPTIONS';
export const METHOD_HEAD = 'HEAD';
export const METHODS = [
METHOD_GET,
METHOD_PUT,
@ -17,3 +18,4 @@ export const METHODS = [
METHOD_OPTIONS,
METHOD_HEAD
];

View File

@ -9,6 +9,7 @@ import RequestUrlBar from '../components/RequestUrlBar'
import Sidebar from '../components/Sidebar'
import * as RequestActions from '../actions/requests'
import * as RequestGroupActions from '../actions/requestGroups'
import * as GlobalActions from '../actions/global'
// Don't inject component styles (use our own)
@ -27,9 +28,9 @@ class App extends Component {
return (
<div className="grid grid-collapse">
<section id="request-pane" className="pane col">
<section id="request-pane" className="pane col tall">
<div className="grid-v">
<header className="pane__header bg-super-light">
<header className="pane__header bg-super-dark">
<RequestUrlBar
onUrlChange={updateRequestUrl}
onMethodChange={updateRequestMethod}
@ -37,18 +38,17 @@ class App extends Component {
</header>
<div className="pane__body grid-v">
<Tabs selectedIndex={0} className="grid-v">
<TabList className="grid">
<Tab><button className="btn">Body</button></Tab>
<Tab><button className="btn">Params</button></Tab>
<Tab><button className="btn">Auth</button></Tab>
<Tab><button className="btn">Headers</button></Tab>
<TabList className="grid txt-sm">
<Tab><button className="btn btn--compact">Body</button></Tab>
<Tab><button className="btn btn--compact">Params</button></Tab>
<Tab><button className="btn btn--compact">Auth</button></Tab>
<Tab><button className="btn btn--compact">Headers</button></Tab>
</TabList>
<TabPanel className="grid-v">
<RequestBodyEditor
className="grid-v"
onChange={updateRequestBody}
request={activeRequest}
options={{mode: activeRequest._mode}}/>
request={activeRequest}/>
</TabPanel>
<TabPanel className="grid-v pad">Params</TabPanel>
<TabPanel className="grid-v pad">Basic Auth</TabPanel>
@ -57,9 +57,9 @@ class App extends Component {
</div>
</div>
</section>
<section id="response-pane" className="pane col">
<section id="response-pane" className="pane col tall">
<div className="grid-v">
<header className="pane__header text-center bg-light">
<header className="pane__header text-center bg-super-dark">
<div className="grid">
<div className="tag success"><strong>200</strong>&nbsp;SUCCESS</div>
<div className="tag">TIME&nbsp;<strong>143ms</strong></div>
@ -67,11 +67,11 @@ class App extends Component {
</header>
<div className="pane__body grid-v">
<Tabs selectedIndex={0} className="grid-v">
<TabList className="grid">
<Tab><button className="btn">Response</button></Tab>
<Tab><button className="btn">Raw</button></Tab>
<Tab><button className="btn">Headers</button></Tab>
<Tab><button className="btn">Cookies</button></Tab>
<TabList className="grid txt-sm">
<Tab><button className="btn btn--compact">Response</button></Tab>
<Tab><button className="btn btn--compact">Raw</button></Tab>
<Tab><button className="btn btn--compact">Headers</button></Tab>
<Tab><button className="btn btn--compact">Cookies</button></Tab>
</TabList>
<TabPanel className="grid-v">
<Editor
@ -111,12 +111,13 @@ class App extends Component {
activateRequest={actions.activateRequest}
changeFilter={actions.changeFilter}
addRequest={actions.addRequest}
toggleRequestGroup={actions.toggleRequestGroup}
activeRequest={activeRequest}
activeFilter={requests.filter}
loading={loading}
requestGroups={requestGroups.all}
requests={requests.all}/>
<div className="col">
<div className="col tall">
{this.renderPageBody(actions, activeRequest)}
</div>
</div>
@ -130,15 +131,15 @@ App.propTypes = {
updateRequestBody: PropTypes.func.isRequired,
updateRequestUrl: PropTypes.func.isRequired,
changeFilter: PropTypes.func.isRequired,
updateRequestMethod: PropTypes.func.isRequired
updateRequestMethod: PropTypes.func.isRequired,
toggleRequestGroup: PropTypes.func.isRequired
}).isRequired,
requests: PropTypes.shape({
all: PropTypes.array.isRequired,
active: PropTypes.string // "required" but can be null
}).isRequired,
requestGroups: PropTypes.shape({
all: PropTypes.array.isRequired,
collapsed: PropTypes.array.isRequired
all: PropTypes.array.isRequired
}).isRequired,
loading: PropTypes.bool.isRequired
};
@ -157,7 +158,8 @@ function mapDispatchToProps (dispatch) {
actions: Object.assign(
{},
bindActionCreators(GlobalActions, dispatch),
bindActionCreators(RequestActions, dispatch)
bindActionCreators(RequestActions, dispatch),
bindActionCreators(RequestGroupActions, dispatch)
)
}
}

View File

@ -3,8 +3,7 @@
.dropdown {
position: relative;
display: block;
height: 100%;
display: inline-block;
&.dropdown--open ul {
display: block;
@ -21,29 +20,44 @@
display: none;
z-index: 10;
width: 180px;
border-radius: $radius-md;
padding: 2px 0;
margin-top: 4px;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.15);
padding-top: 4px;
a, button {
font-size: $font-size-md;
text-align: left;
padding: 10px $padding-md;
width: 100%;
display: block;
li {
background: $bg-super-light;
&:hover {
background: $hl-xs;
&:first-child {
border-top-left-radius: $radius-md;
border-top-right-radius: $radius-md;
padding-top: 2px;
}
&:active {
background: $hl-md;
&:last-child {
border-bottom-left-radius: $radius-md;
border-bottom-right-radius: $radius-md;
padding-bottom: 2px;
}
i {
display: inline-block;
width: 1.5em;
& > button {
font-size: $font-size-md;
text-align: left;
padding: 10px $padding-md;
width: 100%;
display: block;
color: $font-light-bg !important;
&:hover {
background: $hl-sm;
}
&:active {
background: $hl-md;
}
i.fa {
display: inline-block;
width: 1.5em;
}
}
}
}

View File

@ -10,6 +10,7 @@
width: 100%;
font-family: "Source Code Pro", monospace;
box-sizing: border-box;
font-size: 11px;
}
.CodeMirror,
@ -43,7 +44,7 @@
}
.CodeMirror-placeholder {
color: $hl-xl;
color: $hl-xxl;
}
&.editor--readonly .CodeMirror-cursors {

View File

@ -34,14 +34,13 @@
}
.btn {
cursor: pointer;
text-align: center;
padding: 0 ($padding-md * 1.5);
height: $line-height-md;
&.btn--compact {
padding: 0 ($padding-md);
height: $line-height-md / 2;
padding: 0 $padding-md;
height: $line-height-sm;
}
}
@ -71,4 +70,6 @@ input, button {
button {
width: auto;
cursor: pointer;
padding: 0;
margin: 0;
}

View File

@ -15,7 +15,6 @@
display: block;
flex: 1 1 auto;
width: 100%;
height: 100%;
overflow: hidden;
box-sizing: border-box;
}

View File

@ -2,5 +2,4 @@
a {
text-decoration: none;
color: darken($bg-primary, 10%);
}

View File

@ -8,7 +8,7 @@
}
.pane__header {
border-color: transparent;
//border-color: transparent;
height: $line-height-md;
line-height: $line-height-md;

View File

@ -3,7 +3,7 @@
.ReactTabs {
.ReactTabs__TabList {
height: $line-height-md;
height: $line-height-sm;
flex-shrink: 0;
}

View File

@ -7,8 +7,16 @@
margin-right: 1em;
line-height: 1em;
box-sizing: border-box;
border-radius: $radius-md;
font-size: $font-size-sm;
&:last-child {
margin-right: 0;
}
&.tag--small {
padding: $padding-xxs;
font-size: $font-size-xs;
border-radius: $radius-sm;
}
}

View File

@ -1,31 +1,71 @@
/* Background Colors */
$bg-primary: #7D70CC;
//$bg-primary: #7D70CC;
$bg-light: #efefef;
$bg-super-light: #fff;
$bg-dark: #272824;
$bg-super-dark: #222320;
// For light theme
//$bg-super-dark: #efefef;
//$bg-dark: #fff;
//$bg-light: #272824;
//$bg-super-light: #22231f;
/* Font Colors */
$font-light-bg: #555;
$font-dark-bg: #ddd;
$font-super-dark-bg: #ccc;
$font-primary-bg: #eee;
// For light theme
//$font-dark-bg: #444;
//$font-light-bg: #ddd;
//$font-super-dark-bg: #333;
//$font-primary-bg: #eee;
$hl-xxs: rgba(140, 140, 140, 0.05);
$hl-xs: rgba(140, 140, 140, 0.09);
$hl-sm: rgba(140, 140, 140, 0.15);
$hl-md: rgba(140, 140, 140, 0.25);
$hl-lg: rgba(140, 140, 140, 0.35);
$hl-xl: rgba(140, 140, 140, 0.5);
$hl: rgba(130, 130, 130, 1);
$hl-xxl: rgba(140, 140, 140, 0.7);
$hl: rgba(140, 140, 140, 1);
$success: #598f07;
$success: #91c74e;
$warning: #ffad36;
$danger: #ff6d59;
$info: #52adc1;
$faint-opacity: 0.5;
$faint-opacity: 0.4;
.success {
.success,
.method-POST {
border-color: $success !important;
color: $success;
}
.warning,
.method-PUT,
.method-PATCH {
border-color: $warning !important;
color: $warning;
}
.danger,
.method-DELETE {
border-color: $danger !important;
color: $danger;
}
.info,
.method-GET,
.method-OPTIONS,
.method-HEAD {
border-color: $info !important;
color: $info;
}
.bg-dark {
background: $bg-dark;
&, a, textarea, input, button {
@ -33,14 +73,21 @@ $faint-opacity: 0.5;
}
}
.bg-primary {
background: $bg-primary;
.bg-super-dark {
background: $bg-super-dark;
&, a, textarea, input, button {
color: $font-primary-bg;
color: $font-super-dark-bg;
}
}
//.bg-primary {
// background: $bg-primary;
//
// &, a, textarea, input, button {
// color: $font-primary-bg;
// }
//}
.bg-light {
background: $bg-light;

View File

@ -1,10 +1,13 @@
/* Padding */
$padding-md: 1.25rem;
$padding-sm: $padding-md / 2;
$padding-sm: $padding-md * 0.5;
$padding-xs: $padding-md * 0.35;
$padding-xxs: $padding-md * 0.20;
$padding-lg: $padding-md * 2;
/* Fonts */
$font-size: 12px;
$font-size-xs: 0.7rem;
$font-size-sm: 0.8rem;
$font-size-md: 1.0rem;
$font-size-lg: 1.2rem;
@ -17,10 +20,10 @@ $line-height-sm: $line-height-md * 0.75;
/* Sidebar */
$sidebar-width: 20rem;
$sidebar-width-sm: $sidebar-width * 0.75;
/* Borders */
$radius-md: 2px;
$radius-sm: 1px;
$radius-md: 3px;
/* Breakpoints */
$breakpoint-lg: 1500px;

View File

@ -32,20 +32,38 @@ h2 {
text-align: left !important;
}
.pull-right {
float: right;
}
.tall {
height: 100%;
display: block;
box-sizing: border-box;
}
/* Make all font awesome icons the same width */
i.fa {
width: 1.1em;
text-align: center;
}
.faint {
opacity: $faint-opacity;
}
.no-wrap {
white-space: nowrap;
}
.wide {
width: 100%;
}
.block {
display: block !important;
}
.inline-block {
display: inline-block !important;
}
.hide-scrollbars {
&::-webkit-scrollbar {
display: none;

View File

@ -7,58 +7,86 @@
.pane__body {
overflow-y: auto;
width: $sidebar-width;
height: 100%;
}
.pane__header, .pane__body {
.pane__header,
.pane__body {
border-left: 0;
}
ul {
li.active .sidebar__item {
color: inherit;
.sidebar__item__row {
width: 100%;
text-align: left;
box-sizing: border-box;
height: $line-height-sm;
line-height: $line-height-sm;
color: inherit;
&:hover {
background: $hl-xxs;
}
.sidebar__item, .sidebar__item-btn {
height: $line-height-sm;
line-height: $line-height-sm;
& > button {
padding: 0 $padding-md;
color: inherit;
height: 100%;
width: 100%;
}
}
.sidebar__item__btn {
display: none;
position: absolute;
right: 0;
top: 0;
bottom: 0;
color: $hl-xl;
& > button,
& > .dropdown > button {
padding: 0 $padding-sm;
height: 100%;
color: inherit;
&:hover {
background: $hl-xs;
color: $font-dark-bg;
}
&:active {
background: $hl-md;
}
}
& .sidebar__item {
display: flex;
align-items: center;
color: $hl;
box-sizing: border-box;
width: 100%;
text-align: left;
&.sidebar__item--right-menu {
padding-right: 0;
}
}
& .sidebar__item .sidebar__item {
padding-left: $padding-sm * 5;
}
& .sidebar__item .sidebar__item .sidebar__item {
padding-left: $padding-md * 3;
}
}
}
@media (max-width: $breakpoint-sm) {
.sidebar {
.pane__body {
overflow-y: auto;
width: $sidebar-width-sm;
.sidebar__item {
color: $hl;
box-sizing: border-box;
width: 100%;
position: relative;
&.sidebar__item--active {
color: $font-dark-bg;
.tag {
opacity: 1;
}
}
&.sidebar__item--right-menu {
padding-right: 0;
}
&:hover .sidebar__item__btn {
display: block;
}
&.sidebar__item--bordered {
border-top: 1px solid $hl-xs;
}
.tag {
opacity: $faint-opacity;
}
}
ul ul .sidebar__item__row > button {
padding-left: $padding-sm * 3 !important;
}
}

View File

@ -1,31 +1,25 @@
jest.unmock('../requests');
jest.unmock('../requestGroups');
jest.unmock('../../constants/actionTypes');
import reducer from '../requests';
import * as types from '../../constants/actionTypes';
import reducer from '../requestGroups';
describe('Requests Reducer', () => {
describe('RequestGroups Reducer', () => {
var initialState;
var request;
var requestGroup;
beforeEach(() => {
initialState = {
all: [],
active: null,
filter: ''
collapsed: []
};
request = {
_mode: 'json',
id: 'req_1234567890',
requestGroup = {
id: 'rg_1234567890',
created: Date.now(),
modified: Date.now(),
name: 'My Request',
method: 'GET',
body: '{"foo": "bar"}',
authentication: {username: 'user', password: 'secret'},
params: [{name: 'page', value: '3'}],
headers: [{name: 'Content-Type', value: 'application/json'}]
name: 'My Group',
environment: {},
children: []
};
});
@ -34,126 +28,4 @@ describe('Requests Reducer', () => {
reducer(undefined, {})
).toEqual(initialState);
});
it('returns initial same state with unknown action', () => {
const state = {foo: 'bar'};
expect(
reducer(state, {type: '__INVALID__'})
).toBe(state);
});
it('should add request', () => {
expect(
reducer(undefined, {
type: types.REQUEST_ADD,
request: request
})
).toEqual({
all: [request],
active: request.id,
filter: ''
});
});
it('should add request of same name', () => {
initialState.all.push(request);
const newRequest = Object.assign({}, request);
expect(
reducer(initialState, {
type: types.REQUEST_ADD,
request: newRequest
})
).toEqual({
all: [
Object.assign(newRequest, {name: `${request.name} (1)`}),
request
],
active: request.id,
filter: ''
});
});
it('should delete request', () => {
initialState.all.push(request);
initialState.active = request.id;
expect(
reducer(initialState, {
type: types.REQUEST_DELETE,
id: request.id
})
).toEqual({
all: [],
active: null,
filter: ''
});
});
it('should delete request and not remove active', () => {
initialState.all.push(request);
initialState.active = 'req_9999999999';
expect(
reducer(initialState, {
type: types.REQUEST_DELETE,
id: request.id
})
).toEqual({
all: [],
active: 'req_9999999999',
filter: ''
});
});
it('should update request', () => {
const state = reducer(undefined, {
type: types.REQUEST_ADD,
request: request
});
const patch = {
id: request.id,
name: 'New Name'
};
expect(reducer(state, {
type: types.REQUEST_UPDATE,
patch: patch
})).toEqual({
all: [Object.assign({}, request, patch)],
active: request.id,
filter: ''
});
});
it('should not update unknown request', () => {
expect(reducer(initialState, {
type: types.REQUEST_UPDATE,
patch: {id: 'req_1234567890123'}
})).toEqual(initialState);
});
it('should activate request', () => {
initialState.all = [request];
initialState.active = null;
expect(reducer(initialState, {
type: types.REQUEST_ACTIVATE,
id: request.id
})).toEqual({
all: [request],
active: request.id,
filter: ''
});
});
it('should not activate invalid request', () => {
initialState.all = [request];
initialState.active = null;
expect(reducer(initialState, {
type: types.REQUEST_ACTIVATE
})).toEqual(initialState);
});
});

View File

@ -16,7 +16,6 @@ describe('Requests Reducer', () => {
};
request = {
_mode: 'json',
id: 'req_1234567890',
created: Date.now(),
modified: Date.now(),

View File

@ -1,12 +1,12 @@
import * as types from "../constants/actionTypes";
const initialState = {
all: [],
collapsed: []
all: []
};
function requestGroupsReducer (state = [], action) {
switch (action.type) {
case types.REQUEST_GROUP_ADD:
// Change name if there is a duplicate
const requestGroup = action.requestGroup;
@ -18,14 +18,37 @@ function requestGroupsReducer (state = [], action) {
}
}
return [requestGroup, ...state];
case types.REQUEST_GROUP_UPDATE:
return state.map(requestGroup => {
if (requestGroup.id === action.patch.id) {
return Object.assign({}, requestGroup, action.patch);
return state.map(rg => {
if (rg.id === action.patch.id) {
return Object.assign({}, rg, action.patch);
} else {
return requestGroup;
return rg;
}
});
case types.REQUEST_GROUP_TOGGLE:
return state.map(rg => {
if (rg.id === action.id) {
const collapsed = !rg.collapsed;
return Object.assign({}, rg, {collapsed});
} else {
return rg;
}
});
case types.REQUEST_GROUP_ADD_CHILD_REQUEST:
return state.map(rg => {
if (rg.id === action.id) {
rg.children = [
{type: 'Request', id: action.requestId},
...rg.children
];
}
return rg;
});
default:
return state;
}
@ -34,23 +57,28 @@ function requestGroupsReducer (state = [], action) {
export default function (state = initialState, action) {
let all, collapsed;
switch (action.type) {
case types.REQUEST_GROUP_ADD:
all = requestGroupsReducer(state.all, action);
return Object.assign({}, state, {all});
case types.REQUEST_GROUP_DELETE:
// TODO: Remove from collapsed as well
all = state.all.filter(rg => rg.id !== action.id);
return Object.assign({}, state, {all});
case types.REQUEST_GROUP_ADD_CHILD_REQUEST:
all = requestGroupsReducer(state.all, action);
return Object.assign({}, state, {all});
case types.REQUEST_GROUP_TOGGLE:
if (state.collapsed.indexOf(action.id) >= 0) {
collapsed = state.collapsed.filter(id => id !== action.id);
} else {
collapsed = [...state.collapsed, action.id]
}
return Object.assign({}, state, {collapsed});
all = requestGroupsReducer(state.all, action);
return Object.assign({}, state, {all});
case types.REQUEST_GROUP_UPDATE:
all = requestGroupsReducer(state.all, action);
return Object.assign({}, state, {all});
default:
return state
}

View File

@ -9,7 +9,6 @@ describe('RequestSchema', () => {
beforeEach(() => {
request = {
_mode: 'json',
id: 'rq_1234567890123',
created: Date.now(),
modified: Date.now(),

View File

@ -47,7 +47,6 @@ const requestSchema = {
id: '/Request',
type: 'object',
properties: {
_mode: {type: 'string'},
id: {type: 'string', pattern: '^rq_[\\w]{13}$'},
created: {type: 'number', minimum: 1000000000000, maximum: 10000000000000},
modified: {type: 'number', minimum: 1000000000000, maximum: 10000000000000},
@ -65,7 +64,6 @@ const requestSchema = {
headers: {type: 'array', items: {$ref: '/Header'}}
},
required: [
'_mode',
'id',
'url',
'created',

View File

@ -19,7 +19,6 @@ const requestGroupSchema = {
environment: {type: 'object'}
},
required: [
'_mode',
'id',
'created',
'modified',

View File

@ -21,8 +21,8 @@ app.on('ready', function () {
width: IS_DEV ? 1600 : 1200,
height: 800,
minHeight: 500,
minWidth: 500,
acceptFirstMouse: true,
minWidth: 520,
acceptFirstMouse: true
// titleBarStyle: IS_MAC ? 'hidden-inset' : 'default'
});