summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRussell Branca <chewbranca@gmail.com>2013-02-05 17:09:48 -0800
committerRussell Branca <chewbranca@gmail.com>2013-03-15 14:35:12 -0700
commitb76c028817474e8aa4767d683bac6e7a0f2c7168 (patch)
tree56088475808e4b6e805c19bb27e384a9c663c32d
parent206fd49ab6f733f4a7f3b5397ccef7e6f4b6f5fa (diff)
downloadcouchdb-b76c028817474e8aa4767d683bac6e7a0f2c7168.tar.gz
Initial view editor functionality
-rw-r--r--src/fauxton/app/api.js10
-rw-r--r--src/fauxton/app/modules/documents/routes.js42
-rw-r--r--src/fauxton/app/modules/documents/views.js197
-rw-r--r--src/fauxton/app/templates/documents/sidebar.html4
-rw-r--r--src/fauxton/app/templates/documents/view_editor.html65
-rw-r--r--src/fauxton/assets/css/cloudant-additions.css8
-rw-r--r--src/fauxton/assets/less/cloudant.less22
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">&times;</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;
+}