Removed Request.ContentType, added urlencoded editor, and added Copy to headers

This commit is contained in:
Gregory Schier 2016-07-22 13:02:17 -07:00
parent cf572d79a5
commit 3ed07c67ee
13 changed files with 256 additions and 87 deletions

View File

@ -3,16 +3,14 @@ import React, {PropTypes} from 'react';
import Dropdown from '../components/base/Dropdown';
import {CONTENT_TYPES, getContentTypeName} from '../lib/contentTypes';
const ContentTypeDropdown = ({updateRequestContentType, activeContentType}) => {
const contentTypes = CONTENT_TYPES.filter(ct => ct !== activeContentType);
const ContentTypeDropdown = ({updateRequestContentType}) => {
return (
<Dropdown>
<button className="tall">
<i className="fa fa-caret-down"></i>
</button>
<ul>
{contentTypes.map(contentType => (
{CONTENT_TYPES.map(contentType => (
<li key={contentType}>
<button onClick={e => updateRequestContentType(contentType)}>
{getContentTypeName(contentType)}
@ -25,8 +23,7 @@ const ContentTypeDropdown = ({updateRequestContentType, activeContentType}) => {
};
ContentTypeDropdown.propTypes = {
updateRequestContentType: PropTypes.func.isRequired,
activeContentType: PropTypes.string.isRequired
updateRequestContentType: PropTypes.func.isRequired
};
export default ContentTypeDropdown;

View File

@ -1,25 +1,63 @@
import React, {PropTypes} from 'react';
import React, {PropTypes, Component} from 'react';
import Editor from './base/Editor';
import KeyValueEditor from './base/KeyValueEditor';
import {CONTENT_TYPE_FORM_URLENCODED} from '../lib/contentTypes';
import {getContentTypeFromHeaders} from '../lib/contentTypes';
import * as querystring from '../lib/querystring';
const RequestBodyEditor = ({fontSize, lineWrapping, body, contentType, onChange, className}) => (
class RequestBodyEditor extends Component {
static _getBodyFromPairs (pairs) {
const params = [];
for (let {name, value} of pairs) {
params.push({name, value});
}
return querystring.buildFromParams(params, false);
}
static _getPairsFromBody (body) {
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 (
<KeyValueEditor
onChange={pairs => onChange(RequestBodyEditor._getBodyFromPairs(pairs))}
pairs={RequestBodyEditor._getPairsFromBody(request.body)}
uniquenessKey={request._id}
/>
)
} else {
return (
<Editor
fontSize={fontSize}
value={body}
value={request.body}
className={className}
onChange={onChange}
mode={contentType}
lineWrapping={lineWrapping}
placeholder="request body here..."
/>
);
)
}
}
}
RequestBodyEditor.propTypes = {
// Functions
onChange: PropTypes.func.isRequired,
// Other
body: PropTypes.string.isRequired,
contentType: PropTypes.string.isRequired,
request: PropTypes.object.isRequired,
// Optional
fontSize: PropTypes.number,

View File

@ -11,6 +11,7 @@ import RequestUrlBar from '../components/RequestUrlBar';
import {getContentTypeName} from '../lib/contentTypes';
import {renderRequest} from '../lib/render';
import {getContentTypeFromHeaders} from '../lib/contentTypes';
class RequestPane extends Component {
render () {
@ -54,11 +55,8 @@ class RequestPane extends Component {
<Tabs className="pane__body">
<TabList>
<Tab>
<button>{getContentTypeName(request.contentType)}</button>
<ContentTypeDropdown
activeContentType={request.contentType}
updateRequestContentType={updateRequestContentType}
/>
<button>{getContentTypeName(getContentTypeFromHeaders(request.headers))}</button>
<ContentTypeDropdown updateRequestContentType={updateRequestContentType}/>
</Tab>
<Tab>
<button>
@ -80,12 +78,10 @@ class RequestPane extends Component {
</TabList>
<TabPanel className="editor-wrapper">
<RequestBodyEditor
request={request}
onChange={updateRequestBody}
requestId={request._id}
contentType={request.contentType}
fontSize={editorFontSize}
lineWrapping={editorLineWrapping}
body={request.body}
/>
</TabPanel>
<TabPanel>

View File

@ -0,0 +1,41 @@
import React, {PropTypes} from 'react';
import CopyButton from './base/CopyButton';
const ResponseHeadersViewer = ({headers}) => {
const headersString = headers.map(
h => `${h.name}: ${h.value}`
).join('\n');
return (
<div>
<table className="wide">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{headers.map((h, i) => (
<tr className="selectable" key={i}>
<td>{h.name}</td>
<td>{h.value}</td>
</tr>
))}
</tbody>
</table>
<p className="pad-top">
<CopyButton
className="pull-right btn btn--super-compact btn--outlined"
content={headersString}
/>
</p>
</div>
)
};
ResponseHeadersViewer.propTypes = {
headers: PropTypes.array.isRequired
};
export default ResponseHeadersViewer;

View File

@ -4,8 +4,9 @@ import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'
import StatusTag from './StatusTag';
import SizeTag from './SizeTag';
import TimeTag from './TimeTag';
import PreviewModeDropdown from '../components/PreviewModeDropdown';
import ResponseViewer from '../components/ResponseViewer';
import PreviewModeDropdown from './PreviewModeDropdown';
import ResponseBodyViewer from './ResponseBodyViewer';
import ResponseHeadersViewer from './ResponseHeadersViewer';
import {getPreviewModeName} from '../lib/previewModes';
import {PREVIEW_MODE_SOURCE} from '../lib/previewModes';
import {REQUEST_TIME_TO_SHOW_COUNTER} from '../lib/constants';
@ -108,7 +109,7 @@ class ResponsePane extends Component {
</TabList>
<TabPanel>
{response.error ? (
<ResponseViewer
<ResponseBodyViewer
contentType={response.contentType}
previewMode={PREVIEW_MODE_SOURCE}
editorLineWrapping={editorLineWrapping}
@ -117,7 +118,7 @@ class ResponsePane extends Component {
url={response.url}
/>
) : (
<ResponseViewer
<ResponseBodyViewer
contentType={response.contentType}
previewMode={previewMode}
editorLineWrapping={editorLineWrapping}
@ -129,22 +130,7 @@ class ResponsePane extends Component {
)}
</TabPanel>
<TabPanel className="scrollable pad">
<table className="wide">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{response.headers.map((h, i) => (
<tr className="selectable" key={i}>
<td>{h.name}</td>
<td>{h.value}</td>
</tr>
))}
</tbody>
</table>
<ResponseHeadersViewer headers={response.headers}/>
</TabPanel>
</Tabs>
</section>

View File

@ -0,0 +1,43 @@
import React, {Component, PropTypes} from 'react';
const {clipboard} = require('electron');
class CopyButton extends Component {
constructor (props) {
super(props);
this.state = {
showConfirmation: false
}
}
_handleClick (e) {
e.preventDefault();
clipboard.writeText(this.props.content);
this.setState({showConfirmation: true});
this._timeout = setTimeout(() => {
this.setState({showConfirmation: false});
}, 2000);
}
componentWillUnmount() {
clearTimeout(this._timeout);
}
render () {
const {content, ...other} = this.props;
const {showConfirmation} = this.state;
return (
<button onClick={this._handleClick.bind(this)} {...other}>
{showConfirmation ? 'Copied' : 'Copy'}
</button>
)
}
}
CopyButton.propTypes = {
content: PropTypes.string.isRequired
};
export default CopyButton;

View File

@ -479,7 +479,7 @@ class App extends Component {
updateRequestParameters={parameters => db.requestUpdate(activeRequest, {parameters})}
updateRequestAuthentication={authentication => db.requestUpdate(activeRequest, {authentication})}
updateRequestHeaders={headers => db.requestUpdate(activeRequest, {headers})}
updateRequestContentType={contentType => db.requestUpdate(activeRequest, {contentType})}
updateRequestContentType={contentType => db.requestUpdateContentType(activeRequest, contentType)}
updateSettingsShowPasswords={showPasswords => db.settingsUpdate(settings, {showPasswords})}
/>

View File

@ -5,8 +5,8 @@ import * as fs from 'fs';
import * as methods from '../lib/constants';
import {generateId} from './util';
import {PREVIEW_MODE_SOURCE} from '../lib/previewModes';
import {CONTENT_TYPE_TEXT} from '../lib/contentTypes';
import {DB_PERSIST_INTERVAL, DEFAULT_SIDEBAR_WIDTH} from '../lib/constants';
import {CONTENT_TYPE_JSON} from '../lib/contentTypes';
export const TYPE_SETTINGS = 'Settings';
export const TYPE_WORKSPACE = 'Workspace';
@ -40,10 +40,12 @@ const MODEL_DEFAULTS = {
url: '',
name: 'New Request',
method: methods.METHOD_GET,
contentType: CONTENT_TYPE_TEXT,
body: '',
parameters: [],
headers: [],
headers: [{
name: 'Content-Type',
value: CONTENT_TYPE_JSON
}],
authentication: {},
metaPreviewMode: PREVIEW_MODE_SOURCE,
metaSortKey: -1 * Date.now()
@ -324,6 +326,24 @@ export function requestUpdate (request, patch) {
return docUpdate(request, patch);
}
export function requestUpdateContentType (request, contentType) {
let headers = [...request.headers];
const contentTypeHeader = headers.find(
h => h.name.toLowerCase() === 'content-type'
);
if (!contentType) {
// Remove the contentType header if we are unsetting it
headers = headers.filter(h => h !== contentTypeHeader);
} else if (contentTypeHeader) {
contentTypeHeader.value = contentType;
} else {
headers.push({name: 'Content-Type', value: contentType})
}
return docUpdate(request, {headers});
}
export function requestCopy (request) {
const name = `${request.name} (Copy)`;
return requestCreate(Object.assign({}, request, {name}));

View File

@ -1,13 +1,15 @@
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 = 'application/x-www-form-urlencoded';
export const CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded';
export const CONTENT_TYPE_OTHER = '';
const contentTypeMap = {
[CONTENT_TYPE_JSON]: 'JSON',
[CONTENT_TYPE_XML]: 'XML',
[CONTENT_TYPE_TEXT]: 'Plain Text',
[CONTENT_TYPE_FORM]: 'Form Data'
[CONTENT_TYPE_TEXT]: 'Text',
[CONTENT_TYPE_OTHER]: 'Other',
[CONTENT_TYPE_FORM_URLENCODED]: 'Form URL Encoded'
};
export const CONTENT_TYPES = Object.keys(contentTypeMap);
@ -19,6 +21,10 @@ export const CONTENT_TYPES = Object.keys(contentTypeMap);
* @returns {*|string}
*/
export function getContentTypeName (contentType) {
// TODO: Make this more robust maybe...
return contentTypeMap[contentType] || 'Unknown';
return contentTypeMap[contentType] || contentTypeMap[CONTENT_TYPE_OTHER];
}
export function getContentTypeFromHeaders (headers) {
const header = headers.find(({name}) => name.toLowerCase() === 'content-type');
return header ? header.value : null;
}

View File

@ -186,13 +186,6 @@ export function exportCurl (requestId) {
// Headers
const hasContentTypeHeader = !!renderedRequest.headers.find(h => h.name.toUpperCase() === 'CONTENT-TYPE');
if (!hasContentTypeHeader && !IS_GET_REQUEST) {
const value = renderedRequest.contentType;
const name = 'Content-Type';
renderedRequest.headers.push({name, value})
}
for (let i = 0; i < renderedRequest.headers.length; i++) {
const {name, value} = renderedRequest.headers[i];

View File

@ -1,5 +1,6 @@
import * as db from '../../database';
import {getAppVersion} from '../appInfo';
import {getContentTypeFromHeaders} from '../contentTypes';
const VERSION_CHROME_APP = 1;
const VERSION_DESKTOP_APP = 2;
@ -64,18 +65,33 @@ function importRequest (importedRequest, parentId, exportFormat) {
}
}
// Add the content type header
const headers = importedRequest.headers || [];
const contentType = getContentTypeFromHeaders(headers);
if (!contentType) {
const derivedContentType = FORMAT_MAP[importedRequest.__insomnia.format];
if (derivedContentType) {
headers.push({
name: 'Content-Type',
value: FORMAT_MAP[importedRequest.__insomnia.format]
});
}
}
db.requestCreate({
parentId,
name: importedRequest.name,
url: importedRequest.url,
method: importedRequest.method,
body: importedRequest.body,
headers: importedRequest.headers || [],
headers: headers,
parameters: importedRequest.params || [],
contentType: FORMAT_MAP[importedRequest.__insomnia.format] || 'text/plain',
authentication: auth
});
} else if (exportFormat === VERSION_DESKTOP_APP) {
const headers =
db.requestCreate({
parentId,
name: importedRequest.name,
@ -84,7 +100,6 @@ function importRequest (importedRequest, parentId, exportFormat) {
body: importedRequest.body,
headers: importedRequest.headers,
parameters: importedRequest.parameters,
contentType: importedRequest.contentType,
authentication: importedRequest.authentication
});
} else {
@ -175,7 +190,6 @@ export function exportJSON () {
url: r.url,
name: r.name,
method: r.method,
contentType: r.contentType,
body: r.body,
parameters: r.parameters,
headers: r.headers,

View File

@ -1,30 +1,39 @@
export function getJoiner (url) {
url = url || '';
return url.indexOf('?') === -1 ? '?' : '&';
}
export function joinURL (url, qs) {
if (!qs) { return url; }
if (!qs) {
return url;
}
url = url || '';
return url + getJoiner(url) + qs;
}
export function build (param) {
// Skip non-name ones
if (!param.name) { return ''; }
export function build (param, strict = true) {
// Skip non-name ones in strict mode
if (strict && !param.name) {
return '';
}
if (param.value) {
if (!strict || param.value) {
return encodeURIComponent(param.name) + '=' + encodeURIComponent(param.value);
} else {
return encodeURIComponent(param.name);
}
}
export function buildFromParams (parameters) {
/**
*
* @param parameters
* @param strict allow empty names and values
* @returns {string}
*/
export function buildFromParams (parameters, strict = true) {
let items = [];
for (var i = 0; i < parameters.length; i++) {
let built = build(parameters[i]);
let built = build(parameters[i], strict);
if (!built) {
continue;
@ -35,3 +44,29 @@ export function buildFromParams (parameters) {
return items.join('&');
}
/**
*
* @param qs
* @param strict allow empty names and values
* @returns {Array}
*/
export function deconstructToParams (qs, strict = true) {
const stringPairs = qs.split('&');
const pairs = [];
for (let stringPair of stringPairs) {
const tmp = stringPair.split('=');
const name = decodeURIComponent(tmp[0] || '');
const value = decodeURIComponent(tmp[1] || '');
if (strict && !name) {
continue;
}
pairs.push({name, value});
}
return pairs;
}