From 6c1c03cef6404635eff1045b52372f5681c3c1c2 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sun, 27 Nov 2016 13:42:38 -0800 Subject: [PATCH] Various Improvements (#59) * Better create, started response history * Response history working * A bunch --- app/common/constants.js | 5 +- app/common/database.js | 19 ++- app/common/network.js | 4 + app/models/request.js | 16 +- app/models/response.js | 28 +++- app/ui/components/RequestPane.js | 4 +- app/ui/components/RequestUrlBar.js | 42 +++-- app/ui/components/ResponsePane.js | 102 ++++++------ app/ui/components/ResponseTimer.js | 47 ++++++ app/ui/components/Wrapper.js | 59 ++++--- app/ui/components/base/KeyValueEditor.js | 8 +- app/ui/components/base/dropdown/Dropdown.js | 92 ++++++----- .../base/dropdown/DropdownButton.js | 2 +- .../base/dropdown/DropdownDivider.js | 2 +- .../components/base/dropdown/DropdownItem.js | 62 +++++-- .../dropdowns/ContentTypeDropdown.js | 50 +++--- app/ui/components/dropdowns/MethodDropdown.js | 31 ++++ .../dropdowns/PreviewModeDropdown.js | 50 +++--- .../dropdowns/RequestActionsDropdown.js | 49 +++--- .../dropdowns/ResponseHistoryDropdown.js | 100 ++++++++++++ app/ui/components/editors/body/BodyEditor.js | 2 +- app/ui/components/modals/PaymentModal.js | 4 +- .../modals/PaymentNotificationModal.js | 24 +-- .../components/modals/RequestCreateModal.js | 151 ++++++++++++++++++ app/ui/components/modals/SignupModal.js | 2 +- app/ui/components/settings/SettingsGeneral.js | 2 +- app/ui/components/sidebar/Sidebar.js | 3 + app/ui/components/sidebar/SidebarChildren.js | 3 + .../components/sidebar/SidebarRequestRow.js | 3 + app/ui/components/tags/MethodTag.js | 2 +- app/ui/components/tags/SizeTag.js | 14 +- app/ui/components/tags/StatusTag.js | 13 +- app/ui/components/tags/TimeTag.js | 15 +- app/ui/containers/App.js | 38 +++-- app/ui/css/components/methoddropdown.less | 12 ++ app/ui/css/components/modal.less | 2 +- app/ui/css/components/pane.less | 6 + app/ui/css/components/responsepane.less | 2 +- app/ui/css/components/sidebar.less | 6 + app/ui/css/components/tag.less | 8 +- app/ui/css/components/urlbar.less | 20 --- app/ui/css/constants/colors.less | 32 ++-- app/ui/css/constants/dimensions.less | 6 +- app/ui/css/index.less | 1 + app/ui/css/layout/base.less | 61 +++++-- app/ui/redux/create.js | 4 +- app/ui/redux/modules/global.js | 33 ---- app/ui/redux/modules/requestMeta.js | 12 ++ 48 files changed, 881 insertions(+), 372 deletions(-) create mode 100644 app/ui/components/ResponseTimer.js create mode 100644 app/ui/components/dropdowns/MethodDropdown.js create mode 100644 app/ui/components/dropdowns/ResponseHistoryDropdown.js create mode 100644 app/ui/components/modals/RequestCreateModal.js create mode 100644 app/ui/css/components/methoddropdown.less diff --git a/app/common/constants.js b/app/common/constants.js index 10e1c7702..0ac44a8b8 100644 --- a/app/common/constants.js +++ b/app/common/constants.js @@ -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'; diff --git a/app/common/database.js b/app/common/database.js index e6e34a8a4..7aff4a88b 100644 --- a/app/common/database.js +++ b/app/common/database.js @@ -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) { diff --git a/app/common/network.js b/app/common/network.js index 45d7164ec..0ef0d781c 100644 --- a/app/common/network.js +++ b/app/common/network.js @@ -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', diff --git a/app/models/request.js b/app/models/request.js index bbf1ffbe2..356b0322c 100644 --- a/app/models/request.js +++ b/app/models/request.js @@ -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) { diff --git a/app/models/response.js b/app/models/response.js index 4b2607e4e..168cd811c 100644 --- a/app/models/response.js +++ b/app/models/response.js @@ -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); } diff --git a/app/ui/components/RequestPane.js b/app/ui/components/RequestPane.js index 0dd9fbf74..f88d33892 100644 --- a/app/ui/components/RequestPane.js +++ b/app/ui/components/RequestPane.js @@ -131,7 +131,9 @@ class RequestPane extends PureComponent { {" "} {numBodyParams ? ({numBodyParams}) : null} - + diff --git a/app/ui/components/RequestUrlBar.js b/app/ui/components/RequestUrlBar.js index 1306f9e27..77a56ad26 100644 --- a/app/ui/components/RequestUrlBar.js +++ b/app/ui/components/RequestUrlBar.js @@ -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 (
- - - {method} - - {METHODS.map(method => ( - { - onMethodChange(method); - trackEvent('Request', 'Method Change', method); - }}> - {method} - - ))} - -
+ + {method} + +
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}/>
-
-
- ) - } - if (!request) { return (
@@ -135,7 +110,11 @@ class ResponsePane extends Component { if (!response) { return (
- {timer} +
@@ -178,15 +157,31 @@ class ResponsePane extends Component { return (
- {timer} + {!response ? null : ( -
- +
+ + + +
+ null} + className="tall pane__header__right" + right={true} /> - -
)} @@ -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, diff --git a/app/ui/components/ResponseTimer.js b/app/ui/components/ResponseTimer.js new file mode 100644 index 000000000..d3dd62c6c --- /dev/null +++ b/app/ui/components/ResponseTimer.js @@ -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 ( +
+ {elapsedTime > REQUEST_TIME_TO_SHOW_COUNTER ? ( +

{elapsedTime} seconds...

+ ) : ( +

Loading...

+ )} + +
+ + +
+
+ +
+
+ ) + } +} + +ResponseTimer.propTypes = { + handleCancel: PropTypes.func.isRequired, + loadStartTime: PropTypes.number.isRequired, +}; + +export default ResponseTimer; diff --git a/app/ui/components/Wrapper.js b/app/ui/components/Wrapper.js index 829bb9394..6f960e3bb 100644 --- a/app/ui/components/Wrapper.js +++ b/app/ui/components/Wrapper.js @@ -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 { + { this._updatePair(i, {fileName}); this.props.onChooseFile && this.props.onChooseFile(); }} - path={pair.fileName || ''} /> ) : ( 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)} /> )}
diff --git a/app/ui/components/base/dropdown/Dropdown.js b/app/ui/components/base/dropdown/Dropdown.js index 2ac2c6093..6181e475c 100644 --- a/app/ui/components/base/dropdown/Dropdown.js +++ b/app/ui/components/base/dropdown/Dropdown.js @@ -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], -
    this._dropdownList = n}>{dropdownItems}
