diff --git a/app/common/har.js b/app/common/har.js index 82c850dc4..1b44cbb06 100644 --- a/app/common/har.js +++ b/app/common/har.js @@ -13,6 +13,7 @@ import type {Response as ResponseModel} from '../models/response'; import {getAuthHeader} from '../network/authentication'; import {getAppVersion} from './constants'; import {getSetCookieHeaders} from './misc'; +import {RenderError} from '../templating/index'; export type HarCookie = { name: string, @@ -264,8 +265,16 @@ export async function exportHarRequest (requestId: string, environmentId: string } export async function exportHarWithRequest (request: Request, environmentId: string, addContentLength: boolean = false): Promise { - const renderedRequest = await getRenderedRequest(request, environmentId); - return await exportHarWithRenderedRequest(renderedRequest, addContentLength); + try { + const renderedRequest = await getRenderedRequest(request, environmentId); + return exportHarWithRenderedRequest(renderedRequest, addContentLength); + } catch (err) { + if (err instanceof RenderError) { + throw new Error(`Failed to render "${request.name}:${err.path}"\n ${err.message}`); + } else { + throw new Error(`Failed to export request "${request.name}"\n ${err.message}`); + } + } } export async function exportHarWithRenderedRequest (renderedRequest: RenderedRequest, addContentLength: boolean = false): Promise { @@ -328,8 +337,7 @@ function getReponseCookies (response: ResponseModel): Array { } return mapCookie(cookie); - }) - .filter(Boolean); + }).filter(Boolean); } function mapCookie (cookie: Cookie): HarCookie { diff --git a/app/ui/components/modals/ask-modal.js b/app/ui/components/modals/ask-modal.js index ee81c99d5..abacfec7e 100644 --- a/app/ui/components/modals/ask-modal.js +++ b/app/ui/components/modals/ask-modal.js @@ -20,6 +20,10 @@ class AskModal extends PureComponent { this.modal = m; } + _setYesButtonRef (n) { + this.yesButton = n; + } + _handleYes () { this.hide(); this._doneCallback && this._doneCallback(true); @@ -49,6 +53,10 @@ class AskModal extends PureComponent { this.modal.show(); + setTimeout(() => { + this.yesButton && this.yesButton.focus(); + }, 100); + return new Promise(resolve => { this._promiseCallback = resolve; }); @@ -68,7 +76,7 @@ class AskModal extends PureComponent { - diff --git a/app/ui/components/modals/error-modal.js b/app/ui/components/modals/error-modal.js index f68d7622a..1ce8dc38d 100644 --- a/app/ui/components/modals/error-modal.js +++ b/app/ui/components/modals/error-modal.js @@ -46,7 +46,7 @@ class ErrorModal extends PureComponent { const {error, message, title, addCancel} = this.state; return ( - + {title || 'Uh Oh!'} {message ? ( diff --git a/app/ui/components/modals/select-modal.js b/app/ui/components/modals/select-modal.js new file mode 100644 index 000000000..33b321878 --- /dev/null +++ b/app/ui/components/modals/select-modal.js @@ -0,0 +1,101 @@ +// @flow +import * as React from 'react'; +import autobind from 'autobind-decorator'; +import Modal from '../base/modal'; +import ModalBody from '../base/modal-body'; +import ModalHeader from '../base/modal-header'; +import ModalFooter from '../base/modal-footer'; + +type Props = {}; + +type State = { + title: string, + options: Array<{name: string, value: string}>, + value: string, + message: string +}; + +@autobind +class SelectModal extends React.PureComponent { + modal: ?Modal; + doneButton: ?HTMLSelectElement; + _doneCallback: ?Function; + + constructor (props: Props) { + super(props); + + this.state = { + title: '', + options: [], + message: '', + value: '' + }; + } + + _setModalRef (m: ?Modal) { + this.modal = m; + } + + _setDoneButtonRef (n: ?HTMLSelectElement) { + this.doneButton = n; + } + + _handleDone () { + this.hide(); + this._doneCallback && this._doneCallback(this.state.value); + } + + _handleSelectChange (e: SyntheticEvent) { + this.setState({value: e.currentTarget.value}); + } + + hide () { + this.modal && this.modal.hide(); + } + + show (data: Object = {}) { + const { + title, + message, + options, + value, + onDone + } = data; + + this._doneCallback = onDone; + + this.setState({title, message, options, value}); + + this.modal && this.modal.show(); + setTimeout(() => { + this.doneButton && this.doneButton.focus(); + }, 100); + } + + render () { + const {message, title, options, value} = this.state; + + return ( + + {title || 'Confirm?'} + +

{message}

+
+ +
+
+ + + +
+ ); + } +} + +export default SelectModal; diff --git a/app/ui/components/wrapper.js b/app/ui/components/wrapper.js index 9e7006d8d..7055237d7 100644 --- a/app/ui/components/wrapper.js +++ b/app/ui/components/wrapper.js @@ -22,6 +22,7 @@ import PaymentNotificationModal from './modals/payment-notification-modal'; import NunjucksModal from './modals/nunjucks-modal'; import PromptModal from './modals/prompt-modal'; import AskModal from './modals/ask-modal'; +import SelectModal from './modals/select-modal'; import RequestCreateModal from './modals/request-create-modal'; import RequestPane from './request-pane'; import RequestSwitcherModal from './modals/request-switcher-modal'; @@ -403,6 +404,7 @@ class Wrapper extends React.PureComponent { + diff --git a/app/ui/redux/modules/global.js b/app/ui/redux/modules/global.js index 5c965e154..be3aad384 100644 --- a/app/ui/redux/modules/global.js +++ b/app/ui/redux/modules/global.js @@ -13,6 +13,8 @@ import {showModal} from '../../components/modals'; import PaymentNotificationModal from '../../components/modals/payment-notification-modal'; import LoginModal from '../../components/modals/login-modal'; import * as models from '../../../models'; +import SelectModal from '../../components/modals/select-modal'; +import {showError} from '../../components/modals/index'; const LOCALSTORAGE_PREFIX = `insomnia::meta`; @@ -180,79 +182,108 @@ export function importUri (workspaceId, uri) { } export function exportFile (workspaceId = null) { - return async dispatch => { + return dispatch => { dispatch(loadStart()); - const workspace = await models.workspace.getById(workspaceId); + const VALUE_JSON = 'json'; + const VALUE_HAR = 'har'; - // Check if we want to export private environments - let environments; - if (workspace) { - const parentEnv = await models.environment.getOrCreateForWorkspace(workspace); - environments = [ - parentEnv, - ...await models.environment.findByParentId(parentEnv._id) - ]; - } else { - environments = await models.environment.all(); - } - - let exportPrivateEnvironments = false; - const privateEnvironments = environments.filter(e => e.isPrivate); - if (privateEnvironments.length) { - const names = privateEnvironments.map(e => e.name).join(', '); - exportPrivateEnvironments = await showModal(AskModal, { - title: 'Export Private Environments?', - message: `Do you want to include private environments (${names}) in your export?` - }); - } - - const date = moment().format('YYYY-MM-DD'); - const name = (workspace ? workspace.name : 'Insomnia All').replace(/ /g, '-'); - const lastDir = window.localStorage.getItem('insomnia.lastExportPath'); - const dir = lastDir || electron.remote.app.getPath('desktop'); - - const options = { - title: 'Export Insomnia Data', - buttonLabel: 'Export', - defaultPath: path.join(dir, `${name}_${date}`), - filters: [{ - name: 'Insomnia Export', extensions: ['json'] - }, { - name: 'HTTP Archive 1.2', extensions: ['har'] - }] - }; - - electron.remote.dialog.showSaveDialog(options, async filename => { - if (!filename) { - trackEvent('Export', 'Cancel'); - // It was cancelled, so let's bail out + showModal(SelectModal, { + title: 'Select Export Type', + options: [ + {name: 'Insomnia – Sharable with other Insomnia users', value: VALUE_JSON}, + {name: 'HAR – HTTP Archive Format', value: VALUE_HAR} + ], + message: 'Which format would you like to export as?', + onCancel: () => { dispatch(loadStop()); - return; - } + }, + onDone: async selectedFormat => { + const workspace = await + models.workspace.getById(workspaceId); - let json; - if (path.extname(filename) === '.har') { - json = await importUtils.exportHAR(workspace, exportPrivateEnvironments); - } else { - json = await importUtils.exportJSON(workspace, exportPrivateEnvironments); - } - - // Remember last exported path - window.localStorage.setItem( - 'insomnia.lastExportPath', - path.dirname(filename) - ); - - fs.writeFile(filename, json, {}, err => { - if (err) { - console.warn('Export failed', err); - trackEvent('Export', 'Failure'); - return; + // Check if we want to export private environments + let environments; + if (workspace) { + const parentEnv = await models.environment.getOrCreateForWorkspace(workspace); + environments = [ + parentEnv, + ...await models.environment.findByParentId(parentEnv._id) + ]; + } else { + environments = await models.environment.all(); } - trackEvent('Export', 'Success'); - dispatch(loadStop()); - }); + + let exportPrivateEnvironments = false; + const privateEnvironments = environments.filter(e => e.isPrivate); + if (privateEnvironments.length) { + const names = privateEnvironments.map(e => e.name).join(', '); + exportPrivateEnvironments = await showModal(AskModal, { + title: 'Export Private Environments?', + message: `Do you want to include private environments (${names}) in your export?` + }); + } + + const date = moment().format('YYYY-MM-DD'); + const name = (workspace ? workspace.name : 'Insomnia All').replace(/ /g, '-'); + const lastDir = window.localStorage.getItem('insomnia.lastExportPath'); + const dir = lastDir || electron.remote.app.getPath('desktop'); + + const options = { + title: 'Export Insomnia Data', + buttonLabel: 'Export', + defaultPath: path.join(dir, `${name}_${date}`), + filters: [] + }; + + if (selectedFormat === VALUE_HAR) { + options.filters = [{name: 'HTTP Archive 1.2', extensions: ['har', 'har.json', 'json']}]; + } else { + options.filters = [{name: 'Insomnia Export', extensions: ['json']}]; + } + + electron.remote.dialog.showSaveDialog(options, async filename => { + if (!filename) { + trackEvent('Export', 'Cancel'); + // It was cancelled, so let's bail out + dispatch(loadStop()); + return; + } + + let json; + try { + if (selectedFormat === VALUE_HAR) { + json = await importUtils.exportHAR(workspace, exportPrivateEnvironments); + } else { + json = await importUtils.exportJSON(workspace, exportPrivateEnvironments); + } + } catch (err) { + showError({ + title: 'Export Failed', + error: err, + message: 'Export failed due to an unexpected error' + }); + dispatch(loadStop()); + return; + } + + // Remember last exported path + window.localStorage.setItem( + 'insomnia.lastExportPath', + path.dirname(filename) + ); + + fs.writeFile(filename, json, {}, err => { + if (err) { + console.warn('Export failed', err); + trackEvent('Export', 'Failure'); + return; + } + trackEvent('Export', 'Success'); + dispatch(loadStop()); + }); + }); + } }); }; } diff --git a/package-lock.json b/package-lock.json index 476b46369..2a28e9b79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@types/node": { - "version": "7.0.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.43.tgz", - "integrity": "sha512-7scYwwfHNppXvH/9JzakbVxk0o0QUILVk1Lv64GRaxwPuGpnF1QBiwdvhDpLcymb8BpomQL3KYoWKq3wUdDMhQ==", + "version": "7.0.46", + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.46.tgz", + "integrity": "sha512-u+JAi1KtmaUoU/EHJkxoiuvzyo91FCE41Z9TZWWcOUU3P8oUdlDLdrGzCGWySPgbRMD17B0B+1aaJLYI9egQ6A==", "dev": true }, "7zip": { @@ -2997,14 +2997,14 @@ "dev": true }, "electron": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/electron/-/electron-1.7.8.tgz", - "integrity": "sha1-J7eRpolRcafVKZG5lELNvRCjU50=", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/electron/-/electron-1.7.9.tgz", + "integrity": "sha1-rdVOn4+D7QL2UZ7BATX2mLGTNs8=", "dev": true, "requires": { - "@types/node": "7.0.43", + "@types/node": "7.0.46", "electron-download": "3.3.0", - "extract-zip": "1.6.5" + "extract-zip": "1.6.6" } }, "electron-builder": { @@ -4057,24 +4057,24 @@ } }, "extract-zip": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.5.tgz", - "integrity": "sha1-maBnNbbqIOqbcF13ms/8yHz/BEA=", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", + "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", "dev": true, "requires": { "concat-stream": "1.6.0", - "debug": "2.2.0", + "debug": "2.6.9", "mkdirp": "0.5.0", "yauzl": "2.4.1" }, "dependencies": { "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { - "ms": "0.7.1" + "ms": "2.0.0" } }, "mkdirp": { @@ -4085,12 +4085,6 @@ "requires": { "minimist": "0.0.8" } - }, - "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", - "dev": true } } }, diff --git a/package.json b/package.json index 4015ff584..ce617a2d0 100644 --- a/package.json +++ b/package.json @@ -181,7 +181,7 @@ "concurrently": "^2.0.0", "cross-env": "^2.0.0", "css-loader": "^0.26.2", - "electron": "^1.7.8", + "electron": "^1.7.9", "electron-builder": "^10.17.3", "electron-rebuild": "^1.6.0", "eslint": "^3.16.1",