diff options
author | Russell Branca <chewbranca@gmail.com> | 2013-02-05 17:09:48 -0800 |
---|---|---|
committer | Russell Branca <chewbranca@gmail.com> | 2013-03-15 14:35:12 -0700 |
commit | b76c028817474e8aa4767d683bac6e7a0f2c7168 (patch) | |
tree | 56088475808e4b6e805c19bb27e384a9c663c32d | |
parent | 206fd49ab6f733f4a7f3b5397ccef7e6f4b6f5fa (diff) | |
download | couchdb-b76c028817474e8aa4767d683bac6e7a0f2c7168.tar.gz |
Initial view editor functionality
-rw-r--r-- | src/fauxton/app/api.js | 10 | ||||
-rw-r--r-- | src/fauxton/app/modules/documents/routes.js | 42 | ||||
-rw-r--r-- | src/fauxton/app/modules/documents/views.js | 197 | ||||
-rw-r--r-- | src/fauxton/app/templates/documents/sidebar.html | 4 | ||||
-rw-r--r-- | src/fauxton/app/templates/documents/view_editor.html | 65 | ||||
-rw-r--r-- | src/fauxton/assets/css/cloudant-additions.css | 8 | ||||
-rw-r--r-- | src/fauxton/assets/less/cloudant.less | 22 |
7 files changed, 322 insertions, 26 deletions
diff --git a/src/fauxton/app/api.js b/src/fauxton/app/api.js index 7aebd372f..9ec2894df 100644 --- a/src/fauxton/app/api.js +++ b/src/fauxton/app/api.js @@ -17,6 +17,16 @@ function(app, Fauxton) { initialize: function() {} }; + // List of JSHINT errors to ignore + // Gets around problem of anonymous functions not being a valid statement + FauxtonAPI.excludedViewErrors = [ + "Missing name in function declaration." + ]; + + FauxtonAPI.isIgnorableError = function(msg) { + return _.contains(FauxtonAPI.excludedViewErrors, msg); + }; + FauxtonAPI.View = Backbone.View.extend({ // This should return an array of promises, an empty array, or null establish: function() { diff --git a/src/fauxton/app/modules/documents/routes.js b/src/fauxton/app/modules/documents/routes.js index dfa208f4f..ab6667073 100644 --- a/src/fauxton/app/modules/documents/routes.js +++ b/src/fauxton/app/modules/documents/routes.js @@ -59,6 +59,47 @@ function(app, FauxtonAPI, Documents, Databases) { }; }; + var newViewEditorCallback = function(databaseName) { + var data = { + database: new Databases.Model({id:databaseName}) + }; + data.designDocs = new Documents.AllDocs(null, { + database: data.database, + params: {startkey: '"_design"', + endkey: '"_design1"', + include_docs: true} + }); + + return { + layout: "with_tabs_sidebar", + + data: data, + + crumbs: [ + {"name": "Databases", "link": "/_all_dbs"}, + {"name": data.database.id, "link": Databases.databaseUrl(data.database)} + ], + + views: { + "#sidebar-content": new Documents.Views.Sidebar({ + collection: data.designDocs + }), + + "#tabs": new Documents.Views.Tabs({ + collection: data.designDocs, + database: data.database.id + }), + + "#dashboard-content": new Documents.Views.ViewEditor({ + model: data.database, + ddocs: data.designDocs + }) + }, + + apiUrl: data.database.url() + }; + }; + // HACK: this kind of works // Basically need a way to share state between different routes, for // instance making a new doc won't work for switching back and forth @@ -295,6 +336,7 @@ function(app, FauxtonAPI, Documents, Databases) { }, "database/:database/new": newDocCodeEditorCallback, + "database/:database/new_view": newViewEditorCallback, // TODO: fix optional search params // Can't get ":view(?*search)" to work diff --git a/src/fauxton/app/modules/documents/views.js b/src/fauxton/app/modules/documents/views.js index 0d6eb7e6f..d41d53ec5 100644 --- a/src/fauxton/app/modules/documents/views.js +++ b/src/fauxton/app/modules/documents/views.js @@ -507,16 +507,14 @@ function(app, FauxtonAPI, Codemirror, JSHint) { json = JSON.parse(this.editor.getValue()); this.model.set(json); notification = FauxtonAPI.addNotification({msg: "Saving document."}); - this.model.save().error( - function(xhr) { - var responseText = JSON.parse(xhr.responseText).reason; - notification = FauxtonAPI.addNotification({ - msg: "Save failed: " + responseText, - type: "error", - clear: true - }); - } - ); + this.model.save().error(function(xhr) { + var responseText = JSON.parse(xhr.responseText).reason; + notification = FauxtonAPI.addNotification({ + msg: "Save failed: " + responseText, + type: "error", + clear: true + }); + }); } else { notification = FauxtonAPI.addNotification({ msg: "Please fix the JSON errors and try again.", @@ -609,11 +607,183 @@ function(app, FauxtonAPI, Codemirror, JSHint) { } }); + Views.ViewEditor = FauxtonAPI.View.extend({ + template: "templates/documents/view_editor", + + events: { + "click button.save": "saveView", + "change select#reduce-function-selector": "updateReduce" + }, + + langTemplates: { + "javascript": { + map: "function(doc) {\n emit(null, doc);\n}", + reduce: "function(keys, values, rereduce){\n if (rereduce){\n return sum(values);\n } else {\n return values.length;\n }\n}" + } + }, + + defaultLang: "javascript", + + initialize: function(options) { + this.ddocs = options.ddocs; + }, + + 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") { + $reduceContainer.show(); + } else { + $reduceContainer.hide(); + } + }, + + establish: function() { + return [this.ddocs.fetch(), this.model.fetch()]; + }, + + saveView: function(event) { + var json, notification; + if (this.hasValidCode()) { + var mapVal = this.mapEditor.getValue(); + var reduceVal = this.reduceEditor.getValue(); + notification = FauxtonAPI.addNotification({ + msg: "Saving document.", + selector: "#define-view .errors-container" + }); + /* + this.model.save().error(function(xhr) { + var responseText = JSON.parse(xhr.responseText).reason; + notification = FauxtonAPI.addNotification({ + msg: "Save failed: " + responseText, + type: "error", + clear: true + }); + }); + */ + } else { + notification = FauxtonAPI.addNotification({ + msg: "Please fix the JSON errors and try again.", + type: "error", + selector: "#define-view .errors-container" + }); + } + }, + + isCustomReduceEnabled: function() { + return $("#reduce-function-selector").val() == "CUSTOM"; + }, + + reduceVal: function() { + }, + + hasValidCode: function() { + return _.every(["mapEditor", "reduceEditor"], function(editorName) { + var editor = this[editorName]; + if (editorName == "reduceEditor" && ! this.isCustomReduceEnabled()) { + return true; + } else if (JSHINT(editor.getValue()) !== false) { + return true; + } else { + // By default CouchDB view functions don't pass lint + return _.every(JSHINT.errors, function(error) { + return FauxtonAPI.isIgnorableError(error.reason); + }); + } + }, this); + }, + + runJSHint: function(editorName) { + var editor = this[editorName]; + var json = editor.getValue(); + var output = JSHint(json); + + // Clear existing markers + for (var i = 0, l = editor.lineCount(); i < l; i++) { + editor.clearMarker(i); + } + + if (output === false) { + _.map(JSHint.errors, function(error) { + // By default CouchDB view functions don't pass lint + if (FauxtonAPI.isIgnorableError(error.reason)) return true; + + var line = error.line - 1; + var className = "view-code-error-line-" + line; + editor.setMarker(line, "●", "view-code-error "+className); + + setTimeout(function() { + $(".CodeMirror ."+className).tooltip({ + title: "ERROR: " + error.reason + }); + }, 0); + }, this); + } + }, + + serialize: function() { + return { + database: this.model, + ddocs: this.ddocs + }; + }, + + afterRender: function() { + this.model.on("sync", this.updateValues, this); + var that = this; + var mapFun = $("#map-function"); + mapFun.val(this.langTemplates[this.defaultLang].map); + var reduceFun = $("#reduce-function"); + reduceFun.val(this.langTemplates[this.defaultLang].reduce); + this.mapEditor = Codemirror.fromTextArea(mapFun.get()[0], { + mode: "javascript", + lineNumbers: true, + matchBrackets: true, + lineWrapping: true, + onChange: function() { + that.runJSHint("mapEditor"); + }, + extraKeys: { + "Ctrl-S": function(instance) { that.saveDoc(); }, + "Ctrl-/": "undo" + } + }); + this.reduceEditor = Codemirror.fromTextArea(reduceFun.get()[0], { + mode: "javascript", + lineNumbers: true, + matchBrackets: true, + lineWrapping: true, + onChange: function() { + that.runJSHint("reduceEditor"); + }, + extraKeys: { + "Ctrl-S": function(instance) { that.saveDoc(); }, + "Ctrl-/": "undo" + } + }); + // HACK: this should be in the html + // but CodeMirror's head explodes and it won't set the hight properly. + // So render it first, set the editor, then hide. + $(".control-group.reduce-function").hide(); + } + }); + Views.Sidebar = FauxtonAPI.View.extend({ template: "templates/documents/sidebar", events: { "click a.new#index": "newIndex", - "click .nav-list.views a.new": "showNew", // "click .nav-list.views a.toggle-view": "toggleView", "click .nav-list a.toggle-view#all-docs": "toggleView", "click .nav-list a.toggle-view#design-docs": "toggleView" @@ -640,11 +810,6 @@ function(app, FauxtonAPI, Codemirror, JSHint) { alert('coming soon'); }, - showNew: function(event){ - event.preventDefault(); - alert('show new view dialog'); - }, - toggleView: function(event){ alert('filter data by search/view/type'); event.preventDefault(); diff --git a/src/fauxton/app/templates/documents/sidebar.html b/src/fauxton/app/templates/documents/sidebar.html index b1e07b5d8..13f4c565f 100644 --- a/src/fauxton/app/templates/documents/sidebar.html +++ b/src/fauxton/app/templates/documents/sidebar.html @@ -1,7 +1,7 @@ <div id="sidenav"> <a class="btn btn-small new" id="doc" href="#<%= database.url('app') %>/new"><i class="icon-file"></i> New doc</a> <div class="btn-group" id="new-index"> - <button class="btn btn-small" href="#">New view</button> + <a class="btn btn-small" href="#<%= database.url('app') %>/new_view">New view</a> </div> <hr> <ul class="nav nav-list"> @@ -10,6 +10,6 @@ </ul> <ul class="nav nav-list views"> <li class="nav-header">Secondary Indexes</li> - <li><a href="#new-view-index" class="new"><i class="icon-plus"></i> New</a></li> + <li><a href="#<%= database.url('app') %>/new_view" class="new"><i class="icon-plus"></i> New</a></li> </ul> </div> diff --git a/src/fauxton/app/templates/documents/view_editor.html b/src/fauxton/app/templates/documents/view_editor.html new file mode 100644 index 000000000..5be92223e --- /dev/null +++ b/src/fauxton/app/templates/documents/view_editor.html @@ -0,0 +1,65 @@ +<div id="define-view" class="ddoc-alert well"> + <div class="errors-container"> + <div class="alert"> + <button type="button" class="close" data-dismiss="alert">×</button> + <strong>Warning!</strong> Preview executes the Map/Reduce functions in your browser, and may behave differently from CouchDB. + </div> + </div> + <form class="form-horizontal"> + <h3>Define your index</h3> + <div class="control-group"> + <label class="control-label" for="ddoc">Design document <a target="_couch_docs" href="http://docs.couchdb.org/en/latest/ddocs/#design-docs"><i class="icon-question-sign"></i></a></label> + <div class="controls"> + <select id="ddoc"> + <optgroup label="Select a document"> + <option>New document</option> + <% ddocs.each(function(ddoc) { %> + <option><%= ddoc.id %></option> + <% }); %> + <option selected="selected">_design/views101</option> + </optgroup> + </select> + </div> + </div> + <div class="control-group"> + <label class="control-label" for="index-name">Index name <a target="_couch_docs" href="http://docs.couchdb.org/en/latest/ddocs/#view-functions"><i class="icon-question-sign"></i></a></label> + <div class="controls"> + <input type="text" id="index-name" value="" placeholder="Index name" /> + </div> + </div> + <div class="control-group"> + <label class="control-label" for="map-function">Map function <a target="_couch_docs" href="http://docs.couchdb.org/en/latest/ddocs/#map-functions"><i class="icon-question-sign"></i></a></label> + <div class="controls"> + <textarea class="js-editor" id="map-function"></textarea> + </div> + </div> + <div class="control-group"> + <label class="control-label" for="reduce-function-selector">Reduce function <a target="_couch_docs" href="http://docs.couchdb.org/en/latest/ddocs/#reduce-and-rereduce-functions"><i class="icon-question-sign"></i></a></label> + <div class="controls"> + <select id="reduce-function-selector"> + <option value="" selected="selected">None</option> + <option value="_sum">_sum</option> + <option value="_count">_count</option> + <option value="_stats">_stats</option> + <option value="CUSTOM">Custom reduce</option> + </select> + <span class="help-block">Reduce functions are optional.</span> + </div> + </div> + <div class="control-group reduce-function"> + <label class="control-label" for="reduce-function">Custom Reduce</label> + <div class="controls"> + <textarea class="js-editor" id="reduce-function"></textarea> + </div> + </div> + <div class="control-group"> + <hr /> + <div class="controls"> + <button class="btn btn-small btn-inverse cancel">Cancel</button> + <button class="btn btn-small btn-info preview">Preview</button> + <button class="btn btn-primary save">Save</button> + </div> + </div> + <div class="clearfix"></div> + </form> +</div> diff --git a/src/fauxton/assets/css/cloudant-additions.css b/src/fauxton/assets/css/cloudant-additions.css deleted file mode 100644 index 7acfd46b7..000000000 --- a/src/fauxton/assets/css/cloudant-additions.css +++ /dev/null @@ -1,8 +0,0 @@ -pre.view-code-error { - color: red; -} - -.CodeMirror-scroll { - height: auto; - overflow-y: visible; -}
\ No newline at end of file diff --git a/src/fauxton/assets/less/cloudant.less b/src/fauxton/assets/less/cloudant.less index c91e14ab8..deae5e05c 100644 --- a/src/fauxton/assets/less/cloudant.less +++ b/src/fauxton/assets/less/cloudant.less @@ -58,3 +58,25 @@ // This function is defined in mixins .nav-divider(transparent, @white); } + + +// Misc +// ------ + +pre.view-code-error { + color: red !important; // yuck +} + +.CodeMirror-scroll { + height: auto; + overflow-y: visible; +} + +#define-view form textarea.js-editor { + width: 95%; +} + +#define-view .CodeMirror-scroll { + height: auto; + min-height: 50px; +} |