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 BF34F1740A for ; Wed, 11 Mar 2015 23:16:13 +0000 (UTC) Received: (qmail 84058 invoked by uid 500); 11 Mar 2015 23:16:13 -0000 Delivered-To: apmail-couchdb-commits-archive@couchdb.apache.org Received: (qmail 84006 invoked by uid 500); 11 Mar 2015 23:16:13 -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 83997 invoked by uid 99); 11 Mar 2015 23:16:13 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 11 Mar 2015 23:16:13 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 7DF05E10A9; Wed, 11 Mar 2015 23:16:13 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: benkeen@apache.org To: commits@couchdb.apache.org Message-Id: <29c2a43801c14aa89afcd181d8510cb5@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: fauxton commit: updated refs/heads/master to b3e5b44 Date: Wed, 11 Mar 2015 23:16:13 +0000 (UTC) Repository: couchdb-fauxton Updated Branches: refs/heads/master b51fec6d3 -> b3e5b44e2 Porting over Changes page content to React This also fixes COUCHDB-2216 by limiting the max number of changes that can be output at one time to 1000. Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/b3e5b44e Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/b3e5b44e Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/b3e5b44e Branch: refs/heads/master Commit: b3e5b44e278c6aa750eb21c1b2f1025ede83c9a8 Parents: b51fec6 Author: Ben Keen Authored: Fri Feb 27 11:25:10 2015 -0800 Committer: Ben Keen Committed: Wed Mar 11 16:17:19 2015 -0700 ---------------------------------------------------------------------- app/addons/databases/resources.js | 4 + app/addons/documents/assets/less/changes.less | 17 +- app/addons/documents/changes/actions.js | 11 +- app/addons/documents/changes/actiontypes.js | 6 +- .../documents/changes/components.react.jsx | 204 ++++++++++++++++--- app/addons/documents/changes/stores.js | 97 ++++++--- app/addons/documents/routes-documents.js | 14 +- app/addons/documents/templates/changes.html | 69 ------- .../tests/changes.componentsSpec.react.jsx | 195 +++++++++++++++++- .../documents/tests/changes.storesSpec.js | 74 ++++--- .../documents/tests/nightwatch/changes.js | 2 +- .../documents/tests/nightwatch/changesFilter.js | 48 +++++ .../tests/nightwatch/previousFromView.js | 2 +- app/addons/documents/tests/views-changesSpec.js | 94 --------- app/addons/documents/views-changes.js | 89 ++------ app/addons/fauxton/components.react.jsx | 76 ++++++- assets/less/animations.less | 19 ++ 17 files changed, 673 insertions(+), 348 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/databases/resources.js ---------------------------------------------------------------------- diff --git a/app/addons/databases/resources.js b/app/addons/databases/resources.js index ba883b6..17113d4 100644 --- a/app/addons/databases/resources.js +++ b/app/addons/databases/resources.js @@ -77,6 +77,10 @@ function(app, FauxtonAPI, Documents) { return app.utils.safeURLName(this.id); }, buildChanges: function (params) { + if (!params.limit) { + params.limit = 100; + } + this.changes = new Databases.Changes({ database: this, params: params http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/assets/less/changes.less ---------------------------------------------------------------------- diff --git a/app/addons/documents/assets/less/changes.less b/app/addons/documents/assets/less/changes.less index b6cde72..0e98ecd 100644 --- a/app/addons/documents/assets/less/changes.less +++ b/app/addons/documents/assets/less/changes.less @@ -86,12 +86,23 @@ height: 0px; }); - -.toggleChangesFilter-enter { +.toggle-changes-filter-enter { .animation(slideDownChangesFilter 1s both); } -.toggleChangesFilter-leave { +.toggle-changes-filter-leave { .animation(slideUpChangesFilter 1s both); } +.toggle-changes-code-enter { + .animation(slideDown .6s both); +} + +.toggle-changes-code-leave { + .animation(slideUp .6s both); +} + +.changes-result-limit { + margin-left: 20px; +} + http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/changes/actions.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/changes/actions.js b/app/addons/documents/changes/actions.js index bcc6002..a62a6fe 100644 --- a/app/addons/documents/changes/actions.js +++ b/app/addons/documents/changes/actions.js @@ -29,9 +29,6 @@ function (app, FauxtonAPI, ActionTypes) { type: ActionTypes.ADD_CHANGES_FILTER_ITEM, filter: filter }); - - // TODO for backward compatibility. Remove later. - FauxtonAPI.triggerRouteEvent('changesFilterAdd', filter); }, removeFilter: function (filter) { @@ -39,9 +36,13 @@ function (app, FauxtonAPI, ActionTypes) { type: ActionTypes.REMOVE_CHANGES_FILTER_ITEM, filter: filter }); + }, - // TODO for backward compatibility. Remove later. - FauxtonAPI.triggerRouteEvent('changesFilterRemove', filter); + setChanges: function (options) { + FauxtonAPI.dispatch({ + type: ActionTypes.SET_CHANGES, + options: options + }); } }; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/changes/actiontypes.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/changes/actiontypes.js b/app/addons/documents/changes/actiontypes.js index c76b844..7f91ae6 100644 --- a/app/addons/documents/changes/actiontypes.js +++ b/app/addons/documents/changes/actiontypes.js @@ -10,11 +10,13 @@ // License for the specific language governing permissions and limitations under // the License. -define([], function() { +define([], function () { return { + SET_CHANGES: 'SET_CHANGES', TOGGLE_CHANGES_TAB_VISIBILITY: 'TOGGLE_CHANGES_TAB_VISIBILITY', ADD_CHANGES_FILTER_ITEM: 'ADD_CHANGES_FILTER_ITEM', REMOVE_CHANGES_FILTER_ITEM: 'REMOVE_CHANGES_FILTER_ITEM', - UPDATE_CHANGES_FILTER: 'UPDATE_CHANGES_FILTER' + UPDATE_CHANGES_FILTER: 'UPDATE_CHANGES_FILTER', + TOGGLE_CHANGES_CODE_VISIBILITY: 'TOGGLE_CHANGES_CODE_VISIBILITY' }; }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/changes/components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/documents/changes/components.react.jsx b/app/addons/documents/changes/components.react.jsx index 977319e..019f3bf 100644 --- a/app/addons/documents/changes/components.react.jsx +++ b/app/addons/documents/changes/components.react.jsx @@ -11,34 +11,40 @@ // the License. define([ + 'app', + 'api', 'react', 'addons/documents/changes/actions', - 'addons/documents/changes/stores' -], function (React, Actions, Stores) { + 'addons/documents/changes/stores', + 'addons/fauxton/components.react', + 'plugins/prettify' +], function (app, FauxtonAPI, React, Actions, Stores, Components) { + var changesStore = Stores.changesStore; var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; - - // the top-level component for the Changes Filter section. Handles hiding/showing - var ChangesHeader = React.createClass({ + // the top-level component for the Changes Filter section. Handles hiding/showing of the filters form + var ChangesHeaderController = React.createClass({ getInitialState: function () { + return this.getStoreState(); + }, + + getStoreState: function () { return { - showTabContent: Stores.changesHeaderStore.isTabVisible() + showTabContent: changesStore.isTabVisible() }; }, onChange: function () { - this.setState({ - showTabContent: Stores.changesHeaderStore.isTabVisible() - }); + this.setState(this.getStoreState()); }, componentDidMount: function () { - Stores.changesHeaderStore.on('change', this.onChange, this); + changesStore.on('change', this.onChange, this); }, componentWillUnmount: function () { - Stores.changesHeaderStore.off('change', this.onChange); + changesStore.off('change', this.onChange); }, toggleFilterSection: function () { @@ -54,7 +60,7 @@ define([ return (
- + {tabContent}
@@ -87,7 +93,7 @@ define([ var ChangesFilter = React.createClass({ getStoreState: function () { return { - filters: Stores.changesFilterStore.getFilters() + filters: changesStore.getFilters() }; }, @@ -96,11 +102,11 @@ define([ }, componentDidMount: function () { - Stores.changesFilterStore.on('change', this.onChange, this); + changesStore.on('change', this.onChange, this); }, componentWillUnmount: function () { - Stores.changesFilterStore.off('change', this.onChange); + changesStore.off('change', this.onChange); }, getInitialState: function () { @@ -125,19 +131,17 @@ define([ }, hasFilter: function (filter) { - return Stores.changesFilterStore.hasFilter(filter); + return changesStore.hasFilter(filter); }, render: function () { - var filters = this.getFilters(); - return (
-
    {filters}
+
    {this.getFilters()}
@@ -268,18 +272,172 @@ define([ }); + var ChangesController = React.createClass({ + getInitialState: function () { + return this.getStoreState(); + }, + + getStoreState: function () { + return { + changes: changesStore.getChanges(), + databaseName: changesStore.getDatabaseName(), + isShowingSubset: changesStore.isShowingSubset() + }; + }, + + onChange: function () { + this.setState(this.getStoreState()); + }, + + componentDidMount: function () { + changesStore.on('change', this.onChange, this); + }, + + componentWillUnmount: function () { + changesStore.off('change', this.onChange); + }, + + showingSubsetMsg: function () { + var msg = ''; + if (this.state.isShowingSubset) { + var numChanges = this.state.changes.length; + msg =

Limiting results to latest {numChanges} changes.

; + } + return msg; + }, + + getRows: function () { + return _.map(this.state.changes, function (change) { + return ; + }, this); + }, + + render: function () { + return ( +
+ {this.showingSubsetMsg()} + {this.getRows()} +
+ ); + } + }); + + + var ChangeRow = React.createClass({ + propTypes: function () { + return { + change: React.PropTypes.object, + databaseName: React.PropTypes.string.isRequired + }; + }, + + getInitialState: function () { + return { + codeVisible: false + }; + }, + + toggleJSON: function (e) { + e.preventDefault(); + this.setState({ codeVisible: !this.state.codeVisible }); + }, + + getChangesCode: function () { + var json = ''; + if (this.state.codeVisible) { + json = ; + } + return json; + }, + + getChangeCode: function () { + return { + changes: this.props.change.changes, + doc: this.props.change.doc + }; + }, + + render: function () { + var jsonBtnClasses = "btn btn-small " + (this.state.codeVisible ? 'btn-secondary' : 'btn-primary'); + + return ( +
+
+
+
seq
+
{this.props.change.seq}
+
+ +
+
+ +
+
id
+
+ +
+
+ +
+
+ +
+
changes
+
+ +
+
+ + + {this.getChangesCode()} + + +
+
deleted
+
{this.props.change.deleted ? 'True' : 'False'}
+
+
+
+ ); + } + }); + + + var ChangeID = React.createClass({ + render: function () { + if (this.props.deleted) { + return ( + {this.props.id} + ); + } else { + var link = FauxtonAPI.urls('document', 'app', this.props.databaseName, this.props.id); + return ( + {this.props.id} + ); + } + } + }); + + return { renderHeader: function (el) { - React.render(, el); + React.render(, el); + }, + renderChanges: function (el) { + React.render(, el); }, - removeHeader: function (el) { + remove: function (el) { React.unmountComponentAtNode(el); }, // exposed for testing purposes only - ChangesHeader: ChangesHeader, + ChangesHeaderController: ChangesHeaderController, ChangesHeaderTab: ChangesHeaderTab, - ChangesFilter: ChangesFilter + ChangesFilter: ChangesFilter, + ChangesController: ChangesController, + ChangeRow: ChangeRow }; }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/changes/stores.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/changes/stores.js b/app/addons/documents/changes/stores.js index e36b392..2e8f279 100644 --- a/app/addons/documents/changes/stores.js +++ b/app/addons/documents/changes/stores.js @@ -15,51 +15,62 @@ define([ 'addons/documents/changes/actiontypes' ], function (FauxtonAPI, ActionTypes) { - var Stores = {}; - - // tracks the state of the header (open/closed) - var ChangesHeaderStore = FauxtonAPI.Store.extend({ + var ChangesStore = FauxtonAPI.Store.extend({ initialize: function () { this.reset(); }, reset: function () { this._tabVisible = false; + this._filters = []; + this._changes = []; + this._databaseName = ''; + this._maxChangesListed = 100; + this._showingSubset = false; }, - toggleTabVisibility: function () { - this._tabVisible = !this._tabVisible; + setChanges: function (options) { + this._filters = options.filters; + this._databaseName = options.databaseName; + this._changes = _.map(options.changes.models, function (change) { + return { + id: change.get('id'), + seq: change.get('seq'), + deleted: change.get('deleted') ? change.get('deleted') : false, + changes: change.get('changes'), + doc: change.get('doc') // only populated with ?include_docs=true + }; + }); }, - isTabVisible: function () { - return this._tabVisible; + getChanges: function () { + this._showingSubset = false; + var numMatches = 0; + + return _.filter(this._changes, function (change) { + if (numMatches >= this._maxChangesListed) { + this._showingSubset = true; + return false; + } + var changeStr = JSON.stringify(change); + var match = _.every(this._filters, function (filter) { + return new RegExp(filter, 'i').test(changeStr); + }); + + if (match) { + numMatches++; + } + return match; + }, this); }, - dispatch: function (action) { - - // can I use an if-statement for a single item? - switch (action.type) { - case ActionTypes.TOGGLE_CHANGES_TAB_VISIBILITY: - this.toggleTabVisibility(); - this.triggerChange(); - break; - } - } - }); - - Stores.changesHeaderStore = new ChangesHeaderStore(); - Stores.changesHeaderStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.changesHeaderStore.dispatch); - - - // tracks the list of filters - var ChangesFilterStore = FauxtonAPI.Store.extend({ - initialize: function () { - this.reset(); + toggleTabVisibility: function () { + this._tabVisible = !this._tabVisible; }, - reset: function () { - this._filters = []; + isTabVisible: function () { + return this._tabVisible; }, addFilter: function (filter) { @@ -78,8 +89,29 @@ define([ return _.contains(this._filters, filter); }, + getDatabaseName: function () { + return this._databaseName; + }, + + isShowingSubset: function () { + return this._showingSubset; + }, + + // added to speed up the tests + setMaxChanges: function (num) { + this._maxChangesListed = num; + }, + dispatch: function (action) { switch (action.type) { + case ActionTypes.SET_CHANGES: + this.setChanges(action.options); + this.triggerChange(); + break; + case ActionTypes.TOGGLE_CHANGES_TAB_VISIBILITY: + this.toggleTabVisibility(); + this.triggerChange(); + break; case ActionTypes.ADD_CHANGES_FILTER_ITEM: this.addFilter(action.filter); this.triggerChange(); @@ -92,9 +124,10 @@ define([ } }); - Stores.changesFilterStore = new ChangesFilterStore(); - Stores.changesFilterStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.changesFilterStore.dispatch); + var Stores = {}; + Stores.changesStore = new ChangesStore(); + Stores.changesStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.changesStore.dispatch); return Stores; }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/routes-documents.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js index c8b35b0..0131adc 100644 --- a/app/addons/documents/routes-documents.js +++ b/app/addons/documents/routes-documents.js @@ -48,8 +48,6 @@ function(app, FauxtonAPI, BaseRoute, Documents, Changes, Index, DocEditor, events: { "route:reloadDesignDocs": "reloadDesignDocs", - "route:changesFilterAdd": "addFilter", - "route:changesFilterRemove": "removeFilter", 'route:updateAllDocs': 'updateAllDocsFromView', 'route:paginate': 'paginate', 'route:perPageChange': 'perPageChange', @@ -194,7 +192,7 @@ function(app, FauxtonAPI, BaseRoute, Documents, Changes, Index, DocEditor, var docParams = app.getParams(); this.database.buildChanges(docParams); - this.changesView = this.setView("#dashboard-lower-content", new Changes.Changes({ + this.changesView = this.setView("#dashboard-lower-content", new Changes.ChangesReactWrapper({ model: this.database })); @@ -214,16 +212,6 @@ function(app, FauxtonAPI, BaseRoute, Documents, Changes, Index, DocEditor, }; }, - addFilter: function (filter) { - this.changesView.filters.push(filter); - this.changesView.render(); - }, - - removeFilter: function (filter) { - this.changesView.filters.splice(this.changesView.filters.indexOf(filter), 1); - this.changesView.render(); - }, - cleanup: function () { if (this.reactHeader) { this.removeView('#react-headerbar'); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/templates/changes.html ---------------------------------------------------------------------- diff --git a/app/addons/documents/templates/changes.html b/app/addons/documents/templates/changes.html deleted file mode 100644 index e06e34b..0000000 --- a/app/addons/documents/templates/changes.html +++ /dev/null @@ -1,69 +0,0 @@ - -
- <% _.each(changes, function (change) { %> -
-
-
-
- seq -
-
- <%- change.seq %> -
-
- - - -
-
-
-
- id -
-
- <% if (change.deleted) { %> - <%- change.id %> - <% } else { %> - <%- change.id %> - <% } %>
-
- - - -
-
-
-
- changes -
-
- -
-
-
-
<%- JSON.stringify({changes: change.changes, doc: change.doc}, null, " ") %>
-
-
-
- deleted -
-
- <%- change.deleted ? "True" : "False" %> -
-
-
-
- <% }); %> -
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/tests/changes.componentsSpec.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/changes.componentsSpec.react.jsx b/app/addons/documents/tests/changes.componentsSpec.react.jsx index 4f3dce8..ecfb8bb 100644 --- a/app/addons/documents/tests/changes.componentsSpec.react.jsx +++ b/app/addons/documents/tests/changes.componentsSpec.react.jsx @@ -25,6 +25,15 @@ define([ var TestUtils = React.addons.TestUtils; + // suppresses unwanted console.log()'s on missing URLs + FauxtonAPI.registerUrls('document', { + server: function (database, doc) { return app.host + '/' + database + '/' + doc; }, + app: function (database, doc) { return '/database/' + database + '/' + doc; }, + apiurl: function (database, doc) { return window.location.origin + '/' + database + '/' + doc; }, + 'web-index': function (database, doc) { return '/database/' + database + '/' + doc; } + }); + + describe('ChangesHeader', function () { var container, tab, spy; @@ -32,11 +41,11 @@ define([ beforeEach(function () { spy = sinon.spy(Actions, 'toggleTabVisibility'); container = document.createElement('div'); - tab = TestUtils.renderIntoDocument(, container); + tab = TestUtils.renderIntoDocument(, container); }); afterEach(function () { - Stores.changesFilterStore.reset(); + Stores.changesStore.reset(); React.unmountComponentAtNode(container); }); @@ -58,7 +67,7 @@ define([ }); afterEach(function () { - Stores.changesFilterStore.reset(); + Stores.changesStore.reset(); React.unmountComponentAtNode(container); }); @@ -78,7 +87,7 @@ define([ }); afterEach(function () { - Stores.changesFilterStore.reset(); + Stores.changesStore.reset(); React.unmountComponentAtNode(container); }); @@ -179,7 +188,185 @@ define([ assert.equal(1, $el.find('.js-remove-filter').length); }); + }); + + + // tests Changes Controller; includes tests in conjunction with ChangesHeaderController + describe('ChangesController', function () { + var containerEl, headerEl, $headerEl, changesEl, $changesEl; + + var changesCollection = new Backbone.Collection([ + { id: 'doc_1', seq: 4, deleted: false, changes: { code: 'here' } }, + { id: 'doc_2', seq: 1, deleted: false, changes: { code: 'here' } }, + { id: 'doc_3', seq: 6, deleted: true, changes: { code: 'here' } }, + { id: 'doc_4', seq: 7, deleted: false, changes: { code: 'here' } }, + { id: 'doc_5', seq: 1, deleted: true, changes: { code: 'here' } } + ]); + + beforeEach(function () { + Actions.setChanges({ + changes: changesCollection, + filters: [], + databaseName: 'testDatabase' + }); + headerEl = TestUtils.renderIntoDocument(, containerEl); + $headerEl = $(headerEl.getDOMNode()); + changesEl = TestUtils.renderIntoDocument(, containerEl); + $changesEl = $(changesEl.getDOMNode()); + }); + + afterEach(function () { + Stores.changesStore.reset(); + React.unmountComponentAtNode(containerEl); + }); + + + it('should list the right number of changes', function () { + assert.equal(changesCollection.length, $changesEl.find('.change-box').length); + }); + + + it('"false"/"true" filter strings should apply to change deleted status', function () { + // expand the header + TestUtils.Simulate.click($headerEl.find('a')[0]); + + // add a filter + var addItemField = $headerEl.find('.js-changes-filter-field')[0]; + var submitBtn = $headerEl.find('[type="submit"]')[0]; + addItemField.value = 'true'; + TestUtils.Simulate.change(addItemField); + TestUtils.Simulate.submit(submitBtn); + + // confirm only the two deleted items shows up and the IDs maps to the deleted rows + assert.equal(2, $changesEl.find('.change-box').length); + assert.equal('doc_3', $($changesEl.find('.js-doc-id').get(0)).html()); + assert.equal('doc_5', $($changesEl.find('.js-doc-id').get(1)).html()); + }); + + + it('confirms that a filter affects the actual search results', function () { + // expand the header + TestUtils.Simulate.click($headerEl.find('a')[0]); + + // add a filter + var addItemField = $headerEl.find('.js-changes-filter-field')[0]; + var submitBtn = $headerEl.find('[type="submit"]')[0]; + addItemField.value = '6'; // should match doc_3's sequence ID + TestUtils.Simulate.change(addItemField); + TestUtils.Simulate.submit(submitBtn); + + // confirm only one item shows up and the ID maps to what we'd expect + assert.equal(1, $changesEl.find('.change-box').length); + assert.equal('doc_3', $($changesEl.find('.js-doc-id').get(0)).html()); + }); + + + // confirms that if there are multiple filters, ALL are applied to return the subset of results that match + // all filters + it('multiple filters should all be applied to results', function () { + TestUtils.Simulate.click($headerEl.find('a')[0]); + + // add the filters + var addItemField = $headerEl.find('.js-changes-filter-field')[0]; + var submitBtn = $headerEl.find('[type="submit"]')[0]; + + // *** should match doc_1, doc_2 and doc_5 + addItemField.value = '1'; + TestUtils.Simulate.change(addItemField); + TestUtils.Simulate.submit(submitBtn); + + // *** should match doc_3 and doc_5 + addItemField.value = 'true'; + TestUtils.Simulate.change(addItemField); + TestUtils.Simulate.submit(submitBtn); + + // confirm only one item shows up and that it's doc_5 + assert.equal(1, $changesEl.find('.change-box').length); + assert.equal('doc_5', $($changesEl.find('.js-doc-id').get(0)).html()); + }); + }); + + + describe('ChangesController max results', function () { + var containerEl, changesEl; + var maxChanges = 10; + + beforeEach(function () { + + // to keep the test speedy, override the default value (1000) + Stores.changesStore.setMaxChanges(maxChanges); + + var changes = []; + _.times(maxChanges + 10, function (i) { + changes.push(new Backbone.Model({ id: 'doc_' + i, seq: 1, changes: { code: 'here' } })); + }); + var changesCollection = new Backbone.Collection(changes); + + Actions.setChanges({ + changes: changesCollection, + filters: [], + databaseName: 'test' + }); + changesEl = TestUtils.renderIntoDocument(, containerEl); + }); + + afterEach(function () { + Stores.changesStore.reset(); + React.unmountComponentAtNode(containerEl); + }); + + it('should truncate the number of results with very large # of changes', function () { + // check there's no more than maxChanges results + assert.equal(maxChanges, $(changesEl.getDOMNode()).find('.change-box').length); + }); + + it('should show a message if the results are truncated', function () { + assert.equal(1, $(changesEl.getDOMNode()).find('.changes-result-limit').length); + }); }); + + describe('ChangeRow', function () { + var container; + var change = { + id: '123', + seq: 5, + deleted: false, + changes: { code: 'here' } + }; + + beforeEach(function () { + container = document.createElement('div'); + }); + + afterEach(function () { + React.unmountComponentAtNode(container); + }); + + + it('clicking the toggle-json button shows the code section', function () { + var changeRow = TestUtils.renderIntoDocument(, container); + + // confirm it's hidden by default + assert.equal(0, $(changeRow.getDOMNode()).find('.prettyprint').length); + + // confirm clicking it shows the element + TestUtils.Simulate.click($(changeRow.getDOMNode()).find('button.btn')[0]); + assert.equal(1, $(changeRow.getDOMNode()).find('.prettyprint').length); + }); + + it('deleted docs should not be clickable', function () { + change.deleted = true; + var changeRow = TestUtils.renderIntoDocument(, container); + assert.equal(0, $(changeRow.getDOMNode()).find('a.js-doc-link').length); + }); + + it('non-deleted docs should be clickable', function () { + change.deleted = false; + var changeRow = TestUtils.renderIntoDocument(, container); + assert.equal(1, $(changeRow.getDOMNode()).find('a.js-doc-link').length); + }); + }); + }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/tests/changes.storesSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/changes.storesSpec.js b/app/addons/documents/tests/changes.storesSpec.js index f140c7c..9bcc023 100644 --- a/app/addons/documents/tests/changes.storesSpec.js +++ b/app/addons/documents/tests/changes.storesSpec.js @@ -21,31 +21,30 @@ define([ var assert = utils.assert; - describe('ChangesHeaderStore', function () { - it('toggleTabVisibility() changes state in store', function() { - assert.ok(Stores.changesHeaderStore.isTabVisible() === false); - Stores.changesHeaderStore.toggleTabVisibility(); - assert.ok(Stores.changesHeaderStore.isTabVisible() === true); - }); + describe('ChangesStore', function () { - it('reset() changes tab visibility to hidden', function() { - Stores.changesHeaderStore.toggleTabVisibility(); - Stores.changesHeaderStore.reset(); - assert.ok(Stores.changesHeaderStore.isTabVisible() === false); - }); - }); + var collection = new Backbone.Collection(); + afterEach(function () { + Stores.changesStore.reset(); + }); - describe('ChangesFilterStore', function () { + it('toggleTabVisibility() changes state in store', function() { + assert.ok(Stores.changesStore.isTabVisible() === false); + Stores.changesStore.toggleTabVisibility(); + assert.ok(Stores.changesStore.isTabVisible() === true); + }); - afterEach(function () { - Stores.changesFilterStore.reset(); + it('reset() changes tab visibility to hidden', function() { + Stores.changesStore.toggleTabVisibility(); + Stores.changesStore.reset(); + assert.ok(Stores.changesStore.isTabVisible() === false); }); it('addFilter() adds item in store', function () { var filter = 'My filter'; - Stores.changesFilterStore.addFilter(filter); - var filters = Stores.changesFilterStore.getFilters(); + Stores.changesStore.addFilter(filter); + var filters = Stores.changesStore.getFilters(); assert.ok(filters.length === 1); assert.ok(filters[0] === filter); }); @@ -53,21 +52,50 @@ define([ it('removeFilter() removes item from store', function () { var filter1 = 'My filter 1'; var filter2 = 'My filter 2'; - Stores.changesFilterStore.addFilter(filter1); - Stores.changesFilterStore.addFilter(filter2); - Stores.changesFilterStore.removeFilter(filter1); + Stores.changesStore.addFilter(filter1); + Stores.changesStore.addFilter(filter2); + Stores.changesStore.removeFilter(filter1); - var filters = Stores.changesFilterStore.getFilters(); + var filters = Stores.changesStore.getFilters(); assert.ok(filters.length === 1); assert.ok(filters[0] === filter2); }); it('hasFilter() finds item in store', function () { var filter = 'My filter'; - Stores.changesFilterStore.addFilter(filter); - assert.ok(Stores.changesFilterStore.hasFilter(filter) === true); + Stores.changesStore.addFilter(filter); + assert.ok(Stores.changesStore.hasFilter(filter) === true); + }); + + + it('getDatabaseName() returns database name', function () { + var dbName = 'hoopoes'; + Stores.changesStore.setChanges({ databaseName: dbName, changes: collection }); + assert.equal(Stores.changesStore.getDatabaseName(), dbName); + + Stores.changesStore.reset(); + assert.equal(Stores.changesStore.getDatabaseName(), ''); }); + it("getChanges() should return a subset if there are a lot of changes", function () { + + // to keep the test speedy, override the default max value + var maxChanges = 10; + Stores.changesStore.setMaxChanges(maxChanges); + + var changes = []; + _.times(maxChanges + 10, function (i) { + changes.push(new Backbone.Model({ id: 'doc_' + i, seq: 1, changes: { } })); + }); + var changesCollection = new Backbone.Collection(changes); + Stores.changesStore.setChanges({ + changes: changesCollection, + databaseName: "test" + }); + + var results = Stores.changesStore.getChanges(); + assert.equal(maxChanges, results.length); + }); }); }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/tests/nightwatch/changes.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/changes.js b/app/addons/documents/tests/nightwatch/changes.js index 90eec6e..9f55fe0 100644 --- a/app/addons/documents/tests/nightwatch/changes.js +++ b/app/addons/documents/tests/nightwatch/changes.js @@ -21,7 +21,7 @@ module.exports = { .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs') .waitForElementPresent('.control-toggle-alternative-header', waitTime, false) .click('#changes') - .waitForElementVisible('.changes-view', waitTime, false) + .waitForElementPresent('.js-changes-view', waitTime, false) .assert.elementNotPresent('.control-toggle-alternative-header') .end(); } http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/tests/nightwatch/changesFilter.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/changesFilter.js b/app/addons/documents/tests/nightwatch/changesFilter.js new file mode 100644 index 0000000..8663fd1 --- /dev/null +++ b/app/addons/documents/tests/nightwatch/changesFilter.js @@ -0,0 +1,48 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +module.exports = { + + // some basic test for the changes page. All of this and more is covered in the + // changes.componentsSpec.react.jsx mocha tests; this is more of a sanity end-to-end test + 'Check changes filter results' : function (client) { + + var waitTime = 10000, + newDatabaseName = client.globals.testDatabaseName, + baseUrl = client.globals.test_settings.launch_url; + + client + .loginToGUI() + .createDocument('doc_1', newDatabaseName) + .createDocument('doc_2', newDatabaseName) + .createDocument('doc_3', newDatabaseName) + .url(baseUrl + '/#/database/' + newDatabaseName + '/_changes') + + // confirm all 3 changes are there + .waitForElementPresent('.change-box[data-id="doc_1"]', waitTime, false) + .waitForElementPresent('.change-box[data-id="doc_2"]', waitTime, false) + .waitForElementPresent('.change-box[data-id="doc_3"]', waitTime, false) + + // add a filter + .click("#db-views-tabs-nav a") + .waitForElementVisible('.js-changes-filter-field', waitTime, false) + .setValue('.js-changes-filter-field', "doc_1") + .click('.js-filter-form button[type="submit"]') + + // confirm only the single result is now listed in the page + .waitForElementVisible('span.label-info', waitTime, false) + .waitForElementPresent('.change-box[data-id="doc_1"]', waitTime, false) + .waitForElementNotPresent('.change-box[data-id="doc_2"]', waitTime, false) + .waitForElementNotPresent('.change-box[data-id="doc_3"]', waitTime, false) + .end(); + } +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/tests/nightwatch/previousFromView.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/previousFromView.js b/app/addons/documents/tests/nightwatch/previousFromView.js index 062427b..2abc379 100644 --- a/app/addons/documents/tests/nightwatch/previousFromView.js +++ b/app/addons/documents/tests/nightwatch/previousFromView.js @@ -24,7 +24,7 @@ module.exports = { .clickWhenVisible('#nav-design-function-keyviewviews') .clickWhenVisible('#keyview_keyview') .clickWhenVisible('.breadcrumb-back-link .fonticon-left-open') - .waitForElementPresent('.changes-view', waitTime) + .waitForElementPresent('.js-changes-view', waitTime) .end(); } }; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/tests/views-changesSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/views-changesSpec.js b/app/addons/documents/tests/views-changesSpec.js deleted file mode 100644 index 362a0e9..0000000 --- a/app/addons/documents/tests/views-changesSpec.js +++ /dev/null @@ -1,94 +0,0 @@ -// 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/views-changes', - 'addons/databases/base', - 'testUtils' -], function (Views, Databases, testUtils) { - var assert = testUtils.assert, - ViewSandbox = testUtils.ViewSandbox, - viewSandbox; - - describe('Documents Changes', function () { - var model, - filteredView, - handlerSpy, - view; - - - beforeEach(function (done) { - var database = new Databases.Model({id: 'bla'}); - - model = new Databases.Model({id: 'foo'}); - model.buildChanges(); - - database.buildChanges({descending: 'true', limit: '100', include_docs: 'true'} ); - filteredView = new Views.Changes({ - model: database - }); - - handlerSpy = sinon.spy(Views.Changes.prototype, 'toggleJson'); - - view = new Views.Changes({ - model: model - }); - viewSandbox = new ViewSandbox(); - viewSandbox.renderView(view, done); - }); - - afterEach(function () { - handlerSpy.restore(); - view.afterRender.restore && view.afterRender.restore(); - viewSandbox.remove(); - }); - - it('does not keep filters in memory', function () { - view.filters.push('cat'); - view = new Views.Changes({ - model: model - }); - - view.filters.push('mat'); - - assert.deepEqual(view.filters, ['mat']); - }); - - it('filter false in case of deleted documents in the changes feed', function () { - filteredView.filters = [false]; - var res = filteredView.createFilteredData([ - {id: 'LALA', bar: 'ENTE'}, - {id: '1', bar: '1', deleted: true}, - {id: '2', bar: '2'} - ]); - - assert.equal(res.length, 2); - }); - - it('the toggle-json button calls a handler', function () { - view.$('.js-toggle-json').trigger('click'); - assert.ok(handlerSpy.calledOnce); - }); - - it('rerenders on the sync event', function () { - var spy = sinon.spy(view, 'afterRender'); - model.changes.trigger('sync'); - - assert.ok(spy.calledOnce); - }); - - it('rerenders on the cachesync event', function () { - var spy = sinon.spy(view, 'afterRender'); - model.changes.trigger('cachesync'); - assert.ok(spy.calledOnce); - }); - }); -}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/documents/views-changes.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/views-changes.js b/app/addons/documents/views-changes.js index 11add17..fd56412 100644 --- a/app/addons/documents/views-changes.js +++ b/app/addons/documents/views-changes.js @@ -18,108 +18,49 @@ define([ // Libs "addons/fauxton/components", 'addons/documents/changes/components.react', - - // Plugins - "plugins/prettify" + 'addons/documents/changes/actions' ], -function(app, FauxtonAPI, Components, Changes, prettify, ZeroClipboard) { +function(app, FauxtonAPI, Components, Changes, ChangesActions) { var Views = {}; - // wrapper for React component. The wrapper allows us to tie the React component into the Fauxton + // wrappers for React components. The wrapper allows us to tie the React component into the Fauxton // page load lifecycle Views.ChangesHeaderReactWrapper = FauxtonAPI.View.extend({ afterRender: function () { Changes.renderHeader(this.el); }, cleanup: function () { - Changes.removeHeader(this.el); + Changes.remove(this.el); } }); - Views.Changes = Components.FilteredView.extend({ - template: "addons/documents/templates/changes", - + Views.ChangesReactWrapper = FauxtonAPI.View.extend({ initialize: function () { - this.listenTo(this.model.changes, 'sync', this.render); - this.listenTo(this.model.changes, 'cachesync', this.render); this.filters = []; }, - events: { - "click button.js-toggle-json": "toggleJson" - }, - - toggleJson: function(event) { - event.preventDefault(); - - var $button = this.$(event.target), - $container = $button.closest('.change-box').find(".js-json-container"); - - if (!$container.is(":visible")) { - $button - .text("Close JSON") - .addClass("btn-secondary") - .removeClass("btn-primary"); - } else { - $button.text("View JSON") - .addClass("btn-primary") - .removeClass("btn-secondary"); - } - - $container.slideToggle(); + afterRender: function () { + ChangesActions.setChanges({ + changes: this.model.changes, + filters: this.filters, + databaseName: this.model.id + }); + Changes.renderChanges(this.el); }, establish: function() { - return [ this.model.changes.fetchOnce({prefill: true})]; - }, - - serialize: function () { - var json = this.model.changes.toJSON(), - filteredData = this.createFilteredData(json); - - return { - changes: filteredData, - database: this.model, - href: function (db, id) { - return FauxtonAPI.urls('document', 'app', db, id); - } - }; - }, - - createFilteredData: function (json) { - return _.reduce(this.filters, function (elements, filter) { - return _.filter(elements, function (element) { - var match = false; - - // make deleted searchable - if (!element.deleted) { - element.deleted = false; - } - _.each(element, function (value) { - if (new RegExp(filter, 'i').test(value.toString())) { - match = true; - } - }); - return match; - }); - - - }, json, this); + return [this.model.changes.fetchOnce({ prefill: true })]; }, - afterRender: function(){ - prettyPrint(); - var client = new Components.Clipboard({ - $el: this.$('.js-copy') - }); + cleanup: function () { + Changes.remove(this.el); } }); - return Views; }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/app/addons/fauxton/components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/fauxton/components.react.jsx b/app/addons/fauxton/components.react.jsx index eb54a1c..32a07c7 100644 --- a/app/addons/fauxton/components.react.jsx +++ b/app/addons/fauxton/components.react.jsx @@ -11,13 +11,15 @@ // the License. define([ + 'app', 'api', 'react', 'addons/fauxton/stores', - 'addons/fauxton/actions' + 'addons/fauxton/actions', + 'plugins/zeroclipboard/ZeroClipboard' ], -function(FauxtonAPI, React, Stores, Actions) { +function(app, FauxtonAPI, React, Stores, Actions, ZeroClipboard) { var navBarStore = Stores.navBarStore; var Footer = React.createClass({ @@ -150,12 +152,78 @@ function(FauxtonAPI, React, Stores, Actions) { }); + // super basic right now, but can be expanded later to handle all the varieties of copy-to-clipboards + // (target content element, custom label, classes, notifications, etc.) + var Clipboard = React.createClass({ + propTypes: function () { + return { + text: React.PropTypes.string.isRequired + }; + }, + + componentWillMount: function () { + ZeroClipboard.config({ moviePath: app.zeroClipboardPath }); + }, + + componentDidMount: function () { + var el = this.getDOMNode(); + this.clipboard = new ZeroClipboard(el); + }, + + render: function () { + return ( + + + + ); + } + }); + + // formats a block of code and pretty-prints it in the page. Currently uses the prettyPrint plugin + var CodeFormat = React.createClass({ + getDefaultProps: function () { + return { + lang: "js" + }; + }, + + getClasses: function () { + // added for forward compatibility. This component defines an api via it's props so you can pass lang="N" and + // not the class that prettyprint requires for that lang. If (when, hopefully!) we drop prettyprint we won't + // have any change this component's props API and break things + var classMap = { + js: 'lang-js' + }; + + var classNames = 'prettyprint'; + if (_.has(classMap, this.props.lang)) { + classNames += ' ' + classMap[this.props.lang]; + } + return classNames; + }, + + componentDidMount: function () { + // this one function is all the lib offers. It parses the entire page and pretty-prints anything with + // a .prettyprint class; only executes on an element once + prettyPrint(); + }, + + render: function () { + var code = JSON.stringify(this.props.code, null, " "); + return ( +
{code}
+ ); + } + }); + + return { renderNavBar: function (el) { React.render(, el); }, - - Burger: Burger + Burger: Burger, + Clipboard: Clipboard, + CodeFormat: CodeFormat }; }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/b3e5b44e/assets/less/animations.less ---------------------------------------------------------------------- diff --git a/assets/less/animations.less b/assets/less/animations.less index 3f80f85..b7269d2 100644 --- a/assets/less/animations.less +++ b/assets/less/animations.less @@ -46,3 +46,22 @@ from { background: @red; } to { background: white; } } + +/* a generic slide-up/down effect that looks smooth for items with unknown heights */ +.keyframes(slideDown, { + opacity: 0; + max-height: 0px; +}, +{ + opacity: 1; + max-height: 1000px; +}); + +.keyframes(slideUp, { + max-height: 1000px; + opacity: 1; +}, +{ + max-height: 0px; + opacity: 0; +});