Repository: couchdb-fauxton Updated Branches: refs/heads/master 8f7ed8cb4 -> 54b118562
views: fix several issues with editing views e.g. - saving views multiple times freezes the ui - reduce and map function is reset to default after changing the view name closes COUCHDB-2662 PR: #402 PR-URL: https://github.com/apache/couchdb-fauxton/pull/402 Reviewed-By: garren smith <[email protected]> Reviewed-By: Benjamin Keen <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/54b11856 Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/54b11856 Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/54b11856 Branch: refs/heads/master Commit: 54b118562813113d3ee93244ab8c41da01123e47 Parents: 8f7ed8c Author: Robert Kowalski <[email protected]> Authored: Tue Apr 14 17:42:04 2015 +0200 Committer: Robert Kowalski <[email protected]> Committed: Mon May 11 18:14:36 2015 +0200 ---------------------------------------------------------------------- .../components/react-components.react.jsx | 191 +++++++++++++++---- app/addons/components/tests/codeEditorSpec.js | 115 +++++++++++ app/addons/components/tests/codeEditorSpec.jsx | 115 +++++++++++ app/addons/documents/helpers.js | 2 +- app/addons/documents/index-editor/actions.js | 28 ++- .../documents/index-editor/actiontypes.js | 6 +- .../documents/index-editor/components.react.jsx | 36 +++- app/addons/documents/index-editor/stores.js | 57 ++++-- .../mango/tests/mango.componentsSpec.react.jsx | 4 +- app/addons/documents/routes-index-editor.js | 4 +- app/addons/documents/shared-resources.js | 14 ++ app/addons/documents/tests/actionsSpec.js | 56 +++++- .../documents/tests/nightwatch/changesFilter.js | 4 +- .../documents/tests/nightwatch/viewCreate.js | 26 ++- .../tests/nightwatch/viewCreateBadView.js | 9 +- .../documents/tests/nightwatch/viewEdit.js | 2 +- .../tests/nightwatch/viewSaveManyTimes.js | 50 +++++ .../tests/viewIndex.componentsSpec.react.jsx | 18 +- app/addons/documents/views-doceditor.js | 2 +- app/core/routeObject.js | 3 +- 20 files changed, 631 insertions(+), 111 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/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 fd4e2f5..3830073 100644 --- a/app/addons/components/react-components.react.jsx +++ b/app/addons/components/react-components.react.jsx @@ -15,10 +15,11 @@ define([ 'api', 'react', 'addons/fauxton/components', + 'ace/ace', 'plugins/beautify' ], -function (app, FauxtonAPI, React, Components, beautifyHelper) { +function (app, FauxtonAPI, React, Components, ace, beautifyHelper) { var ToggleHeaderButton = React.createClass({ render: function () { @@ -63,15 +64,138 @@ function (app, FauxtonAPI, React, Components, beautifyHelper) { }); var CodeEditor = React.createClass({ - render: function () { - var code = this.aceEditor ? this.aceEditor.getValue() : this.props.code; - return ( - <div className="control-group"> - {this.getTitleFragment()} - <div className="js-editor" id={this.props.id}>{this.props.code}</div> - <Beautify code={code} beautifiedCode={this.setEditorValue} /> - </div> - ); + getDefaultProps: function () { + return { + id: 'code-editor', + mode: 'javascript', + theme: 'idle_fingers', + fontSize: 13, + code: '', + showGutter: true, + highlightActiveLine: true, + showPrintMargin: false, + autoScrollEditorIntoView: true, + setHeightWithJS: true, + isFullPageEditor: false, + change: function () {} + }; + }, + + hasChanged: function () { + return !_.isEqual(this.props.code, this.getValue()); + }, + + setupAce: function (props, shouldUpdateCode) { + var el = this.getDOMNode(this.refs.ace); + //set the id so our nightwatch tests can find it + el.id = props.id; + + this.editor = ace.edit(el); + // Automatically scrolling cursor into view after selection + // change this will be disabled in the next version + // set editor.$blockScrolling = Infinity to disable this message + this.editor.$blockScrolling = Infinity; + + if (shouldUpdateCode) { + this.setEditorValue(props.code); + } + + this.editor.setShowPrintMargin(props.showPrintMargin); + this.editor.autoScrollEditorIntoView = props.autoScrollEditorIntoView; + this.setHeightToLineCount(); + this.removeIncorrectAnnotations(); + this.editor.getSession().setMode("ace/mode/" + props.mode); + this.editor.setTheme("ace/theme/" + props.theme); + this.editor.setFontSize(props.fontSize); + + }, + + setupEvents: function () { + $(window).on('beforeunload.editor_' + this.props.id, _.bind(this.quitWarningMsg)); + FauxtonAPI.beforeUnload('editor_' + this.props.id, _.bind(this.quitWarningMsg, this)); + + this.editor.on('blur', _.bind(this.saveCodeChange, this)); + }, + + saveCodeChange: function () { + this.props.change(this.getValue()); + }, + + quitWarningMsg: function () { + if (this.hasChanged()) { + return 'Your changes have not been saved. Click cancel to return to the document.'; + } + }, + + removeEvents: function () { + $(window).off('beforeunload.editor_' + this.props.id); + FauxtonAPI.removeBeforeUnload('editor_' + this.props.id); + }, + + setHeightToLineCount: function () { + if (!this.props.setHeightWithJS) { + return; + } + + var lines = this.editor.getSession().getDocument().getLength(); + + if (this.props.isFullPageEditor) { + var maxLines = this.getMaxAvailableLinesOnPage(); + lines = lines < maxLines ? lines : maxLines; + } + this.editor.setOptions({ + maxLines: lines + }); + }, + + // List of JSHINT errors to ignore + // Gets around problem of anonymous functions not being a valid statement + excludedViewErrors: [ + "Missing name in function declaration.", + "['{a}'] is better written in dot notation." + ], + + isIgnorableError: function (msg) { + return _.contains(this.excludedViewErrors, msg); + }, + + removeIncorrectAnnotations: function () { + var editor = this.editor, + isIgnorableError = this.isIgnorableError; + + this.editor.getSession().on("changeAnnotation", function () { + var annotations = editor.getSession().getAnnotations(); + + var newAnnotations = _.reduce(annotations, function (annotations, error) { + if (!isIgnorableError(error.raw)) { + annotations.push(error); + } + return annotations; + }, []); + + if (annotations.length !== newAnnotations.length) { + editor.getSession().setAnnotations(newAnnotations); + } + }); + }, + + componentDidMount: function () { + this.setupAce(this.props, true); + this.setupEvents(); + }, + + componentWillUnmount: function () { + this.removeEvents(); + this.editor.destroy(); + }, + + componentWillReceiveProps: function (nextProps) { + var codeChanged = !_.isEqual(nextProps.code, this.getValue()); + this.setupAce(nextProps, codeChanged); + }, + + editSaved: function () { + return this.hasChanged(); }, getTitleFragment: function () { @@ -94,40 +218,39 @@ function (app, FauxtonAPI, React, Components, beautifyHelper) { ); }, - setEditorValue: function (code) { - this.aceEditor.setValue(code); - //this is not a good practice normally but because we working with a backbone view as the mapeditor - //that keeps the map code state this is the best way to force a render so that the beautify button will hide - this.forceUpdate(); + getAnnotations: function () { + return this.editor.getSession().getAnnotations(); }, - getValue: function () { - return this.aceEditor.getValue(); + hadValidCode: function () { + var errors = this.getAnnotations(); + // By default CouchDB view functions don't pass lint + return _.every(errors, function (error) { + return this.isIgnorableError(error.raw); + }, this); }, - getEditor: function () { - return this.aceEditor; + setEditorValue: function (code, lineNumber) { + lineNumber = lineNumber ? lineNumber : -1; + this.editor.setValue(code, lineNumber); }, - componentDidMount: function () { - this.aceEditor = new Components.Editor({ - editorId: this.props.id, - mode: 'javascript', - couchJSHINT: true - }); - this.aceEditor.render(); + getValue: function () { + return this.editor.getValue(); }, - shouldComponentUpdate: function () { - //we don't want to re-render the map editor as we are using backbone underneath - //which will cause the editor to break - this.aceEditor.editSaved(); - - return false; + getEditor: function () { + return this; }, - componentWillUnmount: function () { - this.aceEditor.remove(); + render: function () { + return ( + <div className="control-group"> + {this.getTitleFragment()} + <div ref="ace" className="js-editor" id={this.props.id}></div> + <Beautify code={this.props.code} beautifiedCode={this.setEditorValue} /> + </div> + ); } }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/components/tests/codeEditorSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/components/tests/codeEditorSpec.js b/app/addons/components/tests/codeEditorSpec.js new file mode 100644 index 0000000..62e7c3a --- /dev/null +++ b/app/addons/components/tests/codeEditorSpec.js @@ -0,0 +1,115 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +define([ + 'api', + 'addons/components/react-components.react', + + 'testUtils', + 'react' +], function (FauxtonAPI, ReactComponents, utils, React) { + + var assert = utils.assert; + var TestUtils = React.addons.TestUtils; + var code = 'function (doc) {\n emit(doc._id, 1);\n}'; + var code2 = 'function (doc) {\n if(doc._id) { \n emit(doc._id, 2); \n } \n}'; + + describe('Code Editor', function () { + var container, codeEditorEl, spy; + + beforeEach(function () { + spy = sinon.spy(); + container = document.createElement('div'); + codeEditorEl = TestUtils.renderIntoDocument( + React.createElement(ReactComponents.CodeEditor, {code: code, change: spy}), + container + ); + }); + + afterEach(function () { + React.unmountComponentAtNode(container); + }); + + describe('Tracking edits', function () { + + it('no change on mount', function () { + assert.notOk(codeEditorEl.hasChanged()); + }); + + it('detects change on user input', function () { + codeEditorEl.editor.setValue(code2, -1); + + assert.ok(codeEditorEl.hasChanged()); + }); + + }); + + describe('onBlur', function () { + + it('calls changed function', function () { + codeEditorEl.editor._emit('blur'); + assert.ok(spy.calledOnce); + }); + + }); + + describe('setHeightToLineCount', function () { + + beforeEach(function () { + codeEditorEl = TestUtils.renderIntoDocument( + React.createElement(ReactComponents.CodeEditor, {code: code, isFullPageEditor: false, setHeightWithJS: true}), + container + ); + + }); + + it('sets line height correctly for non full page', function () { + var spy = sinon.spy(codeEditorEl.editor, 'setOptions'); + + codeEditorEl.setHeightToLineCount(); + assert.ok(spy.calledOnce); + assert.equal(spy.getCall(0).args[0].maxLines, 3); + }); + + }); + + describe('removeIncorrectAnnotations', function () { + + beforeEach(function () { + codeEditorEl = TestUtils.renderIntoDocument( + React.createElement(ReactComponents.CodeEditor, {code: code}), + container + ); + + }); + + it('removes default errors that do not apply to CouchDB Views', function () { + assert.equal(codeEditorEl.getAnnotations(), 0); + }); + + }); + + describe('setEditorValue', function () { + + it('sets new code', function () { + codeEditorEl = TestUtils.renderIntoDocument( + React.createElement(ReactComponents.CodeEditor, {code: code}), + container + ); + + codeEditorEl.setEditorValue(code2); + assert.deepEqual(codeEditorEl.getValue(), code2); + }); + + }); + + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/components/tests/codeEditorSpec.jsx ---------------------------------------------------------------------- diff --git a/app/addons/components/tests/codeEditorSpec.jsx b/app/addons/components/tests/codeEditorSpec.jsx new file mode 100644 index 0000000..849f36c --- /dev/null +++ b/app/addons/components/tests/codeEditorSpec.jsx @@ -0,0 +1,115 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +define([ + 'api', + 'addons/components/react-components.react', + + 'testUtils', + 'react' +], function (FauxtonAPI, ReactComponents, utils, React) { + + var assert = utils.assert; + var TestUtils = React.addons.TestUtils; + var code = 'function (doc) {\n emit(doc._id, 1);\n}'; + var code2 = 'function (doc) {\n if(doc._id) { \n emit(doc._id, 2); \n } \n}'; + + describe('Code Editor', function () { + var container, codeEditorEl, spy; + + beforeEach(function () { + spy = sinon.spy(); + container = document.createElement('div'); + codeEditorEl = TestUtils.renderIntoDocument( + <ReactComponents.CodeEditor code={code} change={spy} />, + container + ); + }); + + afterEach(function () { + React.unmountComponentAtNode(container); + }); + + describe('Tracking edits', function () { + + it('no change on mount', function () { + assert.notOk(codeEditorEl.hasChanged()); + }); + + it('detects change on user input', function () { + codeEditorEl.editor.setValue(code2, -1); + + assert.ok(codeEditorEl.hasChanged()); + }); + + }); + + describe('onBlur', function () { + + it('calls changed function', function () { + codeEditorEl.editor._emit('blur'); + assert.ok(spy.calledOnce); + }); + + }); + + describe('setHeightToLineCount', function () { + + beforeEach(function () { + codeEditorEl = TestUtils.renderIntoDocument( + <ReactComponents.CodeEditor code={code} isFullPageEditor={false} setHeightWithJS={true}/>, + container + ); + + }); + + it('sets line height correctly for non full page', function () { + var spy = sinon.spy(codeEditorEl.editor, 'setOptions'); + + codeEditorEl.setHeightToLineCount(); + assert.ok(spy.calledOnce); + assert.equal(spy.getCall(0).args[0].maxLines, 3); + }); + + }); + + describe('removeIncorrectAnnotations', function () { + + beforeEach(function () { + codeEditorEl = TestUtils.renderIntoDocument( + <ReactComponents.CodeEditor code={code}/>, + container + ); + + }); + + it('removes default errors that do not apply to CouchDB Views', function () { + assert.equal(codeEditorEl.getAnnotations(), 0); + }); + + }); + + describe('setEditorValue', function () { + + it('sets new code', function () { + codeEditorEl = TestUtils.renderIntoDocument( + <ReactComponents.CodeEditor code={code}/>, + container + ); + + codeEditorEl.setEditorValue(code2); + assert.deepEqual(codeEditorEl.getValue(), code2); + }); + + }); + + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/helpers.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/helpers.js b/app/addons/documents/helpers.js index e04e3b4..1d156b2 100644 --- a/app/addons/documents/helpers.js +++ b/app/addons/documents/helpers.js @@ -23,7 +23,7 @@ define([ if (!wasCloned && lastPages.length >= 2) { // if we came from "/new", we don't want to link the user there - if (/new$/.test(lastPages[1])) { + if (/(new|new_view)$/.test(lastPages[1])) { previousPage = lastPages[0]; } else { previousPage = lastPages[1]; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/index-editor/actions.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/index-editor/actions.js b/app/addons/documents/index-editor/actions.js index a7b33c7..b5250a0 100644 --- a/app/addons/documents/index-editor/actions.js +++ b/app/addons/documents/index-editor/actions.js @@ -107,9 +107,6 @@ function (app, FauxtonAPI, Documents, ActionTypes, IndexResultsActions) { viewInfo.reduce); if (result) { - FauxtonAPI.dispatch({ - type: ActionTypes.SAVE_VIEW - }); FauxtonAPI.addNotification({ msg: "Saving View...", @@ -124,19 +121,29 @@ function (app, FauxtonAPI, Documents, ActionTypes, IndexResultsActions) { clear: true }); + if (_.any([viewInfo.designDocChanged, viewInfo.hasViewNameChanged, viewInfo.newDesignDoc, viewInfo.newView])) { FauxtonAPI.dispatch({ type: ActionTypes.VIEW_SAVED }); var fragment = FauxtonAPI.urls('view', 'showNewlySavedView', viewInfo.database.safeID(), designDoc.safeID(), app.utils.safeURLName(viewInfo.viewName)); FauxtonAPI.navigate(fragment, {trigger: true}); + } else { + this.updateDesignDoc(designDoc); } IndexResultsActions.reloadResultsList(); - }); + }.bind(this)); } }, + updateDesignDoc: function (designDoc) { + FauxtonAPI.dispatch({ + type: ActionTypes.VIEW_UPDATE_DESIGN_DOC, + designDoc: designDoc.toJSON() + }); + }, + deleteView: function (options) { var viewName = options.viewName; var database = options.database; @@ -156,7 +163,20 @@ function (app, FauxtonAPI, Documents, ActionTypes, IndexResultsActions) { FauxtonAPI.navigate(url); FauxtonAPI.triggerRouteEvent('reloadDesignDocs'); }); + }, + updateMapCode: function (code) { + FauxtonAPI.dispatch({ + type: ActionTypes.VIEW_UPDATE_MAP_CODE, + code: code + }); + }, + + updateReduceCode: function (code) { + FauxtonAPI.dispatch({ + type: ActionTypes.VIEW_UPDATE_REDUCE_CODE, + code: code + }); } }; }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/index-editor/actiontypes.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/index-editor/actiontypes.js b/app/addons/documents/index-editor/actiontypes.js index fcce54e..e13c57a 100644 --- a/app/addons/documents/index-editor/actiontypes.js +++ b/app/addons/documents/index-editor/actiontypes.js @@ -17,9 +17,11 @@ define([], function () { SELECT_REDUCE_CHANGE: 'SELECT_REDUCE_CHANGE', VIEW_SAVED: 'VIEW_SAVED', VIEW_CREATED: 'VIEW_CREATED', - SAVE_VIEW: 'SAVE_VIEW', DESIGN_DOC_CHANGE: 'DESIGN_DOC_CHANGE', NEW_DESIGN_DOC: 'NEW_DESIGN_DOC', - VIEW_NAME_CHANGE: 'VIEW_NAME_CHANGE' + VIEW_NAME_CHANGE: 'VIEW_NAME_CHANGE', + VIEW_UPDATE_DESIGN_DOC: 'VIEW_UPDATE_DESIGN_DOC', + VIEW_UPDATE_MAP_CODE: 'VIEW_UPDATE_MAP_CODE', + VIEW_UPDATE_REDUCE_CODE: 'VIEW_UPDATE_REDUCE_CODE' }; }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/index-editor/components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/documents/index-editor/components.react.jsx b/app/addons/documents/index-editor/components.react.jsx index c38be29..1ba8bc6 100644 --- a/app/addons/documents/index-editor/components.react.jsx +++ b/app/addons/documents/index-editor/components.react.jsx @@ -27,6 +27,7 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents) var CodeEditor = ReactComponents.CodeEditor; var PaddedBorderedBox = ReactComponents.PaddedBorderedBox; var ConfirmButton = ReactComponents.ConfirmButton; + var LoadLines = ReactComponents.LoadLines; var DesignDocSelector = React.createClass({ @@ -58,7 +59,7 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents) getDesignDocOptions: function () { return this.state.designDocs.map(function (doc, i) { - return <option key={i} value={doc.id}> {doc.id} </option>; + return <option key={i} value={doc.id}>{doc.id}</option>; }); }, @@ -67,7 +68,7 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents) return ( <optgroup label="Select a document"> - <option value="new">New Design Document </option> + <option value="new">New Design Document</option> {designDocOptions} </optgroup> ); @@ -151,7 +152,7 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents) getOptionsList: function () { return _.map(this.state.reduceOptions, function (reduce, i) { - return <option key={i} value={reduce}> {reduce} </option>; + return <option key={i} value={reduce}>{reduce}</option>; }, this); }, @@ -174,10 +175,15 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents) render: function () { var reduceOptions = this.getOptionsList(), - customReduceSection; + customReduceSection; if (this.state.hasCustomReduce) { - customReduceSection = <CodeEditor ref='reduceEditor' id={'reduce-function'} code={this.state.reduce} docs={false} title={'Custom Reduce function'} />; + customReduceSection = <CodeEditor + ref='reduceEditor' + id='reduce-function' + code={this.state.reduce} + change={this.updateReduceCode} + docs={false} title={'Custom Reduce function'} />; } return ( @@ -206,6 +212,10 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents) ); }, + updateReduceCode: function (code) { + Actions.updateReduceCode(code); + }, + selectChange: function (event) { Actions.selectReduceChanged(event.target.value); }, @@ -278,7 +288,8 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents) hasDesignDocChanged: indexEditorStore.hasDesignDocChanged(), newDesignDoc: indexEditorStore.isNewDesignDoc(), designDocId: indexEditorStore.getDesignDocId(), - map: indexEditorStore.getMap() + map: indexEditorStore.getMap(), + isLoading: indexEditorStore.isLoading() }; }, @@ -350,7 +361,19 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents) Actions.changeViewName(event.target.value); }, + updateMapCode: function (code) { + Actions.updateMapCode(code); + }, + render: function () { + if (this.state.isLoading) { + return ( + <div className="define-view"> + <LoadLines /> + </div> + ); + } + return ( <div className="define-view"> <PaddedBorderedBox> @@ -389,6 +412,7 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents) ref="mapEditor" title={"Map function"} docs={getDocUrl('MAP_FUNCS')} + change={this.updateMapCode} code={this.state.map} /> </PaddedBorderedBox> </div> http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/index-editor/stores.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/index-editor/stores.js b/app/addons/documents/index-editor/stores.js index 9839001..3f7bb7d 100644 --- a/app/addons/documents/index-editor/stores.js +++ b/app/addons/documents/index-editor/stores.js @@ -25,13 +25,9 @@ function (FauxtonAPI, ActionTypes) { initialize: function () { this._designDocs = []; - this._view = { - reduce: this.defaultMap, - map: this.defaultReduce - }; - this._database = { - id: '0' - }; + this._isLoading = true; + this._view = { reduce: '', map: this.defaultMap }; + this._database = { id: '0' }; }, editIndex: function (options) { @@ -43,14 +39,19 @@ function (FauxtonAPI, ActionTypes) { this._designDocId = options.designDocId; this._designDocChanged = false; this._viewNameChanged = false; + this.setView(); + this._isLoading = false; + }, - if (!this._newView && !this._newDesignDoc) { - this._view = this.getDesignDoc().get('views')[this._viewName]; + isLoading: function () { + return this._isLoading; + }, + + setView: function () { + if (this._newView || this._newDesignDoc) { + this._view = { reduce: '', map: this.defaultMap }; } else { - this._view = { - reduce: '', - map: '' - }; + this._view = this.getDesignDoc().get('views')[this._viewName]; } }, @@ -59,13 +60,13 @@ function (FauxtonAPI, ActionTypes) { }, getMap: function () { - if (this._newView) { - return this.defaultMap; - } - return this._view.map; }, + setMap: function (map) { + this._view.map = map; + }, + getReduce: function () { return this._view.reduce; }, @@ -83,7 +84,7 @@ function (FauxtonAPI, ActionTypes) { getDesignDocs: function () { return this._designDocs.filter(function (ddoc) { - return ddoc.get('doc').language !== 'query'; + return !ddoc.isMangoDoc(); }); }, @@ -168,6 +169,10 @@ function (FauxtonAPI, ActionTypes) { this.setReduce(selectedReduce); }, + updateDesignDoc: function (designDoc) { + this._designDocs.add(designDoc, {merge: true}); + }, + dispatch: function (action) { switch (action.type) { case ActionTypes.EDIT_INDEX: @@ -208,6 +213,22 @@ function (FauxtonAPI, ActionTypes) { this.triggerChange(); break; + case ActionTypes.VIEW_UPDATE_DESIGN_DOC: + this.updateDesignDoc(action.designDoc); + this.setView(); + this.triggerChange(); + break; + + case ActionTypes.VIEW_UPDATE_MAP_CODE: + this.setMap(action.code); + this.triggerChange(); + break; + + case ActionTypes.VIEW_UPDATE_REDUCE_CODE: + this.setReduce(action.code); + this.triggerChange(); + break; + default: return; // do nothing http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/mango/tests/mango.componentsSpec.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/documents/mango/tests/mango.componentsSpec.react.jsx b/app/addons/documents/mango/tests/mango.componentsSpec.react.jsx index c50897b..0f71fa3 100644 --- a/app/addons/documents/mango/tests/mango.componentsSpec.react.jsx +++ b/app/addons/documents/mango/tests/mango.componentsSpec.react.jsx @@ -60,18 +60,16 @@ define([ MangoActions.setDatabase({ database: database }); - $('body').append('<div id="query-field"></div>'); }); afterEach(function () { React.unmountComponentAtNode(container); - $('#query-field').remove(); }); it('renders a default index definition', function () { editor = TestUtils.renderIntoDocument(<Views.MangoIndexEditorController description="foo" />, container); var $el = $(editor.getDOMNode()); - var payload = JSON.parse($el.find('.js-editor').text()); + var payload = JSON.parse(editor.refs.indexQueryEditor.getValue()); assert.equal(payload.index.fields[0], '_id'); }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/routes-index-editor.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/routes-index-editor.js b/app/addons/documents/routes-index-editor.js index e04b35b..b15177c 100644 --- a/app/addons/documents/routes-index-editor.js +++ b/app/addons/documents/routes-index-editor.js @@ -126,8 +126,8 @@ function (app, FauxtonAPI, Helpers, BaseRoute, Documents, IndexEditorComponents, this.breadcrumbs = this.setView('#breadcrumbs', new Components.Breadcrumbs({ toggleDisabled: true, crumbs: [ - {'type': 'back', 'link': Helpers.getPreviousPage(this.database)}, - {'name': 'Create new index', 'link': Databases.databaseUrl(this.database) } + { type: 'back', link: Helpers.getPreviousPage(this.database) }, + { name: 'Create new index', link: Databases.databaseUrl(this.database) } ] })); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/shared-resources.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/shared-resources.js b/app/addons/documents/shared-resources.js index a78709a..35c0e51 100644 --- a/app/addons/documents/shared-resources.js +++ b/app/addons/documents/shared-resources.js @@ -130,11 +130,25 @@ define([ this.set({views: views}); }, + isMangoDoc: function () { + if (!this.isDdoc()) return false; + if (this.get('language') === 'query') { + return true; + } + + if (this.get('doc') && this.get('doc').language === 'query') { + return true; + } + + return false; + }, + dDocModel: function () { if (!this.isDdoc()) return false; var doc = this.get('doc'); if (doc) { + doc._rev = this.get('_rev'); return new Documents.Doc(doc, {database: this.database}); } http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/tests/actionsSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/actionsSpec.js b/app/addons/documents/tests/actionsSpec.js index d0a44bd..dec71fb 100644 --- a/app/addons/documents/tests/actionsSpec.js +++ b/app/addons/documents/tests/actionsSpec.js @@ -15,10 +15,11 @@ define([ 'addons/documents/index-editor/actions', 'addons/documents/resources', 'addons/documents/index-editor/actiontypes', + 'addons/documents/index-editor/stores', 'testUtils', 'addons/documents/index-results/actions', 'addons/documents/base' -], function (FauxtonAPI, Actions, Documents, ActionTypes, testUtils, IndexResultsActions) { +], function (FauxtonAPI, Actions, Documents, ActionTypes, Stores, testUtils, IndexResultsActions) { var assert = testUtils.assert; var restore = testUtils.restore; @@ -34,14 +35,15 @@ define([ beforeEach(function () { designDoc = { _id: '_design/test-doc', + _rev: '1-231313', views: { 'test-view': { map: 'function () {};', } } }; - - designDocs = new Documents.AllDocs([designDoc], { + var doc = new Documents.Doc(designDoc, {database: database}); + designDocs = new Documents.AllDocs([doc], { params: { limit: 10 }, database: database }); @@ -53,6 +55,7 @@ define([ restore(FauxtonAPI.navigate); restore(FauxtonAPI.triggerRouteEvent); restore(IndexResultsActions.reloadResultsList); + restore(Actions.updateDesignDoc); }); it('shows a notification if no design doc id given', function () { @@ -127,6 +130,45 @@ define([ assert.ok(spy.calledOnce); }); + it('updates design doc', function () { + var viewInfo = { + viewName: 'test-view', + designDocId: '_design/test-doc', + map: 'map', + reduce: '_sum', + newDesignDoc: false, + newView: false, + designDocs: designDocs, + database: { + safeID: function () { return '1';} + } + }; + + designDocs.find = function () {}; + designDocs.add = function () {}; + designDocs.dDocModel = function () {}; + + Actions.editIndex({ + database: {id: 'rockos-db'}, + newView: true, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDocs[0]._id + }); + + var promise = FauxtonAPI.Deferred(); + promise.resolve(); + + var updatedDesignDoc = _.first(designDocs).dDocModel(); + var stub = sinon.stub(updatedDesignDoc, 'save'); + stub.returns(promise); + + var spy = sinon.spy(Actions, 'updateDesignDoc'); + Actions.saveView(viewInfo); + + assert.ok(spy.calledOnce); + }); + it('navigates to new url for new view', function () { var spy = sinon.spy(FauxtonAPI, 'navigate'); @@ -178,6 +220,9 @@ define([ return promise; }; + var stub = sinon.stub(Actions, 'updateDesignDoc'); + stub.returns(true); + Actions.saveView(viewInfo); assert.ok(spy.calledOnce); }); @@ -194,6 +239,7 @@ define([ designDocId = '_design/test-doc'; designDocs = new Documents.AllDocs([{ _id: designDocId , + _rev: '1-231', views: { 'test-view': { map: 'function () {};', @@ -212,8 +258,8 @@ define([ }); afterEach(function () { - FauxtonAPI.navigate.restore && FauxtonAPI.navigate.restore(); - FauxtonAPI.triggerRouteEvent.restore && FauxtonAPI.triggerRouteEvent.restore(); + restore(FauxtonAPI.navigate); + restore(FauxtonAPI.triggerRouteEvent); }); it('removes view from design doc', function () { http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/tests/nightwatch/changesFilter.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/changesFilter.js b/app/addons/documents/tests/nightwatch/changesFilter.js index e2e4e5b..73846d8 100644 --- a/app/addons/documents/tests/nightwatch/changesFilter.js +++ b/app/addons/documents/tests/nightwatch/changesFilter.js @@ -33,10 +33,10 @@ module.exports = { .waitForElementPresent('.change-box[data-id="doc_3"]', waitTime, false) // add a filter - .click("#db-views-tabs-nav a") + .clickWhenVisible("#db-views-tabs-nav a") .waitForElementVisible('.js-changes-filter-field', waitTime, false) .setValue('.js-changes-filter-field', "doc_1") - .click('.js-filter-form button[type="submit"]') + .clickWhenVisible('.js-filter-form button[type="submit"]') // confirm only the single result is now listed in the page .waitForElementVisible('span.label-info', waitTime, false) http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/tests/nightwatch/viewCreate.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/viewCreate.js b/app/addons/documents/tests/nightwatch/viewCreate.js index 61d8dcc..b96eab6 100644 --- a/app/addons/documents/tests/nightwatch/viewCreate.js +++ b/app/addons/documents/tests/nightwatch/viewCreate.js @@ -10,16 +10,11 @@ // License for the specific language governing permissions and limitations under // the License. -var waitTime, - baseUrl, - newDatabaseName, - newDocumentName, - modifier; - -var tests = { +module.exports = { 'Creates a Design Doc using the dropdown at "all documents"': function (client) { var waitTime = client.globals.maxWaitTime; + var baseUrl = client.globals.test_settings.launch_url; /*jshint multistr: true */ openDifferentDropdownsAndClick(client, '#header-dropdown-menu') @@ -41,6 +36,7 @@ var tests = { 'Creates a Design Doc using the dropdown at "the upper dropdown in the header"': function (client) { var waitTime = client.globals.maxWaitTime; + var baseUrl = client.globals.test_settings.launch_url; /*jshint multistr: true */ openDifferentDropdownsAndClick(client, '#header-dropdown-menu') @@ -48,6 +44,7 @@ var tests = { .setValue('#new-ddoc', 'test_design_doc-selenium-2') .clearValue('#index-name') .setValue('#index-name', 'gaenseindex') + .sendKeys("textarea.ace_text-input", client.Keys.Enter) .execute('\ var editor = ace.edit("map-function");\ editor.getSession().setValue("function (doc) { emit(\'gansgans\'); }");\ @@ -62,11 +59,14 @@ var tests = { 'Adds a View to a DDoc using an existing DDoc': function (client) { var waitTime = client.globals.maxWaitTime; + var baseUrl = client.globals.test_settings.launch_url; + var newDatabaseName = client.globals.testDatabaseName; /*jshint multistr: true */ openDifferentDropdownsAndClick(client, '#nav-header-testdesigndoc') .clearValue('#index-name') .setValue('#index-name', 'test-new-view') + .sendKeys("textarea.ace_text-input", client.Keys.Enter) .execute('\ var editor = ace.edit("map-function");\ editor.getSession().setValue("function (doc) { emit(\'enteente\', 1); }");\ @@ -89,11 +89,11 @@ var tests = { }; function openDifferentDropdownsAndClick (client, dropDownElement) { - modifier = dropDownElement.slice(1); - waitTime = client.globals.maxWaitTime; - newDatabaseName = client.globals.testDatabaseName; - newDocumentName = 'create_view_doc' + modifier; - baseUrl = client.globals.test_settings.launch_url; + var modifier = dropDownElement.slice(1); + var waitTime = client.globals.maxWaitTime; + var newDatabaseName = client.globals.testDatabaseName; + var newDocumentName = 'create_view_doc' + modifier; + var baseUrl = client.globals.test_settings.launch_url; return client .loginToGUI() @@ -104,5 +104,3 @@ function openDifferentDropdownsAndClick (client, dropDownElement) { .click(dropDownElement + ' a[href*="new_view"]') .waitForElementPresent('.editor-wrapper', waitTime, false); } - -module.exports = tests; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/tests/nightwatch/viewCreateBadView.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/viewCreateBadView.js b/app/addons/documents/tests/nightwatch/viewCreateBadView.js index 19a79bd..f82f7af 100644 --- a/app/addons/documents/tests/nightwatch/viewCreateBadView.js +++ b/app/addons/documents/tests/nightwatch/viewCreateBadView.js @@ -22,17 +22,20 @@ module.exports = { client .loginToGUI() .populateDatabase(newDatabaseName) - .url(baseUrl + '/#/database/' + newDatabaseName + '/new_view') + .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs') + .waitForElementPresent(dropDownElement, waitTime, false) + .click(dropDownElement + ' a') + .click(dropDownElement + ' a[href*="new_view"]') .waitForElementVisible('#new-ddoc', waitTime, false) .setValue('#new-ddoc', 'test_design_doc-selenium-bad-reduce') .clearValue('#index-name') .setValue('#index-name', 'hasenindex') + .click('#reduce-function-selector') + .keys(['\uE013', '\uE013', '\uE013', '\uE013', '\uE006']) .execute('\ var editor = ace.edit("map-function");\ editor.getSession().setValue("function (doc) { emit(\'boom\', doc._id); }");\ ') - .click('#reduce-function-selector') - .keys(['\uE013', '\uE013', '\uE013', '\uE013', '\uE006']) .execute('$(".save")[0].scrollIntoView();') .click('button.btn-success.save') .waitForElementVisible('.alert-error', waitTime, false) http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/tests/nightwatch/viewEdit.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/viewEdit.js b/app/addons/documents/tests/nightwatch/viewEdit.js index f23cda2..cb04c99 100644 --- a/app/addons/documents/tests/nightwatch/viewEdit.js +++ b/app/addons/documents/tests/nightwatch/viewEdit.js @@ -55,10 +55,10 @@ module.exports = { .execute('\ var editor = ace.edit("map-function");\ editor.getSession().setValue("function (doc) { emit(\'hasehase5000\', 1); }");\ + editor._emit(\'blur\');\ ') .execute('$(".save")[0].scrollIntoView();') .clickWhenVisible('button.btn-success.save') - .waitForElementNotVisible('.global-notification', waitTime, false) .waitForElementNotPresent('.loading-lines', waitTime, false) .assert.containsText('.prettyprint', 'hasehase5000') http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/tests/nightwatch/viewSaveManyTimes.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/viewSaveManyTimes.js b/app/addons/documents/tests/nightwatch/viewSaveManyTimes.js new file mode 100644 index 0000000..6f6570d --- /dev/null +++ b/app/addons/documents/tests/nightwatch/viewSaveManyTimes.js @@ -0,0 +1,50 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +module.exports = { + + 'View can be saved multiple times': function (client) { + /*jshint multistr: true */ + var waitTime = client.globals.maxWaitTime, + newDatabaseName = client.globals.testDatabaseName, + dropDownElement = '#header-dropdown-menu', + baseUrl = client.globals.test_settings.launch_url; + + client + .loginToGUI() + .populateDatabase(newDatabaseName) + .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs') + .waitForElementPresent(dropDownElement, waitTime, false) + .click(dropDownElement + ' a') + .click(dropDownElement + ' a[href*="new_view"]') + .waitForElementPresent('.editor-wrapper', waitTime, false) + .setValue('#new-ddoc', 'test_design_doc-save-many-times') + .clearValue('#index-name') + .setValue('#index-name', 'multiple-saves') + .sendKeys("textarea.ace_text-input", client.Keys.Enter) + .execute('\ + var editor = ace.edit("map-function");\ + editor.getSession().setValue("function (doc) { emit(\'boom\', doc._id); }");\ + editor._emit(\'blur\');\ + ') + .click('button.btn-success.save') + .waitForElementVisible('.alert-success', waitTime, false) + .waitForElementNotVisible('.alert-success', waitTime, false) + .click('button.btn-success.save') + .waitForElementVisible('.alert-success', waitTime, false) + .waitForElementNotVisible('.alert-success', waitTime, false) + .click('button.btn-success.save') + .waitForElementVisible('.alert-success', waitTime, false) + .assert.containsText('.alert-success', 'View Saved.') + .end(); + }, +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/tests/viewIndex.componentsSpec.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/viewIndex.componentsSpec.react.jsx b/app/addons/documents/tests/viewIndex.componentsSpec.react.jsx index 68c4033..9cac62d 100644 --- a/app/addons/documents/tests/viewIndex.componentsSpec.react.jsx +++ b/app/addons/documents/tests/viewIndex.componentsSpec.react.jsx @@ -22,20 +22,13 @@ define([ var assert = utils.assert; var TestUtils = React.addons.TestUtils; + var restore = utils.restore; var resetStore = function (designDocs) { designDocs = designDocs.map(function (doc) { return Documents.Doc.prototype.parse(doc); }); - designDocs.map(function (ddoc) { - return new Documents.Doc(ddoc, { - database: { - safeID: function () { return 'id'; } - } - }); - }); - var ddocs = new Documents.AllDocs(designDocs, { params: { limit: 10 }, database: { @@ -179,8 +172,8 @@ define([ afterEach(function () { - Actions.newDesignDoc.restore && Actions.newDesignDoc.restore(); - Actions.designDocChange.restore && Actions.designDocChange.restore(); + restore(Actions.newDesignDoc); + restore(Actions.designDocChange); React.unmountComponentAtNode(container); }); @@ -243,13 +236,13 @@ define([ }); it('returns false on invalid map editor code', function () { - var stub = sinon.stub(editorEl.refs.mapEditor.aceEditor, 'hadValidCode'); + var stub = sinon.stub(editorEl.refs.mapEditor, 'hadValidCode'); stub.returns(false); assert.notOk(editorEl.hasValidCode()); }); it('returns true on valid map editor code', function () { - var stub = sinon.stub(editorEl.refs.mapEditor.aceEditor, 'hadValidCode'); + var stub = sinon.stub(editorEl.refs.mapEditor, 'hadValidCode'); stub.returns(true); assert.ok(editorEl.hasValidCode()); }); @@ -271,6 +264,5 @@ define([ }); assert.ok(spy.calledWith(viewName)); }); - }); }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/addons/documents/views-doceditor.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/views-doceditor.js b/app/addons/documents/views-doceditor.js index 0e2f4c7..7df5efe 100644 --- a/app/addons/documents/views-doceditor.js +++ b/app/addons/documents/views-doceditor.js @@ -227,7 +227,7 @@ function (app, FauxtonAPI, Components, Documents, Databases, prettify) { }, goBack: function () { - FauxtonAPI.navigate(FauxtonAPI.urls('allDocs', 'app', this.database.id, '?limit=20')); + FauxtonAPI.navigate(FauxtonAPI.urls('allDocs', 'app', this.database.id)); }, destroy: function () { http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/54b11856/app/core/routeObject.js ---------------------------------------------------------------------- diff --git a/app/core/routeObject.js b/app/core/routeObject.js index 818f4eb..c3705b8 100644 --- a/app/core/routeObject.js +++ b/app/core/routeObject.js @@ -283,7 +283,6 @@ function (FauxtonAPI, React, Backbone) { }, removeViews: function () { - this.reactComponents = {}; _.each(this.views, function (view, selector) { view.remove(); delete this.views[selector]; @@ -304,8 +303,8 @@ function (FauxtonAPI, React, Backbone) { }, cleanup: function () { - this.removeViews(); this.removeComponents(); + this.removeViews(); this.rejectPromises(); },