+
    + {dropdownItems} +
] } return (
e.preventDefault()}> + onClick={this._handleClick} + onMouseDown={this._handleMouseDown}> {children}
diff --git a/app/ui/components/base/dropdown/DropdownButton.js b/app/ui/components/base/dropdown/DropdownButton.js index 75e7f3d36..618dcedac 100644 --- a/app/ui/components/base/dropdown/DropdownButton.js +++ b/app/ui/components/base/dropdown/DropdownButton.js @@ -1,7 +1,7 @@ import React from 'react'; const DropdownButton = ({children, ...props}) => ( - ); diff --git a/app/ui/components/base/dropdown/DropdownDivider.js b/app/ui/components/base/dropdown/DropdownDivider.js index 7f18d0593..85c9d02aa 100644 --- a/app/ui/components/base/dropdown/DropdownDivider.js +++ b/app/ui/components/base/dropdown/DropdownDivider.js @@ -17,7 +17,7 @@ const DropdownDivider = ({name}) => { }; DropdownDivider.propTypes = { - name: PropTypes.string + name: PropTypes.any }; export default DropdownDivider; diff --git a/app/ui/components/base/dropdown/DropdownItem.js b/app/ui/components/base/dropdown/DropdownItem.js index 98e1d9225..02df4bde4 100644 --- a/app/ui/components/base/dropdown/DropdownItem.js +++ b/app/ui/components/base/dropdown/DropdownItem.js @@ -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 = ( -
- {children} -
- ); +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 ( -
  • {button}
  • - ) -}; + render () { + const { + buttonClass, + children, + className, + onClick, // Don't want this in ...props + ...props + } = this.props; + + const inner = ( +
    + {children} +
    + ); + + const buttonProps = { + type: 'button', + onClick: this._handleClick, + ...props + }; + + const button = React.createElement(buttonClass || 'button', buttonProps, inner); + return ( +
  • {button}
  • + ) + } +} DropdownItem.propTypes = { buttonClass: PropTypes.any, - stayOpenAfterClick: PropTypes.bool + stayOpenAfterClick: PropTypes.bool, + value: PropTypes.any, }; export default DropdownItem; diff --git a/app/ui/components/dropdowns/ContentTypeDropdown.js b/app/ui/components/dropdowns/ContentTypeDropdown.js index 96042054d..dce2e655d 100644 --- a/app/ui/components/dropdowns/ContentTypeDropdown.js +++ b/app/ui/components/dropdowns/ContentTypeDropdown.js @@ -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 ( - { - this.props.updateRequestMimeType(mimeType); - trackEvent('Request', 'Content-Type Change', contentTypesMap[mimeType]); - }}> - + + {forcedName || getContentTypeName(mimeType)} ) } render () { - const {children, className} = this.props; + const {children, className, ...extraProps} = this.props; return ( - + {children} - - {this._renderDropdownItem(constants.CONTENT_TYPE_FORM_DATA, 'fa-bars')} - {this._renderDropdownItem(constants.CONTENT_TYPE_FORM_URLENCODED, 'fa-bars')} - - {this._renderDropdownItem(constants.CONTENT_TYPE_JSON, 'fa-code')} - {this._renderDropdownItem(constants.CONTENT_TYPE_XML, 'fa-code')} - {this._renderDropdownItem(constants.CONTENT_TYPE_OTHER, 'fa-code')} - - {this._renderDropdownItem(constants.CONTENT_TYPE_FILE, 'fa-file-o')} - {this._renderDropdownItem(null, 'fa-ban', 'No Body')} + Form Data}/> + {this._renderDropdownItem(constants.CONTENT_TYPE_FORM_DATA)} + {this._renderDropdownItem(constants.CONTENT_TYPE_FORM_URLENCODED)} + Raw Text}/> + {this._renderDropdownItem(constants.CONTENT_TYPE_JSON)} + {this._renderDropdownItem(constants.CONTENT_TYPE_XML)} + {this._renderDropdownItem(constants.CONTENT_TYPE_OTHER)} + Other}/> + {this._renderDropdownItem(constants.CONTENT_TYPE_FILE)} + {this._renderDropdownItem(EMPTY_MIME_TYPE, 'No Body')} ) } } ContentTypeDropdown.propTypes = { - updateRequestMimeType: PropTypes.func.isRequired + onChange: PropTypes.func.isRequired, + + // Optional + contentType: PropTypes.string, // Can be null }; export default ContentTypeDropdown; diff --git a/app/ui/components/dropdowns/MethodDropdown.js b/app/ui/components/dropdowns/MethodDropdown.js new file mode 100644 index 000000000..2e4448b17 --- /dev/null +++ b/app/ui/components/dropdowns/MethodDropdown.js @@ -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 ( + + + {method} + + {constants.HTTP_METHODS.map(method => ( + + {method} + + ))} + + ) + } +} + +MethodDropdown.propTypes = { + onChange: PropTypes.func.isRequired, + method: PropTypes.string.isRequired, +}; + +export default MethodDropdown; diff --git a/app/ui/components/dropdowns/PreviewModeDropdown.js b/app/ui/components/dropdowns/PreviewModeDropdown.js index 74e8b1ecb..dfde72f90 100644 --- a/app/ui/components/dropdowns/PreviewModeDropdown.js +++ b/app/ui/components/dropdowns/PreviewModeDropdown.js @@ -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}) => ( - - - - - {PREVIEW_MODES.map(previewMode => ( - { - updatePreviewMode(previewMode); - trackEvent('Response', 'Preview Mode Change', previewMode); - }}> - {getPreviewModeName(previewMode)} - - ))} - - - Download - - -); +class PreviewModeDropdown extends PureComponent { + _handleClick = previewMode => { + this.props.updatePreviewMode(previewMode); + trackEvent('Response', 'Preview Mode Change', mode); + }; + + render () { + const {download, previewMode} = this.props; + return ( + + + + + + {PREVIEW_MODES.map(mode => ( + + {previewMode === mode ? : } + {getPreviewModeName(mode)} + + ))} + + + + Save to File + + + ) + } +} PreviewModeDropdown.propTypes = { // Functions diff --git a/app/ui/components/dropdowns/RequestActionsDropdown.js b/app/ui/components/dropdowns/RequestActionsDropdown.js index 6cedf3c89..c90d60549 100644 --- a/app/ui/components/dropdowns/RequestActionsDropdown.js +++ b/app/ui/components/dropdowns/RequestActionsDropdown.js @@ -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 { - { - models.request.duplicate(request); - trackEvent('Request', 'Duplicate', 'Request Action'); - }}> + Duplicate - { - this._promptUpdateName(); - trackEvent('Request', 'Rename', 'Request Action'); - }}> + Rename - { - showModal(GenerateCodeModal, request); - trackEvent('Request', 'Generate Code', 'Request Action'); - }}> + Generate Code - { - models.request.remove(request); - trackEvent('Request', 'Delete', 'Action'); - }} - addIcon={true}> + Delete @@ -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; diff --git a/app/ui/components/dropdowns/ResponseHistoryDropdown.js b/app/ui/components/dropdowns/ResponseHistoryDropdown.js new file mode 100644 index 000000000..9a7fb0ae3 --- /dev/null +++ b/app/ui/components/dropdowns/ResponseHistoryDropdown.js @@ -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 ( + + {active ? : } + + + + + ) + }; + + render () { + const { + activeResponseId, + handleSetActiveResponse, + handleDeleteResponses, + isLatestResponseActive, + ...extraProps + } = this.props; + const {responses} = this.state; + + return ( + + + {isLatestResponseActive ? + : + } + + + + + Clear History + + + {responses.map(this.renderDropdownItem)} + + ) + } +} + +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; diff --git a/app/ui/components/editors/body/BodyEditor.js b/app/ui/components/editors/body/BodyEditor.js index 74f8b0bf2..fcec14fed 100644 --- a/app/ui/components/editors/body/BodyEditor.js +++ b/app/ui/components/editors/body/BodyEditor.js @@ -78,7 +78,7 @@ class BodyEditor extends PureComponent { ) } else { return ( -
    +


    diff --git a/app/ui/components/modals/PaymentModal.js b/app/ui/components/modals/PaymentModal.js index 67af8bf47..0fc686ea5 100644 --- a/app/ui/components/modals/PaymentModal.js +++ b/app/ui/components/modals/PaymentModal.js @@ -160,7 +160,7 @@ class PaymentModal extends Component { {message} ) : null}

    -
    +
    Expiration Date -
    +
    this._input = n} type="text"/> +
    +
    + +
    + {!shouldNotHaveBody ? ( +
    + + {getContentTypeName(selectedContentType)} + {" "} + + +
    + ) : null} + + + +
    + * hint: 'TIP: Import Curl command by pasting it into the URL bar' +
    +
    + + +
    +
    + + ) + } +} + +RequestCreateModal.propTypes = {}; + +export default RequestCreateModal; diff --git a/app/ui/components/modals/SignupModal.js b/app/ui/components/modals/SignupModal.js index 31e19ac87..05d0a3d4c 100644 --- a/app/ui/components/modals/SignupModal.js +++ b/app/ui/components/modals/SignupModal.js @@ -128,7 +128,7 @@ class SignupModal extends Component {

    -
    -
    +
    diff --git a/app/ui/components/sidebar/Sidebar.js b/app/ui/components/sidebar/Sidebar.js index 5772641b9..3530fa8a2 100644 --- a/app/ui/components/sidebar/Sidebar.js +++ b/app/ui/components/sidebar/Sidebar.js @@ -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, diff --git a/app/ui/components/sidebar/SidebarChildren.js b/app/ui/components/sidebar/SidebarChildren.js index 6f307d221..a53fe9ea8 100644 --- a/app/ui/components/sidebar/SidebarChildren.js +++ b/app/ui/components/sidebar/SidebarChildren.js @@ -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, diff --git a/app/ui/components/sidebar/SidebarRequestRow.js b/app/ui/components/sidebar/SidebarRequestRow.js index 0d1ff9605..b946fdf38 100644 --- a/app/ui/components/sidebar/SidebarRequestRow.js +++ b/app/ui/components/sidebar/SidebarRequestRow.js @@ -38,6 +38,7 @@ class SidebarRequestRow extends PureComponent { render () { const { + handleDuplicateRequest, connectDragSource, connectDropTarget, isDragging, @@ -85,6 +86,7 @@ class SidebarRequestRow extends PureComponent {
    { } return ( -
    +
    {methodName}
    ) diff --git a/app/ui/components/tags/SizeTag.js b/app/ui/components/tags/SizeTag.js index 6316906eb..19bd332a2 100644 --- a/app/ui/components/tags/SizeTag.js +++ b/app/ui/components/tags/SizeTag.js @@ -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 ( -
    +
    SIZE {responseSizeString}
    ); }; SizeTag.propTypes = { - bytes: PropTypes.number.isRequired + // Required + bytes: PropTypes.number.isRequired, + + // Optional + small: PropTypes.bool, }; export default SizeTag; diff --git a/app/ui/components/tags/StatusTag.js b/app/ui/components/tags/StatusTag.js index 23d89bde3..1af08812f 100644 --- a/app/ui/components/tags/StatusTag.js +++ b/app/ui/components/tags/StatusTag.js @@ -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 ( -
    - {statusCode} {statusMessage || backupStatusMessage} +
    + {statusCode} + {" "} + {typeof statusMessage === 'string' ? statusMessage : backupStatusMessage}
    ); }; StatusTag.propTypes = { + // Required statusCode: PropTypes.number.isRequired, + + // Optional + small: PropTypes.bool, statusMessage: PropTypes.string }; diff --git a/app/ui/components/tags/TimeTag.js b/app/ui/components/tags/TimeTag.js index d7becaef8..664d6d0e7 100644 --- a/app/ui/components/tags/TimeTag.js +++ b/app/ui/components/tags/TimeTag.js @@ -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 ( -
    +
    TIME {number} {unit}
    ) -} +}; TimeTag.propTypes = { - milliseconds: PropTypes.number.isRequired + // Required + milliseconds: PropTypes.number.isRequired, + + // Optional + small: PropTypes.bool, + startTime: PropTypes.bool, }; export default TimeTag; diff --git a/app/ui/containers/App.js b/app/ui/containers/App.js index 373a48eb7..669fcf9af 100644 --- a/app/ui/containers/App.js +++ b/app/ui/containers/App.js @@ -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, diff --git a/app/ui/css/components/methoddropdown.less b/app/ui/css/components/methoddropdown.less new file mode 100644 index 000000000..4898d103d --- /dev/null +++ b/app/ui/css/components/methoddropdown.less @@ -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; + } +} diff --git a/app/ui/css/components/modal.less b/app/ui/css/components/modal.less index d69a21597..e7caed853 100644 --- a/app/ui/css/components/modal.less +++ b/app/ui/css/components/modal.less @@ -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; diff --git a/app/ui/css/components/pane.less b/app/ui/css/components/pane.less index 1c087d96e..37dda85ac 100644 --- a/app/ui/css/components/pane.less +++ b/app/ui/css/components/pane.less @@ -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 { diff --git a/app/ui/css/components/responsepane.less b/app/ui/css/components/responsepane.less index bba5380ad..53eada237 100644 --- a/app/ui/css/components/responsepane.less +++ b/app/ui/css/components/responsepane.less @@ -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; diff --git a/app/ui/css/components/sidebar.less b/app/ui/css/components/sidebar.less index f76cd6c6c..52dcde8aa 100644 --- a/app/ui/css/components/sidebar.less +++ b/app/ui/css/components/sidebar.less @@ -112,6 +112,12 @@ overflow: hidden; text-overflow: ellipsis; } + + i.fa { + // Bump the drop down caret down a bit + position: relative; + top: 1px; + } } .btn { diff --git a/app/ui/css/components/tag.less b/app/ui/css/components/tag.less index 4d33219d1..b139d0903 100644 --- a/app/ui/css/components/tag.less +++ b/app/ui/css/components/tag.less @@ -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 { diff --git a/app/ui/css/components/urlbar.less b/app/ui/css/components/urlbar.less index 72f0610b4..843402572 100644 --- a/app/ui/css/components/urlbar.less +++ b/app/ui/css/components/urlbar.less @@ -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; } diff --git a/app/ui/css/constants/colors.less b/app/ui/css/constants/colors.less index b4bf13aeb..8b8e7577a 100644 --- a/app/ui/css/constants/colors.less +++ b/app/ui/css/constants/colors.less @@ -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); diff --git a/app/ui/css/constants/dimensions.less b/app/ui/css/constants/dimensions.less index 10675af20..2fa80564b 100644 --- a/app/ui/css/constants/dimensions.less +++ b/app/ui/css/constants/dimensions.less @@ -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 */ diff --git a/app/ui/css/index.less b/app/ui/css/index.less index 06f41ddaa..f47fdd32f 100644 --- a/app/ui/css/index.less +++ b/app/ui/css/index.less @@ -35,3 +35,4 @@ @import 'components/keyvalueeditor'; @import 'components/editable'; @import 'components/responsepane'; +@import 'components/methoddropdown'; diff --git a/app/ui/css/layout/base.less b/app/ui/css/layout/base.less index 4f0594f9f..67783be76 100644 --- a/app/ui/css/layout/base.less +++ b/app/ui/css/layout/base.less @@ -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; diff --git a/app/ui/redux/create.js b/app/ui/redux/create.js index 5ce53c7f9..42614b6d0 100644 --- a/app/ui/redux/create.js +++ b/app/ui/redux/create.js @@ -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)); diff --git a/app/ui/redux/modules/global.js b/app/ui/redux/modules/global.js index 171bcf1b5..d595c911b 100644 --- a/app/ui/redux/modules/global.js +++ b/app/ui/redux/modules/global.js @@ -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, }); diff --git a/app/ui/redux/modules/requestMeta.js b/app/ui/redux/modules/requestMeta.js index 700a655f4..08346eb42 100644 --- a/app/ui/redux/modules/requestMeta.js +++ b/app/ui/redux/modules/requestMeta.js @@ -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); } }