From e7bc8bb860cb173c85bbc0fffdb06055f9040e75 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 1 Jun 2017 15:58:09 -0700 Subject: [PATCH] UI/UX and tweaks to Markdown Descriptions (#279) * Styles and editor done for requests * Add description markdown to WOrkspace settings * Show file icon beside request with description * Add tracking --- app/common/misc.js | 15 +- app/ui/components/base/link.js | 18 +- app/ui/components/codemirror/code-editor.js | 12 +- .../dropdowns/request-actions-dropdown.js | 10 +- app/ui/components/markdown-editor.js | 165 ++++++++++++++++++ app/ui/components/markdown.js | 74 -------- .../modals/request-settings-modal.js | 85 ++++++--- .../modals/workspace-settings-modal.js | 109 ++++++++---- .../components/sidebar/sidebar-request-row.js | 30 +++- app/ui/components/wrapper.js | 18 +- app/ui/css/components/forms.less | 25 +-- app/ui/css/components/markdown-editor.less | 124 +++++++++++++ app/ui/css/components/tabs.less | 5 +- app/ui/css/editor/general.less | 10 ++ app/ui/css/index.less | 1 + package-lock.json | 5 + package.json | 3 +- 17 files changed, 534 insertions(+), 175 deletions(-) create mode 100644 app/ui/components/markdown-editor.js delete mode 100644 app/ui/components/markdown.js create mode 100644 app/ui/css/components/markdown-editor.less diff --git a/app/common/misc.js b/app/common/misc.js index 15744b79f8..21374b2cb4 100644 --- a/app/common/misc.js +++ b/app/common/misc.js @@ -1,7 +1,8 @@ import uuid from 'uuid'; import {parse as urlParse, format as urlFormat} from 'url'; -import {DEBOUNCE_MILLIS} from './constants'; +import {DEBOUNCE_MILLIS, getAppVersion, isDevelopment} from './constants'; import * as querystring from './querystring'; +import {shell} from 'electron'; const URL_PATH_CHARACTER_WHITELIST = '+,;@=:'; @@ -257,3 +258,15 @@ export function preventDefault (e) { export function stopPropagation (e) { e.stopPropagation(); } + +export function clickLink (href) { + if (href.match(/^http/i)) { + const appName = isDevelopment() ? 'Insomnia Dev' : 'Insomnia'; + const qs = `utm_source=${appName}&utm_medium=app&utm_campaign=v${getAppVersion()}`; + const attributedHref = querystring.joinUrl(href, qs); + shell.openExternal(attributedHref); + } else { + // Don't modify non-http urls + shell.openExternal(href); + } +} diff --git a/app/ui/components/base/link.js b/app/ui/components/base/link.js index 2f09383197..0ac4fc9147 100644 --- a/app/ui/components/base/link.js +++ b/app/ui/components/base/link.js @@ -1,9 +1,7 @@ -import React, {PureComponent, PropTypes} from 'react'; +import React, {PropTypes, PureComponent} from 'react'; import autobind from 'autobind-decorator'; -import {shell} from 'electron'; import {trackEvent} from '../../../analytics/index'; -import {getAppVersion, isDevelopment} from '../../../common/constants'; -import * as querystring from '../../../common/querystring'; +import * as misc from '../../../common/misc'; @autobind class Link extends PureComponent { @@ -13,17 +11,7 @@ class Link extends PureComponent { // Also call onClick that was passed to us if there was one onClick && onClick(e); - - if (href.match(/^http/i)) { - const appName = isDevelopment() ? 'Insomnia Dev' : 'Insomnia'; - const qs = `utm_source=${appName}&utm_medium=app&utm_campaign=v${getAppVersion()}`; - const attributedHref = querystring.joinUrl(href, qs); - shell.openExternal(attributedHref); - } else { - // Don't modify non-http urls - shell.openExternal(href); - } - + misc.clickLink(href); trackEvent('Link', 'Click', href); } diff --git a/app/ui/components/codemirror/code-editor.js b/app/ui/components/codemirror/code-editor.js index 60059992ea..f3c619aa7b 100644 --- a/app/ui/components/codemirror/code-editor.js +++ b/app/ui/components/codemirror/code-editor.js @@ -339,7 +339,8 @@ class CodeEditor extends PureComponent { hideScrollbars, noStyleActiveLine, noLint, - indentSize + indentSize, + dynamicHeight } = this.props; let mode; @@ -410,6 +411,10 @@ class CodeEditor extends PureComponent { }; } + if (dynamicHeight) { + options.viewportMargin = Infinity; + } + // Strip of charset if there is one const cm = this.codeMirror; Object.keys(options).map(key => { @@ -558,11 +563,13 @@ class CodeEditor extends PureComponent { onMouseLeave, onClick, className, + dynamicHeight, style } = this.props; const classes = classnames(className, { 'editor': true, + 'editor--dynamic-height': dynamicHeight, 'editor--readonly': readOnly }); @@ -673,7 +680,8 @@ CodeEditor.propTypes = { readOnly: PropTypes.bool, filter: PropTypes.string, singleLine: PropTypes.bool, - debounceMillis: PropTypes.number + debounceMillis: PropTypes.number, + dynamicHeight: PropTypes.bool }; export default CodeEditor; diff --git a/app/ui/components/dropdowns/request-actions-dropdown.js b/app/ui/components/dropdowns/request-actions-dropdown.js index 11023e7ef4..2bee836139 100644 --- a/app/ui/components/dropdowns/request-actions-dropdown.js +++ b/app/ui/components/dropdowns/request-actions-dropdown.js @@ -2,9 +2,7 @@ import React, {PropTypes, PureComponent} from 'react'; import autobind from 'autobind-decorator'; import PromptButton from '../base/prompt-button'; import {Dropdown, DropdownButton, DropdownHint, DropdownItem} from '../base/dropdown'; -import RequestSettingsModal from '../modals/request-settings-modal'; import * as models from '../../../models'; -import {showModal} from '../modals/index'; import {trackEvent} from '../../../analytics/index'; import {DropdownDivider} from '../base/dropdown/index'; @@ -25,10 +23,6 @@ class RequestActionsDropdown extends PureComponent { trackEvent('Request', 'Generate Code', 'Request Action'); } - _handleShowRequestSettings () { - showModal(RequestSettingsModal, this.props.request); - } - _handleRemove () { const {request} = this.props; models.request.remove(request); @@ -42,6 +36,7 @@ class RequestActionsDropdown extends PureComponent { render () { const { request, // eslint-disable-line no-unused-vars + handleShowSettings, ...other } = this.props; @@ -63,7 +58,7 @@ class RequestActionsDropdown extends PureComponent { - + Settings @@ -75,6 +70,7 @@ class RequestActionsDropdown extends PureComponent { RequestActionsDropdown.propTypes = { handleDuplicateRequest: PropTypes.func.isRequired, handleGenerateCode: PropTypes.func.isRequired, + handleShowSettings: PropTypes.func.isRequired, request: PropTypes.object.isRequired }; diff --git a/app/ui/components/markdown-editor.js b/app/ui/components/markdown-editor.js new file mode 100644 index 0000000000..8678022d92 --- /dev/null +++ b/app/ui/components/markdown-editor.js @@ -0,0 +1,165 @@ +import React, {PropTypes, PureComponent} from 'react'; +import ReactDOM from 'react-dom'; +import autobind from 'autobind-decorator'; +import classnames from 'classnames'; +import marked from 'marked'; +import highlight from 'highlight.js'; +import {Tab, TabList, TabPanel, Tabs} from 'react-tabs'; +import {trackEvent} from '../../analytics'; +import Button from './base/button'; +import CodeEditor from './codemirror/code-editor'; +import * as misc from '../../common/misc'; + +@autobind +class MarkdownEditor extends PureComponent { + constructor (props) { + super(props); + this.state = { + markdown: props.defaultValue, + compiled: '' + }; + } + + _trackTab (name) { + trackEvent('Request', 'Markdown Editor Tab', name); + } + + _handleChange (markdown) { + this.props.onChange(markdown); + this._compileMarkdown(markdown); + } + + async _compileMarkdown (markdown) { + const compiled = marked(await this.props.handleRender(markdown)); + this.setState({markdown, compiled}); + } + + _setPreviewRef (n) { + this._preview = n; + } + + _highlightCodeBlocks () { + if (!this._preview) { + return; + } + + const el = ReactDOM.findDOMNode(this._preview); + for (const block of el.querySelectorAll('pre > code')) { + highlight.highlightBlock(block); + } + + for (const a of el.querySelectorAll('a')) { + a.addEventListener('click', e => { + e.preventDefault(); + misc.clickLink(e.target.getAttribute('href')); + }); + } + } + + componentWillMount () { + this._compileMarkdown(this.state.markdown); + } + + componentDidUpdate () { + this._highlightCodeBlocks(); + } + + componentDidMount () { + marked.setOptions({ + renderer: new marked.Renderer(), + gfm: true, + tables: true, + breaks: false, + pedantic: false, + smartLists: true, + smartypants: false + }); + + this._highlightCodeBlocks(); + } + + render () { + const { + fontSize, + lineWrapping, + indentSize, + keyMap, + placeholder, + defaultPreviewMode, + className, + handleRender, + handleGetRenderContext + } = this.props; + + const {markdown, compiled} = this.state; + + return ( + + + + + + + + + + +
+ +
+
+ Styling with Markdown is supported +
+
+ +
+ {/* Set from above */} +
+
+
+ ); + } +} + +MarkdownEditor.propTypes = { + // Required + onChange: PropTypes.func.isRequired, + defaultValue: PropTypes.string.isRequired, + fontSize: PropTypes.number.isRequired, + indentSize: PropTypes.number.isRequired, + keyMap: PropTypes.string.isRequired, + lineWrapping: PropTypes.bool.isRequired, + handleRender: PropTypes.func.isRequired, + handleGetRenderContext: PropTypes.func.isRequired, + + // Optional + placeholder: PropTypes.string, + defaultPreviewMode: PropTypes.bool, + className: PropTypes.string +}; + +export default MarkdownEditor; diff --git a/app/ui/components/markdown.js b/app/ui/components/markdown.js deleted file mode 100644 index 6ae810d820..0000000000 --- a/app/ui/components/markdown.js +++ /dev/null @@ -1,74 +0,0 @@ -import React, {PropTypes, PureComponent} from 'react'; -import autobind from 'autobind-decorator'; -import {Tab, TabList, TabPanel, Tabs} from 'react-tabs'; -import {trackEvent} from '../../analytics'; -import Button from './base/button'; -import CodeEditor from './codemirror/code-editor'; -import marked from 'marked'; - -marked.setOptions({ - renderer: new marked.Renderer(), - gfm: true, - tables: true, - breaks: false, - pedantic: false, - smartLists: true, - smartypants: false -}); - -@autobind -class Markdown extends PureComponent { - constructor (props) { - super(props); - this._defaultValue = props.defaultValue; - this._compiled = marked(this._defaultValue); - } - - _trackTab (name) { - trackEvent('Request', 'Markdown Editor Tab', name); - } - - _handleChange (value) { - this._compiled = marked(value); - this.props.onChange(value); - } - - render () { - return ( - - - - - - - - - - - - - -
-
-
- ); - } -} - -Markdown.propTypes = { - // Required - onChange: PropTypes.func.isRequired, - defaultValue: PropTypes.string.isRequired -}; - -export default Markdown; diff --git a/app/ui/components/modals/request-settings-modal.js b/app/ui/components/modals/request-settings-modal.js index 0f1b799761..2721f10991 100644 --- a/app/ui/components/modals/request-settings-modal.js +++ b/app/ui/components/modals/request-settings-modal.js @@ -1,6 +1,5 @@ -import React, {PureComponent} from 'react'; +import React, {PropTypes, PureComponent} from 'react'; import autobind from 'autobind-decorator'; -import Link from '../base/link'; import Modal from '../base/modal'; import ModalBody from '../base/modal-body'; import ModalHeader from '../base/modal-header'; @@ -8,14 +7,16 @@ import HelpTooltip from '../help-tooltip'; import * as models from '../../../models'; import {trackEvent} from '../../../analytics/index'; import DebouncedInput from '../base/debounced-input'; -import Markdown from '../markdown'; +import MarkdownEditor from '../markdown-editor'; @autobind class RequestSettingsModal extends PureComponent { constructor (props) { super(props); this.state = { - request: null + request: null, + showDescription: false, + defaultPreviewMode: false }; } @@ -38,12 +39,22 @@ class RequestSettingsModal extends PureComponent { async _handleDescriptionChange (description) { const request = await models.request.update(this.state.request, {description}); - this.setState({request}); + this.setState({request, defaultPreviewMode: false}); + } + + _handleAddDescription () { + trackEvent('Request', 'Add Description'); + this.setState({showDescription: true}); } show (request) { this.modal.show(); - this.setState({request}); + const hasDescription = !!request.description; + this.setState({ + request, + showDescription: hasDescription, + defaultPreviewMode: hasDescription + }); } hide () { @@ -62,10 +73,21 @@ class RequestSettingsModal extends PureComponent { } renderModalBody (request) { + const { + editorLineWrapping, + editorFontSize, + editorIndentSize, + editorKeyMap, + handleRender, + handleGetRenderContext + } = this.props; + + const {showDescription, defaultPreviewMode} = this.state; + return (
-
-
- -
-
-

Cookie Handling

+ {showDescription ? ( + + ) : ( + + )} +
-
-
-

Advanced Settings