Repository: couchdb-fauxton Updated Branches: refs/heads/master 41967231e -> efc2a1e23
Move all React modals to use ReactBootstrap.Modal Benefits are less custom code and all modals standardized by default, so behaviour like clicking outside the modal or clicking escape will close it. These are configurable, but the âout the boxâ modals will be all the same. Affects: - Confirmation modal (e.g. full page doc editor, âare you sure you want to delete this doc?â) - Upload file modal - String Edit modal - Clone Doc modal Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/efc2a1e2 Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/efc2a1e2 Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/efc2a1e2 Branch: refs/heads/master Commit: efc2a1e23772861a5c7afedc5a4010e4c1575e02 Parents: 4196723 Author: Ben Keen <ben.k...@gmail.com> Authored: Mon Dec 14 10:39:09 2015 -0800 Committer: Ben Keen <ben.k...@gmail.com> Committed: Tue Jan 5 08:54:33 2016 -0800 ---------------------------------------------------------------------- .../components/react-components.react.jsx | 76 ++++++-------- .../tests/stringEditModalSpec.react.jsx | 47 +++++---- .../documents/doc-editor/components.react.jsx | 104 +++++-------------- .../tests/doc-editor.componentsSpec.react.jsx | 27 +++-- .../tests/nightwatch/createsDocument.js | 2 +- app/addons/fauxton/components.react.jsx | 33 +++--- assets/less/fauxton.less | 5 + 7 files changed, 127 insertions(+), 167 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/efc2a1e2/app/addons/components/react-components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/components/react-components.react.jsx b/app/addons/components/react-components.react.jsx index 7154070..c501396 100644 --- a/app/addons/components/react-components.react.jsx +++ b/app/addons/components/react-components.react.jsx @@ -25,6 +25,7 @@ function (app, FauxtonAPI, React, Stores, FauxtonComponents, ace, beautifyHelper var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; var componentStore = Stores.componentStore; + var Modal = ReactBootstrap.Modal; var ToggleHeaderButton = React.createClass({ @@ -355,7 +356,7 @@ function (app, FauxtonAPI, React, Stores, FauxtonComponents, ace, beautifyHelper stringEditModalVisible: false, stringEditIconVisible: false, stringEditIconStyle: {}, - stringEditModalDefaultString: '' + stringEditModalValue: '' }; }, @@ -560,8 +561,10 @@ function (app, FauxtonAPI, React, Stores, FauxtonComponents, ace, beautifyHelper } string = string.substring(1, lastChar); - this.setState({ stringEditModalVisible: true }); - this.refs.stringEditModal.setValue(string); + this.setState({ + stringEditModalVisible: true, + stringEditModalValue: string + }); }, saveStringEditModal: function (newString) { @@ -648,6 +651,7 @@ function (app, FauxtonAPI, React, Stores, FauxtonComponents, ace, beautifyHelper <StringEditModal ref="stringEditModal" visible={this.state.stringEditModalVisible} + value={this.state.stringEditModalValue} onSave={this.saveStringEditModal} onClose={this.closeStringEditModal} /> </div> @@ -660,6 +664,7 @@ function (app, FauxtonAPI, React, Stores, FauxtonComponents, ace, beautifyHelper var StringEditModal = React.createClass({ propTypes: { + value: React.PropTypes.string.isRequired, visible: React.PropTypes.bool.isRequired, onClose: React.PropTypes.func.isRequired, onSave: React.PropTypes.func.isRequired @@ -673,46 +678,34 @@ function (app, FauxtonAPI, React, Stores, FauxtonComponents, ace, beautifyHelper }; }, - componentDidUpdate: function () { - var params = (this.props.visible) ? { show: true, backdrop: 'static', keyboard: true } : 'hide'; - $(React.findDOMNode(this)).modal(params); + componentDidMount: function () { + if (!this.props.visible) { + return; + } + this.initEditor(this.props.value); + }, - $(React.findDOMNode(this)).on('shown.bs.modal', function () { - this.editor.focus(); + componentDidUpdate: function (prevProps) { + if (!this.props.visible) { + return; + } + var val = ''; + if (!prevProps.visible && this.props.visible) { + val = JSON.parse('"' + this.props.value + '"'); // this ensures newlines are converted + } - // re-opening the modal to edit a second string doesn't update the content. This forces the editor to redraw - // to show the latest content each time it opens - this.editor.resize(); - this.editor.renderer.updateFull(); - }.bind(this)); + this.initEditor(val); }, - // ensure that if the user clicks ESC to close the window, the store gets wind of it - componentDidMount: function () { - $(React.findDOMNode(this)).on('hidden.bs.modal', function () { - this.props.onClose(); - }.bind(this)); - + initEditor: function (val) { this.editor = ace.edit(React.findDOMNode(this.refs.stringEditor)); - - // suppresses an Ace editor error - this.editor.$blockScrolling = Infinity; - + this.editor.$blockScrolling = Infinity; // suppresses an Ace editor error this.editor.setShowPrintMargin(false); this.editor.setOption('highlightActiveLine', true); this.editor.setTheme('ace/theme/idle_fingers'); - }, - - setValue: function (val) { - // we do the JSON.parse so the string editor modal shows newlines - val = JSON.parse('"' + val + '"'); //returns an object, expects a JSON string this.editor.setValue(val, -1); }, - componentWillUnmount: function () { - $(React.findDOMNode(this)).off('hidden.bs.modal shown.bs.modal'); - }, - closeModal: function () { this.props.onClose(); }, @@ -723,22 +716,21 @@ function (app, FauxtonAPI, React, Stores, FauxtonComponents, ace, beautifyHelper render: function () { return ( - <div className="modal hide fade string-editor-modal" tabIndex="-1"> - <div className="modal-header"> - <button type="button" className="close" onClick={this.closeModal} aria-hidden="true">×</button> - <h3>Edit text <span id="string-edit-header"></span></h3> - </div> - <div className="modal-body"> + <Modal dialogClassName="string-editor-modal" show={this.props.visible} onHide={this.closeModal}> + <Modal.Header closeButton={true}> + <Modal.Title>Edit text <span id="string-edit-header"></span></Modal.Title> + </Modal.Header> + <Modal.Body> <div id="modal-error" className="hide alert alert-error"/> <div id="string-editor-wrapper"><div ref="stringEditor" className="doc-code"></div></div> - </div> - <div className="modal-footer"> + </Modal.Body> + <Modal.Footer> <button className="cancel-button btn" onClick={this.closeModal}><i className="icon fonticon-circle-x"></i> Cancel</button> <button id="string-edit-save-btn" onClick={this.save} className="btn btn-success save"> <i className="fonticon-circle-check"></i> Save </button> - </div> - </div> + </Modal.Footer> + </Modal> ); } }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/efc2a1e2/app/addons/components/tests/stringEditModalSpec.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/components/tests/stringEditModalSpec.react.jsx b/app/addons/components/tests/stringEditModalSpec.react.jsx index d67e356..bc1fd71 100644 --- a/app/addons/components/tests/stringEditModalSpec.react.jsx +++ b/app/addons/components/tests/stringEditModalSpec.react.jsx @@ -12,15 +12,18 @@ define([ 'api', 'addons/components/react-components.react', + 'libs/react-bootstrap', 'testUtils', 'react' -], function (FauxtonAPI, ReactComponents, utils, React) { +], function (FauxtonAPI, ReactComponents, ReactBootstrap, utils, React) { var assert = utils.assert; var TestUtils = React.addons.TestUtils; + var Modal = ReactBootstrap.Modal; + describe('String Edit Modal', function () { - var container, modalEl; + var container, el; var stub = function () { }; beforeEach(function () { @@ -34,52 +37,58 @@ define([ describe('event methods called', function () { it('onClose called by top (x)', function () { var spy = sinon.spy(); - modalEl = TestUtils.renderIntoDocument( + el = TestUtils.renderIntoDocument( <ReactComponents.StringEditModal visible={true} onClose={spy} onSave={stub} />, container ); - TestUtils.Simulate.click($(modalEl.getDOMNode()).find('.close')[0]); + var modal = TestUtils.findRenderedComponentWithType(el, Modal); + var modalEl = React.findDOMNode(modal.refs.modal); + + TestUtils.Simulate.click($(modalEl).find('.close')[0]); assert.ok(spy.calledOnce); }); it('onClose called by cancel button', function () { var spy = sinon.spy(); - modalEl = TestUtils.renderIntoDocument( + el = TestUtils.renderIntoDocument( <ReactComponents.StringEditModal visible={true} onClose={spy} onSave={stub} />, container ); - TestUtils.Simulate.click($(modalEl.getDOMNode()).find('.cancel-button')[0]); + var modal = TestUtils.findRenderedComponentWithType(el, Modal); + var modalEl = React.findDOMNode(modal.refs.modal); + TestUtils.Simulate.click($(modalEl).find('.cancel-button')[0]); assert.ok(spy.calledOnce); }); }); - describe('setValue / onSave', function () { - it('setValue ensures same content returns on saving', function () { + describe('onSave', function () { + it('ensures same content returns on saving', function () { + var string = "a string!"; var spy = sinon.spy(); - modalEl = TestUtils.renderIntoDocument( - <ReactComponents.StringEditModal visible={true} onClose={stub} onSave={spy} />, + el = TestUtils.renderIntoDocument( + <ReactComponents.StringEditModal visible={true} onClose={stub} onSave={spy} value={string} />, container ); + var modal = TestUtils.findRenderedComponentWithType(el, Modal); + var modalEl = React.findDOMNode(modal.refs.modal); - var string = "a string!"; - - modalEl.setValue(string); - TestUtils.Simulate.click($(modalEl.getDOMNode()).find('#string-edit-save-btn')[0]); + TestUtils.Simulate.click($(modalEl).find('#string-edit-save-btn')[0]); assert.ok(spy.calledOnce); assert.ok(spy.calledWith(string)); }); it('replaces "\\n" with actual newlines', function () { var spy = sinon.spy(); - modalEl = TestUtils.renderIntoDocument( - <ReactComponents.StringEditModal visible={true} onSave={spy} />, + var string = 'I am a string\\nwith\\nlinebreaks\\nin\\nit'; + el = TestUtils.renderIntoDocument( + <ReactComponents.StringEditModal visible={true} onSave={spy} value={string} />, container ); - var string = 'I am a string\\nwith\\nlinebreaks\\nin\\nit'; + var modal = TestUtils.findRenderedComponentWithType(el, Modal); + var modalEl = React.findDOMNode(modal.refs.modal); - modalEl.setValue(string); - TestUtils.Simulate.click($(modalEl.getDOMNode()).find('#string-edit-save-btn')[0]); + TestUtils.Simulate.click($(modalEl).find('#string-edit-save-btn')[0]); assert.ok(spy.calledOnce); }); }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/efc2a1e2/app/addons/documents/doc-editor/components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/documents/doc-editor/components.react.jsx b/app/addons/documents/doc-editor/components.react.jsx index 376ef30..462915a 100644 --- a/app/addons/documents/doc-editor/components.react.jsx +++ b/app/addons/documents/doc-editor/components.react.jsx @@ -6,11 +6,12 @@ define([ 'addons/documents/doc-editor/stores', 'addons/fauxton/components.react', 'addons/components/react-components.react', + 'libs/react-bootstrap', 'helpers' -], function (FauxtonAPI, app, React, Actions, Stores, FauxtonComponents, GeneralComponents, Helpers) { +], function (FauxtonAPI, app, React, Actions, Stores, FauxtonComponents, GeneralComponents, ReactBootstrap, Helpers) { var store = Stores.docEditorStore; - + var Modal = ReactBootstrap.Modal; var DocEditorController = React.createClass({ @@ -281,34 +282,12 @@ define([ }; }, - componentDidUpdate: function () { - var params = (this.props.visible) ? { show: true, backdrop: 'static', keyboard: true } : 'hide'; - $(React.findDOMNode(this)).modal(params); - }, - - // ensure that if the user clicks ESC to close the window, the store gets wind of it - componentDidMount: function () { - $(React.findDOMNode(this)).on('hidden.bs.modal', function () { - Actions.hideUploadModal(); - }); - }, - - componentWillUnmount: function () { - $(React.findDOMNode(this)).off('hidden.bs.modal'); - }, - closeModal: function () { if (this.state.inProgress) { Actions.cancelUpload(); } Actions.hideUploadModal(); - - // timeout needed to only clear it once the animate close effect is done, otherwise the user sees it reset - // as it closes, which looks bad - setTimeout(function () { - Actions.resetUploadModal(); - React.findDOMNode(this.refs.uploadForm).reset(); - }.bind(this), 1000); + Actions.resetUploadModal(); }, upload: function () { @@ -330,14 +309,12 @@ define([ } return ( - <div className="modal hide fade upload-file-modal" tabIndex="-1" data-js-visible={this.props.visible}> - <div className="modal-header"> - <button type="button" className="close" onClick={this.closeModal} aria-hidden="true">×</button> - <h3>Upload Attachment</h3> - </div> - <div className="modal-body"> + <Modal dialogClassName="upload-file-modal" show={this.props.visible} onHide={this.closeModal}> + <Modal.Header closeButton={true}> + <Modal.Title>Upload Attachment</Modal.Title> + </Modal.Header> + <Modal.Body> <div className={errorClasses}>{this.state.errorMessage}</div> - <div> <form ref="uploadForm" className="form" method="post"> <p> @@ -348,21 +325,20 @@ define([ <br /> </form> - <div ref="loadIndicator" className={loadIndicatorClasses}> + <div className={loadIndicatorClasses}> <div className="bar" style={{ width: this.state.loadPercentage + '%'}}></div> </div> </div> - - </div> - <div className="modal-footer"> - <button href="#" data-bypass="true" className="btn" onClick={this.closeModal}> + </Modal.Body> + <Modal.Footer> + <button href="#" data-bypass="true" className="btn" onClick={this.closeModal}> <i className="icon fonticon-cancel-circled"></i> Cancel </button> <button href="#" id="upload-btn" data-bypass="true" className="btn btn-success save" onClick={this.upload}> <i className="icon fonticon-ok-circled"></i> Upload </button> - </div> - </div> + </Modal.Footer> + </Modal> ); } }); @@ -392,34 +368,6 @@ define([ uuid.fetch().then(function () { this.setState({ uuid: uuid.next() }); }.bind(this)); - return; - } - - var params = (this.props.visible) ? { show: true, backdrop: 'static', keyboard: true } : 'hide'; - $(React.findDOMNode(this)).modal(params); - this.clearEvents(); - - // ensure that if the user clicks ESC to close the window, the store gets wind of it - $(React.findDOMNode(this)).on('hidden.bs.modal', function () { - Actions.hideCloneDocModal(); - }); - - $(React.findDOMNode(this)).on('shown.bs.modal', function () { - this.focus(); - }.bind(this)); - }, - - focus: function () { - $(React.findDOMNode(this.refs.newDocId)).focus(); - }, - - componentWillUnmount: function () { - this.clearEvents(); - }, - - clearEvents: function () { - if (this.refs.newDocId) { - $(React.findDOMNode(this.refs.newDocId)).off('shown.bs.modal hidden.bs.modal'); } }, @@ -437,28 +385,28 @@ define([ } return ( - <div className="modal hide fade clone-doc-modal" data-js-visible={this.props.visible} tabIndex="-1"> - <div className="modal-header"> - <button type="button" className="close" onClick={this.closeModal} aria-hidden="true">×</button> - <h3>Clone Document</h3> - </div> - <div className="modal-body"> + <Modal dialogClassName="clone-doc-modal" show={this.props.visible} onHide={this.closeModal}> + <Modal.Header closeButton={true}> + <Modal.Title>Clone Document</Modal.Title> + </Modal.Header> + <Modal.Body> <form className="form" method="post"> <p> Set new document's ID: </p> - <input ref="newDocId" type="text" className="input-block-level" onChange={this.docIDChange} value={this.state.uuid} /> + <input ref="newDocId" type="text" autoFocus={true} className="input-block-level" + onChange={this.docIDChange} value={this.state.uuid} /> </form> - </div> - <div className="modal-footer"> + </Modal.Body> + <Modal.Footer> <button className="btn" onClick={this.closeModal}> <i className="icon fonticon-cancel-circled"></i> Cancel </button> <button className="btn btn-success save" onClick={this.cloneDoc}> <i className="fonticon-ok-circled"></i> Clone </button> - </div> - </div> + </Modal.Footer> + </Modal> ); } }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/efc2a1e2/app/addons/documents/doc-editor/tests/doc-editor.componentsSpec.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/documents/doc-editor/tests/doc-editor.componentsSpec.react.jsx b/app/addons/documents/doc-editor/tests/doc-editor.componentsSpec.react.jsx index eeaf2a2..cdbc192 100644 --- a/app/addons/documents/doc-editor/tests/doc-editor.componentsSpec.react.jsx +++ b/app/addons/documents/doc-editor/tests/doc-editor.componentsSpec.react.jsx @@ -20,9 +20,14 @@ define([ 'addons/documents/doc-editor/actions', 'addons/documents/doc-editor/actiontypes', 'addons/databases/base', - 'testUtils' -], function (app, FauxtonAPI, React, Documents, Components, Stores, Actions, ActionTypes, Databases, utils) { + 'testUtils', + 'libs/react-bootstrap' +], function (app, FauxtonAPI, React, Documents, Components, Stores, Actions, ActionTypes, Databases, utils, + ReactBoostrap) { + FauxtonAPI.router = new FauxtonAPI.Router([]); + var Modal = ReactBoostrap.Modal; + var assert = utils.assert; var TestUtils = React.addons.TestUtils; @@ -157,9 +162,15 @@ define([ doc: doc } }); - assert.ok($(el.getDOMNode()).find('.confirmation-modal')[0].getAttribute('data-js-visible') == 'false'); + + // this is unfortunate, but I can't find a better way to do it. Refs won't work for bootstrap modals because + // they add the modal to the page at the top level outside the component. There are 3 modals in the + // component: the upload modal, clone modal, delete doc modal. We locate it by index + var modals = TestUtils.scryRenderedComponentsWithType(el, Modal); + + assert.equal(React.findDOMNode(modals[2].refs.modal), null); Actions.showDeleteDocModal(); - assert.ok($(el.getDOMNode()).find('.confirmation-modal')[0].getAttribute('data-js-visible') == 'true'); + assert.notEqual(React.findDOMNode(modals[2].refs.modal), null); }); it('setting uploadDocModal=true in store shows modal', function () { @@ -171,13 +182,15 @@ define([ doc: doc } }); - assert.ok($(el.getDOMNode()).find('.upload-file-modal')[0].getAttribute('data-js-visible') == 'false'); + var modals = TestUtils.scryRenderedComponentsWithType(el, Modal); + + assert.equal(React.findDOMNode(modals[1].refs.modal), null); Actions.showUploadModal(); - assert.ok($(el.getDOMNode()).find('.upload-file-modal')[0].getAttribute('data-js-visible') == 'true'); + assert.notEqual(React.findDOMNode(modals[1].refs.modal), null); }); - }); + describe("AttachmentsPanelButton", function () { var container, doc; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/efc2a1e2/app/addons/documents/tests/nightwatch/createsDocument.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/createsDocument.js b/app/addons/documents/tests/nightwatch/createsDocument.js index 1b4e321..ef04bae 100644 --- a/app/addons/documents/tests/nightwatch/createsDocument.js +++ b/app/addons/documents/tests/nightwatch/createsDocument.js @@ -26,7 +26,7 @@ module.exports = { .clickWhenVisible('#new-all-docs-button a[href="#/database/' + newDatabaseName + '/new"]') .waitForElementPresent('#editor-container', waitTime, false) .verify.urlEquals(baseUrl + '/#/database/' + newDatabaseName + '/new') - .waitForElementPresent('.ace_layer.ace_cursor-layer.ace_hidden-cursors', waitTime, false) + .waitForElementPresent('.ace_gutter-active-line', waitTime, false) // confirm the header elements are showing up .waitForElementVisible('.js-lastelement', waitTime, true) http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/efc2a1e2/app/addons/fauxton/components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/fauxton/components.react.jsx b/app/addons/fauxton/components.react.jsx index 784c514..3cae556 100644 --- a/app/addons/fauxton/components.react.jsx +++ b/app/addons/fauxton/components.react.jsx @@ -15,12 +15,15 @@ define([ 'api', 'react', 'addons/fauxton/dependencies/ZeroClipboard', + 'libs/react-bootstrap', // needed to run the test individually. Don't remove 'velocity.ui' ], -function (app, FauxtonAPI, React, ZeroClipboard) { +function (app, FauxtonAPI, React, ZeroClipboard, ReactBootstrap) { + + var Modal = ReactBootstrap.Modal; // the path to the swf depends on whether we're in a bundled environment (e.g. prod) or local @@ -339,32 +342,22 @@ function (app, FauxtonAPI, React, ZeroClipboard) { }; }, - componentDidUpdate: function () { - var params = (this.props.visible) ? { show: true, backdrop: 'static', keyboard: true } : 'hide'; - $(React.findDOMNode(this)).modal(params); - - $(React.findDOMNode(this)).on('hidden.bs.modal', function () { - this.props.onClose(); - }.bind(this)); - }, - render: function () { return ( - <div className="modal hide confirmation-modal fade" tabIndex="-1" data-js-visible={this.props.visible}> - <div className="modal-header"> - <button type="button" className="close" onClick={this.props.onClose} aria-hidden="true">×</button> - <h3>{this.props.title}</h3> - </div> - <div className="modal-body"> + <Modal dialogClassName="confirmation-modal" show={this.props.visible} onHide={this.props.onClose}> + <Modal.Header closeButton={true}> + <Modal.Title>{this.props.title}</Modal.Title> + </Modal.Header> + <Modal.Body> <p> {this.props.text} </p> - </div> - <div className="modal-footer"> + </Modal.Body> + <Modal.Footer> <button className="btn" onClick={this.props.onClose}><i className="icon fonticon-cancel-circled"></i> Cancel</button> <button className="btn btn-success js-btn-success" onClick={this.props.onSubmit}><i className="fonticon-ok-circled"></i> Okay</button> - </div> - </div> + </Modal.Footer> + </Modal> ); } }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/efc2a1e2/assets/less/fauxton.less ---------------------------------------------------------------------- diff --git a/assets/less/fauxton.less b/assets/less/fauxton.less index 2d66676..f71cf57 100644 --- a/assets/less/fauxton.less +++ b/assets/less/fauxton.less @@ -601,6 +601,11 @@ footer.pagination-footer { z-index: 6; } +.modal-title { + font-size: 22px; + margin: 0; + line-height: 30px; +} // left navigationbar is opened @media (max-width: 780px) {