Repository: couchdb-fauxton Updated Branches: refs/heads/master b164d652b -> 16ec9ab45
Index Editor in React.js Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/16ec9ab4 Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/16ec9ab4 Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/16ec9ab4 Branch: refs/heads/master Commit: 16ec9ab45a3474b3c26acc2ba2112af5263eb2b4 Parents: b164d65 Author: Garren Smith <[email protected]> Authored: Mon Jan 19 12:51:10 2015 +0200 Committer: Garren Smith <[email protected]> Committed: Wed Jan 28 12:15:10 2015 +0200 ---------------------------------------------------------------------- .../databases/tests/nightwatch/createsView.js | 4 +- app/addons/documents/assets/less/documents.less | 5 + .../documents/assets/less/viewEditor.less | 85 +++ app/addons/documents/index-editor/actions.js | 169 ++++++ .../documents/index-editor/actiontypes.js | 26 + .../documents/index-editor/components.react.jsx | 534 +++++++++++++++++++ app/addons/documents/index-editor/stores.js | 211 ++++++++ app/addons/documents/routes-documents.js | 32 +- app/addons/documents/tests/actionsSpec.js | 292 ++++++++++ app/addons/documents/tests/storesSpec.js | 382 +++++++++++++ .../tests/viewIndex.componentsSpec.react.jsx | 252 +++++++++ app/addons/documents/tests/viewsSpec.js | 4 +- app/addons/documents/views-index.js | 526 +----------------- 13 files changed, 1990 insertions(+), 532 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/app/addons/databases/tests/nightwatch/createsView.js ---------------------------------------------------------------------- diff --git a/app/addons/databases/tests/nightwatch/createsView.js b/app/addons/databases/tests/nightwatch/createsView.js index 59c65f6..0523993 100644 --- a/app/addons/databases/tests/nightwatch/createsView.js +++ b/app/addons/databases/tests/nightwatch/createsView.js @@ -48,7 +48,9 @@ module.exports = { .click('#nav-header-test_design_doc .dropdown-toggle.icon.fonticon-plus-circled') .waitForElementPresent('#nav-header-test_design_doc', waitTime, false) .click('#nav-header-test_design_doc a[href="#/database/'+newDatabaseName+'/new_view/test_design_doc"]') - .verify.valueContains('#index-name','newView') + .waitForElementPresent('#db-views-tabs-nav', waitTime, false) + .click('#db-views-tabs-nav') + .verify.valueContains('#index-name','new-view') .clearValue('#index-name') .setValue('#index-name','odd_ids') .execute('\ http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/app/addons/documents/assets/less/documents.less ---------------------------------------------------------------------- diff --git a/app/addons/documents/assets/less/documents.less b/app/addons/documents/assets/less/documents.less index d752de9..ed28272 100644 --- a/app/addons/documents/assets/less/documents.less +++ b/app/addons/documents/assets/less/documents.less @@ -14,6 +14,7 @@ @import "../../../../../assets/less/bootstrap/variables.less"; @import "../../../../../assets/less/bootstrap/mixins.less"; @import "queryOptions.less"; +@import "viewEditor.less"; @import "changes.less"; @import "sidenav.less"; @@ -66,6 +67,10 @@ button.beautify { padding: 20px; } +button.delete { + margin-left: 4px; +} + button.string-edit { position: absolute; padding: 0; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/app/addons/documents/assets/less/viewEditor.less ---------------------------------------------------------------------- diff --git a/app/addons/documents/assets/less/viewEditor.less b/app/addons/documents/assets/less/viewEditor.less new file mode 100644 index 0000000..37fc797 --- /dev/null +++ b/app/addons/documents/assets/less/viewEditor.less @@ -0,0 +1,85 @@ +// 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. + +@import "../../../../../assets/less/animations.less"; +@import "../../../../../assets/less/variables.less"; + +.keyframes(fadeInDownNoReduce, { + opacity: 0; + height: 0px; +}, +{ + opacity: 1; + height: 546px; +}); + + +.keyframes(fadeInDownReduce, { + opacity: 0; + height: 0px; +}, +{ + opacity: 1; + height: 745px; +}); + +.keyframes(fadeOutUpReduce, { + opacity: 1; + height: 745px; +}, +{ + opacity: 0; + height: 0px; +}); + +.keyframes(fadeOutUpNoReduce, { + opacity: 1; + height: 546px; +}, +{ + opacity: 0; + height: 0px; +}); + + +.fadeInDownNoReduce-enter { + .animation(fadeInDownNoReduce 1s both); +} + +.fadeInDownNoReduce-leave { + .animation(fadeOutUpNoReduce 1s both); +} + +.fadeInDownReduce-enter { + .animation(fadeInDownReduce 1s both); +} + +.fadeInDownReduce-leave { + .animation(fadeOutUpReduce 1s both); +} + +#dashboard-upper-content{ + .editor-wrapper { + padding-bottom: 70px; + } + + .tab-content { + height: auto; + padding-top: 70px; + } + + #define-view { + .help-link { + margin-left: 3px; + } + } +} http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/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 new file mode 100644 index 0000000..e8d9a41 --- /dev/null +++ b/app/addons/documents/index-editor/actions.js @@ -0,0 +1,169 @@ +// 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([ + 'app', + 'api', + 'addons/documents/resources', + 'addons/documents/index-editor/actiontypes' +], +function (app, FauxtonAPI, Documents, ActionTypes) { + var ActionHelpers = { + createNewDesignDoc: function (id, database) { + var designDoc = { + _id: id, + views: { + } + }; + + return new Documents.Doc(designDoc, {database: database}); + }, + + findDesignDoc: function (designDocs, designDocId) { + return designDocs.find(function (doc) { + return doc.id === designDocId; + }).dDocModel(); + + } + }; + + return { + //helpers are added here for use in testing actions + helpers: ActionHelpers, + toggleEditor: function () { + FauxtonAPI.dispatch({ + type: ActionTypes.TOGGLE_EDITOR + }); + }, + + selectReduceChanged: function (reduceOption) { + FauxtonAPI.dispatch({ + type: ActionTypes.SELECT_REDUCE_CHANGE, + reduceSelectedOption: reduceOption + }); + }, + + newDesignDoc: function () { + FauxtonAPI.dispatch({ + type: ActionTypes.NEW_DESIGN_DOC + }); + }, + + designDocChange: function (id, newDesignDoc) { + FauxtonAPI.dispatch({ + type: ActionTypes.DESIGN_DOC_CHANGE, + newDesignDoc: newDesignDoc, + designDocId: id + }); + }, + + editIndex: function (options) { + FauxtonAPI.dispatch({ + type: ActionTypes.EDIT_INDEX, + options: options + }); + }, + + saveView: function (viewInfo) { + var designDoc; + var designDocs = viewInfo.designDocs; + + if (_.isUndefined(viewInfo.designDocId)) { + FauxtonAPI.addNotification({ + msg: "Please enter a design doc name.", + type: "error", + clear: true + }); + + return; + } + + if (viewInfo.newDesignDoc) { + designDoc = ActionHelpers.createNewDesignDoc(viewInfo.designDocId, viewInfo.database); + + } else { + designDoc = ActionHelpers.findDesignDoc(designDocs, viewInfo.designDocId); + } + + var result = designDoc.setDdocView(viewInfo.viewName, + viewInfo.map, + viewInfo.reduce); + + if (result) { + FauxtonAPI.dispatch({ + type: ActionTypes.SAVE_VIEW + }); + + FauxtonAPI.addNotification({ + msg: "Saving View...", + type: "info", + clear: true + }); + + designDoc.save().then(function () { + FauxtonAPI.addNotification({ + msg: "View Saved.", + type: "success", + clear: true + }); + + if (_.any([viewInfo.designDocChanged, viewInfo.newDesignDoc, viewInfo.newView])) { + FauxtonAPI.dispatch({ + type: ActionTypes.VIEW_SAVED + }); + + var fragment = '/database/' + + viewInfo.database.safeID() + + '/' + designDoc.safeID() + + '/_view/' + + app.utils.safeURLName(viewInfo.viewName); + + FauxtonAPI.navigate(fragment); + + //This should be changed to a dispatch once implemented + FauxtonAPI.triggerRouteEvent('reloadDesignDocs', { + selectedTab: app.utils.removeSpecialCharacters(designDoc.id.replace(/_design\//,'')) + '_' + app.utils.removeSpecialCharacters(viewInfo.viewName) + }); + } else { + FauxtonAPI.dispatch({ + type: ActionTypes.VIEW_SAVED + }); + //This will should be changed to a dispatch once implemented + FauxtonAPI.triggerRouteEvent('updateAllDocs', {ddoc: designDoc.id, view: viewInfo.viewName}); + } + }); + } + }, + + deleteView: function (options) { + var viewName = options.viewName; + var database = options.database; + var designDoc = ActionHelpers.findDesignDoc(options.designDocs, options.designDocId); + var promise; + + designDoc.removeDdocView(viewName); + + if (designDoc.hasViews()) { + promise = designDoc.save(); + } else { + promise = designDoc.destroy(); + } + + promise.then(function () { + FauxtonAPI.navigate('/database/' + database.safeID() + '/_all_docs?limit=' + FauxtonAPI.constants.DATABASES.DOCUMENT_LIMIT); + FauxtonAPI.triggerRouteEvent('reloadDesignDocs'); + }); + + } + }; +}); + http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/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 new file mode 100644 index 0000000..26d1698 --- /dev/null +++ b/app/addons/documents/index-editor/actiontypes.js @@ -0,0 +1,26 @@ +// 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([], function () { + return { + EDIT_INDEX: 'EDIT_INDEX', + EDIT_NEW_INDEX: 'EDIT_NEW_INDEX', + TOGGLE_EDITOR: 'TOGGLE_EDITOR', + 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' + }; +}); + http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/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 new file mode 100644 index 0000000..acecbeb --- /dev/null +++ b/app/addons/documents/index-editor/components.react.jsx @@ -0,0 +1,534 @@ +// 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([ + 'app', + 'api', + 'react', + 'addons/documents/index-editor/stores', + 'addons/documents/index-editor/actions', + 'addons/fauxton/components', + 'plugins/beautify' +], + +function(app, FauxtonAPI, React, Stores, Actions, Components, beautifyHelper) { + var indexEditorStore = Stores.indexEditorStore; + var getDocUrl = app.helpers.getDocUrl; + + var ToggleButton = React.createClass({ + + render: function() { + return ( + <div className="dashboard-upper-menu"> + <ul className="nav nav-tabs" id="db-views-tabs-nav"> + <li> + <a ref="toggle" data-bypass="true" id="index-nav" data-toggle="tab" href="#index" onClick={this.props.toggleEditor}> + <i className="fonticon-wrench fonticon"></i> + {this.props.title} + </a> + </li> + </ul> + </div> + ); + } + }); + + var DesignDocSelector = React.createClass({ + + getStoreState: function () { + return { + designDocId: indexEditorStore.getDesignDocId(), + designDocs: indexEditorStore.getDesignDocs(), + newDesignDoc: indexEditorStore.isNewDesignDoc() + }; + }, + + getInitialState: function () { + return this.getStoreState(); + }, + + getNewDesignDocInput: function () { + return ( + <div id="new-ddoc-section" className="span5"> + <label className="control-label" htmlFor="new-ddoc"> _design/ </label> + <div className="controls"> + <input value={this.state.designDoc} type="text" id="new-ddoc" onChange={this.onDesignDocChange} placeholder="newDesignDoc" /> + </div> + </div> + ); + }, + + onDesignDocChange: function (event) { + Actions.designDocChange('_design/' + event.target.value, true); + }, + + getDesignDocOptions: function () { + return this.state.designDocs.map(function (doc, i) { + return <option key={i} value={doc.id}> {doc.id} </option>; + }); + }, + + render: function () { + var designDocOptions = this.getDesignDocOptions(); + var designDocInput; + var designDocId = this.state.designDocId; + + if (this.state.newDesignDoc) { + designDocInput = this.getNewDesignDocInput(); + designDocId = 'new'; + } + + return ( + <div className="control-group design-doc-group"> + <div className="span3"> + <label htmlFor="ddoc">Save to Design Document + <a className="help-link" data-bypass="true" href={getDocUrl('DESIGN_DOCS')} target="_blank"> + <i className="icon-question-sign"> + </i> + </a> + </label> + <select id="ddoc" value={designDocId} onChange={this.selectChange}> + <optgroup label="Select a document"> + <option value="new">New Design Document </option> + {designDocOptions} + </optgroup> + </select> + </div> + + {designDocInput} + </div> + ); + }, + + selectChange: function (event) { + var designDocId = event.target.value; + + if (designDocId === 'new') { + Actions.newDesignDoc(); + } else { + Actions.designDocChange(designDocId, false); + } + }, + + onChange: function () { + this.setState(this.getStoreState()); + }, + + componentDidMount: function () { + indexEditorStore.on('change', this.onChange, this); + }, + + componentWillUnmount: function() { + indexEditorStore.off('change', this.onChange); + }, + + }); + + var Beautify = React.createClass({ + noOfLines: function () { + return this.props.code.split(/\r\n|\r|\n/).length; + }, + + canBeautify: function () { + if (this.noOfLines() === 1) { + return true; + } + + return false; + }, + + addTooltip: function () { + if (this.canBeautify) { + $('.beautify-tooltip').tooltip(); + } + }, + + componentDidMount: function () { + this.addTooltip(); + }, + + beautify: function (event) { + event.preventDefault(); + var beautifiedCode = beautifyHelper(this.props.code); + this.props.beautifiedCode(beautifiedCode); + + }, + + render: function () { + if(!this.canBeautify()) { + return null; + } + + return ( + <button onClick={this.beautify} className="beautify beautify_map btn btn-primary btn-large beautify-tooltip" type="button" data-toggle="tooltip" title="Reformat your minified code to make edits to it."> + beautify this code + </button> + ); + } + }); + + var CodeEditor = React.createClass({ + render: function () { + var code = this.aceEditor ? this.aceEditor.getValue() : this.props.code; + var docsLink; + if (this.props.docs) { + docsLink = <a className="help-link" data-bypass="true" href={getDocUrl(this.props.docs)} target="_blank"> + <i className="icon-question-sign"></i> + </a>; + + } + return ( + <div className="control-group"> + <label htmlFor="ace-function"> + {this.props.title} + {docsLink} + </label> + <div className="js-editor" id={this.props.id}>{this.props.code}</div> + <Beautify code={code} beautifiedCode={this.setEditorValue} /> + </div> + ); + }, + + 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(); + }, + + getValue: function () { + return this.aceEditor.getValue(); + }, + + getEditor: function () { + return this.aceEditor; + }, + + componentDidMount: function () { + this.aceEditor = new Components.Editor({ + editorId: this.props.id, + mode: 'javascript', + couchJSHINT: true + }); + this.aceEditor.render(); + }, + + 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; + }, + + componentWillUnmount: function () { + this.aceEditor.remove(); + }, + + }); + + var ReduceEditor = React.createClass({ + + getStoreState: function () { + return { + reduce: indexEditorStore.getReduce(), + reduceOptions: indexEditorStore.reduceOptions(), + reduceSelectedOption: indexEditorStore.reduceSelectedOption(), + hasCustomReduce: indexEditorStore.hasCustomReduce(), + hasReduce: indexEditorStore.hasReduce() + }; + }, + + getInitialState: function () { + return this.getStoreState(); + }, + + getOptionsList: function () { + return _.map(this.state.reduceOptions, function (reduce, i) { + return <option key={i} value={reduce}> {reduce} </option>; + }, this); + + }, + + getReduceValue: function () { + if (!this.state.hasReduce) { + return null; + } + + if (!this.state.hasCustomReduce) { + return this.state.reduce; + } + + return this.refs.reduceEditor.getValue(); + }, + + getEditor: function () { + return this.refs.reduceEditor.getEditor(); + }, + + render: function () { + var reduceOptions = this.getOptionsList(), + customReduceSection; + + if (this.state.hasCustomReduce) { + //customReduceSection = <CustomReduce ref="reduceEditor" reduce={this.state.reduce} />; + customReduceSection = <CodeEditor ref='reduceEditor' id={'reduce-function'} code={this.state.reduce} docs={false} title={'Custom Reduce function'} />; + } + + return ( + <div> + <div className="control-group"> + <label htmlFor="reduce-function-selector">Reduce (optional)<a className="help-link" data-bypass="true" href={getDocUrl('REDUCE_FUNCS')} target="_blank"><i className="icon-question-sign"></i></a></label> + + <select id="reduce-function-selector" value={this.state.reduceSelectedOption} onChange={this.selectChange}> + {reduceOptions} + </select> + </div> + + {customReduceSection} + </div> + ); + }, + + selectChange: function (event) { + Actions.selectReduceChanged(event.target.value); + }, + + onChange: function () { + this.setState(this.getStoreState()); + }, + + componentDidMount: function () { + indexEditorStore.on('change', this.onChange, this); + }, + + componentWillUnmount: function() { + indexEditorStore.off('change', this.onChange); + }, + + }); + + var DeleteView = React.createClass({ + getStoreState: function () { + return { + isNewView: indexEditorStore.isNewView(), + designDocs: indexEditorStore.getDesignDocs(), + viewName: indexEditorStore.getViewName(), + designDocId: indexEditorStore.getDesignDocId(), + database: indexEditorStore.getDatabase() + }; + }, + + getInitialState: function () { + return this.getStoreState(); + }, + + render: function () { + if (this.state.isNewView) { + return null; + } + + return ( + <button onClick={this.deleteView} className="btn btn-danger delete"> + <i className="icon fonticon-cancel-circled"></i> + Delete + </button> + ); + }, + + deleteView: function (event) { + event.preventDefault(); + + if (!confirm('Are you sure you want to delete this view?')) {return;} + + Actions.deleteView({ + designDocs: this.state.designDocs, + viewName: this.state.viewName, + designDocId: this.state.designDocId, + database: this.state.database + }); + } + + }); + + var Editor = React.createClass({ + getStoreState: function () { + return { + database: indexEditorStore.getDatabase(), + isNewView: indexEditorStore.isNewView(), + viewName: indexEditorStore.getViewName(), + designDocs: indexEditorStore.getDesignDocs(), + hasDesignDocChanged: indexEditorStore.hasDesignDocChanged(), + newDesignDoc: indexEditorStore.isNewDesignDoc(), + designDocId: indexEditorStore.getDesignDocId(), + map: indexEditorStore.getMap() + }; + }, + + getInitialState: function () { + return this.getStoreState(); + }, + + onChange: function () { + this.setState(this.getStoreState()); + }, + + componentDidMount: function () { + indexEditorStore.on('change', this.onChange, this); + }, + + componentWillUnmount: function() { + indexEditorStore.off('change', this.onChange); + }, + + hasValidCode: function() { + return _.every(['mapEditor', 'reduceEditor'], function(editorName) { + if (editorName === 'reduceEditor' && !indexEditorStore.hasCustomReduce()) { + return true; + } + var editor = this.refs[editorName].getEditor(); + return editor.hadValidCode(); + }, this); + }, + + saveView: function (event) { + event.preventDefault(); + + if (!this.hasValidCode()) { + FauxtonAPI.addNotification({ + msg: 'Please fix the Javascript errors and try again.', + type: 'error', + clear: true + }); + return; + } + + Actions.saveView({ + database: this.state.database, + newView: this.state.isNewView, + viewName: this.state.viewName, + designDocId: this.state.designDocId, + newDesignDoc: this.state.newDesignDoc, + designDocChanged: this.state.hasDesignDocChanged, + map: this.refs.mapEditor.getValue(), + reduce: this.refs.reduceEditor.getReduceValue(), + designDocs: this.state.designDocs + }); + }, + + viewChange: function (event) { + this.setState({viewName: event.target.value}); + }, + + render: function () { + return ( + <div className="tab-content" > + <div className="tab-pane active" id="index"> + <div id="define-view" className="ddoc-alert well"> + <form className="form-horizontal view-query-save" onSubmit={this.saveView}> + + <DesignDocSelector /> + + <div className="control-group"> + <label htmlFor="index-name">Index name<a className="help-link" data-bypass="true" href={getDocUrl('VIEW_FUNCS')} target="_blank"><i className="icon-question-sign"></i></a></label> + <input type="text" id="index-name" value={this.state.viewName} onChange={this.viewChange} placeholder="Index name" /> + </div> + + <CodeEditor id={'map-function'} ref="mapEditor" title={"Map function"} docs={'MAP_FUNCS'} code={this.state.map}/> + <ReduceEditor ref="reduceEditor"/> + + <div className="control-group"> + <button className="btn btn-success save"><i className="icon fonticon-ok-circled"></i> Save & Build Index</button> + <DeleteView /> + </div> + </form> + </div> + + </div> + </div> + ); + } + }); + + var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; + + var EditorController = React.createClass({ + getInitialState: function () { + return { + showEditor: indexEditorStore.showEditor(), + isNewView: indexEditorStore.isNewView(), + title: indexEditorStore.getTitle(), + hasCustomReduce: indexEditorStore.hasCustomReduce() + }; + }, + + onChange: function () { + this.setState({showEditor: indexEditorStore.showEditor()}); + }, + + componentDidMount: function() { + indexEditorStore.on('change', this.onChange, this); + }, + + componentWillUnmount: function() { + indexEditorStore.off('change', this.onChange); + }, + + toggleEditor: function () { + Actions.toggleEditor(); + }, + + render: function () { + var editor = null; + //a bit of hack for now. + var wrapperClassName = 'editor-wrapper'; + var doTransitions = !this.state.isNewView; + var editorTransitionName = 'fadeInDownNoReduce'; + + if (this.state.showEditor) { + //key is needed for animation; + editor = <Editor key={1} />; + wrapperClassName = ''; + + if (this.state.hasCustomReduce) { + editorTransitionName = 'fadeInDownReduce'; + } + } + + return ( + <div className={wrapperClassName}> + <ToggleButton title={this.state.title} toggleEditor={this.toggleEditor} /> + <ReactCSSTransitionGroup transitionName={editorTransitionName} transitionLeave={doTransitions} transitionEnter={doTransitions}> + {editor} + </ReactCSSTransitionGroup> + </div> + ); + } + + }); + + var Views = { + renderEditor: function (el) { + React.render(<EditorController/>, el); + }, + removeEditor: function (el) { + React.unmountComponentAtNode(el); + }, + ToggleButton: ToggleButton, + ReduceEditor: ReduceEditor, + Editor: Editor, + DesignDocSelector: DesignDocSelector, + Beautify: Beautify + }; + + return Views; +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/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 new file mode 100644 index 0000000..7b9222b --- /dev/null +++ b/app/addons/documents/index-editor/stores.js @@ -0,0 +1,211 @@ +// 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/documents/index-editor/actiontypes' +], + +function(FauxtonAPI, ActionTypes) { + var Stores = {}; + + Stores.IndexEditorStore = FauxtonAPI.Store.extend({ + + defaultMap: 'function(doc) {\n emit(doc._id, 1);\n}', + defaultReduce: 'function(keys, values, rereduce){\n if (rereduce){\n return sum(values);\n } else {\n return values.length;\n }\n}', + + editIndex: function (options) { + this._database = options.database; + this._newView = options.newView; + this._newDesignDoc = options.newDesignDoc || false; + this._viewName = options.viewName || 'viewName'; + this._designDocs = options.designDocs; + this._designDocId = options.designDocId; + this._showEditor = this._newView; + this._designDocChanged = false; + + if (!this._newView && !this._newDesignDoc) { + this._view = this.getDesignDoc().get('views')[this._viewName]; + } else { + this._view = { + reduce: '', + map: '' + }; + } + }, + + getDatabase: function () { + return this._database; + }, + + getMap: function () { + if (this._newView) { + return this.defaultMap; + } + + return this._view.map; + }, + + getReduce: function () { + return this._view.reduce; + }, + + setReduce: function (reduce) { + this._view.reduce = reduce; + }, + + getDesignDoc: function () { + return this._designDocs.find(function (ddoc) { + return this._designDocId == ddoc.id; + }, this).dDocModel(); + + }, + + getDesignDocs: function () { + return this._designDocs; + }, + + getDesignDocId: function () { + return this._designDocId; + }, + + setDesignDocId: function (designDocId, newDesignDoc) { + this._designDocId = designDocId; + this._newDesignDoc = newDesignDoc; + this._designDocChanged = true; + }, + + hasDesignDocChanged: function () { + return this._designDocChanged; + }, + + isNewDesignDoc: function () { + return this._newDesignDoc; + }, + + isNewView: function () { + return this._newView; + }, + + getTitle: function () { + return this._newView ? 'Create Index' : 'Edit Index'; + }, + + getViewName: function () { + return this._viewName; + }, + + showEditor: function () { + return this._showEditor; + }, + + hasCustomReduce: function () { + if (!this.hasReduce()) {return false; } + + return !_.contains(this.builtInReduces(), this.getReduce()); + }, + + hasReduce: function () { + if (!this.getReduce()) { return false; } + + return true; + }, + + builtInReduces: function () { + return ['_sum', '_count', '_stats']; + }, + + reduceSelectedOption: function () { + if (!this.hasReduce()) { + return 'NONE'; + } + + if (this.hasCustomReduce()) { + return 'CUSTOM'; + } + + return this.getReduce(); + }, + + reduceOptions: function () { + return this.builtInReduces().concat(['CUSTOM', 'NONE']); + }, + + updateReduceFromSelect: function (selectedReduce) { + if (selectedReduce === 'NONE') { + this.setReduce(null); + return; + } + + if (selectedReduce === 'CUSTOM') { + this.setReduce(this.defaultReduce); + return; + } + + this.setReduce(selectedReduce); + }, + + dispatch: function (action) { + switch(action.type) { + case ActionTypes.EDIT_INDEX: + this.editIndex(action.options); + this.triggerChange(); + break; + + case ActionTypes.EDIT_NEW_INDEX: + this.editIndex(action.options); + this.triggerChange(); + break; + + case ActionTypes.TOGGLE_EDITOR: + this._showEditor = !this._showEditor; + this.triggerChange(); + break; + + case ActionTypes.SELECT_REDUCE_CHANGE: + this.updateReduceFromSelect(action.reduceSelectedOption); + this.triggerChange(); + break; + + case ActionTypes.DESIGN_DOC_CHANGE: + this.setDesignDocId(action.designDocId, action.newDesignDoc); + this.triggerChange(); + break; + + case ActionTypes.NEW_DESIGN_DOC: + this.setDesignDocId('', true); + this.triggerChange(); + break; + + case ActionTypes.VIEW_SAVED: + this.triggerChange(); + break; + + case ActionTypes.VIEW_CREATED: + this.triggerChange(); + break; + + default: + return; + // do nothing + } + } + + }); + + Stores.indexEditorStore = new Stores.IndexEditorStore(); + + Stores.indexEditorStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.indexEditorStore.dispatch); + + return Stores; + +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/app/addons/documents/routes-documents.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js index 5b07b1f..c776a48 100644 --- a/app/addons/documents/routes-documents.js +++ b/app/addons/documents/routes-documents.js @@ -249,14 +249,12 @@ function(app, FauxtonAPI, BaseRoute, Documents, Changes, Index, DocEditor, Datab } }); - this.viewEditor = this.setView("#dashboard-upper-content", new Index.ViewEditor({ - model: this.database, - ddocs: this.designDocs, + this.viewEditor = this.setView("#dashboard-upper-content", new Index.ViewEditorReact({ viewName: viewName, - params: urlParams, newView: false, database: this.database, - ddocInfo: this.ddocInfo(decodeDdoc, this.designDocs, viewName) + designDocs: this.designDocs, + designDocId: "_design/" + decodeDdoc })); this.toolsView && this.toolsView.remove(); @@ -282,8 +280,8 @@ function(app, FauxtonAPI, BaseRoute, Documents, Changes, Index, DocEditor, Datab showQueryOptions: function (urlParams, ddoc, viewName) { var promise = this.designDocs.fetch({reset: true}), - that = this, - hasReduceFunction; + that = this, + hasReduceFunction; promise.then(function(resp) { var design = _.findWhere(that.designDocs.models, {id: '_design/'+ddoc}); @@ -343,19 +341,27 @@ function(app, FauxtonAPI, BaseRoute, Documents, Changes, Index, DocEditor, Datab })); }, - newViewEditor: function (database, designDoc) { + newViewEditor: function (database, _designDoc) { var params = app.getParams(); + var newDesignDoc = true; + var designDoc; + + if (!_.isUndefined(_designDoc)) { + designDoc = "_design/" + _designDoc; + newDesignDoc = false; + } this.footer && this.footer.remove(); this.toolsView && this.toolsView.remove(); this.documentsView && this.documentsView.remove(); - this.viewEditor = this.setView("#dashboard-upper-content", new Index.ViewEditor({ - currentddoc: "_design/" + designDoc || "", - ddocs: this.designDocs, - params: params, + this.viewEditor = this.setView("#dashboard-upper-content", new Index.ViewEditorReact({ + viewName: 'new-view', + newView: true, database: this.database, - newView: true + designDocs: this.designDocs, + designDocId: designDoc, + newDesignDoc: newDesignDoc })); this.sidebar.setSelectedTab("new-view"); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/app/addons/documents/tests/actionsSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/actionsSpec.js b/app/addons/documents/tests/actionsSpec.js new file mode 100644 index 0000000..68b2172 --- /dev/null +++ b/app/addons/documents/tests/actionsSpec.js @@ -0,0 +1,292 @@ +// 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/documents/index-editor/actions', + 'addons/documents/resources', + 'addons/documents/index-editor/actiontypes', + 'testUtils' +], function (FauxtonAPI, Actions, Documents, ActionTypes, testUtils) { + var assert = testUtils.assert; + + FauxtonAPI.router = new FauxtonAPI.Router([]); + + describe('Index Editor Actions', function () { + var database = { + safeID: function () { return 'id';} + }; + + describe('save view', function () { + var designDoc, designDocs; + beforeEach(function () { + designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: 'function () {};', + } + } + }; + + designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: database + }); + }); + + afterEach(function () { + FauxtonAPI.navigate.restore && FauxtonAPI.navigate.restore(); + FauxtonAPI.triggerRouteEvent.restore && FauxtonAPI.triggerRouteEvent.restore(); + }); + + it('shows a notification if no design doc id given', function () { + var spy = sinon.spy(FauxtonAPI, 'addNotification'); + + var viewInfo = { + database: database, + viewName: 'new-doc', + designDocId: undefined, + map: 'map', + reduce: '_sum', + newDesignDoc: true, + newView: true, + designDocs: designDocs + }; + + Actions.saveView(viewInfo); + assert.ok(spy.calledOnce); + FauxtonAPI.addNotification.restore(); + }); + + it('creates new design Doc for new design doc', function () { + var spy = sinon.spy(Actions.helpers, 'createNewDesignDoc'); + + var viewInfo = { + database: database, + viewName: 'new-doc', + designDocId: '_design/test-doc', + map: 'map', + reduce: '_sum', + newDesignDoc: true, + newView: true, + designDocs: designDocs + }; + + Actions.saveView(viewInfo); + assert.ok(spy.calledOnce); + }); + + it('sets the design doc with updated view', function () { + var viewInfo = { + viewName: 'test-view', + designDocId: '_design/test-doc', + map: 'map', + reduce: '_sum', + newDesignDoc: false, + newView: true, + designDocs: designDocs + }; + + Actions.saveView(viewInfo); + + var updatedDesignDoc = designDocs.first().dDocModel(); + assert.equal(updatedDesignDoc.get('views')['test-view'].reduce, '_sum'); + }); + + it('saves doc', function () { + var viewInfo = { + viewName: 'test-view', + designDocId: '_design/test-doc', + map: 'map', + reduce: '_sum', + newDesignDoc: false, + newView: true, + designDocs: designDocs + }; + + var updatedDesignDoc = designDocs.first().dDocModel(); + var spy = sinon.spy(updatedDesignDoc, 'save'); + Actions.saveView(viewInfo); + + assert.ok(spy.calledOnce); + }); + + it('navigates to new url for new view', function () { + var spy = sinon.spy(FauxtonAPI, 'navigate'); + + var viewInfo = { + database: database, + viewName: 'test-view', + designDocId: '_design/test-doc', + map: 'map', + reduce: '_sum', + newDesignDoc: false, + newView: true, + designDocs: designDocs + }; + var designDoc = designDocs.first(); + + designDoc.save = function () { + var promise = $.Deferred(); + promise.resolve(); + return promise; + }; + + Actions.saveView(viewInfo); + assert.ok(spy.calledOnce); + assert.ok(spy.getCall(0).args[0].match(/_view\/test-view/)); + }); + + it('triggers update all docs', function () { + var spy = sinon.spy(FauxtonAPI, 'triggerRouteEvent'); + + var viewInfo = { + viewName: 'test-view', + designDocId: '_design/test-doc', + map: 'map', + reduce: '_sum', + newDesignDoc: false, + newView: false, + designDocs: designDocs + }; + var designDoc = designDocs.first(); + + designDoc.save = function () { + var promise = $.Deferred(); + promise.resolve(); + return promise; + }; + + Actions.saveView(viewInfo); + assert.ok(spy.calledOnce); + assert.equal(spy.getCall(0).args[0], 'updateAllDocs'); + }); + }); + + describe('delete view', function () { + var designDocs, database, designDoc, designDocId, viewName; + beforeEach(function () { + database = { + safeID: function () { return 'safeid';} + }; + + viewName = 'test-view'; + designDocId = '_design/test-doc'; + designDocs = new Documents.AllDocs([{ + _id: designDocId , + views: { + 'test-view': { + map: 'function () {};', + }, + 'test-view2': { + map: 'function () {};', + } + } + }], { + params: { limit: 10 }, + database: database + }); + + designDoc = designDocs.first(); + + }); + + afterEach(function () { + FauxtonAPI.navigate.restore && FauxtonAPI.navigate.restore(); + FauxtonAPI.triggerRouteEvent.restore && FauxtonAPI.triggerRouteEvent.restore(); + }); + + it('removes view from design doc', function () { + + Actions.deleteView({ + viewName: viewName, + designDocId: designDocId, + database: database, + designDocs: designDocs + }); + + assert.ok(_.isUndefined(designDoc.getDdocView(viewName))); + }); + + it('saves design do if has other views', function () { + var spy = sinon.spy(designDoc, 'save'); + + Actions.deleteView({ + viewName: viewName, + designDocId: designDocId, + database: database, + designDocs: designDocs + }); + + assert.ok(spy.calledOnce); + }); + + it('deletes design doc if has no other views', function () { + var spy = sinon.spy(designDoc, 'destroy'); + designDoc.removeDdocView('test-view2'); + + Actions.deleteView({ + viewName: viewName, + designDocId: designDocId, + database: database, + designDocs: designDocs + }); + + assert.ok(spy.calledOnce); + + }); + + it('navigates to all docs', function () { + var spy = sinon.spy(FauxtonAPI, 'navigate'); + + designDoc.save = function () { + var promise = $.Deferred(); + promise.resolve(); + return promise; + }; + + Actions.deleteView({ + viewName: viewName, + designDocId: designDocId, + database: database, + designDocs: designDocs + }); + + + assert.ok(spy.getCall(0).args[0].match(/_all_docs/)); + assert.ok(spy.calledOnce); + }); + + it('triggers design doc reload', function () { + var spy = sinon.spy(FauxtonAPI, 'triggerRouteEvent'); + + designDoc.save = function () { + var promise = $.Deferred(); + promise.resolve(); + return promise; + }; + + Actions.deleteView({ + viewName: viewName, + designDocId: designDocId, + database: database, + designDocs: designDocs + }); + + assert.ok(spy.calledOnce); + assert.equal(spy.getCall(0).args[0], 'reloadDesignDocs'); + }); + + }); + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/app/addons/documents/tests/storesSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/storesSpec.js b/app/addons/documents/tests/storesSpec.js new file mode 100644 index 0000000..5511a8e --- /dev/null +++ b/app/addons/documents/tests/storesSpec.js @@ -0,0 +1,382 @@ +// 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/documents/index-editor/stores', + 'addons/documents/index-editor/actiontypes', + 'addons/documents/resources', + 'testUtils' +], function (FauxtonAPI, Stores, ActionTypes, Documents, testUtils) { + var assert = testUtils.assert; + var store; + var dispatchToken; + + + describe('IndexEditorStore', function () { + + beforeEach(function () { + store = new Stores.IndexEditorStore(); + dispatchToken = FauxtonAPI.dispatcher.register(store.dispatch); + }); + + afterEach(function () { + FauxtonAPI.dispatcher.unregister(dispatchToken); + }); + + describe('TOGGLE EDITOR', function () { + + it('toggles editor', function () { + var designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: 'boom' + } + } + }; + + var designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: function () { return 'id';} + } + }); + + FauxtonAPI.dispatch({ + type: ActionTypes.EDIT_NEW_INDEX, + options: { + newView: false, + designDocs: designDocs, + designDocId: '_design/test-doc' + } + }); + + + FauxtonAPI.dispatch({ + type: ActionTypes.TOGGLE_EDITOR + }); + + assert.ok(store.showEditor()); + }); + + }); + + describe('map editor', function () { + + describe('new view', function () { + + beforeEach(function () { + + FauxtonAPI.dispatch({ + type: ActionTypes.EDIT_NEW_INDEX, + options: { + newView: true + } + }); + }); + + it('returns default map', function () { + assert.equal(store.getMap(), 'function(doc) {\n emit(doc._id, 1);\n}'); + }); + + it('Edit Index as title', function () { + assert.equal(store.getTitle(), 'Create Index'); + }); + }); + + }); + + describe('reduce editor', function () { + + describe('has custom reduce', function () { + + it('is false for no reduce', function () { + var designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: 'function () {};' + } + } + }; + + var designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: function () { return 'id';} + } + }); + + FauxtonAPI.dispatch({ + type: ActionTypes.EDIT_NEW_INDEX, + options: { + newView: false, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDoc._id + } + }); + + assert.notOk(store.hasCustomReduce()); + }); + + it('is false for built in reduce', function () { + var designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: 'function () {};', + reduce: '_sum' + } + } + }; + + var designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: function () { return 'id';} + } + }); + FauxtonAPI.dispatch({ + type: ActionTypes.EDIT_NEW_INDEX, + options: { + newView: false, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDoc._id + } + }); + + assert.notOk(store.hasCustomReduce()); + }); + + it('is true for custom reduce', function () { + var designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: 'function () {};', + reduce: 'function (reduce) { reduce(); }' + } + } + }; + + var designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: function () { return 'id';} + } + }); + + FauxtonAPI.dispatch({ + type: ActionTypes.EDIT_NEW_INDEX, + options: { + newView: false, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDoc._id + } + }); + + assert.ok(store.hasCustomReduce()); + }); + + }); + + //show default reduce + describe('SELECT_REDUCE_CHANGE', function () { + + beforeEach(function () { + var designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: 'function () {};' + } + } + }; + + var designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: function () { return 'id';} + } + }); + + FauxtonAPI.dispatch({ + type: ActionTypes.EDIT_NEW_INDEX, + options: { + newView: false, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDoc._id + } + }); + }); + + it('NONE returns null reduce', function () { + FauxtonAPI.dispatch({ + type: ActionTypes.SELECT_REDUCE_CHANGE, + reduceSelectedOption: 'NONE' + }); + assert.ok(_.isNull(store.getReduce())); + }); + + it('builtin returns bultin reduce', function () { + FauxtonAPI.dispatch({ + type: ActionTypes.SELECT_REDUCE_CHANGE, + reduceSelectedOption: '_sum' + }); + assert.equal(store.getReduce(), '_sum'); + }); + + it('custom returns custom reduce', function () { + FauxtonAPI.dispatch({ + type: ActionTypes.SELECT_REDUCE_CHANGE, + reduceSelectedOption: 'CUSTOM' + }); + assert.equal(store.getReduce(), 'function(keys, values, rereduce){\n if (rereduce){\n return sum(values);\n } else {\n return values.length;\n }\n}'); + }); + }); + }); + + + describe('design doc selector', function () { + var designDoc; + + beforeEach(function () { + designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: 'boom' + } + } + }; + + var designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: function () { return 'id';} + } + }); + + FauxtonAPI.dispatch({ + type: ActionTypes.EDIT_INDEX, + options: { + newView: false, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDoc._id + } + }); + }); + + it('DESIGN_DOC_CHANGE changes design doc id', function () { + var designDocId = 'another-one'; + FauxtonAPI.dispatch({ + type: ActionTypes.DESIGN_DOC_CHANGE, + designDocId: designDocId, + newDesignDoc: false + }); + + assert.equal(store.getDesignDocId(), designDocId); + assert.notOk(store.isNewDesignDoc()); + }); + + it('sets new design doc on NEW_DESIGN_DOC', function () { + FauxtonAPI.dispatch({ + type: ActionTypes.NEW_DESIGN_DOC + }); + + assert.ok(store.isNewDesignDoc()); + assert.equal(store.getDesignDocId(), ''); + }); + }); + + describe('EDIT_INDEX', function () { + var designDoc, designDocs; + + beforeEach(function () { + designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: 'boom' + } + } + }; + + designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: function () { return 'id';} + } + }); + + }); + + it('can set reduce for new design doc', function () { + FauxtonAPI.dispatch({ + type: ActionTypes.EDIT_INDEX, + options: { + newView: true, + newDesignDoc: true, + viewName: 'test-view', + designDocs: designDocs, + designDocId: undefined + } + }); + + FauxtonAPI.dispatch({ + type: ActionTypes.SELECT_REDUCE_CHANGE, + reduceSelectedOption: '_sum' + }); + + assert.equal(store.getReduce(), '_sum'); + }); + + it('showEditor() is false for editing index', function () { + FauxtonAPI.dispatch({ + type: ActionTypes.EDIT_INDEX, + options: { + newView: false, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDoc._id + } + }); + + assert.notOk(store.showEditor()); + }); + + it('showEditor() is true for creating index', function () { + FauxtonAPI.dispatch({ + type: ActionTypes.EDIT_INDEX, + options: { + newView: true, + viewName: 'test-view', + newDesignDoc: false, + designDocs: designDocs, + designDocId: designDoc._id + } + }); + + assert.ok(store.showEditor()); + }); + + }); + + }); +}); + http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/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 new file mode 100644 index 0000000..4f6cb14 --- /dev/null +++ b/app/addons/documents/tests/viewIndex.componentsSpec.react.jsx @@ -0,0 +1,252 @@ +// 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/documents/index-editor/components.react', + 'addons/documents/index-editor/stores', + 'addons/documents/index-editor/actions', + 'addons/documents/resources', + 'testUtils', + "react" +], function (FauxtonAPI, Views, Stores, Actions, Documents, utils, React) { + FauxtonAPI.router = new FauxtonAPI.Router([]); + + var assert = utils.assert; + var TestUtils = React.addons.TestUtils; + + var resetStore = function (designDoc) { + var designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: function () { return 'id';} + } + }); + + Actions.editIndex({ + newView: false, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDoc._id + }); + }; + + describe('View editor', function () { + + describe('Toggle button', function () { + var container, toggleEl, toggleEditor; + + beforeEach(function () { + toggleEditor = sinon.spy(); + container = document.createElement('div'); + toggleEl = TestUtils.renderIntoDocument(<Views.ToggleButton toggleEditor={toggleEditor} />, container); + }); + + afterEach(function () { + React.unmountComponentAtNode(container); + }); + + + it('should toggle editor on click', function () { + TestUtils.Simulate.click($(toggleEl.getDOMNode()).find('a')[0]); + assert.ok(toggleEditor.calledOnce); + }); + + }); + + }); + + describe('reduce editor', function () { + var container, reduceEl; + + beforeEach(function () { + container = document.createElement('div'); + }); + + afterEach(function () { + React.unmountComponentAtNode(container); + }); + + describe('getReduceValue', function () { + var container; + + beforeEach(function () { + container = document.createElement('div'); + $('body').append('<div id="reduce-function"></div>'); + }); + + it('returns null for none', function () { + var store = Stores.indexEditorStore; + + var designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: 'function () {};', + //reduce: 'function (reduce) { reduce(); }' + } + } + }; + + resetStore(designDoc); + + reduceEl = TestUtils.renderIntoDocument(<Views.ReduceEditor/>, container); + assert.ok(_.isNull(reduceEl.getReduceValue())); + }); + + it('returns built in for built in reduce', function () { + var store = Stores.indexEditorStore; + + var designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: 'function () {};', + reduce: '_sum' + } + } + }; + + resetStore(designDoc); + + reduceEl = TestUtils.renderIntoDocument(<Views.ReduceEditor/>, container); + assert.equal(reduceEl.getReduceValue(), '_sum'); + }); + + }); + }); + + describe('design Doc Selector', function () { + var container, selectorEl; + + beforeEach(function () { + container = document.createElement('div'); + $('body').append('<div id="map-function"></div>'); + $('body').append('<div id="editor"></div>'); + selectorEl = TestUtils.renderIntoDocument(<Views.DesignDocSelector/>, container); + }); + + + afterEach(function () { + Actions.newDesignDoc.restore && Actions.newDesignDoc.restore(); + Actions.designDocChange.restore && Actions.designDocChange.restore(); + React.unmountComponentAtNode(container); + }); + + it('calls new design doc on new selected', function () { + var spy = sinon.spy(Actions, 'newDesignDoc'); + TestUtils.Simulate.change($(selectorEl.getDOMNode()).find('#ddoc')[0], { + target: { + value: 'new' + } + }); + + assert.ok(spy.calledOnce); + }); + + it('calls design doc changed on a different design doc selected', function () { + var spy = sinon.spy(Actions, 'designDocChange'); + TestUtils.Simulate.change($(selectorEl.getDOMNode()).find('#ddoc')[0], { + target: { + value: 'another-doc' + } + }); + + assert.ok(spy.calledWith('another-doc', false)); + }); + + it('calls design doc changed on new design doc entered', function () { + var spy = sinon.spy(Actions, 'designDocChange'); + Actions.newDesignDoc(); + TestUtils.Simulate.change($(selectorEl.getDOMNode()).find('#new-ddoc')[0], { + target: { + value: 'new-doc-entered' + } + }); + + assert.ok(spy.calledWith('_design/new-doc-entered', true)); + }); + + }); + + describe('Editor', function () { + var container, editorEl, reduceStub; + + beforeEach(function () { + container = document.createElement('div'); + $('body').append('<div id="map-function"></div>'); + $('body').append('<div id="editor"></div>'); + editorEl = TestUtils.renderIntoDocument(<Views.Editor/>, container); + }); + + afterEach(function () { + React.unmountComponentAtNode(container); + }); + + it('returns false on invalid map editor code', function () { + var stub = sinon.stub(editorEl.refs.mapEditor.aceEditor, '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'); + stub.returns(true); + assert.ok(editorEl.hasValidCode()); + }); + + it('returns true on non-custom reduce', function () { + var stub = sinon.stub(Stores.indexEditorStore, 'hasCustomReduce'); + stub.returns(false); + assert.ok(editorEl.hasValidCode()); + }); + + }); + + describe('Beautify', function () { + var container, beautifyEl, reduceStub; + + beforeEach(function () { + container = document.createElement('div'); + }); + + afterEach(function () { + React.unmountComponentAtNode(container); + }); + + it('should be empty for multi-lined code', function () { + var correctCode = 'function() {\n console.log("hello");\n}'; + beautifyEl = TestUtils.renderIntoDocument(<Views.Beautify code={correctCode}/>, container); + assert.ok(_.isNull(beautifyEl.getDOMNode())); + }); + + it('should have button to beautify for single line code', function () { + var badCode = 'function () { console.log("hello"); }'; + beautifyEl = TestUtils.renderIntoDocument(<Views.Beautify code={badCode}/>, container); + assert.ok($(beautifyEl.getDOMNode()).hasClass('beautify')); + }); + + it('on click beautifies code', function () { + var fixedCode; + var correctCode = 'function() {\n console.log("hello");\n}'; + + var beautifiedCode = function (code) { + fixedCode = code; + }; + + beautifyEl = TestUtils.renderIntoDocument(<Views.Beautify beautifiedCode={beautifiedCode} code={'function () { console.log("hello"); }'} noOfLines={1}/>, container); + TestUtils.Simulate.click(beautifyEl.getDOMNode()); + assert.equal(fixedCode, correctCode); + + }); + + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/app/addons/documents/tests/viewsSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/viewsSpec.js b/app/addons/documents/tests/viewsSpec.js index 3d0a008..5e416bb 100644 --- a/app/addons/documents/tests/viewsSpec.js +++ b/app/addons/documents/tests/viewsSpec.js @@ -20,7 +20,7 @@ define([ ViewSandbox = testUtils.ViewSandbox, viewSandbox; - describe('AllDocsList', function () { + /* describe('AllDocsList', function () { var database = new Databases.Model({id: 'registry'}), bulkDeleteDocCollection = new Resources.BulkDeleteDocCollection([], {databaseId: 'registry'}); @@ -69,5 +69,5 @@ define([ view.$('button.js-all').trigger('click'); assert.equal(bulkDeleteDocCollection.length, 1); }); - }); + });*/ }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/16ec9ab4/app/addons/documents/views-index.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/views-index.js b/app/addons/documents/views-index.js index 0d319e1..eb24944 100644 --- a/app/addons/documents/views-index.js +++ b/app/addons/documents/views-index.js @@ -11,533 +11,27 @@ // the License. define([ - "app", "api", - "addons/fauxton/components", - "addons/documents/resources", - "addons/databases/resources", - "addons/pouchdb/base", - - //views - "addons/documents/views-queryoptions", - - // Plugins - "plugins/beautify", - "plugins/prettify" + "addons/documents/index-editor/components.react", + "addons/documents/index-editor/actions", ], -function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, QueryOptions, beautify) { +function(FauxtonAPI, ViewEditor, Actions) { var Views = {}; - Views.ViewEditor = FauxtonAPI.View.extend({ - template: "addons/documents/templates/view_editor", - builtinReduces: ['_sum', '_count', '_stats'], - - events: { - "click button.save": "saveView", - "click button.delete": "deleteView", - "change select#reduce-function-selector": "updateReduce", - "click button.preview": "previewView", - "click #db-views-tabs-nav": 'toggleIndexNav', - "click .beautify_map": "beautifyCode", - "click .beautify_reduce": "beautifyCode", - "click #query-options-wrapper": 'toggleIndexNav' - }, - - langTemplates: { - "javascript": { - map: "function(doc) {\n emit(doc._id, 1);\n}", - reduce: "function(keys, values, rereduce){\n if (rereduce){\n return sum(values);\n } else {\n return values.length;\n }\n}" - } - }, - - defaultLang: "javascript", - rendered: false, - - initialize: function(options) { - this.rightHeader = options.rightHeader; - this.newView = options.newView || false; - this.ddocs = options.ddocs; - this.params = options.params; - this.database = options.database; - this.currentDdoc = options.currentddoc; - - if (this.newView) { - this.viewName = 'newView'; - } else { - this.ddocID = options.ddocInfo.id; - this.viewName = options.viewName; - this.ddocInfo = new Documents.DdocInfo({_id: this.ddocID},{database: this.database}); - } - - this.showIndex = false; - _.bindAll(this); - }, - - establish: function () { - if (this.ddocInfo) { - return this.ddocInfo.fetch(); - } - }, - - updateValues: function() { - var notification; - if (this.model.changedAttributes()) { - notification = FauxtonAPI.addNotification({ - msg: "Document saved successfully.", - type: "success", - clear: true - }); - this.editor.setValue(this.model.prettyJSON()); - } - }, - - updateReduce: function(event) { - var $ele = $("#reduce-function-selector"); - var $reduceContainer = $(".control-group.reduce-function"); - if ($ele.val() == "CUSTOM") { - this.createReduceEditor(); - this.reduceEditor.setValue(this.langTemplates.javascript.reduce); - $reduceContainer.show(); - } else { - $reduceContainer.hide(); - } - }, - - deleteView: function (event) { - event.preventDefault(); - - if (this.newView) { return alert('Cannot delete a new view.'); } - if (!confirm('Are you sure you want to delete this view?')) {return;} - - var that = this, - promise, - viewName = this.$('#index-name').val(), - ddocName = this.$('#ddoc :selected').val(), - ddoc = this.getCurrentDesignDoc(); - - ddoc.removeDdocView(viewName); - - if (ddoc.hasViews()) { - promise = ddoc.save(); - } else { - promise = ddoc.destroy(); - } - - promise.then(function () { - FauxtonAPI.navigate('/database/' + that.database.safeID() + '/_all_docs?limit=' + Databases.DocLimit); - FauxtonAPI.triggerRouteEvent('reloadDesignDocs'); - }); - }, - - saveView: function(event) { - var notification, - that = this; - - if (event) { event.preventDefault(); } - - $('#dashboard-content').scrollTop(0); //scroll up - - if (this.hasValidCode() && this.$('#new-ddoc:visible').val() !== "") { - var mapVal = this.mapEditor.getValue(), - reduceVal = this.reduceVal(), - viewName = this.$('#index-name').val(), - ddoc = this.getCurrentDesignDoc(), - ddocName = ddoc.id, - viewNameChange = false; - - if (this.viewName !== viewName) { - ddoc.removeDdocView(this.viewName); - this.viewName = viewName; - viewNameChange = true; - } - - notification = FauxtonAPI.addNotification({ - msg: "Saving document.", - clear: true - }); - - ddoc.setDdocView(viewName, mapVal, reduceVal); - - ddoc.save().then(function () { - that.ddocs.add(ddoc); - that.mapEditor.editSaved(); - that.reduceEditor && that.reduceEditor.editSaved(); - - FauxtonAPI.addNotification({ - msg: "View has been saved.", - type: "success", - clear: true - }); - - if (that.newView || viewNameChange) { - var fragment = '/database/' + that.database.safeID() +'/' + ddoc.safeID() + '/_view/' + app.utils.safeURLName(viewName); - - FauxtonAPI.navigate(fragment); - that.newView = false; - that.ddocID = ddoc.safeID(); - that.viewName = viewName; - that.ddocInfo = ddoc; - that.showIndex = true; - that.currentDdoc = ddoc; - that.render(); - FauxtonAPI.triggerRouteEvent('reloadDesignDocs', { - selectedTab: app.utils.removeSpecialCharacters(ddocName.replace(/_design\//,'')) + '_' + app.utils.removeSpecialCharacters(viewName) - }); - } - - if (that.reduceFunStr !== reduceVal) { - that.reduceFunStr = reduceVal; - FauxtonAPI.triggerRouteEvent("updateQueryOptions", { hasReduce: that.hasReduce() }); - } - - FauxtonAPI.triggerRouteEvent('updateAllDocs', {ddoc: ddocName, view: viewName}); - - }, function(xhr) { - var responseText = JSON.parse(xhr.responseText).reason; - notification = FauxtonAPI.addNotification({ - msg: "Save failed: " + responseText, - type: "error", - clear: true - }); - }); - } else { - var errormessage = (this.$('#new-ddoc:visible').val() ==="")?"Enter a design doc name":"Please fix the Javascript errors and try again."; - notification = FauxtonAPI.addNotification({ - msg: errormessage, - type: "error", - clear: true - }); - } - }, - - updateView: function(event, paramInfo) { - event.preventDefault(); - - if (this.newView) { return alert('Please save this new view before querying it.'); } - - var errorParams = paramInfo.errorParams, - params = paramInfo.params; - - if (_.any(errorParams)) { - _.map(errorParams, function(param) { - return FauxtonAPI.addNotification({ - msg: 'JSON Parse Error on field: ' + param.name, - type: 'error', - clear: true - }); - }); - FauxtonAPI.addNotification({ - msg: "Make sure that strings are properly quoted and any other values are valid JSON structures", - type: "warning", - clear: true - }); - - return false; - } - - var url = app.utils.replaceQueryParams(params); - FauxtonAPI.navigate(url, {trigger: false}); - FauxtonAPI.triggerRouteEvent('updateAllDocs', {ddoc: this.ddocID, view: this.viewName}); - }, - - - previewView: function(event, paramsInfo) { - event.preventDefault(); - var that = this, - mapVal = this.mapVal(), - reduceVal = this.reduceVal(), - paramsArr = []; - - if (paramsInfo && paramsInfo.params) { - paramsArr = paramsInfo.params; - } - - var params = _.reduce(paramsArr, function (params, param) { - params[param.name] = param.value; - return params; - }, {reduce: false}); - - FauxtonAPI.addNotification({ - msg: "<strong>Warning!</strong> Preview executes the Map/Reduce functions in your browser, and may behave differently from CouchDB.", - type: "warning", - escape: false // beware of possible XSS when the message changes - }); - - var promise = FauxtonAPI.Deferred(); - - if (!this.database.allDocs || this.database.allDocs.params.include_docs !== true) { - this.database.buildAllDocs({limit: Databases.DocLimit.toString(), include_docs: true}); - promise = this.database.allDocs.fetch(); - } else { - promise.resolve(); - } - - promise.then(function () { - params.docs = that.database.allDocs.map(function (model) { return model.get('doc');}); - var queryPromise = pouchdb.runViewQuery({map: mapVal, reduce: reduceVal}, params); - queryPromise.then(function (results) { - FauxtonAPI.triggerRouteEvent('updatePreviewDocs', {rows: results.rows, ddoc: that.getCurrentDesignDoc().id, view: that.viewName}); - }); - }); - }, - - getCurrentDesignDoc: function () { - return this.designDocSelector.getCurrentDesignDoc(); - }, - - isCustomReduceEnabled: function() { - return $("#reduce-function-selector").val() == "CUSTOM"; - }, - - mapVal: function () { - if (this.mapEditor) { - return this.mapEditor.getValue(); - } - return this.$('#map-function').text(); - }, - - reduceVal: function() { - var reduceOption = this.$('#reduce-function-selector :selected').val(), - reduceVal = ""; - - if (reduceOption === 'CUSTOM') { - if (!this.reduceEditor) { this.createReduceEditor(); } - reduceVal = this.reduceEditor.getValue(); - } else if ( reduceOption !== 'NONE') { - reduceVal = reduceOption; - } - - return reduceVal; - }, - - - hasValidCode: function() { - return _.every(["mapEditor", "reduceEditor"], function(editorName) { - var editor = this[editorName]; - if (editorName === "reduceEditor" && ! this.isCustomReduceEnabled()) { - return true; - } - return editor.hadValidCode(); - }, this); - }, - - toggleIndexNav: function (event) { - $('#dashboard-content').scrollTop(0); //scroll up - - var $targetId = this.$(event.target).attr('id'), - $previousTab = this.$(this.$('li.active a').attr('href')), - $targetTab = this.$(this.$(event.target).attr('href')); - - if ($targetTab.attr('id') !== $previousTab.attr('id')) { - $previousTab.removeAttr('style'); - } - - if ($targetId === 'index-nav') { - if (this.newView) { return; } - var that = this; - $('#dashboard-content').scrollTop(0); //scroll up - $targetTab.toggle('slow', function(){ - that.showEditors(); - }); - } else { - $targetTab.toggle('slow'); - } - }, - - serialize: function() { - return { - ddocs: this.ddocs, - ddoc: this.model, - ddocName: this.model.id, - viewName: this.viewName, - reduceFunStr: this.reduceFunStr, - isCustomReduce: this.hasCustomReduce(), - newView: this.newView, - langTemplates: this.langTemplates.javascript - }; - }, - - hasCustomReduce: function() { - return this.reduceFunStr && ! _.contains(this.builtinReduces, this.reduceFunStr); - }, - - hasReduce: function () { - return this.reduceFunStr || false; - }, - - createReduceEditor: function () { - if (this.reduceEditor) { - this.reduceEditor.remove(); - } - - this.reduceEditor = new Components.Editor({ - editorId: "reduce-function", - mode: "javascript", - couchJSHINT: true - }); - this.reduceEditor.render(); - - if (this.reduceEditor.getLines() === 1){ - this.$('.beautify_reduce').removeClass("hide"); - $('.beautify-tooltip').tooltip(); - } - }, - - beforeRender: function () { - if (this.newView) { - this.reduceFunStr = ''; - if (this.ddocs.length === 0) { - this.model = new Documents.Doc(null, {database: this.database}); - } else { - this.model = this.ddocs.first().dDocModel(); - } - this.ddocID = this.model.id; - } else { - var ddocDecode = decodeURIComponent(this.ddocID); - this.model = this.ddocs.get(this.ddocID).dDocModel(); - this.reduceFunStr = this.model.viewHasReduce(this.viewName); - } - - var viewFilters = FauxtonAPI.getExtensions('sidebar:viewFilters'), - filteredModels = this.ddocs.models, - designDocs = this.ddocs; - - if (!_.isEmpty(viewFilters)) { - _.each(viewFilters, function (filter) { - filteredModels = _.filter(filteredModels, filter); - }); - designDocs.reset(filteredModels, {silent: true}); - } - - if (!this.designDocSelector) { - this.designDocSelector = this.setView('.design-doc-group', new Views.DesignDocSelector({ - collection: designDocs, - ddocName: this.currentDdoc || this.model.id, - database: this.database - })); - } - - // if this isn't a new View, add in whatever extensions have been associated with this location - if (!this.newView) { - var buttonViews = FauxtonAPI.getExtensions('ViewEditor:ButtonRow'); - _.each(buttonViews, function (view) { - this.insertView("#viewBtnExtensions", view); - view.update(this.database, this.ddocInfo.safeID(), this.viewName); - }, this); - } - }, - - afterRender: function() { - - this.designDocSelector.updateDesignDoc(); - if (this.newView || this.showIndex) { - this.showEditors(); - this.showIndex = false; - } else { - this.$('#index').hide(); - this.$('#index-nav').parent().removeClass('active'); - } - - }, - - showEditors: function () { - this.mapEditor = new Components.Editor({ - editorId: "map-function", - mode: "javascript", - couchJSHINT: true - }); - this.mapEditor.render(); - - if (this.hasCustomReduce()) { - this.createReduceEditor(); - } else { - $(".control-group.reduce-function").hide(); - } - - if (this.newView) { - this.mapEditor.setValue(this.langTemplates[this.defaultLang].map); - //Use a built in view by default - //this.reduceEditor.setValue(this.langTemplates[this.defaultLang].reduce); - } - - this.mapEditor.editSaved(); - this.reduceEditor && this.reduceEditor.editSaved(); - - if (this.mapEditor.getLines() === 1){ - this.$('.beautify_map').removeClass("hide"); - $('.beautify-tooltip').tooltip(); - } - }, - beautifyCode: function(e){ - e.preventDefault(); - var targetEditor = $(e.currentTarget).hasClass('beautify_reduce')?this.reduceEditor:this.mapEditor; - var beautifiedCode = beautify(targetEditor.getValue()); - targetEditor.setValue(beautifiedCode); - }, - cleanup: function () { - this.mapEditor && this.mapEditor.remove(); - this.reduceEditor && this.reduceEditor.remove(); - } - }); - - Views.DesignDocSelector = FauxtonAPI.View.extend({ - template: "addons/documents/templates/design_doc_selector", - - events: { - "change select#ddoc": "updateDesignDoc" - }, - + Views.ViewEditorReact = FauxtonAPI.View.extend({ initialize: function (options) { - this.ddocName = options.ddocName; - this.database = options.database; - this.listenTo(this.collection, 'add', this.ddocAdded); - this.DocModel = options.DocModel || Documents.Doc; - }, - - ddocAdded: function (ddoc) { - this.ddocName = ddoc.id; - }, - - serialize: function () { - return { - ddocName: this.ddocName, - ddocs: this.collection - }; - }, - - updateDesignDoc: function () { - if (this.newDesignDoc()) { - this.$('#new-ddoc-section').show(); - } else { - this.$('#new-ddoc-section').hide(); - } + this.options = options; }, - newDesignDoc: function () { - return this.$('#ddoc').val() === 'new-doc'; + afterRender: function () { + Actions.editIndex(this.options); + ViewEditor.renderEditor(this.el); }, - newDocValidation: function(){ - return this.newDesignDoc() && this.$('#new-ddoc').val()===""; - }, - - getCurrentDesignDoc: function () { - if (this.newDesignDoc()) { - var doc = { - _id: '_design/' + this.$('#new-ddoc').val(), - views: {}, - language: "javascript" - }; - var ddoc = new this.DocModel(doc, {database: this.database}); - return ddoc; - } - - var ddocName = this.$('#ddoc').val(); - return this.collection.find(function (ddoc) { - return ddoc.id === ddocName; - }).dDocModel(); + cleanup: function () { + ViewEditor.removeEditor(this.el); } });
