Return-Path: X-Original-To: apmail-couchdb-commits-archive@www.apache.org Delivered-To: apmail-couchdb-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 73B3B10146 for ; Wed, 12 Feb 2014 06:24:28 +0000 (UTC) Received: (qmail 1968 invoked by uid 500); 12 Feb 2014 06:20:08 -0000 Delivered-To: apmail-couchdb-commits-archive@couchdb.apache.org Received: (qmail 1480 invoked by uid 500); 12 Feb 2014 06:20:00 -0000 Mailing-List: contact commits-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list commits@couchdb.apache.org Received: (qmail 1295 invoked by uid 99); 12 Feb 2014 06:19:56 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 12 Feb 2014 06:19:56 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 01D94553A3; Wed, 12 Feb 2014 06:19:55 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: davisp@apache.org To: commits@couchdb.apache.org Date: Wed, 12 Feb 2014 06:19:58 -0000 Message-Id: <76e96ea8aacc454e9d3b4f69759287e1@git.apache.org> In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [04/52] [abbrv] Fauxton: move modules to addons http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/5f9a88f6/app/addons/documents/templates/index_row_tabular.html ---------------------------------------------------------------------- diff --git a/app/addons/documents/templates/index_row_tabular.html b/app/addons/documents/templates/index_row_tabular.html new file mode 100644 index 0000000..f5f68fa --- /dev/null +++ b/app/addons/documents/templates/index_row_tabular.html @@ -0,0 +1,25 @@ + + + + +
+
<%- JSON.stringify(doc.get("key")) %>
+
+ + +
+
<%- JSON.stringify(doc.get("value")) %>
+
+ http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/5f9a88f6/app/addons/documents/templates/jumpdoc.html ---------------------------------------------------------------------- diff --git a/app/addons/documents/templates/jumpdoc.html b/app/addons/documents/templates/jumpdoc.html new file mode 100644 index 0000000..43fdb9c --- /dev/null +++ b/app/addons/documents/templates/jumpdoc.html @@ -0,0 +1,19 @@ + + +
+ + + +
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/5f9a88f6/app/addons/documents/templates/search.html ---------------------------------------------------------------------- diff --git a/app/addons/documents/templates/search.html b/app/addons/documents/templates/search.html new file mode 100644 index 0000000..bb84891 --- /dev/null +++ b/app/addons/documents/templates/search.html @@ -0,0 +1,15 @@ + + + \ No newline at end of file http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/5f9a88f6/app/addons/documents/templates/sidebar.html ---------------------------------------------------------------------- diff --git a/app/addons/documents/templates/sidebar.html b/app/addons/documents/templates/sidebar.html new file mode 100644 index 0000000..8a73ae9 --- /dev/null +++ b/app/addons/documents/templates/sidebar.html @@ -0,0 +1,67 @@ + + +
+
+
+
+ + +
+ +
+ + +
+ +
+
+ + +
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/5f9a88f6/app/addons/documents/templates/tabs.html ---------------------------------------------------------------------- diff --git a/app/addons/documents/templates/tabs.html b/app/addons/documents/templates/tabs.html new file mode 100644 index 0000000..f8b0c4b --- /dev/null +++ b/app/addons/documents/templates/tabs.html @@ -0,0 +1,18 @@ + + + http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/5f9a88f6/app/addons/documents/templates/upload_modal.html ---------------------------------------------------------------------- diff --git a/app/addons/documents/templates/upload_modal.html b/app/addons/documents/templates/upload_modal.html new file mode 100644 index 0000000..9a5c5cd --- /dev/null +++ b/app/addons/documents/templates/upload_modal.html @@ -0,0 +1,42 @@ + + + + http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/5f9a88f6/app/addons/documents/templates/view_editor.html ---------------------------------------------------------------------- diff --git a/app/addons/documents/templates/view_editor.html b/app/addons/documents/templates/view_editor.html new file mode 100644 index 0000000..ddb5a0c --- /dev/null +++ b/app/addons/documents/templates/view_editor.html @@ -0,0 +1,87 @@ + +
+ +
+
+
+
+
+
+ +
+
+ +
+ + +
+ + +
+ + <% if (newView) { %> +
<%= langTemplates.map %>
+ <% } else { %> +
<%- ddoc.get('views')[viewName].map %>
+ <% } %> +
+ + +
+ + + +
+ +
+ + <% if (newView) { %> +
<%= langTemplates.reduce %>
+ <% } else { %> +
<%- ddoc.get('views')[viewName].reduce %>
+ <% } %> +
+ +
+ + + <% if (!newView) { %> + + <% } %> +
+
+
+
+
+
+
+
+
+
+
+
+ http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/5f9a88f6/app/addons/documents/tests/resourcesSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/resourcesSpec.js b/app/addons/documents/tests/resourcesSpec.js new file mode 100644 index 0000000..380a4e4 --- /dev/null +++ b/app/addons/documents/tests/resourcesSpec.js @@ -0,0 +1,84 @@ +// 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([ + 'addons/documents/resources', + 'testUtils' +], function (Models, testUtils) { + var assert = testUtils.assert; + + describe('IndexCollection', function () { + var collection; + beforeEach(function () { + collection = new Models.IndexCollection([{ + id:'myId1', + doc: 'num1' + }, + { + id:'myId2', + doc: 'num2' + }], { + database: {id: 'databaseId', safeID: function () { return this.id; }}, + design: '_design/myDoc' + }); + + }); + + it('Should return urlNext', function () { + var url = collection.urlNextPage(20); + + assert.equal(url, 'database/databaseId/_design/myDoc/_view/?limit=21&reduce=false&startkey_docid=myId2&startkey='); + + }); + + it('Should return urlPrevious', function () { + var url = collection.urlPreviousPage(20, {limit: 21, reduce: false, startkey_docid: "myId1",startkey:"myId1"} ); + + assert.equal(url, 'database/databaseId/_design/myDoc/_view/?limit=20&reduce=false&startkey_docid=myId1&startkey=myId1'); + + }); + + }); + + describe('AllDocs', function () { + var collection; + beforeEach(function () { + collection = new Models.AllDocs([{ + _id:'myId1', + doc: 'num1' + }, + { + _id:'myId2', + doc: 'num2' + }], { + database: {id: 'databaseId', safeID: function () { return this.id; }}, + params: {limit: 20} + }); + + }); + + it('Should return urlNext', function () { + var url = collection.urlNextPage(20); + + assert.equal(url, 'database/databaseId/_all_docs?limit=21&startkey_docid=%22myId2%22&startkey=%22myId2%22'); + + }); + + it('Should return urlPrevious', function () { + var url = collection.urlPreviousPage(20, {limit: 21, startkey_docid: "myId1",startkey:"myId1"} ); + assert.equal(url, 'database/databaseId/_all_docs?limit=20&startkey_docid=myId1&startkey=myId1'); + }); + + + }); + +}); + http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/5f9a88f6/app/addons/documents/views.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/views.js b/app/addons/documents/views.js new file mode 100644 index 0000000..97d58e3 --- /dev/null +++ b/app/addons/documents/views.js @@ -0,0 +1,1855 @@ +// 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/fauxton/components", + + "addons/documents/resources", + "addons/databases/resources", + "addons/pouchdb/base", + + // Libs + "resizeColumns", + + // Plugins + "plugins/prettify" + +], + +function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColumns) { + var Views = {}; + Views.Tabs = FauxtonAPI.View.extend({ + template: "templates/documents/tabs", + initialize: function(options){ + this.collection = options.collection; + this.database = options.database; + this.active_id = options.active_id; + }, + + events: { + "click #delete-database": "delete_database" + }, + + serialize: function () { + return { + // TODO make this not hard coded here + changes_url: '#' + this.database.url('changes'), + db_url: '#' + this.database.url('index') + '?limit=' + Databases.DocLimit, + }; + }, + + beforeRender: function(manage) { + this.insertView("#search", new Views.SearchBox({ + collection: this.collection, + database: this.database.id + })); + }, + + afterRender: function () { + if (this.active_id) { + this.$('.active').removeClass('active'); + this.$('#'+this.active_id).addClass('active'); + } + }, + + delete_database: function (event) { + event.preventDefault(); + + var result = confirm("Are you sure you want to delete this database?"); + + if (!result) { return; } + FauxtonAPI.addNotification({ + msg: "Deleting your database...", + type: "error" + }); + return this.database.destroy().done(function () { + app.router.navigate('#/_all_dbs', {trigger: true}); + }); + } + }); + + Views.SearchBox = FauxtonAPI.View.extend({ + template: "templates/documents/search", + tagName: "form", + initialize: function(options){ + this.collection = options.collection; + this.database = options.database; + }, + afterRender: function(){ + var collection = this.collection; + var form = this.$el; + var searchbox = form.find("input#searchbox"); + var database = this.database; + + form.submit(function(evt){ + evt.preventDefault(); + var viewname = form.find("input#view").val().split('/'); + var url = "#database/" + database + "/_design/"; + url += viewname[0] + "/_view/" + viewname[1]; + if (searchbox.val() !== ""){ + // TODO: this'll need to work when val() is a number etc. + url += '?startkey="' + searchbox.val() + '"'; + } + FauxtonAPI.navigate(url); + }); + + searchbox.typeahead({ + source: function(query, process) { + // TODO: include _all_docs and view keys somehow + var views = _.map(collection.pluck('doc'), function(d){ + return _.map(_.keys(d.views), function(view){ + return d._id.split('/')[1] + "/" + view; + }); + }); + return _.flatten(views); + }, + minLength: 3, + updater: function(item){ + // TODO: some way to return the original search box + this.$element.removeClass('span12'); + this.$element.addClass('span6'); + this.$element.attr('placeholder', 'Search by view key'); + $('' + item +'').insertBefore(this.$element); + $('').insertBefore(this.$element); + // Remove the type ahead for now + $('.typehead').unbind(); + } + }); + } + }); + + Views.UploadModal = FauxtonAPI.View.extend({ + template: "templates/documents/upload_modal", + + disableLoader: true, + + initialize: function (options) { + _.bindAll(this); + }, + + events: { + "click a#upload-btn": "uploadFile" + }, + + uploadFile: function (event) { + event.preventDefault(); + + var docRev = this.model.get('_rev'), + that = this, + $form = this.$('#file-upload'); + + if (!docRev) { + return this.set_error_msg('The document needs to be saved before adding an attachment.'); + } + + if ($('input[type="file"]')[0].files.length === 0) { + return this.set_error_msg('Selected a file to be uploaded.'); + } + + this.$('#_rev').val(docRev); + + $form.ajaxSubmit({ + url: this.model.url(), + type: 'POST', + beforeSend: this.beforeSend, + uploadProgress: this.uploadProgress, + success: this.success, + error: function (resp) { + console.log('ERR on upload', resp); + return that.set_error_msg('Could not upload document: ' + JSON.parse(resp.responseText).reason); + } + }); + }, + + success: function (resp) { + var hideModal = this.hideModal, + $form = this.$('#file-upload'); + + FauxtonAPI.triggerRouteEvent('reRenderDoc'); + //slight delay to make this transistion a little more fluid and less jumpy + setTimeout(function () { + $form.clearForm(); + hideModal(); + $('.modal-backdrop').remove(); + }, 1000); + }, + + uploadProgress: function(event, position, total, percentComplete) { + this.$('.bar').css({width: percentComplete + '%'}); + }, + + beforeSend: function () { + this.$('.progress').removeClass('hide'); + }, + + showModal: function () { + this.$('.bar').css({width: '0%'}); + this.$('.progress').addClass('hide'); + this.clear_error_msg(); + this.$('.modal').modal(); + // hack to get modal visible + $('.modal-backdrop').css('z-index',1025); + }, + + hideModal: function () { + this.$('.modal').modal('hide'); + }, + + set_error_msg: function (msg) { + var text; + if (typeof(msg) == 'string') { + text = msg; + } else { + text = JSON.parse(msg.responseText).reason; + } + this.$('#modal-error').text(text).removeClass('hide'); + }, + + clear_error_msg: function () { + this.$('#modal-error').text(' ').addClass('hide'); + }, + + serialize: function () { + return this.model.toJSON(); + } + }); + + Views.DuplicateDocModal = FauxtonAPI.View.extend({ + template: "templates/documents/duplicate_doc_modal", + + initialize: function () { + _.bindAll(this); + }, + + events: { + "click #duplicate-btn":"duplicate" + + }, + + duplicate: function (event) { + event.preventDefault(); + var newId = this.$('#dup-id').val(), + encodedID = app.mixins.safeURLName(newId); + + this.hideModal(); + FauxtonAPI.triggerRouteEvent('duplicateDoc', encodedID); + }, + + _showModal: function () { + this.$('.bar').css({width: '0%'}); + this.$('.progress').addClass('hide'); + this.clear_error_msg(); + this.$('.modal').modal(); + // hack to get modal visible + $('.modal-backdrop').css('z-index',1025); + }, + + showModal: function () { + var showModal = this._showModal, + setDefaultIdValue = this.setDefaultIdValue, + uuid = new FauxtonAPI.UUID(); + + uuid.fetch().then(function () { + setDefaultIdValue(uuid.next()); + showModal(); + }); + }, + + setDefaultIdValue: function (id) { + this.$('#dup-id').val(id); + }, + + hideModal: function () { + this.$('.modal').modal('hide'); + }, + + set_error_msg: function (msg) { + var text; + if (typeof(msg) == 'string') { + text = msg; + } else { + text = JSON.parse(msg.responseText).reason; + } + this.$('#modal-error').text(text).removeClass('hide'); + }, + + clear_error_msg: function () { + this.$('#modal-error').text(' ').addClass('hide'); + }, + + serialize: function () { + return this.model.toJSON(); + } + + }); + + Views.FieldEditorTabs = FauxtonAPI.View.extend({ + template: "templates/documents/doc_field_editor_tabs", + disableLoader: true, + initialize: function(options) { + this.selected = options.selected; + }, + + events: { + }, + updateSelected: function (selected) { + this.selected = selected; + this.$('.active').removeClass('active'); + this.$('#'+this.selected).addClass('active'); + }, + + serialize: function() { + var selected = this.selected; + return { + doc: this.model, + isNewDoc: this.model.isNewDoc(), + isSelectedClass: function(item) { + return item && item === selected ? "active" : ""; + } + }; + }, + + establish: function() { + return [this.model.fetch()]; + } + }); + + Views.Document = FauxtonAPI.View.extend({ + template: "templates/documents/all_docs_item", + tagName: "tr", + className: "all-docs-item", + + events: { + "click button.delete": "destroy", + "dblclick pre.prettyprint": "edit" + }, + + attributes: function() { + return { + "data-id": this.model.id + }; + }, + + serialize: function() { + return { + doc: this.model + }; + }, + + establish: function() { + return [this.model.fetch()]; + }, + + edit: function(event) { + event.preventDefault(); + FauxtonAPI.navigate("#" + this.model.url('app')); + }, + + destroy: function(event) { + event.preventDefault(); + var that = this; + + if (!window.confirm("Are you sure you want to delete this doc?")) { + return false; + } + + this.model.destroy().then(function(resp) { + FauxtonAPI.addNotification({ + msg: "Succesfully destroyed your doc" + }); + that.$el.fadeOut(function () { + that.remove(); + }); + + that.model.collection.remove(that.model.id); + if (!!that.model.id.match('_design')) { + FauxtonAPI.triggerRouteEvent('reloadDesignDocs'); + } + }, function(resp) { + FauxtonAPI.addNotification({ + msg: "Failed to destroy your doc!", + type: "error" + }); + }); + } + }); + + Views.Row = FauxtonAPI.View.extend({ + template: "templates/documents/index_row_docular", + tagName: "tr", + + events: { + "click button.delete": "destroy" + }, + + destroy: function (event) { + event.preventDefault(); + + window.alert('Cannot delete a document generated from a view.'); + }, + + serialize: function() { + return { + doc: this.model + }; + } + }); + + Views.IndexItem = FauxtonAPI.View.extend({ + template: "templates/documents/index_menu_item", + tagName: "li", + + initialize: function(options){ + this.index = options.index; + this.ddoc = options.ddoc; + this.database = options.database; + this.selected = !! options.selected; + }, + + serialize: function() { + return { + index: this.index, + ddoc: this.ddoc, + database: this.database, + index_clean: app.mixins.removeSpecialCharacters(this.index), + ddoc_clean: app.mixins.removeSpecialCharacters(this.ddoc), + index_encoded: app.mixins.safeURLName(this.index), + ddoc_encoded: app.mixins.safeURLName(this.ddoc), + database_encoded: app.mixins.safeURLName(this.database), + selected: this.selected + }; + }, + + afterRender: function() { + if (this.selected) { + $("#sidenav ul.nav-list li").removeClass("active"); + this.$el.addClass("active"); + } + } + }); + + Views.AllDocsNumber = FauxtonAPI.View.extend({ + template: "templates/documents/all_docs_number", + + initialize: function (options) { + this.newView = options.newView || false; + this.showNumbers = options.showNumbers; + this.pagination = options.pagination; + + this.listenTo(this.collection, 'totalRows:decrement', this.render); + }, + + serialize: function () { + var totalRows = 0, + recordStart = 0, + updateSeq = false, + pageStart = 0, + pageEnd = 20; + + if (!this.newView) { + totalRows = this.collection.totalRows(); + updateSeq = this.collection.updateSeq(); + } + + recordStart = this.collection.recordStart(); + if (this.pagination) { + pageStart = this.pagination.pageStart(); + pageEnd = this.pagination.pageEnd(); + } + + return { + database: app.mixins.safeURLName(this.collection.database.id), + updateSeq: updateSeq, + offset: recordStart, + totalRows: totalRows, + showNumbers: this.showNumbers, + numModels: this.collection.models.length + recordStart - 1, + pageStart: pageStart, + pageEnd: pageEnd + }; + } + + }); + + Views.AllDocsLayout = FauxtonAPI.View.extend({ + template: "templates/documents/all_docs_layout", + className: "row", + + initialize: function (options) { + this.database = options.database; + this.params = options.params; + }, + + events: { + 'click #toggle-query': "toggleQuery" + }, + + toggleQuery: function (event) { + $('#dashboard-content').scrollTop(0); + this.$('#query').toggle('fast'); + }, + + beforeRender: function () { + this.advancedOptions = this.insertView('#query', new Views.AdvancedOptions({ + updateViewFn: this.updateAllDocs, + previewFn: this.previewView, + hasReduce: false, + showPreview: false, + database: this.database + })); + + this.$('#query').hide(); + }, + + afterRender: function () { + if (this.params) { + this.advancedOptions.updateFromParams(this.params); + } + + }, + + updateAllDocs: function (event, paramInfo) { + event.preventDefault(); + + var errorParams = paramInfo.errorParams, + params = paramInfo.params; + + if (_.any(errorParams)) { + _.map(errorParams, function(param) { + + // TODO: Where to add this error? + // bootstrap wants the error on a control-group div, but we're not using that + //$('form.view-query-update input[name='+param+'], form.view-query-update select[name='+param+']').addClass('error'); + return FauxtonAPI.addNotification({ + msg: "JSON Parse Error on field: "+param.name, + type: "error", + selector: ".advanced-options .errors-container" + }); + }); + FauxtonAPI.addNotification({ + msg: "Make sure that strings are properly quoted and any other values are valid JSON structures", + type: "warning", + selector: ".advanced-options .errors-container" + }); + + return false; + } + + var fragment = window.location.hash.replace(/\?.*$/, ''); + fragment = fragment + '?' + $.param(params); + FauxtonAPI.navigate(fragment, {trigger: false}); + + FauxtonAPI.triggerRouteEvent('updateAllDocs', {allDocs: true}); + }, + + previewView: function (event) { + event.preventDefault(); + } + + }); + + // TODO: Rename to reflect that this is a list of rows or documents + Views.AllDocsList = FauxtonAPI.View.extend({ + template: "templates/documents/all_docs_list", + events: { + "click button.all": "selectAll", + "click button.bulk-delete": "bulkDelete", + "click #collapse": "collapse", + "change .row-select":"toggleTrash" + }, + + toggleTrash: function () { + if (this.$('.row-select:checked').length > 0) { + this.$('.bulk-delete').removeClass('disabled'); + } else { + this.$('.bulk-delete').addClass('disabled'); + } + }, + + initialize: function(options){ + this.nestedView = options.nestedView || Views.Document; + this.rows = {}; + this.viewList = !! options.viewList; + this.database = options.database; + if (options.ddocInfo) { + this.designDocs = options.ddocInfo.designDocs; + this.ddocID = options.ddocInfo.id; + } + this.newView = options.newView || false; + this.expandDocs = true; + }, + + establish: function() { + if (this.newView) { return null; } + + return this.collection.fetch({reset: true}).fail(function() { + // TODO: handle error requests that slip through + // This should just throw a notification, not break the page + console.log("ERROR: ", arguments); + }); + }, + + selectAll: function(evt){ + $('.all-docs').find("input:checkbox").prop('checked', !$(evt.target).hasClass('active')).trigger('change'); + }, + + serialize: function() { + var requestDuration = false; + + if (this.collection.requestDurationInString) { + requestDuration = this.collection.requestDurationInString(); + } + + return { + viewList: this.viewList, + requestDuration: requestDuration, + expandDocs: this.expandDocs + }; + }, + + collapse: function (event) { + event.preventDefault(); + + if (this.expandDocs) { + this.expandDocs = false; + } else { + this.expandDocs = true; + } + + this.render(); + }, + + /* + * TODO: this should be reconsidered + * This currently performs delete operations on the model level, + * when we could be using bulk docs with _deleted = true. Using + * individual models is cleaner from a backbone standpoint, but + * not from the couchdb api. + * Also, the delete method is naive and leaves the body intact, + * when we should switch the doc to only having id/rev/deleted. + */ + bulkDelete: function() { + var that = this; + // yuck, data binding ftw? + var eles = this.$el.find("input.row-select:checked") + .parents("tr.all-docs-item") + .map(function(e) { return $(this).attr("data-id"); }) + .get(); + + if (eles.length === 0 || !window.confirm("Are you sure you want to delete these " + eles.length + " docs?")) { + return false; + } + + _.each(eles, function(ele) { + var model = this.collection.get(ele); + + model.destroy().then(function(resp) { + that.rows[ele].$el.fadeOut(function () { + $(this).remove(); + }); + + model.collection.remove(model.id); + if (!!model.id.match('_design')) { + FauxtonAPI.triggerRouteEvent('reloadDesignDocs'); + } + that.$('.bulk-delete').addClass('disabled'); + }, function(resp) { + FauxtonAPI.addNotification({ + msg: "Failed to destroy your doc!", + type: "error" + }); + }); + }, this); + }, + + addPagination: function () { + var collection = this.collection; + var perPage = function () { + if (collection.params.limit && collection.skipFirstItem) { + return parseInt(collection.params.limit, 10) - 1; + } else if (collection.params.limit) { + return parseInt(collection.params.limit, 10); + } + + return 20; + }; + + this.pagination = new Components.IndexPagination({ + collection: this.collection, + scrollToSelector: '#dashboard-content', + previousUrlfn: function () { + return collection.urlPreviousPage(perPage(), this.previousParams.pop()); + }, + canShowPreviousfn: function () { + if (this.previousParams.length === 0) { + return false; + } + + return true; + }, + canShowNextfn: function () { + if (collection.length < (perPage() -1)) { + return false; + } + + return true; + }, + + nextUrlfn: function () { + return collection.urlNextPage(perPage()); + } + }); + }, + + cleanup: function () { + //if (!this.pagination) { return; } + this.pagination.remove(); + //this.pagination = null; + this.allDocsNumber.remove(); + _.each(this.rows, function (row) {row.remove();}); + }, + + beforeRender: function() { + var showNumbers = true; + + if (!this.pagination) { + this.addPagination(); + } + + this.insertView('#documents-pagination', this.pagination); + + if (this.designDocs || this.collection.idxType === '_view' || this.collection.params.startkey === '"_design"') { + showNumbers = false; + } + + this.allDocsNumber = this.setView('#item-numbers', new Views.AllDocsNumber({ + collection: this.collection, + newView: this.newView, + showNumbers: showNumbers, + pagination: this.pagination + })); + + var docs = this.expandDocs ? this.collection : this.collection.simple(); + + docs.each(function(doc) { + this.rows[doc.id] = this.insertView("table.all-docs tbody", new this.nestedView({ + model: doc + })); + }, this); + }, + + afterRender: function(){ + prettyPrint(); + } + }); + + Views.Doc = FauxtonAPI.View.extend({ + template: "templates/documents/doc", + events: { + "click button.save-doc": "saveDoc", + "click button.delete": "destroy", + "click button.duplicate": "duplicate", + "click button.upload": "upload", + "click button.cancel-button": "goback" + }, + disableLoader: true, + initialize: function (options) { + this.database = options.database; + _.bindAll(this); + }, + goback: function(){ + FauxtonAPI.navigate(this.database.url("index") + "?limit=100"); + }, + destroy: function(event) { + if (this.model.isNewDoc()) { + FauxtonAPI.addNotification({ + msg: 'This document has not been saved yet.', + type: 'warning' + }); + return; + } + + if (!window.confirm("Are you sure you want to delete this doc?")) { + return false; + } + + var database = this.model.database; + + this.model.destroy().then(function(resp) { + FauxtonAPI.addNotification({ + msg: "Succesfully destroyed your doc" + }); + FauxtonAPI.navigate(database.url("index")); + }, function(resp) { + FauxtonAPI.addNotification({ + msg: "Failed to destroy your doc!", + type: "error" + }); + }); + }, + + beforeRender: function () { + this.uploadModal = this.setView('#upload-modal', new Views.UploadModal({model: this.model})); + this.uploadModal.render(); + + this.duplicateModal = this.setView('#duplicate-modal', new Views.DuplicateDocModal({model: this.model})); + this.duplicateModal.render(); + }, + + upload: function (event) { + event.preventDefault(); + if (this.model.isNewDoc()) { + FauxtonAPI.addNotification({ + msg: 'Please save the document before uploading an attachment.', + type: 'warning' + }); + return; + } + this.uploadModal.showModal(); + }, + + duplicate: function(event) { + if (this.model.isNewDoc()) { + FauxtonAPI.addNotification({ + msg: 'Please save the document before duplicating it.', + type: 'warning' + }); + return; + } + event.preventDefault(); + this.duplicateModal.showModal(); + }, + + 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()); + } + }, + + establish: function() { + var promise = this.model.fetch(), + databaseId = this.database.safeID(), + deferred = $.Deferred(), + that = this; + + promise.then(function () { + deferred.resolve(); + }, function (xhr, reason, msg) { + if (xhr.status === 404) { + FauxtonAPI.addNotification({ + msg: 'The document does not exist', + type: 'error', + clear: true + }); + that.goback(); + } + deferred.reject(); + }); + + return deferred; + }, + + saveDoc: function(event) { + var json, notification, + that = this, + editor = this.editor, + validDoc = this.getDocFromEditor(); + + if (validDoc) { + this.getDocFromEditor(); + + notification = FauxtonAPI.addNotification({msg: "Saving document."}); + + this.model.save().then(function () { + editor.editSaved(); + FauxtonAPI.navigate('/database/' + that.database.safeID() + '/' + that.model.id); + }).fail(function(xhr) { + + var responseText = JSON.parse(xhr.responseText).reason; + notification = FauxtonAPI.addNotification({ + msg: "Save failed: " + responseText, + type: "error", + clear: true, + selector: "#doc .errors-container" + }); + }); + } else if(this.model.validationError && this.model.validationError === 'Cannot change a documents id.') { + notification = FauxtonAPI.addNotification({ + msg: "Cannot save: " + 'Cannot change a documents _id, try Duplicate doc instead!', + type: "error", + selector: "#doc .errors-container" + }); + delete this.model.validationError; + } else { + notification = FauxtonAPI.addNotification({ + msg: "Please fix the JSON errors and try again.", + type: "error", + selector: "#doc .errors-container" + }); + } + }, + + getDocFromEditor: function () { + if (!this.hasValidCode()) { + return false; + } + + json = JSON.parse(this.editor.getValue()); + + this.model.clear().set(json, {validate: true}); + if (this.model.validationError) { + return false; + } + + return this.model; + }, + + hasValidCode: function() { + var errors = this.editor.getAnnotations(); + return errors.length === 0; + }, + + serialize: function() { + return { + doc: this.model, + attachments: this.getAttachments() + }; + }, + + getAttachments: function () { + var attachments = this.model.get('_attachments'); + + if (!attachments) { return false; } + + return _.map(attachments, function (att, key) { + return { + fileName: key, + size: att.length, + contentType: att.content_type, + url: this.model.url() + '/' + key + }; + }, this); + }, + + afterRender: function() { + var saveDoc = this.saveDoc; + + this.editor = new Components.Editor({ + editorId: "editor-container", + commands: [{ + name: 'save', + bindKey: {win: 'Ctrl-S', mac: 'Ctrl-S'}, + exec: function(editor) { + saveDoc(); + }, + readOnly: true // false if this command should not apply in readOnly mode + }] + }); + this.editor.render(); + this.model.on("sync", this.updateValues, this); + }, + + cleanup: function () { + if (this.editor) this.editor.remove(); + } + }); + + Views.DocFieldEditor = FauxtonAPI.View.extend({ + template: "templates/documents/doc_field_editor", + disableLoader: true, + events: { + "click button.save": "saveDoc" + }, + + saveDoc: function(event) { + FauxtonAPI.addNotification({ + type: "warning", + msg: "Save functionality coming soon." + }); + }, + + serialize: function() { + return { + doc: this.getModelWithoutAttachments(), + attachments: this.getAttachments() + }; + }, + + getModelWithoutAttachments: function() { + var model = this.model.toJSON(); + delete model._attachments; + return model; + }, + + getAttachments: function () { + var attachments = this.model.get('_attachments'); + + if (!attachments) { return []; } + + return _.map(attachments, function (att, key) { + return { + fileName: key, + size: att.length, + contentType: att.content_type, + url: this.model.url() + '/' + key + }; + }, this); + }, + + establish: function() { + return [this.model.fetch()]; + } + }); + + Views.AdvancedOptions = FauxtonAPI.View.extend({ + template: "templates/documents/advanced_options", + className: "advanced-options well", + + initialize: function (options) { + this.database = options.database; + this.ddocName = options.ddocName; + this.viewName = options.viewName; + this.updateViewFn = options.updateViewFn; + this.previewFn = options.previewFn; + //this.hadReduce = options.hasReduce || true; + + if (typeof(options.hasReduce) === 'undefined') { + this.hasReduce = true; + } else { + this.hasReduce = options.hasReduce; + } + + if (typeof(options.showPreview) === 'undefined') { + this.showPreview = true; + } else { + this.showPreview = options.showPreview; + } + }, + + events: { + "change form.view-query-update input": "updateFilters", + "change form.view-query-update select": "updateFilters", + "submit form.view-query-update": "updateView", + "click button.preview": "previewView" + }, + + beforeRender: function () { + if (this.viewName && this.ddocName) { + var buttonViews = FauxtonAPI.getExtensions('advancedOptions:ViewButton'); + _.each(buttonViews, function (view) { + this.insertView('#button-options', view); + view.update(this.database, this.ddocName, this.viewName); + }, this); + } + }, + + renderOnUpdatehasReduce: function (hasReduce) { + this.hasReduce = hasReduce; + this.render(); + }, + + queryParams: function () { + var $form = this.$(".view-query-update"); + // Ignore params without a value + var params = _.filter($form.serializeArray(), function(param) { + return param.value; + }); + + // Validate *key* params to ensure they're valid JSON + var keyParams = ["key","keys","startkey","endkey"]; + var errorParams = _.filter(params, function(param) { + if (_.contains(keyParams, param.name)) { + try { + JSON.parse(param.value); + return false; + } catch(e) { + return true; + } + } else { + return false; + } + }); + + return {params: params, errorParams: errorParams}; + }, + + updateFilters: function(event) { + event.preventDefault(); + var $ele = $(event.currentTarget); + var name = $ele.attr('name'); + this.updateFiltersFor(name, $ele); + }, + + updateFiltersFor: function(name, $ele) { + var $form = $ele.parents("form.view-query-update:first"); + switch (name) { + // Reduce constraints + // - Can't include_docs for reduce=true + // - can't include group_level for reduce=false + case "reduce": + if ($ele.prop('checked') === true) { + if ($form.find("input[name=include_docs]").prop("checked") === true) { + $form.find("input[name=include_docs]").prop("checked", false); + var notification = FauxtonAPI.addNotification({ + msg: "include_docs has been disabled as you cannot include docs on a reduced view", + type: "warn", + selector: ".view.show .all-docs-list.errors-container" + }); + } + $form.find("input[name=include_docs]").prop("disabled", true); + $form.find("select[name=group_level]").prop("disabled", false); + } else { + $form.find("select[name=group_level]").prop("disabled", true); + $form.find("input[name=include_docs]").prop("disabled", false); + } + break; + case "include_docs": + break; + } + }, + + updateFromParams: function (params) { + var $form = this.$el.find("form.view-query-update"); + _.each(params, function(val, key) { + var $ele; + switch (key) { + case "limit": + case "group_level": + $form.find("select[name='"+key+"']").val(val); + break; + case "include_docs": + case "stale": + case "descending": + case "inclusive_end": + $form.find("input[name='"+key+"']").prop('checked', true); + break; + case "reduce": + $ele = $form.find("input[name='"+key+"']"); + if (val == "true") { + $ele.prop('checked', true); + } + this.updateFiltersFor(key, $ele); + break; + default: + $form.find("input[name='"+key+"']").val(val); + break; + } + }, this); + }, + + updateView: function (event) { + this.updateViewFn(event, this.queryParams()); + }, + + previewView: function (event) { + this.previewFn(event, this.queryParams()); + }, + + serialize: function () { + return { + hasReduce: this.hasReduce, + showPreview: this.showPreview + }; + } + }); + + Views.DesignDocSelector = FauxtonAPI.View.extend({ + template: "templates/documents/design_doc_selector", + + events: { + "change select#ddoc": "updateDesignDoc" + }, + + 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; + this.render(); + }, + + 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(); + } + }, + + newDesignDoc: function () { + + return this.$('#ddoc').val() === 'new-doc'; + }, + + 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}); + //this.collection.add(ddoc); + return ddoc; + } else if ( !this.newDesignDoc() ) { + var ddocName = this.$('#ddoc').val(); + return this.collection.find(function (ddoc) { + return ddoc.id === ddocName; + }).dDocModel(); + } + } + }); + + Views.ViewEditor = FauxtonAPI.View.extend({ + template: "templates/documents/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' + }, + + 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", + + initialize: function(options) { + this.newView = options.newView || false; + this.ddocs = options.ddocs; + this.params = options.params; + this.database = options.database; + 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 json, 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; + + this.viewName = viewName; + + notification = FauxtonAPI.addNotification({ + msg: "Saving document.", + selector: "#define-view .errors-container", + 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", + selector: "#define-view .errors-container", + clear: true + }); + + if (that.newView) { + var fragment = '/database/' + that.database.safeID() +'/' + ddoc.safeID() + '/_view/' + app.mixins.safeURLName(viewName); + + FauxtonAPI.navigate(fragment, {trigger: false}); + that.newView = false; + that.ddocID = ddoc.safeID(); + that.viewName = viewName; + that.ddocInfo = ddoc; + that.showIndex = true; + that.render(); + FauxtonAPI.triggerRouteEvent('reloadDesignDocs', { + selectedTab: app.mixins.removeSpecialCharacters(ddocName.replace(/_design\//,'')) + '_' + app.mixins.removeSpecialCharacters(viewName) + }); + } + + if (that.reduceFunStr !== reduceVal) { + that.reduceFunStr = reduceVal; + that.advancedOptions.renderOnUpdatehasReduce(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", + selector: "#define-view .errors-container", + 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) { + + // TODO: Where to add this error? + // bootstrap wants the error on a control-group div, but we're not using that + //$('form.view-query-update input[name='+param+'], form.view-query-update select[name='+param+']').addClass('error'); + return FauxtonAPI.addNotification({ + msg: "JSON Parse Error on field: "+param.name, + type: "error", + selector: ".advanced-options .errors-container", + clear: true + }); + }); + FauxtonAPI.addNotification({ + msg: "Make sure that strings are properly quoted and any other values are valid JSON structures", + type: "warning", + selector: ".advanced-options .errors-container", + clear: true + }); + + return false; + } + + var fragment = window.location.hash.replace(/\?.*$/, ''); + fragment = fragment + '?' + $.param(params); + FauxtonAPI.navigate(fragment, {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}); + + event.preventDefault(); + + FauxtonAPI.addNotification({ + msg: "Warning! Preview executes the Map/Reduce functions in your browser, and may behave differently from CouchDB.", + type: "warning", + selector: ".advanced-options .errors-container", + fade: true + }); + + var promise = FauxtonAPI.Deferred(); + + if (!this.database.allDocs) { + 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') { + 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) { + 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(); + }, + + beforeRender: function () { + + if (this.newView) { + this.reduceFunStr = '_sum'; + 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(ddocDecode).dDocModel(); + this.reduceFunStr = this.model.viewHasReduce(this.viewName); + this.setView('#ddoc-info', new Views.DdocInfo({model: this.ddocInfo })); + } + + this.designDocSelector = this.setView('.design-doc-group', new Views.DesignDocSelector({ + collection: this.ddocs, + ddocName: this.model.id, + database: this.database + })); + + this.advancedOptions = this.insertView('#query', new Views.AdvancedOptions({ + updateViewFn: this.updateView, + previewFn: this.previewView, + database: this.database, + viewName: this.viewName, + ddocName: this.model.id, + hasReduce: this.hasReduce() + })); + }, + + afterRender: function() { + if (this.params) { + this.advancedOptions.updateFromParams(this.params); + } + + 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(); + }, + + cleanup: function () { + this.mapEditor && this.mapEditor.remove(); + this.reduceEditor && this.reduceEditor.remove(); + } + }); + + Views.JumpToDoc = FauxtonAPI.View.extend({ + template: "templates/documents/jumpdoc", + + initialize: function (options) { + this.database = options.database; + }, + + events: { + "submit #jump-to-doc": "jumpToDoc" + }, + + jumpToDoc: function (event) { + event.preventDefault(); + var docId = this.$('#jump-to-doc-id').val().trim(); + FauxtonAPI.navigate('/database/' + app.mixins.safeURLName(this.database.id) +'/' + app.mixins.safeURLName(docId), {trigger: true}); + }, + + afterRender: function () { + this.typeAhead = new Components.DocSearchTypeahead({el: '#jump-to-doc-id', database: this.database}); + this.typeAhead.render(); + } + }); + + Views.Sidebar = FauxtonAPI.View.extend({ + template: "templates/documents/sidebar", + events: { + "click button#delete-database": "deleteDatabase" + }, + + initialize: function(options) { + this.database = options.database; + if (options.ddocInfo) { + this.ddocID = options.ddocInfo.id; + this.currView = options.ddocInfo.currView; + } + }, + + deleteDatabase: function (event) { + event.preventDefault(); + + var result = confirm('Are you sure you want to delete this database?'); + + if (!result) { return; } + var databaseName = this.database.id; + FauxtonAPI.addNotification({ + msg: "Deleting your database...", + type: "error", + clear: true + }); + + this.database.destroy().then(function () { + FauxtonAPI.navigate('#/_all_dbs'); + FauxtonAPI.addNotification({ + msg: 'The database ' + databaseName + ' has been deleted.', + clear: true + }); + }).fail(function (rsp, error, msg) { + FauxtonAPI.addNotification({ + msg: 'Could not delete the database, reason ' + msg + '.', + type: 'error', + clear: true + }); + }); + }, + + serialize: function() { + var docLinks = FauxtonAPI.getExtensions('docLinks'), + newLinks = FauxtonAPI.getExtensions('sidebar:newLinks'), + addLinks = FauxtonAPI.getExtensions('sidebar:links'), + extensionList = FauxtonAPI.getExtensions('sidebar:list'); + return { + changes_url: '#' + this.database.url('changes'), + permissions_url: '#' + this.database.url('app') + '/permissions', + db_url: '#' + this.database.url('index') + '?limit=' + Databases.DocLimit, + database: this.collection.database, + database_url: '#' + this.database.url('app'), + docLinks: docLinks, + docLimit: Databases.DocLimit, + addLinks: addLinks, + newLinks: newLinks, + extensionList: extensionList > 0 + }; + }, + + buildIndexList: function(collection, selector, design){ + _.each(_.keys(collection), function(key){ + var selected = this.ddocID == "_design/"+design; + this.insertView("ul.nav." + selector, new Views.IndexItem({ + ddoc: design, + index: key, + database: this.collection.database.id, + selected: selected && key == this.currView + })); + }, this); + }, + + beforeRender: function(manage) { + + var sidebarListViews = FauxtonAPI.getExtensions('sidebar:list'); + _.each(sidebarListViews, function (view) { + var extension = this.insertView('#extension-navs', view); + extension.update(this.database, this.collection, this.viewName); + extension.render(); + }, this); + + + this.collection.each(function(design) { + if (design.has('doc')){ + var ddoc = design.id.replace(/^_design\//,""); + if (design.get('doc').views){ + this.buildIndexList(design.get('doc').views, "views", ddoc); + } + } + }, this); + }, + + + afterRender: function () { + if (this.selectedTab) { + this.setSelectedTab(this.selectedTab); + } + }, + + setSelectedTab: function (selectedTab) { + this.selectedTab = selectedTab; + this.$('li').removeClass('active'); + this.$('#' + selectedTab).parent().addClass('active'); + } + }); + + Views.Indexed = FauxtonAPI.View.extend({}); + + Views.Changes = FauxtonAPI.View.extend({ + template: "templates/documents/changes", + + establish: function() { + return [ this.model.changes.fetch()]; + }, + + serialize: function () { + return { + changes: this.model.changes.toJSON(), + database: this.model + }; + }, + + afterRender: function(){ + prettyPrint(); + } + }); + + Views.DdocInfo = FauxtonAPI.View.extend({ + template: "templates/documents/ddoc_info", + + initialize: function (options) { + this.refreshTime = options.refreshTime || 5000; + this.listenTo(this.model, 'change', this.render); + }, + + serialize: function () { + return { + view_index: this.model.get('view_index') + }; + }, + + afterRender: function () { + this.startRefreshInterval(); + }, + + startRefreshInterval: function () { + var model = this.model; + + // Interval already set + if (this.intervalId) { return ; } + + this.intervalId = setInterval(function () { + model.fetch(); + }, this.refreshTime); + }, + + stopRefreshInterval: function () { + clearInterval(this.intervalId); + }, + + cleanup: function () { + this.stopRefreshInterval(); + } + }); + + Documents.Views = Views; + return Documents; +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/5f9a88f6/app/addons/fauxton/base.js ---------------------------------------------------------------------- diff --git a/app/addons/fauxton/base.js b/app/addons/fauxton/base.js new file mode 100644 index 0000000..aa4c3d4 --- /dev/null +++ b/app/addons/fauxton/base.js @@ -0,0 +1,271 @@ +// 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", + // Libs + "backbone", + "resizeColumns", +], + +function(app, Backbone, resizeColumns) { + + + //resizeAnimation + app.resizeColumns = new resizeColumns({}); + app.resizeColumns.onResizeHandler(); + + var Fauxton = {}; + + Fauxton.Breadcrumbs = Backbone.View.extend({ + template: "templates/fauxton/breadcrumbs", + + serialize: function() { + var crumbs = _.clone(this.crumbs); + return { + crumbs: crumbs + }; + }, + + initialize: function(options) { + this.crumbs = options.crumbs; + } + }); + + Fauxton.VersionInfo = Backbone.Model.extend({ + url: app.host + }); + + // TODO: this View should extend from FauxtonApi.View. + // Chicken and egg problem, api.js extends fauxton/base.js. + // Need to sort the loading order. + Fauxton.Footer = Backbone.View.extend({ + template: "templates/fauxton/footer", + + initialize: function() { + this.versionInfo = new Fauxton.VersionInfo(); + }, + + establish: function() { + return [this.versionInfo.fetch()]; + }, + + serialize: function() { + return { + version: this.versionInfo.get("version") + }; + } + }); + + Fauxton.NavBar = Backbone.View.extend({ + className:"navbar", + template: "templates/fauxton/nav_bar", + // TODO: can we generate this list from the router? + navLinks: [ + {href:"#/_all_dbs", title:"Databases", icon: "fonticon-database", className: 'databases'} + ], + + bottomNavLinks: [], + footerNavLinks: [], + + serialize: function() { + return { + navLinks: this.navLinks, + bottomNavLinks: this.bottomNavLinks, + footerNavLinks: this.footerNavLinks + }; + }, + + addLink: function(link) { + // link.top means it gets pushed to the top of the array, + // link.bottomNav means it goes to the additional bottom nav + // link.footerNav means goes to the footer nav + if (link.top && !link.bottomNav){ + this.navLinks.unshift(link); + } else if (link.top && link.bottomNav){ + this.bottomNavLinks.unshift(link); + } else if (link.bottomNav) { + this.bottomNavLinks.push(link); + } else if (link.footerNav) { + this.footerNavLinks.push(link); + } else { + this.navLinks.push(link); + } + }, + + removeLink: function (removeLink) { + var links = this.navlinks; + + if (removeLink.bottomNav) { + links = this.bottomNavLinks; + } else if (removeLink.footerNav) { + links = this.footerNavLinks; + } + + var foundIndex = -1; + + _.each(links, function (link, index) { + if (link.title === removeLink.title) { + foundIndex = index; + } + }); + + if (foundIndex === -1) {return;} + links.splice(foundIndex, 1); + this.render(); + }, + + afterRender: function(){ + + $('#primary-navbar li[data-nav-name="' + app.selectedHeader + '"]').addClass('active'); + + var menuOpen = true; + var $selectorList = $('body'); + $('.brand').off(); + $('.brand').on({ + click: function(e){ + if(!$(e.target).is('a')){ + toggleMenu(); + } + } + }); + + function toggleMenu(){ + $selectorList.toggleClass('closeMenu'); + menuOpen = $selectorList.hasClass('closeMenu'); + app.resizeColumns.onResizeHandler(); + } + + $('#primary-navbar').on("click", ".nav a", function(){ + if (!($selectorList.hasClass('closeMenu'))){ + setTimeout( + function(){ + $selectorList.addClass('closeMenu'); + app.resizeColumns.onResizeHandler(); + },3000); + + } + }); + + app.resizeColumns.initialize(); + }, + + beforeRender: function () { + this.addLinkViews(); + }, + + addLinkViews: function () { + var that = this; + + _.each(_.union(this.navLinks, this.bottomNavLinks), function (link) { + if (!link.view) { return; } + + //TODO check if establish is a function + var establish = link.establish || []; + $.when.apply(null, establish).then( function () { + var selector = link.bottomNav ? '#bottom-nav-links' : '#nav-links'; + that.insertView(selector, link.view).render(); + }); + }, this); + } + + // TODO: ADD ACTIVE CLASS + }); + + Fauxton.ApiBar = Backbone.View.extend({ + template: "templates/fauxton/api_bar", + endpoint: '_all_docs', + + documentation: 'docs', + + events: { + "click .api-url-btn" : "toggleAPIbar" + }, + + toggleAPIbar: function(e){ + var $currentTarget = $(e.currentTarget).find('span'); + if ($currentTarget.hasClass("fonticon-plus")){ + $currentTarget.removeClass("fonticon-plus").addClass("fonticon-minus"); + }else{ + $currentTarget.removeClass("fonticon-minus").addClass("fonticon-plus"); + } + + $('.api-navbar').toggle(); + + }, + + serialize: function() { + return { + endpoint: this.endpoint, + documentation: this.documentation + }; + }, + + hide: function(){ + this.$el.addClass('hide'); + }, + show: function(){ + this.$el.removeClass('hide'); + }, + update: function(endpoint) { + this.show(); + this.endpoint = endpoint[0]; + this.documentation = endpoint[1]; + this.render(); + } + + }); + + Fauxton.Notification = Backbone.View.extend({ + fadeTimer: 5000, + + initialize: function(options) { + this.msg = options.msg; + this.type = options.type || "info"; + this.selector = options.selector; + this.fade = options.fade === undefined ? true : options.fade; + this.clear = options.clear; + this.data = options.data || ""; + this.template = options.template || "templates/fauxton/notification"; + }, + + serialize: function() { + return { + data: this.data, + msg: this.msg, + type: this.type + }; + }, + + delayedFade: function() { + var that = this; + if (this.fade) { + setTimeout(function() { + that.$el.fadeOut(); + }, this.fadeTimer); + } + }, + + renderNotification: function(selector) { + selector = selector || this.selector; + if (this.clear) { + $(selector).html(''); + } + this.render().$el.appendTo(selector); + this.delayedFade(); + return this; + } + }); + + + return Fauxton; +});