mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-20 14:17:29 -04:00
Add multi-part form support (#49)
* Add multi-part form support * tests for form and multipart * Better Analytics Tracking
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import * as networkUtils from '../network';
|
||||
import * as db from '../database';
|
||||
import nock from 'nock';
|
||||
import {resolve as pathResolve, join as pathJoin} from 'path';
|
||||
import {getRenderedRequest} from '../render';
|
||||
import * as models from '../../models';
|
||||
import {CONTENT_TYPE_FORM_URLENCODED} from '../constants';
|
||||
import {CONTENT_TYPE_FILE} from '../constants';
|
||||
import {CONTENT_TYPE_FORM_DATA} from '../constants';
|
||||
|
||||
describe('buildRequestConfig()', () => {
|
||||
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
|
||||
@@ -128,7 +131,7 @@ describe('actuallySend()', () => {
|
||||
.matchHeader('Content-Type', 'application/json')
|
||||
.matchHeader('Authorization', 'Basic dXNlcjpwYXNz')
|
||||
.matchHeader('Cookie', 'foo=barrrrr')
|
||||
.post('/')
|
||||
.post('/', 'foo=bar')
|
||||
.query({'foo bar': 'hello&world'})
|
||||
.reply(200, 'response body')
|
||||
.log(console.log);
|
||||
@@ -141,7 +144,7 @@ describe('actuallySend()', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
mimeType: CONTENT_TYPE_FORM_URLENCODED,
|
||||
text: 'foo=bar'
|
||||
params: [{name: 'foo', value: 'bar'}]
|
||||
},
|
||||
url: 'http://localhost',
|
||||
authentication: {
|
||||
@@ -152,9 +155,106 @@ describe('actuallySend()', () => {
|
||||
|
||||
const renderedRequest = await getRenderedRequest(request);
|
||||
const response = await networkUtils._actuallySend(renderedRequest, settings);
|
||||
|
||||
expect(mock.basePath).toBe('http://::1:80');
|
||||
expect(response.error).toBe('');
|
||||
expect(response.url).toBe('http://localhost/?foo%20bar=hello%26world');
|
||||
expect(response.body).toBe(new Buffer('response body').toString('base64'));
|
||||
expect(response.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('sends a file', async () => {
|
||||
let mock;
|
||||
|
||||
const workspace = await models.workspace.create();
|
||||
const settings = await models.settings.create();
|
||||
await models.cookieJar.create({parentId: workspace._id});
|
||||
|
||||
mock = nock('http://[::1]:80')
|
||||
.matchHeader('Content-Type', 'application/octet-stream')
|
||||
.post('/', 'Hello World!')
|
||||
.reply(200, 'response body')
|
||||
.log(console.log);
|
||||
|
||||
const request = Object.assign(models.request.init(), {
|
||||
_id: 'req_123',
|
||||
parentId: workspace._id,
|
||||
headers: [{name: 'Content-Type', value: 'application/octet-stream'}],
|
||||
url: 'http://localhost',
|
||||
method: 'POST',
|
||||
body: {
|
||||
mimeType: CONTENT_TYPE_FILE,
|
||||
fileName: pathResolve(pathJoin(__dirname, './testfile.txt')) // Let's send ourselves
|
||||
}
|
||||
});
|
||||
|
||||
const renderedRequest = await getRenderedRequest(request);
|
||||
const response = await networkUtils._actuallySend(renderedRequest, settings);
|
||||
|
||||
expect(mock.basePath).toBe('http://::1:80');
|
||||
expect(response.error).toBe('');
|
||||
expect(response.url).toBe('http://localhost/');
|
||||
expect(response.body).toBe(new Buffer('response body').toString('base64'));
|
||||
expect(response.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('sends multipart form data', async () => {
|
||||
let mock;
|
||||
|
||||
const workspace = await models.workspace.create();
|
||||
const settings = await models.settings.create();
|
||||
await models.cookieJar.create({parentId: workspace._id});
|
||||
const fileName = pathResolve(pathJoin(__dirname, './testfile.txt'));
|
||||
let requestBody = 'n/a';
|
||||
mock = nock('http://[::1]:80')
|
||||
.matchHeader('Content-Type', /^multipart\/form-data/)
|
||||
.post('/', body => {
|
||||
requestBody = body;
|
||||
return true;
|
||||
})
|
||||
.reply(200, 'response body')
|
||||
.log(console.log);
|
||||
|
||||
const request = Object.assign(models.request.init(), {
|
||||
_id: 'req_123',
|
||||
parentId: workspace._id,
|
||||
headers: [{name: 'Content-Type', value: 'multipart/form-data'}],
|
||||
url: 'http://localhost',
|
||||
method: 'POST',
|
||||
body: {
|
||||
mimeType: CONTENT_TYPE_FORM_DATA,
|
||||
params: [
|
||||
// Should ignore value and send the file since type is set to file
|
||||
{name: 'foo', fileName: fileName, value: 'bar', type: 'file'},
|
||||
|
||||
// Some extra params
|
||||
{name: 'a', value: 'AA'},
|
||||
{name: 'baz', value: 'qux', disabled: true},
|
||||
]
|
||||
},
|
||||
});
|
||||
|
||||
const renderedRequest = await getRenderedRequest(request);
|
||||
const response = await networkUtils._actuallySend(renderedRequest, settings);
|
||||
|
||||
expect(mock.basePath).toBe('http://::1:80');
|
||||
expect(response.error).toBe('');
|
||||
expect(response.url).toBe('http://localhost/');
|
||||
expect(response.body).toBe(new Buffer('response body').toString('base64'));
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const lines = requestBody.split(/\r\n/);
|
||||
expect(lines.length).toBe(11);
|
||||
expect(lines[0]).toMatch(/^----------------------------\d{24}/);
|
||||
expect(lines[1]).toBe('Content-Disposition: form-data; name="foo"');
|
||||
expect(lines[2]).toBe('Content-Type: text/plain');
|
||||
expect(lines[3]).toBe('');
|
||||
expect(lines[4]).toBe('Hello World!\n');
|
||||
expect(lines[5]).toMatch(/^----------------------------\d{24}/);
|
||||
expect(lines[6]).toBe('Content-Disposition: form-data; name="a"');
|
||||
expect(lines[7]).toBe('');
|
||||
expect(lines[8]).toBe('AA');
|
||||
expect(lines[9]).toMatch(/^----------------------------\d{24}--/);
|
||||
expect(lines[10]).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('getBasicAuthHeader()', () => {
|
||||
|
||||
describe('joinUrl()', () => {
|
||||
it('gets joiner for bare URL', () => {
|
||||
const url = querystringUtils.joinURL(
|
||||
const url = querystringUtils.joinUrl(
|
||||
'http://google.com',
|
||||
'foo=bar'
|
||||
);
|
||||
@@ -39,7 +39,7 @@ describe('joinUrl()', () => {
|
||||
});
|
||||
|
||||
it('gets joiner for URL with querystring', () => {
|
||||
const url = querystringUtils.joinURL(
|
||||
const url = querystringUtils.joinUrl(
|
||||
'http://google.com?hi=there',
|
||||
'foo=bar%20baz'
|
||||
);
|
||||
|
||||
1
app/common/__tests__/testfile.txt
Normal file
1
app/common/__tests__/testfile.txt
Normal file
@@ -0,0 +1 @@
|
||||
Hello World!
|
||||
@@ -122,9 +122,9 @@ export const CONTENT_TYPE_RAW = '';
|
||||
export const contentTypesMap = {
|
||||
[CONTENT_TYPE_JSON]: 'JSON',
|
||||
[CONTENT_TYPE_XML]: 'XML',
|
||||
// [CONTENT_TYPE_FORM_DATA]: 'Form Data',
|
||||
[CONTENT_TYPE_FORM_DATA]: 'Form Data',
|
||||
[CONTENT_TYPE_FORM_URLENCODED]: 'Form Url Encoded',
|
||||
[CONTENT_TYPE_FILE]: 'File Upload',
|
||||
[CONTENT_TYPE_FILE]: 'Binary',
|
||||
[CONTENT_TYPE_TEXT]: 'Plain Text',
|
||||
[CONTENT_TYPE_RAW]: 'Raw Body',
|
||||
};
|
||||
@@ -136,7 +136,7 @@ export const contentTypesMap = {
|
||||
* @returns {*|string}
|
||||
*/
|
||||
export function getContentTypeName (contentType) {
|
||||
return contentTypesMap[contentType] || 'Other';
|
||||
return contentTypesMap[contentType] || 'Body';
|
||||
}
|
||||
|
||||
export function getContentTypeFromHeaders (headers) {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import networkRequest from 'request';
|
||||
import {parse as urlParse} from 'url';
|
||||
import mime from 'mime-types';
|
||||
import {basename as pathBasename} from 'path';
|
||||
import * as models from '../models';
|
||||
import * as querystring from './querystring';
|
||||
import {buildFromParams} from './querystring';
|
||||
import * as util from './misc.js';
|
||||
import {DEBOUNCE_MILLIS, STATUS_CODE_PEBKAC, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED} from './constants';
|
||||
import {DEBOUNCE_MILLIS, STATUS_CODE_PEBKAC, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_FILE} from './constants';
|
||||
import {jarFromCookies, cookiesFromJar, cookieHeaderValueForUri} from './cookies';
|
||||
import {setDefaultProtocol} from './misc';
|
||||
import {getRenderedRequest} from './render';
|
||||
import {swapHost} from './dns';
|
||||
import {CONTENT_TYPE_FILE} from './constants';
|
||||
import * as fs from 'fs';
|
||||
|
||||
let cancelRequestFunction = null;
|
||||
@@ -51,7 +52,21 @@ export function _buildRequestConfig (renderedRequest, patch = {}) {
|
||||
if (renderedRequest.body.mimeType === CONTENT_TYPE_FORM_URLENCODED) {
|
||||
config.body = buildFromParams(renderedRequest.body.params || [], true);
|
||||
} else if (renderedRequest.body.mimeType === CONTENT_TYPE_FORM_DATA) {
|
||||
// TODO: This
|
||||
const formData = {};
|
||||
for (const param of renderedRequest.body.params) {
|
||||
if (param.type === 'file' && param.fileName) {
|
||||
formData[param.name] = {
|
||||
value: fs.readFileSync(param.fileName),
|
||||
options: {
|
||||
fileName: pathBasename(param.fileName),
|
||||
contentType: mime.lookup(param.fileName) // Guess the mime-type
|
||||
}
|
||||
}
|
||||
} else {
|
||||
formData[param.name] = param.value || '';
|
||||
}
|
||||
}
|
||||
config.formData = formData;
|
||||
} else if (renderedRequest.body.mimeType === CONTENT_TYPE_FILE) {
|
||||
config.body = fs.readFileSync(renderedRequest.body.fileName);
|
||||
} else {
|
||||
@@ -73,7 +88,7 @@ export function _buildRequestConfig (renderedRequest, patch = {}) {
|
||||
|
||||
// Set the URL, including the query parameters
|
||||
const qs = querystring.buildFromParams(renderedRequest.parameters);
|
||||
const url = querystring.joinURL(renderedRequest.url, qs);
|
||||
const url = querystring.joinUrl(renderedRequest.url, qs);
|
||||
config.url = util.prepareUrlForSending(url);
|
||||
config.headers.host = urlParse(config.url).host;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as util from './misc.js';
|
||||
|
||||
/** Join a URL with a querystring */
|
||||
export function joinURL (url, qs) {
|
||||
export function joinUrl (url, qs) {
|
||||
if (!qs) {
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -25,24 +25,6 @@ export function init () {
|
||||
};
|
||||
}
|
||||
|
||||
export function getBodyDescription (body) {
|
||||
if (body.fileName) {
|
||||
return 'File Upload';
|
||||
} else if (body.mimeType === CONTENT_TYPE_FORM_URLENCODED) {
|
||||
return 'Form Url Encoded';
|
||||
} else if (body.mimeType === CONTENT_TYPE_FORM_DATA) {
|
||||
return 'Form Data';
|
||||
} else if (body.mimeType === CONTENT_TYPE_JSON) {
|
||||
return 'JSON';
|
||||
} else if (body.mimeType === CONTENT_TYPE_XML) {
|
||||
return 'XML';
|
||||
} else if (body.mimeType === CONTENT_TYPE_TEXT) {
|
||||
return 'Plain Text';
|
||||
} else {
|
||||
return 'Raw Body';
|
||||
}
|
||||
}
|
||||
|
||||
export function newBodyRaw (rawBody, contentType) {
|
||||
if (!contentType) {
|
||||
return {text: rawBody};
|
||||
@@ -53,6 +35,11 @@ export function newBodyRaw (rawBody, contentType) {
|
||||
}
|
||||
|
||||
export function newBodyFormUrlEncoded (parameters) {
|
||||
// Remove any properties (eg. fileName) that might not fit
|
||||
parameters = (parameters || []).map(
|
||||
p => ({name: p.name, value: p.value})
|
||||
);
|
||||
|
||||
return {
|
||||
mimeType: CONTENT_TYPE_FORM_URLENCODED,
|
||||
params: parameters
|
||||
@@ -121,19 +108,20 @@ export function updateMimeType (request, mimeType) {
|
||||
|
||||
// 2. Make a new request body
|
||||
// TODO: When switching mime-type, try to convert formats nicely
|
||||
if (mimeType === CONTENT_TYPE_FORM_URLENCODED) {
|
||||
request.body = newBodyFormUrlEncoded(request.body.params || []);
|
||||
let body;
|
||||
if (mimeType === request.body.mimeType) {
|
||||
body = request.body;
|
||||
} else if (mimeType === CONTENT_TYPE_FORM_URLENCODED) {
|
||||
body = newBodyFormUrlEncoded(request.body.params);
|
||||
} else if (mimeType === CONTENT_TYPE_FORM_DATA) {
|
||||
request.body = newBodyForm(request.body.params || []);
|
||||
} else if (mimeType === CONTENT_TYPE_JSON) {
|
||||
request.body = newBodyRaw(request.body.text || '');
|
||||
body = newBodyForm(request.body.params || []);
|
||||
} else if (mimeType === CONTENT_TYPE_FILE) {
|
||||
request.body = newBodyFile('');
|
||||
body = newBodyFile('');
|
||||
} else {
|
||||
request.body = newBodyRaw(request.body.text || '', mimeType);
|
||||
body = newBodyRaw(request.body.text || '', mimeType);
|
||||
}
|
||||
|
||||
return update(request, {headers});
|
||||
return update(request, {headers, body});
|
||||
}
|
||||
|
||||
export function duplicate (request) {
|
||||
|
||||
@@ -18,7 +18,7 @@ class RenderedQueryString extends Component {
|
||||
const {request, environmentId} = props;
|
||||
const {url, parameters} = await getRenderedRequest(request, environmentId);
|
||||
const qs = querystring.buildFromParams(parameters);
|
||||
const fullUrl = querystring.joinURL(url, qs);
|
||||
const fullUrl = querystring.joinUrl(url, qs);
|
||||
this.setState({string: util.prepareUrlForSending(fullUrl)});
|
||||
}, delay ? 200 : 0);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import RenderedQueryString from './RenderedQueryString';
|
||||
import BodyEditor from './editors/body/BodyEditor';
|
||||
import AuthEditor from './editors/AuthEditor';
|
||||
import RequestUrlBar from './RequestUrlBar.js';
|
||||
import {MOD_SYM, getContentTypeName, getContentTypeFromHeaders} from '../../common/constants';
|
||||
import {MOD_SYM, getContentTypeName} from '../../common/constants';
|
||||
import {debounce} from '../../common/misc';
|
||||
import {getBodyDescription} from '../../models/request';
|
||||
import {trackEvent} from '../../analytics/index';
|
||||
|
||||
class RequestPane extends Component {
|
||||
render () {
|
||||
@@ -102,23 +102,23 @@ class RequestPane extends Component {
|
||||
</header>
|
||||
<Tabs className="pane__body">
|
||||
<TabList>
|
||||
<Tab>
|
||||
<Tab onClick={() => trackEvent('Request Pane', 'View', 'Body')}>
|
||||
<button>
|
||||
{getContentTypeName(request.body.mimeType)}
|
||||
{getContentTypeName(request.body.mimeType || '')}
|
||||
</button>
|
||||
<ContentTypeDropdown updateRequestMimeType={updateRequestMimeType}/>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<Tab onClick={() => trackEvent('Request Pane', 'View', 'Auth')}>
|
||||
<button>
|
||||
Auth {hasAuth ? <i className="fa fa-lock txt-sm"></i> : null}
|
||||
</button>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<Tab onClick={() => trackEvent('Request Pane', 'View', 'Query')}>
|
||||
<button>
|
||||
Query {numParameters ? <span className="txt-sm">({numParameters})</span> : null}
|
||||
</button>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<Tab onClick={() => trackEvent('Request Pane', 'View', 'Headers')}>
|
||||
<button>
|
||||
Headers {numHeaders ? <span className="txt-sm">({numHeaders})</span> : null}
|
||||
</button>
|
||||
@@ -159,6 +159,9 @@ class RequestPane extends Component {
|
||||
key={request._id}
|
||||
namePlaceholder="name"
|
||||
valuePlaceholder="value"
|
||||
onToggleDisable={pair => trackEvent('Query', 'Toggle', pair.disabled ? 'Disable' : 'Enable')}
|
||||
onCreate={() => trackEvent('Query', 'Create')}
|
||||
onDelete={() => trackEvent('Query', 'Delete')}
|
||||
pairs={request.parameters}
|
||||
onChange={updateRequestParameters}
|
||||
/>
|
||||
@@ -175,7 +178,10 @@ class RequestPane extends Component {
|
||||
<div className="pad-right text-right">
|
||||
<button
|
||||
className="margin-top-sm btn btn--outlined btn--super-compact"
|
||||
onClick={() => updateSettingsUseBulkHeaderEditor(!useBulkHeaderEditor)}>
|
||||
onClick={() => {
|
||||
updateSettingsUseBulkHeaderEditor(!useBulkHeaderEditor);
|
||||
trackEvent('Headers', 'Toggle Bulk', !useBulkHeaderEditor ? 'On' : 'Off');
|
||||
}}>
|
||||
{useBulkHeaderEditor ? 'Regular Edit' : 'Bulk Edit'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -55,16 +55,16 @@ class ResponsePane extends Component {
|
||||
|
||||
remote.dialog.showSaveDialog(options, filename => {
|
||||
if (!filename) {
|
||||
trackEvent('Response', 'Save', 'Cancel');
|
||||
trackEvent('Response', 'Save Cancel');
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFile(filename, bodyBuffer, {}, err => {
|
||||
if (err) {
|
||||
console.warn('Failed to save response body', err);
|
||||
trackEvent('Response', 'Save', 'Failure');
|
||||
trackEvent('Response', 'Save Failure');
|
||||
} else {
|
||||
trackEvent('Response', 'Save', 'Success');
|
||||
trackEvent('Response', 'Save Success');
|
||||
|
||||
}
|
||||
});
|
||||
@@ -197,7 +197,7 @@ class ResponsePane extends Component {
|
||||
)}
|
||||
<Tabs className="pane__body">
|
||||
<TabList>
|
||||
<Tab>
|
||||
<Tab onClick={() => trackEvent('Response Pane', 'View', 'Response')}>
|
||||
<button>
|
||||
{getPreviewModeName(previewMode)}
|
||||
</button>
|
||||
@@ -207,7 +207,7 @@ class ResponsePane extends Component {
|
||||
updatePreviewMode={handleSetPreviewMode}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<Tab onClick={() => trackEvent('Response Pane', 'View', 'Cookies')}>
|
||||
<button>
|
||||
Cookies {cookieHeaders.length ? (
|
||||
<span className="txt-sm">
|
||||
@@ -216,7 +216,7 @@ class ResponsePane extends Component {
|
||||
) : null}
|
||||
</button>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<Tab onClick={() => trackEvent('Response Pane', 'View', 'Headers')}>
|
||||
<button>
|
||||
Headers {response.headers.length ? (
|
||||
<span className="txt-sm">
|
||||
|
||||
@@ -17,11 +17,14 @@ class Toast extends Component {
|
||||
visible: false,
|
||||
};
|
||||
|
||||
this._notifications = [{
|
||||
key: KEY_PLUS_IS_HERE,
|
||||
message: 'Cloud sync is here!',
|
||||
cta: 'Show'
|
||||
}];
|
||||
this._notifications = [
|
||||
// TODO: Fetch these from remote server
|
||||
// {
|
||||
// key: KEY_PLUS_IS_HERE,
|
||||
// message: 'Cloud sync is here!',
|
||||
// cta: 'Show'
|
||||
// }
|
||||
];
|
||||
}
|
||||
|
||||
_loadSeen () {
|
||||
@@ -70,6 +73,11 @@ class Toast extends Component {
|
||||
if (notification) {
|
||||
this.setState({visible: false});
|
||||
}
|
||||
|
||||
// Give time for toast to fade out, then remove it
|
||||
setTimeout(() => {
|
||||
this.setState({notification: null});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@@ -80,6 +88,10 @@ class Toast extends Component {
|
||||
render () {
|
||||
const {notification, visible} = this.state;
|
||||
|
||||
if (!notification) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classnames('toast', {'toast--show': visible})}>
|
||||
<div className="toast__message">
|
||||
|
||||
@@ -16,6 +16,10 @@ class Editable extends Component {
|
||||
this._input && this._input.focus();
|
||||
this._input && this._input.select();
|
||||
});
|
||||
|
||||
if (this.props.onEditStart) {
|
||||
this.props.onEditStart();
|
||||
}
|
||||
}
|
||||
|
||||
async _handleEditEnd () {
|
||||
@@ -47,7 +51,7 @@ class Editable extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {value, singleClick, ...extra} = this.props;
|
||||
const {value, singleClick, onEditStart, ...extra} = this.props;
|
||||
const {editing} = this.state;
|
||||
|
||||
if (editing) {
|
||||
@@ -79,7 +83,8 @@ Editable.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
|
||||
// Optional
|
||||
singleClick: PropTypes.bool
|
||||
singleClick: PropTypes.bool,
|
||||
onEditStart: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Editable;
|
||||
|
||||
@@ -43,6 +43,7 @@ import {showModal} from '../modals/index';
|
||||
import AlertModal from '../modals/AlertModal';
|
||||
import Link from '../base/Link';
|
||||
import * as misc from '../../../common/misc';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
|
||||
const BASE_CODEMIRROR_OPTIONS = {
|
||||
@@ -158,6 +159,7 @@ class Editor extends Component {
|
||||
}
|
||||
|
||||
_handleBeautify () {
|
||||
trackEvent('Request', 'Beautify');
|
||||
this._prettify(this.codeMirror.getValue());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {basename as pathBasename} from 'path';
|
||||
import {remote} from 'electron';
|
||||
import {Dropdown, DropdownButton, DropdownItem} from '../base/dropdown';
|
||||
import PromptButton from '../base/PromptButton';
|
||||
|
||||
class FileInputButton extends Component {
|
||||
_handleUnsetFile () {
|
||||
this.props.onChange('');
|
||||
}
|
||||
|
||||
_handleChooseFile () {
|
||||
const options = {
|
||||
title: 'Import File',
|
||||
@@ -26,30 +21,23 @@ class FileInputButton extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {className} = this.props;
|
||||
const {showFileName, path, ...extraProps} = this.props;
|
||||
const fileName = pathBasename(path);
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownButton className={className}>
|
||||
Choose File <i className="fa fa-caret-down"></i>
|
||||
</DropdownButton>
|
||||
<DropdownItem onClick={e => this._handleChooseFile()}>
|
||||
<i className="fa fa-file"></i>
|
||||
Choose File
|
||||
</DropdownItem>
|
||||
<DropdownItem buttonClass={PromptButton}
|
||||
addIcon={true}
|
||||
onClick={e => this._handleUnsetFile()}>
|
||||
<i className="fa fa-close"></i>
|
||||
Clear File
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
<button onClick={e => this._handleChooseFile()} {...extraProps}>
|
||||
{showFileName && fileName ? `${fileName}`: 'Choose File'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FileInputButton.propTypes = {
|
||||
// Required
|
||||
onChange: PropTypes.func.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
|
||||
// Optional
|
||||
showFileName: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default FileInputButton;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import classnames from 'classnames';
|
||||
import {DEBOUNCE_MILLIS} from '../../../common/constants';
|
||||
import FileInputButton from '../base/FileInputButton';
|
||||
import {Dropdown, DropdownItem, DropdownButton} from './dropdown/index';
|
||||
|
||||
const NAME = 'name';
|
||||
const VALUE = 'value';
|
||||
@@ -49,6 +51,8 @@ class KeyValueEditor extends Component {
|
||||
...this.state.pairs.slice(position)
|
||||
];
|
||||
|
||||
this.props.onCreate && this.props.onCreate();
|
||||
|
||||
this._onChange(pairs);
|
||||
}
|
||||
|
||||
@@ -56,13 +60,20 @@ class KeyValueEditor extends Component {
|
||||
if (this._focusedPair >= position) {
|
||||
this._focusedPair = this._focusedPair - 1;
|
||||
}
|
||||
this._onChange(this.state.pairs.filter((_, i) => i !== position));
|
||||
|
||||
const pair = this.state.pairs[position];
|
||||
this.props.onDelete && this.props.onDelete(pair);
|
||||
|
||||
const pairs = this.state.pairs.filter((_, i) => i !== position);
|
||||
|
||||
this._onChange(pairs);
|
||||
}
|
||||
|
||||
_updatePair (position, pairPatch) {
|
||||
const pairs = this.state.pairs.map(
|
||||
(p, i) => i == position ? Object.assign({}, p, pairPatch) : p
|
||||
);
|
||||
const pairs = this.state.pairs.map((p, i) => (
|
||||
i == position ? Object.assign({}, p, pairPatch) : p
|
||||
));
|
||||
|
||||
this._onChange(pairs);
|
||||
}
|
||||
|
||||
@@ -70,6 +81,10 @@ class KeyValueEditor extends Component {
|
||||
const pairs = this.state.pairs.map(
|
||||
(p, i) => i == position ? Object.assign({}, p, {disabled: !p.disabled}) : p
|
||||
);
|
||||
|
||||
const pair = pairs[position];
|
||||
this.props.onToggleDisable && this.props.onToggleDisable(pair);
|
||||
|
||||
this._onChange(pairs, true);
|
||||
}
|
||||
|
||||
@@ -169,7 +184,7 @@ class KeyValueEditor extends Component {
|
||||
|
||||
render () {
|
||||
const {pairs} = this.state;
|
||||
const {maxPairs, className, valueInputType} = this.props;
|
||||
const {maxPairs, className, valueInputType, multipart} = this.props;
|
||||
|
||||
return (
|
||||
<ul className={classnames('key-value-editor', 'wide', className)}>
|
||||
@@ -200,35 +215,62 @@ class KeyValueEditor extends Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={classnames(
|
||||
'form-control form-control--wide', {
|
||||
'form-control--underlined': valueInputType !== 'file',
|
||||
'form-control--padded': valueInputType === 'file',
|
||||
}
|
||||
)}>
|
||||
<input
|
||||
type={valueInputType || 'text'}
|
||||
placeholder={this.props.valuePlaceholder || 'Value'}
|
||||
ref={n => this._valueInputs[i] = n}
|
||||
defaultValue={pair.value}
|
||||
onChange={e => this._updatePair(i, {value: e.target.value})}
|
||||
onFocus={e => {
|
||||
this._focusedPair = i;
|
||||
this._focusedField = VALUE;
|
||||
this._focusedInput = e.target;
|
||||
}}
|
||||
onBlur={() => {
|
||||
this._focusedPair = -1
|
||||
}}
|
||||
onKeyDown={this._keyDown.bind(this)}
|
||||
/>
|
||||
<div className="wide">
|
||||
<div className="form-control form-control--wide form-control--underlined">
|
||||
{pair.type === 'file' ? (
|
||||
<FileInputButton
|
||||
showFileName={true}
|
||||
className="btn btn--super-compact btn--outlined wide ellipsis txt-sm"
|
||||
onChange={fileName => {
|
||||
this._updatePair(i, {fileName});
|
||||
this.props.onChooseFile && this.props.onChooseFile();
|
||||
}}
|
||||
path={pair.fileName || ''}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type={valueInputType || 'text'}
|
||||
placeholder={this.props.valuePlaceholder || 'Value'}
|
||||
ref={n => this._valueInputs[i] = n}
|
||||
defaultValue={pair.value}
|
||||
onChange={e => this._updatePair(i, {value: e.target.value})}
|
||||
onFocus={e => {
|
||||
this._focusedPair = i;
|
||||
this._focusedField = VALUE;
|
||||
this._focusedInput = e.target;
|
||||
}}
|
||||
onBlur={() => {
|
||||
this._focusedPair = -1
|
||||
}}
|
||||
onKeyDown={this._keyDown.bind(this)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button tabIndex="-1"
|
||||
onClick={e => this._togglePair(i)}
|
||||
title={pair.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{multipart ? (
|
||||
<Dropdown right={true}>
|
||||
<DropdownButton className="tall">
|
||||
<i className="fa fa-caret-down"></i>
|
||||
</DropdownButton>
|
||||
<DropdownItem onClick={e => {
|
||||
this._updatePair(i, {type: 'text', value: '', fileName: ''});
|
||||
this.props.onChangeType && this.props.onChangeType('text');
|
||||
}}>
|
||||
Text
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={e => {
|
||||
this._updatePair(i, {type: 'file', value: '', fileName: ''});
|
||||
this.props.onChangeType && this.props.onChangeType('file');
|
||||
}}>
|
||||
File
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
onClick={e => this._togglePair(i)}
|
||||
title={pair.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{pair.disabled ?
|
||||
<i className="fa fa-square-o"></i> :
|
||||
<i className="fa fa-check-square-o"></i>
|
||||
@@ -261,9 +303,17 @@ class KeyValueEditor extends Component {
|
||||
this._addPair()
|
||||
}}/>
|
||||
</div>
|
||||
|
||||
{multipart ? (
|
||||
<button disabled={true} tabIndex="-1">
|
||||
<i className="fa fa-blank"></i>
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<button disabled={true} tabIndex="-1">
|
||||
<i className="fa fa-blank"></i>
|
||||
</button>
|
||||
|
||||
<button disabled={true} tabIndex="-1">
|
||||
<i className="fa fa-blank"></i>
|
||||
</button>
|
||||
@@ -279,10 +329,16 @@ KeyValueEditor.propTypes = {
|
||||
pairs: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
// Optional
|
||||
multipart: PropTypes.bool,
|
||||
maxPairs: PropTypes.number,
|
||||
namePlaceholder: PropTypes.string,
|
||||
valuePlaceholder: PropTypes.string,
|
||||
valueInputType: PropTypes.string
|
||||
valueInputType: PropTypes.string,
|
||||
onToggleDisable: PropTypes.func,
|
||||
onChangeType: PropTypes.func,
|
||||
onChooseFile: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onCreate: PropTypes.func,
|
||||
};
|
||||
|
||||
export default KeyValueEditor;
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {shell} from 'electron';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
import * as querystring from '../../../common/querystring';
|
||||
import {getAppVersion} from '../../../common/constants';
|
||||
|
||||
class Link extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this._boundHandleClick = this._handleClick.bind(this);
|
||||
}
|
||||
_handleClick (e) {
|
||||
e && e.preventDefault();
|
||||
const {href} = this.props;
|
||||
if (href.match(/^http/i)) {
|
||||
const qs = `utm_source=Insomnia&utm_medium=App&utm_campaign=v${getAppVersion()}`;
|
||||
const attributedHref = querystring.joinUrl(href, qs);
|
||||
shell.openExternal(attributedHref);
|
||||
} else {
|
||||
// Don't modify non-http urls
|
||||
shell.openExternal(href);
|
||||
}
|
||||
|
||||
trackEvent('Link', 'Click', href)
|
||||
}
|
||||
render () {
|
||||
const {button, href, children, ...other} = this.props;
|
||||
return button ? (
|
||||
<button onClick={() => shell.openExternal(href)} {...other}>
|
||||
<button onClick={this._boundHandleClick} {...other}>
|
||||
{children}
|
||||
</button>
|
||||
) :(
|
||||
<a href={href} onClick={e => {e.preventDefault(); shell.openExternal(href)}} {...other}>
|
||||
<a href={href} onClick={this._boundHandleClick} {...other}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Dropdown, DropdownButton, DropdownItem} from '../base/dropdown';
|
||||
import {contentTypesMap} from '../../../common/constants';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
const ContentTypeDropdown = ({updateRequestMimeType}) => {
|
||||
return (
|
||||
@@ -9,7 +10,10 @@ const ContentTypeDropdown = ({updateRequestMimeType}) => {
|
||||
<i className="fa fa-caret-down"></i>
|
||||
</DropdownButton>
|
||||
{Object.keys(contentTypesMap).map(mimeType => (
|
||||
<DropdownItem key={mimeType} onClick={e => updateRequestMimeType(mimeType)}>
|
||||
<DropdownItem key={mimeType} onClick={e => {
|
||||
updateRequestMimeType(mimeType);
|
||||
trackEvent('Request', 'Content-Type Change', contentTypesMap[mimeType]);
|
||||
}}>
|
||||
{contentTypesMap[mimeType]}
|
||||
</DropdownItem>
|
||||
))}
|
||||
|
||||
@@ -4,6 +4,7 @@ import classnames from 'classnames';
|
||||
import EnvironmentsModal from '../modals/WorkspaceEnvironmentsEditModal';
|
||||
import {Dropdown, DropdownDivider, DropdownButton, DropdownItem} from '../base/dropdown';
|
||||
import {showModal} from '../modals/index';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
|
||||
const EnvironmentsDropdown = ({
|
||||
@@ -37,11 +38,17 @@ const EnvironmentsDropdown = ({
|
||||
<DropdownDivider name="Switch Environment"/>
|
||||
{subEnvironments.map(environment => (
|
||||
<DropdownItem key={environment._id}
|
||||
onClick={e => handleChangeEnvironment(environment._id)}>
|
||||
onClick={e => {
|
||||
handleChangeEnvironment(environment._id);
|
||||
trackEvent('Environment', 'Activate');
|
||||
}}>
|
||||
<i className="fa fa-random"></i> Use <strong>{environment.name}</strong>
|
||||
</DropdownItem>
|
||||
))}
|
||||
<DropdownItem onClick={() => baseEnvironment && handleChangeEnvironment(null)}>
|
||||
<DropdownItem onClick={() => {
|
||||
baseEnvironment && handleChangeEnvironment(null);
|
||||
trackEvent('Environment', 'Deactivate');
|
||||
}}>
|
||||
<i className="fa fa-empty"></i> No Environment
|
||||
</DropdownItem>
|
||||
<DropdownDivider name="General"/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Dropdown, DropdownDivider, DropdownButton, DropdownItem} from '../base/dropdown';
|
||||
import {PREVIEW_MODES, getPreviewModeName} from '../../../common/constants';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
const PreviewModeDropdown = ({updatePreviewMode, download}) => (
|
||||
<Dropdown>
|
||||
@@ -8,7 +9,10 @@ const PreviewModeDropdown = ({updatePreviewMode, download}) => (
|
||||
<i className="fa fa-caret-down"></i>
|
||||
</DropdownButton>
|
||||
{PREVIEW_MODES.map(previewMode => (
|
||||
<DropdownItem key={previewMode} onClick={() => updatePreviewMode(previewMode)}>
|
||||
<DropdownItem key={previewMode} onClick={() => {
|
||||
updatePreviewMode(previewMode);
|
||||
trackEvent('Response', 'Preview Mode Change', previewMode);
|
||||
}}>
|
||||
{getPreviewModeName(previewMode)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
|
||||
@@ -5,6 +5,7 @@ import GenerateCodeModal from '../modals/GenerateCodeModal';
|
||||
import PromptModal from '../modals/PromptModal';
|
||||
import * as models from '../../../models';
|
||||
import {showModal} from '../modals/index';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
|
||||
class RequestActionsDropdown extends Component {
|
||||
@@ -28,18 +29,30 @@ class RequestActionsDropdown extends Component {
|
||||
<DropdownButton>
|
||||
<i className="fa fa-caret-down"></i>
|
||||
</DropdownButton>
|
||||
<DropdownItem onClick={e => models.request.duplicate(request)}>
|
||||
<DropdownItem onClick={e => {
|
||||
models.request.duplicate(request);
|
||||
trackEvent('Request', 'Duplicate', 'Action');
|
||||
}}>
|
||||
<i className="fa fa-copy"></i> Duplicate
|
||||
<DropdownHint char="D"></DropdownHint>
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={e => this._promptUpdateName()}>
|
||||
<DropdownItem onClick={e => {
|
||||
this._promptUpdateName();
|
||||
trackEvent('Request', 'Rename', 'Action');
|
||||
}}>
|
||||
<i className="fa fa-edit"></i> Rename
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={e => showModal(GenerateCodeModal, request)}>
|
||||
<DropdownItem onClick={e => {
|
||||
showModal(GenerateCodeModal, request);
|
||||
trackEvent('Request', 'Action', 'Generate Code');
|
||||
}}>
|
||||
<i className="fa fa-code"></i> Generate Code
|
||||
</DropdownItem>
|
||||
<DropdownItem buttonClass={PromptButton}
|
||||
onClick={e => models.request.remove(request)}
|
||||
onClick={e => {
|
||||
models.request.remove(request);
|
||||
trackEvent('Request', 'Delete', 'Action');
|
||||
}}
|
||||
addIcon={true}>
|
||||
<i className="fa fa-trash-o"></i> Delete
|
||||
</DropdownItem>
|
||||
|
||||
@@ -5,6 +5,7 @@ import EnvironmentEditModal from '../modals/EnvironmentEditModal';
|
||||
import PromptModal from '../modals/PromptModal';
|
||||
import * as models from '../../../models';
|
||||
import {showModal} from '../modals';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
class RequestGroupActionsDropdown extends Component {
|
||||
async _promptUpdateName () {
|
||||
@@ -16,6 +17,8 @@ class RequestGroupActionsDropdown extends Component {
|
||||
});
|
||||
|
||||
models.requestGroup.update(requestGroup, {name});
|
||||
|
||||
trackEvent('Folder', 'Rename', 'Folder Action');
|
||||
}
|
||||
|
||||
async _requestCreate () {
|
||||
@@ -29,11 +32,13 @@ class RequestGroupActionsDropdown extends Component {
|
||||
const parentId = requestGroup._id;
|
||||
const request = await models.request.create({parentId, name});
|
||||
this.props.actions.global.activateRequest(workspace, request);
|
||||
trackEvent('Request', 'Create', 'Folder Action');
|
||||
}
|
||||
|
||||
_requestGroupDuplicate () {
|
||||
const {requestGroup} = this.props;
|
||||
models.requestGroup.duplicate(requestGroup);
|
||||
trackEvent('Folder', 'Duplicate', 'Folder Action');
|
||||
}
|
||||
|
||||
async _requestGroupCreate () {
|
||||
@@ -45,6 +50,8 @@ class RequestGroupActionsDropdown extends Component {
|
||||
|
||||
const {requestGroup} = this.props;
|
||||
models.requestGroup.create({parentId: requestGroup._id, name});
|
||||
|
||||
trackEvent('Folder', 'Create', 'Folder Action');
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -73,9 +80,10 @@ class RequestGroupActionsDropdown extends Component {
|
||||
onClick={e => showModal(EnvironmentEditModal, requestGroup)}>
|
||||
<i className="fa fa-code"></i> Environment
|
||||
</DropdownItem>
|
||||
<DropdownItem buttonClass={PromptButton}
|
||||
onClick={e => models.requestGroup.remove(requestGroup)}
|
||||
addIcon={true}>
|
||||
<DropdownItem buttonClass={PromptButton} addIcon={true} onClick={e => {
|
||||
models.requestGroup.remove(requestGroup);
|
||||
trackEvent('Folder', 'Delete', 'Folder Action');
|
||||
}}>
|
||||
<i className="fa fa-trash-o"></i> Delete
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
|
||||
@@ -120,10 +120,9 @@ class SyncDropdown extends Component {
|
||||
if (!loggedIn) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown wide={true}
|
||||
className="wide tall"
|
||||
onClick={e => trackEvent('Sync', 'Show Menu', 'Guest')}>
|
||||
<DropdownButton className="btn btn--compact wide">
|
||||
<Dropdown wide={true} className="wide tall">
|
||||
<DropdownButton className="btn btn--compact wide"
|
||||
onClick={e => trackEvent('Sync', 'Show Menu', 'Guest')}>
|
||||
Sync Settings
|
||||
</DropdownButton>
|
||||
<DropdownDivider name="Insomnia Cloud Sync"/>
|
||||
@@ -167,10 +166,9 @@ class SyncDropdown extends Component {
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown wide={true}
|
||||
className="wide tall"
|
||||
onClick={e => trackEvent('Sync', 'Show Menu', 'Authenticated')}>
|
||||
<DropdownButton className="btn btn--compact wide">
|
||||
<Dropdown wide={true} className="wide tall">
|
||||
<DropdownButton className="btn btn--compact wide"
|
||||
onClick={e => trackEvent('Sync', 'Show Menu', 'Authenticated')}>
|
||||
{description}
|
||||
</DropdownButton>
|
||||
<DropdownDivider name={`Workspace Synced ${syncPercent}%`}/>
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as models from '../../../models';
|
||||
import {getAppVersion} from '../../../common/constants';
|
||||
import {showModal} from '../modals/index';
|
||||
import {TAB_INDEX_EXPORT} from '../modals/SettingsModal';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
class WorkspaceDropdown extends Component {
|
||||
async _promptUpdateName () {
|
||||
@@ -72,13 +73,19 @@ class WorkspaceDropdown extends Component {
|
||||
</h1>
|
||||
</DropdownButton>
|
||||
<DropdownDivider name="Current Workspace"/>
|
||||
<DropdownItem onClick={e => this._promptUpdateName()}>
|
||||
<DropdownItem onClick={e => {
|
||||
this._promptUpdateName();
|
||||
trackEvent('Workspace', 'Rename');
|
||||
}}>
|
||||
<i className="fa fa-pencil-square-o"></i> Rename
|
||||
{" "}
|
||||
<strong>{activeWorkspace.name}</strong>
|
||||
</DropdownItem>
|
||||
<DropdownItem buttonClass={PromptButton}
|
||||
onClick={e => this._workspaceRemove()}
|
||||
onClick={e => {
|
||||
this._workspaceRemove();
|
||||
trackEvent('Workspace', 'Delete');
|
||||
}}
|
||||
addIcon={true}>
|
||||
<i className="fa fa-trash-o"></i> Delete
|
||||
{" "}
|
||||
@@ -88,13 +95,19 @@ class WorkspaceDropdown extends Component {
|
||||
<DropdownDivider name="Workspaces"/>
|
||||
|
||||
{workspaces.map(w => w._id === activeWorkspace._id ? null : (
|
||||
<DropdownItem key={w._id} onClick={() => handleSetActiveWorkspace(w._id)}>
|
||||
<DropdownItem key={w._id} onClick={() => {
|
||||
handleSetActiveWorkspace(w._id);
|
||||
trackEvent('Workspace', 'Switch');
|
||||
}}>
|
||||
<i className="fa fa-random"></i> Switch to
|
||||
{" "}
|
||||
<strong>{w.name}</strong>
|
||||
</DropdownItem>
|
||||
))}
|
||||
<DropdownItem onClick={e => this._workspaceCreate()}>
|
||||
<DropdownItem onClick={e => {
|
||||
this._workspaceCreate();
|
||||
trackEvent('Workspace', 'Create');
|
||||
}}>
|
||||
<i className="fa fa-blank"></i> New Workspace
|
||||
</DropdownItem>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import KeyValueEditor from '../base/KeyValueEditor';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
const AuthEditor = ({request, showPasswords, onChange, ...other}) => {
|
||||
const auth = request.authentication;
|
||||
@@ -16,6 +17,9 @@ const AuthEditor = ({request, showPasswords, onChange, ...other}) => {
|
||||
namePlaceholder="Username"
|
||||
valuePlaceholder="********"
|
||||
valueInputType={showPasswords ? 'text' : 'password'}
|
||||
onToggleDisable={pair => trackEvent('Auth Editor', 'Toggle', pair.disabled ? 'Disable' : 'Enable')}
|
||||
onCreate={() => trackEvent('Auth Editor', 'Create')}
|
||||
onDelete={() => trackEvent('Auth Editor', 'Delete')}
|
||||
onChange={pairs => onChange({
|
||||
username: pairs.length ? pairs[0].name : '',
|
||||
password: pairs.length ? pairs[0].value : '',
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, {Component, PropTypes} from 'react';
|
||||
|
||||
import KeyValueEditor from '../base/KeyValueEditor';
|
||||
import Editor from '../base/Editor';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
class RequestHeadersEditor extends Component {
|
||||
_handleBulkUpdate (headersString) {
|
||||
@@ -73,6 +74,9 @@ class RequestHeadersEditor extends Component {
|
||||
namePlaceholder="My-Header"
|
||||
valuePlaceholder="Value"
|
||||
pairs={headers}
|
||||
onToggleDisable={pair => trackEvent('Headers Editor', 'Toggle', pair.disabled ? 'Disable' : 'Enable')}
|
||||
onCreate={() => trackEvent('Headers Editor', 'Create')}
|
||||
onDelete={() => trackEvent('Headers Editor', 'Delete')}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,9 @@ import fs from 'fs';
|
||||
import electron from 'electron';
|
||||
import React, {PropTypes, Component} from 'react';
|
||||
import FileInputButton from '../../base/FileInputButton';
|
||||
import PromptButton from '../../base/PromptButton';
|
||||
import * as misc from '../../../../common/misc';
|
||||
import {trackEvent} from '../../../../analytics/index';
|
||||
|
||||
class FileEditor extends Component {
|
||||
render () {
|
||||
@@ -35,11 +37,25 @@ class FileEditor extends Component {
|
||||
<code className="super-faint">No file selected</code>
|
||||
)}
|
||||
</p>
|
||||
<FileInputButton
|
||||
path={path}
|
||||
className="btn btn--super-compact btn--outlined"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div>
|
||||
<PromptButton className="btn btn--super-compact"
|
||||
disabled={!path}
|
||||
onClick={e => {
|
||||
onChange('');
|
||||
trackEvent('File Editor', 'Reset')
|
||||
}}>
|
||||
Reset File
|
||||
</PromptButton>
|
||||
|
||||
<FileInputButton
|
||||
path={path}
|
||||
className="btn btn--super-compact btn--outlined"
|
||||
onChange={path => {
|
||||
onChange(path);
|
||||
trackEvent('File Editor', 'Choose')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {PropTypes, Component} from 'react';
|
||||
import KeyValueEditor from '../../base/KeyValueEditor';
|
||||
import {trackEvent} from '../../../../analytics/index';
|
||||
|
||||
class FormEditor extends Component {
|
||||
render () {
|
||||
@@ -8,7 +9,16 @@ class FormEditor extends Component {
|
||||
return (
|
||||
<div className="scrollable-container tall wide">
|
||||
<div className="scrollable">
|
||||
<KeyValueEditor onChange={onChange} pairs={parameters} valueInputType="file"/>
|
||||
<KeyValueEditor
|
||||
onToggleDisable={pair => trackEvent('Form Editor', `Toggle ${pair.type || 'text'}`, pair.disabled ? 'Disable' : 'Enable')}
|
||||
onChangeType={type => trackEvent('Form Editor', 'Change Type', type)}
|
||||
onChooseFile={() => trackEvent('Form Editor', 'Choose File')}
|
||||
onCreate={() => trackEvent('Form Editor', 'Create')}
|
||||
onDelete={() => trackEvent('Form Editor', 'Delete')}
|
||||
onChange={onChange}
|
||||
pairs={parameters}
|
||||
multipart={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {PropTypes, Component} from 'react';
|
||||
import KeyValueEditor from '../../base/KeyValueEditor';
|
||||
import {trackEvent} from '../../../../analytics/index';
|
||||
|
||||
class UrlEncodedEditor extends Component {
|
||||
render () {
|
||||
@@ -8,7 +9,13 @@ class UrlEncodedEditor extends Component {
|
||||
return (
|
||||
<div className="scrollable-container tall wide">
|
||||
<div className="scrollable">
|
||||
<KeyValueEditor onChange={onChange} pairs={parameters}/>
|
||||
<KeyValueEditor
|
||||
onChange={onChange}
|
||||
onToggleDisable={pair => trackEvent('Url Encoded Editor', 'Toggle', pair.disabled ? 'Disable' : 'Enable')}
|
||||
onCreate={() => trackEvent('Url Encoded Editor', 'Create')}
|
||||
onDelete={() => trackEvent('Url Encoded Editor', 'Delete')}
|
||||
pairs={parameters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import ModalHeader from '../base/ModalHeader';
|
||||
import ModalFooter from '../base/ModalFooter';
|
||||
import CookiesEditor from '../editors/CookiesEditor';
|
||||
import * as models from '../../../models';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
class CookiesModal extends Component {
|
||||
constructor (props) {
|
||||
@@ -34,6 +35,7 @@ class CookiesModal extends Component {
|
||||
];
|
||||
|
||||
this._saveChanges(cookieJar);
|
||||
trackEvent('Cookie', 'Update');
|
||||
}
|
||||
|
||||
_handleCookieAdd (cookie) {
|
||||
@@ -41,6 +43,7 @@ class CookiesModal extends Component {
|
||||
const {cookies} = cookieJar;
|
||||
cookieJar.cookies = [cookie, ...cookies];
|
||||
this._saveChanges(cookieJar);
|
||||
trackEvent('Cookie', 'Create');
|
||||
}
|
||||
|
||||
_handleCookieDelete (cookie) {
|
||||
@@ -51,10 +54,12 @@ class CookiesModal extends Component {
|
||||
cookieJar.cookies = cookies.filter(c => c !== cookie);
|
||||
|
||||
this._saveChanges(cookieJar);
|
||||
trackEvent('Cookie', 'Delete');
|
||||
}
|
||||
|
||||
_onFilterChange (filter) {
|
||||
this.setState({filter});
|
||||
trackEvent('Cookie Editor', 'Filter Change');
|
||||
}
|
||||
|
||||
_getFilteredSortedCookies () {
|
||||
@@ -81,6 +86,7 @@ class CookiesModal extends Component {
|
||||
await this._load(workspace);
|
||||
this.modal.show();
|
||||
this.filterInput.focus();
|
||||
trackEvent('Cookie Editor', 'Show');
|
||||
}
|
||||
|
||||
toggle (workspace) {
|
||||
|
||||
@@ -49,7 +49,7 @@ class LoginModal extends Component {
|
||||
|
||||
this.modal.hide();
|
||||
showModal(SignupModal);
|
||||
trackEvent('Auth', 'Switch', 'To Signup');
|
||||
trackEvent('Login', 'Switch to Signup');
|
||||
}
|
||||
|
||||
show (options = {}) {
|
||||
@@ -113,7 +113,7 @@ class LoginModal extends Component {
|
||||
<p>
|
||||
If you have any questions or concerns, send you email to
|
||||
{" "}
|
||||
<Link href="mailto:support@insomnia.rest">
|
||||
<Link href="https://insomnia.rest/documentation/support-and-feedback/">
|
||||
support@insomnia.rest
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -207,7 +207,7 @@ class RequestSwitcherModal extends Component {
|
||||
<Modal ref={m => this.modal = m} top={true}
|
||||
dontFocus={true} {...this.props}>
|
||||
<ModalHeader hideCloseButton={true}>
|
||||
<p className="pull-right txt-md">
|
||||
<div className="pull-right txt-md">
|
||||
<span className="monospace">tab</span> or
|
||||
|
||||
<span className="monospace">↑ ↓</span> to navigate
|
||||
@@ -215,8 +215,8 @@ class RequestSwitcherModal extends Component {
|
||||
<span className="monospace">↵</span> to select
|
||||
|
||||
<span className="monospace">esc</span> to dismiss
|
||||
</p>
|
||||
<p>Quick Switch</p>
|
||||
</div>
|
||||
<div>Quick Switch</div>
|
||||
</ModalHeader>
|
||||
<ModalBody className="pad request-switcher">
|
||||
<div className="form-control form-control--outlined no-margin">
|
||||
|
||||
@@ -74,25 +74,28 @@ class SettingsModal extends Component {
|
||||
<Tabs onSelect={i => this._handleTabSelect(i)} selectedIndex={currentTabIndex}>
|
||||
<TabList>
|
||||
<Tab selected={this._currentTabIndex === 0}>
|
||||
<button>General</button>
|
||||
<button onClick={e => trackEvent('Setting', 'Tab General')}>General</button>
|
||||
</Tab>
|
||||
<Tab selected={this._currentTabIndex === 1}>
|
||||
<button>Import/Export</button>
|
||||
<button onClick={e => trackEvent('Setting', 'Tab Import/Export')}>Import/Export</button>
|
||||
</Tab>
|
||||
<Tab selected={this._currentTabIndex === 3}>
|
||||
<button>Shortcuts</button>
|
||||
<button onClick={e => trackEvent('Setting', 'Tab Shortcuts')}>Shortcuts</button>
|
||||
</Tab>
|
||||
<Tab selected={this._currentTabIndex === 2}>
|
||||
<button>Insomnia Plus</button>
|
||||
<button onClick={e => trackEvent('Setting', 'Tab Plus')}>Insomnia Plus</button>
|
||||
</Tab>
|
||||
<Tab selected={this._currentTabIndex === 4}>
|
||||
<button>About</button>
|
||||
<button onClick={e => trackEvent('Setting', 'Tab About')}>About</button>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className="pad scrollable">
|
||||
<SettingsGeneral
|
||||
settings={settings}
|
||||
updateSetting={(key, value) => models.settings.update(settings, {[key]: value})}
|
||||
updateSetting={(key, value) => {
|
||||
models.settings.update(settings, {[key]: value});
|
||||
trackEvent('Setting', 'Change', key)
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className="pad scrollable">
|
||||
|
||||
@@ -54,7 +54,7 @@ class SignupModal extends Component {
|
||||
|
||||
this.modal.hide();
|
||||
showModal(LoginModal, {});
|
||||
trackEvent('Auth', 'Switch', 'To Login');
|
||||
trackEvent('Signup', 'Switch to Login');
|
||||
}
|
||||
|
||||
_checkPasswordsMatch () {
|
||||
|
||||
@@ -10,6 +10,7 @@ import ModalBody from '../base/ModalBody';
|
||||
import ModalHeader from '../base/ModalHeader';
|
||||
import ModalFooter from '../base/ModalFooter';
|
||||
import * as models from '../../../models'
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
|
||||
class WorkspaceEnvironmentsEditModal extends Component {
|
||||
@@ -28,6 +29,7 @@ class WorkspaceEnvironmentsEditModal extends Component {
|
||||
show (workspace) {
|
||||
this.modal.show();
|
||||
this._load(workspace);
|
||||
trackEvent('Environment Editor', 'Show');
|
||||
}
|
||||
|
||||
toggle (workspace) {
|
||||
@@ -60,9 +62,10 @@ class WorkspaceEnvironmentsEditModal extends Component {
|
||||
const parentId = rootEnvironment._id;
|
||||
const environment = await models.environment.create({parentId});
|
||||
this._load(workspace, environment);
|
||||
trackEvent('Environment', 'Create');
|
||||
}
|
||||
|
||||
_handleActivateEnvironment (environment) {
|
||||
_handleShowEnvironment (environment) {
|
||||
// Don't allow switching if the current one has errors
|
||||
if (!this._envEditor.isValid()) {
|
||||
return;
|
||||
@@ -70,6 +73,7 @@ class WorkspaceEnvironmentsEditModal extends Component {
|
||||
|
||||
const {workspace} = this.state;
|
||||
this._load(workspace, environment);
|
||||
trackEvent('Environment Editor', 'Show Environment');
|
||||
}
|
||||
|
||||
async _handleDeleteEnvironment (environment) {
|
||||
@@ -84,6 +88,7 @@ class WorkspaceEnvironmentsEditModal extends Component {
|
||||
await models.environment.remove(environment);
|
||||
|
||||
this._load(workspace, rootEnvironment);
|
||||
trackEvent('Environment', 'Delete');
|
||||
}
|
||||
|
||||
async _handleChangeEnvironmentName (environment, name) {
|
||||
@@ -94,6 +99,8 @@ class WorkspaceEnvironmentsEditModal extends Component {
|
||||
const realEnvironment = await models.environment.getById(environment._id);
|
||||
await models.environment.update(realEnvironment, {name});
|
||||
this._load(workspace);
|
||||
|
||||
trackEvent('Environment', 'Rename');
|
||||
}
|
||||
|
||||
_didChange () {
|
||||
@@ -138,7 +145,7 @@ class WorkspaceEnvironmentsEditModal extends Component {
|
||||
<ModalHeader>Manage Environments (JSON Format)</ModalHeader>
|
||||
<ModalBody noScroll={true} className="env-modal">
|
||||
<div className="env-modal__sidebar">
|
||||
<li onClick={() => this._handleActivateEnvironment(rootEnvironment)}
|
||||
<li onClick={() => this._handleShowEnvironment(rootEnvironment)}
|
||||
className={classnames(
|
||||
'env-modal__sidebar-root-item',
|
||||
{'env-modal__sidebar-item--active': activeEnvironment === rootEnvironment}
|
||||
@@ -161,7 +168,7 @@ class WorkspaceEnvironmentsEditModal extends Component {
|
||||
return (
|
||||
<li key={environment._id} className={classes}>
|
||||
<button
|
||||
onClick={() => this._handleActivateEnvironment(environment)}>
|
||||
onClick={() => this._handleShowEnvironment(environment)}>
|
||||
<Editable
|
||||
onSubmit={name => this._handleChangeEnvironmentName(environment, name)}
|
||||
value={environment.name}
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, {PropTypes} from 'react';
|
||||
|
||||
const SettingsGeneral = ({settings, updateSetting}) => (
|
||||
<div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
id="setting-follow-redirects"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Link from '../base/Link';
|
||||
import PromptButton from '../base/PromptButton';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
const SettingsSync = ({
|
||||
loggedIn,
|
||||
@@ -46,7 +47,7 @@ const SettingsSync = ({
|
||||
</p>
|
||||
] : [
|
||||
<p key="0">
|
||||
<Link href="https://insomnia.rest/plus">Insomnia Plus</Link> helps you <i>rest</i> easy by
|
||||
<Link href="https://insomnia.rest/plus/">Insomnia Plus</Link> helps you <i>rest</i> easy by
|
||||
keeping your workspaces securely backed up and synced across all of your devices.
|
||||
</p>,
|
||||
<p key="1">
|
||||
@@ -59,6 +60,7 @@ const SettingsSync = ({
|
||||
<button className="btn txt-lg btn--outlined"
|
||||
onClick={() => {
|
||||
handleExit();
|
||||
trackEvent('Settings Sync', 'Click Upgrade');
|
||||
handleShowSignup()
|
||||
}}>
|
||||
Upgrade to Plus
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {Dropdown, DropdownHint, DropdownButton, DropdownItem} from '../base/dropdown';
|
||||
import {DEBOUNCE_MILLIS} from '../../../common/constants';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
|
||||
class SidebarFilter extends Component {
|
||||
@@ -8,6 +9,7 @@ class SidebarFilter extends Component {
|
||||
clearTimeout(this._triggerTimeout);
|
||||
this._triggerTimeout = setTimeout(() => {
|
||||
this.props.onChange(value);
|
||||
trackEvent('Sidebar', 'Filter');
|
||||
}, DEBOUNCE_MILLIS);
|
||||
}
|
||||
|
||||
@@ -28,11 +30,17 @@ class SidebarFilter extends Component {
|
||||
<DropdownButton className="btn btn--compact">
|
||||
<i className="fa fa-plus-circle"></i>
|
||||
</DropdownButton>
|
||||
<DropdownItem onClick={e => requestCreate()}>
|
||||
<DropdownItem onClick={e => {
|
||||
requestCreate();
|
||||
trackEvent('Request', 'Create', 'Sidebar Filter');
|
||||
}}>
|
||||
<i className="fa fa-plus-circle"></i> New Request
|
||||
<DropdownHint char="N"></DropdownHint>
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={e => requestGroupCreate()}>
|
||||
<DropdownItem onClick={e => {
|
||||
requestGroupCreate();
|
||||
trackEvent('Folder', 'Create', 'Sidebar Filter');
|
||||
}}>
|
||||
<i className="fa fa-folder"></i> New Folder
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
|
||||
@@ -5,6 +5,7 @@ import classnames from 'classnames';
|
||||
|
||||
import RequestGroupActionsDropdown from '../dropdowns/RequestGroupActionsDropdown';
|
||||
import SidebarRequestRow from './SidebarRequestRow';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
class SidebarRequestGroupRow extends Component {
|
||||
constructor (props) {
|
||||
@@ -60,7 +61,10 @@ class SidebarRequestGroupRow extends Component {
|
||||
<li key={requestGroup._id} className={classes}>
|
||||
<div
|
||||
className={classnames('sidebar__item sidebar__item--big', {'sidebar__item--active': isActive})}>
|
||||
<button onClick={e => handleSetRequestGroupCollapsed(requestGroup._id, !isCollapsed)}>
|
||||
<button onClick={e => {
|
||||
handleSetRequestGroupCollapsed(requestGroup._id, !isCollapsed);
|
||||
trackEvent('Folder', 'Toggle Visible', !isCollapsed ? 'Close' : 'Open')
|
||||
}}>
|
||||
<div className="sidebar__clickable">
|
||||
<i className={'sidebar__item__icon fa ' + folderIconClass}></i>
|
||||
<span>{requestGroup.name}</span>
|
||||
@@ -122,6 +126,7 @@ SidebarRequestGroupRow.propTypes = {
|
||||
*/
|
||||
const dragSource = {
|
||||
beginDrag(props) {
|
||||
trackEvent('Folder', 'Drag', 'Begin');
|
||||
return {
|
||||
requestGroup: props.requestGroup
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import RequestActionsDropdown from '../dropdowns/RequestActionsDropdown';
|
||||
import Editable from '../base/Editable';
|
||||
import MethodTag from '../tags/MethodTag';
|
||||
import * as models from '../../../models';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
|
||||
class SidebarRequestRow extends Component {
|
||||
@@ -51,7 +52,10 @@ class SidebarRequestRow extends Component {
|
||||
<li className={classes}>
|
||||
<div className="sidebar__item" tabIndex={0}>
|
||||
<button className="sidebar__clickable"
|
||||
onClick={() => requestCreate()}>
|
||||
onClick={() => {
|
||||
requestCreate();
|
||||
trackEvent('Request', 'Create', 'Empty Folder');
|
||||
}}>
|
||||
<em>click to add first request...</em>
|
||||
</button>
|
||||
</div>
|
||||
@@ -61,11 +65,15 @@ class SidebarRequestRow extends Component {
|
||||
node = (
|
||||
<li className={classes}>
|
||||
<div className={classnames('sidebar__item', {'sidebar__item--active': isActive})}>
|
||||
<button className="wide" onClick={e => handleActivateRequest(request)}>
|
||||
<button className="wide" onClick={e => {
|
||||
handleActivateRequest(request);
|
||||
trackEvent('Request', 'Activate', 'Sidebar');
|
||||
}}>
|
||||
<div className="sidebar__clickable">
|
||||
<MethodTag method={request.method}/>
|
||||
<Editable
|
||||
value={request.name}
|
||||
onEditStart={() => trackEvent('Request', 'Rename', 'In Place')}
|
||||
onSubmit={name => models.request.update(request, {name})}
|
||||
/>
|
||||
</div>
|
||||
@@ -113,6 +121,7 @@ SidebarRequestRow.propTypes = {
|
||||
*/
|
||||
const dragSource = {
|
||||
beginDrag(props) {
|
||||
trackEvent('Request', 'Drag', 'Begin');
|
||||
return {
|
||||
request: props.request
|
||||
};
|
||||
|
||||
@@ -25,8 +25,9 @@ class ResponseError extends Component {
|
||||
)
|
||||
} else {
|
||||
msg = (
|
||||
<Link button={true} className="btn btn--super-compact btn--outlined"
|
||||
href="http://docs.insomnia.rest">
|
||||
<Link button={true}
|
||||
className="btn btn--super-compact btn--outlined"
|
||||
href="https://insomnia.rest/documentation/">
|
||||
Documentation
|
||||
</Link>
|
||||
)
|
||||
@@ -45,7 +46,7 @@ class ResponseError extends Component {
|
||||
{msg}
|
||||
|
||||
<Link button={true} className="btn btn--super-compact btn--outlined margin-top-sm"
|
||||
href="mailto:support@insomnia.rest">
|
||||
href="https://insomnia.rest/documentation/support-and-feedback/">
|
||||
Contact Support
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -16,16 +16,14 @@ import RequestSwitcherModal from '../components/modals/RequestSwitcherModal';
|
||||
import PromptModal from '../components/modals/PromptModal';
|
||||
import ChangelogModal from '../components/modals/ChangelogModal';
|
||||
import SettingsModal from '../components/modals/SettingsModal';
|
||||
import {MAX_PANE_WIDTH, MIN_PANE_WIDTH, DEFAULT_PANE_WIDTH, MAX_SIDEBAR_REMS, MIN_SIDEBAR_REMS, DEFAULT_SIDEBAR_WIDTH, getAppVersion} from '../../common/constants';
|
||||
import {MAX_PANE_WIDTH, MIN_PANE_WIDTH, DEFAULT_PANE_WIDTH, MAX_SIDEBAR_REMS, MIN_SIDEBAR_REMS, DEFAULT_SIDEBAR_WIDTH, getAppVersion, PREVIEW_MODE_SOURCE} from '../../common/constants';
|
||||
import * as globalActions from '../redux/modules/global';
|
||||
import * as workspaceMetaActions from '../redux/modules/workspaceMeta';
|
||||
import * as requestMetaActions from '../redux/modules/requestMeta';
|
||||
import * as requestGroupMetaActions from '../redux/modules/requestGroupMeta';
|
||||
import * as db from '../../common/database';
|
||||
import * as models from '../../models';
|
||||
import {importRaw} from '../../common/import';
|
||||
import {trackEvent, trackLegacyEvent} from '../../analytics';
|
||||
import {PREVIEW_MODE_SOURCE} from '../../common/constants';
|
||||
|
||||
|
||||
class App extends Component {
|
||||
@@ -56,6 +54,7 @@ class App extends Component {
|
||||
// Show Request Switcher
|
||||
'mod+p': () => {
|
||||
toggleModal(RequestSwitcherModal);
|
||||
trackEvent('HotKey', 'Quick Switcher');
|
||||
},
|
||||
|
||||
// Request Send
|
||||
@@ -65,24 +64,28 @@ class App extends Component {
|
||||
activeRequest ? activeRequest._id : 'n/a',
|
||||
activeEnvironment ? activeEnvironment._id : 'n/a',
|
||||
);
|
||||
trackEvent('HotKey', 'Send');
|
||||
},
|
||||
|
||||
// Edit Workspace Environments
|
||||
'mod+e': () => {
|
||||
const {activeWorkspace} = this.props;
|
||||
toggleModal(WorkspaceEnvironmentsEditModal, activeWorkspace);
|
||||
trackEvent('HotKey', 'Environments');
|
||||
},
|
||||
|
||||
// Focus URL Bar
|
||||
'mod+l': () => {
|
||||
const node = document.body.querySelector('.urlbar input');
|
||||
node && node.focus();
|
||||
trackEvent('HotKey', 'Url');
|
||||
},
|
||||
|
||||
// Edit Cookies
|
||||
'mod+k': () => {
|
||||
const {activeWorkspace} = this.props;
|
||||
toggleModal(CookiesModal, activeWorkspace);
|
||||
trackEvent('HotKey', 'Cookies');
|
||||
},
|
||||
|
||||
// Request Create
|
||||
@@ -91,6 +94,7 @@ class App extends Component {
|
||||
|
||||
const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id;
|
||||
this._requestCreate(parentId);
|
||||
trackEvent('HotKey', 'Request Create');
|
||||
},
|
||||
|
||||
// Request Duplicate
|
||||
@@ -102,7 +106,8 @@ class App extends Component {
|
||||
}
|
||||
|
||||
const request = await models.request.duplicate(activeRequest);
|
||||
handleSetActiveRequest(activeWorkspace._id, request._id)
|
||||
handleSetActiveRequest(activeWorkspace._id, request._id);
|
||||
trackEvent('HotKey', 'Request Duplicate');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,10 +165,12 @@ class App extends Component {
|
||||
}
|
||||
|
||||
_startDragSidebar () {
|
||||
trackEvent('Sidebar', 'Drag Start');
|
||||
this.setState({draggingSidebar: true})
|
||||
}
|
||||
|
||||
_resetDragSidebar () {
|
||||
trackEvent('Sidebar', 'Drag Reset');
|
||||
// TODO: Remove setTimeout need be not triggering drag on double click
|
||||
setTimeout(() => {
|
||||
const {handleSetSidebarWidth, activeWorkspace} = this.props;
|
||||
@@ -172,10 +179,12 @@ class App extends Component {
|
||||
}
|
||||
|
||||
_startDragPane () {
|
||||
trackEvent('App Pane', 'Drag Start');
|
||||
this.setState({draggingPane: true})
|
||||
}
|
||||
|
||||
_resetDragPane () {
|
||||
trackEvent('App Pane', 'Reset');
|
||||
// TODO: Remove setTimeout need be not triggering drag on double click
|
||||
setTimeout(() => {
|
||||
const {handleSetPaneWidth, activeWorkspace} = this.props;
|
||||
@@ -217,6 +226,7 @@ class App extends Component {
|
||||
_handleToggleSidebar () {
|
||||
const {activeWorkspace, sidebarHidden, handleSetSidebarHidden} = this.props;
|
||||
handleSetSidebarHidden(activeWorkspace._id, !sidebarHidden);
|
||||
trackEvent('Sidebar', 'Toggle Visibility', !sidebarHidden ? 'Hide' : 'Show');
|
||||
}
|
||||
|
||||
_forceHardRefresh () {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
.key-value-editor__row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.5fr) minmax(0, 0.5fr) auto auto;
|
||||
grid-template-columns: minmax(0, 0.5fr) minmax(0, 0.5fr) auto auto auto;
|
||||
grid-template-rows: auto;
|
||||
|
||||
&.key-value-editor__row--disabled input {
|
||||
@@ -32,7 +32,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
& > button {
|
||||
& > button,
|
||||
.dropdown > button {
|
||||
color: @hl;
|
||||
|
||||
&:hover,
|
||||
@@ -40,7 +41,8 @@
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
"json-loader": "^0.5.4",
|
||||
"less": "^2.7.1",
|
||||
"less-loader": "^2.2.3",
|
||||
"nock": "^8.0.0",
|
||||
"nock": "^9.0.2",
|
||||
"react-addons-perf": "^15.3.0",
|
||||
"react-addons-test-utils": "^15.1.0",
|
||||
"react-hot-loader": "^1.3.0",
|
||||
|
||||
Reference in New Issue
Block a user