diff --git a/src/internal-packages/validation/lib/actions/index.js b/src/internal-packages/validation/lib/actions/index.js index 32ebc613542..84a4b4101e5 100644 --- a/src/internal-packages/validation/lib/actions/index.js +++ b/src/internal-packages/validation/lib/actions/index.js @@ -13,6 +13,7 @@ const ValidationActions = Reflux.createActions([ 'setRuleNullable', 'setValidationLevel', 'setValidationAction', + 'setValidatorDocument', 'switchView', 'saveChanges', 'cancelChanges' diff --git a/src/internal-packages/validation/lib/components/common/editable.jsx b/src/internal-packages/validation/lib/components/common/editable.jsx index 427b3e220d9..ca7a09e7da1 100644 --- a/src/internal-packages/validation/lib/components/common/editable.jsx +++ b/src/internal-packages/validation/lib/components/common/editable.jsx @@ -23,6 +23,17 @@ class Editable extends React.Component { ); } + if (this.props.editState === 'error') { + return ( +
+ +
+ ); + } return null; } @@ -39,7 +50,7 @@ class Editable extends React.Component { case 'error': if (errorMsg) { return name ? `${name} could not be updated: ${errorMsg}` : - `Error during update: ${errorMsg}`; + `Error: ${errorMsg}`; } return name ? `${name} could not be updated.` : 'An error occurred during the update.'; default: return ''; diff --git a/src/internal-packages/validation/lib/components/common/view-switcher.jsx b/src/internal-packages/validation/lib/components/common/view-switcher.jsx index 005cac798e5..7dd39414a3d 100644 --- a/src/internal-packages/validation/lib/components/common/view-switcher.jsx +++ b/src/internal-packages/validation/lib/components/common/view-switcher.jsx @@ -15,7 +15,12 @@ class ViewSwitcher extends React.Component { return _.map(this.props.buttonLabels, (label) => { const active = this.props.activeButton === label; return ( - ); @@ -44,13 +49,15 @@ ViewSwitcher.propTypes = { label: React.PropTypes.string, buttonLabels: React.PropTypes.arrayOf(React.PropTypes.string).isRequired, activeButton: React.PropTypes.string, - onClick: React.PropTypes.func + onClick: React.PropTypes.func, + disabled: React.PropTypes.bool }; ViewSwitcher.defaultProps = { label: '', activeButton: '', - onClick: () => {} + onClick: () => {}, + disabled: false }; ViewSwitcher.displayName = 'ViewSwitcher'; diff --git a/src/internal-packages/validation/lib/components/json-view.jsx b/src/internal-packages/validation/lib/components/json-view.jsx index 2caac435d3f..92d6b20a6f9 100644 --- a/src/internal-packages/validation/lib/components/json-view.jsx +++ b/src/internal-packages/validation/lib/components/json-view.jsx @@ -3,22 +3,47 @@ const ValidationActions = require('../actions'); const OptionSelector = require('./common/option-selector'); const Editable = require('./common/editable'); -const ReactBootstrap = require('react-bootstrap'); -const Grid = ReactBootstrap.Grid; -const Row = ReactBootstrap.Row; -const Col = ReactBootstrap.Col; +const {Grid, Row, Col, FormGroup, FormControl} = require('react-bootstrap'); // const debug = require('debug')('validation:json-view'); class JSONView extends React.Component { + constructor(props) { + super(props); + this.state = { + isValidJSON: true, + input: props.validatorDoc ? + JSON.stringify(props.validatorDoc.validator, null, 2) : '{}' + }; + } + + componentWillReceiveProps(newProps) { + this.setState({ + input: JSON.stringify(newProps.validatorDoc.validator, null, 2) + }); + } + + onInputChanged(evt) { + this.setState({ + input: evt.target.value + }); + } + + onBlur() { + const doc = this.validate(); + if (doc) { + ValidationActions.setValidatorDocument(doc); + } + } + /** * New value from the validation action dropdown chosen. * * @param {String} action the chosen action, one of `warn`, `error`. */ onActionSelect(action) { - ValidationActions.setValidationAction(action); + ValidationActions.setValidationAction(action, false); } /** @@ -27,7 +52,7 @@ class JSONView extends React.Component { * @param {String} level the chosen level, one of `off`, `moderate`, `strict` */ onLevelSelect(level) { - ValidationActions.setValidationLevel(level); + ValidationActions.setValidationLevel(level, false); } /** @@ -35,6 +60,9 @@ class JSONView extends React.Component { * Revert all changes to the server state. */ onCancel() { + this.setState({ + isValidJSON: true + }); ValidationActions.cancelChanges(); } @@ -46,19 +74,46 @@ class JSONView extends React.Component { ValidationActions.saveChanges(); } + validate() { + try { + const doc = { + validator: JSON.parse(this.state.input), + validationLevel: this.props.validationLevel, + validationAction: this.props.validationAction + }; + this.setState({ + isValidJSON: true + }); + return doc; + } catch (e) { + this.setState({ + isValidJSON: false + }); + return false; + } + } + /** * Render status row component. * * @returns {React.Component} The component. */ render() { + const editableProps = { + editState: this.props.editState, + childName: 'Validation', + onCancel: this.onCancel.bind(this), + onUpdate: this.onUpdate.bind(this) + }; + + if (!this.state.isValidJSON) { + editableProps.editState = 'error'; + editableProps.errorMessage = 'Input is not valid JSON.'; + delete editableProps.childName; + } + return ( - + @@ -85,11 +140,15 @@ class JSONView extends React.Component {
-
{JSON.stringify(this.props.validatorDoc, null, 2)}
+ + +
diff --git a/src/internal-packages/validation/lib/components/rule-builder.jsx b/src/internal-packages/validation/lib/components/rule-builder.jsx index 9fe06a9acb8..dd3f957120f 100644 --- a/src/internal-packages/validation/lib/components/rule-builder.jsx +++ b/src/internal-packages/validation/lib/components/rule-builder.jsx @@ -30,7 +30,7 @@ class RuleBuilder extends React.Component { * @param {String} action the chosen action, one of `warn`, `error`. */ onActionSelect(action) { - ValidationActions.setValidationAction(action); + ValidationActions.setValidationAction(action, true); } /** @@ -39,7 +39,7 @@ class RuleBuilder extends React.Component { * @param {String} level the chosen level, one of `off`, `moderate`, `strict` */ onLevelSelect(level) { - ValidationActions.setValidationLevel(level); + ValidationActions.setValidationLevel(level, true); } /** diff --git a/src/internal-packages/validation/lib/components/validation.jsx b/src/internal-packages/validation/lib/components/validation.jsx index a0cd5e3e8e9..3bfa7d00dc3 100644 --- a/src/internal-packages/validation/lib/components/validation.jsx +++ b/src/internal-packages/validation/lib/components/validation.jsx @@ -40,12 +40,14 @@ class Validation extends React.Component { render() { const view = this.props.viewMode === 'Rule Builder' ? ( - +
+ +
) : ( ); + + const activeButton = this.props.isExpressibleByRules ? + this.props.viewMode : 'JSON'; + return (
- - This is an example status row with a link. - {' '} - more info - {view} @@ -80,6 +82,7 @@ class Validation extends React.Component { Validation.propTypes = { editState: React.PropTypes.oneOf(['unmodified', 'modified', 'updating', 'error', 'success']).isRequired, viewMode: React.PropTypes.oneOf(['Rule Builder', 'JSON']).isRequired, + isExpressibleByRules: React.PropTypes.bool.isRequired, validationAction: React.PropTypes.oneOf(['warn', 'error']).isRequired, validatorDoc: React.PropTypes.object.isRequired, validationLevel: React.PropTypes.oneOf(['off', 'moderate', 'strict']).isRequired, @@ -89,6 +92,7 @@ Validation.propTypes = { Validation.defaultProps = { editState: 'unmodified', viewMode: 'Rule Builder', + isExpressibleByRules: true, validationAction: 'warn', validatorDoc: {}, validationLevel: 'off', diff --git a/src/internal-packages/validation/lib/stores/index.js b/src/internal-packages/validation/lib/stores/index.js index 92cfafa3508..07913332d0f 100644 --- a/src/internal-packages/validation/lib/stores/index.js +++ b/src/internal-packages/validation/lib/stores/index.js @@ -34,7 +34,6 @@ const ValidationStore = Reflux.createStore({ */ init() { this.lastFetchedValidatorDoc = {}; - NamespaceStore.listen((ns) => { if (ns && toNS(ns).collection) { ValidationActions.fetchValidationRules(); @@ -86,9 +85,6 @@ const ValidationStore = Reflux.createStore({ const rules = _.map(validator, (field) => { const fieldName = field[0]; const rule = field[1]; - debug('structure of field is:', field); - debug('rule is:', rule); - debug('fieldName is:', fieldName); let parameters; const result = helper.nullableOrValidator(fieldName, rule); @@ -241,7 +237,6 @@ const ValidationStore = Reflux.createStore({ fetchState: 'fetching' }); this._fetchFromServer((err, res) => { - debug('result from server', err, res); if (err || !_.has(res, 'options')) { // an error occured during fetch, e.g. missing permissions this.setState({ @@ -252,8 +247,8 @@ const ValidationStore = Reflux.createStore({ const result = this._deconstructValidatorDoc(res.options); // store result from server - this.lastFetchedValidatorDoc = this._constructValidatorDoc(result); - const validatorDoc = _.clone(this.lastFetchedValidatorDoc); + const validatorDoc = res.options; + this.lastFetchedValidatorDoc = _.clone(validatorDoc); if (!result) { // the validatorDoc has an unexpected format. @@ -277,6 +272,7 @@ const ValidationStore = Reflux.createStore({ this.setState({ fetchState: 'success', isExpressibleByRules: false, + viewMode: 'JSON', validatorDoc: validatorDoc, validationLevel: result.level, validationAction: result.action, @@ -289,6 +285,7 @@ const ValidationStore = Reflux.createStore({ this.setState({ fetchState: 'success', isExpressibleByRules: true, + viewMode: 'Rule Builder', validatorDoc: validatorDoc, validationRules: result.rules, validationLevel: result.level, @@ -359,16 +356,54 @@ const ValidationStore = Reflux.createStore({ this._updateState({rules: rules}); }, - setValidationAction(validationAction) { - if (_.includes(['warn', 'error'], validationAction)) { + setValidatorDocument(validatorDoc) { + const result = this._deconstructValidatorDoc(validatorDoc); + + this.setState({ + validatorDoc: validatorDoc, + validationRules: result.rules || [], + validationLevel: result.level, + validationAction: result.action, + isExpressibleByRules: _.isArray(result.rules), + editState: _.isEqual(this.lastFetchedValidatorDoc, validatorDoc) ? + 'unmodified' : 'modified' + }); + }, + + setValidationAction(validationAction, setByRuleBuilder) { + if (!_.includes(['warn', 'error'], validationAction)) { + return; + } + if (setByRuleBuilder) { this._updateState({action: validationAction}); + return; } + const validatorDoc = _.clone(this.state.validatorDoc); + validatorDoc.validationAction = validationAction; + this.setState({ + validatorDoc: validatorDoc, + validationAction: validationAction, + editState: _.isEqual(this.lastFetchedValidatorDoc, validatorDoc) ? + 'unmodified' : 'modified' + }); }, - setValidationLevel(validationLevel) { - if (_.includes(['off', 'moderate', 'strict'], validationLevel)) { + setValidationLevel(validationLevel, setByRuleBuilder) { + if (!_.includes(['off', 'moderate', 'strict'], validationLevel)) { + return; + } + if (setByRuleBuilder) { this._updateState({level: validationLevel}); + return; } + const validatorDoc = _.clone(this.state.validatorDoc); + validatorDoc.validationLevel = validationLevel; + this.setState({ + validatorDoc: validatorDoc, + validationLevel: validationLevel, + editState: _.isEqual(this.lastFetchedValidatorDoc, validatorDoc) ? + 'unmodified' : 'modified' + }); }, /** @@ -384,9 +419,16 @@ const ValidationStore = Reflux.createStore({ } }, - cancelChanges() { - const params = this._deconstructValidatorDoc(this.lastFetchedValidatorDoc); - this._updateState(params); + cancelChanges(setByRuleBuilder) { + if (setByRuleBuilder) { + const params = this._deconstructValidatorDoc(this.lastFetchedValidatorDoc); + this._updateState(params); + return; + } + this.setState({ + editState: 'unmodified', + validatorDoc: _.clone(this.lastFetchedValidatorDoc) + }); }, saveChanges() { diff --git a/src/internal-packages/validation/styles/index.less b/src/internal-packages/validation/styles/index.less index d8e96a4ee1c..6056a1148e5 100644 --- a/src/internal-packages/validation/styles/index.less +++ b/src/internal-packages/validation/styles/index.less @@ -6,3 +6,4 @@ @import './json-view.less'; @import './option-selector.less'; @import './editable.less'; +@import './json-input.less'; diff --git a/src/internal-packages/validation/styles/json-input.less b/src/internal-packages/validation/styles/json-input.less new file mode 100644 index 00000000000..1b785145088 --- /dev/null +++ b/src/internal-packages/validation/styles/json-input.less @@ -0,0 +1,8 @@ +.json-input { + &-textarea { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 13px; + height: 250px !important; + width: 100%; + } +} diff --git a/src/internal-packages/validation/styles/validation.less b/src/internal-packages/validation/styles/validation.less index 4f7ea1454e3..091177ddae4 100644 --- a/src/internal-packages/validation/styles/validation.less +++ b/src/internal-packages/validation/styles/validation.less @@ -1,2 +1,6 @@ .validation { + &-rule-builder-wrapper { + // TODO work-around so that the dropdowns can be shown, otherwise they get cut off. + padding-bottom: 150px; + } } diff --git a/test/validation.store.test.js b/test/validation.store.test.js index d522960a094..bab5dff478f 100644 --- a/test/validation.store.test.js +++ b/test/validation.store.test.js @@ -564,27 +564,6 @@ describe('ValidationStore', function() { }, 10); }); - it('updates editState to `unmodified` when rules are changed back to the original rules', function(done) { - mockFetchFromServer(null, mockValidatorDoc); - - const spy = sinon.spy(); - unsubscribe = ValidationStore.listen(spy); - - ValidationStore.fetchValidationRules(); - - setTimeout(() => { - const id = ValidationStore.state.validationRules[0].id; - ValidationStore.setRuleField(id, 'foobar'); - ValidationStore.setRuleField(id, 'number'); - - expect(spy.callCount).to.be.equal(4); - - const editState = spy.thirdCall.args[0].editState; - expect(editState).to.have.equal('unmodified'); - done(); - }, 10); - }); - it('addValidationRule() adds a new empty rule', function(done) { mockFetchFromServer(null, mockValidatorDoc);