diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/src/js/__tests__/components/Modal/OptionSpec.js | 99 | ||||
-rw-r--r-- | web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap | 235 | ||||
-rw-r--r-- | web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap | 64 | ||||
-rw-r--r-- | web/src/js/__tests__/ducks/optionsSpec.js | 32 | ||||
-rw-r--r-- | web/src/js/__tests__/ducks/tutils.js | 6 | ||||
-rw-r--r-- | web/src/js/__tests__/ducks/ui/keyboardSpec.js | 7 | ||||
-rw-r--r-- | web/src/js/__tests__/ducks/ui/optionEditorSpec.js | 32 | ||||
-rw-r--r-- | web/src/js/components/Modal/Option.jsx | 141 | ||||
-rw-r--r-- | web/src/js/components/Modal/OptionMaster.jsx | 119 | ||||
-rw-r--r-- | web/src/js/components/Modal/OptionModal.jsx | 57 | ||||
-rw-r--r-- | web/src/js/ducks/connection.js | 2 | ||||
-rw-r--r-- | web/src/js/ducks/options.js | 34 | ||||
-rw-r--r-- | web/src/js/ducks/settings.js | 1 | ||||
-rw-r--r-- | web/src/js/ducks/ui/index.js | 4 | ||||
-rw-r--r-- | web/src/js/ducks/ui/keyboard.js | 7 | ||||
-rw-r--r-- | web/src/js/ducks/ui/optionsEditor.js | 73 |
16 files changed, 678 insertions, 235 deletions
diff --git a/web/src/js/__tests__/components/Modal/OptionSpec.js b/web/src/js/__tests__/components/Modal/OptionSpec.js new file mode 100644 index 00000000..a275aee6 --- /dev/null +++ b/web/src/js/__tests__/components/Modal/OptionSpec.js @@ -0,0 +1,99 @@ +import React from 'react' +import renderer from 'react-test-renderer' +import { Options, ChoicesOption } from '../../../components/Modal/Option' + +describe('BooleanOption Component', () => { + let BooleanOption = Options['bool'], + onChangeFn = jest.fn(), + booleanOption = renderer.create( + <BooleanOption value={true} onChange={onChangeFn}/> + ), + tree = booleanOption.toJSON() + + it('should render correctly', () => { + expect(tree).toMatchSnapshot() + }) + + it('should handle onChange', () => { + let input = tree.children[0].children[0], + mockEvent = { target: { checked: true }} + input.props.onChange(mockEvent) + expect(onChangeFn).toBeCalledWith(mockEvent.target.checked) + }) +}) + +describe('StringOption Component', () => { + let StringOption = Options['str'], + onChangeFn = jest.fn(), + stringOption = renderer.create( + <StringOption value="foo" onChange={onChangeFn}/> + ), + tree = stringOption.toJSON() + + it('should render correctly', () => { + expect(tree).toMatchSnapshot() + }) + + it('should handle onChange', () => { + let mockEvent = { target: { value: 'bar' }} + tree.props.onChange(mockEvent) + expect(onChangeFn).toBeCalledWith(mockEvent.target.value) + }) + +}) + +describe('NumberOption Component', () => { + let NumberOption = Options['int'], + onChangeFn = jest.fn(), + numberOption = renderer.create( + <NumberOption value={1} onChange={onChangeFn}/> + ), + tree = numberOption.toJSON() + + it('should render correctly', () => { + expect(tree).toMatchSnapshot() + }) + + it('should handle onChange', () => { + let mockEvent = {target: { value: '2'}} + tree.props.onChange(mockEvent) + expect(onChangeFn).toBeCalledWith(2) + }) +}) + +describe('ChoiceOption Component', () => { + let onChangeFn = jest.fn(), + choiceOption = renderer.create( + <ChoicesOption value='a' choices={['a', 'b', 'c']} onChange={onChangeFn}/> + ), + tree = choiceOption.toJSON() + + it('should render correctly', () => { + expect(tree).toMatchSnapshot() + }) + + it('should handle onChange', () => { + let mockEvent = { target: {value: 'b'} } + tree.props.onChange(mockEvent) + expect(onChangeFn).toBeCalledWith(mockEvent.target.value) + }) +}) + +describe('StringOption Component', () => { + let onChangeFn = jest.fn(), + StringSequenceOption = Options['sequence of str'], + stringSequenceOption = renderer.create( + <StringSequenceOption value={['a', 'b']} onChange={onChangeFn}/> + ), + tree = stringSequenceOption.toJSON() + + it('should render correctly', () => { + expect(tree).toMatchSnapshot() + }) + + it('should handle onChange', () => { + let mockEvent = { target: {value: 'a\nb\nc\n'}} + tree.props.onChange(mockEvent) + expect(onChangeFn).toBeCalledWith(['a', 'b', 'c', '']) + }) +}) diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap index af587ae4..bfd855bd 100644 --- a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap +++ b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap @@ -47,94 +47,173 @@ exports[`Modal Component should render correctly 2`] = ` className="modal-body" > <div - className="menu-entry" + className="form-horizontal" > - <label> - booleanOption - <input - checked={false} - onChange={[Function]} - title="foo" - type="checkbox" - /> - </label> - </div> - <div - className="menu-entry" - > - <label - htmlFor="" + <div + className="form-group" > - choiceOption - <select - name="choiceOption" - onChange={[Function]} - selected="b" - title="foo" + <div + className="col-xs-6" > - <option - value="a" + <label + htmlFor="booleanOption" > - - a - - </option> - <option - value="b" + booleanOption + </label> + <div + className="help-block small" > - - b - - </option> - <option - value="c" + foo + </div> + </div> + <div + className="col-xs-6" + > + <div + className="" > - - c - - </option> - </select> - </label> - </div> - <div - className="menu-entry" - > - <label> - intOption - <input - onChange={[Function]} - onKeyDown={[Function]} - title="foo" - type="number" - value={1} - /> - </label> - </div> - <div - className="menu-entry" - > - <label> - strOption - <input - onChange={[Function]} - onKeyDown={[Function]} - title="foo" - type="text" - value="str content" - /> - </label> + <div + className="checkbox" + > + <label> + <input + checked={false} + name="booleanOption" + onChange={[Function]} + onKeyDown={[Function]} + type="checkbox" + /> + Enable + </label> + </div> + </div> + </div> + </div> + <div + className="form-group" + > + <div + className="col-xs-6" + > + <label + htmlFor="choiceOption" + > + choiceOption + </label> + <div + className="help-block small" + > + foo + </div> + </div> + <div + className="col-xs-6" + > + <div + className="" + > + <select + className="form-control" + name="choiceOption" + onChange={[Function]} + onKeyDown={[Function]} + selected="b" + > + <option + value="a" + > + a + </option> + <option + value="b" + > + b + </option> + <option + value="c" + > + c + </option> + </select> + </div> + </div> + </div> + <div + className="form-group" + > + <div + className="col-xs-6" + > + <label + htmlFor="intOption" + > + intOption + </label> + <div + className="help-block small" + > + foo + </div> + </div> + <div + className="col-xs-6" + > + <div + className="" + > + <input + className="form-control" + name="intOption" + onChange={[Function]} + onKeyDown={[Function]} + type="number" + value={1} + /> + </div> + </div> + </div> + <div + className="form-group" + > + <div + className="col-xs-6" + > + <label + htmlFor="strOption" + > + strOption + </label> + <div + className="help-block small" + > + foo + </div> + </div> + <div + className="col-xs-6" + > + <div + className="has-error" + > + <input + className="form-control" + name="strOption" + onChange={[Function]} + onKeyDown={[Function]} + type="text" + value="str content" + /> + </div> + <div + className="small text-danger" + /> + </div> + </div> </div> </div> <div className="modal-footer" - > - <button - className="btn btn-primary" - type="button" - > - Save Changes - </button> - </div> + /> </div> </div> </div> diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap new file mode 100644 index 00000000..514e0eb5 --- /dev/null +++ b/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BooleanOption Component should render correctly 1`] = ` +<div + className="checkbox" +> + <label> + <input + checked={true} + onChange={[Function]} + type="checkbox" + /> + Enable + </label> +</div> +`; + +exports[`ChoiceOption Component should render correctly 1`] = ` +<select + onChange={[Function]} + selected="a" +> + <option + value="a" + > + a + </option> + <option + value="b" + > + b + </option> + <option + value="c" + > + c + </option> +</select> +`; + +exports[`NumberOption Component should render correctly 1`] = ` +<input + onChange={[Function]} + type="number" + value={1} +/> +`; + +exports[`StringOption Component should render correctly 1`] = ` +<input + onChange={[Function]} + type="text" + value="foo" +/> +`; + +exports[`StringOption Component should render correctly 2`] = ` +<textarea + onChange={[Function]} + rows={2} + value="a +b" +/> +`; diff --git a/web/src/js/__tests__/ducks/optionsSpec.js b/web/src/js/__tests__/ducks/optionsSpec.js index 62019715..0925fcc1 100644 --- a/web/src/js/__tests__/ducks/optionsSpec.js +++ b/web/src/js/__tests__/ducks/optionsSpec.js @@ -1,6 +1,9 @@ -jest.mock('../../utils') - import reduceOptions, * as OptionsActions from '../../ducks/options' +import configureStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import * as OptionsEditorActions from '../../ducks/ui/optionsEditor' + +const mockStore = configureStore([ thunk ]) describe('option reducer', () => { it('should return initial state', () => { @@ -18,8 +21,31 @@ describe('option reducer', () => { }) }) +let store = mockStore() + describe('option actions', () => { + it('should be possible to update option', () => { - expect(reduceOptions(undefined, OptionsActions.update())).toEqual({}) + let mockResponse = { status: 200 }, + promise = Promise.resolve(mockResponse) + global.fetch = r => { return promise } + store.dispatch(OptionsActions.update('foo', 'bar')) + expect(store.getActions()).toEqual([ + { type: OptionsEditorActions.OPTION_UPDATE_START, option: 'foo', value: 'bar'} + ]) + store.clearActions() + }) +}) + +describe('sendUpdate', () => { + + it('should handle error', () => { + let mockResponse = { status: 400, text: p => Promise.resolve('error') }, + promise = Promise.resolve(mockResponse) + global.fetch = r => { return promise } + OptionsActions.pureSendUpdate('bar', 'error') + expect(store.getActions()).toEqual([ + { type: OptionsEditorActions.OPTION_UPDATE_SUCCESS, option: 'foo'} + ]) }) }) diff --git a/web/src/js/__tests__/ducks/tutils.js b/web/src/js/__tests__/ducks/tutils.js index a3e9c168..22240448 100644 --- a/web/src/js/__tests__/ducks/tutils.js +++ b/web/src/js/__tests__/ducks/tutils.js @@ -35,6 +35,12 @@ export function TStore(){ }, modal: { activeModal: undefined + }, + optionsEditor: { + booleanOption: { isUpdating: true, error: false }, + strOption: { error: true }, + intOption: {}, + choiceOption: {}, } }, settings: { diff --git a/web/src/js/__tests__/ducks/ui/keyboardSpec.js b/web/src/js/__tests__/ducks/ui/keyboardSpec.js index 500733cb..cf17943f 100644 --- a/web/src/js/__tests__/ducks/ui/keyboardSpec.js +++ b/web/src/js/__tests__/ducks/ui/keyboardSpec.js @@ -6,6 +6,7 @@ import reduceFlows from '../../../ducks/flows' import reduceUI from '../../../ducks/ui/index' import * as flowsActions from '../../../ducks/flows' import * as UIActions from '../../../ducks/ui/flow' +import * as modalActions from '../../../ducks/ui/modal' import configureStore from 'redux-mock-store' import thunk from 'redux-thunk' import { fetchApi } from '../../../utils' @@ -154,4 +155,10 @@ describe('onKeyDown', () => { expect(fetchApi).not.toBeCalled() }) + it('should close modal', () => { + store.getState().ui.modal.activeModal = true + store.dispatch(createKeyEvent(Key.ESC)) + expect(store.getActions()).toEqual([ {type: modalActions.HIDE_MODAL} ]) + }) + }) diff --git a/web/src/js/__tests__/ducks/ui/optionEditorSpec.js b/web/src/js/__tests__/ducks/ui/optionEditorSpec.js new file mode 100644 index 00000000..df9161a4 --- /dev/null +++ b/web/src/js/__tests__/ducks/ui/optionEditorSpec.js @@ -0,0 +1,32 @@ +import reduceOptionsEditor, * as optionsEditorActions from '../../../ducks/ui/optionsEditor' +import { HIDE_MODAL } from '../../../ducks/ui/modal' + +describe('optionsEditor reducer', () => { + + it('should return initial state', () => { + expect(reduceOptionsEditor(undefined, {})).toEqual({}) + }) + + let state = undefined + it('should handle option update start', () => { + state = reduceOptionsEditor(undefined, optionsEditorActions.startUpdate('foo', 'bar')) + expect(state).toEqual({ foo: {error: false, isUpdating: true, value: 'bar'}}) + }) + + it('should handle option update success', () => { + expect(reduceOptionsEditor(state, optionsEditorActions.updateSuccess('foo'))).toEqual({foo: undefined}) + }) + + it('should handle option update error', () => { + state = reduceOptionsEditor(state, optionsEditorActions.updateError('foo', 'errorMsg')) + expect(state).toEqual({ foo: {error: 'errorMsg', isUpdating: false, value: 'bar'}}) + // boolean type + state = reduceOptionsEditor(undefined, optionsEditorActions.startUpdate('foo', true)) + state = reduceOptionsEditor(state, optionsEditorActions.updateError('foo', 'errorMsg')) + expect(state).toEqual({ foo: {error: 'errorMsg', isUpdating: false, value: false}}) + }) + + it('should handle hide modal', () => { + expect(reduceOptionsEditor(undefined, {type: HIDE_MODAL})).toEqual({}) + }) +}) diff --git a/web/src/js/components/Modal/Option.jsx b/web/src/js/components/Modal/Option.jsx new file mode 100644 index 00000000..58b863d1 --- /dev/null +++ b/web/src/js/components/Modal/Option.jsx @@ -0,0 +1,141 @@ +import React, { Component } from "react" +import PropTypes from "prop-types" +import { connect } from "react-redux" +import { update as updateOptions } from "../../ducks/options" +import { Key } from "../../utils" +import classnames from 'classnames' + +const stopPropagation = e => { + if (e.keyCode !== Key.ESC) { + e.stopPropagation() + } +} + +BooleanOption.PropTypes = { + value: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, +} +function BooleanOption({ value, onChange, ...props }) { + return ( + <div className="checkbox"> + <label> + <input type="checkbox" + checked={value} + onChange={e => onChange(e.target.checked)} + {...props} + /> + Enable + </label> + </div> + ) +} + +StringOption.PropTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +} +function StringOption({ value, onChange, ...props }) { + return ( + <input type="text" + value={value || ""} + onChange={e => onChange(e.target.value)} + {...props} + /> + ) +} +function Optional(Component) { + return function ({ onChange, ...props }) { + return <Component + onChange={x => onChange(x ? x : null)} + {...props} + /> + } +} + +NumberOption.PropTypes = { + value: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired, +} +function NumberOption({ value, onChange, ...props }) { + return ( + <input type="number" + value={value} + onChange={(e) => onChange(parseInt(e.target.value))} + {...props} + /> + ) +} + +ChoicesOption.PropTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +} +export function ChoicesOption({ value, onChange, choices, ...props }) { + return ( + <select + onChange={(e) => onChange(e.target.value)} + selected={value} + {...props} + > + { choices.map( + choice => ( + <option key={choice} value={choice}>{choice}</option> + ) + )} + </select> + ) +} + +StringSequenceOption.PropTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +} +function StringSequenceOption({ value, onChange, ...props }) { + const height = Math.max(value.length, 1) + return <textarea + rows={height} + value={value.join('\n')} + onChange={e => onChange(e.target.value.split("\n"))} + {...props} + /> +} + +export const Options = { + "bool": BooleanOption, + "str": StringOption, + "int": NumberOption, + "optional str": Optional(StringOption), + "sequence of str": StringSequenceOption, +} + +function PureOption({ choices, type, value, onChange, name, error }) { + let Opt, props = {} + if (choices) { + Opt = ChoicesOption; + props.choices = choices + } else { + Opt = Options[type] + } + if (Opt !== BooleanOption) { + props.className = "form-control" + } + + return <div className={classnames({'has-error':error})}> + <Opt + name={name} + value={value} + onChange={onChange} + onKeyDown={stopPropagation} + {...props} + /> + </div> +} +export default connect( + (state, { name }) => ({ + ...state.options[name], + ...state.ui.optionsEditor[name] + }), + (dispatch, { name }) => ({ + onChange: value => dispatch(updateOptions(name, value)) + }) +)(PureOption) diff --git a/web/src/js/components/Modal/OptionMaster.jsx b/web/src/js/components/Modal/OptionMaster.jsx deleted file mode 100644 index c25dda72..00000000 --- a/web/src/js/components/Modal/OptionMaster.jsx +++ /dev/null @@ -1,119 +0,0 @@ -import PropTypes from 'prop-types' - -PureBooleanOption.PropTypes = { - value: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, -} - -function PureBooleanOption({ value, onChange, name, help}) { - return ( - <label> - { name } - <input type="checkbox" - checked={value} - onChange={onChange} - title={help} - /> - </label> - ) -} - -PureStringOption.PropTypes = { - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, -} - -function PureStringOption( { value, onChange, name, help }) { - let onKeyDown = (e) => {e.stopPropagation()} - return ( - <label> - { name } - <input type="text" - value={value} - onChange={onChange} - title={help} - onKeyDown={onKeyDown} - /> - </label> - ) -} - -PureNumberOption.PropTypes = { - value: PropTypes.number.isRequired, - onChange: PropTypes.func.isRequired, -} - -function PureNumberOption( {value, onChange, name, help }) { - let onKeyDown = (e) => {e.stopPropagation()} - return ( - <label> - { name } - <input type="number" - value={value} - onChange={onChange} - title={help} - onKeyDown={onKeyDown} - /> - </label> - ) -} - -PureChoicesOption.PropTypes = { - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, -} - -function PureChoicesOption( { value, onChange, name, help, choices }) { - return ( - <label htmlFor=""> - { name } - <select name={name} onChange={onChange} title={help} selected={value}> - { choices.map((choice, index) => ( - <option key={index} value={choice}> {choice} </option> - ))} - </select> - </label> - ) -} - -const OptionTypes = { - bool: PureBooleanOption, - str: PureStringOption, - int: PureNumberOption, - "optional str": PureStringOption, - "sequence of str": PureStringOption, -} - -export default function OptionMaster({option, name, updateOptions, ...props}) { - let WrappedComponent = null - if (option.choices) { - WrappedComponent = PureChoicesOption - } else { - WrappedComponent = OptionTypes[option.type] - } - - let onChange = (e) => { - switch (option.type) { - case 'bool' : - updateOptions({[name]: !option.value}) - break - case 'int': - updateOptions({[name]: parseInt(e.target.value)}) - break - default: - updateOptions({[name]: e.target.value}) - } - } - return ( - <div className="menu-entry"> - <WrappedComponent - children={props.children} - value={option.value} - onChange={onChange} - name={name} - help={option.help} - choices={option.choices} - /> - </div> - ) -} diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx index ef3a224a..5741ee8c 100644 --- a/web/src/js/components/Modal/OptionModal.jsx +++ b/web/src/js/components/Modal/OptionModal.jsx @@ -1,8 +1,22 @@ -import React, { Component } from 'react' -import { connect } from 'react-redux' -import * as modalAction from '../../ducks/ui/modal' -import { update as updateOptions } from '../../ducks/options' -import Option from './OptionMaster' +import React, { Component } from "react" +import { connect } from "react-redux" +import * as modalAction from "../../ducks/ui/modal" +import Option from "./Option" + +function PureOptionHelp({help}){ + return <div className="help-block small">{help}</div>; +} +const OptionHelp = connect((state, {name}) => ({ + help: state.options[name].help, +}))(PureOptionHelp); + +function PureOptionError({error}){ + if(!error) return null; + return <div className="small text-danger">{error}</div>; +} +const OptionError = connect((state, {name}) => ({ + error: state.ui.optionsEditor[name] && state.ui.optionsEditor[name].error +}))(PureOptionError); class PureOptionModal extends Component { @@ -28,23 +42,25 @@ class PureOptionModal extends Component { </div> <div className="modal-body"> - { - Object.keys(options).sort() - .map((key, index) => { - let option = options[key]; - return ( - <Option - key={index} - name={key} - updateOptions={updateOptions} - option={option} - />) - }) - } + <div className="form-horizontal"> + { + options.map(name => + <div key={name} className="form-group"> + <div className="col-xs-6"> + <label htmlFor={name}>{name}</label> + <OptionHelp name={name}/> + </div> + <div className="col-xs-6"> + <Option name={name}/> + <OptionError name={name}/> + </div> + </div> + ) + } + </div> </div> <div className="modal-footer"> - <button type="button" className="btn btn-primary">Save Changes</button> </div> </div> ) @@ -53,10 +69,9 @@ class PureOptionModal extends Component { export default connect( state => ({ - options: state.options + options: Object.keys(state.options).sort() }), { hideModal: modalAction.hideModal, - updateOptions: updateOptions, } )(PureOptionModal) diff --git a/web/src/js/ducks/connection.js b/web/src/js/ducks/connection.js index ffa2c309..151277fb 100644 --- a/web/src/js/ducks/connection.js +++ b/web/src/js/ducks/connection.js @@ -1,6 +1,6 @@ export const ConnectionState = { INIT: Symbol("init"), - FETCHING: Symbol("fetching"), // WebSocket is established, but still startFetching resources. + FETCHING: Symbol("fetching"), // WebSocket is established, but still fetching resources. ESTABLISHED: Symbol("established"), ERROR: Symbol("error"), OFFLINE: Symbol("offline"), // indicates that there is no live (websocket) backend. diff --git a/web/src/js/ducks/options.js b/web/src/js/ducks/options.js index 39c2f3fc..06144a3c 100644 --- a/web/src/js/ducks/options.js +++ b/web/src/js/ducks/options.js @@ -1,13 +1,12 @@ -import { fetchApi } from '../utils' +import { fetchApi } from "../utils" +import * as optionsEditorActions from "./ui/optionsEditor" +import _ from "lodash" -export const RECEIVE = 'OPTIONS_RECEIVE' -export const UPDATE = 'OPTIONS_UPDATE' +export const RECEIVE = 'OPTIONS_RECEIVE' +export const UPDATE = 'OPTIONS_UPDATE' export const REQUEST_UPDATE = 'REQUEST_UPDATE' -export const UNKNOWN_CMD = 'OPTIONS_UNKNOWN_CMD' -const defaultState = { - -} +const defaultState = {} export default function reducer(state = defaultState, action) { switch (action.type) { @@ -26,7 +25,22 @@ export default function reducer(state = defaultState, action) { } } -export function update(options) { - fetchApi.put('/options', options) - return { type: REQUEST_UPDATE } +export function pureSendUpdate (option, value, dispatch) { + fetchApi.put('/options', { [option]: value }).then(response => { + if (response.status === 200) { + dispatch(optionsEditorActions.updateSuccess(option)) + } else { + response.text().then(error => { + dispatch(optionsEditorActions.updateError(option, error)) + }) + } + }) +} +let sendUpdate = _.throttle(pureSendUpdate, 700, { leading: true, trailing: true }) + +export function update(option, value) { + return dispatch => { + dispatch(optionsEditorActions.startUpdate(option, value)) + sendUpdate(option, value, dispatch); + } } diff --git a/web/src/js/ducks/settings.js b/web/src/js/ducks/settings.js index a2e360de..38c36842 100644 --- a/web/src/js/ducks/settings.js +++ b/web/src/js/ducks/settings.js @@ -3,7 +3,6 @@ import { fetchApi } from '../utils' export const RECEIVE = 'SETTINGS_RECEIVE' export const UPDATE = 'SETTINGS_UPDATE' export const REQUEST_UPDATE = 'REQUEST_UPDATE' -export const UNKNOWN_CMD = 'SETTINGS_UNKNOWN_CMD' const defaultState = { diff --git a/web/src/js/ducks/ui/index.js b/web/src/js/ducks/ui/index.js index 741671b2..f5e6851f 100644 --- a/web/src/js/ducks/ui/index.js +++ b/web/src/js/ducks/ui/index.js @@ -2,10 +2,12 @@ import { combineReducers } from 'redux' import flow from './flow' import header from './header' import modal from './modal' +import optionsEditor from './optionsEditor' // TODO: Just move ducks/ui/* into ducks/? export default combineReducers({ flow, header, - modal + modal, + optionsEditor, }) diff --git a/web/src/js/ducks/ui/keyboard.js b/web/src/js/ducks/ui/keyboard.js index 0e3491fa..e3f8c33c 100644 --- a/web/src/js/ducks/ui/keyboard.js +++ b/web/src/js/ducks/ui/keyboard.js @@ -1,6 +1,7 @@ import { Key } from "../../utils" import { selectTab } from "./flow" import * as flowsActions from "../flows" +import * as modalActions from "./modal" export function onKeyDown(e) { @@ -46,7 +47,11 @@ export function onKeyDown(e) { break case Key.ESC: - dispatch(flowsActions.select(null)) + if(getState().ui.modal.activeModal){ + dispatch(modalActions.hideModal()) + } else { + dispatch(flowsActions.select(null)) + } break case Key.LEFT: { diff --git a/web/src/js/ducks/ui/optionsEditor.js b/web/src/js/ducks/ui/optionsEditor.js new file mode 100644 index 00000000..a8a8f69e --- /dev/null +++ b/web/src/js/ducks/ui/optionsEditor.js @@ -0,0 +1,73 @@ +import { HIDE_MODAL } from "./modal" + +export const OPTION_UPDATE_START = 'UI_OPTION_UPDATE_START' +export const OPTION_UPDATE_SUCCESS = 'UI_OPTION_UPDATE_SUCCESS' +export const OPTION_UPDATE_ERROR = 'UI_OPTION_UPDATE_ERROR' + +const defaultState = { + /* optionName -> {isUpdating, value (client-side), error} */ +} + +export default function reducer(state = defaultState, action) { + switch (action.type) { + case OPTION_UPDATE_START: + return { + ...state, + [action.option]: { + isUpdating: true, + value: action.value, + error: false, + } + } + + case OPTION_UPDATE_SUCCESS: + return { + ...state, + [action.option]: undefined + } + + case OPTION_UPDATE_ERROR: + let val = state[action.option].value; + if (typeof(val) === "boolean") { + // If a boolean option errs, reset it to its previous state to be less confusing. + // Example: Start mitmweb, check "add_upstream_certs_to_client_chain". + val = !val; + } + return { + ...state, + [action.option]: { + value: val, + isUpdating: false, + error: action.error + } + } + + case HIDE_MODAL: + return {} + + default: + return state + } +} + +export function startUpdate(option, value) { + return { + type: OPTION_UPDATE_START, + option, + value, + } +} +export function updateSuccess(option) { + return { + type: OPTION_UPDATE_SUCCESS, + option, + } +} + +export function updateError(option, error) { + return { + type: OPTION_UPDATE_ERROR, + option, + error, + } +} |