From 8a3a21bba1e6706295cc22e1b3a876a7a86cb705 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 21 Jul 2016 01:14:55 -0700 Subject: web: fix ValueEditor, clean up code --- web/src/css/codemirror.less | 10 +- web/src/css/flowdetail.less | 18 +- web/src/css/flowview.less | 5 +- web/src/js/components/ContentView.jsx | 14 +- web/src/js/components/FlowView.jsx | 4 +- .../js/components/FlowView/FlowEditorButton.jsx | 39 --- web/src/js/components/FlowView/Headers.jsx | 71 +++-- web/src/js/components/FlowView/Messages.jsx | 226 ++++++++-------- web/src/js/components/FlowView/ToggleEdit.jsx | 38 +++ web/src/js/components/Footer.jsx | 2 +- web/src/js/components/Header.jsx | 15 +- web/src/js/components/Header/MainMenu.jsx | 120 +++------ web/src/js/components/Header/OptionMenu.jsx | 27 +- web/src/js/components/MainView.jsx | 18 -- web/src/js/components/ProxyApp.jsx | 21 +- web/src/js/components/ValueEditor.jsx | 26 -- web/src/js/components/ValueEditor/EditorBase.jsx | 165 ------------ .../js/components/ValueEditor/ValidateEditor.jsx | 44 ++-- web/src/js/components/ValueEditor/ValueEditor.jsx | 168 ++++++++++++ web/src/js/ducks/flows.js | 30 +-- web/src/js/ducks/index.js | 2 +- web/src/js/ducks/settings.js | 9 +- web/src/js/ducks/ui.js | 292 --------------------- web/src/js/ducks/ui/flow.js | 97 +++++++ web/src/js/ducks/ui/header.js | 50 ++++ web/src/js/ducks/ui/index.js | 8 + web/src/js/ducks/ui/keyboard.js | 122 +++++++++ web/src/js/flow/utils.js | 7 - 28 files changed, 779 insertions(+), 869 deletions(-) delete mode 100644 web/src/js/components/FlowView/FlowEditorButton.jsx create mode 100644 web/src/js/components/FlowView/ToggleEdit.jsx delete mode 100755 web/src/js/components/ValueEditor.jsx delete mode 100755 web/src/js/components/ValueEditor/EditorBase.jsx create mode 100644 web/src/js/components/ValueEditor/ValueEditor.jsx delete mode 100644 web/src/js/ducks/ui.js create mode 100644 web/src/js/ducks/ui/flow.js create mode 100644 web/src/js/ducks/ui/header.js create mode 100644 web/src/js/ducks/ui/index.js create mode 100644 web/src/js/ducks/ui/keyboard.js diff --git a/web/src/css/codemirror.less b/web/src/css/codemirror.less index 6504db50..f88ea8b1 100644 --- a/web/src/css/codemirror.less +++ b/web/src/css/codemirror.less @@ -1,9 +1,7 @@ -.ReactCodeMirror { - border: 1px solid #ccc; -} - -.CodeMirror{ +.CodeMirror { + border: 1px solid #ccc; height: auto !important; - max-height: 1000px !important; + max-height: 2048px !important; } + @import (inline) "../../node_modules/codemirror/lib/codemirror.css"; diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less index 43078eff..35857729 100644 --- a/web/src/css/flowdetail.less +++ b/web/src/css/flowdetail.less @@ -26,14 +26,14 @@ word-break: break-all; max-height: 100px; overflow-y: auto; + + .inline-input.editable { + border-color: rgba(255,255,255,0.5); + } } .request-line { margin-bottom: 2px; } - /*.request .response-line, - .response .request-line { - opacity: 0.7; - }*/ hr { margin: 0 0 5px; @@ -42,8 +42,14 @@ } .inline-input { - margin: 0 -5px; - padding: 0 5px; + display: inline; + margin: 0 -3px; + padding: 0 3px; + + border: solid transparent 1px; + &.editable { + border-color: #ccc; + } &[contenteditable] { diff --git a/web/src/css/flowview.less b/web/src/css/flowview.less index 419739a4..1827db5c 100644 --- a/web/src/css/flowview.less +++ b/web/src/css/flowview.less @@ -13,15 +13,16 @@ } .edit-flow { + cursor: pointer; position: absolute; - right: 0px; + right: 0; top: 5px; height: 40px; width: 40px; border-radius: 20px; z-index: 10000; - background-color: white; + background-color: rgba(255, 255, 255, 0.7); border: solid 2px rgba(248, 145, 59, 0.7); text-align: center; diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx index 766de26f..f7eafc89 100644 --- a/web/src/js/components/ContentView.jsx +++ b/web/src/js/components/ContentView.jsx @@ -5,7 +5,7 @@ import * as ContentViews from './ContentView/ContentViews' import * as MetaViews from './ContentView/MetaViews' import ContentLoader from './ContentView/ContentLoader' import ViewSelector from './ContentView/ViewSelector' -import { setContentView, setDisplayLarge, setModifiedFlowContent } from '../ducks/ui' +import { setContentView, displayLarge, updateEdit } from '../ducks/ui/flow' import CodeEditor from './common/CodeEditor' ContentView.propTypes = { @@ -30,7 +30,7 @@ function ContentView(props) { } if (!displayLarge && ContentView.isContentTooLarge(message)) { - return setDisplayLarge(true)}/> + return } const View = ContentViews[contentView] @@ -80,13 +80,13 @@ function ContentView(props) { export default connect( state => ({ - contentView: state.ui.contentView, - displayLarge: state.ui.displayLarge, - isFlowEditorOpen : state.ui.isFlowEditorOpen + contentView: state.ui.flow.contentView, + displayLarge: state.ui.flow.displayLarge, + isFlowEditorOpen : !!state.ui.flow.modifiedFlow // FIXME }), { selectView: setContentView, - setDisplayLarge, - setModifiedFlowContent + displayLarge, + updateEdit, } )(ContentView) diff --git a/web/src/js/components/FlowView.jsx b/web/src/js/components/FlowView.jsx index 5ba472a5..a80dc040 100644 --- a/web/src/js/components/FlowView.jsx +++ b/web/src/js/components/FlowView.jsx @@ -7,7 +7,7 @@ import { Request, Response, ErrorView as Error } from './FlowView/Messages' import Details from './FlowView/Details' import Prompt from './Prompt' -import { setPrompt, selectTab } from '../ducks/ui' +import { selectTab } from '../ducks/ui/flow' export default class FlowView extends Component { @@ -90,9 +90,9 @@ export default class FlowView extends Component { export default connect( state => ({ promptOpen: state.ui.promptOpen, + tab: state.ui.flow.tab }), { - setPrompt, selectTab, } )(FlowView) diff --git a/web/src/js/components/FlowView/FlowEditorButton.jsx b/web/src/js/components/FlowView/FlowEditorButton.jsx deleted file mode 100644 index 3d0d1d16..00000000 --- a/web/src/js/components/FlowView/FlowEditorButton.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { PropTypes, Component } from 'react' -import { connect } from 'react-redux' - -import {closeFlowEditor} from '../../ducks/ui.js' -import {openFlowEditor} from '../../ducks/ui.js' - -FlowEditorButton.propTypes = { - isFlowEditorOpen: PropTypes.bool.isRequired, - content: PropTypes.string.isRequired, - onContentChange: PropTypes.func.isRequired -} - -function FlowEditorButton ({ isFlowEditorOpen, closeFlowEditor, openFlowEditor, onContentChange, content }) { - return ( -
- {isFlowEditorOpen ? - {onContentChange(content); closeFlowEditor()}}> - - - : - openFlowEditor()}> - - - } -
- ) -} - -export default connect( - state => ({ - isFlowEditorOpen: state.ui.isFlowEditorOpen, - content: state.ui.modifiedFlow.content - }), - { - closeFlowEditor, - openFlowEditor - - } -)(FlowEditorButton) diff --git a/web/src/js/components/FlowView/Headers.jsx b/web/src/js/components/FlowView/Headers.jsx index 880eeda1..706dd404 100644 --- a/web/src/js/components/FlowView/Headers.jsx +++ b/web/src/js/components/FlowView/Headers.jsx @@ -1,12 +1,21 @@ import React, { Component, PropTypes } from 'react' import ReactDOM from 'react-dom' -import ValueEditor from '../ValueEditor' -import { Key } from '../../utils.js' +import ValueEditor from '../ValueEditor/ValueEditor' +import { Key } from '../../utils' class HeaderEditor extends Component { + constructor(props) { + super(props) + this.onKeyDown = this.onKeyDown.bind(this) + } + render() { - return + let { onTab, ...props } = this.props + return } focus() { @@ -21,6 +30,7 @@ class HeaderEditor extends Component { this.props.onRemove(e) } break + case Key.ENTER: case Key.TAB: if (!e.shiftKey) { this.props.onTab(e) @@ -66,7 +76,12 @@ export default class Headers extends Component { onTab(row, col, e) { const headers = this.props.message.headers - if (row !== headers.length - 1 || col !== 1) { + if (col === 0) { + this._nextSel = `${row}-value` + return + } + if (row !== headers.length - 1) { + this._nextSel = `${row + 1}-key` return } @@ -96,33 +111,35 @@ export default class Headers extends Component { } render() { - const { message } = this.props + const { message, readonly } = this.props return ( - {message.headers.map((header, i) => ( - - - - - ))} + {message.headers.map((header, i) => ( + + + + + ))}
- this.onChange(i, 0, val)} - onRemove={event => this.onRemove(i, 0, event)} - onTab={event => this.onTab(i, 0, event)} - />: - - this.onChange(i, 1, val)} - onRemove={event => this.onRemove(i, 1, event)} - onTab={event => this.onTab(i, 1, event)} - /> -
+ this.onChange(i, 0, val)} + onRemove={event => this.onRemove(i, 0, event)} + onTab={event => this.onTab(i, 0, event)} + />: + + this.onChange(i, 1, val)} + onRemove={event => this.onRemove(i, 1, event)} + onTab={event => this.onTab(i, 1, event)} + /> +
) diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx index 2f03c712..133b2883 100644 --- a/web/src/js/components/FlowView/Messages.jsx +++ b/web/src/js/components/FlowView/Messages.jsx @@ -1,172 +1,180 @@ import React, { Component, PropTypes } from 'react' -import _ from 'lodash' - -import { RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion } from '../../flow/utils.js' -import { Key, formatTimeStamp } from '../../utils.js' -import ContentView from '../ContentView' -import ValueEditor from '../ValueEditor' -import Headers from './Headers' -import * as flowActions from '../../ducks/flows' -import FlowEditorButton from './FlowEditorButton' +import { connect } from 'react-redux' -class RequestLine extends Component { +import { RequestUtils, isValidHttpVersion, parseUrl } from '../../flow/utils.js' +import { formatTimeStamp } from '../../utils.js' +import ContentView from '../ContentView' +import ValidateEditor from '../ValueEditor/ValidateEditor' +import ValueEditor from '../ValueEditor/ValueEditor' - render() { - const { flow, updateFlow } = this.props +import Headers from './Headers' +import { startEdit, updateEdit } from '../../ducks/ui/flow' +import ToggleEdit from './ToggleEdit' - return ( -
+function RequestLine({ flow, readonly, updateFlow }) { + return ( +
+
updateFlow({ request: { method } })} - inline />   - updateFlow({ request: Object.assign({ path: '' }, parseUrl(url)) })} + readonly={readonly} + onDone={url => updateFlow({ request: {path: '', ...parseUrl(url)}})} isValid={url => !!parseUrl(url).host} - inline />   - updateFlow({ request: { http_version: parseHttpVersion(ver) } })} + readonly={readonly} + onDone={http_version => updateFlow({ request: { http_version } })} isValid={isValidHttpVersion} - inline />
- ) - } +
+ ) } -class ResponseLine extends Component { - - render() { - const { flow, updateFlow } = this.props +function ResponseLine({ flow, readonly, updateFlow }) { + return ( +
+ updateFlow({ response: { http_version: nextVer } })} + isValid={isValidHttpVersion} + /> +   + updateFlow({ response: { code: parseInt(code) } })} + isValid={code => /^\d+$/.test(code)} + /> +   + updateFlow({ response: { msg } })} + /> +
+ ) +} - return ( -
- updateFlow({ response: { http_version: parseHttpVersion(nextVer) } })} - isValid={isValidHttpVersion} - inline - /> -   - updateFlow({ response: { code: parseInt(code) } })} - isValid={code => /^\d+$/.test(code)} - inline - /> -   - updateFlow({ response: { msg } })} - inline - /> -
- ) +const Message = connect( + state => ({ + flow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]], + isEdit: !!state.ui.flow.modifiedFlow, + }), + { + updateFlow: updateEdit, } -} +) export class Request extends Component { - render() { - const { flow, updateFlow } = this.props - let onContentChange = content => flowActions.updateContent(this.props.flow, content, "request") + render() { + const { flow, isEdit, updateFlow } = this.props return (
- - + + updateFlow({ request: { headers } })} />
- +
) } + edit(k) { - switch (k) { - case 'm': - this.refs.requestLine.refs.method.focus() - break - case 'u': - this.refs.requestLine.refs.url.focus() - break - case 'v': - this.refs.requestLine.refs.httpVersion.focus() - break - case 'h': - this.refs.headers.edit() - break - default: - throw new Error(`Unimplemented: ${k}`) - } + throw "unimplemented" + /* + switch (k) { + case 'm': + this.refs.requestLine.refs.method.focus() + break + case 'u': + this.refs.requestLine.refs.url.focus() + break + case 'v': + this.refs.requestLine.refs.httpVersion.focus() + break + case 'h': + this.refs.headers.edit() + break + default: + throw new Error(`Unimplemented: ${k}`) + } + */ } + } -export class Response extends Component { +Request = Message(Request) +export class Response extends Component { render() { - const { flow, updateFlow } = this.props - let onContentChange = content => flowActions.updateContent(this.props.flow, content, "response") + const { flow, isEdit, updateFlow } = this.props return (
- - + + updateFlow({ response: { headers } })} />
- +
) } edit(k) { - switch (k) { - case 'c': - this.refs.responseLine.refs.status_code.focus() - break - case 'm': - this.refs.responseLine.refs.msg.focus() - break - case 'v': - this.refs.responseLine.refs.httpVersion.focus() - break - case 'h': - this.refs.headers.edit() - break - default: - throw new Error(`'Unimplemented: ${k}`) - } + throw "unimplemented" + /* + switch (k) { + case 'c': + this.refs.responseLine.refs.status_code.focus() + break + case 'm': + this.refs.responseLine.refs.msg.focus() + break + case 'v': + this.refs.responseLine.refs.httpVersion.focus() + break + case 'h': + this.refs.headers.edit() + break + default: + throw new Error(`'Unimplemented: ${k}`) + } + */ } } +Response = Message(Response) + + ErrorView.propTypes = { flow: PropTypes.object.isRequired, } diff --git a/web/src/js/components/FlowView/ToggleEdit.jsx b/web/src/js/components/FlowView/ToggleEdit.jsx new file mode 100644 index 00000000..0c8cbbd8 --- /dev/null +++ b/web/src/js/components/FlowView/ToggleEdit.jsx @@ -0,0 +1,38 @@ +import React, { PropTypes, Component } from 'react' +import { connect } from 'react-redux' + +import { startEdit, stopEdit } from '../../ducks/ui/flow' + +ToggleEdit.propTypes = { + isEdit: PropTypes.bool.isRequired, + flow: PropTypes.object.isRequired, + startEdit: PropTypes.func.isRequired, + stopEdit: PropTypes.func.isRequired, +} + +function ToggleEdit({ isEdit, startEdit, stopEdit, flow }) { + return ( + + ) +} + +export default connect( + state => ({ + isEdit: !!state.ui.flow.modifiedFlow, + flow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]] + }), + { + startEdit, + stopEdit, + } +)(ToggleEdit) diff --git a/web/src/js/components/Footer.jsx b/web/src/js/components/Footer.jsx index 82d6d8a1..2bda70e1 100644 --- a/web/src/js/components/Footer.jsx +++ b/web/src/js/components/Footer.jsx @@ -48,6 +48,6 @@ function Footer({ settings }) { export default connect( state => ({ - settings: state.settings.settings, + settings: state.settings, }) )(Footer) diff --git a/web/src/js/components/Header.jsx b/web/src/js/components/Header.jsx index 5de885ae..702786e6 100644 --- a/web/src/js/components/Header.jsx +++ b/web/src/js/components/Header.jsx @@ -6,7 +6,7 @@ import ViewMenu from './Header/ViewMenu' import OptionMenu from './Header/OptionMenu' import FileMenu from './Header/FileMenu' import FlowMenu from './Header/FlowMenu' -import {setActiveMenu} from '../ducks/ui.js' +import {setActiveMenu} from '../ducks/ui/header' class Header extends Component { static entries = [MainMenu, ViewMenu, OptionMenu] @@ -17,7 +17,7 @@ class Header extends Component { } render() { - const { query, selectedFlowId, activeMenu} = this.props + const { selectedFlowId, activeMenu} = this.props let entries = [...Header.entries] if(selectedFlowId) @@ -39,10 +39,7 @@ class Header extends Component { ))}
- +
) @@ -52,13 +49,9 @@ class Header extends Component { export default connect( state => ({ selectedFlowId: state.flows.selected[0], - activeMenu: state.ui.activeMenu, + activeMenu: state.ui.header.activeMenu, }), { setActiveMenu, - }, - null, - { - withRef: true, } )(Header) diff --git a/web/src/js/components/Header/MainMenu.jsx b/web/src/js/components/Header/MainMenu.jsx index 27a4be60..7236d31f 100644 --- a/web/src/js/components/Header/MainMenu.jsx +++ b/web/src/js/components/Header/MainMenu.jsx @@ -1,92 +1,50 @@ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' import FilterInput from './FilterInput' -import { Query } from '../../actions.js' import { update as updateSettings } from '../../ducks/settings' -import { updateQuery, setSelectedInput } from '../../ducks/ui' +import { updateFilter, updateHighlight } from '../../ducks/flowView' -class MainMenu extends Component { +MainMenu.title = "Start" - static title = 'Start' - static route = 'flows' - - static propTypes = { - query: PropTypes.object.isRequired, - settings: PropTypes.object.isRequired, - updateSettings: PropTypes.func.isRequired, - updateQuery: PropTypes.func.isRequired, - } - - constructor(props, context) { - super(props, context) - this.onSearchChange = this.onSearchChange.bind(this) - this.onHighlightChange = this.onHighlightChange.bind(this) - } - - componentWillReceiveProps(nextProps) { - if(this.refs[nextProps.selectedInput]) { - this.refs[nextProps.selectedInput].select() - } - this.props.setSelectedInput(undefined) - } - - onSearchChange(val) { - this.props.updateQuery({ [Query.SEARCH]: val }) - } - - onHighlightChange(val) { - this.props.updateQuery({ [Query.HIGHLIGHT]: val }) - } - - render() { - const { query, settings, updateSettings } = this.props - - return ( -
-
- - - updateSettings({ intercept })} - /> -
-
+export default function MainMenu() { + return ( +
+
+ + +
- ) - } +
+
+ ) } -export default connect( +const InterceptInput = connect( + state => ({ + value: state.settings.intercept || '', + placeholder: 'Intercept', + type: 'pause', + color: 'hsl(208, 56%, 53%)' + }), + { onChange: intercept => updateSettings({ intercept }) } +)(FilterInput); + +const FlowFilterInput = connect( + state => ({ + value: state.flowView.filter || '', + placeholder: 'Search', + type: 'search', + color: 'black' + }), + { onChange: updateFilter } +)(FilterInput); + +const HighlightInput = connect( state => ({ - settings: state.settings.settings, - selectedInput: state.ui.selectedInput + value: state.flowView.highlight || '', + placeholder: 'Highlight', + type: 'tag', + color: 'hsl(48, 100%, 50%)' }), - { - updateSettings, - updateQuery, - setSelectedInput - }, - null, - { - withRef: true, - } -)(MainMenu); + { onChange: updateHighlight } +)(FilterInput); diff --git a/web/src/js/components/Header/OptionMenu.jsx b/web/src/js/components/Header/OptionMenu.jsx index 4a487cc9..a338fed0 100644 --- a/web/src/js/components/Header/OptionMenu.jsx +++ b/web/src/js/components/Header/OptionMenu.jsx @@ -8,53 +8,52 @@ OptionMenu.title = 'Options' OptionMenu.propTypes = { settings: PropTypes.object.isRequired, - onSettingsChange: PropTypes.func.isRequired, + updateSettings: PropTypes.func.isRequired, } -function OptionMenu({ settings, onSettingsChange }) { - // @todo use settings.map +function OptionMenu({ settings, updateSettings }) { return (
onSettingsChange({ showhost: !settings.showhost })} + onToggle={() => updateSettings({ showhost: !settings.showhost })} /> onSettingsChange({ no_upstream_cert: !settings.no_upstream_cert })} + onToggle={() => updateSettings({ no_upstream_cert: !settings.no_upstream_cert })} /> onSettingsChange({ rawtcp: !settings.rawtcp })} + onToggle={() => updateSettings({ rawtcp: !settings.rawtcp })} /> onSettingsChange({ http2: !settings.http2 })} + onToggle={() => updateSettings({ http2: !settings.http2 })} /> onSettingsChange({ anticache: !settings.anticache })} + onToggle={() => updateSettings({ anticache: !settings.anticache })} /> onSettingsChange({ anticomp: !settings.anticomp })} + onToggle={() => updateSettings({ anticomp: !settings.anticomp })} /> onSettingsChange({ stickyauth: !settings.stickyauth ? txt : null })} + onToggleChanged={txt => updateSettings({ stickyauth: !settings.stickyauth ? txt : null })} /> onSettingsChange({ stickycookie: !settings.stickycookie ? txt : null })} + onToggleChanged={txt => updateSettings({ stickycookie: !settings.stickycookie ? txt : null })} /> onSettingsChange({ stream: !settings.stream ? txt : null })} + onToggleChanged={txt => updateSettings({ stream: !settings.stream ? txt : null })} />
@@ -64,9 +63,9 @@ function OptionMenu({ settings, onSettingsChange }) { export default connect( state => ({ - settings: state.settings.settings, + settings: state.settings, }), { - onSettingsChange: updateSettings, + updateSettings, } )(OptionMenu) diff --git a/web/src/js/components/MainView.jsx b/web/src/js/components/MainView.jsx index b0bbf70e..d7d1ebeb 100644 --- a/web/src/js/components/MainView.jsx +++ b/web/src/js/components/MainView.jsx @@ -1,6 +1,5 @@ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' -import { Query } from '../actions.js' import Splitter from './common/Splitter' import FlowTable from './FlowTable' import FlowView from './FlowView' @@ -14,19 +13,6 @@ class MainView extends Component { sort: PropTypes.object, } - /** - * @todo move to actions - * @todo replace with mapStateToProps - */ - componentWillReceiveProps(nextProps) { - if (nextProps.location.query[Query.SEARCH] !== nextProps.filter) { - this.props.updateFilter(nextProps.location.query[Query.SEARCH], false) - } - if (nextProps.location.query[Query.HIGHLIGHT] !== nextProps.highlight) { - this.props.updateHighlight(nextProps.location.query[Query.HIGHLIGHT], false) - } - } - render() { const { flows, selectedFlow, highlight } = this.props return ( @@ -66,9 +52,5 @@ export default connect( updateFilter, updateHighlight, updateFlow: flowsActions.update, - }, - undefined, - { - withRef: true } )(MainView) diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx index 2962fc2b..f8a6e262 100644 --- a/web/src/js/components/ProxyApp.jsx +++ b/web/src/js/components/ProxyApp.jsx @@ -1,14 +1,11 @@ import React, { Component, PropTypes } from 'react' -import ReactDOM from 'react-dom' -import _ from 'lodash' import { connect } from 'react-redux' import { init as appInit, destruct as appDestruct } from '../ducks/app' -import { onKeyDown } from '../ducks/ui' +import { onKeyDown } from '../ducks/ui/keyboard' import Header from './Header' import EventLog from './EventLog' import Footer from './Footer' -import { Key } from '../utils.js' class ProxyAppMain extends Component { @@ -27,6 +24,15 @@ class ProxyAppMain extends Component { } componentWillReceiveProps(nextProps) { + /* + FIXME: improve react-router -> redux integration. + if (nextProps.location.query[Query.SEARCH] !== nextProps.filter) { + this.props.updateFilter(nextProps.location.query[Query.SEARCH], false) + } + if (nextProps.location.query[Query.HIGHLIGHT] !== nextProps.highlight) { + this.props.updateHighlight(nextProps.location.query[Query.HIGHLIGHT], false) + } + */ if (nextProps.query === this.props.query && nextProps.selectedFlowId === this.props.selectedFlowId && nextProps.panel === this.props.panel) { return } @@ -35,13 +41,14 @@ class ProxyAppMain extends Component { } else { this.context.router.replace({ pathname: '/flows', query: nextProps.query }) } + } render() { const { showEventLog, location, children, query } = this.props return (
-
+
{React.cloneElement( children, { ref: 'view', location, query } @@ -58,8 +65,8 @@ class ProxyAppMain extends Component { export default connect( state => ({ showEventLog: state.eventLog.visible, - query: state.ui.query, - panel: state.ui.panel, + query: state.flowView.filter, + panel: state.ui.flow.tab, selectedFlowId: state.flows.selected[0] }), { diff --git a/web/src/js/components/ValueEditor.jsx b/web/src/js/components/ValueEditor.jsx deleted file mode 100755 index 5f1bf2dc..00000000 --- a/web/src/js/components/ValueEditor.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { Component, PropTypes } from 'react' -import ReactDOM from 'react-dom' -import ValidateEditor from './ValueEditor/ValidateEditor' - -export default class ValueEditor extends Component { - - static propTypes = { - content: PropTypes.string.isRequired, - onDone: PropTypes.func.isRequired, - inline: PropTypes.bool, - } - - render() { - var tag = this.props.inline ? 'span' : 'div' - return ( - - ) - } - - focus() { - ReactDOM.findDOMNode(this).focus(); - } -} diff --git a/web/src/js/components/ValueEditor/EditorBase.jsx b/web/src/js/components/ValueEditor/EditorBase.jsx deleted file mode 100755 index aa09dad5..00000000 --- a/web/src/js/components/ValueEditor/EditorBase.jsx +++ /dev/null @@ -1,165 +0,0 @@ -import React, { Component, PropTypes } from 'react' -import ReactDOM from 'react-dom' -import {Key} from '../../utils.js' - -export default class EditorBase extends Component { - - static propTypes = { - content: PropTypes.string.isRequired, - onDone: PropTypes.func.isRequired, - contentToHtml: PropTypes.func, - nodeToContent: PropTypes.func, - onStop: PropTypes.func, - submitOnEnter: PropTypes.bool, - className: PropTypes.string, - tag: PropTypes.string, - } - - static defaultProps = { - contentToHtml: content => _.escape(content), - nodeToContent: node => node.textContent, - submitOnEnter: true, - className: '', - tag: 'div', - onStop: _.noop, - onMouseDown: _.noop, - onBlur: _.noop, - onInput: _.noop, - } - - constructor(props) { - super(props) - this.state = {editable: false} - - this.onPaste = this.onPaste.bind(this) - this.onMouseDown = this.onMouseDown.bind(this) - this.onMouseUp = this.onMouseUp.bind(this) - this.onFocus = this.onFocus.bind(this) - this.onClick = this.onClick.bind(this) - this.stop = this.stop.bind(this) - this.onBlur = this.onBlur.bind(this) - this.reset = this.reset.bind(this) - this.onKeyDown = this.onKeyDown.bind(this) - this.onInput = this.onInput.bind(this) - } - - stop() { - // a stop would cause a blur as a side-effect. - // but a blur event must trigger a stop as well. - // to fix this, make stop = blur and do the actual stop in the onBlur handler. - ReactDOM.findDOMNode(this).blur() - this.props.onStop() - } - - render() { - return ( - - ) - } - - onPaste(e) { - e.preventDefault() - var content = e.clipboardData.getData('text/plain') - document.execCommand('insertHTML', false, content) - } - - onMouseDown(e) { - this._mouseDown = true - window.addEventListener('mouseup', this.onMouseUp) - this.props.onMouseDown(e) - } - - onMouseUp() { - if (this._mouseDown) { - this._mouseDown = false - window.removeEventListener('mouseup', this.onMouseUp) - } - } - - onClick(e) { - this.onMouseUp() - this.onFocus(e) - } - - onFocus(e) { - if (this._mouseDown || this._ignore_events || this.state.editable) { - return - } - - // contenteditable in FireFox is more or less broken. - // - we need to blur() and then focus(), otherwise the caret is not shown. - // - blur() + focus() == we need to save the caret position before - // Firefox sometimes just doesn't set a caret position => use caretPositionFromPoint - const sel = window.getSelection() - let range - if (sel.rangeCount > 0) { - range = sel.getRangeAt(0) - } else if (document.caretPositionFromPoint && e.clientX && e.clientY) { - const pos = document.caretPositionFromPoint(e.clientX, e.clientY) - range = document.createRange() - range.setStart(pos.offsetNode, pos.offset) - } else if (document.caretRangeFromPoint && e.clientX && e.clientY) { - range = document.caretRangeFromPoint(e.clientX, e.clientY) - } else { - range = document.createRange() - range.selectNodeContents(ReactDOM.findDOMNode(this)) - } - - this._ignore_events = true - this.setState({ editable: true }, () => { - const node = ReactDOM.findDOMNode(this) - node.blur() - node.focus() - this._ignore_events = false - }) - } - - onBlur(e) { - if (this._ignore_events) { - return - } - window.getSelection().removeAllRanges() //make sure that selection is cleared on blur - this.setState({ editable: false }) - this.props.onDone(this.props.nodeToContent(ReactDOM.findDOMNode(this))) - this.props.onBlur(e) - } - - reset() { - ReactDOM.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content) - } - - onKeyDown(e) { - e.stopPropagation() - switch (e.keyCode) { - case Key.ESC: - e.preventDefault() - this.reset() - this.stop() - break - case Key.ENTER: - if (this.props.submitOnEnter && !e.shiftKey) { - e.preventDefault() - this.stop() - } - break - default: - break - } - } - - onInput() { - this.props.onInput(this.props.nodeToContent(ReactDOM.findDOMNode(this))) - } -} diff --git a/web/src/js/components/ValueEditor/ValidateEditor.jsx b/web/src/js/components/ValueEditor/ValidateEditor.jsx index 2f362986..7415c1b8 100755 --- a/web/src/js/components/ValueEditor/ValidateEditor.jsx +++ b/web/src/js/components/ValueEditor/ValidateEditor.jsx @@ -1,57 +1,57 @@ import React, { Component, PropTypes } from 'react' -import ReactDOM from 'react-dom' -import EditorBase from './EditorBase' +import ValueEditor from './ValueEditor' +import classnames from 'classnames' + export default class ValidateEditor extends Component { static propTypes = { content: PropTypes.string.isRequired, + readonly: PropTypes.bool, onDone: PropTypes.func.isRequired, - onInput: PropTypes.func, - isValid: PropTypes.func, className: PropTypes.string, + isValid: PropTypes.func.isRequired, } constructor(props) { super(props) - this.state = { currentContent: props.content } + this.state = { valid: props.isValid(props.content) } this.onInput = this.onInput.bind(this) this.onDone = this.onDone.bind(this) } componentWillReceiveProps(nextProps) { - this.setState({ currentContent: nextProps.content }) + this.setState({ valid: nextProps.isValid(nextProps.content) }) } - onInput(currentContent) { - this.setState({ currentContent }) - this.props.onInput && this.props.onInput(currentContent) + onInput(content) { + this.setState({ valid: this.props.isValid(content) }) } onDone(content) { - if (this.props.isValid && !this.props.isValid(content)) { - this.refs.editor.reset() + if (!this.props.isValid(content)) { + this.editor.reset() content = this.props.content } this.props.onDone(content) } render() { - let className = this.props.className || '' - if (this.props.isValid) { - if (this.props.isValid(this.state.currentContent)) { - className += ' has-success' - } else { - className += ' has-warning' + let className = classnames( + this.props.className, + { + 'has-success': this.state.valid, + 'has-warning': !this.state.valid } - } + ) return ( - this.editor = e} /> ) } diff --git a/web/src/js/components/ValueEditor/ValueEditor.jsx b/web/src/js/components/ValueEditor/ValueEditor.jsx new file mode 100644 index 00000000..dd9c2cde --- /dev/null +++ b/web/src/js/components/ValueEditor/ValueEditor.jsx @@ -0,0 +1,168 @@ +import React, { Component, PropTypes } from 'react' +import _ from "lodash" +import classnames from 'classnames' + +import { Key } from '../../utils' + +export default class ValueEditor extends Component { + + static propTypes = { + content: PropTypes.string.isRequired, + readonly: PropTypes.bool, + onDone: PropTypes.func.isRequired, + className: PropTypes.string, + onInput: PropTypes.func, + onKeyDown: PropTypes.func, + } + + static defaultProps = { + onInput: () => {}, + onKeyDown: () => {}, + } + + constructor(props) { + super(props) + this.state = { editable: false } + + this.onPaste = this.onPaste.bind(this) + this.onMouseDown = this.onMouseDown.bind(this) + this.onMouseUp = this.onMouseUp.bind(this) + this.onFocus = this.onFocus.bind(this) + this.onClick = this.onClick.bind(this) + this.blur = this.blur.bind(this) + this.onBlur = this.onBlur.bind(this) + this.reset = this.reset.bind(this) + this.onKeyDown = this.onKeyDown.bind(this) + this.onInput = this.onInput.bind(this) + } + + blur() { + // a stop would cause a blur as a side-effect. + // but a blur event must trigger a stop as well. + // to fix this, make stop = blur and do the actual stop in the onBlur handler. + this.input.blur() + } + + reset() { + this.input.innerHTML = _.escape(this.props.content) + } + + render() { + let className = classnames( + 'inline-input', + { + 'readonly': this.props.readonly, + 'editable': !this.props.readonly + }, + this.props.className + ) + return ( +
this.input = input} + tabIndex={!this.props.readonly && "0"} + className={className} + contentEditable={this.state.editable || undefined} + onFocus={this.onFocus} + onMouseDown={this.onMouseDown} + onClick={this.onClick} + onBlur={this.onBlur} + onKeyDown={this.onKeyDown} + onInput={this.onInput} + onPaste={this.onPaste} + dangerouslySetInnerHTML={{ __html: _.escape(this.props.content) }} + >
+ ) + } + + onPaste(e) { + e.preventDefault() + var content = e.clipboardData.getData('text/plain') + document.execCommand('insertHTML', false, content) + } + + onMouseDown(e) { + this._mouseDown = true + window.addEventListener('mouseup', this.onMouseUp) + } + + onMouseUp() { + if (this._mouseDown) { + this._mouseDown = false + window.removeEventListener('mouseup', this.onMouseUp) + } + } + + onClick(e) { + this.onMouseUp() + this.onFocus(e) + } + + onFocus(e) { + if (this._mouseDown || this._ignore_events || this.state.editable || this.props.readonly) { + return + } + + // contenteditable in FireFox is more or less broken. + // - we need to blur() and then focus(), otherwise the caret is not shown. + // - blur() + focus() == we need to save the caret position before + // Firefox sometimes just doesn't set a caret position => use caretPositionFromPoint + const sel = window.getSelection() + let range + if (sel.rangeCount > 0) { + range = sel.getRangeAt(0) + } else if (document.caretPositionFromPoint && e.clientX && e.clientY) { + const pos = document.caretPositionFromPoint(e.clientX, e.clientY) + range = document.createRange() + range.setStart(pos.offsetNode, pos.offset) + } else if (document.caretRangeFromPoint && e.clientX && e.clientY) { + range = document.caretRangeFromPoint(e.clientX, e.clientY) + } else { + range = document.createRange() + range.selectNodeContents(this.input) + } + + this._ignore_events = true + this.setState({ editable: true }, () => { + this.input.blur() + this.input.focus() + this._ignore_events = false + range.selectNodeContents(this.input) + sel.removeAllRanges(); + sel.addRange(range); + }) + } + + onBlur(e) { + if (this._ignore_events || this.props.readonly) { + return + } + window.getSelection().removeAllRanges() //make sure that selection is cleared on blur + this.setState({ editable: false }) + this.props.onDone(this.input.textContent) + } + + + onKeyDown(e) { + e.stopPropagation() + switch (e.keyCode) { + case Key.ESC: + e.preventDefault() + this.reset() + this.blur() + break + case Key.ENTER: + if (!e.shiftKey) { + e.preventDefault() + this.blur() + } + break + default: + break + } + this.props.onKeyDown(e) + } + + onInput() { + this.props.onInput(this.input.textContent) + } +} diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js index ffb7ac87..f18e48e6 100644 --- a/web/src/js/ducks/flows.js +++ b/web/src/js/ducks/flows.js @@ -67,56 +67,49 @@ export default function reduce(state = defaultState, action) { * @public */ export function accept(flow) { - fetchApi(`/flows/${flow.id}/accept`, { method: 'POST' }) - return { type: REQUEST_ACTION } + return dispatch => fetchApi(`/flows/${flow.id}/accept`, { method: 'POST' }) } /** * @public */ export function acceptAll() { - fetchApi('/flows/accept', { method: 'POST' }) - return { type: REQUEST_ACTION } + return dispatch => fetchApi('/flows/accept', { method: 'POST' }) } /** * @public */ export function remove(flow) { - fetchApi(`/flows/${flow.id}`, { method: 'DELETE' }) - return { type: REQUEST_ACTION } + return dispatch => fetchApi(`/flows/${flow.id}`, { method: 'DELETE' }) } /** * @public */ export function duplicate(flow) { - fetchApi(`/flows/${flow.id}/duplicate`, { method: 'POST' }) - return { type: REQUEST_ACTION } + return dispatch => fetchApi(`/flows/${flow.id}/duplicate`, { method: 'POST' }) } /** * @public */ export function replay(flow) { - fetchApi(`/flows/${flow.id}/replay`, { method: 'POST' }) - return { type: REQUEST_ACTION } + return dispatch => fetchApi(`/flows/${flow.id}/replay`, { method: 'POST' }) } /** * @public */ export function revert(flow) { - fetchApi(`/flows/${flow.id}/revert`, { method: 'POST' }) - return { type: REQUEST_ACTION } + return dispatch => fetchApi(`/flows/${flow.id}/revert`, { method: 'POST' }) } /** * @public */ export function update(flow, data) { - fetchApi.put(`/flows/${flow.id}`, data) - return { type: REQUEST_ACTION } + return dispatch => fetchApi.put(`/flows/${flow.id}`, data) } export function updateContent(flow, file, type) { @@ -124,8 +117,7 @@ export function updateContent(flow, file, type) { if (typeof file !== File) file = new Blob([file], {type: 'plain/text'}) body.append('file', file) - fetchApi(`/flows/${flow.id}/${type}/content`, {method: 'post', body} ) - return { type: REQUEST_ACTION } + return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, {method: 'post', body} ) } @@ -133,8 +125,7 @@ export function updateContent(flow, file, type) { * @public */ export function clear() { - fetchApi('/clear', { method: 'POST' }) - return { type: REQUEST_ACTION } + return dispatch => fetchApi('/clear', { method: 'POST' }) } /** @@ -151,8 +142,7 @@ export function download() { export function upload(file) { const body = new FormData() body.append('file', file) - fetchApi('/flows/dump', { method: 'post', body }) - return { type: REQUEST_ACTION } + return dispatch => fetchApi('/flows/dump', { method: 'post', body }) } diff --git a/web/src/js/ducks/index.js b/web/src/js/ducks/index.js index 16193530..b90b24ff 100644 --- a/web/src/js/ducks/index.js +++ b/web/src/js/ducks/index.js @@ -4,7 +4,7 @@ import websocket from './websocket' import flows from './flows' import flowView from './flowView' import settings from './settings' -import ui from './ui' +import ui from './ui/index' import msgQueue from './msgQueue' export default combineReducers({ diff --git a/web/src/js/ducks/settings.js b/web/src/js/ducks/settings.js index 7ad97b87..6b21baec 100644 --- a/web/src/js/ducks/settings.js +++ b/web/src/js/ducks/settings.js @@ -11,22 +11,19 @@ export const REQUEST_UPDATE = 'REQUEST_UPDATE' export const UNKNOWN_CMD = 'SETTINGS_UNKNOWN_CMD' const defaultState = { - settings: {}, + } export default function reducer(state = defaultState, action) { switch (action.type) { case RECEIVE: - return { - ...state, - settings: action.settings, - } + return action.settings case UPDATE: return { ...state, - settings: { ...state.settings, ...action.settings }, + ...action.settings, } default: diff --git a/web/src/js/ducks/ui.js b/web/src/js/ducks/ui.js deleted file mode 100644 index ccd17eb6..00000000 --- a/web/src/js/ducks/ui.js +++ /dev/null @@ -1,292 +0,0 @@ -import { selectRelative as selectFlowRelative } from './flowView' -import { Key } from '../utils.js' -import * as flowsActions from './flows' - -export const SET_ACTIVE_MENU = 'UI_SET_ACTIVE_MENU' -export const SET_CONTENT_VIEW = 'UI_SET_CONTENT_VIEW' -export const SET_SELECTED_INPUT = 'UI_SET_SELECTED_INPUT' -export const UPDATE_QUERY = 'UI_UPDATE_QUERY' -export const SELECT_TAB = 'UI_SELECT_TAB' -export const SET_PROMPT = 'UI_SET_PROMPT' -export const SET_DISPLAY_LARGE = 'UI_SET_DISPLAY_LARGE' -export const OPEN_FLOW_EDITOR= 'UI_OPEN_FLOW_EDITOR' -export const CLOSE_FLOW_EDITOR = 'UI_CLOSE_FLOW_EDITOR' -export const SET_MODIFIED_FLOW_CONTENT = 'UI_SET_MODIFIED_FLOW' - -const defaultState = { - activeMenu: 'Start', - isFlowSelected: false, - selectedInput: null, - displayLarge: false, - promptOpen: false, - contentView: 'ViewAuto', - query: {}, - panel: 'request', - modifiedFlow: {headers: "", content: ""}, - isFlowEditorOpen: false -} - -export default function reducer(state = defaultState, action) { - switch (action.type) { - - case SET_ACTIVE_MENU: - return { - ...state, - activeMenu: action.activeMenu, - } - - case flowsActions.SELECT: - let s = {...state, isFlowEditorOpen: false} - if (action.flowIds.length && !state.isFlowSelected) { - return { - ...s, - displayLarge: false, - activeMenu: 'Flow', - isFlowSelected: true, - } - } - - if (!action.flowIds.length && state.isFlowSelected) { - let activeMenu = state.activeMenu - if (activeMenu == 'Flow') { - activeMenu = 'Start' - } - return { - ...s, - activeMenu, - isFlowSelected: false, - } - } - - return { - ...s, - displayLarge: false, - } - - case SET_CONTENT_VIEW: - return { - ...state, - contentView: action.contentView, - } - - case SET_SELECTED_INPUT: - return { - ...state, - selectedInput: action.input - } - - case UPDATE_QUERY: - return { - ...state, - query: { ...state.query, ...action.query } - } - - case SELECT_TAB: - return { - ...state, - isFlowEditorOpen: false, - panel: action.panel - } - - case SET_PROMPT: - return { - ...state, - promptOpen: action.open, - } - - case SET_DISPLAY_LARGE: - return { - ...state, - displayLarge: action.displayLarge, - } - case OPEN_FLOW_EDITOR: - return { - ...state, - isFlowEditorOpen: true - } - case CLOSE_FLOW_EDITOR: - return { - ...state, - isFlowEditorOpen: false - } - case SET_MODIFIED_FLOW_CONTENT: - return{ - ...state, - modifiedFlow: {...state.modifiedFlow, content: action.content} - } - default: - return state - } -} - -export function setActiveMenu(activeMenu) { - return { type: SET_ACTIVE_MENU, activeMenu } -} - -export function setContentView(contentView) { - return { type: SET_CONTENT_VIEW, contentView } -} - -export function setSelectedInput(input) { - return { type: SET_SELECTED_INPUT, input } -} - -export function updateQuery(query) { - return { type: UPDATE_QUERY, query } -} - -export function selectTab(panel) { - return { type: SELECT_TAB, panel } -} - -export function setPrompt(open) { - return { type: SET_PROMPT, open } -} - -export function setDisplayLarge(displayLarge) { - return { type: SET_DISPLAY_LARGE, displayLarge } -} - -export function openFlowEditor(){ - return { type: OPEN_FLOW_EDITOR } -} - -export function closeFlowEditor(){ - return { type: CLOSE_FLOW_EDITOR } -} - -export function setModifiedFlowContent(content) { - return { type: SET_MODIFIED_FLOW_CONTENT, content } -} - -export function onKeyDown(e) { - if (e.ctrlKey) { - return () => { - } - } - var key = e.keyCode - var shiftKey = e.shiftKey - e.preventDefault() - return (dispatch, getState) => { - - const flow = getState().flows.byId[getState().flows.selected[0]] - - switch (key) { - - case Key.I: - dispatch(setSelectedInput('intercept')) - break - - case Key.L: - dispatch(setSelectedInput('search')) - break - - case Key.H: - dispatch(setSelectedInput('highlight')) - break - - case Key.K: - case Key.UP: - dispatch(selectFlowRelative(-1)) - break - - case Key.J: - case Key.DOWN: - dispatch(selectFlowRelative(+1)) - break - - case Key.SPACE: - case Key.PAGE_DOWN: - dispatch(selectFlowRelative(+10)) - break - - case Key.PAGE_UP: - dispatch(selectFlowRelative(-10)) - break - - case Key.END: - dispatch(selectFlowRelative(+1e10)) - break - - case Key.HOME: - dispatch(selectFlowRelative(-1e10)) - break - - case Key.ESC: - dispatch(flowsActions.select(null)) - break - - case Key.LEFT: - { - let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']), - currentTab = getState().ui.panel, - nextTab = tabs[(tabs.indexOf(currentTab) - 1 + tabs.length) % tabs.length] - dispatch(selectTab(nextTab)) - break - } - - case Key.TAB: - case Key.RIGHT: - { - let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']), - currentTab = getState().ui.panel, - nextTab = tabs[(tabs.indexOf(currentTab) + 1) % tabs.length] - dispatch(selectTab(nextTab)) - break - } - - case Key.C: - if (shiftKey) { - dispatch(flowsActions.clear()) - } - break - - case Key.D: - { - if (!flow) { - return - } - if (shiftKey) { - dispatch(flowsActions.duplicate(flow)) - } else { - dispatch(flowsActions.remove(flow)) - } - break - } - - case Key.A: - { - if (shiftKey) { - dispatch(flowsActions.acceptAll()) - } else if (flow && flow.intercepted) { - dispatch(flowsActions.accept(flow)) - } - break - } - - case Key.R: - { - if (!shiftKey && flow) { - dispatch(flowsActions.replay(flow)) - } - break - } - - case Key.V: - { - if (!shiftKey && flow && flow.modified) { - dispatch(flowsActions.revert(flow)) - } - break - } - - case Key.E: - dispatch(setPrompt(true)) - break - - default: - return () => { - } - } - } -} diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js new file mode 100644 index 00000000..b1fe535f --- /dev/null +++ b/web/src/js/ducks/ui/flow.js @@ -0,0 +1,97 @@ +import * as flowsActions from '../flows' +import _ from 'lodash' + +export const SET_CONTENT_VIEW = 'UI_FLOWVIEW_SET_CONTENT_VIEW', + DISPLAY_LARGE = 'UI_FLOWVIEW_DISPLAY_LARGE', + SET_TAB = "UI_FLOWVIEW_SET_TAB", + START_EDIT = 'UI_FLOWVIEW_START_EDIT', + UPDATE_EDIT = 'UI_FLOWVIEW_UPDATE_EDIT', + STOP_EDIT = 'UI_FLOWVIEW_STOP_EDIT' + + +const defaultState = { + displayLarge: false, + modifiedFlow: false, + contentView: 'ViewAuto', + tab: 'request', +} + +export default function reducer(state = defaultState, action) { + switch (action.type) { + + case START_EDIT: + return { + ...state, + modifiedFlow: action.flow + } + + case UPDATE_EDIT: + return { + ...state, + modifiedFlow: _.merge({}, state.modifiedFlow, action.update) + } + + case STOP_EDIT: + return { + ...state, + modifiedFlow: false + } + + case flowsActions.SELECT: + return { + ...state, + modifiedFlow: false, + displayLarge: false, + } + + case SET_TAB: + return { + ...state, + tab: action.tab, + displayLarge: false, + } + + case SET_CONTENT_VIEW: + return { + ...state, + contentView: action.contentView, + } + + case DISPLAY_LARGE: + return { + ...state, + displayLarge: true, + } + default: + return state + } +} + +export function setContentView(contentView) { + return { type: SET_CONTENT_VIEW, contentView } +} + +export function displayLarge() { + return { type: DISPLAY_LARGE } +} + +export function selectTab(tab) { + return { type: SET_TAB, tab } +} + +export function startEdit(flow) { + return { type: START_EDIT, flow } +} + +export function updateEdit(update) { + return { type: UPDATE_EDIT, update } +} + +export function stopEdit(flow) { + return (dispatch) => { + dispatch(flowsActions.update(flow, flow)).then(() => { + dispatch(flowsActions.updateFlow(flow)) + dispatch({ type: STOP_EDIT }) + }) + } +} diff --git a/web/src/js/ducks/ui/header.js b/web/src/js/ducks/ui/header.js new file mode 100644 index 00000000..25dfe602 --- /dev/null +++ b/web/src/js/ducks/ui/header.js @@ -0,0 +1,50 @@ +import * as flowsActions from '../flows' + +export const SET_ACTIVE_MENU = 'UI_SET_ACTIVE_MENU' + + +const defaultState = { + activeMenu: 'Start', + isFlowSelected: false, +} + +export default function reducer(state = defaultState, action) { + switch (action.type) { + + case SET_ACTIVE_MENU: + return { + ...state, + activeMenu: action.activeMenu, + } + + case flowsActions.SELECT: + // First Select + if (action.flowIds.length && !state.isFlowSelected) { + return { + ...state, + activeMenu: 'Flow', + isFlowSelected: true, + } + } + + // Deselect + if (!action.flowIds.length && state.isFlowSelected) { + let activeMenu = state.activeMenu + if (activeMenu == 'Flow') { + activeMenu = 'Start' + } + return { + ...state, + activeMenu, + isFlowSelected: false, + } + } + return state + default: + return state + } +} + +export function setActiveMenu(activeMenu) { + return { type: SET_ACTIVE_MENU, activeMenu } +} diff --git a/web/src/js/ducks/ui/index.js b/web/src/js/ducks/ui/index.js new file mode 100644 index 00000000..f3c5f59e --- /dev/null +++ b/web/src/js/ducks/ui/index.js @@ -0,0 +1,8 @@ +import { combineReducers } from 'redux' +import flow from './flow' +import header from './header' + +export default combineReducers({ + flow, + header, +}) diff --git a/web/src/js/ducks/ui/keyboard.js b/web/src/js/ducks/ui/keyboard.js new file mode 100644 index 00000000..10c69853 --- /dev/null +++ b/web/src/js/ducks/ui/keyboard.js @@ -0,0 +1,122 @@ +import { Key } from '../../utils' +import { selectRelative as selectFlowRelative } from '../flowView' +import { selectTab } from './flow' +import * as flowsActions from '../flows' + + +export function onKeyDown(e) { + console.debug("onKeyDown", e) + if (e.ctrlKey) { + return () => { + } + } + var key = e.keyCode + var shiftKey = e.shiftKey + e.preventDefault() + return (dispatch, getState) => { + + const flow = getState().flows.byId[getState().flows.selected[0]] + + switch (key) { + case Key.K: + case Key.UP: + dispatch(selectFlowRelative(-1)) + break + + case Key.J: + case Key.DOWN: + dispatch(selectFlowRelative(+1)) + break + + case Key.SPACE: + case Key.PAGE_DOWN: + dispatch(selectFlowRelative(+10)) + break + + case Key.PAGE_UP: + dispatch(selectFlowRelative(-10)) + break + + case Key.END: + dispatch(selectFlowRelative(+1e10)) + break + + case Key.HOME: + dispatch(selectFlowRelative(-1e10)) + break + + case Key.ESC: + dispatch(flowsActions.select(null)) + break + + case Key.LEFT: + { + if(!flow) break + let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']), + currentTab = getState().ui.flow.tab, + nextTab = tabs[(tabs.indexOf(currentTab) - 1 + tabs.length) % tabs.length] + dispatch(selectTab(nextTab)) + break + } + + case Key.TAB: + case Key.RIGHT: + { + if(!flow) break + let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']), + currentTab = getState().ui.flow.tab, + nextTab = tabs[(tabs.indexOf(currentTab) + 1) % tabs.length] + dispatch(selectTab(nextTab)) + break + } + + case Key.C: + if (shiftKey) { + dispatch(flowsActions.clear()) + } + break + + case Key.D: + { + if (!flow) { + return + } + if (shiftKey) { + dispatch(flowsActions.duplicate(flow)) + } else { + dispatch(flowsActions.remove(flow)) + } + break + } + + case Key.A: + { + if (shiftKey) { + dispatch(flowsActions.acceptAll()) + } else if (flow && flow.intercepted) { + dispatch(flowsActions.accept(flow)) + } + break + } + + case Key.R: + { + if (!shiftKey && flow) { + dispatch(flowsActions.replay(flow)) + } + break + } + + case Key.V: + { + if (!shiftKey && flow && flow.modified) { + dispatch(flowsActions.revert(flow)) + } + break + } + + default: + return + } + } +} diff --git a/web/src/js/flow/utils.js b/web/src/js/flow/utils.js index 1f9f3d07..d24f984c 100644 --- a/web/src/js/flow/utils.js +++ b/web/src/js/flow/utils.js @@ -107,10 +107,3 @@ var isValidHttpVersion_regex = /^HTTP\/\d+(\.\d+)*$/i; export var isValidHttpVersion = function (httpVersion) { return isValidHttpVersion_regex.test(httpVersion); }; - -export var parseHttpVersion = function (httpVersion) { - httpVersion = httpVersion.replace("HTTP/", "").split("."); - return _.map(httpVersion, function (x) { - return parseInt(x); - }); -}; -- cgit v1.2.3