Switch Request body to be object instead of string (#47)

* Basic import summary

* Started (broken)
This commit is contained in:
Gregory Schier
2016-11-22 11:42:10 -08:00
committed by GitHub
parent 5f7c2ee18f
commit 5d5d6eecc4
45 changed files with 841 additions and 387 deletions

View File

@@ -96,7 +96,7 @@ describe('requestCreate()', () => {
};
const r = await models.request.create(patch);
expect(Object.keys(r).length).toBe(13);
expect(Object.keys(r).length).toBe(14);
expect(r._id).toMatch(/^req_[a-zA-Z0-9]{32}$/);
expect(r.created).toBeGreaterThanOrEqual(now);
@@ -105,7 +105,7 @@ describe('requestCreate()', () => {
expect(r.name).toBe('My Request');
expect(r.url).toBe('');
expect(r.method).toBe('GET');
expect(r.body).toBe('');
expect(r.body).toEqual({});
expect(r.parameters).toEqual([]);
expect(r.headers).toEqual([]);
expect(r.authentication).toEqual({});

View File

@@ -29,7 +29,9 @@ describe('exportHarWithRequest()', () => {
headers: [{name: 'Content-Type', value: 'application/json'}],
parameters: [{name: 'foo bar', value: 'hello&world'}],
method: 'POST',
body: 'foo bar',
body: {
text: 'foo bar'
},
url: 'http://google.com',
authentication: {
username: 'user',
@@ -62,7 +64,9 @@ describe('exportHarWithRequest()', () => {
headersSize: -1,
httpVersion: 'HTTP/1.1',
method: 'POST',
postData: {text: 'foo bar'},
postData: {
text: 'foo bar'
},
queryString: [{name: 'foo bar', value: 'hello&world'}],
url: 'http://google.com/'
});

View File

@@ -208,23 +208,3 @@ describe('debounce()', () => {
expect(resultList).toEqual([['foo', 'bar3']]);
})
});
describe('copyObjectAndUpdate()', () => {
it('handles assignment', () => {
const actual = misc.copyObjectAndUpdate(
{foo: 'hi', bar: {baz: 'qux'}},
{foo: 'hi again', a: 'b'},
{a: 'c', foo: 'final foo'},
);
expect(actual).toEqual({
foo: 'final foo', bar: {baz: 'qux'}
});
});
it('makes a copy of the object', () => {
const obj = {foo: 'bar'};
const newObj = misc.copyObjectAndUpdate(obj, {hi: 'there'});
expect(newObj).toBe(newObj);
expect(newObj).not.toBe(obj);
});
});

View File

@@ -3,6 +3,7 @@ import * as db from '../database';
import nock from 'nock';
import {getRenderedRequest} from '../render';
import * as models from '../../models';
import {CONTENT_TYPE_FORM_URLENCODED} from '../constants';
describe('buildRequestConfig()', () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
@@ -23,7 +24,7 @@ describe('buildRequestConfig()', () => {
forever: true,
gzip: true,
headers: {host: ''},
maxRedirects: 20,
maxRedirects: 50,
method: 'GET',
proxy: null,
rejectUnauthorized: true,
@@ -37,12 +38,28 @@ describe('buildRequestConfig()', () => {
const workspace = await models.workspace.create();
const request = Object.assign(models.request.init(), {
parentId: workspace._id,
headers: [{host: '', name: 'Content-Type', value: 'application/json'}],
parameters: [{name: 'foo bar', value: 'hello&world'}],
headers: [
{name: 'Content-Type', value: 'application/json', disabled: false},
{name: 'hi', value: 'there', disabled: true},
{name: 'x-hello', value: 'world'},
],
parameters: [
{name: 'foo bar', value: 'hello&world', disabled: false},
{name: 'b', value: 'bb&world', disabled: true},
{name: 'a', value: 'aa'},
],
method: 'POST',
body: 'foo=bar',
body: {
mimeType: CONTENT_TYPE_FORM_URLENCODED,
params: [
{name: 'X', value: 'XX', disabled: false},
{name: 'Y', value: 'YY', disabled: true},
{name: 'Z', value: 'ZZ'},
]
},
url: 'http://foo.com:3332/★/hi@gmail.com/foo%20bar?bar=baz',
authentication: {
disabled: false,
username: 'user',
password: 'pass'
}
@@ -51,7 +68,7 @@ describe('buildRequestConfig()', () => {
const renderedRequest = await getRenderedRequest(request);
const config = networkUtils._buildRequestConfig(renderedRequest);
expect(config).toEqual({
body: 'foo=bar',
body: 'X=XX&Z=ZZ',
encoding: null,
followAllRedirects: true,
followRedirect: true,
@@ -60,15 +77,16 @@ describe('buildRequestConfig()', () => {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic dXNlcjpwYXNz',
'host': 'foo.com:3332'
'host': 'foo.com:3332',
'x-hello': 'world'
},
maxRedirects: 20,
maxRedirects: 50,
method: 'POST',
proxy: null,
rejectUnauthorized: true,
time: true,
timeout: 0,
url: 'http://foo.com:3332/%E2%98%85/hi@gmail.com/foo%20bar?bar=baz&foo%20bar=hello%26world'
url: 'http://foo.com:3332/%E2%98%85/hi@gmail.com/foo%20bar?bar=baz&foo%20bar=hello%26world&a=aa'
})
})
});
@@ -121,7 +139,10 @@ describe('actuallySend()', () => {
headers: [{name: 'Content-Type', value: 'application/json'}],
parameters: [{name: 'foo bar', value: 'hello&world'}],
method: 'POST',
body: 'foo=bar',
body: {
mimeType: CONTENT_TYPE_FORM_URLENCODED,
text: 'foo=bar'
},
url: 'http://localhost',
authentication: {
username: 'user',

View File

@@ -115,17 +115,22 @@ export const CONTENT_TYPE_JSON = 'application/json';
export const CONTENT_TYPE_XML = 'application/xml';
export const CONTENT_TYPE_TEXT = 'text/plain';
export const CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded';
export const CONTENT_TYPE_FORM_DATA = 'multipart/form-data';
export const CONTENT_TYPE_OTHER = '';
const contentTypeMap = {
export const contentTypesMap = {
[CONTENT_TYPE_JSON]: 'JSON',
[CONTENT_TYPE_XML]: 'XML',
[CONTENT_TYPE_FORM_URLENCODED]: 'Form Encoded',
// [CONTENT_TYPE_FORM_DATA]: 'Form Data',
[CONTENT_TYPE_FORM_URLENCODED]: 'Url Encoded',
[CONTENT_TYPE_TEXT]: 'Plain Text',
[CONTENT_TYPE_OTHER]: 'Other'
[CONTENT_TYPE_OTHER]: 'Other',
};
export const CONTENT_TYPES = Object.keys(contentTypeMap);
export const BODY_TYPE_RAW = 'raw';
export const BODY_TYPE_FILE = 'file';
export const BODY_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded';
export const BODY_TYPE_FORM = 'multipart/form-data';
/**
* Get the friendly name for a given content type
@@ -134,10 +139,14 @@ export const CONTENT_TYPES = Object.keys(contentTypeMap);
* @returns {*|string}
*/
export function getContentTypeName (contentType) {
return contentTypeMap[contentType] || contentTypeMap[CONTENT_TYPE_OTHER];
return contentTypesMap[contentType] || contentTypesMap[CONTENT_TYPE_OTHER];
}
export function getContentTypeFromHeaders (headers) {
if (!Array.isArray(headers)) {
return null;
}
const header = headers.find(({name}) => name.toLowerCase() === 'content-type');
return header ? header.value : null;
}

View File

@@ -4,7 +4,6 @@ import fsPath from 'path';
import {DB_PERSIST_INTERVAL} from './constants';
import {generateId} from './misc';
import {getModel, initModel} from '../models';
import * as misc from './misc';
export const CHANGE_INSERT = 'insert';
export const CHANGE_UPDATE = 'update';
@@ -128,7 +127,7 @@ export function find (type, query = {}) {
}
const docs = rawDocs.map(rawDoc => {
return Object.assign(initModel(type), rawDoc);
return initModel(type, rawDoc);
});
resolve(docs);
@@ -152,8 +151,7 @@ export function getWhere (type, query) {
return resolve(null);
}
const doc = Object.assign(initModel(type), rawDocs[0]);
resolve(doc);
resolve(initModel(type, rawDocs[0]));
})
})
}
@@ -245,8 +243,8 @@ export function removeBulkSilently (type, query) {
// ~~~~~~~~~~~~~~~~~~~ //
export function docUpdate (originalDoc, patch = {}) {
const doc = misc.copyObjectAndUpdate(
initModel(originalDoc.type),
const doc = initModel(
originalDoc.type,
originalDoc,
patch,
{modified: Date.now()},
@@ -262,8 +260,8 @@ export function docCreate (type, patch = {}) {
throw new Error(`No ID prefix for ${type}`)
}
const doc = misc.copyObjectAndUpdate(
initModel(type),
const doc = initModel(
type,
{_id: generateId(idPrefix)},
patch,

View File

@@ -1,21 +1,26 @@
import * as models from '../models';
import {getRenderedRequest} from './render';
import {getRenderedRequest} from './render';
import {jarFromCookies} from './cookies';
import * as util from './misc';
import * as misc from './misc';
export function exportHarWithRequest (renderedRequest, addContentLength = false) {
if (addContentLength) {
const hasContentLengthHeader = !!renderedRequest.headers.find(
h => h.name.toLowerCase() === 'content-length'
);
const hasContentLengthHeader = misc.filterHeaders(
renderedRequest.headers,
'content-length'
).length > 0;
if (!hasContentLengthHeader) {
const name = 'content-length';
const value = Buffer.byteLength(renderedRequest.body).toString();
const value = Buffer.byteLength(body).toString();
renderedRequest.headers.push({name, value})
}
}
// Luckily, Insomnia uses the same body format as HAR :)
const postData = renderedRequest.body;
return {
method: renderedRequest.method,
url: util.prepareUrlForSending(renderedRequest.url),
@@ -23,7 +28,7 @@ export function exportHarWithRequest (renderedRequest, addContentLength = false)
cookies: getCookies(renderedRequest),
headers: renderedRequest.headers,
queryString: renderedRequest.parameters,
postData: {text: renderedRequest.body},
postData: postData,
headersSize: -1,
bodySize: -1
};

View File

@@ -41,6 +41,14 @@ export async function importRaw (workspace, rawContent, generateNewIds = false)
// Also always replace __WORKSPACE_ID__ with the current workspace if we see it
generatedIds['__WORKSPACE_ID__'] = workspace._id;
// Import everything backwards so they get inserted in the correct order
data.resources.reverse();
const importedDocs = {};
for (const model of models.all()) {
importedDocs[model.type] = [];
}
for (const resource of data.resources) {
// Buffer DB changes
// NOTE: Doing it inside here so it's more "scalable"
@@ -67,15 +75,16 @@ export async function importRaw (workspace, rawContent, generateNewIds = false)
}
const doc = await model.getById(resource._id);
const newDoc = doc ?
await model.update(doc, resource) :
await model.create(resource);
if (doc) {
await model.update(doc, resource)
} else {
await model.create(resource)
}
importedDocs[newDoc.type].push(newDoc);
}
db.flushChanges();
return importedDocs;
}
export async function exportJSON (parentDoc = null) {

View File

@@ -38,6 +38,11 @@ export function getContentTypeHeader (headers) {
return matches.length ? matches[0] : null;
}
export function getContentLengthHeader (headers) {
const matches = filterHeaders(headers, 'content-length');
return matches.length ? matches[0] : null;
}
export function setDefaultProtocol (url, defaultProto = 'http:') {
// Default the proto if it doesn't exist
if (url.indexOf('://') === -1) {
@@ -149,17 +154,3 @@ export function debounce (callback, millis = DEBOUNCE_MILLIS) {
callback.apply(null, results['__key__'])
}, millis).bind(null, '__key__');
}
/** Same as Object.assign but only add fields that exist on target */
export function copyObjectAndUpdate (target, ...sources) {
// Make a new object so we don't mutate the input
const newObject = Object.assign({}, target, ...sources);
for (const key of Object.keys(newObject)) {
if (!target.hasOwnProperty(key)) {
delete newObject[key];
}
}
return newObject;
}

View File

@@ -2,13 +2,13 @@ import networkRequest from 'request';
import {parse as urlParse} from 'url';
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} from './constants';
import {jarFromCookies, cookiesFromJar} from './cookies';
import {DEBOUNCE_MILLIS, STATUS_CODE_PEBKAC, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED} from './constants';
import {jarFromCookies, cookiesFromJar, cookieHeaderValueForUri} from './cookies';
import {setDefaultProtocol} from './misc';
import {getRenderedRequest} from './render';
import {swapHost} from './dns';
import {cookieHeaderValueForUri} from './cookies';
let cancelRequestFunction = null;
@@ -20,14 +20,10 @@ export function cancelCurrentRequest () {
export function _buildRequestConfig (renderedRequest, patch = {}) {
const config = {
method: renderedRequest.method,
body: renderedRequest.body,
headers: {},
// Setup redirect rules
followAllRedirects: true,
followRedirect: true,
maxRedirects: 20,
maxRedirects: 50, // Arbitrary (large) number
timeout: 0,
// Unzip gzipped responses
@@ -49,19 +45,34 @@ export function _buildRequestConfig (renderedRequest, patch = {}) {
encoding: null,
};
// Set the body
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
} else {
config.body = renderedRequest.body.text || '';
}
// Set the method
config.method = renderedRequest.method;
// Set the headers
const headers = {};
for (let i = 0; i < renderedRequest.headers.length; i++) {
let header = renderedRequest.headers[i];
if (header.name) {
headers[header.name] = header.value;
}
}
config.headers = headers;
// Set the URL, including the query parameters
const qs = querystring.buildFromParams(renderedRequest.parameters);
const url = querystring.joinURL(renderedRequest.url, qs);
config.url = util.prepareUrlForSending(url);
config.headers.host = urlParse(config.url).host;
for (let i = 0; i < renderedRequest.headers.length; i++) {
let header = renderedRequest.headers[i];
if (header.name) {
config.headers[header.name] = header.value;
}
}
return Object.assign(config, patch);
}

View File

@@ -60,6 +60,10 @@ export function buildFromParams (parameters, strict = true) {
* @param strict allow empty names and values
*/
export function deconstructToParams (qs, strict = true) {
if (qs === '') {
return [];
}
const stringPairs = qs.split('&');
const pairs = [];

View File

@@ -129,6 +129,22 @@ export async function getRenderedRequest (request, environmentId) {
// Render all request properties
const renderedRequest = recursiveRender(request, renderContext);
// Remove disabled params
renderedRequest.parameters = renderedRequest.parameters.filter(p => !p.disabled);
// Remove disabled headers
renderedRequest.headers = renderedRequest.headers.filter(p => !p.disabled);
// Remove disabled body params
if (renderedRequest.body && Array.isArray(renderedRequest.body.params)) {
renderedRequest.body.params = renderedRequest.body.params.filter(p => !p.disabled);
}
// Remove disabled authentication
if (renderedRequest.authentication && renderedRequest.authentication.disabled) {
renderedRequest.authentication = {}
}
// Default the proto if it doesn't exist
renderedRequest.url = setDefaultProtocol(renderedRequest.url);

View File

@@ -8,6 +8,7 @@ import path from 'path';
import electron from 'electron';
import * as packageJSON from './package.json';
import LocalStorage from './common/LocalStorage';
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer';
// Some useful helpers
const IS_DEV = process.env.INSOMNIA_ENV === 'development';
@@ -476,6 +477,10 @@ function createWindow () {
];
if (IS_DEV) {
installExtension(REACT_DEVELOPER_TOOLS)
.then((name) => console.log(`Added Extension: ${name}`))
.catch((err) => console.log('An error occurred: ', err));
template.push({
label: 'Developer',
position: 'before=help',

View File

@@ -1,15 +1,18 @@
import * as db from '../../common/database';
import * as models from '../../models';
import * as requestModel from '../../models/request';
import {types as allModelTypes} from '../../models';
describe('init()', () => {
beforeEach(() => {
return db.init(models.types(), {inMemoryOnly: true}, true);
return db.init(allModelTypes(), {inMemoryOnly: true}, true);
});
it('contains all required fields', async () => {
Date.now = jest.genMockFunction().mockReturnValue(1478795580200);
expect(models.request.init()).toEqual({
expect(requestModel.init()).toEqual({
_schema: 1,
authentication: {},
body: '',
body: {},
headers: [],
metaSortKey: -1478795580200,
method: 'GET',
@@ -22,21 +25,22 @@ describe('init()', () => {
describe('create()', async () => {
beforeEach(() => {
return db.init(models.types(), {inMemoryOnly: true}, true);
return db.init(allModelTypes(), {inMemoryOnly: true}, true);
});
it('creates a valid request', async () => {
Date.now = jest.genMockFunction().mockReturnValue(1478795580200);
const request = await models.request.create({name: 'Test Request', parentId: 'fld_124'});
const request = await requestModel.create({name: 'Test Request', parentId: 'fld_124'});
const expected = {
_id: 'req_dd2ccc1a2745477a881a9e8ef9d42403',
_schema: 1,
created: 1478795580200,
modified: 1478795580200,
parentId: 'fld_124',
type: 'Request',
authentication: {},
body: '',
body: {},
headers: [],
metaSortKey: -1478795580200,
method: 'GET',
@@ -46,30 +50,30 @@ describe('create()', async () => {
};
expect(request).toEqual(expected);
expect(await models.request.getById(expected._id)).toEqual(expected);
expect(await requestModel.getById(expected._id)).toEqual(expected);
});
it('fails when missing parentId', async () => {
Date.now = jest.genMockFunction().mockReturnValue(1478795580200);
expect(() => models.request.create({name: 'Test Request'})).toThrow('New Requests missing `parentId`')
expect(() => requestModel.create({name: 'Test Request'})).toThrow('New Requests missing `parentId`')
});
});
describe('updateContentType()', async () => {
describe('updateMimeType()', async () => {
beforeEach(() => {
return db.init(models.types(), {inMemoryOnly: true}, true);
return db.init(allModelTypes(), {inMemoryOnly: true}, true);
});
it('adds header when does not exist', async () => {
const request = await models.request.create({name: 'My Request', parentId: 'fld_1'});
const request = await requestModel.create({name: 'My Request', parentId: 'fld_1'});
expect(request).not.toBeNull();
const newRequest = await models.request.updateContentType(request, 'text/html');
const newRequest = await requestModel.updateMimeType(request, 'text/html');
expect(newRequest.headers).toEqual([{name: 'Content-Type', value: 'text/html'}]);
});
it('replaces header when exists', async () => {
const request = await models.request.create({
const request = await requestModel.create({
name: 'My Request',
parentId: 'fld_1',
headers: [
@@ -81,7 +85,7 @@ describe('updateContentType()', async () => {
});
expect(request).not.toBeNull();
const newRequest = await models.request.updateContentType(request, 'text/html');
const newRequest = await requestModel.updateMimeType(request, 'text/html');
expect(newRequest.headers).toEqual([
{name: 'content-tYPE', value: 'text/html'},
{name: 'foo', value: 'bar'},
@@ -91,27 +95,155 @@ describe('updateContentType()', async () => {
});
it('replaces header when exists', async () => {
const request = await models.request.create({
const request = await requestModel.create({
name: 'My Request',
parentId: 'fld_1',
headers: [{name: 'content-tYPE', value: 'application/json'}]
});
expect(request).not.toBeNull();
const newRequest = await models.request.updateContentType(request, 'text/html');
const newRequest = await requestModel.updateMimeType(request, 'text/html');
expect(newRequest.headers).toEqual([{name: 'content-tYPE', value: 'text/html'}]);
});
it('removes content-type', async () => {
const request = await models.request.create({
const request = await requestModel.create({
name: 'My Request',
parentId: 'fld_1',
headers: [{name: 'content-tYPE', value: 'application/json'}]
});
expect(request).not.toBeNull();
const newRequest = await models.request.updateContentType(request, null);
const newRequest = await requestModel.updateMimeType(request, null);
expect(newRequest.headers).toEqual([]);
});
});
describe('migrate()', () => {
it('migrates basic case', () => {
const original = {
headers: [],
body: 'hello world!'
};
const expected = {
_schema: 1,
headers: [],
body: {text: 'hello world!'}
};
expect(requestModel.migrate(original)).toEqual(expected);
});
it('migrates form-urlencoded', () => {
const original = {
headers: [{name: 'content-type', value: 'application/x-www-form-urlencoded'}],
body: 'foo=bar&baz={{ hello }}'
};
const expected = {
_schema: 1,
headers: [{name: 'content-type', value: 'application/x-www-form-urlencoded'}],
body: {
mimeType: 'application/x-www-form-urlencoded',
params: [
{name: 'foo', value: 'bar'},
{name: 'baz', value: '{{ hello }}'}
]
}
};
expect(requestModel.migrate(original)).toEqual(expected);
});
it('migrates form-urlencoded with charset', () => {
const original = {
headers: [{name: 'content-type', value: 'application/x-www-form-urlencoded; charset=utf-8'}],
body: 'foo=bar&baz={{ hello }}'
};
const expected = {
_schema: 1,
headers: [{name: 'content-type', value: 'application/x-www-form-urlencoded; charset=utf-8'}],
body: {
mimeType: 'application/x-www-form-urlencoded',
params: [
{name: 'foo', value: 'bar'},
{name: 'baz', value: '{{ hello }}'}
]
}
};
expect(requestModel.migrate(original)).toEqual(expected);
});
it('migrates form-urlencoded malformed', () => {
const original = {
headers: [{name: 'content-type', value: 'application/x-www-form-urlencoded'}],
body: '{"foo": "bar"}'
};
const expected = {
_schema: 1,
headers: [{name: 'content-type', value: 'application/x-www-form-urlencoded'}],
body: {
mimeType: 'application/x-www-form-urlencoded',
params: [
{name: '{"foo": "bar"}', value: ''}
]
}
};
expect(requestModel.migrate(original)).toEqual(expected);
});
it('migrates mime-type', () => {
const contentToMimeMap = {
'application/json; charset=utf-8': 'application/json',
'text/plain': 'text/plain',
'malformed': 'malformed'
};
for (const contentType of Object.keys(contentToMimeMap)) {
const original = {
headers: [{name: 'content-type', value: contentType}],
body: ''
};
const expected = {
_schema: 1,
headers: [{name: 'content-type', value: contentType}],
body: {mimeType: contentToMimeMap[contentType], text: ''}
};
expect(requestModel.migrate(original)).toEqual(expected);
}
});
it('skips migrate for schema 1', () => {
const original = {
_schema: 1,
body: {mimeType: 'text/plain', text: 'foo'}
};
expect(requestModel.migrate(original)).toBe(original);
});
it('migrates with no schema and schema < 1', () => {
const withoutSchema = {body: 'foo bar!'};
const withSchema = {_schema: 0, body: 'foo bar!'};
const withWeirdSchema = {_schema: -4, body: 'foo bar!'};
const expected = {
_schema: 1,
body: {
text: 'foo bar!'
}
};
expect(requestModel.migrate(withSchema)).toEqual(expected);
expect(requestModel.migrate(withoutSchema)).toEqual(expected);
expect(requestModel.migrate(withWeirdSchema)).toEqual(expected);
});
});

View File

@@ -1,5 +1,6 @@
import * as db from '../common/database';
export const name = 'Cookie Jar';
export const type = 'CookieJar';
export const prefix = 'jar';
export function init () {
@@ -9,6 +10,10 @@ export function init () {
}
}
export function migrate (doc) {
return doc;
}
export function create (patch = {}) {
return db.docCreate(type, patch);
}

View File

@@ -1,5 +1,6 @@
import * as db from '../common/database';
export const name = 'Environment';
export const type = 'Environment';
export const prefix = 'env';
export function init () {
@@ -9,6 +10,10 @@ export function init () {
}
}
export function migrate (doc) {
return doc;
}
export function create (patch = {}) {
if (!patch.parentId) {
throw new Error('New Environment missing `parentId`', patch);

View File

@@ -40,16 +40,46 @@ export function getModel (type) {
return _models[type] || null;
}
export function initModel (type) {
const baseDefaults = {
export function getModelName (type, count = 1) {
const model = getModel(type);
if (!model) {
return 'Unknown';
} else if (count === 1) {
return model.name;
} else if (!model.name.match(/s$/)) {
// Add an 's' if it doesn't already end in one
return `${model.name}s`;
} else {
return model.name
}
}
export function initModel (type, ...sources) {
const model = getModel(type);
// Define global default fields
const objectDefaults = Object.assign({
type: type,
_id: null,
_schema: 0,
parentId: null,
modified: Date.now(),
created: Date.now(),
};
}, model.init());
const modelDefaults = getModel(type).init();
// Make a new object
const fullObject = Object.assign({}, objectDefaults, ...sources);
return Object.assign(baseDefaults, modelDefaults);
// Migrate the model
// NOTE: Do migration before pruning because we might need to look at those fields
const migratedObject = model.migrate(fullObject);
// Prune extra keys from doc
for (const key of Object.keys(migratedObject)) {
if (!objectDefaults.hasOwnProperty(key)) {
delete migratedObject[key];
}
}
return migratedObject;
}

View File

@@ -1,16 +1,20 @@
import {METHOD_GET} from '../common/constants';
import {METHOD_GET, getContentTypeFromHeaders, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_FORM_DATA} from '../common/constants';
import * as db from '../common/database';
import {getContentTypeHeader} from '../common/misc';
import {deconstructToParams} from '../common/querystring';
import {CONTENT_TYPE_JSON} from '../common/constants';
export const name = 'Request';
export const type = 'Request';
export const prefix = 'req';
export function init () {
return {
_schema: 1,
url: '',
name: 'New Request',
method: METHOD_GET,
body: '',
body: {},
parameters: [],
headers: [],
authentication: {},
@@ -18,6 +22,40 @@ export function init () {
};
}
export function newBodyRaw (rawBody, contentType) {
if (!contentType) {
return {text: rawBody};
}
const mimeType = contentType.split(';')[0];
return {mimeType, text: rawBody};
}
export function newBodyFormUrlEncoded (parameters) {
return {
mimeType: CONTENT_TYPE_FORM_URLENCODED,
params: parameters
}
}
export function newBodyForm (parameters) {
return {
mimeType: CONTENT_TYPE_FORM_DATA,
params: parameters
}
}
export function migrate (doc) {
const schema = doc._schema || 0;
if (schema <= 0) {
doc = migrateTo1(doc);
doc._schema = 1;
}
return doc;
}
export function create (patch = {}) {
if (!patch.parentId) {
throw new Error('New Requests missing `parentId`', patch);
@@ -38,17 +76,31 @@ export function update (request, patch) {
return db.docUpdate(request, patch);
}
export function updateContentType (request, contentType) {
export function updateMimeType (request, mimeType) {
let headers = [...request.headers];
const contentTypeHeader = getContentTypeHeader(headers);
if (!contentType) {
// 1. Update Content-Type header
if (!mimeType) {
// Remove the contentType header if we are un-setting it
headers = headers.filter(h => h !== contentTypeHeader);
} else if (contentTypeHeader) {
contentTypeHeader.value = contentType;
contentTypeHeader.value = mimeType;
} else {
headers.push({name: 'Content-Type', value: contentType})
headers.push({name: 'Content-Type', value: 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 || []);
} 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 || '');
} else {
request.body = newBodyRaw(request.body.text || '', mimeType);
}
return update(request, {headers});
@@ -67,3 +119,24 @@ export function remove (request) {
export function all () {
return db.all(type);
}
// ~~~~~~~~~~ //
// Migrations //
// ~~~~~~~~~~ //
function migrateTo1 (request) {
// Second, convert all existing urlencoded bodies to new format
const contentType = getContentTypeFromHeaders(request.headers) || '';
const wasFormUrlEncoded = !!contentType.match(/^application\/x-www-form-urlencoded/i);
if (wasFormUrlEncoded) {
// Convert old-style form-encoded request bodies to new style
const params = deconstructToParams(request.body, false);
request.body = newBodyFormUrlEncoded(params);
} else {
request.body = newBodyRaw(request.body, contentType);
}
return request;
}

View File

@@ -1,7 +1,9 @@
import * as db from '../common/database';
export const name = 'Folder';
export const type = 'RequestGroup';
export const prefix = 'fld';
export function init () {
return {
name: 'New Folder',
@@ -10,6 +12,10 @@ export function init () {
}
}
export function migrate (doc) {
return doc;
}
export function create (patch = {}) {
if (!patch.parentId) {
throw new Error('New Requests missing `parentId`', patch);

View File

@@ -1,7 +1,9 @@
import * as db from '../common/database';
export const name = 'Response';
export const type = 'Response';
export const prefix = 'res';
export function init () {
return {
statusCode: 0,
@@ -18,6 +20,10 @@ export function init () {
}
}
export function migrate (doc) {
return doc;
}
export function create (patch = {}) {
if (!patch.parentId) {
throw new Error('New Response missing `parentId`');

View File

@@ -1,7 +1,9 @@
import * as db from '../common/database';
export const name = 'Settings';
export const type = 'Settings';
export const prefix = 'set';
export function init () {
return {
showPasswords: true,
@@ -17,6 +19,10 @@ export function init () {
};
}
export function migrate (doc) {
return doc;
}
export async function all () {
const settings = await db.all(type);
if (settings.length === 0) {

View File

@@ -1,7 +1,9 @@
import * as db from '../common/database';
export const name = 'Stats';
export const type = 'Stats';
export const prefix = 'sta';
export function init () {
return {
lastLaunch: Date.now(),
@@ -10,6 +12,10 @@ export function init () {
};
}
export function migrate (doc) {
return doc;
}
export function create (patch = {}) {
return db.docCreate(type, patch);
}

View File

@@ -1,13 +1,19 @@
import * as db from '../common/database';
export const name = 'Workspace';
export const type = 'Workspace';
export const prefix = 'wrk';
export function init () {
return {
name: 'New Workspace',
};
}
export function migrate (doc) {
return doc;
}
export function getById (id) {
return db.get(type, id);
}

View File

@@ -625,7 +625,7 @@ async function _getOrCreateAllActiveResources (resourceGroupId = null) {
try {
activeResourceMap[doc._id] = await _createResourceForDoc(doc);
} catch (e) {
logger.error(`Failed to create resource for ${doc._id}`, doc);
logger.error(`Failed to create resource for ${doc._id}`, e, {doc});
}
}
}

View File

@@ -13,14 +13,14 @@ class RenderedQueryString extends Component {
}
_update (props, delay = false) {
clearTimeout(this._askTimeout);
this._askTimeout = setTimeout(async () => {
clearTimeout(this._triggerTimeout);
this._triggerTimeout = setTimeout(async () => {
const {request, environmentId} = props;
const {url, parameters} = await getRenderedRequest(request, environmentId);
const qs = querystring.buildFromParams(parameters);
const fullUrl = querystring.joinURL(url, qs);
this.setState({string: util.prepareUrlForSending(fullUrl)});
}, delay ? 300 : 0);
}, delay ? 200 : 0);
}
componentDidMount () {
@@ -28,7 +28,7 @@ class RenderedQueryString extends Component {
}
componentWillUnmount () {
clearTimeout(this._askTimeout);
clearTimeout(this._triggerTimeout);
}
componentWillReceiveProps (nextProps) {

View File

@@ -4,7 +4,7 @@ import KeyValueEditor from './base/KeyValueEditor';
import RequestHeadersEditor from './editors/RequestHeadersEditor';
import ContentTypeDropdown from './dropdowns/ContentTypeDropdown';
import RenderedQueryString from './RenderedQueryString';
import BodyEditor from './editors/BodyEditor';
import BodyEditor from './editors/body/BodyEditor';
import AuthEditor from './editors/AuthEditor';
import RequestUrlBar from './RequestUrlBar.js';
import {MOD_SYM, getContentTypeName, getContentTypeFromHeaders} from '../../common/constants';
@@ -28,7 +28,7 @@ class RequestPane extends Component {
updateRequestParameters,
updateRequestAuthentication,
updateRequestHeaders,
updateRequestContentType,
updateRequestMimeType,
updateSettingsUseBulkHeaderEditor
} = this.props;
@@ -102,7 +102,7 @@ class RequestPane extends Component {
{getContentTypeName(getContentTypeFromHeaders(request.headers))}
</button>
<ContentTypeDropdown
updateRequestContentType={updateRequestContentType}/>
updateRequestMimeType={updateRequestMimeType}/>
</Tab>
<Tab>
<button>
@@ -197,7 +197,7 @@ RequestPane.propTypes = {
updateRequestParameters: PropTypes.func.isRequired,
updateRequestAuthentication: PropTypes.func.isRequired,
updateRequestHeaders: PropTypes.func.isRequired,
updateRequestContentType: PropTypes.func.isRequired,
updateRequestMimeType: PropTypes.func.isRequired,
updateSettingsShowPasswords: PropTypes.func.isRequired,
updateSettingsUseBulkHeaderEditor: PropTypes.func.isRequired,
handleImportFile: PropTypes.func.isRequired,

View File

@@ -19,6 +19,7 @@ import Sidebar from './sidebar/Sidebar';
import RequestPane from './RequestPane';
import ResponsePane from './ResponsePane';
import * as models from '../../models/index';
import {updateMimeType} from '../../models/request';
const Wrapper = props => {
@@ -123,7 +124,7 @@ const Wrapper = props => {
updateRequestParameters={parameters => models.request.update(activeRequest, {parameters})}
updateRequestAuthentication={authentication => models.request.update(activeRequest, {authentication})}
updateRequestHeaders={headers => models.request.update(activeRequest, {headers})}
updateRequestContentType={contentType => models.request.updateContentType(activeRequest, contentType)}
updateRequestMimeType={mimeType => updateMimeType(activeRequest, mimeType)}
updateSettingsShowPasswords={showPasswords => models.settings.update(settings, {showPasswords})}
updateSettingsUseBulkHeaderEditor={useBulkHeaderEditor => models.settings.update(settings, {useBulkHeaderEditor})}
handleSend={handleSendRequestWithEnvironment.bind(

View File

@@ -15,13 +15,13 @@ class CopyButton extends Component {
this.setState({showConfirmation: true});
this._askTimeout = setTimeout(() => {
this._triggerTimeout = setTimeout(() => {
this.setState({showConfirmation: false});
}, 2000);
}
componentWillUnmount() {
clearTimeout(this._askTimeout);
clearTimeout(this._triggerTimeout);
}
render () {

View File

@@ -1,6 +1,7 @@
import React, {Component, PropTypes} from 'react';
import classnames from 'classnames';
import {DEBOUNCE_MILLIS} from '../../../common/constants';
import PromptButton from '../base/PromptButton';
const NAME = 'name';
const VALUE = 'value';
@@ -17,6 +18,9 @@ class KeyValueEditor extends Component {
this._focusedPair = -1;
this._focusedField = NAME;
this._nameInputs = {};
this._valueInputs = {};
this._focusedInput = null;
this.state = {
pairs: props.pairs
@@ -24,8 +28,8 @@ class KeyValueEditor extends Component {
}
_onChange (pairs, updateState = true) {
clearTimeout(this._askTimeout);
this._askTimeout = setTimeout(() => this.props.onChange(pairs), DEBOUNCE_MILLIS);
clearTimeout(this._triggerTimeout);
this._triggerTimeout = setTimeout(() => this.props.onChange(pairs), DEBOUNCE_MILLIS);
updateState && this.setState({pairs});
}
@@ -63,6 +67,13 @@ class KeyValueEditor extends Component {
this._onChange(pairs);
}
_togglePair (position) {
const pairs = this.state.pairs.map(
(p, i) => i == position ? Object.assign({}, p, {disabled: !p.disabled}) : p
);
this._onChange(pairs, true);
}
_focusNext (addIfValue = false) {
if (this._focusedField === NAME) {
this._focusedField = VALUE;
@@ -136,25 +147,21 @@ class KeyValueEditor extends Component {
}
_updateFocus () {
const refName = `${this._focusedPair}.${this._focusedField}`;
const ref = this.refs[refName];
if (ref) {
ref.focus();
// Focus at the end of the text
ref.selectionStart = ref.selectionEnd = ref.value.length;
let ref;
if (this._focusedField === NAME) {
ref = this._nameInputs[this._focusedPair];
} else {
ref = this._valueInputs[this._focusedPair];
}
}
shouldComponentUpdate (nextProps, nextState) {
// NOTE: Only ever re-render if we're changing length. This prevents cursor jumping
// inside inputs.
return (
nextProps.valueInputType !== this.props.valueInputType ||
nextProps.pairs.length !== this.state.pairs.length ||
nextState.pairs.length !== this.state.pairs.length
);
// If you focus an already focused input
if (!ref || this._focusedInput === ref) {
return;
}
// Focus at the end of the text
ref.focus();
ref.selectionStart = ref.selectionEnd = ref.value.length;
}
componentDidUpdate () {
@@ -166,88 +173,102 @@ class KeyValueEditor extends Component {
const {maxPairs, className, valueInputType} = this.props;
return (
<ul key={pairs.length}
className={classnames('key-value-editor', 'wide', className)}>
{pairs.map((pair, i) => {
if (typeof pair.value !== 'string') {
return null;
}
return (
<li key={i}>
<div className="key-value-editor__row">
<div>
<div
className="form-control form-control--underlined form-control--wide">
<input
type="text"
ref={`${i}.${NAME}`}
placeholder={this.props.namePlaceholder || 'Name'}
defaultValue={pair.name}
onChange={e => this._updatePair(i, {name: e.target.value})}
onFocus={() => {
this._focusedPair = i;
this._focusedField = NAME
}}
onBlur={() => {
this._focusedPair = -1
}}
onKeyDown={this._keyDown.bind(this)}/>
</div>
</div>
<div>
<div
className="form-control form-control--underlined form-control--wide">
<input
type={valueInputType || 'text'}
placeholder={this.props.valuePlaceholder || 'Value'}
ref={`${i}.${VALUE}`}
defaultValue={pair.value}
onChange={e => this._updatePair(i, {value: e.target.value})}
onFocus={() => {
this._focusedPair = i;
this._focusedField = VALUE
}}
onBlur={() => {
this._focusedPair = -1
}}
onKeyDown={this._keyDown.bind(this)}/>
</div>
</div>
<button tabIndex="-1" onClick={e => this._deletePair(i)}>
<i className="fa fa-trash-o"></i>
</button>
<ul className={classnames('key-value-editor', 'wide', className)}>
{pairs.map((pair, i) => (
<li key={`${i}.pair`}
className={classnames(
'key-value-editor__row',
{'key-value-editor__row--disabled': pair.disabled}
)}>
<div>
<div className="form-control form-control--underlined form-control--wide">
<input
type="text"
key="name"
ref={n => this._nameInputs[i] = n}
placeholder={this.props.namePlaceholder || 'Name'}
defaultValue={pair.name}
onChange={e => this._updatePair(i, {name: e.target.value})}
onFocus={e => {
this._focusedPair = i;
this._focusedField = NAME;
this._focusedInput = e.target;
}}
onBlur={() => {
this._focusedPair = -1
}}
onKeyDown={this._keyDown.bind(this)}
/>
</div>
</li>
)
})}
{maxPairs === undefined || pairs.length < maxPairs ? (
<li>
<div className="key-value-editor__row">
<div
className="form-control form-control--underlined form-control--wide">
<input type="text"
placeholder={this.props.namePlaceholder || 'Name'}
onFocus={() => {
this._focusedField = NAME;
this._addPair()
}}/>
</div>
<div
className="form-control form-control--underlined form-control--wide">
<input type={valueInputType || 'text'}
placeholder={this.props.valuePlaceholder || 'Value'}
onFocus={() => {
this._focusedField = VALUE;
this._addPair()
}}/>
</div>
<button disabled={true} tabIndex="-1">
<i className="fa fa-blank"></i>
</button>
</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>
</div>
<button tabIndex="-1"
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>
}
</button>
<PromptButton key={Math.random()}
tabIndex="-1"
onClick={e => this._deletePair(i)}
title="Delete item"
confirmMessage={<i className="fa fa-trash-o"></i>}>
<i className="fa fa-trash-o"></i>
</PromptButton>
</li>
))}
{!maxPairs || pairs.length < maxPairs ? (
<li className="key-value-editor__row">
<div className="form-control form-control--underlined form-control--wide">
<input type="text"
placeholder={this.props.namePlaceholder || 'Name'}
onFocus={() => {
this._focusedField = NAME;
this._addPair()
}}/>
</div>
<div className="form-control form-control--underlined form-control--wide">
<input type="text"
placeholder={this.props.valuePlaceholder || 'Value'}
onFocus={() => {
this._focusedField = VALUE;
this._addPair()
}}/>
</div>
<button disabled={true} tabIndex="-1">
<i className="fa fa-blank"></i>
</button>
<button disabled={true} tabIndex="-1">
<i className="fa fa-blank"></i>
</button>
</li>
) : null}
</ul>
@@ -257,7 +278,7 @@ class KeyValueEditor extends Component {
KeyValueEditor.propTypes = {
onChange: PropTypes.func.isRequired,
pairs: PropTypes.array.isRequired,
pairs: PropTypes.arrayOf(PropTypes.object).isRequired,
// Optional
maxPairs: PropTypes.number,

View File

@@ -12,7 +12,7 @@ class PromptButton extends Component {
_confirm (e) {
// Clear existing timeouts
clearTimeout(this._askTimeout);
clearTimeout(this._triggerTimeout);
// Fire the click handler
this.props.onClick(e);
@@ -23,7 +23,7 @@ class PromptButton extends Component {
}, 100);
// Set a timeout to hide the confirmation
this._askTimeout = setTimeout(() => {
this._triggerTimeout = setTimeout(() => {
this.setState({state: STATE_DEFAULT});
}, 2000);
}
@@ -37,7 +37,7 @@ class PromptButton extends Component {
this.setState({state: STATE_ASK});
// Set a timeout to hide the confirmation
this._askTimeout = setTimeout(() => {
this._triggerTimeout = setTimeout(() => {
this.setState({state: STATE_DEFAULT});
}, 2000);
}
@@ -54,7 +54,7 @@ class PromptButton extends Component {
}
componentWillUnmount () {
clearTimeout(this._askTimeout);
clearTimeout(this._triggerTimeout);
clearTimeout(this._doneTimeout);
}
@@ -90,7 +90,7 @@ class PromptButton extends Component {
PromptButton.propTypes = {
addIcon: PropTypes.bool,
confirmMessage: PropTypes.string
confirmMessage: PropTypes.any,
};
export default PromptButton;

View File

@@ -1,16 +1,16 @@
import React, {PropTypes} from 'react';
import {Dropdown, DropdownButton, DropdownItem} from '../base/dropdown';
import {CONTENT_TYPES, getContentTypeName} from '../../../common/constants';
import {contentTypesMap} from '../../../common/constants';
const ContentTypeDropdown = ({updateRequestContentType}) => {
const ContentTypeDropdown = ({updateRequestMimeType}) => {
return (
<Dropdown>
<DropdownButton className="tall">
<i className="fa fa-caret-down"></i>
</DropdownButton>
{CONTENT_TYPES.map(contentType => (
<DropdownItem key={contentType} onClick={e => updateRequestContentType(contentType)}>
{getContentTypeName(contentType)}
{Object.keys(contentTypesMap).map(mimeType => (
<DropdownItem key={mimeType} onClick={e => updateRequestMimeType(mimeType)}>
{contentTypesMap[mimeType]}
</DropdownItem>
))}
</Dropdown>
@@ -18,7 +18,7 @@ const ContentTypeDropdown = ({updateRequestContentType}) => {
};
ContentTypeDropdown.propTypes = {
updateRequestContentType: PropTypes.func.isRequired
updateRequestMimeType: PropTypes.func.isRequired
};
export default ContentTypeDropdown;

View File

@@ -5,7 +5,8 @@ const AuthEditor = ({request, showPasswords, onChange, ...other}) => {
const auth = request.authentication;
const pairs = [{
name: auth.username || '',
value: auth.password || ''
value: auth.password || '',
disabled: auth.disabled || false,
}];
return (
@@ -17,7 +18,8 @@ const AuthEditor = ({request, showPasswords, onChange, ...other}) => {
valueInputType={showPasswords ? 'text' : 'password'}
onChange={pairs => onChange({
username: pairs.length ? pairs[0].name : '',
password: pairs.length ? pairs[0].value : ''
password: pairs.length ? pairs[0].value : '',
disabled: pairs.length ? pairs[0].disabled : false,
})}
{...other}
/>

View File

@@ -1,107 +0,0 @@
import React, {PropTypes, Component} from 'react';
import Editor from '../base/Editor';
import KeyValueEditor from '../base/KeyValueEditor';
import {CONTENT_TYPE_FORM_URLENCODED, getContentTypeFromHeaders} from '../../../common/constants';
import * as querystring from '../../../common/querystring';
class BodyEditor extends Component {
static _getBodyFromPairs (pairs) {
// HACK: Form data needs to be encoded, but we shouldn't encode templating
// Lets try out best to keep {% ... %} and {{ ... }} untouched
// 1. Replace all template tags with urlencode-friendly tags
const encodingMap = {};
const re = /(\{\{\s*[^{}]+\s*}})|(\{%\s*[^{}]+\s*%})/g;
const next = (s) => {
const results = re.exec(s);
if (results) {
const key = `XYZ${Object.keys(encodingMap).length}ZYX`;
const word = results[0];
const index = results['index'];
encodingMap[key] = word;
const newS = `${s.slice(0, index)}${key}${s.slice(index + word.length)}`;
return next(newS);
}
return s;
};
const encodedPairs = JSON.parse(next(JSON.stringify(pairs)));
// 2. Generate the body
const params = [];
for (let {name, value} of encodedPairs) {
params.push({name, value});
}
let body = querystring.buildFromParams(params, false);
// 3. Put all the template tags back
for (const key in encodingMap) {
body = body.replace(key, encodingMap[key]);
}
return body;
}
static _getPairsFromBody (body) {
if (body === '') {
return [];
} else {
return querystring.deconstructToParams(body, false);
}
}
render () {
const {
fontSize,
lineWrapping,
request,
onChange,
className
} = this.props;
const contentType = getContentTypeFromHeaders(request.headers);
if (contentType === CONTENT_TYPE_FORM_URLENCODED) {
return (
<div className="scrollable-container tall wide">
<div className="scrollable">
<KeyValueEditor
onChange={pairs => onChange(BodyEditor._getBodyFromPairs(pairs))}
pairs={BodyEditor._getPairsFromBody(request.body)}
/>
</div>
</div>
)
} else {
return (
<Editor
manualPrettify={true}
fontSize={fontSize}
value={request.body}
className={className}
onChange={onChange}
mode={contentType}
lineWrapping={lineWrapping}
placeholder="..."
/>
)
}
}
}
BodyEditor.propTypes = {
// Functions
onChange: PropTypes.func.isRequired,
request: PropTypes.object.isRequired,
// Optional
fontSize: PropTypes.number,
lineWrapping: PropTypes.bool
};
export default BodyEditor;

View File

@@ -40,7 +40,7 @@ class RequestHeadersEditor extends Component {
let headersString = '';
for (const header of headers) {
if (!header.name || !header.value) {
if (header.disabled || !header.name || !header.value) {
// Not a valid header
continue;
}

View File

@@ -0,0 +1,89 @@
import React, {PropTypes, Component} from 'react';
import RawEditor from './RawEditor';
import UrlEncodedEditor from './UrlEncodedEditor';
import FormEditor from './FormEditor';
import {getContentTypeFromHeaders, BODY_TYPE_FORM_URLENCODED, BODY_TYPE_FORM, BODY_TYPE_FILE} from '../../../../common/constants';
import {newBodyRaw, newBodyFormUrlEncoded, newBodyForm} from '../../../../models/request';
import {CONTENT_TYPE_FORM_URLENCODED} from '../../../../common/constants';
import {CONTENT_TYPE_FORM_DATA} from '../../../../common/constants';
class BodyEditor extends Component {
constructor (props) {
super(props);
this._boundHandleRawChange = this._handleRawChange.bind(this);
this._boundHandleFormUrlEncodedChange = this._handleFormUrlEncodedChange.bind(this);
this._boundHandleFormChange = this._handleFormChange.bind(this);
}
_handleRawChange (rawValue) {
const {onChange, request} = this.props;
const contentType = getContentTypeFromHeaders(request.headers);
const newBody = newBodyRaw(rawValue, contentType);
onChange(newBody);
}
_handleFormUrlEncodedChange (parameters) {
const {onChange} = this.props;
const newBody = newBodyFormUrlEncoded(parameters);
onChange(newBody);
}
_handleFormChange (parameters) {
const {onChange} = this.props;
const newBody = newBodyForm(parameters);
onChange(newBody);
}
render () {
const {fontSize, lineWrapping, request} = this.props;
const bodyType = request.body.mimeType;
const fileName = request.body.fileName;
if (bodyType === CONTENT_TYPE_FORM_URLENCODED) {
return (
<UrlEncodedEditor
key={request._id}
onChange={this._boundHandleFormUrlEncodedChange}
parameters={request.body.params || []}
/>
)
} else if (bodyType === CONTENT_TYPE_FORM_DATA) {
return (
<FormEditor
key={request._id}
onChange={this._boundHandleFormChange}
parameters={request.body.params || []}
/>
)
} else if (bodyType === BODY_TYPE_FILE) {
// TODO
return null
} else {
const contentType = getContentTypeFromHeaders(request.headers);
return (
<RawEditor
key={`${request._id}::${contentType}`}
fontSize={fontSize}
lineWrapping={lineWrapping}
contentType={contentType || 'text/plain'}
content={request.body.text || ''}
onChange={this._boundHandleRawChange}
/>
)
}
}
}
BodyEditor.propTypes = {
// Required
onChange: PropTypes.func.isRequired,
request: PropTypes.object.isRequired,
// Optional
fontSize: PropTypes.number,
lineWrapping: PropTypes.bool
};
export default BodyEditor;

View File

View File

@@ -0,0 +1,24 @@
import React, {PropTypes, Component} from 'react';
import KeyValueEditor from '../../base/KeyValueEditor';
class FormEditor extends Component {
render () {
const {parameters, onChange} = this.props;
return (
<div className="scrollable-container tall wide">
<div className="scrollable">
<KeyValueEditor onChange={onChange} pairs={parameters} valueInputType="file"/>
</div>
</div>
)
}
}
FormEditor.propTypes = {
// Required
onChange: PropTypes.func.isRequired,
parameters: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default FormEditor;

View File

@@ -0,0 +1,41 @@
import React, {PropTypes, Component} from 'react';
import Editor from '../../base/Editor';
class RawEditor extends Component {
render () {
const {
contentType,
content,
fontSize,
lineWrapping,
onChange,
className
} = this.props;
return (
<Editor
manualPrettify={true}
fontSize={fontSize}
value={content}
className={className}
onChange={onChange}
mode={contentType}
lineWrapping={lineWrapping}
placeholder="..."
/>
)
}
}
RawEditor.propTypes = {
// Required
onChange: PropTypes.func.isRequired,
content: PropTypes.string.isRequired,
contentType: PropTypes.string.isRequired,
// Optional
fontSize: PropTypes.number,
lineWrapping: PropTypes.bool
};
export default RawEditor;

View File

@@ -0,0 +1,24 @@
import React, {PropTypes, Component} from 'react';
import KeyValueEditor from '../../base/KeyValueEditor';
class UrlEncodedEditor extends Component {
render () {
const {parameters, onChange} = this.props;
return (
<div className="scrollable-container tall wide">
<div className="scrollable">
<KeyValueEditor onChange={onChange} pairs={parameters}/>
</div>
</div>
)
}
}
UrlEncodedEditor.propTypes = {
// Required
onChange: PropTypes.func.isRequired,
parameters: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default UrlEncodedEditor;

View File

@@ -10,8 +10,10 @@ class AlertModal extends Component {
this.state = {};
}
show ({title, message}) {
show (options = {}) {
this.modal.show();
const {title, message} = options;
this.setState({title, message});
}

View File

@@ -5,8 +5,8 @@ import {DEBOUNCE_MILLIS} from '../../../common/constants';
class SidebarFilter extends Component {
_onChange (value) {
clearTimeout(this._askTimeout);
this._askTimeout = setTimeout(() => {
clearTimeout(this._triggerTimeout);
this._triggerTimeout = setTimeout(() => {
this.props.onChange(value);
}, DEBOUNCE_MILLIS);
}

View File

@@ -16,6 +16,7 @@
border: 1px solid @warning !important;
}
&.form-control--padded,
&.form-control--outlined,
&.form-control--underlined {
height: auto;
@@ -34,6 +35,13 @@
}
}
&.form-control--padded {
textarea,
input {
border: 0;
}
}
&.form-control--underlined {
textarea,
input {

View File

@@ -4,37 +4,44 @@
.key-value-editor {
padding: @padding-md 0;
li {
.key-value-editor__label {
padding: 0 @padding-md;
opacity: 0.5;
font-size: @font-size-sm;
}
.key-value-editor__label {
padding: 0 @padding-md;
opacity: 0.5;
font-size: @font-size-sm;
.key-value-editor__row {
display: grid;
grid-template-columns: minmax(0, 0.5fr) minmax(0, 0.5fr) auto auto;
grid-template-rows: auto;
&.key-value-editor__row--disabled input {
text-decoration: line-through;
color: @hl-xxl;
}
.key-value-editor__row {
display: grid;
grid-template-columns: minmax(0, 0.5fr) minmax(0, 0.5fr) auto;
grid-template-rows: auto;
& > * {
padding: 0 @padding-sm;
& > * {
padding: 0 @padding-sm;
&:first-child {
padding-left: @padding-md;
}
&:last-child {
padding-right: @padding-md;
}
&:first-child {
padding-left: @padding-md;
}
& > button {
color: @hl;
&:last-child {
padding-right: @padding-md;
}
}
&:hover {
color: inherit;
}
& > button {
color: @hl;
&:hover,
&:focus {
background: transparent;
}
&:hover {
color: inherit;
}
}
}

View File

@@ -139,7 +139,7 @@ export function importFile (workspaceId) {
}]
};
electron.remote.dialog.showOpenDialog(options, paths => {
electron.remote.dialog.showOpenDialog(options, async paths => {
if (!paths) {
// It was cancelled, so let's bail out
dispatch(loadStop());
@@ -148,20 +148,32 @@ export function importFile (workspaceId) {
}
// Let's import all the paths!
paths.map(path => {
fs.readFile(path, 'utf8', async (err, data) => {
for (const path of paths) {
try {
const data = fs.readFileSync(path, 'utf8');
dispatch(loadStop());
if (err) {
trackEvent('Import', 'Failure');
console.warn('Import Failed', err);
return;
}
const summary = await importRaw(workspace, data);
importRaw(workspace, data);
let statements = Object.keys(summary).map(type => {
const count = summary[type].length;
const name = models.getModelName(type, count);
return count === 0 ? null :`${count} ${name}`;
}).filter(s => s !== null);
let message;
if (statements.length === 0) {
message = 'Nothing was found to import.';
} else {
message = `You imported ${statements.join(', ')}!`;
}
showModal(AlertModal, {title: 'Import Succeeded', message});
trackEvent('Import', 'Success');
});
})
} catch (e) {
showModal(AlertModal, {title: 'Import Failed', message: e + ''});
trackEvent('Import', 'Failure', e);
}
}
});
}
}

View File

@@ -13,6 +13,7 @@
"scripts": {
"test:noisy": "jest",
"test:coverage": "jest --coverage --silent && open ./coverage/lcov-report/index.html",
"test:watch": "jest --silent --watch",
"test": "jest --silent",
"start-hot": "cross-env HOT=1 INSOMNIA_ENV=development electron -r babel-register ./app/main.development.js",
"hot-server": "babel-node ./webpack/server.js",
@@ -133,6 +134,7 @@
"css-loader": "^0.23.1",
"electron": "^1.4.7",
"electron-builder": "^8.6.0",
"electron-devtools-installer": "^2.0.1",
"express": "^4.14.0",
"file-loader": "^0.9.0",
"jest": "^17.0.0",