insomnia/app/ui/containers/app.js

1036 lines
33 KiB
JavaScript
Raw Normal View History

import React, {PropTypes, PureComponent} from 'react';
import autobind from 'autobind-decorator';
import fs from 'fs';
2017-06-29 18:31:22 +00:00
import {clipboard, ipcRenderer, remote} from 'electron';
import {parse as urlParse} from 'url';
2017-06-29 18:31:22 +00:00
import HTTPSnippet from 'httpsnippet';
2016-07-19 16:15:03 +00:00
import ReactDOM from 'react-dom';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {showModal} from '../components/modals';
import Wrapper from '../components/wrapper';
import WorkspaceEnvironmentsEditModal from '../components/modals/workspace-environments-edit-modal';
import Toast from '../components/toast';
import CookiesModal from '../components/modals/cookies-modal';
import RequestSwitcherModal from '../components/modals/request-switcher-modal';
import ChangelogModal from '../components/modals/changelog-modal';
import SettingsModal from '../components/modals/settings-modal';
import {COLLAPSE_SIDEBAR_REMS, DEFAULT_PANE_HEIGHT, DEFAULT_PANE_WIDTH, DEFAULT_SIDEBAR_WIDTH, getAppVersion, isMac, MAX_PANE_HEIGHT, MAX_PANE_WIDTH, MAX_SIDEBAR_REMS, MIN_PANE_HEIGHT, MIN_PANE_WIDTH, MIN_SIDEBAR_REMS, PREVIEW_MODE_SOURCE} from '../../common/constants';
import * as globalActions from '../redux/modules/global';
2016-11-10 05:56:23 +00:00
import * as db from '../../common/database';
import * as models from '../../models';
import {trackEvent} from '../../analytics';
2017-07-27 22:59:07 +00:00
import {selectActiveOAuth2Token, selectActiveRequest, selectActiveRequestMeta, selectActiveRequestResponses, selectActiveResponse, selectActiveWorkspace, selectActiveWorkspaceMeta, selectEntitiesLists, selectSidebarChildren, selectUnseenWorkspaces, selectWorkspaceRequestsAndRequestGroups} from '../redux/selectors';
import RequestCreateModal from '../components/modals/request-create-modal';
import GenerateCodeModal from '../components/modals/generate-code-modal';
import WorkspaceSettingsModal from '../components/modals/workspace-settings-modal';
import RequestSettingsModal from '../components/modals/request-settings-modal';
import RequestRenderErrorModal from '../components/modals/request-render-error-modal';
import * as network from '../../network/network';
import {debounce} from '../../common/misc';
import * as mime from 'mime-types';
import * as path from 'path';
2017-02-13 08:12:02 +00:00
import * as render from '../../common/render';
import {getKeys} from '../../templating/utils';
2017-07-17 22:37:24 +00:00
import {showAlert, showPrompt} from '../components/modals/index';
import {exportHar} from '../../common/har';
const KEY_ENTER = 13;
const KEY_COMMA = 188;
const KEY_SLASH = 191;
const KEY_D = 68;
const KEY_E = 69;
const KEY_K = 75;
const KEY_L = 76;
const KEY_N = 78;
const KEY_P = 80;
const KEY_R = 82;
const KEY_F5 = 116;
2016-03-20 04:47:43 +00:00
@autobind
class App extends PureComponent {
constructor (props) {
super(props);
this.state = {
showDragOverlay: false,
draggingSidebar: false,
draggingPaneHorizontal: false,
draggingPaneVertical: false,
sidebarWidth: props.sidebarWidth || DEFAULT_SIDEBAR_WIDTH,
paneWidth: props.paneWidth || DEFAULT_PANE_WIDTH,
paneHeight: props.paneHeight || DEFAULT_PANE_HEIGHT
};
2016-11-25 20:55:15 +00:00
this._getRenderContextPromiseCache = {};
this._savePaneWidth = debounce(paneWidth => this._updateActiveWorkspaceMeta({paneWidth}));
this._savePaneHeight = debounce(paneHeight => this._updateActiveWorkspaceMeta({paneHeight}));
this._saveSidebarWidth = debounce(sidebarWidth => this._updateActiveWorkspaceMeta({sidebarWidth}));
this._globalKeyMap = null;
}
_setGlobalKeyMap () {
this._globalKeyMap = [
{ // Show Workspace Settings
meta: true,
shift: true,
alt: false,
key: KEY_COMMA,
callback: () => {
const {activeWorkspace} = this.props;
showModal(WorkspaceSettingsModal, activeWorkspace);
trackEvent('HotKey', 'Workspace Settings');
}
}, {
meta: true,
shift: true,
alt: true,
key: KEY_COMMA,
callback: () => {
if (this.props.activeRequest) {
showModal(RequestSettingsModal, {request: this.props.activeRequest});
trackEvent('HotKey', 'Request Settings');
}
}
}, {
meta: true,
shift: false,
alt: false,
key: KEY_SLASH,
callback: () => {
showModal(SettingsModal, 3);
trackEvent('HotKey', 'Settings Shortcuts');
}
}, {
meta: true,
shift: false,
alt: false,
key: KEY_P,
callback: () => {
showModal(RequestSwitcherModal);
trackEvent('HotKey', 'Quick Switcher');
}
}, {
meta: true,
shift: false,
alt: false,
key: [KEY_ENTER, KEY_R],
callback: this._handleSendShortcut
}, {
meta: false,
shift: false,
alt: false,
key: [KEY_F5],
callback: this._handleSendShortcut
}, {
meta: true,
shift: false,
alt: false,
key: KEY_E,
callback: () => {
const {activeWorkspace} = this.props;
showModal(WorkspaceEnvironmentsEditModal, activeWorkspace);
trackEvent('HotKey', 'Environments');
}
}, {
meta: true,
shift: false,
alt: false,
key: KEY_L,
callback: () => {
const node = document.body.querySelector('.urlbar input');
node && node.focus();
trackEvent('HotKey', 'Url');
}
}, {
meta: true,
shift: false,
alt: false,
key: KEY_K,
callback: () => {
const {activeWorkspace} = this.props;
showModal(CookiesModal, activeWorkspace);
trackEvent('HotKey', 'Cookies');
}
}, {
meta: true,
shift: false,
alt: false,
key: KEY_N,
callback: () => {
const {activeRequest, activeWorkspace} = this.props;
const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id;
this._requestCreate(parentId);
trackEvent('HotKey', 'Request Create');
}
}, {
meta: true,
shift: false,
alt: false,
key: KEY_D,
callback: async () => {
await this._requestDuplicate(this.props.activeRequest);
trackEvent('HotKey', 'Request Duplicate');
}
}
];
}
async _handleSendShortcut () {
const {activeRequest, activeEnvironment} = this.props;
await this._handleSendRequestWithEnvironment(
activeRequest ? activeRequest._id : 'n/a',
activeEnvironment ? activeEnvironment._id : 'n/a',
);
trackEvent('HotKey', 'Send');
}
_setRequestPaneRef (n) {
this._requestPane = n;
}
_setResponsePaneRef (n) {
this._responsePane = n;
}
2016-11-25 20:55:15 +00:00
_setSidebarRef (n) {
this._sidebar = n;
}
_isDragging () {
return (
this.state.draggingPaneHorizontal ||
this.state.draggingPaneVertical ||
this.state.draggingSidebar
);
}
_requestGroupCreate (parentId) {
showPrompt({
title: 'New Folder',
defaultValue: 'My Folder',
2016-11-23 20:44:46 +00:00
submitName: 'Create',
label: 'Name',
selectText: true,
onComplete: name => {
models.requestGroup.create({parentId, name});
}
});
}
_requestCreate (parentId) {
showModal(RequestCreateModal, {
parentId,
onComplete: request => {
this._handleSetActiveRequest(request._id);
}
});
}
async _requestGroupDuplicate (requestGroup) {
models.requestGroup.duplicate(requestGroup);
}
2016-11-28 07:12:17 +00:00
async _requestDuplicate (request) {
if (!request) {
return;
}
const newRequest = await models.request.duplicate(request);
await this._handleSetActiveRequest(newRequest._id);
}
2017-02-13 08:12:02 +00:00
2017-06-07 00:07:09 +00:00
async _workspaceDuplicate (callback) {
const workspace = this.props.activeWorkspace;
showPrompt({
title: 'Duplicate Workspace',
2017-06-07 00:07:09 +00:00
defaultValue: `${workspace.name} (Copy)`,
submitName: 'Duplicate',
selectText: true,
onComplete: async name => {
const newWorkspace = await db.duplicate(workspace, {name});
await this.props.handleSetActiveWorkspace(newWorkspace._id);
callback();
}
});
}
async _fetchRenderContext () {
const {activeEnvironment, activeRequest} = this.props;
const environmentId = activeEnvironment ? activeEnvironment._id : null;
return render.getRenderContext(activeRequest, environmentId, null);
}
async _handleGetRenderContext () {
const context = await this._fetchRenderContext();
const keys = getKeys(context);
return {context, keys};
}
/**
* Heavily optimized render function
*
* @param text - template to render
* @param contextCacheKey - if rendering multiple times in parallel, set this
* @returns {Promise}
* @private
*/
async _handleRenderText (text, contextCacheKey = null) {
if (!contextCacheKey || !this._getRenderContextPromiseCache[contextCacheKey]) {
const context = this._fetchRenderContext();
// NOTE: We're caching promises here to avoid race conditions
this._getRenderContextPromiseCache[contextCacheKey] = context;
}
// Set timeout to delete the key eventually
setTimeout(() => delete this._getRenderContextPromiseCache[contextCacheKey], 5000);
const context = await this._getRenderContextPromiseCache[contextCacheKey];
return render.render(text, context);
}
_handleGenerateCodeForActiveRequest () {
this._handleGenerateCode(this.props.activeRequest);
}
_handleGenerateCode (request) {
showModal(GenerateCodeModal, request);
}
2016-11-28 07:12:17 +00:00
async _handleCopyAsCurl (request) {
const {activeEnvironment} = this.props;
const environmentId = activeEnvironment ? activeEnvironment._id : 'n/a';
const har = await exportHar(request._id, environmentId);
const snippet = new HTTPSnippet(har);
const cmd = snippet.convert('shell', 'curl');
clipboard.writeText(cmd);
}
async _updateRequestGroupMetaByParentId (requestGroupId, patch) {
const requestGroupMeta = await models.requestGroupMeta.getByParentId(requestGroupId);
if (requestGroupMeta) {
await models.requestGroupMeta.update(requestGroupMeta, patch);
} else {
const newPatch = Object.assign({parentId: requestGroupId}, patch);
await models.requestGroupMeta.create(newPatch);
}
}
2016-11-25 23:09:17 +00:00
async _updateActiveWorkspaceMeta (patch) {
const workspaceId = this.props.activeWorkspace._id;
2017-07-27 22:59:07 +00:00
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspaceId);
if (workspaceMeta) {
return models.workspaceMeta.update(workspaceMeta, patch);
} else {
const newPatch = Object.assign({parentId: workspaceId}, patch);
return models.workspaceMeta.create(newPatch);
}
}
async _updateRequestMetaByParentId (requestId, patch) {
const requestMeta = await models.requestMeta.getByParentId(requestId);
if (requestMeta) {
return models.requestMeta.update(requestMeta, patch);
} else {
const newPatch = Object.assign({parentId: requestId}, patch);
return models.requestMeta.create(newPatch);
}
}
_handleSetPaneWidth (paneWidth) {
this.setState({paneWidth});
this._savePaneWidth(paneWidth);
}
_handleSetPaneHeight (paneHeight) {
this.setState({paneHeight});
this._savePaneHeight(paneHeight);
}
async _handleSetActiveRequest (activeRequestId) {
2017-02-13 08:12:02 +00:00
await this._updateActiveWorkspaceMeta({activeRequestId});
}
async _handleSetActiveEnvironment (activeEnvironmentId) {
2017-02-13 08:12:02 +00:00
await this._updateActiveWorkspaceMeta({activeEnvironmentId});
2017-03-29 02:21:49 +00:00
// Give it time to update and re-render
setTimeout(() => {
this._wrapper._forceRequestPaneRefresh();
}, 100);
}
_handleSetSidebarWidth (sidebarWidth) {
this.setState({sidebarWidth});
this._saveSidebarWidth(sidebarWidth);
}
async _handleSetSidebarHidden (sidebarHidden) {
2017-02-13 08:12:02 +00:00
await this._updateActiveWorkspaceMeta({sidebarHidden});
}
async _handleSetSidebarFilter (sidebarFilter) {
2017-02-13 08:12:02 +00:00
await this._updateActiveWorkspaceMeta({sidebarFilter});
}
_handleSetRequestGroupCollapsed (requestGroupId, collapsed) {
this._updateRequestGroupMetaByParentId(requestGroupId, {collapsed});
}
_handleSetResponsePreviewMode (requestId, previewMode) {
this._updateRequestMetaByParentId(requestId, {previewMode});
}
async _handleSetResponseFilter (requestId, responseFilter) {
await this._updateRequestMetaByParentId(requestId, {responseFilter});
clearTimeout(this._responseFilterHistorySaveTimeout);
this._responseFilterHistorySaveTimeout = setTimeout(async () => {
const meta = await models.requestMeta.getByParentId(requestId);
const responseFilterHistory = meta.responseFilterHistory.slice(0, 10);
// Already in history?
if (responseFilterHistory.includes(responseFilter)) {
return;
}
2017-06-08 02:53:46 +00:00
// Blank?
if (!responseFilter) {
return;
}
responseFilterHistory.unshift(responseFilter);
await this._updateRequestMetaByParentId(requestId, {responseFilterHistory});
}, 2000);
}
async _handleSendAndDownloadRequestWithEnvironment (requestId, environmentId, dir) {
const request = await models.request.getById(requestId);
if (!request) {
return;
}
// NOTE: Since request is by far the most popular event, we will throttle
// it so that we only track it if the request has changed since the last one
const key = request._id;
if (this._sendRequestTrackingKey !== key) {
trackEvent('Request', 'Send and Download');
this._sendRequestTrackingKey = key;
}
// Start loading
this.props.handleStartLoading(requestId);
try {
const {response: responsePatch, bodyBuffer} = await network.send(requestId, environmentId);
if (responsePatch.statusCode >= 200 && responsePatch.statusCode < 300) {
const extension = mime.extension(responsePatch.contentType) || '';
const name = request.name.replace(/\s/g, '-').toLowerCase();
const filename = path.join(dir, `${name}.${extension}`);
const partialResponse = Object.assign({}, responsePatch);
await models.response.create(partialResponse, `Saved to ${filename}`);
fs.writeFile(filename, bodyBuffer, err => {
if (err) {
console.warn('Failed to download request after sending', err);
}
});
} else {
await models.response.create(responsePatch, bodyBuffer);
}
2017-07-17 22:37:24 +00:00
} catch (err) {
showAlert({
title: 'Unexpected Request Failure',
message: (
<div>
<p>The request failed due to an unhandled error:</p>
<code className="wide selectable">
<pre>{err.message}</pre>
</code>
</div>
)
});
}
// Unset active response because we just made a new one
await this._updateRequestMetaByParentId(requestId, {activeResponseId: null});
// Stop loading
this.props.handleStopLoading(requestId);
}
async _handleSendRequestWithEnvironment (requestId, environmentId) {
2016-12-05 22:42:40 +00:00
const request = await models.request.getById(requestId);
if (!request) {
return;
}
2016-12-05 22:42:40 +00:00
// NOTE: Since request is by far the most popular event, we will throttle
// it so that we only track it if the request has changed since the last noe
const key = `${request._id}::${request.modified}`;
if (this._sendRequestTrackingKey !== key) {
trackEvent('Request', 'Send');
this._sendRequestTrackingKey = key;
}
this.props.handleStartLoading(requestId);
try {
const {response: responsePatch, bodyBuffer} = await network.send(requestId, environmentId);
await models.response.create(responsePatch, bodyBuffer);
} catch (err) {
if (err.type === 'render') {
showModal(RequestRenderErrorModal, {request, error: err});
2017-07-17 22:37:24 +00:00
} else {
showAlert({
title: 'Unexpected Request Failure',
message: (
<div>
<p>The request failed due to an unhandled error:</p>
<code className="wide selectable">
<pre>{err.message}</pre>
</code>
</div>
)
});
}
}
// Unset active response because we just made a new one
await this._updateRequestMetaByParentId(requestId, {activeResponseId: null});
// Stop loading
this.props.handleStopLoading(requestId);
}
2017-07-17 18:20:38 +00:00
async _handleSetActiveResponse (requestId, activeResponse = null) {
const activeResponseId = activeResponse ? activeResponse._id : null;
await this._updateRequestMetaByParentId(requestId, {activeResponseId});
let response;
if (activeResponseId) {
response = await models.response.getById(activeResponseId);
} else {
response = await models.response.getLatestForRequest(requestId);
}
const requestVersionId = response ? response.requestVersionId : 'n/a';
const request = await models.requestVersion.restore(requestVersionId);
if (request) {
// Refresh app to reflect changes. Using timeout because we need to
// wait for the request update to propagate.
setTimeout(() => this._wrapper._forceRequestPaneRefresh(), 500);
} else {
// Couldn't restore request. That's okay
}
}
_requestCreateForWorkspace () {
this._requestCreate(this.props.activeWorkspace._id);
}
2016-11-25 23:09:17 +00:00
_startDragSidebar () {
2016-12-05 22:42:40 +00:00
trackEvent('Sidebar', 'Drag');
this.setState({draggingSidebar: true});
}
2016-06-19 00:08:14 +00:00
_resetDragSidebar () {
2016-12-05 22:42:40 +00:00
trackEvent('Sidebar', 'Drag');
// TODO: Remove setTimeout need be not triggering drag on double click
setTimeout(() => this._handleSetSidebarWidth(DEFAULT_SIDEBAR_WIDTH), 50);
}
2016-06-19 00:08:14 +00:00
_startDragPaneHorizontal () {
trackEvent('App Pane', 'Drag Start');
this.setState({draggingPaneHorizontal: true});
}
2016-06-19 00:08:14 +00:00
_startDragPaneVertical () {
trackEvent('App Pane', 'Drag Start Vertical');
this.setState({draggingPaneVertical: true});
}
_resetDragPaneHorizontal () {
2016-12-05 22:42:40 +00:00
trackEvent('App Pane', 'Drag Reset');
// TODO: Remove setTimeout need be not triggering drag on double click
setTimeout(() => this._handleSetPaneWidth(DEFAULT_PANE_WIDTH), 50);
}
_resetDragPaneVertical () {
trackEvent('App Pane', 'Drag Reset Vertical');
// TODO: Remove setTimeout need be not triggering drag on double click
setTimeout(() => this._handleSetPaneHeight(DEFAULT_PANE_HEIGHT), 50);
}
_handleMouseMove (e) {
if (this.state.draggingPaneHorizontal) {
// Only pop the overlay after we've moved it a bit (so we don't block doubleclick);
const distance = this.props.paneWidth - this.state.paneWidth;
if (!this.state.showDragOverlay && Math.abs(distance) > 0.02 /* % */) {
this.setState({showDragOverlay: true});
}
const requestPane = ReactDOM.findDOMNode(this._requestPane);
const responsePane = ReactDOM.findDOMNode(this._responsePane);
const requestPaneWidth = requestPane.offsetWidth;
const responsePaneWidth = responsePane.offsetWidth;
const pixelOffset = e.clientX - requestPane.offsetLeft;
let paneWidth = pixelOffset / (requestPaneWidth + responsePaneWidth);
paneWidth = Math.min(Math.max(paneWidth, MIN_PANE_WIDTH), MAX_PANE_WIDTH);
this._handleSetPaneWidth(paneWidth);
} else if (this.state.draggingPaneVertical) {
// Only pop the overlay after we've moved it a bit (so we don't block doubleclick);
const distance = this.props.paneHeight - this.state.paneHeight;
if (!this.state.showDragOverlay && Math.abs(distance) > 0.02 /* % */) {
this.setState({showDragOverlay: true});
}
const requestPane = ReactDOM.findDOMNode(this._requestPane);
const responsePane = ReactDOM.findDOMNode(this._responsePane);
const requestPaneHeight = requestPane.offsetHeight;
const responsePaneHeight = responsePane.offsetHeight;
const pixelOffset = e.clientY - requestPane.offsetTop;
let paneHeight = pixelOffset / (requestPaneHeight + responsePaneHeight);
paneHeight = Math.min(Math.max(paneHeight, MIN_PANE_HEIGHT), MAX_PANE_HEIGHT);
this._handleSetPaneHeight(paneHeight);
} else if (this.state.draggingSidebar) {
// Only pop the overlay after we've moved it a bit (so we don't block doubleclick);
const distance = this.props.sidebarWidth - this.state.sidebarWidth;
if (!this.state.showDragOverlay && Math.abs(distance) > 2 /* ems */) {
this.setState({showDragOverlay: true});
}
const currentPixelWidth = ReactDOM.findDOMNode(this._sidebar).offsetWidth;
const ratio = e.clientX / currentPixelWidth;
const width = this.state.sidebarWidth * ratio;
let sidebarWidth = Math.min(width, MAX_SIDEBAR_REMS);
if (sidebarWidth < COLLAPSE_SIDEBAR_REMS) {
sidebarWidth = MIN_SIDEBAR_REMS;
}
this._handleSetSidebarWidth(sidebarWidth);
}
}
_handleMouseUp () {
if (this.state.draggingSidebar) {
this.setState({draggingSidebar: false, showDragOverlay: false});
}
2016-03-22 05:01:58 +00:00
if (this.state.draggingPaneHorizontal) {
this.setState({draggingPaneHorizontal: false, showDragOverlay: false});
}
if (this.state.draggingPaneVertical) {
this.setState({draggingPaneVertical: false, showDragOverlay: false});
2016-06-19 07:21:43 +00:00
}
}
_handleKeyDown (e) {
const isMetaPressed = isMac() ? e.metaKey : e.ctrlKey;
const isAltPressed = isMac() ? e.ctrlKey : e.altKey;
const isShiftPressed = e.shiftKey;
for (const {meta, shift, alt, key, callback} of this._globalKeyMap) {
const keys = Array.isArray(key) ? key : [key];
for (const key of keys) {
if ((alt && !isAltPressed) || (!alt && isAltPressed)) {
continue;
}
if ((meta && !isMetaPressed) || (!meta && isMetaPressed)) {
continue;
}
if ((shift && !isShiftPressed) || (!shift && isShiftPressed)) {
continue;
}
if (key !== e.keyCode) {
continue;
}
callback();
}
}
}
Merge develop for 5.1.0 (#246) * Add Slack badge to README * Add Twitter badge * Appveyor badge * Fix badges again on README * Fix Twitter badge link * Simplify README.md * Migrate Travis secure items to Travis project settings (#198) * Remove docker linux build (using Travis now) (#199) * Fix travis build * Update Issue and PR templates (#200) * Add template for future pull requests * Format issue template like pull request template * Will not clear selected file if dialog is dismissed (#202) * #183, Body type "Text -> Other" reverts to previous body type (#193) * ISSUE#183 * Adding condition to check mime-type to other * Removing older changes for fixing issue. * Save full response to a file (#207) * Save full response to a file * Add a new button on the response preview pane * Save full response to file when button clicked * Update after PR comments * It's a Response, not a Request * Remove file extension requirement * Implement lazy tag rendering and some fixes (#211) * expanding to 3 decimals (#213) * Update PULL_REQUEST_TEMPLATE.md (#214) * Show build info in console (#216) * Add waiting message in dev mode while webpack compile happens * Switch license from GPL to AGPL (#221) * Default remote URLs to production * Don't use Curl's cookie handling (#220) * Some improvements to the response tag * Add tests for XPath response queries * Refactor conditional element syntax * Add option to toggle Menu Bar showing for Windows/Linux (#225) * Add option to toggle MenuBar showing on Windows/Linux * Extract Toggling Menu Bar functionality to App Container. Default show Menu Bar. Remove tip from Response Pane. * Finalize {% response ... %} Tag (#224) * Some improvements to the response tag * Add tests for XPath response queries * Refactor conditional element syntax * Update nunjucks-tags.js * Better Nunjucks Tag Editor (#234) * Helper to tokenize Nunjucks tag * More granular types * Add tag definitions * Improve editor to be more WYSIWYG * Fixed tests * Added raw response tag * A few improvements to tag editor enum * fix NTLM typo (#244) * Tweaks and fixes for next release (#245)
2017-05-24 16:25:22 +00:00
_handleToggleMenuBar (hide) {
for (const win of remote.BrowserWindow.getAllWindows()) {
2017-05-24 16:46:18 +00:00
if (win.isMenuBarAutoHide() !== hide) {
Merge develop for 5.1.0 (#246) * Add Slack badge to README * Add Twitter badge * Appveyor badge * Fix badges again on README * Fix Twitter badge link * Simplify README.md * Migrate Travis secure items to Travis project settings (#198) * Remove docker linux build (using Travis now) (#199) * Fix travis build * Update Issue and PR templates (#200) * Add template for future pull requests * Format issue template like pull request template * Will not clear selected file if dialog is dismissed (#202) * #183, Body type "Text -> Other" reverts to previous body type (#193) * ISSUE#183 * Adding condition to check mime-type to other * Removing older changes for fixing issue. * Save full response to a file (#207) * Save full response to a file * Add a new button on the response preview pane * Save full response to file when button clicked * Update after PR comments * It's a Response, not a Request * Remove file extension requirement * Implement lazy tag rendering and some fixes (#211) * expanding to 3 decimals (#213) * Update PULL_REQUEST_TEMPLATE.md (#214) * Show build info in console (#216) * Add waiting message in dev mode while webpack compile happens * Switch license from GPL to AGPL (#221) * Default remote URLs to production * Don't use Curl's cookie handling (#220) * Some improvements to the response tag * Add tests for XPath response queries * Refactor conditional element syntax * Add option to toggle Menu Bar showing for Windows/Linux (#225) * Add option to toggle MenuBar showing on Windows/Linux * Extract Toggling Menu Bar functionality to App Container. Default show Menu Bar. Remove tip from Response Pane. * Finalize {% response ... %} Tag (#224) * Some improvements to the response tag * Add tests for XPath response queries * Refactor conditional element syntax * Update nunjucks-tags.js * Better Nunjucks Tag Editor (#234) * Helper to tokenize Nunjucks tag * More granular types * Add tag definitions * Improve editor to be more WYSIWYG * Fixed tests * Added raw response tag * A few improvements to tag editor enum * fix NTLM typo (#244) * Tweaks and fixes for next release (#245)
2017-05-24 16:25:22 +00:00
win.setAutoHideMenuBar(hide);
win.setMenuBarVisibility(!hide);
}
}
}
async _handleToggleSidebar () {
const sidebarHidden = !this.props.sidebarHidden;
await this._handleSetSidebarHidden(sidebarHidden);
trackEvent('Sidebar', 'Toggle Visibility', sidebarHidden ? 'Hide' : 'Show');
}
_setWrapperRef (n) {
this._wrapper = n;
}
2016-11-09 04:16:38 +00:00
/**
* Update document.title to be "Workspace (Environment) Request"
* @private
*/
_updateDocumentTitle () {
const {
activeWorkspace,
activeEnvironment,
activeRequest
} = this.props;
let title = activeWorkspace.name;
if (activeEnvironment) {
title += ` (${activeEnvironment.name})`;
}
if (activeRequest) {
title += ` ${activeRequest.name}`;
}
document.title = title;
}
componentDidUpdate () {
this._updateDocumentTitle();
}
async componentDidMount () {
// Bind mouse and key handlers
document.addEventListener('mouseup', this._handleMouseUp);
document.addEventListener('mousemove', this._handleMouseMove);
document.addEventListener('keydown', this._handleKeyDown);
this._setGlobalKeyMap();
// Update title
this._updateDocumentTitle();
// Update Stats Object
2016-11-10 01:15:27 +00:00
const {lastVersion, launches} = await models.stats.get();
const firstLaunch = !lastVersion;
if (firstLaunch) {
// TODO: Show a welcome message
2016-12-07 17:57:01 +00:00
trackEvent('General', 'First Launch', getAppVersion(), {nonInteraction: true});
} else if (lastVersion !== getAppVersion()) {
2016-12-07 17:57:01 +00:00
trackEvent('General', 'Updated', getAppVersion(), {nonInteraction: true});
showModal(ChangelogModal);
2016-12-05 22:42:40 +00:00
} else {
2016-12-07 17:57:01 +00:00
trackEvent('General', 'Launched', getAppVersion(), {nonInteraction: true});
}
2017-02-13 08:12:02 +00:00
db.onChange(async changes => {
for (const change of changes) {
const [
_, // eslint-disable-line no-unused-vars
doc,
fromSync
] = change;
const {activeRequest} = this.props;
// No active request, so we don't need to force refresh anything
if (!activeRequest) {
return;
}
// Force refresh if environment changes
2017-01-13 19:21:03 +00:00
// TODO: Only do this for environments in this workspace (not easy because they're nested)
if (doc.type === models.environment.type) {
2017-01-11 21:59:35 +00:00
console.log('[App] Forcing update from environment change', change);
this._wrapper._forceRequestPaneRefresh();
}
// Force refresh if sync changes the active request
if (fromSync && doc._id === activeRequest._id) {
this._wrapper._forceRequestPaneRefresh();
2017-01-11 21:59:35 +00:00
console.log('[App] Forcing update from request change', change);
}
}
});
2016-11-10 01:15:27 +00:00
models.stats.update({
launches: launches + 1,
lastLaunch: Date.now(),
lastVersion: getAppVersion()
});
2016-07-27 20:07:50 +00:00
2016-08-22 20:05:42 +00:00
ipcRenderer.on('toggle-preferences', () => {
showModal(SettingsModal);
});
ipcRenderer.on('run-command', (e, commandUri) => {
const parsed = urlParse(commandUri, true);
const command = `${parsed.hostname}${parsed.pathname}`;
const args = JSON.parse(JSON.stringify(parsed.query));
args.workspaceId = args.workspaceId || this.props.activeWorkspace._id;
this.props.handleCommand(command, args);
});
2016-11-23 19:59:26 +00:00
ipcRenderer.on('toggle-changelog', () => {
showModal(ChangelogModal);
2016-11-23 19:59:26 +00:00
});
2016-11-25 20:55:15 +00:00
ipcRenderer.on('toggle-sidebar', this._handleToggleSidebar);
process.nextTick(() => ipcRenderer.send('app-ready'));
Merge develop for 5.1.0 (#246) * Add Slack badge to README * Add Twitter badge * Appveyor badge * Fix badges again on README * Fix Twitter badge link * Simplify README.md * Migrate Travis secure items to Travis project settings (#198) * Remove docker linux build (using Travis now) (#199) * Fix travis build * Update Issue and PR templates (#200) * Add template for future pull requests * Format issue template like pull request template * Will not clear selected file if dialog is dismissed (#202) * #183, Body type "Text -> Other" reverts to previous body type (#193) * ISSUE#183 * Adding condition to check mime-type to other * Removing older changes for fixing issue. * Save full response to a file (#207) * Save full response to a file * Add a new button on the response preview pane * Save full response to file when button clicked * Update after PR comments * It's a Response, not a Request * Remove file extension requirement * Implement lazy tag rendering and some fixes (#211) * expanding to 3 decimals (#213) * Update PULL_REQUEST_TEMPLATE.md (#214) * Show build info in console (#216) * Add waiting message in dev mode while webpack compile happens * Switch license from GPL to AGPL (#221) * Default remote URLs to production * Don't use Curl's cookie handling (#220) * Some improvements to the response tag * Add tests for XPath response queries * Refactor conditional element syntax * Add option to toggle Menu Bar showing for Windows/Linux (#225) * Add option to toggle MenuBar showing on Windows/Linux * Extract Toggling Menu Bar functionality to App Container. Default show Menu Bar. Remove tip from Response Pane. * Finalize {% response ... %} Tag (#224) * Some improvements to the response tag * Add tests for XPath response queries * Refactor conditional element syntax * Update nunjucks-tags.js * Better Nunjucks Tag Editor (#234) * Helper to tokenize Nunjucks tag * More granular types * Add tag definitions * Improve editor to be more WYSIWYG * Fixed tests * Added raw response tag * A few improvements to tag editor enum * fix NTLM typo (#244) * Tweaks and fixes for next release (#245)
2017-05-24 16:25:22 +00:00
// handle this
this._handleToggleMenuBar(this.props.settings.autoHideMenuBar);
}
2016-07-07 20:10:55 +00:00
componentWillUnmount () {
// Remove mouse and key handlers
document.removeEventListener('mouseup', this._handleMouseUp);
document.removeEventListener('mousemove', this._handleMouseMove);
document.removeEventListener('keydown', this._handleKeyDown);
}
2016-07-07 20:10:55 +00:00
render () {
return (
<div className="app">
<Wrapper
{...this.props}
2016-11-26 00:37:59 +00:00
ref={this._setWrapperRef}
paneWidth={this.state.paneWidth}
paneHeight={this.state.paneHeight}
sidebarWidth={this.state.sidebarWidth}
2016-11-25 23:09:17 +00:00
handleCreateRequestForWorkspace={this._requestCreateForWorkspace}
handleSetRequestGroupCollapsed={this._handleSetRequestGroupCollapsed}
handleActivateRequest={this._handleSetActiveRequest}
2016-11-25 20:55:15 +00:00
handleSetRequestPaneRef={this._setRequestPaneRef}
handleSetResponsePaneRef={this._setResponsePaneRef}
handleSetSidebarRef={this._setSidebarRef}
handleStartDragSidebar={this._startDragSidebar}
handleResetDragSidebar={this._resetDragSidebar}
handleStartDragPaneHorizontal={this._startDragPaneHorizontal}
handleStartDragPaneVertical={this._startDragPaneVertical}
handleResetDragPaneHorizontal={this._resetDragPaneHorizontal}
handleResetDragPaneVertical={this._resetDragPaneVertical}
2016-11-25 20:55:15 +00:00
handleCreateRequest={this._requestCreate}
2017-02-13 08:12:02 +00:00
handleRender={this._handleRenderText}
handleGetRenderContext={this._handleGetRenderContext}
handleDuplicateRequest={this._requestDuplicate}
2016-11-28 07:12:17 +00:00
handleDuplicateRequestGroup={this._requestGroupDuplicate}
2017-06-07 00:07:09 +00:00
handleDuplicateWorkspace={this._workspaceDuplicate}
2016-11-25 20:55:15 +00:00
handleCreateRequestGroup={this._requestGroupCreate}
2016-11-28 07:12:17 +00:00
handleGenerateCode={this._handleGenerateCode}
handleGenerateCodeForActiveRequest={this._handleGenerateCodeForActiveRequest}
handleCopyAsCurl={this._handleCopyAsCurl}
handleSetResponsePreviewMode={this._handleSetResponsePreviewMode}
handleSetResponseFilter={this._handleSetResponseFilter}
handleSendRequestWithEnvironment={this._handleSendRequestWithEnvironment}
handleSendAndDownloadRequestWithEnvironment={this._handleSendAndDownloadRequestWithEnvironment}
handleSetActiveResponse={this._handleSetActiveResponse}
handleSetActiveRequest={this._handleSetActiveRequest}
handleSetActiveEnvironment={this._handleSetActiveEnvironment}
handleSetSidebarFilter={this._handleSetSidebarFilter}
Merge develop for 5.1.0 (#246) * Add Slack badge to README * Add Twitter badge * Appveyor badge * Fix badges again on README * Fix Twitter badge link * Simplify README.md * Migrate Travis secure items to Travis project settings (#198) * Remove docker linux build (using Travis now) (#199) * Fix travis build * Update Issue and PR templates (#200) * Add template for future pull requests * Format issue template like pull request template * Will not clear selected file if dialog is dismissed (#202) * #183, Body type "Text -> Other" reverts to previous body type (#193) * ISSUE#183 * Adding condition to check mime-type to other * Removing older changes for fixing issue. * Save full response to a file (#207) * Save full response to a file * Add a new button on the response preview pane * Save full response to file when button clicked * Update after PR comments * It's a Response, not a Request * Remove file extension requirement * Implement lazy tag rendering and some fixes (#211) * expanding to 3 decimals (#213) * Update PULL_REQUEST_TEMPLATE.md (#214) * Show build info in console (#216) * Add waiting message in dev mode while webpack compile happens * Switch license from GPL to AGPL (#221) * Default remote URLs to production * Don't use Curl's cookie handling (#220) * Some improvements to the response tag * Add tests for XPath response queries * Refactor conditional element syntax * Add option to toggle Menu Bar showing for Windows/Linux (#225) * Add option to toggle MenuBar showing on Windows/Linux * Extract Toggling Menu Bar functionality to App Container. Default show Menu Bar. Remove tip from Response Pane. * Finalize {% response ... %} Tag (#224) * Some improvements to the response tag * Add tests for XPath response queries * Refactor conditional element syntax * Update nunjucks-tags.js * Better Nunjucks Tag Editor (#234) * Helper to tokenize Nunjucks tag * More granular types * Add tag definitions * Improve editor to be more WYSIWYG * Fixed tests * Added raw response tag * A few improvements to tag editor enum * fix NTLM typo (#244) * Tweaks and fixes for next release (#245)
2017-05-24 16:25:22 +00:00
handleToggleMenuBar={this._handleToggleMenuBar}
/>
<Toast/>
{/* Block all mouse activity by showing an overlay while dragging */}
{this.state.showDragOverlay ? <div className="blocker-overlay"></div> : null}
</div>
);
}
}
App.propTypes = {
// Required
sidebarWidth: PropTypes.number.isRequired,
paneWidth: PropTypes.number.isRequired,
paneHeight: PropTypes.number.isRequired,
handleCommand: PropTypes.func.isRequired,
Merge develop for 5.1.0 (#246) * Add Slack badge to README * Add Twitter badge * Appveyor badge * Fix badges again on README * Fix Twitter badge link * Simplify README.md * Migrate Travis secure items to Travis project settings (#198) * Remove docker linux build (using Travis now) (#199) * Fix travis build * Update Issue and PR templates (#200) * Add template for future pull requests * Format issue template like pull request template * Will not clear selected file if dialog is dismissed (#202) * #183, Body type "Text -> Other" reverts to previous body type (#193) * ISSUE#183 * Adding condition to check mime-type to other * Removing older changes for fixing issue. * Save full response to a file (#207) * Save full response to a file * Add a new button on the response preview pane * Save full response to file when button clicked * Update after PR comments * It's a Response, not a Request * Remove file extension requirement * Implement lazy tag rendering and some fixes (#211) * expanding to 3 decimals (#213) * Update PULL_REQUEST_TEMPLATE.md (#214) * Show build info in console (#216) * Add waiting message in dev mode while webpack compile happens * Switch license from GPL to AGPL (#221) * Default remote URLs to production * Don't use Curl's cookie handling (#220) * Some improvements to the response tag * Add tests for XPath response queries * Refactor conditional element syntax * Add option to toggle Menu Bar showing for Windows/Linux (#225) * Add option to toggle MenuBar showing on Windows/Linux * Extract Toggling Menu Bar functionality to App Container. Default show Menu Bar. Remove tip from Response Pane. * Finalize {% response ... %} Tag (#224) * Some improvements to the response tag * Add tests for XPath response queries * Refactor conditional element syntax * Update nunjucks-tags.js * Better Nunjucks Tag Editor (#234) * Helper to tokenize Nunjucks tag * More granular types * Add tag definitions * Improve editor to be more WYSIWYG * Fixed tests * Added raw response tag * A few improvements to tag editor enum * fix NTLM typo (#244) * Tweaks and fixes for next release (#245)
2017-05-24 16:25:22 +00:00
settings: PropTypes.object.isRequired,
activeWorkspace: PropTypes.shape({
_id: PropTypes.string.isRequired
}).isRequired,
2017-06-07 00:07:09 +00:00
handleSetActiveWorkspace: PropTypes.func.isRequired,
// Optional
activeRequest: PropTypes.object,
activeEnvironment: PropTypes.shape({
_id: PropTypes.string.isRequired
})
};
2016-11-25 23:09:17 +00:00
function mapStateToProps (state, props) {
const {
entities,
global
} = state;
const {
isLoading,
loadingRequestIds
} = global;
// Entities
2016-11-25 23:09:17 +00:00
const entitiesLists = selectEntitiesLists(state, props);
const {
workspaces,
environments,
requests,
requestGroups
} = entitiesLists;
const settings = entitiesLists.settings[0];
// Workspace stuff
const workspaceMeta = selectActiveWorkspaceMeta(state, props) || {};
2016-11-25 23:09:17 +00:00
const activeWorkspace = selectActiveWorkspace(state, props);
const sidebarHidden = workspaceMeta.sidebarHidden || false;
const sidebarFilter = workspaceMeta.sidebarFilter || '';
const sidebarWidth = workspaceMeta.sidebarWidth || DEFAULT_SIDEBAR_WIDTH;
const paneWidth = workspaceMeta.paneWidth || DEFAULT_PANE_WIDTH;
const paneHeight = workspaceMeta.paneHeight || DEFAULT_PANE_HEIGHT;
// Request stuff
const requestMeta = selectActiveRequestMeta(state, props) || {};
const activeRequest = selectActiveRequest(state, props);
const responsePreviewMode = requestMeta.previewMode || PREVIEW_MODE_SOURCE;
const responseFilter = requestMeta.responseFilter || '';
const responseFilterHistory = requestMeta.responseFilterHistory || [];
2017-07-17 18:20:38 +00:00
// Response stuff
const activeRequestResponses = selectActiveRequestResponses(state, props) || [];
const activeResponse = selectActiveResponse(state, props) || null;
// Environment stuff
const activeEnvironmentId = workspaceMeta.activeEnvironmentId;
const activeEnvironment = entities.environments[activeEnvironmentId];
// OAuth2Token stuff
const oAuth2Token = selectActiveOAuth2Token(state, props);
// Find other meta things
const loadStartTime = loadingRequestIds[activeRequest ? activeRequest._id : 'n/a'] || -1;
2016-11-25 23:09:17 +00:00
const sidebarChildren = selectSidebarChildren(state, props);
2016-11-26 07:33:55 +00:00
const workspaceChildren = selectWorkspaceRequestsAndRequestGroups(state, props);
2017-07-27 22:59:07 +00:00
const unseenWorkspaces = selectUnseenWorkspaces(state, props);
return Object.assign({}, state, {
settings,
workspaces,
2017-07-27 22:59:07 +00:00
unseenWorkspaces,
requestGroups,
requests,
oAuth2Token,
isLoading,
loadStartTime,
activeWorkspace,
activeRequest,
2017-07-17 18:20:38 +00:00
activeRequestResponses,
activeResponse,
sidebarHidden,
sidebarFilter,
sidebarWidth,
paneWidth,
paneHeight,
responsePreviewMode,
responseFilter,
responseFilterHistory,
sidebarChildren,
environments,
activeEnvironment,
workspaceChildren
});
}
function mapDispatchToProps (dispatch) {
const global = bindActionCreators(globalActions, dispatch);
return {
handleStartLoading: global.loadRequestStart,
handleStopLoading: global.loadRequestStop,
handleSetActiveWorkspace: global.setActiveWorkspace,
handleImportFileToWorkspace: global.importFile,
handleImportUriToWorkspace: global.importUri,
handleCommand: global.newCommand,
handleExportFile: global.exportFile,
2017-06-13 20:45:15 +00:00
handleMoveDoc: _moveDoc
};
}
2017-06-13 20:45:15 +00:00
async function _moveDoc (docToMove, parentId, targetId, targetOffset) {
if (docToMove._id === targetId) {
// Nothing to do. We are in the same spot as we started
return;
}
2017-06-13 20:45:15 +00:00
function __updateDoc (doc, patch) {
models.getModel(docToMove.type).update(doc, patch);
}
2016-03-20 04:47:43 +00:00
if (targetId === null) {
// We are moving to an empty area. No sorting required
2017-06-13 20:45:15 +00:00
await __updateDoc(docToMove, {parentId});
return;
}
// NOTE: using requestToTarget's parentId so we can switch parents!
2017-06-13 20:45:15 +00:00
let docs = [
...await models.request.findByParentId(parentId),
...await models.requestGroup.findByParentId(parentId)
].sort((a, b) => a.metaSortKey < b.metaSortKey ? -1 : 1);
2017-06-13 20:45:15 +00:00
// Find the index of doc B so we can re-order and save everything
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
2017-06-13 20:45:15 +00:00
if (doc._id === targetId) {
let before, after;
if (targetOffset < 0) {
// We're moving to below
2017-06-13 20:45:15 +00:00
before = docs[i];
after = docs[i + 1];
} else {
// We're moving to above
2017-06-13 20:45:15 +00:00
before = docs[i - 1];
after = docs[i];
}
2017-06-13 20:45:15 +00:00
const beforeKey = before ? before.metaSortKey : docs[0].metaSortKey - 100;
const afterKey = after ? after.metaSortKey : docs[docs.length - 1].metaSortKey + 100;
if (Math.abs(afterKey - beforeKey) < 0.000001) {
// If sort keys get too close together, we need to redistribute the list. This is
// not performant at all (need to update all siblings in DB), but it is extremely rare
// anyway
console.log(`-- Recreating Sort Keys ${beforeKey} ${afterKey} --`);
db.bufferChanges(300);
2017-06-13 20:45:15 +00:00
docs.map((r, i) => __updateDoc(r, {metaSortKey: i * 100, parentId}));
} else {
const metaSortKey = afterKey - ((afterKey - beforeKey) / 2);
2017-06-13 20:45:15 +00:00
__updateDoc(docToMove, {metaSortKey, parentId});
}
break;
2016-04-16 23:24:57 +00:00
}
2016-03-20 04:47:43 +00:00
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);