From 3056921fdab478ae5cb6561acda42db07749fee8 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 9 Jun 2017 14:42:19 -0700 Subject: [PATCH] Support multiline text in form data editors (#299) * Support multiline text in form data editors * Some tweaks around rendering and syntax * Embed markdown editor --- app/common/misc.js | 12 +- app/ui/components/base/file-input-button.js | 4 +- .../components/codemirror/one-line-editor.js | 18 ++ .../dropdowns/content-type-dropdown.js | 5 +- app/ui/components/editors/body/form-editor.js | 3 +- .../editors/body/url-encoded-editor.js | 1 + app/ui/components/key-value-editor/editor.js | 12 +- app/ui/components/key-value-editor/row.js | 210 +++++++++++++----- app/ui/components/markdown-editor.js | 19 +- app/ui/components/modals/code-prompt-modal.js | 180 +++++++++++++++ app/ui/components/modals/prompt-modal.js | 30 ++- .../components/modals/request-create-modal.js | 8 +- app/ui/components/settings/account.js | 12 +- app/ui/components/settings/general.js | 5 +- app/ui/components/settings/plugins.js | 2 +- app/ui/components/wrapper.js | 11 + app/ui/containers/app.js | 14 +- app/ui/css/components/forms.less | 10 +- app/ui/css/components/key-value-editor.less | 5 +- app/ui/css/components/markdown-editor.less | 14 +- app/ui/css/editor/general.less | 4 +- app/ui/css/layout/base.less | 4 + 22 files changed, 463 insertions(+), 120 deletions(-) create mode 100644 app/ui/components/modals/code-prompt-modal.js diff --git a/app/common/misc.js b/app/common/misc.js index bdc7b447c..726f08163 100644 --- a/app/common/misc.js +++ b/app/common/misc.js @@ -221,26 +221,26 @@ export function debounce (callback, millis = DEBOUNCE_MILLIS) { }, millis).bind(null, '__key__'); } -export function describeByteSize (bytes) { +export function describeByteSize (bytes, long) { bytes = Math.round(bytes * 10) / 10; let size; // NOTE: We multiply these by 2 so we don't end up with // values like 0 GB - let unit = 'B'; + let unit = long ? 'bytes' : 'B'; if (bytes < 1024 * 2) { size = bytes; - unit = 'B'; + unit = long ? 'bytes' : 'B'; } else if (bytes < 1024 * 1024 * 2) { size = bytes / 1024; - unit = 'KB'; + unit = long ? 'kilobytes' : 'KB'; } else if (bytes < 1024 * 1024 * 1024 * 2) { size = bytes / 1024 / 1024; - unit = 'MB'; + unit = long ? 'megabytes' : 'MB'; } else { size = bytes / 1024 / 1024 / 1024; - unit = 'GB'; + unit = long ? 'gigabytes' : 'GB'; } const rounded = (Math.round(size * 10) / 10); diff --git a/app/ui/components/base/file-input-button.js b/app/ui/components/base/file-input-button.js index 175e3ad1d..becf22b7e 100644 --- a/app/ui/components/base/file-input-button.js +++ b/app/ui/components/base/file-input-button.js @@ -36,13 +36,14 @@ class FileInputButton extends PureComponent { } render () { - const {showFileName, path, name, ...extraProps} = this.props; + const {showFileName, showFileIcon, path, name, ...extraProps} = this.props; const fileName = pathBasename(path); return ( ); @@ -56,6 +57,7 @@ FileInputButton.propTypes = { // Optional showFileName: PropTypes.bool, + showFileIcon: PropTypes.bool, name: PropTypes.string }; diff --git a/app/ui/components/codemirror/one-line-editor.js b/app/ui/components/codemirror/one-line-editor.js index 0103d8785..438e5b7c6 100644 --- a/app/ui/components/codemirror/one-line-editor.js +++ b/app/ui/components/codemirror/one-line-editor.js @@ -63,6 +63,24 @@ class OneLineEditor extends PureComponent { } } + getSelectionStart () { + if (this._editor) { + return this._editor.getSelectionStart(); + } else { + console.warn('Tried to get selection start of one-line-editor when '); + return this._input.value.length; + } + } + + getSelectionEnd () { + if (this._editor) { + return this._editor.getSelectionEnd(); + } else { + console.warn('Tried to get selection end of one-line-editor when '); + return this._input.value.length; + } + } + componentDidMount () { document.body.addEventListener('click', this._handleDocumentClick); } diff --git a/app/ui/components/dropdowns/content-type-dropdown.js b/app/ui/components/dropdowns/content-type-dropdown.js index 75c53e074..04041679f 100644 --- a/app/ui/components/dropdowns/content-type-dropdown.js +++ b/app/ui/components/dropdowns/content-type-dropdown.js @@ -23,9 +23,10 @@ class ContentTypeDropdown extends PureComponent { const hasFile = body.fileName && body.fileName.length; const isEmpty = !hasParams && !hasText && !hasFile; const isFile = body.mimeType === CONTENT_TYPE_FILE; - const isMultipart = body.mimeType === CONTENT_TYPE_FORM_DATA; + const isMultipartWithFiles = body.mimeType === CONTENT_TYPE_FORM_DATA && + body.params.find(p => p.type === 'file'); const isFormUrlEncoded = body.mimeType === CONTENT_TYPE_FORM_URLENCODED; - const isText = !isFile && !isMultipart; + const isText = !isFile && !isMultipartWithFiles; const willBeFile = mimeType === CONTENT_TYPE_FILE; const willBeMultipart = mimeType === CONTENT_TYPE_FORM_DATA; diff --git a/app/ui/components/editors/body/form-editor.js b/app/ui/components/editors/body/form-editor.js index 25c7c9992..c0c1544fe 100644 --- a/app/ui/components/editors/body/form-editor.js +++ b/app/ui/components/editors/body/form-editor.js @@ -42,6 +42,8 @@ class FormEditor extends PureComponent {
diff --git a/app/ui/components/editors/body/url-encoded-editor.js b/app/ui/components/editors/body/url-encoded-editor.js index 876a906f4..f925757dd 100644 --- a/app/ui/components/editors/body/url-encoded-editor.js +++ b/app/ui/components/editors/body/url-encoded-editor.js @@ -34,6 +34,7 @@ class UrlEncodedEditor extends PureComponent {
))} @@ -378,7 +380,8 @@ class Editor extends PureComponent { valuePlaceholder={`New ${valuePlaceholder}`} onFocusName={this._handleAddFromName} onFocusValue={this._handleAddFromValue} - multipart={multipart} + allowMultiline={allowMultiline} + allowFile={allowFile} pair={{name: '', value: ''}} /> : null } @@ -397,7 +400,8 @@ Editor.propTypes = { handleGetRenderContext: PropTypes.func, handleGetAutocompleteNameConstants: PropTypes.func, handleGetAutocompleteValueConstants: PropTypes.func, - multipart: PropTypes.bool, + allowFile: PropTypes.bool, + allowMultiline: PropTypes.bool, sortable: PropTypes.bool, maxPairs: PropTypes.number, namePlaceholder: PropTypes.string, diff --git a/app/ui/components/key-value-editor/row.js b/app/ui/components/key-value-editor/row.js index 8db09249f..562d4feec 100644 --- a/app/ui/components/key-value-editor/row.js +++ b/app/ui/components/key-value-editor/row.js @@ -1,14 +1,16 @@ // eslint-disable-next-line filenames/match-exported -import React, {PureComponent, PropTypes} from 'react'; +import React, {PropTypes, PureComponent} from 'react'; import ReactDOM from 'react-dom'; import autobind from 'autobind-decorator'; import {DragSource, DropTarget} from 'react-dnd'; import classnames from 'classnames'; import FileInputButton from '../base/file-input-button'; -import {Dropdown, DropdownItem, DropdownButton} from '../base/dropdown/index'; +import {Dropdown, DropdownButton, DropdownItem} from '../base/dropdown/index'; import PromptButton from '../base/prompt-button'; +import CodePromptModal from '../modals/code-prompt-modal'; import Button from '../base/button'; import OneLineEditor from '../codemirror/one-line-editor'; +import {showModal} from '../modals/index'; @autobind class KeyValueEditorRow extends PureComponent { @@ -25,16 +27,12 @@ class KeyValueEditorRow extends PureComponent { focusNameEnd () { if (this._nameInput) { this._nameInput.focusEnd(); - } else { - console.warn('Unable to focus non-existing nameInput'); } } focusValueEnd () { if (this._valueInput) { this._valueInput.focusEnd(); - } else { - console.warn('Unable to focus non-existing valueInput'); } } @@ -61,6 +59,24 @@ class KeyValueEditorRow extends PureComponent { this._sendChange({name}); } + _handleValuePaste (e) { + const value = e.clipboardData.getData('text/plain'); + if (value && value.includes('\n')) { + e.preventDefault(); + + // Insert the pasted text into the current selection. Unfortunately, this + // is the easiest way to do this. + const currentValue = this._valueInput.getValue(); + const prefix = currentValue.slice(0, this._valueInput.getSelectionStart()); + const suffix = currentValue.slice(this._valueInput.getSelectionEnd()); + const finalValue = `${prefix}${value}${suffix}`; + + // Update type and value + this._handleTypeChange({type: 'text', multiline: 'text/plain'}); + this._handleValueChange(finalValue); + } + } + _handleValueChange (value) { this._sendChange({value}); } @@ -69,8 +85,14 @@ class KeyValueEditorRow extends PureComponent { this._sendChange({fileName}); } - _handleTypeChange (type) { - this._sendChange({type}); + _handleTypeChange (def) { + // Remove newlines if converting to text + let value = this.props.pair.value || ''; + if (def.type === 'text' && !def.multiline && value.includes('\n')) { + value = value.replace(/\n/g, ''); + } + + this._sendChange({type: def.type, multiline: def.multiline, value}); } _handleDisableChange (disabled) { @@ -123,15 +145,130 @@ class KeyValueEditorRow extends PureComponent { } } + _handleEditMultiline () { + const {pair, handleRender, handleGetRenderContext} = this.props; + + showModal(CodePromptModal, { + submitName: 'Done', + title: `Edit ${pair.name}`, + defaultValue: pair.value, + onChange: this._handleValueChange, + enableRender: handleRender || handleGetRenderContext, + onModeChange: mode => { + this._handleTypeChange(Object.assign({}, pair, {multiline: mode})); + } + }); + } + + renderPairValue () { + const { + pair, + readOnly, + forceInput, + valueInputType, + valuePlaceholder, + handleRender, + handleGetRenderContext + } = this.props; + + if (pair.type === 'file') { + return ( + + ); + } else if (pair.type === 'text' && pair.multiline) { + const numWords = (pair.value || '').replace(/\s+/g, ' ').trim().split(' ').length; + return ( + + ); + } else { + return ( + + ); + } + } + + renderPairSelector () { + const { + hideButtons, + allowMultiline, + allowFile + } = this.props; + + const showDropdown = allowMultiline || allowFile; + + // Put a spacer in for dropdown if needed + if (hideButtons && showDropdown) { + return ( + + ); + } + + if (hideButtons) { + return null; + } + + if (showDropdown) { + return ( + + + + + + Text + + {allowMultiline && ( + + Text (Multi-line) + + )} + {allowFile && ( + + File + + )} + + ); + } else { + return null; + } + } + render () { const { pair, namePlaceholder, - valuePlaceholder, handleRender, handleGetRenderContext, - valueInputType, - multipart, sortable, noDropZone, hideButtons, @@ -185,53 +322,11 @@ class KeyValueEditorRow extends PureComponent { onKeyDown={this._handleKeyDown} />
-
- {pair.type === 'file' ? ( - - ) : ( - - )} +
+ {this.renderPairValue()}
- {multipart && ( - !hideButtons ? ( - - - - - - Text - - - File - - - ) : ( - - ) - )} + {this.renderPairSelector()} {!hideButtons ? ( + + + ); + } +} + +CodePromptModal.propTypes = { + // Required + editorFontSize: PropTypes.number.isRequired, + editorIndentSize: PropTypes.number.isRequired, + editorKeyMap: PropTypes.string.isRequired, + editorLineWrapping: PropTypes.bool.isRequired, + + // Optional + handleGetRenderContext: PropTypes.func, + handleRender: PropTypes.func +}; + +export default CodePromptModal; diff --git a/app/ui/components/modals/prompt-modal.js b/app/ui/components/modals/prompt-modal.js index a6791a862..c4f7d7df0 100644 --- a/app/ui/components/modals/prompt-modal.js +++ b/app/ui/components/modals/prompt-modal.js @@ -24,8 +24,7 @@ class PromptModal extends PureComponent { _done (rawValue) { const value = this.state.upperCase ? rawValue.toUpperCase() : rawValue; - this._onSubmitCallback && this._onSubmitCallback(value); - this._onSubmitCallback2 && this._onSubmitCallback2(value); + this._onComplete && this._onComplete(value); this.hide(); } @@ -75,22 +74,19 @@ class PromptModal extends PureComponent { selectText && this._input.select(); }, 100); - return new Promise(resolve => { - this._onSubmitCallback = resolve; - this._onSubmitCallback2 = onComplete; + this._onComplete = onComplete; - this.setState({ - headerName, - defaultValue, - submitName, - selectText, - placeholder, - upperCase, - hint, - inputType, - label, - hints - }); + this.setState({ + headerName, + defaultValue, + submitName, + selectText, + placeholder, + upperCase, + hint, + inputType, + label, + hints }); } diff --git a/app/ui/components/modals/request-create-modal.js b/app/ui/components/modals/request-create-modal.js index b0e7823b4..847718f79 100644 --- a/app/ui/components/modals/request-create-modal.js +++ b/app/ui/components/modals/request-create-modal.js @@ -46,7 +46,7 @@ class RequestCreateModal extends PureComponent { true, ); - this._onSubmitCallback(finalRequest); + this._onComplete(finalRequest); this.hide(); } @@ -75,7 +75,7 @@ class RequestCreateModal extends PureComponent { this.modal.hide(); } - show ({parentId}) { + show ({parentId, onComplete}) { this.modal.show(); this._input.value = 'My Request'; @@ -91,9 +91,7 @@ class RequestCreateModal extends PureComponent { this._input.select(); }, 200); - return new Promise(resolve => { - this._onSubmitCallback = resolve; - }); + this._onComplete = onComplete; } render () { diff --git a/app/ui/components/settings/account.js b/app/ui/components/settings/account.js index 0dc8f69f6..7b9ab5748 100644 --- a/app/ui/components/settings/account.js +++ b/app/ui/components/settings/account.js @@ -5,6 +5,7 @@ import * as sync from '../../../sync/index'; import Link from '../base/link'; import LoginModal from '../modals/login-modal'; import {hideAllModals, showModal} from '../modals/index'; +import PromptButton from '../base/prompt-button'; @autobind class Account extends PureComponent { @@ -45,7 +46,7 @@ class Account extends PureComponent {

- Or Login + Or Login

); @@ -63,15 +64,12 @@ class Account extends PureComponent { {session.getEmail()}


- + Manage Account - + ); } diff --git a/app/ui/components/settings/general.js b/app/ui/components/settings/general.js index 72765be74..0c40dee71 100644 --- a/app/ui/components/settings/general.js +++ b/app/ui/components/settings/general.js @@ -90,7 +90,10 @@ class General extends PureComponent {
-