mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-22 23:28:33 -04:00
Switch Request body to be object instead of string (#47)
* Basic import summary * Started (broken)
This commit is contained in:
@@ -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({});
|
||||
|
||||
@@ -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/'
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
89
app/ui/components/editors/body/BodyEditor.js
Normal file
89
app/ui/components/editors/body/BodyEditor.js
Normal 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;
|
||||
0
app/ui/components/editors/body/FileEditor.js
Normal file
0
app/ui/components/editors/body/FileEditor.js
Normal file
24
app/ui/components/editors/body/FormEditor.js
Normal file
24
app/ui/components/editors/body/FormEditor.js
Normal 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;
|
||||
41
app/ui/components/editors/body/RawEditor.js
Normal file
41
app/ui/components/editors/body/RawEditor.js
Normal 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;
|
||||
24
app/ui/components/editors/body/UrlEncodedEditor.js
Normal file
24
app/ui/components/editors/body/UrlEncodedEditor.js
Normal 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;
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user