mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 22:30:15 +00:00
Various Improvements (#59)
* Better create, started response history * Response history working * A bunch
This commit is contained in:
parent
8aa274d21b
commit
6c1c03cef6
@ -35,9 +35,9 @@ export function getClientString () {
|
||||
}
|
||||
|
||||
// Global Stuff
|
||||
export const LOCALSTORAGE_KEY = 'insomnia.state';
|
||||
export const DB_PERSIST_INTERVAL = 1000 * 60 * 10;
|
||||
export const DEBOUNCE_MILLIS = 100;
|
||||
export const MAX_RESPONSES = 20;
|
||||
export const REQUEST_TIME_TO_SHOW_COUNTER = 1; // Seconds
|
||||
export const GA_ID = 'UA-86416787-1';
|
||||
export const GA_HOST = 'desktop.insomnia.rest';
|
||||
@ -73,7 +73,7 @@ export const METHOD_HEAD = 'HEAD';
|
||||
export const METHOD_FIND = 'FIND';
|
||||
export const METHOD_PURGE = 'PURGE';
|
||||
export const METHOD_DELETE_HARD = 'DELETEHARD';
|
||||
export const METHODS = [
|
||||
export const HTTP_METHODS = [
|
||||
METHOD_GET,
|
||||
METHOD_POST,
|
||||
METHOD_PUT,
|
||||
@ -113,7 +113,6 @@ export function getPreviewModeName (previewMode) {
|
||||
// Content Types
|
||||
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_FILE = 'application/octet-stream';
|
||||
|
@ -4,6 +4,7 @@ import fsPath from 'path';
|
||||
import {DB_PERSIST_INTERVAL} from './constants';
|
||||
import {generateId} from './misc';
|
||||
import {getModel, initModel} from '../models';
|
||||
import * as models from '../models/index';
|
||||
|
||||
export const CHANGE_INSERT = 'insert';
|
||||
export const CHANGE_UPDATE = 'update';
|
||||
@ -111,10 +112,15 @@ function notifyOfChange (event, doc, fromSync) {
|
||||
// Helpers //
|
||||
// ~~~~~~~ //
|
||||
|
||||
export function getMostRecentlyModified (type, query = {}) {
|
||||
export async function getMostRecentlyModified (type, query = {}) {
|
||||
const docs = await findMostRecentlyModified(type, query, 1);
|
||||
return docs.length ? docs[0] : null;
|
||||
}
|
||||
|
||||
export function findMostRecentlyModified (type, query = {}, limit = null) {
|
||||
return new Promise(resolve => {
|
||||
db[type].find(query).sort({modified: -1}).limit(1).exec((err, docs) => {
|
||||
resolve(docs.length ? docs[0] : null);
|
||||
db[type].find(query).sort({modified: -1}).limit(limit).exec((err, docs) => {
|
||||
resolve(docs);
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -262,11 +268,11 @@ export function docCreate (type, patch = {}) {
|
||||
|
||||
const doc = initModel(
|
||||
type,
|
||||
{_id: generateId(idPrefix)},
|
||||
patch,
|
||||
|
||||
// Fields that the user can't touch
|
||||
{
|
||||
_id: generateId(idPrefix),
|
||||
type: type,
|
||||
modified: Date.now()
|
||||
}
|
||||
@ -347,6 +353,11 @@ export async function duplicate (originalDoc, patch = {}, first = true) {
|
||||
|
||||
// 2. Get all the children
|
||||
for (const type of allTypes()) {
|
||||
// Note: We never want to duplicate a response
|
||||
if (type === models.response.type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parentId = originalDoc._id;
|
||||
const children = await find(type, {parentId});
|
||||
for (const doc of children) {
|
||||
|
@ -116,6 +116,7 @@ export function _actuallySend (renderedRequest, settings, forceIPv4 = false) {
|
||||
}, true);
|
||||
} catch (e) {
|
||||
const response = await models.response.create({
|
||||
url: renderedRequest.url,
|
||||
parentId: renderedRequest._id,
|
||||
elapsedTime: 0,
|
||||
statusMessage: 'Error',
|
||||
@ -169,7 +170,9 @@ export function _actuallySend (renderedRequest, settings, forceIPv4 = false) {
|
||||
}
|
||||
|
||||
await models.response.create({
|
||||
url: originalUrl,
|
||||
parentId: renderedRequest._id,
|
||||
statusMessage: 'Error',
|
||||
error: message
|
||||
});
|
||||
|
||||
@ -223,6 +226,7 @@ export function _actuallySend (renderedRequest, settings, forceIPv4 = false) {
|
||||
req.abort();
|
||||
|
||||
await models.response.create({
|
||||
url: originalUrl,
|
||||
parentId: renderedRequest._id,
|
||||
elapsedTime: Date.now() - requestStartTime,
|
||||
statusMessage: 'Cancelled',
|
||||
|
@ -1,11 +1,7 @@
|
||||
import {METHOD_GET, getContentTypeFromHeaders, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_FORM_DATA} from '../common/constants';
|
||||
import {METHOD_GET, getContentTypeFromHeaders, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FILE} 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';
|
||||
import {CONTENT_TYPE_XML} from '../common/constants';
|
||||
import {CONTENT_TYPE_FILE} from '../common/constants';
|
||||
import {CONTENT_TYPE_TEXT} from '../common/constants';
|
||||
|
||||
export const name = 'Request';
|
||||
export const type = 'Request';
|
||||
@ -91,8 +87,8 @@ export function update (request, patch) {
|
||||
return db.docUpdate(request, patch);
|
||||
}
|
||||
|
||||
export function updateMimeType (request, mimeType) {
|
||||
let headers = [...request.headers];
|
||||
export function updateMimeType (request, mimeType, doCreate = false) {
|
||||
let headers = request.headers ? [...request.headers] : [];
|
||||
const contentTypeHeader = getContentTypeHeader(headers);
|
||||
|
||||
// 1. Update Content-Type header
|
||||
@ -121,7 +117,11 @@ export function updateMimeType (request, mimeType) {
|
||||
body = newBodyRaw(request.body.text || '', mimeType);
|
||||
}
|
||||
|
||||
return update(request, {headers, body});
|
||||
if (doCreate) {
|
||||
return create(Object.assign({}, request, {headers, body}));
|
||||
} else {
|
||||
return update(request, {headers, body});
|
||||
}
|
||||
}
|
||||
|
||||
export function duplicate (request) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as db from '../common/database';
|
||||
import {MAX_RESPONSES} from '../common/constants';
|
||||
|
||||
export const name = 'Response';
|
||||
export const type = 'Response';
|
||||
@ -24,12 +25,35 @@ export function migrate (doc) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
export function create (patch = {}) {
|
||||
export function getById (id) {
|
||||
return db.get(type, id);
|
||||
}
|
||||
|
||||
export function all () {
|
||||
return db.all(type);
|
||||
}
|
||||
|
||||
export async function removeForRequest (parentId) {
|
||||
db.removeBulkSilently(type, {parentId});
|
||||
}
|
||||
|
||||
export function findRecentForRequest (requestId, limit) {
|
||||
return db.findMostRecentlyModified(type, {parentId: requestId}, limit);
|
||||
}
|
||||
|
||||
export async function create (patch = {}) {
|
||||
if (!patch.parentId) {
|
||||
throw new Error('New Response missing `parentId`');
|
||||
}
|
||||
|
||||
db.removeBulkSilently(type, {parentId: patch.parentId});
|
||||
const {parentId} = patch;
|
||||
|
||||
// Delete all other responses before creating the new one
|
||||
const allResponses = await db.findMostRecentlyModified(type, {parentId}, MAX_RESPONSES);
|
||||
const recentIds = allResponses.map(r => r._id);
|
||||
await db.removeBulkSilently(type, {parentId, _id: {$nin: recentIds}});
|
||||
|
||||
// Actually create the new response
|
||||
return db.docCreate(type, patch);
|
||||
}
|
||||
|
||||
|
@ -131,7 +131,9 @@ class RequestPane extends PureComponent {
|
||||
{" "}
|
||||
{numBodyParams ? <span className="txt-sm">({numBodyParams})</span> : null}
|
||||
</button>
|
||||
<ContentTypeDropdown updateRequestMimeType={updateRequestMimeType} className="tall">
|
||||
<ContentTypeDropdown onChange={updateRequestMimeType}
|
||||
contentType={request.body.mimeType}
|
||||
className="tall">
|
||||
<i className="fa fa-caret-down"></i>
|
||||
</ContentTypeDropdown>
|
||||
</Tab>
|
||||
|
@ -1,21 +1,28 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {Dropdown, DropdownButton, DropdownItem} from './base/dropdown';
|
||||
import {METHODS, DEBOUNCE_MILLIS, isMac} from '../../common/constants';
|
||||
import {DEBOUNCE_MILLIS, isMac} from '../../common/constants';
|
||||
import {trackEvent} from '../../analytics';
|
||||
import MethodDropdown from './dropdowns/MethodDropdown';
|
||||
|
||||
|
||||
class RequestUrlBar extends Component {
|
||||
_handleFormSubmit (e) {
|
||||
_handleFormSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
_handleMethodChange = method => {
|
||||
this.props.onMethodChange(method);
|
||||
trackEvent('Request', 'Method Change', method);
|
||||
};
|
||||
|
||||
_handleUrlChange = e => {
|
||||
const url = e.target.value;
|
||||
|
||||
_handleUrlChange (url) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = setTimeout(() => {
|
||||
this.props.onUrlChange(url);
|
||||
}, DEBOUNCE_MILLIS);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this._bodyKeydownHandler = e => {
|
||||
@ -40,31 +47,20 @@ class RequestUrlBar extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {onMethodChange, url, method} = this.props;
|
||||
const {url, method} = this.props;
|
||||
return (
|
||||
<div className="urlbar">
|
||||
<Dropdown>
|
||||
<DropdownButton type="button">
|
||||
{method} <i className="fa fa-caret-down"/>
|
||||
</DropdownButton>
|
||||
{METHODS.map(method => (
|
||||
<DropdownItem key={method} className={`method-${method}`} onClick={() => {
|
||||
onMethodChange(method);
|
||||
trackEvent('Request', 'Method Change', method);
|
||||
}}>
|
||||
{method}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
<form onSubmit={this._handleFormSubmit.bind(this)}>
|
||||
<MethodDropdown onChange={this._handleMethodChange} method={method}>
|
||||
{method} <i className="fa fa-caret-down"/>
|
||||
</MethodDropdown>
|
||||
<form onSubmit={this._handleFormSubmit}>
|
||||
<div className="form-control">
|
||||
<input
|
||||
ref={n => this._input = n}
|
||||
type="text"
|
||||
placeholder="https://api.myproduct.com/v1/users"
|
||||
defaultValue={url}
|
||||
onClick={e => e.preventDefault()}
|
||||
onChange={e => this._handleUrlChange(e.target.value)}/>
|
||||
onChange={this._handleUrlChange}/>
|
||||
</div>
|
||||
<div className="no-wrap">
|
||||
<button type="submit">
|
||||
|
@ -8,10 +8,12 @@ import StatusTag from './tags/StatusTag';
|
||||
import TimeTag from './tags/TimeTag';
|
||||
import PreviewModeDropdown from './dropdowns/PreviewModeDropdown';
|
||||
import ResponseViewer from './viewers/ResponseViewer';
|
||||
import ResponseHistoryDropdown from './dropdowns/ResponseHistoryDropdown';
|
||||
import ResponseTimer from './ResponseTimer';
|
||||
import ResponseHeadersViewer from './viewers/ResponseHeadersViewer';
|
||||
import ResponseCookiesViewer from './viewers/ResponseCookiesViewer';
|
||||
import * as models from '../../models';
|
||||
import {REQUEST_TIME_TO_SHOW_COUNTER, MOD_SYM, PREVIEW_MODE_SOURCE, getPreviewModeName} from '../../common/constants';
|
||||
import {MOD_SYM, PREVIEW_MODE_SOURCE, getPreviewModeName} from '../../common/constants';
|
||||
import {getSetCookieHeaders} from '../../common/misc';
|
||||
import {cancelCurrentRequest} from '../../common/network';
|
||||
import {trackEvent} from '../../analytics';
|
||||
@ -19,13 +21,14 @@ import {trackEvent} from '../../analytics';
|
||||
class ResponsePane extends Component {
|
||||
state = {response: null};
|
||||
|
||||
async _getResponse (request) {
|
||||
if (!request) {
|
||||
this.setState({response: null});
|
||||
} else {
|
||||
const response = await models.response.getLatestByParentId(request._id);
|
||||
this.setState({response});
|
||||
async _getResponse (requestId, responseId) {
|
||||
let response = await models.response.getById(responseId);
|
||||
|
||||
if (!response) {
|
||||
response = await models.response.getLatestByParentId(requestId);
|
||||
}
|
||||
|
||||
this.setState({response});
|
||||
}
|
||||
|
||||
async _handleDownloadResponseBody () {
|
||||
@ -66,11 +69,15 @@ class ResponsePane extends Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this._getResponse(nextProps.request);
|
||||
const activeRequestId = nextProps.request ? nextProps.request._id : null;
|
||||
const activeResponseId = nextProps.activeResponseId;
|
||||
this._getResponse(activeRequestId, activeResponseId);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._getResponse(this.props.request);
|
||||
const activeRequestId = this.props.request ? this.props.request._id : null;
|
||||
const activeResponseId = this.props.activeResponseId;
|
||||
this._getResponse(activeRequestId, activeResponseId);
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -78,51 +85,19 @@ class ResponsePane extends Component {
|
||||
request,
|
||||
previewMode,
|
||||
handleSetPreviewMode,
|
||||
handleSetActiveResponse,
|
||||
handleDeleteResponses,
|
||||
handleSetFilter,
|
||||
loadStartTime,
|
||||
editorLineWrapping,
|
||||
editorFontSize,
|
||||
filter,
|
||||
activeResponseId,
|
||||
showCookiesModal
|
||||
} = this.props;
|
||||
|
||||
const {response} = this.state;
|
||||
|
||||
let timer = null;
|
||||
|
||||
if (loadStartTime >= 0) {
|
||||
// Set a timer to update the UI again soon
|
||||
// TODO: Move this into a child component so we don't rerender too much
|
||||
setTimeout(() => {
|
||||
this.forceUpdate();
|
||||
}, 100);
|
||||
|
||||
// NOTE: subtract 200ms because the request has some time on either end
|
||||
const millis = Date.now() - loadStartTime - 200;
|
||||
const elapsedTime = Math.round(millis / 100) / 10;
|
||||
|
||||
timer = (
|
||||
<div className="response-pane__overlay">
|
||||
{elapsedTime > REQUEST_TIME_TO_SHOW_COUNTER ? (
|
||||
<h2>{elapsedTime} seconds...</h2>
|
||||
) : (
|
||||
<h2>Loading...</h2>
|
||||
)}
|
||||
|
||||
<br/>
|
||||
<i className="fa fa-refresh fa-spin"></i>
|
||||
|
||||
<br/>
|
||||
<div className="pad">
|
||||
<button className="btn btn--clicky"
|
||||
onClick={() => cancelCurrentRequest()}>
|
||||
Cancel Request
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!request) {
|
||||
return (
|
||||
<section className="response-pane pane">
|
||||
@ -135,7 +110,11 @@ class ResponsePane extends Component {
|
||||
if (!response) {
|
||||
return (
|
||||
<section className="response-pane pane">
|
||||
{timer}
|
||||
<ResponseTimer
|
||||
className="response-pane__overlay"
|
||||
handleCancel={cancelCurrentRequest}
|
||||
loadStartTime={loadStartTime}
|
||||
/>
|
||||
|
||||
<header className="pane__header"></header>
|
||||
<div className="pane__body pane__body--placeholder">
|
||||
@ -178,15 +157,31 @@ class ResponsePane extends Component {
|
||||
|
||||
return (
|
||||
<section className="response-pane pane">
|
||||
{timer}
|
||||
<ResponseTimer
|
||||
className="response-pane__overlay"
|
||||
handleCancel={cancelCurrentRequest}
|
||||
loadStartTime={loadStartTime}
|
||||
/>
|
||||
{!response ? null : (
|
||||
<header className="pane__header">
|
||||
<StatusTag
|
||||
statusCode={response.statusCode}
|
||||
statusMessage={response.statusMessage}
|
||||
<header className="pane__header row-spaced">
|
||||
<div className="no-wrap scrollable scrollable--no-bars pad-left">
|
||||
<StatusTag
|
||||
statusCode={response.statusCode}
|
||||
statusMessage={response.statusMessage || null}
|
||||
/>
|
||||
<TimeTag milliseconds={response.elapsedTime} startTime={response.created}/>
|
||||
<SizeTag bytes={response.bytesRead}/>
|
||||
</div>
|
||||
<ResponseHistoryDropdown
|
||||
requestId={request._id}
|
||||
isLatestResponseActive={!activeResponseId}
|
||||
activeResponseId={response._id}
|
||||
handleSetActiveResponse={handleSetActiveResponse}
|
||||
handleDeleteResponses={handleDeleteResponses}
|
||||
onChange={() => null}
|
||||
className="tall pane__header__right"
|
||||
right={true}
|
||||
/>
|
||||
<TimeTag milliseconds={response.elapsedTime}/>
|
||||
<SizeTag bytes={response.bytesRead}/>
|
||||
</header>
|
||||
)}
|
||||
<Tabs className="pane__body">
|
||||
@ -265,6 +260,8 @@ ResponsePane.propTypes = {
|
||||
handleSetFilter: PropTypes.func.isRequired,
|
||||
showCookiesModal: PropTypes.func.isRequired,
|
||||
handleSetPreviewMode: PropTypes.func.isRequired,
|
||||
handleSetActiveResponse: PropTypes.func.isRequired,
|
||||
handleDeleteResponses: PropTypes.func.isRequired,
|
||||
|
||||
// Required
|
||||
previewMode: PropTypes.string.isRequired,
|
||||
@ -272,6 +269,7 @@ ResponsePane.propTypes = {
|
||||
editorFontSize: PropTypes.number.isRequired,
|
||||
editorLineWrapping: PropTypes.bool.isRequired,
|
||||
loadStartTime: PropTypes.number.isRequired,
|
||||
activeResponseId: PropTypes.string.isRequired,
|
||||
|
||||
// Other
|
||||
request: PropTypes.object,
|
||||
|
47
app/ui/components/ResponseTimer.js
Normal file
47
app/ui/components/ResponseTimer.js
Normal file
@ -0,0 +1,47 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {REQUEST_TIME_TO_SHOW_COUNTER} from '../../common/constants';
|
||||
|
||||
class ResponseTimer extends Component {
|
||||
render () {
|
||||
const {loadStartTime, className, handleCancel} = this.props;
|
||||
|
||||
if (loadStartTime < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set a timer to update the UI again soon
|
||||
setTimeout(() => {
|
||||
this.forceUpdate();
|
||||
}, 100);
|
||||
|
||||
const millis = Date.now() - loadStartTime - 200;
|
||||
const elapsedTime = Math.round(millis / 100) / 10;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{elapsedTime > REQUEST_TIME_TO_SHOW_COUNTER ? (
|
||||
<h2>{elapsedTime} seconds...</h2>
|
||||
) : (
|
||||
<h2>Loading...</h2>
|
||||
)}
|
||||
|
||||
<br/>
|
||||
<i className="fa fa-refresh fa-spin"></i>
|
||||
|
||||
<br/>
|
||||
<div className="pad">
|
||||
<button className="btn btn--clicky" onClick={handleCancel}>
|
||||
Cancel Request
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ResponseTimer.propTypes = {
|
||||
handleCancel: PropTypes.func.isRequired,
|
||||
loadStartTime: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default ResponseTimer;
|
@ -5,6 +5,7 @@ import WorkspaceEnvironmentsEditModal from '../components/modals/WorkspaceEnviro
|
||||
import CookiesModal from '../components/modals/CookiesModal';
|
||||
import EnvironmentEditModal from '../components/modals/EnvironmentEditModal';
|
||||
import RequestSwitcherModal from '../components/modals/RequestSwitcherModal';
|
||||
import RequestCreateModal from '../components/modals/RequestCreateModal';
|
||||
import GenerateCodeModal from '../components/modals/GenerateCodeModal';
|
||||
import PromptModal from '../components/modals/PromptModal';
|
||||
import AlertModal from '../components/modals/AlertModal';
|
||||
@ -78,9 +79,15 @@ class Wrapper extends Component {
|
||||
_handleImportFile = () => this.props.handleImportFileToWorkspace(this.props.activeWorkspace._id);
|
||||
_handleExportWorkspaceToFile = () => this.props.handleExportFile(this.props.activeWorkspace._id);
|
||||
_handleSetSidebarFilter = filter => this.props.handleSetSidebarFilter(this.props.activeWorkspace._id, filter);
|
||||
_handleSetActiveResponse = responseId => this.props.handleSetActiveResponse(this.props.activeRequest._id, responseId);
|
||||
_handleShowEnvironmentsModal = () => showModal(WorkspaceEnvironmentsEditModal, this.props.activeWorkspace);
|
||||
_handleShowCookiesModal = () => showModal(CookiesModal, this.props.activeWorkspace);
|
||||
|
||||
_handleDeleteResponses = () => {
|
||||
models.response.removeForRequest(this.props.activeRequest._id);
|
||||
this._handleSetActiveResponse(null);
|
||||
};
|
||||
|
||||
_handleSendRequestWithActiveEnvironment = () => {
|
||||
const {activeRequest, activeEnvironment, handleSendRequestWithEnvironment} = this.props;
|
||||
const activeRequestId = activeRequest ? activeRequest._id : 'n/a';
|
||||
@ -106,40 +113,42 @@ class Wrapper extends Component {
|
||||
|
||||
render () {
|
||||
const {
|
||||
isLoading,
|
||||
loadStartTime,
|
||||
activeWorkspace,
|
||||
activeRequest,
|
||||
activeEnvironment,
|
||||
sidebarHidden,
|
||||
sidebarFilter,
|
||||
sidebarWidth,
|
||||
paneWidth,
|
||||
forceRefreshCounter,
|
||||
workspaces,
|
||||
workspaceChildren,
|
||||
settings,
|
||||
activeRequest,
|
||||
activeResponseId,
|
||||
activeWorkspace,
|
||||
environments,
|
||||
responsePreviewMode,
|
||||
responseFilter,
|
||||
forceRefreshCounter,
|
||||
handleActivateRequest,
|
||||
handleCreateRequest,
|
||||
handleCreateRequestForWorkspace,
|
||||
handleCreateRequestGroup,
|
||||
handleDuplicateRequest,
|
||||
handleExportFile,
|
||||
handleActivateRequest,
|
||||
handleSetActiveWorkspace,
|
||||
handleSetActiveEnvironment,
|
||||
handleSetRequestGroupCollapsed,
|
||||
handleMoveRequest,
|
||||
handleMoveRequestGroup,
|
||||
handleResetDragPane,
|
||||
handleResetDragSidebar,
|
||||
handleSetActiveEnvironment,
|
||||
handleSetActiveWorkspace,
|
||||
handleSetRequestGroupCollapsed,
|
||||
handleSetRequestPaneRef,
|
||||
handleSetResponsePaneRef,
|
||||
handleSetSidebarRef,
|
||||
handleStartDragSidebar,
|
||||
handleResetDragSidebar,
|
||||
handleStartDragPane,
|
||||
handleResetDragPane,
|
||||
handleStartDragSidebar,
|
||||
isLoading,
|
||||
loadStartTime,
|
||||
paneWidth,
|
||||
responseFilter,
|
||||
responsePreviewMode,
|
||||
settings,
|
||||
sidebarChildren,
|
||||
sidebarFilter,
|
||||
sidebarHidden,
|
||||
sidebarWidth,
|
||||
workspaceChildren,
|
||||
workspaces,
|
||||
} = this.props;
|
||||
|
||||
const realSidebarWidth = sidebarHidden ? 0 : sidebarWidth;
|
||||
@ -160,6 +169,7 @@ class Wrapper extends Component {
|
||||
handleImportFile={this._handleImportFile}
|
||||
handleExportFile={handleExportFile}
|
||||
handleSetActiveWorkspace={handleSetActiveWorkspace}
|
||||
handleDuplicateRequest={handleDuplicateRequest}
|
||||
handleSetActiveEnvironment={handleSetActiveEnvironment}
|
||||
moveRequest={handleMoveRequest}
|
||||
moveRequestGroup={handleMoveRequestGroup}
|
||||
@ -218,10 +228,13 @@ class Wrapper extends Component {
|
||||
editorFontSize={settings.editorFontSize}
|
||||
editorLineWrapping={settings.editorLineWrapping}
|
||||
previewMode={responsePreviewMode}
|
||||
activeResponseId={activeResponseId}
|
||||
filter={responseFilter}
|
||||
loadStartTime={loadStartTime}
|
||||
showCookiesModal={this._handleShowCookiesModal}
|
||||
handleSetActiveResponse={this._handleSetActiveResponse}
|
||||
handleSetPreviewMode={this._handleSetPreviewMode}
|
||||
handleDeleteResponses={this._handleDeleteResponses}
|
||||
handleSetFilter={this._handleSetResponseFilter}
|
||||
/>
|
||||
|
||||
@ -233,6 +246,7 @@ class Wrapper extends Component {
|
||||
<PromptModal ref={registerModal}/>
|
||||
<SignupModal ref={registerModal}/>
|
||||
<PaymentModal ref={registerModal}/>
|
||||
<RequestCreateModal ref={registerModal}/>
|
||||
<PaymentNotificationModal ref={registerModal}/>
|
||||
<EnvironmentEditModal
|
||||
ref={registerModal}
|
||||
@ -279,12 +293,14 @@ Wrapper.propTypes = {
|
||||
handleMoveRequest: PropTypes.func.isRequired,
|
||||
handleMoveRequestGroup: PropTypes.func.isRequired,
|
||||
handleCreateRequest: PropTypes.func.isRequired,
|
||||
handleDuplicateRequest: PropTypes.func.isRequired,
|
||||
handleCreateRequestGroup: PropTypes.func.isRequired,
|
||||
handleCreateRequestForWorkspace: PropTypes.func.isRequired,
|
||||
handleSetRequestPaneRef: PropTypes.func.isRequired,
|
||||
handleSetResponsePaneRef: PropTypes.func.isRequired,
|
||||
handleSetResponsePreviewMode: PropTypes.func.isRequired,
|
||||
handleSetResponseFilter: PropTypes.func.isRequired,
|
||||
handleSetActiveResponse: PropTypes.func.isRequired,
|
||||
handleSetSidebarRef: PropTypes.func.isRequired,
|
||||
handleStartDragSidebar: PropTypes.func.isRequired,
|
||||
handleResetDragSidebar: PropTypes.func.isRequired,
|
||||
@ -299,6 +315,7 @@ Wrapper.propTypes = {
|
||||
paneWidth: PropTypes.number.isRequired,
|
||||
responsePreviewMode: PropTypes.string.isRequired,
|
||||
responseFilter: PropTypes.string.isRequired,
|
||||
activeResponseId: PropTypes.string.isRequired,
|
||||
sidebarWidth: PropTypes.number.isRequired,
|
||||
sidebarHidden: PropTypes.bool.isRequired,
|
||||
sidebarFilter: PropTypes.string.isRequired,
|
||||
|
@ -221,11 +221,11 @@ class KeyValueEditor extends Component {
|
||||
<FileInputButton
|
||||
showFileName={true}
|
||||
className="btn btn--clicky wide ellipsis txt-sm"
|
||||
path={pair.fileName || ''}
|
||||
onChange={fileName => {
|
||||
this._updatePair(i, {fileName});
|
||||
this.props.onChooseFile && this.props.onChooseFile();
|
||||
}}
|
||||
path={pair.fileName || ''}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
@ -234,15 +234,13 @@ class KeyValueEditor extends Component {
|
||||
ref={n => this._valueInputs[i] = n}
|
||||
defaultValue={pair.value}
|
||||
onChange={e => this._updatePair(i, {value: e.target.value})}
|
||||
onBlur={() => this._focusedPair = -1}
|
||||
onKeyDown={this._keyDown.bind(this)}
|
||||
onFocus={e => {
|
||||
this._focusedPair = i;
|
||||
this._focusedField = VALUE;
|
||||
this._focusedInput = e.target;
|
||||
}}
|
||||
onBlur={() => {
|
||||
this._focusedPair = -1
|
||||
}}
|
||||
onKeyDown={this._keyDown.bind(this)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -6,47 +6,64 @@ import DropdownItem from './DropdownItem';
|
||||
import DropdownDivider from './DropdownDivider';
|
||||
|
||||
class Dropdown extends Component {
|
||||
state = {open: false, dropUp: false};
|
||||
state = {
|
||||
open: false,
|
||||
dropUp: false,
|
||||
focused: false,
|
||||
};
|
||||
|
||||
_handleClick () {
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
_addKeyListener () {
|
||||
this._bodyKeydownHandler = e => {
|
||||
if (!this.state.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Catch all key presses if we're open
|
||||
_handleKeyDown = e => {
|
||||
// Catch all key presses if we're open
|
||||
if (this.state.open) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Pressed escape?
|
||||
if (e.keyCode === 27) {
|
||||
e.preventDefault();
|
||||
this.hide();
|
||||
}
|
||||
};
|
||||
// Pressed escape?
|
||||
if (this.state.open && e.keyCode === 27) {
|
||||
e.preventDefault();
|
||||
this.hide();
|
||||
}
|
||||
};
|
||||
|
||||
document.body.addEventListener('keydown', this._bodyKeydownHandler);
|
||||
}
|
||||
_checkSize = () => {
|
||||
if (!this.state.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
_removeKeyListener () {
|
||||
document.body.removeEventListener('keydown', this._bodyKeydownHandler);
|
||||
}
|
||||
// Make the dropdown scroll if it drops off screen.
|
||||
const rect = this._dropdownList.getBoundingClientRect();
|
||||
const maxHeight = document.body.clientHeight - rect.top - 10;
|
||||
this._dropdownList.style.maxHeight = `${maxHeight}px`;
|
||||
};
|
||||
|
||||
_handleClick = () => {
|
||||
this.toggle();
|
||||
};
|
||||
|
||||
_handleMouseDown = e => {
|
||||
// Intercept mouse down so that clicks don't trigger things like
|
||||
// drag and drop.
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
_addDropdownListRef = n => this._dropdownList = n;
|
||||
|
||||
componentDidUpdate () {
|
||||
// Make the dropdown scroll if it drops off screen.
|
||||
if (this.state.open) {
|
||||
const rect = this._dropdownList.getBoundingClientRect();
|
||||
const maxHeight = document.body.clientHeight - rect.top - 10;
|
||||
this._dropdownList.style.maxHeight = `${maxHeight}px`;
|
||||
}
|
||||
this._checkSize();
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
document.body.addEventListener('keydown', this._handleKeyDown);
|
||||
window.addEventListener('resize', this._checkSize);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.body.removeEventListener('keydown', this._handleKeyDown);
|
||||
window.removeEventListener('resize', this._checkSize);
|
||||
}
|
||||
|
||||
hide () {
|
||||
this.setState({open: false});
|
||||
this._removeKeyListener();
|
||||
}
|
||||
|
||||
show () {
|
||||
@ -55,7 +72,6 @@ class Dropdown extends Component {
|
||||
const dropUp = dropdownTop > bodyHeight * 0.65;
|
||||
|
||||
this.setState({open: true, dropUp});
|
||||
this._addKeyListener();
|
||||
}
|
||||
|
||||
toggle () {
|
||||
@ -66,10 +82,6 @@ class Dropdown extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._removeKeyListener();
|
||||
}
|
||||
|
||||
_getFlattenedChildren (children) {
|
||||
let newChildren = [];
|
||||
|
||||
@ -119,18 +131,20 @@ class Dropdown extends Component {
|
||||
if (dropdownButtons.length !== 1) {
|
||||
console.error(`Dropdown needs exactly one DropdownButton! Got ${dropdownButtons.length}`, this.props);
|
||||
} else if (dropdownItems.length === 0) {
|
||||
console.error(`Dropdown needs at least one DropdownItem!`);
|
||||
children = dropdownButtons;
|
||||
} else {
|
||||
children = [
|
||||
dropdownButtons[0],
|
||||
<ul key="items" ref={n => this._dropdownList = n}>{dropdownItems}</ul>
|
||||
<ul key="items" ref={this._addDropdownListRef}>
|
||||
{dropdownItems}
|
||||
</ul>
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}
|
||||
onClick={this._handleClick.bind(this)}
|
||||
onMouseDown={e => e.preventDefault()}>
|
||||
onClick={this._handleClick}
|
||||
onMouseDown={this._handleMouseDown}>
|
||||
{children}
|
||||
<div className="dropdown__backdrop"></div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const DropdownButton = ({children, ...props}) => (
|
||||
<button {...props}>
|
||||
<button type="button" {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
@ -17,7 +17,7 @@ const DropdownDivider = ({name}) => {
|
||||
};
|
||||
|
||||
DropdownDivider.propTypes = {
|
||||
name: PropTypes.string
|
||||
name: PropTypes.any
|
||||
};
|
||||
|
||||
export default DropdownDivider;
|
||||
|
@ -1,27 +1,57 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React, {PureComponent, PropTypes} from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const DropdownItem = ({stayOpenAfterClick, buttonClass, onClick, children, className, ...props}) => {
|
||||
const inner = (
|
||||
<div className={classnames('dropdown__inner', className)}>
|
||||
<span className="dropdown__text">{children}</span>
|
||||
</div>
|
||||
);
|
||||
class DropdownItem extends PureComponent {
|
||||
_handleClick = e => {
|
||||
const {stayOpenAfterClick, onClick, disabled} = this.props;
|
||||
|
||||
const buttonProps = {
|
||||
onClick: stayOpenAfterClick ? e => {e.stopPropagation(); onClick(e)} : onClick,
|
||||
...props
|
||||
if (stayOpenAfterClick) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
if (!onClick || disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.hasOwnProperty('value')) {
|
||||
onClick(this.props.value, e);
|
||||
} else {
|
||||
onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
const button = React.createElement(buttonClass || 'button', buttonProps, inner);
|
||||
return (
|
||||
<li>{button}</li>
|
||||
)
|
||||
};
|
||||
render () {
|
||||
const {
|
||||
buttonClass,
|
||||
children,
|
||||
className,
|
||||
onClick, // Don't want this in ...props
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
const inner = (
|
||||
<div className={classnames('dropdown__inner', className)}>
|
||||
<span className="dropdown__text">{children}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const buttonProps = {
|
||||
type: 'button',
|
||||
onClick: this._handleClick,
|
||||
...props
|
||||
};
|
||||
|
||||
const button = React.createElement(buttonClass || 'button', buttonProps, inner);
|
||||
return (
|
||||
<li>{button}</li>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownItem.propTypes = {
|
||||
buttonClass: PropTypes.any,
|
||||
stayOpenAfterClick: PropTypes.bool
|
||||
stayOpenAfterClick: PropTypes.bool,
|
||||
value: PropTypes.any,
|
||||
};
|
||||
|
||||
export default DropdownItem;
|
||||
|
@ -5,43 +5,55 @@ import {trackEvent} from '../../../analytics/index';
|
||||
import * as constants from '../../../common/constants';
|
||||
import {getContentTypeName} from '../../../common/constants';
|
||||
|
||||
const EMPTY_MIME_TYPE = null;
|
||||
|
||||
class ContentTypeDropdown extends Component {
|
||||
_renderDropdownItem (mimeType, iconClass, forcedName = null) {
|
||||
_handleChangeMimeType = mimeType => {
|
||||
this.props.onChange(mimeType);
|
||||
trackEvent('Request', 'Content-Type Change', contentTypesMap[mimeType]);
|
||||
};
|
||||
|
||||
_renderDropdownItem (mimeType, forcedName = null) {
|
||||
const contentType = typeof this.props.contentType !== 'string' ?
|
||||
EMPTY_MIME_TYPE : this.props.contentType;
|
||||
|
||||
const iconClass = mimeType === contentType ? 'fa-check' : 'fa-empty';
|
||||
|
||||
return (
|
||||
<DropdownItem onClick={e => {
|
||||
this.props.updateRequestMimeType(mimeType);
|
||||
trackEvent('Request', 'Content-Type Change', contentTypesMap[mimeType]);
|
||||
}}>
|
||||
<i className={`fa ${iconClass || 'fa-empty'}`}/>
|
||||
<DropdownItem onClick={this._handleChangeMimeType} value={mimeType}>
|
||||
<i className={`fa ${iconClass}`}/>
|
||||
{forcedName || getContentTypeName(mimeType)}
|
||||
</DropdownItem>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {children, className} = this.props;
|
||||
const {children, className, ...extraProps} = this.props;
|
||||
return (
|
||||
<Dropdown debug="true">
|
||||
<Dropdown debug="true" {...extraProps}>
|
||||
<DropdownButton className={className}>
|
||||
{children}
|
||||
</DropdownButton>
|
||||
<DropdownDivider name="Form Data"/>
|
||||
{this._renderDropdownItem(constants.CONTENT_TYPE_FORM_DATA, 'fa-bars')}
|
||||
{this._renderDropdownItem(constants.CONTENT_TYPE_FORM_URLENCODED, 'fa-bars')}
|
||||
<DropdownDivider name="Raw Text"/>
|
||||
{this._renderDropdownItem(constants.CONTENT_TYPE_JSON, 'fa-code')}
|
||||
{this._renderDropdownItem(constants.CONTENT_TYPE_XML, 'fa-code')}
|
||||
{this._renderDropdownItem(constants.CONTENT_TYPE_OTHER, 'fa-code')}
|
||||
<DropdownDivider name="Other"/>
|
||||
{this._renderDropdownItem(constants.CONTENT_TYPE_FILE, 'fa-file-o')}
|
||||
{this._renderDropdownItem(null, 'fa-ban', 'No Body')}
|
||||
<DropdownDivider name={<span><i className="fa fa-bars"></i> Form Data</span>}/>
|
||||
{this._renderDropdownItem(constants.CONTENT_TYPE_FORM_DATA)}
|
||||
{this._renderDropdownItem(constants.CONTENT_TYPE_FORM_URLENCODED)}
|
||||
<DropdownDivider name={<span><i className="fa fa-code"></i> Raw Text</span>}/>
|
||||
{this._renderDropdownItem(constants.CONTENT_TYPE_JSON)}
|
||||
{this._renderDropdownItem(constants.CONTENT_TYPE_XML)}
|
||||
{this._renderDropdownItem(constants.CONTENT_TYPE_OTHER)}
|
||||
<DropdownDivider name={<span><i className="fa fa-ellipsis-h"></i> Other</span>}/>
|
||||
{this._renderDropdownItem(constants.CONTENT_TYPE_FILE)}
|
||||
{this._renderDropdownItem(EMPTY_MIME_TYPE, 'No Body')}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ContentTypeDropdown.propTypes = {
|
||||
updateRequestMimeType: PropTypes.func.isRequired
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
// Optional
|
||||
contentType: PropTypes.string, // Can be null
|
||||
};
|
||||
|
||||
export default ContentTypeDropdown;
|
||||
|
31
app/ui/components/dropdowns/MethodDropdown.js
Normal file
31
app/ui/components/dropdowns/MethodDropdown.js
Normal file
@ -0,0 +1,31 @@
|
||||
import React, {PropTypes, Component} from 'react';
|
||||
import {Dropdown, DropdownButton, DropdownItem} from '../base/dropdown';
|
||||
import * as constants from '../../../common/constants';
|
||||
|
||||
class MethodDropdown extends Component {
|
||||
render () {
|
||||
const {method, onChange, right, ...extraProps} = this.props;
|
||||
return (
|
||||
<Dropdown className="method-dropdown" right={right}>
|
||||
<DropdownButton type="button" {...extraProps}>
|
||||
{method} <i className="fa fa-caret-down"/>
|
||||
</DropdownButton>
|
||||
{constants.HTTP_METHODS.map(method => (
|
||||
<DropdownItem key={method}
|
||||
className={`http-method-${method}`}
|
||||
onClick={onChange}
|
||||
value={method}>
|
||||
{method}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MethodDropdown.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
method: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default MethodDropdown;
|
@ -1,27 +1,37 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React, {PureComponent, PropTypes} from 'react';
|
||||
import {Dropdown, DropdownDivider, DropdownButton, DropdownItem} from '../base/dropdown';
|
||||
import {PREVIEW_MODES, getPreviewModeName} from '../../../common/constants';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
const PreviewModeDropdown = ({updatePreviewMode, download}) => (
|
||||
<Dropdown>
|
||||
<DropdownButton className="tall">
|
||||
<i className="fa fa-caret-down"></i>
|
||||
</DropdownButton>
|
||||
{PREVIEW_MODES.map(previewMode => (
|
||||
<DropdownItem key={previewMode} onClick={() => {
|
||||
updatePreviewMode(previewMode);
|
||||
trackEvent('Response', 'Preview Mode Change', previewMode);
|
||||
}}>
|
||||
{getPreviewModeName(previewMode)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
<DropdownDivider></DropdownDivider>
|
||||
<DropdownItem onClick={download}>
|
||||
Download
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
);
|
||||
class PreviewModeDropdown extends PureComponent {
|
||||
_handleClick = previewMode => {
|
||||
this.props.updatePreviewMode(previewMode);
|
||||
trackEvent('Response', 'Preview Mode Change', mode);
|
||||
};
|
||||
|
||||
render () {
|
||||
const {download, previewMode} = this.props;
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownButton className="tall">
|
||||
<i className="fa fa-caret-down"></i>
|
||||
</DropdownButton>
|
||||
<DropdownDivider name="Preview Mode"/>
|
||||
{PREVIEW_MODES.map(mode => (
|
||||
<DropdownItem key={mode} onClick={this._handleClick} value={mode}>
|
||||
{previewMode === mode ? <i className="fa fa-check"/> : <i className="fa fa-empty"/>}
|
||||
{getPreviewModeName(mode)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
<DropdownDivider name="Response"/>
|
||||
<DropdownItem onClick={download}>
|
||||
<i className="fa fa-save"></i>
|
||||
Save to File
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PreviewModeDropdown.propTypes = {
|
||||
// Functions
|
||||
|
@ -9,7 +9,19 @@ import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
|
||||
class RequestActionsDropdown extends Component {
|
||||
async _promptUpdateName () {
|
||||
_handleDuplicate = () => {
|
||||
const {request, handleDuplicateRequest} = this.props;
|
||||
handleDuplicateRequest(request);
|
||||
trackEvent('Request', 'Duplicate', 'Request Action');
|
||||
};
|
||||
|
||||
_handleGenerateCode = () => {
|
||||
const {request} = this.props;
|
||||
showModal(GenerateCodeModal, request);
|
||||
trackEvent('Request', 'Generate Code', 'Request Action');
|
||||
};
|
||||
|
||||
_handlePromptUpdateName = async () => {
|
||||
const {request} = this.props;
|
||||
|
||||
const name = await showModal(PromptModal, {
|
||||
@ -19,7 +31,15 @@ class RequestActionsDropdown extends Component {
|
||||
});
|
||||
|
||||
models.request.update(request, {name});
|
||||
}
|
||||
|
||||
trackEvent('Request', 'Rename', 'Request Action');
|
||||
};
|
||||
|
||||
_handleRemove = () => {
|
||||
const {request} = this.props;
|
||||
models.request.remove(request);
|
||||
trackEvent('Request', 'Delete', 'Action');
|
||||
};
|
||||
|
||||
render () {
|
||||
const {request, ...other} = this.props;
|
||||
@ -29,31 +49,17 @@ class RequestActionsDropdown extends Component {
|
||||
<DropdownButton>
|
||||
<i className="fa fa-caret-down"></i>
|
||||
</DropdownButton>
|
||||
<DropdownItem onClick={e => {
|
||||
models.request.duplicate(request);
|
||||
trackEvent('Request', 'Duplicate', 'Request Action');
|
||||
}}>
|
||||
<DropdownItem onClick={this._handleDuplicate}>
|
||||
<i className="fa fa-copy"></i> Duplicate
|
||||
<DropdownHint char="D"></DropdownHint>
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={e => {
|
||||
this._promptUpdateName();
|
||||
trackEvent('Request', 'Rename', 'Request Action');
|
||||
}}>
|
||||
<DropdownItem onClick={this._handlePromptUpdateName}>
|
||||
<i className="fa fa-edit"></i> Rename
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={e => {
|
||||
showModal(GenerateCodeModal, request);
|
||||
trackEvent('Request', 'Generate Code', 'Request Action');
|
||||
}}>
|
||||
<DropdownItem onClick={this._handleGenerateCode}>
|
||||
<i className="fa fa-code"></i> Generate Code
|
||||
</DropdownItem>
|
||||
<DropdownItem buttonClass={PromptButton}
|
||||
onClick={e => {
|
||||
models.request.remove(request);
|
||||
trackEvent('Request', 'Delete', 'Action');
|
||||
}}
|
||||
addIcon={true}>
|
||||
<DropdownItem buttonClass={PromptButton} onClick={this._handleRemove} addIcon={true}>
|
||||
<i className="fa fa-trash-o"></i> Delete
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
@ -62,7 +68,8 @@ class RequestActionsDropdown extends Component {
|
||||
}
|
||||
|
||||
RequestActionsDropdown.propTypes = {
|
||||
request: PropTypes.object.isRequired
|
||||
handleDuplicateRequest: PropTypes.func.isRequired,
|
||||
request: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default RequestActionsDropdown;
|
||||
|
100
app/ui/components/dropdowns/ResponseHistoryDropdown.js
Normal file
100
app/ui/components/dropdowns/ResponseHistoryDropdown.js
Normal file
@ -0,0 +1,100 @@
|
||||
import React, {PropTypes, Component} from 'react';
|
||||
import {Dropdown, DropdownButton, DropdownItem, DropdownDivider} from '../base/dropdown';
|
||||
import SizeTag from '../tags/SizeTag';
|
||||
import StatusTag from '../tags/StatusTag';
|
||||
import TimeTag from '../tags/TimeTag';
|
||||
import * as models from '../../../models/index';
|
||||
import PromptButton from '../base/PromptButton';
|
||||
|
||||
class ResponseHistoryDropdown extends Component {
|
||||
state = {
|
||||
responses: [],
|
||||
};
|
||||
|
||||
_handleDeleteResponses = () => {
|
||||
this.props.handleDeleteResponses(this.props.requestId);
|
||||
};
|
||||
|
||||
async _load (requestId) {
|
||||
const responses = await models.response.findRecentForRequest(requestId);
|
||||
|
||||
// NOTE: this is bad practice, but I can't figure out a better way.
|
||||
// This component may not be mounted if the user switches to a request that
|
||||
// doesn't have a response
|
||||
if (!this._unmounted) {
|
||||
this.setState({responses});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this._load(nextProps.requestId);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._unmounted = false;
|
||||
this._load(this.props.requestId);
|
||||
}
|
||||
|
||||
renderDropdownItem = (response, i) => {
|
||||
const {activeResponseId, handleSetActiveResponse} = this.props;
|
||||
const active = response._id === activeResponseId;
|
||||
return (
|
||||
<DropdownItem key={response._id}
|
||||
disabled={active}
|
||||
value={i === 0 ? null : response._id}
|
||||
onClick={handleSetActiveResponse}>
|
||||
{active ? <i className="fa fa-thumb-tack"/> : <i className="fa fa-empty"/>}
|
||||
<StatusTag statusCode={response.statusCode}
|
||||
statusMessage={response.statusMessage || 'Error'}
|
||||
small={true}/>
|
||||
<TimeTag milliseconds={response.elapsedTime} small={true}/>
|
||||
<SizeTag bytes={response.bytesRead} small={true}/>
|
||||
</DropdownItem>
|
||||
)
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
activeResponseId,
|
||||
handleSetActiveResponse,
|
||||
handleDeleteResponses,
|
||||
isLatestResponseActive,
|
||||
...extraProps
|
||||
} = this.props;
|
||||
const {responses} = this.state;
|
||||
|
||||
return (
|
||||
<Dropdown {...extraProps}>
|
||||
<DropdownButton className="btn btn--super-compact tall">
|
||||
{isLatestResponseActive ?
|
||||
<i className="fa fa-history"/> :
|
||||
<i className="fa fa-thumb-tack"/>}
|
||||
</DropdownButton>
|
||||
<DropdownDivider name="Response History"/>
|
||||
<DropdownItem buttonClass={PromptButton}
|
||||
addIcon={true}
|
||||
onClick={this._handleDeleteResponses}>
|
||||
<i className="fa fa-trash-o"/>
|
||||
Clear History
|
||||
</DropdownItem>
|
||||
<DropdownDivider name="Past Responses"/>
|
||||
{responses.map(this.renderDropdownItem)}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ResponseHistoryDropdown.propTypes = {
|
||||
handleSetActiveResponse: PropTypes.func.isRequired,
|
||||
handleDeleteResponses: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
requestId: PropTypes.string.isRequired,
|
||||
activeResponseId: PropTypes.string.isRequired,
|
||||
isLatestResponseActive: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default ResponseHistoryDropdown;
|
@ -78,7 +78,7 @@ class BodyEditor extends PureComponent {
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className="pad center-container text-center">
|
||||
<div className="pad valign-center text-center">
|
||||
<p className="pad super-faint text-sm text-center">
|
||||
<i className="fa fa-ban" style={{fontSize: '6rem', opacity: 0.2}}></i>
|
||||
<br/>
|
||||
|
@ -160,7 +160,7 @@ class PaymentModal extends Component {
|
||||
<Nl2Br className="notice info">{message}</Nl2Br>
|
||||
) : null}
|
||||
<div>
|
||||
<div className="pad-left-half">
|
||||
<div className="pad-left-sm">
|
||||
<div className="inline-block text-center"
|
||||
style={{width: '50%'}}>
|
||||
<input
|
||||
@ -222,7 +222,7 @@ class PaymentModal extends Component {
|
||||
<label htmlFor="payment-expiry">
|
||||
Expiration Date
|
||||
</label>
|
||||
<div className="pad-left-half pad-top-sm">
|
||||
<div className="pad-left-sm pad-top-sm">
|
||||
<select name="payment-expiration-month"
|
||||
id="payment-expiration-month"
|
||||
ref={n => this._expiryMonthInput = n}>
|
||||
|
@ -12,6 +12,16 @@ import {trackEvent} from '../../../analytics';
|
||||
let hidePaymentNotificationUntilNextLaunch = false;
|
||||
|
||||
class PaymentNotificationModal extends Component {
|
||||
_handleHide = () => {
|
||||
this.hide();
|
||||
};
|
||||
|
||||
_handleProceedToPayment = () => {
|
||||
this.hide();
|
||||
showModal(PaymentModal);
|
||||
trackEvent('Billing', 'Trial Ended', 'Proceed')
|
||||
};
|
||||
|
||||
show () {
|
||||
// Don't trigger automatically if user has dismissed it already
|
||||
if (hidePaymentNotificationUntilNextLaunch) {
|
||||
@ -42,23 +52,17 @@ class PaymentNotificationModal extends Component {
|
||||
</p>
|
||||
<br/>
|
||||
<p className="pad-top">
|
||||
<button className="btn btn--compact btn--outlined" onClick={e => {
|
||||
this.hide();
|
||||
showModal(PaymentModal);
|
||||
trackEvent('Billing', 'Trial Ended', 'Proceed')
|
||||
}}>
|
||||
<button className="btn btn--compact btn--outlined"
|
||||
onClick={this._handleProceedToPayment}>
|
||||
Proceed to Billing
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button className="btn" onClick={e => {
|
||||
this.hide();
|
||||
showModal(PaymentModal);
|
||||
}}>Maybe Later
|
||||
<button className="btn" onClick={this._handleHide}>
|
||||
Maybe Later
|
||||
</button>
|
||||
|
||||
<div></div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
151
app/ui/components/modals/RequestCreateModal.js
Normal file
151
app/ui/components/modals/RequestCreateModal.js
Normal file
@ -0,0 +1,151 @@
|
||||
import React, {Component} from 'react';
|
||||
import ContentTypeDropdown from '../dropdowns/ContentTypeDropdown';
|
||||
import MethodDropdown from '../dropdowns/MethodDropdown';
|
||||
import Modal from '../base/Modal';
|
||||
import ModalBody from '../base/ModalBody';
|
||||
import ModalHeader from '../base/ModalHeader';
|
||||
import ModalFooter from '../base/ModalFooter';
|
||||
import {getContentTypeName, METHOD_GET, METHOD_HEAD, METHOD_OPTIONS} from '../../../common/constants';
|
||||
import * as models from '../../../models/index';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
|
||||
class RequestCreateModal extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
let contentType;
|
||||
try {
|
||||
contentType = JSON.parse(localStorage.getItem('insomnia::createRequest::contentType'));
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
let method;
|
||||
try {
|
||||
method = JSON.parse(localStorage.getItem('insomnia::createRequest::method'));
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
this.state = {
|
||||
selectedContentType: contentType || null,
|
||||
selectedMethod: method || METHOD_GET,
|
||||
parentId: null,
|
||||
};
|
||||
}
|
||||
|
||||
_handleSubmit = async e => {
|
||||
e.preventDefault();
|
||||
|
||||
const {parentId, selectedContentType, selectedMethod} = this.state;
|
||||
const request = models.initModel(models.request.type, {
|
||||
parentId,
|
||||
name: this._input.value,
|
||||
method: selectedMethod,
|
||||
});
|
||||
|
||||
const finalRequest = await models.request.updateMimeType(
|
||||
request,
|
||||
selectedContentType,
|
||||
true,
|
||||
);
|
||||
|
||||
this._onSubmitCallback(finalRequest);
|
||||
|
||||
this.hide();
|
||||
};
|
||||
|
||||
_handleChangeSelectedContentType = selectedContentType => {
|
||||
this.setState({selectedContentType});
|
||||
localStorage.setItem(
|
||||
'insomnia::createRequest::contentType',
|
||||
JSON.stringify(selectedContentType)
|
||||
);
|
||||
trackEvent('Request Create', 'Content Type Change', selectedContentType);
|
||||
};
|
||||
|
||||
_handleChangeSelectedMethod = selectedMethod => {
|
||||
this.setState({selectedMethod});
|
||||
localStorage.setItem(
|
||||
'insomnia::createRequest::method',
|
||||
JSON.stringify(selectedMethod)
|
||||
);
|
||||
trackEvent('Request Create', 'Method Change', selectedMethod);
|
||||
};
|
||||
|
||||
_handleHide = () => this.hide();
|
||||
|
||||
hide () {
|
||||
this.modal.hide();
|
||||
}
|
||||
|
||||
show ({parentId}) {
|
||||
this.modal.show();
|
||||
|
||||
this._input.value = 'My Request';
|
||||
this.setState({parentId});
|
||||
|
||||
// Need to do this after render because modal focuses itself too
|
||||
setTimeout(() => {
|
||||
this._input.focus();
|
||||
this._input.select();
|
||||
}, 100);
|
||||
|
||||
return new Promise(resolve => this._onSubmitCallback = resolve);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {selectedContentType, selectedMethod} = this.state;
|
||||
const shouldNotHaveBody =
|
||||
selectedMethod === METHOD_GET ||
|
||||
selectedMethod === METHOD_HEAD ||
|
||||
selectedMethod === METHOD_OPTIONS;
|
||||
|
||||
return (
|
||||
<Modal ref={m => this.modal = m}>
|
||||
<ModalHeader>Create HTTP Request</ModalHeader>
|
||||
<ModalBody noScroll={true}>
|
||||
<form onSubmit={this._handleSubmit} className="pad row-fill">
|
||||
<div className="form-control form-control--outlined form-control--wide wide">
|
||||
<input ref={n => this._input = n} type="text"/>
|
||||
</div>
|
||||
<div className="pad-left-sm">
|
||||
<MethodDropdown
|
||||
className="btn btn--clicky no-wrap"
|
||||
right={true}
|
||||
method={selectedMethod}
|
||||
onChange={this._handleChangeSelectedMethod}
|
||||
/>
|
||||
</div>
|
||||
{!shouldNotHaveBody ? (
|
||||
<div className="pad-left-sm">
|
||||
<ContentTypeDropdown className="btn btn--clicky no-wrap"
|
||||
right={true}
|
||||
onChange={this._handleChangeSelectedContentType}>
|
||||
{getContentTypeName(selectedContentType)}
|
||||
{" "}
|
||||
<i className="fa fa-caret-down"></i>
|
||||
</ContentTypeDropdown>
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left faint italic txt-sm tall">
|
||||
* hint: 'TIP: Import Curl command by pasting it into the URL bar'
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn" onClick={this._handleHide}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn" onClick={this._handleSubmit}>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RequestCreateModal.propTypes = {};
|
||||
|
||||
export default RequestCreateModal;
|
@ -128,7 +128,7 @@ class SignupModal extends Component {
|
||||
</span>
|
||||
</p>
|
||||
<div className="text-left">
|
||||
<label htmlFor="signup-password-confirm" className="pad-left-half">
|
||||
<label htmlFor="signup-password-confirm" className="pad-left-sm">
|
||||
Confirm your Password
|
||||
</label>
|
||||
<div className="form-control form-control--outlined">
|
||||
|
@ -131,7 +131,7 @@ const SettingsGeneral = ({settings, updateSetting}) => (
|
||||
</div>
|
||||
</div>
|
||||
<div className="inline-block" style={{width: '50%'}}>
|
||||
<div className="pad-left-half">
|
||||
<div className="pad-left-sm">
|
||||
<label htmlFor="setting-https-proxy">
|
||||
HTTPS Proxy
|
||||
</label>
|
||||
|
@ -41,6 +41,7 @@ class Sidebar extends PureComponent {
|
||||
handleChangeFilter,
|
||||
isLoading,
|
||||
handleCreateRequest,
|
||||
handleDuplicateRequest,
|
||||
handleCreateRequestGroup,
|
||||
handleSetRequestGroupCollapsed,
|
||||
moveRequest,
|
||||
@ -92,6 +93,7 @@ class Sidebar extends PureComponent {
|
||||
handleCreateRequest={handleCreateRequest}
|
||||
handleCreateRequestGroup={handleCreateRequestGroup}
|
||||
handleSetRequestGroupCollapsed={handleSetRequestGroupCollapsed}
|
||||
handleDuplicateRequest={handleDuplicateRequest}
|
||||
moveRequest={moveRequest}
|
||||
moveRequestGroup={moveRequestGroup}
|
||||
filter={filter}
|
||||
@ -122,6 +124,7 @@ Sidebar.propTypes = {
|
||||
moveRequestGroup: PropTypes.func.isRequired,
|
||||
handleCreateRequest: PropTypes.func.isRequired,
|
||||
handleCreateRequestGroup: PropTypes.func.isRequired,
|
||||
handleDuplicateRequest: PropTypes.func.isRequired,
|
||||
showEnvironmentsModal: PropTypes.func.isRequired,
|
||||
showCookiesModal: PropTypes.func.isRequired,
|
||||
|
||||
|
@ -37,6 +37,7 @@ class SidebarChildren extends PureComponent {
|
||||
handleCreateRequest,
|
||||
handleCreateRequestGroup,
|
||||
handleSetRequestGroupCollapsed,
|
||||
handleDuplicateRequest,
|
||||
moveRequest,
|
||||
moveRequestGroup,
|
||||
handleActivateRequest,
|
||||
@ -61,6 +62,7 @@ class SidebarChildren extends PureComponent {
|
||||
handleActivateRequest={handleActivateRequest}
|
||||
requestCreate={handleCreateRequest}
|
||||
isActive={child.doc._id === activeRequestId}
|
||||
handleDuplicateRequest={handleDuplicateRequest}
|
||||
request={child.doc}
|
||||
workspace={workspace}
|
||||
/>
|
||||
@ -129,6 +131,7 @@ SidebarChildren.propTypes = {
|
||||
handleCreateRequest: PropTypes.func.isRequired,
|
||||
handleCreateRequestGroup: PropTypes.func.isRequired,
|
||||
handleSetRequestGroupCollapsed: PropTypes.func.isRequired,
|
||||
handleDuplicateRequest: PropTypes.func.isRequired,
|
||||
moveRequest: PropTypes.func.isRequired,
|
||||
moveRequestGroup: PropTypes.func.isRequired,
|
||||
children: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
@ -38,6 +38,7 @@ class SidebarRequestRow extends PureComponent {
|
||||
|
||||
render () {
|
||||
const {
|
||||
handleDuplicateRequest,
|
||||
connectDragSource,
|
||||
connectDropTarget,
|
||||
isDragging,
|
||||
@ -85,6 +86,7 @@ class SidebarRequestRow extends PureComponent {
|
||||
|
||||
<div className="sidebar__actions">
|
||||
<RequestActionsDropdown
|
||||
handleDuplicateRequest={handleDuplicateRequest}
|
||||
right={true}
|
||||
request={request}
|
||||
requestGroup={requestGroup}
|
||||
@ -103,6 +105,7 @@ class SidebarRequestRow extends PureComponent {
|
||||
SidebarRequestRow.propTypes = {
|
||||
// Functions
|
||||
handleActivateRequest: PropTypes.func.isRequired,
|
||||
handleDuplicateRequest: PropTypes.func.isRequired,
|
||||
requestCreate: PropTypes.func.isRequired,
|
||||
moveRequest: PropTypes.func.isRequired,
|
||||
|
||||
|
@ -14,7 +14,7 @@ const MethodTag = ({method, fullNames}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'tag tag--no-bg tag--small method-' + method}>
|
||||
<div className={'tag tag--no-bg tag--small http-method-' + method}>
|
||||
<span className='tag__inner'>{methodName}</span>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,18 +1,24 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import classnames from 'classnames';
|
||||
import * as misc from '../../../common/misc';
|
||||
|
||||
const SizeTag = props => {
|
||||
const responseSizeString = misc.describeByteSize(props.bytes);
|
||||
const SizeTag = ({bytes, small, className}) => {
|
||||
const responseSizeString = misc.describeByteSize(bytes);
|
||||
|
||||
return (
|
||||
<div className="tag">
|
||||
<div className={classnames('tag', {'tag--small': small}, className)}
|
||||
title={`${bytes} bytes`}>
|
||||
<strong>SIZE</strong> {responseSizeString}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SizeTag.propTypes = {
|
||||
bytes: PropTypes.number.isRequired
|
||||
// Required
|
||||
bytes: PropTypes.number.isRequired,
|
||||
|
||||
// Optional
|
||||
small: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default SizeTag;
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
STATUS_CODE_PEBKAC
|
||||
} from '../../../common/constants';
|
||||
|
||||
const StatusTag = ({statusMessage, statusCode}) => {
|
||||
const StatusTag = ({statusMessage, statusCode, small}) => {
|
||||
statusCode = String(statusCode);
|
||||
|
||||
let colorClass;
|
||||
@ -42,14 +42,21 @@ const StatusTag = ({statusMessage, statusCode}) => {
|
||||
const description = RESPONSE_CODE_DESCRIPTIONS[statusCode] || 'Unknown Response Code';
|
||||
|
||||
return (
|
||||
<div className={classnames('tag', colorClass)} title={description}>
|
||||
<strong>{statusCode}</strong> {statusMessage || backupStatusMessage}
|
||||
<div className={classnames('tag', colorClass, {'tag--small': small})}
|
||||
title={description}>
|
||||
<strong>{statusCode}</strong>
|
||||
{" "}
|
||||
{typeof statusMessage === 'string' ? statusMessage : backupStatusMessage}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StatusTag.propTypes = {
|
||||
// Required
|
||||
statusCode: PropTypes.number.isRequired,
|
||||
|
||||
// Optional
|
||||
small: PropTypes.bool,
|
||||
statusMessage: PropTypes.string
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const TimeTag = ({milliseconds}) => {
|
||||
const TimeTag = ({milliseconds, startTime, small, className}) => {
|
||||
let unit = 'ms';
|
||||
let number = milliseconds;
|
||||
|
||||
@ -15,15 +16,21 @@ const TimeTag = ({milliseconds}) => {
|
||||
// Round to 2 decimal places
|
||||
number = Math.round(number * 100) / 100;
|
||||
|
||||
let description = `${milliseconds} milliseconds`;
|
||||
return (
|
||||
<div className="tag">
|
||||
<div className={classnames('tag', {'tag--small': small}, className)} title={description}>
|
||||
<strong>TIME</strong> {number} {unit}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
TimeTag.propTypes = {
|
||||
milliseconds: PropTypes.number.isRequired
|
||||
// Required
|
||||
milliseconds: PropTypes.number.isRequired,
|
||||
|
||||
// Optional
|
||||
small: PropTypes.bool,
|
||||
startTime: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default TimeTag;
|
||||
|
@ -24,6 +24,7 @@ import * as db from '../../common/database';
|
||||
import * as models from '../../models';
|
||||
import {trackEvent, trackLegacyEvent} from '../../analytics';
|
||||
import {selectEntitiesLists, selectActiveWorkspace, selectSidebarChildren, selectWorkspaceRequestsAndRequestGroups} from '../redux/selectors';
|
||||
import RequestCreateModal from '../components/modals/RequestCreateModal';
|
||||
|
||||
|
||||
class App extends Component {
|
||||
@ -87,14 +88,7 @@ class App extends Component {
|
||||
|
||||
// Request Duplicate
|
||||
'mod+d': async () => {
|
||||
const {activeWorkspace, activeRequest, handleSetActiveRequest} = this.props;
|
||||
|
||||
if (!activeRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = await models.request.duplicate(activeRequest);
|
||||
handleSetActiveRequest(activeWorkspace._id, request._id);
|
||||
this._requestDuplicate(this.props.activeRequest);
|
||||
trackEvent('HotKey', 'Request Duplicate');
|
||||
}
|
||||
};
|
||||
@ -115,20 +109,23 @@ class App extends Component {
|
||||
};
|
||||
|
||||
_requestCreate = async (parentId) => {
|
||||
const name = await showModal(PromptModal, {
|
||||
headerName: 'Create New Request',
|
||||
defaultValue: 'My Request',
|
||||
selectText: true,
|
||||
submitName: 'Create',
|
||||
hint: 'TIP: Import Curl command by pasting it into the URL bar'
|
||||
});
|
||||
|
||||
const request = await showModal(RequestCreateModal, {parentId});
|
||||
const {activeWorkspace, handleSetActiveRequest} = this.props;
|
||||
const request = await models.request.create({parentId, name});
|
||||
|
||||
handleSetActiveRequest(activeWorkspace._id, request._id);
|
||||
};
|
||||
|
||||
_requestDuplicate = async (request) => {
|
||||
const {activeWorkspace, handleSetActiveRequest} = this.props;
|
||||
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRequest = await models.request.duplicate(request);
|
||||
handleSetActiveRequest(activeWorkspace._id, newRequest._id);
|
||||
};
|
||||
|
||||
_requestCreateForWorkspace = () => {
|
||||
this._requestCreate(this.props.activeWorkspace._id);
|
||||
};
|
||||
@ -303,6 +300,7 @@ class App extends Component {
|
||||
handleStartDragPane={this._startDragPane}
|
||||
handleResetDragPane={this._resetDragPane}
|
||||
handleCreateRequest={this._requestCreate}
|
||||
handleDuplicateRequest={this._requestDuplicate}
|
||||
handleCreateRequestGroup={this._requestGroupCreate}
|
||||
{...this.props}
|
||||
/>
|
||||
@ -331,6 +329,7 @@ function mapStateToProps (state, props) {
|
||||
|
||||
const {
|
||||
loadingRequestIds,
|
||||
activeResponseIds,
|
||||
previewModes,
|
||||
responseFilters,
|
||||
} = requestMeta;
|
||||
@ -364,6 +363,9 @@ function mapStateToProps (state, props) {
|
||||
const responsePreviewMode = previewModes[activeRequestId] || PREVIEW_MODE_SOURCE;
|
||||
const responseFilter = responseFilters[activeRequestId] || '';
|
||||
|
||||
// Response Stuff
|
||||
const activeResponseId = activeResponseIds[activeRequestId] || '';
|
||||
|
||||
// Environment stuff
|
||||
const activeEnvironmentId = activeEnvironmentIds[activeWorkspaceId];
|
||||
const activeEnvironment = entities.environments[activeEnvironmentId];
|
||||
@ -382,6 +384,7 @@ function mapStateToProps (state, props) {
|
||||
loadStartTime,
|
||||
activeWorkspace,
|
||||
activeRequest,
|
||||
activeResponseId,
|
||||
sidebarHidden,
|
||||
sidebarFilter,
|
||||
sidebarWidth,
|
||||
@ -416,6 +419,7 @@ function mapDispatchToProps (dispatch) {
|
||||
handleSendRequestWithEnvironment: requests.send,
|
||||
handleSetResponsePreviewMode: requests.setPreviewMode,
|
||||
handleSetResponseFilter: requests.setResponseFilter,
|
||||
handleSetActiveResponse: requests.setActiveResponse,
|
||||
|
||||
handleSetActiveWorkspace: legacyActions.global.setActiveWorkspace,
|
||||
handleImportFileToWorkspace: legacyActions.global.importFile,
|
||||
|
12
app/ui/css/components/methoddropdown.less
Normal file
12
app/ui/css/components/methoddropdown.less
Normal file
@ -0,0 +1,12 @@
|
||||
@import '../constants/dimensions';
|
||||
|
||||
.method-dropdown {
|
||||
.dropdown__inner::before {
|
||||
content: '\25cf';
|
||||
-webkit-text-stroke: 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dropdown__text {
|
||||
padding-left: @padding-sm;
|
||||
}
|
||||
}
|
@ -39,7 +39,7 @@
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
color: @font-super-light-bg;
|
||||
border-radius: @radius-md;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 0 2rem 0 rgba(0, 0, 0, 0.2);
|
||||
width: @modal-width;
|
||||
|
@ -7,6 +7,7 @@
|
||||
grid-template-columns: 100%;
|
||||
|
||||
.pane__header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
@ -20,6 +21,11 @@
|
||||
background: @bg-light;
|
||||
border-left: 1px solid @hl-xs;
|
||||
}
|
||||
|
||||
.pane__header__right {
|
||||
box-shadow: -@padding-md 0 @padding-md -@padding-sm fade(@bg-super-light, 85%);
|
||||
background: @bg-super-light;
|
||||
}
|
||||
}
|
||||
|
||||
.pane__body {
|
||||
|
@ -10,7 +10,7 @@
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background: fade(@bg-super-dark, 80%);
|
||||
z-index: 10;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -112,6 +112,12 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
i.fa {
|
||||
// Bump the drop down caret down a bit
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
|
@ -7,19 +7,19 @@
|
||||
margin-right: 1em;
|
||||
line-height: 1em;
|
||||
box-sizing: border-box;
|
||||
border-radius: @radius-md;
|
||||
border-radius: @radius-sm;
|
||||
text-align: center;
|
||||
background: @hl-sm;
|
||||
border: 1px solid rgba(0, 0, 0, 0.07);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.tag--small {
|
||||
padding: @padding-xs;
|
||||
padding: @padding-xxs @padding-xs;
|
||||
font-size: @font-size-xs;
|
||||
border-radius: @radius-sm;
|
||||
}
|
||||
|
||||
&.tag--no-bg {
|
||||
|
@ -26,26 +26,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown__inner {
|
||||
border-radius: 0;
|
||||
font-size: @font-size-md;
|
||||
padding: 0;
|
||||
|
||||
&::before {
|
||||
content: '\25cf';
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
font-size: 1.2em;
|
||||
-webkit-text-stroke: 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown__text {
|
||||
color: @hl;
|
||||
padding-left: @padding-sm;
|
||||
}
|
||||
|
||||
input {
|
||||
min-width: 0;
|
||||
}
|
||||
|
@ -34,81 +34,81 @@
|
||||
@surprise: #9b81ff;
|
||||
@info: #24cfff;
|
||||
|
||||
[class^="method-"],
|
||||
[class*=" method-"] {
|
||||
[class^="http-method-"],
|
||||
[class*=" http-method-"] {
|
||||
// Default method color
|
||||
color: @hl;
|
||||
}
|
||||
|
||||
.success,
|
||||
.method-POST {
|
||||
.http-method-POST {
|
||||
color: @success !important;
|
||||
}
|
||||
|
||||
.notice,
|
||||
.method-PATCH {
|
||||
.http-method-PATCH {
|
||||
color: @notice !important;
|
||||
}
|
||||
|
||||
.warning,
|
||||
.method-PUT {
|
||||
.http-method-PUT {
|
||||
color: @warning !important;
|
||||
}
|
||||
|
||||
.danger,
|
||||
.method-DELETE {
|
||||
.http-method-DELETE {
|
||||
color: @danger !important;
|
||||
}
|
||||
|
||||
.info,
|
||||
.method-OPTIONS,
|
||||
.method-HEAD {
|
||||
.http-method-OPTIONS,
|
||||
.http-method-HEAD {
|
||||
color: @info !important;
|
||||
}
|
||||
|
||||
.surprise,
|
||||
.method-GET {
|
||||
.http-method-GET {
|
||||
color: @surprise !important;
|
||||
}
|
||||
|
||||
.bg-success,
|
||||
.bg-method-POST {
|
||||
.bg-http-method-POST {
|
||||
background: @success !important;
|
||||
text-shadow: 0 0 0.05em darken(@success, 20);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bg-notice,
|
||||
.bg-method-PATCH {
|
||||
.bg-http-method-PATCH {
|
||||
background: @notice !important;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 0.05em darken(@notice, 20);
|
||||
}
|
||||
|
||||
.bg-warning,
|
||||
.bg-method-PUT {
|
||||
.bg-http-method-PUT {
|
||||
background: @warning !important;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 0.05em darken(@warning, 20);
|
||||
}
|
||||
|
||||
.bg-danger,
|
||||
.bg-method-DELETE {
|
||||
.bg-http-method-DELETE {
|
||||
background: @danger !important;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 0.05em darken(@danger, 20);
|
||||
}
|
||||
|
||||
.bg-info,
|
||||
.bg-method-OPTIONS,
|
||||
.bg-method-HEAD {
|
||||
.bg-http-method-OPTIONS,
|
||||
.bg-http-method-HEAD {
|
||||
background: @info !important;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 0.05em darken(@info, 20);
|
||||
}
|
||||
|
||||
.bg-surprise,
|
||||
.bg-method-GET {
|
||||
.bg-http-method-GET {
|
||||
background: @surprise !important;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 0.05em darken(@surprise, 20);
|
||||
|
@ -20,8 +20,8 @@
|
||||
@line-height-lg: 4.5rem;
|
||||
@line-height-md: 4rem;
|
||||
@line-height-sm: 3rem;
|
||||
@line-height-xs: 2.4rem;
|
||||
@line-height-xxs: 2rem;
|
||||
@line-height-xs: 2.6rem;
|
||||
@line-height-xxs: 2.1rem;
|
||||
@height-nav: @line-height-md;
|
||||
|
||||
/* Sidebar */
|
||||
@ -31,7 +31,7 @@
|
||||
@scrollbar-width: 0.75rem;
|
||||
|
||||
/* Borders */
|
||||
@radius-sm: 0.15rem;
|
||||
@radius-sm: 0.2rem;
|
||||
@radius-md: 0.3rem;
|
||||
|
||||
/* Dropdowns */
|
||||
|
@ -35,3 +35,4 @@
|
||||
@import 'components/keyvalueeditor';
|
||||
@import 'components/editable';
|
||||
@import 'components/responsepane';
|
||||
@import 'components/methoddropdown';
|
||||
|
@ -150,16 +150,6 @@ code, pre, .monospace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.center-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auto-margin {
|
||||
margin: auto;
|
||||
}
|
||||
@ -176,6 +166,40 @@ code, pre, .monospace {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.row-fill {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-content: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row-spaced {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row-stretch {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: stretch;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.valign-center {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -212,6 +236,10 @@ i.fa {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
white-space: normal;
|
||||
}
|
||||
@ -271,15 +299,19 @@ i.fa {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.no-pad-left {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.no-pad-top {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.pad-left-half {
|
||||
.pad-left-sm {
|
||||
padding-left: @padding-md / 2;
|
||||
}
|
||||
|
||||
.pad-right-half {
|
||||
.pad-right-sm {
|
||||
padding-right: @padding-md / 2;
|
||||
}
|
||||
|
||||
@ -327,8 +359,13 @@ i.fa {
|
||||
.scrollable {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
&.scrollable--no-bars::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.scrollable-container {
|
||||
position: relative;
|
||||
|
||||
|
@ -6,8 +6,8 @@ export default function configureStore () {
|
||||
const middleware = [thunkMiddleware];
|
||||
|
||||
if (__DEV__) {
|
||||
// const createLogger = require('redux-logger');
|
||||
// middleware.push(createLogger({collapsed: true}));
|
||||
const createLogger = require('redux-logger');
|
||||
middleware.push(createLogger({collapsed: true}));
|
||||
}
|
||||
|
||||
const store = createStore(reducer, applyMiddleware(...middleware));
|
||||
|
@ -25,29 +25,6 @@ const COMMAND_TRIAL_END = 'app/billing/trial-end';
|
||||
// REDUCERS //
|
||||
// ~~~~~~~~ //
|
||||
|
||||
/** Helper to update requestGroup metadata */
|
||||
function updateRequestGroupMeta (state = {}, requestGroupId, value, key) {
|
||||
const newState = Object.assign({}, state);
|
||||
newState[requestGroupId] = newState[requestGroupId] || {};
|
||||
newState[requestGroupId][key] = value;
|
||||
return newState;
|
||||
}
|
||||
|
||||
function requestGroupMetaReducer (state = {}, action) {
|
||||
switch (action.type) {
|
||||
case REQUEST_GROUP_TOGGLE_COLLAPSE:
|
||||
const meta = state[action.requestGroupId];
|
||||
return updateRequestGroupMeta(
|
||||
state,
|
||||
action.requestGroupId,
|
||||
meta ? !meta.collapsed : false,
|
||||
'collapsed'
|
||||
);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function activeWorkspaceReducer (state = null, action) {
|
||||
switch (action.type) {
|
||||
case SET_ACTIVE_WORKSPACE:
|
||||
@ -68,19 +45,9 @@ function loadingReducer (state = false, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function commandReducer (state = {}, action) {
|
||||
switch (action.type) {
|
||||
// Nothing yet...
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
isLoading: loadingReducer,
|
||||
requestGroupMeta: requestGroupMetaReducer,
|
||||
activeWorkspaceId: activeWorkspaceReducer,
|
||||
command: commandReducer,
|
||||
});
|
||||
|
||||
|
||||
|
@ -8,6 +8,7 @@ const START_LOADING = 'requests/start-loading';
|
||||
const STOP_LOADING = 'requests/stop-loading';
|
||||
const SET_PREVIEW_MODE = 'requests/preview-mode';
|
||||
const SET_RESPONSE_FILTER = 'requests/response-filter';
|
||||
const SET_ACTIVE_RESPONSE = 'requests/active-response';
|
||||
|
||||
|
||||
// ~~~~~~~~ //
|
||||
@ -18,6 +19,7 @@ export default combineReducers({
|
||||
loadingRequestIds: loadingReducer,
|
||||
..._makePropertyReducer(SET_PREVIEW_MODE, 'previewModes', 'previewMode'),
|
||||
..._makePropertyReducer(SET_RESPONSE_FILTER, 'responseFilters', 'filter'),
|
||||
..._makePropertyReducer(SET_ACTIVE_RESPONSE, 'activeResponseIds', 'responseId'),
|
||||
});
|
||||
|
||||
function loadingReducer (state = {}, action) {
|
||||
@ -54,6 +56,11 @@ export function setResponseFilter (requestId, filter) {
|
||||
return {type: SET_RESPONSE_FILTER, requestId, filter};
|
||||
}
|
||||
|
||||
export function setActiveResponse (requestId, responseId) {
|
||||
_setMeta(requestId, 'activeResponseIds', responseId);
|
||||
return {type: SET_ACTIVE_RESPONSE, requestId, responseId};
|
||||
}
|
||||
|
||||
export function send(requestId, environmentId) {
|
||||
return async function (dispatch) {
|
||||
dispatch(startLoading(requestId));
|
||||
@ -67,6 +74,10 @@ export function send(requestId, environmentId) {
|
||||
// It's OK
|
||||
}
|
||||
|
||||
// Unset pinned response
|
||||
dispatch(setActiveResponse(requestId, null));
|
||||
|
||||
// Stop loading
|
||||
dispatch(stopLoading(requestId));
|
||||
}
|
||||
}
|
||||
@ -87,6 +98,7 @@ export function init () {
|
||||
|
||||
callAction('previewModes', setPreviewMode);
|
||||
callAction('responseFilters', setResponseFilter);
|
||||
callAction('activeResponseIds', setActiveResponse);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user