import React, {Component, PropTypes} from 'react'; import {ipcRenderer} from 'electron'; import ReactDOM from 'react-dom'; import * as importers from 'insomnia-importers'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import HTML5Backend from 'react-dnd-html5-backend'; import {DragDropContext} from 'react-dnd'; import Mousetrap from '../mousetrap'; import {toggleModal, showModal} from '../components/modals'; import Wrapper from '../components/Wrapper'; import WorkspaceEnvironmentsEditModal from '../components/modals/WorkspaceEnvironmentsEditModal'; import Toast from '../components/Toast'; import CookiesModal from '../components/modals/CookiesModal'; import RequestSwitcherModal from '../components/modals/RequestSwitcherModal'; import PromptModal from '../components/modals/PromptModal'; import ChangelogModal from '../components/modals/ChangelogModal'; import SettingsModal from '../components/modals/SettingsModal'; import {MAX_PANE_WIDTH, MIN_PANE_WIDTH, DEFAULT_PANE_WIDTH, MAX_SIDEBAR_REMS, MIN_SIDEBAR_REMS, DEFAULT_SIDEBAR_WIDTH, getAppVersion} from '../../common/constants'; import * as globalActions from '../redux/modules/global'; import * as workspaceMetaActions from '../redux/modules/workspaceMeta'; import * as requestMetaActions from '../redux/modules/requestMeta'; import * as requestGroupMetaActions from '../redux/modules/requestGroupMeta'; import * as db from '../../common/database'; import * as models from '../../models'; import {importRaw} from '../../common/import'; import {trackEvent, trackLegacyEvent} from '../../analytics'; import {PREVIEW_MODE_SOURCE} from '../../common/constants'; class App extends Component { constructor (props) { super(props); this.state = { draggingSidebar: false, draggingPane: false, forceRefreshCounter: 0, }; // Bind functions once, so we don't have to on every render this._boundStartDragSidebar = this._startDragSidebar.bind(this); this._boundResetDragSidebar = this._resetDragSidebar.bind(this); this._boundStartDragPane = this._startDragPane.bind(this); this._boundResetDragPane = this._resetDragPane.bind(this); this._boundHandleUrlChange = this._handleUrlChanged.bind(this); this._boundRequestCreate = this._requestCreate.bind(this); this._boundRequestGroupCreate = this._requestGroupCreate.bind(this); this.globalKeyMap = { // Show Settings 'mod+,': () => { // NOTE: This is controlled via a global menu shortcut in app.js }, // Show Request Switcher 'mod+p': () => { toggleModal(RequestSwitcherModal); }, // Request Send 'mod+enter': () => { const {handleSendRequestWithEnvironment, activeRequest, activeEnvironment} = this.props; handleSendRequestWithEnvironment( activeRequest ? activeRequest._id : 'n/a', activeEnvironment ? activeEnvironment._id : 'n/a', ); }, // Edit Workspace Environments 'mod+e': () => { const {activeWorkspace} = this.props; toggleModal(WorkspaceEnvironmentsEditModal, activeWorkspace); }, // Focus URL Bar 'mod+l': () => { const node = document.body.querySelector('.urlbar input'); node && node.focus(); }, // Edit Cookies 'mod+k': () => { const {activeWorkspace} = this.props; toggleModal(CookiesModal, activeWorkspace); }, // Request Create 'mod+n': () => { const {activeRequest, activeWorkspace} = this.props; const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id; this._requestCreate(parentId); }, // 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) } } } async _requestGroupCreate (parentId) { const name = await showModal(PromptModal, { headerName: 'Create New Folder', defaultValue: 'My Folder', selectText: true }); models.requestGroup.create({parentId, name}) } async _requestCreate (parentId) { const name = await showModal(PromptModal, { headerName: 'Create New Request', defaultValue: 'My Request', selectText: true }); const {activeWorkspace, handleSetActiveRequest} = this.props; const request = await models.request.create({parentId, name}); handleSetActiveRequest(activeWorkspace._id, request._id); } async _handleUrlChanged (request, url) { // Allow user to paste any import file into the url. If it results in // only one item, it will overwrite the current request. try { const {resources} = importers.import(url); const r = resources[0]; if (r && r._type === 'request') { const cookieHeaders = r.cookies.map(({name, value}) => ( {name: 'cookie', value: `${name}=${value}`} )); // Only pull fields that we want to update await models.request.update(request, { url: r.url, method: r.method, headers: [...r.headers, ...cookieHeaders], body: r.body, authentication: r.authentication, parameters: r.parameters, }); this._forceHardRefresh(); return; } } catch (e) { // Import failed, that's alright } models.request.update(request, {url}); } _startDragSidebar () { this.setState({draggingSidebar: true}) } _resetDragSidebar () { // TODO: Remove setTimeout need be not triggering drag on double click setTimeout(() => { const {handleSetSidebarWidth, activeWorkspace} = this.props; handleSetSidebarWidth(activeWorkspace._id, DEFAULT_SIDEBAR_WIDTH) }, 50); } _startDragPane () { this.setState({draggingPane: true}) } _resetDragPane () { // TODO: Remove setTimeout need be not triggering drag on double click setTimeout(() => { const {handleSetPaneWidth, activeWorkspace} = this.props; handleSetPaneWidth(activeWorkspace._id, DEFAULT_PANE_WIDTH); }, 50); } _handleMouseMove (e) { if (this.state.draggingPane) { 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.props.handleSetPaneWidth(this.props.activeWorkspace._id, paneWidth); } else if (this.state.draggingSidebar) { const currentPixelWidth = ReactDOM.findDOMNode(this._sidebar).offsetWidth; const ratio = e.clientX / currentPixelWidth; const width = this.props.sidebarWidth * ratio; let sidebarWidth = Math.max(Math.min(width, MAX_SIDEBAR_REMS), MIN_SIDEBAR_REMS); this.props.handleSetSidebarWidth(this.props.activeWorkspace._id, sidebarWidth); } } _handleMouseUp () { if (this.state.draggingSidebar) { this.setState({draggingSidebar: false}); } if (this.state.draggingPane) { this.setState({draggingPane: false}); } } _handleToggleSidebar () { const {activeWorkspace, sidebarHidden, handleSetSidebarHidden} = this.props; handleSetSidebarHidden(activeWorkspace._id, !sidebarHidden); } _forceHardRefresh () { this.setState({forceRefreshCounter: this.state.forceRefreshCounter + 1}); } async componentDidMount () { // Bind handlers before we use them this._handleMouseUp = this._handleMouseUp.bind(this); this._handleMouseMove = this._handleMouseMove.bind(this); // Bind mouse handlers document.addEventListener('mouseup', this._handleMouseUp); document.addEventListener('mousemove', this._handleMouseMove); // Map global keyboard shortcuts Object.keys(this.globalKeyMap).map(key => { Mousetrap.bindGlobal(key.split('|'), this.globalKeyMap[key]); }); // Do The Analytics trackLegacyEvent('App Launched'); // Update Stats Object const {lastVersion, launches} = await models.stats.get(); const firstLaunch = !lastVersion; if (firstLaunch) { // TODO: Show a welcome message trackLegacyEvent('First Launch'); } else if (lastVersion !== getAppVersion()) { trackEvent('General', 'Updated', getAppVersion()); showModal(ChangelogModal); } db.onChange(changes => { for (const change of changes) { const [event, doc, fromSync] = change; // Not a sync-related change if (!fromSync) { return; } const {activeRequest} = this.props; // No active request at the moment, so it doesn't matter if (!activeRequest) { return; } // Only force the UI to refresh if the active Request changes // This is because things like the URL and Body editor don't update // when you tell them to. if (doc._id !== activeRequest._id) { return; } console.log('[App] Forcing update'); // All sync-related changes to data force-refresh the app. this._forceHardRefresh(); } }); models.stats.update({ launches: launches + 1, lastLaunch: Date.now(), lastVersion: getAppVersion() }); ipcRenderer.on('toggle-preferences', () => { toggleModal(SettingsModal); }); ipcRenderer.on('toggle-sidebar', this._handleToggleSidebar.bind(this)); } componentWillUnmount () { // Remove mouse handlers document.removeEventListener('mouseup', this._handleMouseUp); document.removeEventListener('mousemove', this._handleMouseMove); // Unbind global keyboard shortcuts Mousetrap.unbind(); } render () { return (