Clickable codemirror links (#81)

* Multiple recursive rendering

* Click links in respnose view
This commit is contained in:
Gregory Schier 2017-02-07 16:31:48 -08:00 committed by GitHub
parent 61a27b5c5f
commit 873a59cdbc
11 changed files with 126 additions and 29 deletions

View File

@ -19,6 +19,13 @@ class RequestUrlBar extends Component {
_urlChangeDebounceTimeout = null;
_lastPastedText = null;
_setDropdownRef = n => this._dropdown = n;
_handleMetaClickSend = e => {
e.preventDefault();
this._dropdown.show();
};
_handleFormSubmit = e => {
e.preventDefault();
e.stopPropagation();
@ -215,8 +222,9 @@ class RequestUrlBar extends Component {
let sendButton;
if (!cancelButton) {
sendButton = (
<Dropdown key="dropdown" className="tall" right={true}>
<Dropdown key="dropdown" className="tall" right={true} ref={this._setDropdownRef}>
<DropdownButton className="urlbar__send-btn"
onContextMenu={this._handleMetaClickSend}
onClick={this._handleClickSend}
type="submit">
{downloadPath ? "Download" : "Send"}

View File

@ -34,20 +34,19 @@ class Toast extends Component {
let notification;
try {
const queryParameters = [
{name: 'lastLaunch', value: stats.lastLaunch},
{name: 'firstLaunch', value: stats.created},
{name: 'launches', value: stats.launches},
{name: 'platform', value: constants.getAppPlatform()},
{name: 'version', value: constants.getAppVersion()},
{name: 'requests', value: (await db.count(models.request.type)) + ''},
{name: 'requestGroups', value: (await db.count(models.requestGroup.type)) + ''},
{name: 'environments', value: (await db.count(models.environment.type)) + ''},
{name: 'workspaces', value: (await db.count(models.workspace.type)) + ''},
];
const data = {
lastLaunch: stats.lastLaunch,
firstLaunch: stats.created,
launches: stats.launches,
platform: constants.getAppPlatform(),
version: constants.getAppVersion(),
requests: await db.count(models.request.type),
requestGroups: await db.count(models.requestGroup.type),
environments: await db.count(models.environment.type),
workspaces: await db.count(models.workspace.type),
};
const qs = querystring.buildFromParams(queryParameters);
notification = await fetch.get(`/notification?${qs}`);
notification = await fetch.post(`/notification`, data);
} catch (e) {
console.warn('[toast] Failed to fetch notifications', e);
}

View File

@ -38,6 +38,7 @@ import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/lint/lint';
import 'codemirror/addon/lint/json-lint';
import 'codemirror/addon/lint/lint.css';
import 'codemirror/addon/mode/overlay';
import 'codemirror/keymap/vim';
import 'codemirror/keymap/emacs';
import 'codemirror/keymap/sublime';
@ -130,26 +131,61 @@ class Editor extends Component {
const {value} = this.props;
// Add overlay to editor to make all links clickable
CodeMirror.defineMode('clickable', (config, parserConfig) => {
const baseMode = CodeMirror.getMode(config, parserConfig.baseMode || 'text/plain');
// Only add the click mode if we have links to click
if (!this.props.onClickLink) {
return baseMode;
}
const overlay = {
token: function (stream, state) {
// console.log('state', state);
if (stream.match(/^(https?:\/\/)?([\da-z.\-]+)\.([a-z.]{2,6})([\/\w .\-]*)*\/?/, true)) {
return 'clickable';
}
while (stream.next() != null && !stream.match("http", false)) {
// Do nothing
}
return null;
}
};
return CodeMirror.overlayMode(baseMode, overlay, true);
});
this.codeMirror = CodeMirror.fromTextArea(textarea, BASE_CODEMIRROR_OPTIONS);
this.codeMirror.on('change', misc.debounce(this._codemirrorValueChanged.bind(this)));
this.codeMirror.on('paste', misc.debounce(this._codemirrorValueChanged.bind(this)));
if (!this.codeMirror.getOption('indentWithTabs')) {
this.codeMirror.setOption('extraKeys', {
Tab: cm => {
var spaces = Array(this.codeMirror.getOption('indentUnit') + 1).join(' ');
const spaces = Array(this.codeMirror.getOption('indentUnit') + 1).join(' ');
cm.replaceSelection(spaces);
}
});
}
// Do this a bit later so we don't block the render process
setTimeout(() => {
this._codemirrorSetValue(value || '');
}, 50);
setTimeout(() => this._codemirrorSetValue(value || ''), 50);
this._codemirrorSetOptions();
};
_handleEditorClick = e => {
if (!this.props.onClickLink) {
return;
}
if (e.target.className.indexOf('cm-clickable') >= 0) {
this.props.onClickLink(e.target.innerHTML);
}
};
_isJSON (mode) {
if (!mode) {
return false;
@ -229,10 +265,15 @@ class Editor extends Component {
// Clone first so we can modify it
const readOnly = this.props.readOnly || false;
const normalizedMode = this.props.mode ? this.props.mode.split(';')[0] : 'text/plain';
let options = {
readOnly,
placeholder: this.props.placeholder || '',
mode: this.props.mode || 'text/plain',
mode: {
name: 'clickable',
baseMode: normalizedMode,
},
lineWrapping: this.props.lineWrapping,
keyMap: this.props.keyMap || 'default',
matchBrackets: !readOnly,
@ -240,7 +281,6 @@ class Editor extends Component {
};
// Strip of charset if there is one
options.mode = options.mode ? options.mode.split(';')[0] : 'text/plain';
Object.keys(options).map(key => {
this.codeMirror.setOption(key, options[key]);
});
@ -432,7 +472,7 @@ class Editor extends Component {
return (
<div className={classes} style={{fontSize: `${fontSize || 12}px`}}>
<div className="editor__container">
<div className="editor__container" onClick={this._handleEditorClick}>
<textarea
ref={this._handleInitTextarea}
defaultValue=" "
@ -449,6 +489,7 @@ class Editor extends Component {
Editor.propTypes = {
onChange: PropTypes.func,
onFocusChange: PropTypes.func,
onClickLink: PropTypes.func,
keyMap: PropTypes.string,
mode: PropTypes.string,
placeholder: PropTypes.string,

View File

@ -10,6 +10,8 @@ import {MOD_SYM} from '../../../common/constants';
class RequestActionsDropdown extends Component {
_setDropdownRef = n => this._dropdown = n;
_handleDuplicate = () => {
const {request, handleDuplicateRequest} = this.props;
handleDuplicateRequest(request);
@ -54,11 +56,15 @@ class RequestActionsDropdown extends Component {
trackEvent('Request', 'Delete', 'Action');
};
show () {
this._dropdown.show();
}
render () {
const {request, ...other} = this.props;
return (
<Dropdown {...other}>
<Dropdown ref={this._setDropdownRef} {...other}>
<DropdownButton>
<i className="fa fa-caret-down"></i>
</DropdownButton>

View File

@ -8,6 +8,8 @@ import {showModal} from '../modals';
import {trackEvent} from '../../../analytics/index';
class RequestGroupActionsDropdown extends Component {
_setDropdownRef = n => this._dropdown = n;
_handleRename = async () => {
const {requestGroup} = this.props;
@ -45,11 +47,15 @@ class RequestGroupActionsDropdown extends Component {
showModal(EnvironmentEditModal, this.props.requestGroup);
};
show () {
this._dropdown.show();
}
render () {
const {requestGroup, ...other} = this.props;
return (
<Dropdown {...other}>
<Dropdown ref={this._setDropdownRef} {...other}>
<DropdownButton>
<i className="fa fa-caret-down"></i>
</DropdownButton>

View File

@ -12,6 +12,7 @@ import SettingsTheme from '../settings/SettingsTheme';
import * as models from '../../../models/index';
import {getAppVersion, getAppName} from '../../../common/constants';
import {trackEvent} from '../../../analytics/index';
import * as session from '../../../sync/session';
export const TAB_INDEX_EXPORT = 1;
@ -64,13 +65,17 @@ class SettingsModal extends Component {
} = this.props;
const {currentTabIndex} = this.state;
const email = session.isLoggedIn() ? session.getEmail() : null;
return (
<Modal ref={m => this.modal = m} tall={true} {...this.props}>
<ModalHeader>
{getAppName()} Preferences
&nbsp;&nbsp;
<span className="faint txt-sm">v{getAppVersion()}</span>
<span className="faint txt-sm">
&nbsp;&nbsp;&nbsp;
v{getAppVersion()}
{email ? ` ${email}` : null}
</span>
</ModalHeader>
<ModalBody noScroll={true}>
<Tabs onSelect={i => this._handleTabSelect(i)} selectedIndex={currentTabIndex}>

View File

@ -9,12 +9,19 @@ import {trackEvent} from '../../../analytics/index';
class SidebarRequestGroupRow extends PureComponent {
state = {dragDirection: 0};
_setRequestGroupActionsDropdownRef = n => this._requestGroupActionsDropdown = n;
_handleCollapse = () => {
const {requestGroup, handleSetRequestGroupCollapsed, isCollapsed} = this.props;
handleSetRequestGroupCollapsed(requestGroup._id, !isCollapsed);
trackEvent('Folder', 'Toggle Visible', !isCollapsed ? 'Close' : 'Open')
};
_handleShowActions = e => {
e.preventDefault();
this._requestGroupActionsDropdown.show();
};
_nullFunction = () => null;
setDragDirection (dragDirection) {
@ -55,7 +62,7 @@ class SidebarRequestGroupRow extends PureComponent {
// NOTE: We only want the button draggable, not the whole container (ie. no children)
const button = connectDragSource(connectDropTarget(
<button onClick={this._handleCollapse}>
<button onClick={this._handleCollapse} onContextMenu={this._handleShowActions}>
<div className="sidebar__clickable">
<i className={'sidebar__item__icon fa ' + folderIconClass}></i>
<span>{requestGroup.name}</span>
@ -72,6 +79,7 @@ class SidebarRequestGroupRow extends PureComponent {
<div className="sidebar__actions">
<RequestGroupActionsDropdown
ref={this._setRequestGroupActionsDropdownRef}
handleCreateRequest={handleCreateRequest}
handleCreateRequestGroup={handleCreateRequestGroup}
handleDuplicateRequestGroup={handleDuplicateRequestGroup}

View File

@ -15,6 +15,13 @@ class SidebarRequestRow extends PureComponent {
isEditing: false,
};
_setRequestActionsDropdownRef = n => this._requestActionsDropdown = n;
_handleShowRequestActions = e => {
e.preventDefault();
this._requestActionsDropdown.show();
};
_handleEditStart = () => {
trackEvent('Request', 'Rename', 'In Place');
this.setState({isEditing: true});
@ -84,8 +91,9 @@ class SidebarRequestRow extends PureComponent {
} else {
node = (
<li className={classes}>
<div className={classnames('sidebar__item', 'sidebar__item--request', {'sidebar__item--active': isActive})}>
<button className="wide" onClick={this._handleRequestActivate}>
<div
className={classnames('sidebar__item', 'sidebar__item--request', {'sidebar__item--active': isActive})}>
<button className="wide" onClick={this._handleRequestActivate} onContextMenu={this._handleShowRequestActions}>
<div className="sidebar__clickable">
<MethodTag method={request.method}/>
<Editable value={request.name}
@ -95,6 +103,7 @@ class SidebarRequestRow extends PureComponent {
</button>
<div className="sidebar__actions">
<RequestActionsDropdown
ref={this._setRequestActionsDropdownRef}
handleDuplicateRequest={handleDuplicateRequest}
handleGenerateCode={handleGenerateCode}
right={true}

View File

@ -1,4 +1,5 @@
import React, {Component, PropTypes} from 'react';
import {shell} from 'electron';
import Editor from '../base/Editor';
import ResponseWebView from './ResponseWebview';
import ResponseRaw from './ResponseRaw';
@ -12,6 +13,10 @@ class ResponseViewer extends Component {
blockingBecauseTooLarge: false
};
_handleOpenLink (link) {
shell.openExternal(link);
}
_handleDismissBlocker () {
this.setState({blockingBecauseTooLarge: false});
}
@ -149,6 +154,7 @@ class ResponseViewer extends Component {
return (
<Editor
onClickLink={this._handleOpenLink}
value={body}
updateFilter={updateFilter}
filter={filter}

View File

@ -243,6 +243,15 @@
color: var(--color-font-danger);
}
span.cm-clickable {
text-decoration: underline;
cursor: pointer;
}
span.cm-clickable:hover {
text-decoration: underline;
}
.CodeMirror-activeline-background {
background: @hl-md;
}

View File

@ -100,7 +100,7 @@
"dependencies": {
"analytics-node": "^2.1.0",
"classnames": "^2.2.3",
"codemirror": "^5.22.0",
"codemirror": "^5.23.0",
"electron-context-menu": "^0.4.0",
"electron-squirrel-startup": "^1.0.0",
"hkdf": "0.0.2",