Dedicated tab for request descriptions (#317)

* Added tab for mardown descriptions

* Some fixes

* Fix dropdown menu theme

* Track description editing
This commit is contained in:
Gregory Schier 2017-06-16 13:29:22 -07:00 committed by GitHub
parent be047d4018
commit 0e57b40ba3
17 changed files with 414 additions and 276 deletions

View File

@ -0,0 +1,16 @@
import marked from 'marked';
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false
});
export function markdownToHTML (markdown) {
return marked(markdown);
}

View File

@ -173,6 +173,7 @@ class Dropdown extends PureComponent {
const menuClasses = classnames({
'dropdown__menu': true,
'theme--dropdown__menu': true,
'dropdown__menu--open': open,
'dropdown__menu--outlined': outline,
'dropdown__menu--up': dropUp,

View File

@ -61,12 +61,11 @@ CodeMirror.defineOption('environmentAutocomplete', null, (cm, options) => {
return;
}
// Put the hints in a container with class "dropdown__menu" (for themes)
let hintsContainer = document.querySelector('#hints-container');
if (!hintsContainer) {
const el = document.createElement('div');
el.id = 'hints-container';
el.className = 'dropdown__menu';
el.className = 'theme--dropdown__menu';
document.body.appendChild(el);
hintsContainer = el;
}
@ -97,7 +96,7 @@ CodeMirror.defineOption('environmentAutocomplete', null, (cm, options) => {
}
// Good for debugging
// closeOnUnfocus: false
// ,closeOnUnfocus: false
});
}

View File

@ -1,23 +1,18 @@
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';
import MarkdownPreview from './markdown-preview';
@autobind
class MarkdownEditor extends PureComponent {
constructor (props) {
super(props);
this.state = {
markdown: props.defaultValue,
compiled: '',
renderError: ''
markdown: props.defaultValue
};
}
@ -27,64 +22,21 @@ class MarkdownEditor extends PureComponent {
_handleChange (markdown) {
this.props.onChange(markdown);
this._compileMarkdown(markdown);
this.setState({markdown});
// So we don't track on every keystroke, give analytics a longer timeout
clearTimeout(this._analyticsTimeout);
this._analyticsTimeout = setTimeout(() => {
trackEvent('Request', 'Edit Description');
}, 2000);
}
async _compileMarkdown (markdown) {
const newState = {markdown};
try {
const rendered = await this.props.handleRender(markdown);
newState.compiled = marked(rendered);
} catch (err) {
newState.renderError = err.message;
}
this.setState(newState);
_setEditorRef (n) {
this._editor = n;
}
_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,
sanitize: true,
smartLists: true,
smartypants: false
});
this._highlightCodeBlocks();
focusEnd () {
this._editor && this._editor.focusEnd();
}
render () {
@ -102,7 +54,7 @@ class MarkdownEditor extends PureComponent {
handleGetRenderContext
} = this.props;
const {markdown, compiled, renderError} = this.state;
const {markdown} = this.state;
const classes = classnames(
'markdown-editor',
@ -130,6 +82,7 @@ class MarkdownEditor extends PureComponent {
<TabPanel className="markdown-editor__edit">
<div className="form-control form-control--outlined">
<CodeEditor
ref={this._setEditorRef}
hideGutters
hideLineNumbers
dynamicHeight={!tall}
@ -153,12 +106,10 @@ class MarkdownEditor extends PureComponent {
</div>
</TabPanel>
<TabPanel className="markdown-editor__preview">
{renderError && <p className="notice error no-margin">Failed to render: {renderError}</p>}
<div className="markdown-editor__preview__content selectable"
ref={this._setPreviewRef}
dangerouslySetInnerHTML={{__html: compiled}}>
{/* Set from above */}
</div>
<MarkdownPreview
markdown={markdown}
handleRender={handleRender}
/>
</TabPanel>
</Tabs>
);

View File

@ -0,0 +1,100 @@
import React, {PropTypes, PureComponent} from 'react';
import ReactDOM from 'react-dom';
import classnames from 'classnames';
import autobind from 'autobind-decorator';
import highlight from 'highlight.js';
import * as misc from '../../common/misc';
import {markdownToHTML} from '../../common/markdown-to-html';
@autobind
class MarkdownPreview extends PureComponent {
constructor (props) {
super(props);
this.state = {
compiled: '',
renderError: ''
};
}
async _compileMarkdown (markdown) {
try {
const rendered = await this.props.handleRender(markdown);
this.setState({
compiled: markdownToHTML(rendered),
renderError: ''
});
} catch (err) {
this.setState({
renderError: err.message,
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.props.markdown);
}
componentDidUpdate () {
this._highlightCodeBlocks();
}
componentWillReceiveProps (nextProps) {
this._compileMarkdown(nextProps.markdown);
}
componentDidMount () {
this._highlightCodeBlocks();
}
render () {
const {className} = this.props;
const {compiled, renderError} = this.state;
return (
<div ref={this._setPreviewRef} className={classnames('markdown-preview', className)}>
{renderError && (
<p className="notice error no-margin">
Failed to render: {renderError}
</p>
)}
<div className="markdown-preview__content" dangerouslySetInnerHTML={{__html: compiled}}>
{/* Set from above */}
</div>
</div>
);
}
}
MarkdownPreview.propTypes = {
// Required
markdown: PropTypes.string.isRequired,
handleRender: PropTypes.func.isRequired,
// Optional
className: PropTypes.string
};
export default MarkdownPreview;

View File

@ -23,7 +23,7 @@ class RequestRenderErrorModal extends PureComponent {
_handleShowRequestSettings () {
this.hide();
showModal(RequestSettingsModal, this.state.request);
showModal(RequestSettingsModal, {request: this.state.request});
}
show ({request, error}) {

View File

@ -24,6 +24,10 @@ class RequestSettingsModal extends PureComponent {
this.modal = n;
}
_setEditorRef (n) {
this._editor = n;
}
async _updateRequestSettingBoolean (e) {
const value = e.target.checked;
const setting = e.target.name;
@ -47,14 +51,20 @@ class RequestSettingsModal extends PureComponent {
this.setState({showDescription: true});
}
show (request) {
show ({request, forceEditMode}) {
this.modal.show();
const hasDescription = !!request.description;
this.setState({
request,
showDescription: hasDescription,
defaultPreviewMode: hasDescription
showDescription: forceEditMode || hasDescription,
defaultPreviewMode: hasDescription && !forceEditMode
});
if (forceEditMode) {
setTimeout(() => {
this._editor.focusEnd();
}, 400);
}
}
hide () {
@ -103,6 +113,7 @@ class RequestSettingsModal extends PureComponent {
</div>
{showDescription ? (
<MarkdownEditor
ref={this._setEditorRef}
className="margin-top"
defaultPreviewMode={defaultPreviewMode}
fontSize={editorFontSize}

View File

@ -16,6 +16,9 @@ import * as querystring from '../../common/querystring';
import * as db from '../../common/database';
import * as models from '../../models';
import Hotkey from './hotkey';
import {showModal} from './modals/index';
import RequestSettingsModal from './modals/request-settings-modal';
import MarkdownPreview from './markdown-preview';
@autobind
class RequestPane extends PureComponent {
@ -25,6 +28,17 @@ class RequestPane extends PureComponent {
this._handleUpdateRequestUrl = debounce(this._handleUpdateRequestUrl);
}
_handleEditDescriptionAdd () {
this._handleEditDescription(true);
}
_handleEditDescription (addDescription) {
showModal(RequestSettingsModal, {
request: this.props.request,
forceEditMode: addDescription
});
}
async _autocompleteUrls () {
const docs = await db.withDescendants(
this.props.workspace,
@ -106,6 +120,10 @@ class RequestPane extends PureComponent {
trackEvent('Request Pane', 'View', 'Headers');
}
_trackTabDescription () {
trackEvent('Request Pane', 'View', 'Description');
}
_trackTabAuthentication () {
trackEvent('Request Pane', 'View', 'Authentication');
}
@ -249,6 +267,16 @@ class RequestPane extends PureComponent {
Header {numHeaders > 0 && <span className="bubble">{numHeaders}</span>}
</button>
</Tab>
<Tab onClick={this._trackTabDescription}>
<button>
Description
{request.description && (
<span className="bubble space-left">
<i className="fa fa--skinny fa-check txt-xxs"/>
</span>
)}
</button>
</Tab>
</TabList>
<TabPanel className="editor-wrapper">
<BodyEditor
@ -334,6 +362,37 @@ class RequestPane extends PureComponent {
</button>
</div>
</TabPanel>
<TabPanel key={uniqueKey}>
{request.description ? (
<div>
<div className="pull-right pad bg-default">
<button className="btn btn--clicky" onClick={this._handleEditDescription}>
Edit
</button>
</div>
<MarkdownPreview
className="pad"
markdown={request.description}
handleRender={handleRender}
/>
</div>
) : (
<div className="overflow-hidden editor vertically-center text-center">
<p className="pad text-sm text-center">
<span className="super-faint">
<i className="fa fa-file-text-o"
style={{fontSize: '8rem', opacity: 0.3}}
/>
</span>
<br/><br/>
<button className="btn btn--clicky faint"
onClick={this._handleEditDescriptionAdd}>
Add Description
</button>
</p>
</div>
)}
</TabPanel>
</Tabs>
</section>
);

View File

@ -58,12 +58,7 @@ class SidebarRequestRow extends PureComponent {
}
_handleShowRequestSettings () {
showModal(RequestSettingsModal, this.props.request);
}
_handleClickDescription () {
trackEvent('Request', 'Click Description Icon');
this._handleShowRequestSettings();
showModal(RequestSettingsModal, {request: this.props.request});
}
setDragDirection (dragDirection) {
@ -121,15 +116,6 @@ class SidebarRequestRow extends PureComponent {
className="inline-block"
onEditStart={this._handleEditStart}
onSubmit={this._handleRequestUpdateName}/>
{request.description && (
<a title={isActive ? 'View description' : null}
onClick={isActive ? this._handleClickDescription : null}
className={classnames('space-left super-duper-faint a--nocolor', {
'icon': isActive
})}>
<i className="fa fa-file-text-o space-left"/>
</a>
)}
</div>
</div>
</button>

View File

@ -154,7 +154,7 @@ class Wrapper extends PureComponent {
}
_handleShowRequestSettingsModal () {
showModal(RequestSettingsModal, this.props.activeRequest);
showModal(RequestSettingsModal, {request: this.props.activeRequest});
}
_handleDeleteResponses () {

View File

@ -88,7 +88,7 @@ class App extends PureComponent {
key: KEY_COMMA,
callback: () => {
if (this.props.activeRequest) {
showModal(RequestSettingsModal, this.props.activeRequest);
showModal(RequestSettingsModal, {request: this.props.activeRequest});
trackEvent('HotKey', 'Request Settings');
}
}

View File

@ -35,170 +35,5 @@
max-height: 35vh;
padding: @padding-sm;
overflow: auto;
.markdown-editor__preview__content {
max-width: 34em;
margin-right: auto;
h1 {
font-size: @font-size-xxl;
border-bottom: 1px solid var(--hl-sm);
font-weight: bold;
}
h2 {
font-size: @font-size-xl;
border-bottom: 1px solid var(--hl-sm);
font-weight: bold;
}
h3 {
font-size: @font-size-lg;
font-weight: bold;
}
h4 {
font-size: @font-size-md;
font-weight: bold;
opacity: 0.9;
}
h5 {
font-size: @font-size-sm;
font-weight: bold;
opacity: 0.9;
}
h6 {
font-size: @font-size-sm;
font-weight: bold;
opacity: 0.8;
}
& > * {
line-height: 1.7em;
margin: @padding-sm 0 @padding-md 0;
padding: 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
blockquote {
border-left: 0.4em solid var(--hl-md);
padding: @padding-xxs @padding-md;
p {
margin: @padding-xs 0;
}
}
*:not(pre) > code {
padding: @padding-xxs @padding-xs;
font-size: @font-size-sm;
line-height: @font-size-xs;
font-family: @font-monospace;
}
code {
background-color: var(--hl-xs);
border: 1px solid var(--hl-sm);
border-radius: @radius-sm;
}
pre > code {
padding: @padding-sm;
}
& > ul {
padding-left: @padding-xs;
list-style: disc;
ul {
list-style: circle;
}
}
ul {
margin-left: @padding-md;
}
.hljs {
width: 100%;
box-sizing: border-box;
.hljs-meta,
.hljs-comment,
.hljs-quote {
color: var(--hl);
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: var(--color-font);
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-built_in,
.hljs-builtin-name,
.hljs-type,
.hljs-class .hljs-title,
.hljs-tag .hljs-attr {
color: var(--color-surprise);
}
.hljs-symbol,
.hljs-bullet,
.hljs-title,
.hljs-section,
.hljs-selector-id,
.hljs-doctag {
color: var(--color-danger);
}
.hljs-string {
color: var(--color-notice);
}
.hljs-subst {
font-weight: normal;
}
.hljs-tag,
.hljs-name,
.hljs-attr,
.hljs-regexp,
.hljs-link,
.hljs-attribute {
color: var(--color-success);
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
}
}
}
}

View File

@ -0,0 +1,170 @@
@import '../constants/dimensions';
@import '../constants/colors';
@import '../constants/fonts';
.markdown-preview {
.markdown-preview__content {
max-width: 34em;
margin-right: auto;
h1 {
font-size: @font-size-xxl;
border-bottom: 1px solid var(--hl-sm);
font-weight: bold;
}
h2 {
font-size: @font-size-xl;
border-bottom: 1px solid var(--hl-sm);
font-weight: bold;
}
h3 {
font-size: @font-size-lg;
font-weight: bold;
}
h4 {
font-size: @font-size-md;
font-weight: bold;
opacity: 0.9;
}
h5 {
font-size: @font-size-sm;
font-weight: bold;
opacity: 0.9;
}
h6 {
font-size: @font-size-sm;
font-weight: bold;
opacity: 0.8;
}
& > * {
line-height: 1.7em;
margin: @padding-sm 0 @padding-md 0;
padding: 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
blockquote {
border-left: 0.4em solid var(--hl-md);
padding: @padding-xxs @padding-md;
p {
margin: @padding-xs 0;
}
}
*:not(pre) > code {
padding: @padding-xxs @padding-xs;
font-size: @font-size-sm;
line-height: @font-size-xs;
font-family: @font-monospace;
}
code {
background-color: var(--hl-xs);
border: 1px solid var(--hl-sm);
border-radius: @radius-sm;
}
pre > code {
padding: @padding-sm;
}
& > ul {
padding-left: @padding-xs;
list-style: disc;
ul {
list-style: circle;
}
}
ul {
margin-left: @padding-md;
}
.hljs {
width: 100%;
box-sizing: border-box;
.hljs-meta,
.hljs-comment,
.hljs-quote {
color: var(--hl);
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: var(--color-font);
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-built_in,
.hljs-builtin-name,
.hljs-type,
.hljs-class .hljs-title,
.hljs-tag .hljs-attr {
color: var(--color-surprise);
}
.hljs-symbol,
.hljs-bullet,
.hljs-title,
.hljs-section,
.hljs-selector-id,
.hljs-doctag {
color: var(--color-danger);
}
.hljs-string {
color: var(--color-notice);
}
.hljs-subst {
font-weight: normal;
}
.hljs-tag,
.hljs-name,
.hljs-attr,
.hljs-regexp,
.hljs-link,
.hljs-attribute {
color: var(--color-success);
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
}
}
}

View File

@ -77,7 +77,7 @@ body {
}
.pane__header,
.dropdown__menu,
.theme--dropdown__menu,
.modal {
--color-bg: #fff;
--color-font: #555;
@ -115,7 +115,7 @@ body {
// Light Parts
.request-pane,
.response-pane,
.dropdown__menu,
.theme--dropdown__menu,
.modal {
--hl-xxs: rgba(130, 130, 130, 0.05);
--hl-xs: rgba(130, 130, 130, 0.1);
@ -164,7 +164,7 @@ body {
--color-bg: #fafaff;
}
.dropdown__menu {
.theme--dropdown__menu {
--color-bg: #fff;
--color-font: #666;
}
@ -186,13 +186,13 @@ body {
--color-bg: #232421;
--color-font: #ddd;
--hl-xxs: rgba(120, 120, 120, 0.05);
--hl-xs: rgba(120, 120, 120, 0.1);
--hl-sm: rgba(120, 120, 120, 0.2);
--hl-md: rgba(120, 120, 120, 0.3);
--hl-lg: rgba(120, 120, 120, 0.5);
--hl-xl: rgba(120, 120, 120, 0.8);
--hl: rgba(120, 120, 120, 1);
--hl-xxs: rgba(130, 130, 130, 0.05);
--hl-xs: rgba(130, 130, 130, 0.1);
--hl-sm: rgba(130, 130, 130, 0.2);
--hl-md: rgba(130, 130, 130, 0.3);
--hl-lg: rgba(130, 130, 130, 0.5);
--hl-xl: rgba(130, 130, 130, 0.8);
--hl: rgba(140, 140, 140, 1);
.tag {
--color-success: #6ea932;
@ -208,7 +208,7 @@ body {
--color-font: #ddd;
}
.dropdown__menu,
.theme--dropdown__menu,
.modal {
--color-bg: #282925;
}
@ -217,7 +217,7 @@ body {
--color-font: #ccc;
}
.dropdown__menu {
.theme--dropdown__menu {
--color-font: #aaa;
}
}
@ -273,7 +273,7 @@ body {
--color-font: #657b83;
}
.dropdown__menu,
.theme--dropdown__menu,
.modal {
--color-bg: #fff8e5;
}
@ -303,7 +303,7 @@ body {
--color-font: #8ea0a2;
}
.dropdown__menu,
.theme--dropdown__menu,
.modal {
--color-bg: #073642;
}
@ -342,7 +342,7 @@ body {
}
.modal > *,
.dropdown__menu {
.theme--dropdown__menu {
--color-bg: #303c43;
--color-font: #dde1e1;
}
@ -370,7 +370,7 @@ body {
.sidebar__item,
.btn,
.tag,
.dropdown__menu,
.theme--dropdown__menu,
.CodeMirror,
.pane__body,
.modal__header,
@ -424,7 +424,7 @@ body {
--color-bg: rgba(238, 231, 213, 0.8);
}
.dropdown__menu,
.theme--dropdown__menu,
.modal {
--color-bg: #fff8e5;
--color-font: #657b83;
@ -477,7 +477,7 @@ body {
--color-font: #e1deda;
}
.dropdown__menu,
.theme--dropdown__menu,
.modal {
--color-bg: #323232;
}
@ -535,6 +535,10 @@ body {
color: var(--color-font) !important;
}
.bg-default {
background: var(--color-bg);
}
.bg-success,
.bg-http-method-POST {
background: var(--color-success) !important;

View File

@ -1,5 +1,6 @@
/* Fonts */
@font-size: 13px;
@font-size-xxs: round(@font-size * 0.6);
@font-size-xs: round(@font-size * 0.8);
@font-size-sm: round(@font-size * 0.9);
@font-size-md: round(@font-size * 1.0);
@ -48,6 +49,10 @@
@breakpoint-md: 880px;
@breakpoint-sm: 660px;
.txt-xxs {
font-size: @font-size-xxs !important;
}
.txt-xs {
font-size: @font-size-xs !important;
}

View File

@ -28,6 +28,7 @@
@import 'components/header-editor';
@import 'components/key-value-editor';
@import 'components/markdown-editor';
@import 'components/markdown-preview';
@import 'components/method-dropdown';
@import 'components/modal';
@import 'components/pane';

View File

@ -343,7 +343,7 @@ p.notice {
}
/* Make all font awesome icons the same width */
i.fa {
i.fa:not(.fa--skinny) {
min-width: 1.1rem;
text-align: center;
}