Repository: couchdb-fauxton Updated Branches: refs/heads/master 946d253b4 -> 1c6ffe915
Convert Permissions to use React.js Convert the permissions module to use React.js Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/1c6ffe91 Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/1c6ffe91 Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/1c6ffe91 Branch: refs/heads/master Commit: 1c6ffe9156336905d19508c24ded513f3211e7cb Parents: 946d253 Author: Garren Smith <[email protected]> Authored: Thu May 14 14:04:58 2015 +0200 Committer: Garren Smith <[email protected]> Committed: Wed May 20 15:55:47 2015 +0200 ---------------------------------------------------------------------- .../components/assets/less/loading-lines.less | 2 +- app/addons/permissions/actions.js | 85 +++++++ app/addons/permissions/actiontypes.js | 23 ++ app/addons/permissions/components.react.jsx | 240 +++++++++++++++++++ app/addons/permissions/resources.js | 29 ++- app/addons/permissions/routes.js | 17 +- app/addons/permissions/stores.js | 109 +++++++++ app/addons/permissions/templates/item.html | 17 -- .../permissions/templates/permissions.html | 17 -- app/addons/permissions/templates/section.html | 46 ---- app/addons/permissions/tests/actionsSpec.js | 124 ++++++++++ .../permissions/tests/componentsSpec.react.jsx | 157 ++++++++++++ app/addons/permissions/tests/resourceSpec.js | 22 ++ app/addons/permissions/tests/viewsSpec.js | 164 ------------- app/addons/permissions/views.js | 198 --------------- 15 files changed, 795 insertions(+), 455 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/components/assets/less/loading-lines.less ---------------------------------------------------------------------- diff --git a/app/addons/components/assets/less/loading-lines.less b/app/addons/components/assets/less/loading-lines.less index 7d3ab49..d9e4267 100644 --- a/app/addons/components/assets/less/loading-lines.less +++ b/app/addons/components/assets/less/loading-lines.less @@ -10,7 +10,7 @@ .loading-lines { display: block; - width: 50px; + width: 80px; margin-left: auto; margin-right: auto; } http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/actions.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/actions.js b/app/addons/permissions/actions.js new file mode 100644 index 0000000..8e8e008 --- /dev/null +++ b/app/addons/permissions/actions.js @@ -0,0 +1,85 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +define([ + 'api', + 'addons/permissions/actiontypes', + 'addons/permissions/stores' +], + +function (FauxtonAPI, ActionTypes, Stores) { + var permissionsStore = Stores.permissionsStore; + return { + + fetchPermissions: function (database, security) { + FauxtonAPI.dispatch({ + type: ActionTypes.PERMISSIONS_FETCHING, + database: database, + security: security + }); + + FauxtonAPI.when([database.fetch(), security.fetch()]).then(function () { + this.editPermissions(database, security); + }.bind(this)); + }, + + editPermissions: function (database, security) { + FauxtonAPI.dispatch({ + type: ActionTypes.PERMISSIONS_EDIT, + database: database, + security: security + }); + }, + addItem: function (options) { + var check = permissionsStore.getSecurity().canAddItem(options.value, options.type, options.section); + + if (check.error) { + FauxtonAPI.addNotification({ + msg: check.msg, + type: 'error' + }); + + return; + } + + FauxtonAPI.dispatch({ + type: ActionTypes.PERMISSIONS_ADD_ITEM, + options: options + }); + + this.savePermissions(); + + }, + removeItem: function (options) { + FauxtonAPI.dispatch({ + type: ActionTypes.PERMISSIONS_REMOVE_ITEM, + options: options + }); + this.savePermissions(); + }, + + savePermissions: function () { + permissionsStore.getSecurity().save().then(function () { + FauxtonAPI.addNotification({ + msg: 'Database permissions has been updated.' + }); + }, function (xhr) { + if (!xhr && !xhr.responseJSON) { return;} + + FauxtonAPI.addNotification({ + msg: 'Could not update permissions - reason: ' + xhr.responseJSON.reason, + type: 'error' + }); + }); + } + }; +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/actiontypes.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/actiontypes.js b/app/addons/permissions/actiontypes.js new file mode 100644 index 0000000..2918ac6 --- /dev/null +++ b/app/addons/permissions/actiontypes.js @@ -0,0 +1,23 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +define([ +], + +function () { + return { + PERMISSIONS_EDIT: 'PERMISSIONS_EDIT', + PERMISSIONS_FETCHING: 'PERMISSIONS_FETCHING', + PERMISSIONS_ADD_ITEM: 'PERMISSIONS_ADD_ITEM', + PERMISSIONS_REMOVE_ITEM: 'PERMISSIONS_REMOVE_ITEM' + }; +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/permissions/components.react.jsx b/app/addons/permissions/components.react.jsx new file mode 100644 index 0000000..7b07a0f --- /dev/null +++ b/app/addons/permissions/components.react.jsx @@ -0,0 +1,240 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +define([ + 'app', + 'api', + 'react', + 'addons/components/react-components.react', + 'addons/permissions/stores', + 'addons/permissions/actions' +], + +function (app, FauxtonAPI, React, Components, Stores, Actions) { + var LoadLines = Components.LoadLines; + var permissionsStore = Stores.permissionsStore; + var getDocUrl = app.helpers.getDocUrl; + + var PermissionsItem = React.createClass({ + + removeItem: function (e) { + this.props.removeItem({ + value: this.props.item, + type: this.props.type, + section: this.props.section + }); + }, + + render: function () { + return ( + <li> + <span>{this.props.item}</span> + <button onClick={this.removeItem} type="button" className="pull-right close">Ã</button> + </li> + ); + } + + }); + + var PermissionsSection = React.createClass({ + getInitialState: function () { + return { + newRole: '', + newName: '' + }; + }, + + getHelp: function () { + if (this.props.section === 'admins') { + return 'Database members can access the database. If no members are defined, the database is public. '; + } + + return 'Database members can access the database. If no members are defined, the database is public. '; + }, + + isEmptyValue: function (value, type) { + if (!_.isEmpty(value)) { + return false; + } + + FauxtonAPI.addNotification({ + msg: 'Cannot add an empty value for ' + type + '.', + type: 'warning' + }); + + return true; + }, + + addNames: function (e) { + e.preventDefault(); + if (this.isEmptyValue(this.state.newRole, 'names')) { + return; + } + this.props.addItem({ + type: 'names', + section: this.props.section, + value: this.state.newName + }); + + this.setState({newName: ''}); + }, + + addRoles: function (e) { + e.preventDefault(); + if (this.isEmptyValue(this.state.newRole, 'roles')) { + return; + } + this.props.addItem({ + type: 'roles', + section: this.props.section, + value: this.state.newRole + }); + + this.setState({newRole: ''}); + }, + + getItems: function (items, type) { + return _.map(items, function (item, i) { + return <PermissionsItem key={i} item={item} section={this.props.section} type={type} removeItem={this.props.removeItem} />; + }, this); + }, + + getNames: function () { + return this.getItems(this.props.names, 'names'); + }, + + getRoles: function () { + return this.getItems(this.props.roles, 'roles'); + }, + + nameChange: function (e) { + this.setState({newName: e.target.value}); + }, + + roleChange: function (e) { + this.setState({newRole: e.target.value}); + }, + + render: function () { + return ( + <div> + <header className="page-header"> + <h3>{this.props.section}</h3> + <p className="help"> + {this.getHelp()} + <a className="help-link" data-bypass="true" href={getDocUrl('DB_PERMISSION')} target="_blank"> + <i className="icon-question-sign"></i> + </a> + </p> + </header> + <div className="row-fluid"> + <div className="span6"> + <header> + <h4>Users</h4> + <p>Specify users who will have {this.props.section} access to this database.</p> + </header> + <form onSubmit={this.addNames} className="permission-item-form form-inline"> + <input onChange={this.nameChange} value={this.state.newName} type="text" className="item input-small" placeholder="Add Name" /> + <button type="submit" className="btn btn-success"><i className="icon fonticon-plus-circled" /> Add Name</button> + </form> + <ul className="clearfix unstyled permission-items span10"> + {this.getNames()} + </ul> + </div> + <div className="span6"> + <header> + <h4>Roles</h4> + <p>Users with any of the following role(s) will have {this.props.section} access.</p> + </header> + <form onSubmit={this.addRoles} className="permission-item-form form-inline"> + <input onChange={this.roleChange} value={this.state.newRole} type="text" className="item input-small" placeholder="Add Role" /> + <button type="submit" className="btn btn-success"><i className="icon fonticon-plus-circled" /> Add Role</button> + </form> + <ul className="unstyled permission-items span10"> + {this.getRoles()} + </ul> + </div> + </div> + </div> + ); + } + + }); + + var PermissionsController = React.createClass({ + + getStoreState: function () { + return { + isLoading: permissionsStore.isLoading(), + adminRoles: permissionsStore.getAdminRoles(), + adminNames: permissionsStore.getAdminNames(), + memberRoles: permissionsStore.getMemberRoles(), + memberNames: permissionsStore.getMemberNames(), + }; + }, + + getInitialState: function () { + return this.getStoreState(); + }, + + componentDidMount: function () { + permissionsStore.on('change', this.onChange, this); + }, + + componentWillUnmount: function () { + permissionsStore.off('change', this.onChange); + }, + + onChange: function () { + this.setState(this.getStoreState()); + }, + + addItem: function (options) { + Actions.addItem(options); + }, + + removeItem: function (options) { + Actions.removeItem(options); + }, + + render: function () { + if (this.state.isLoading) { + return <LoadLines />; + } + + return ( + <div className="scrollable permissions-page"> + <div id="sections"> + <PermissionsSection roles={this.state.adminRoles} + names={this.state.adminNames} + addItem={this.addItem} + removeItem={this.removeItem} + section={'admins'} /> + <PermissionsSection + roles={this.state.memberRoles} + names={this.state.memberNames} + addItem={this.addItem} + removeItem={this.removeItem} + section={'members'} /> + </div> + </div> + ); + } + + }); + + return { + PermissionsController: PermissionsController, + PermissionsSection: PermissionsSection, + PermissionsItem: PermissionsItem + }; +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/resources.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/resources.js b/app/addons/permissions/resources.js index de89e12..66f4647 100644 --- a/app/addons/permissions/resources.js +++ b/app/addons/permissions/resources.js @@ -40,10 +40,20 @@ function (app, FauxtonAPI) { addItem: function (value, type, section) { var sectionValues = this.get(section); + var check = this.canAddItem(value, type, section); + if (check.error) { return check;} + + sectionValues[type].push(value); + return this.set(section, sectionValues); + }, + + canAddItem: function (value, type, section) { + var sectionValues = this.get(section); + if (!sectionValues || !sectionValues[type]) { return { error: true, - msg: 'Section ' + section + 'does not exist' + msg: 'Section ' + section + ' does not exist' }; } @@ -54,11 +64,24 @@ function (app, FauxtonAPI) { }; } - sectionValues[type].push(value); + return { + error: false + }; + }, + + removeItem: function (value, type, section) { + var sectionValues = this.get(section); + var types = sectionValues[type]; + var indexOf = _.indexOf(types, value); + + if (indexOf === -1) { return;} + + types.splice(indexOf, 1); + sectionValues[type] = types; return this.set(section, sectionValues); } + }); return Permissions; }); - http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/routes.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/routes.js b/app/addons/permissions/routes.js index 9b631df..a92666b 100644 --- a/app/addons/permissions/routes.js +++ b/app/addons/permissions/routes.js @@ -14,12 +14,15 @@ define([ 'app', 'api', 'addons/databases/base', - 'addons/permissions/views', + 'addons/permissions/resources', + 'addons/permissions/actions', + 'addons/permissions/components.react', 'addons/documents/shared-routes' ], -function (app, FauxtonAPI, Databases, Permissions, BaseRoute) { +function (app, FauxtonAPI, Databases, Resources, Actions, Permissions, BaseRoute) { var PermissionsRouteObject = BaseRoute.extend({ + roles: ['fx_loggedIn'], routes: { 'database/:database/permissions': 'permissions' }, @@ -34,7 +37,7 @@ function (app, FauxtonAPI, Databases, Permissions, BaseRoute) { initViews: function (databaseName) { this.database = new Databases.Model({ id: databaseName }); - this.security = new Permissions.Security(null, { + this.security = new Resources.Security(null, { database: this.database }); this.allDatabases = new Databases.List(); @@ -50,8 +53,6 @@ function (app, FauxtonAPI, Databases, Permissions, BaseRoute) { establish: function () { return [ - this.database.fetch(), - this.security.fetch(), this.designDocs.fetch({reset: true}), this.allDatabases.fetchOnce() ]; @@ -72,10 +73,8 @@ function (app, FauxtonAPI, Databases, Permissions, BaseRoute) { }, permissions: function () { - this.pageContent = this.setView('#dashboard-content', new Permissions.Permissions({ - database: this.database, - model: this.security - })); + Actions.fetchPermissions(this.database, this.security); + this.setComponent('#dashboard-content', Permissions.PermissionsController); }, crumbs: function () { http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/stores.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/stores.js b/app/addons/permissions/stores.js new file mode 100644 index 0000000..b742d55 --- /dev/null +++ b/app/addons/permissions/stores.js @@ -0,0 +1,109 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +define([ + 'api', + 'addons/permissions/actiontypes' +], + +function (FauxtonAPI, ActionTypes) { + var Stores = {}; + + Stores.PermissionsStore = FauxtonAPI.Store.extend({ + initialize: function () { + this._isLoading = true; + }, + + isLoading: function () { + return this._isLoading; + }, + + editPermissions: function (database, security) { + this._database = database; + this._security = security; + this._isLoading = false; + }, + + getItem: function (section, type) { + if (this._isLoading) {return [];} + + return this._security.get(section)[type]; + }, + + getDatabase: function () { + return this._database; + }, + + getSecurity: function () { + return this._security; + }, + + getAdminRoles: function () { + return this.getItem('admins', 'roles'); + }, + + getAdminNames: function () { + return this.getItem('admins', 'names'); + }, + + getMemberNames: function () { + return this.getItem('members', 'names'); + }, + + getMemberRoles: function () { + return this.getItem('members', 'roles'); + }, + + addItem: function (options) { + this._security.addItem(options.value, options.type, options.section); + }, + + removeItem: function (options) { + this._security.removeItem(options.value, options.type, options.section); + }, + + dispatch: function (action) { + switch (action.type) { + case ActionTypes.PERMISSIONS_FETCHING: + this._isLoading = true; + this.triggerChange(); + break; + + case ActionTypes.PERMISSIONS_EDIT: + this.editPermissions(action.database, action.security); + this.triggerChange(); + break; + + case ActionTypes.PERMISSIONS_ADD_ITEM: + this.addItem(action.options); + this.triggerChange(); + break; + + case ActionTypes.PERMISSIONS_REMOVE_ITEM: + this.removeItem(action.options); + this.triggerChange(); + break; + + default: + return; + // do nothing + } + } + + }); + + Stores.permissionsStore = new Stores.PermissionsStore(); + + Stores.permissionsStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.permissionsStore.dispatch); + + return Stores; +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/templates/item.html ---------------------------------------------------------------------- diff --git a/app/addons/permissions/templates/item.html b/app/addons/permissions/templates/item.html deleted file mode 100644 index fd65a87..0000000 --- a/app/addons/permissions/templates/item.html +++ /dev/null @@ -1,17 +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. -*/ %> - -<span> <%- item %> </span> -<button type="button" class="pull-right close">×</button> - http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/templates/permissions.html ---------------------------------------------------------------------- diff --git a/app/addons/permissions/templates/permissions.html b/app/addons/permissions/templates/permissions.html deleted file mode 100644 index c3c9ab7..0000000 --- a/app/addons/permissions/templates/permissions.html +++ /dev/null @@ -1,17 +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. -*/ %> - -<div class="scrollable permissions-page"> - <div id="sections"> </div> -</div> http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/templates/section.html ---------------------------------------------------------------------- diff --git a/app/addons/permissions/templates/section.html b/app/addons/permissions/templates/section.html deleted file mode 100644 index 1895140..0000000 --- a/app/addons/permissions/templates/section.html +++ /dev/null @@ -1,46 +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. -*/ %> - -<header class="page-header"> - <h3><%- section %></h3> - <p class="help"><%- help %> - <a class="help-link" data-bypass="true" href="<%-getDocUrl('DB_PERMISSION')%>" target="_blank"><i class="icon-question-sign"> </i> </a> - </p> -</header> - -<div class="row-fluid"> - <div class="span6"> - <header> - <h4>Users</h4> - <p>Specify users who will have <%- section %> access to this database.</p> - </header> - <form class="permission-item-form form-inline"> - <input data-section="<%- section %>" data-type="names" type="text" class="item input-small" placeholder="Add Name"> - <button type="submit" class="btn btn-success"><i class="icon fonticon-plus-circled"></i> Add Name</button> - </form> - <ul class="clearfix unstyled permission-items span10" id="<%- section %>-items-names"></ul> - </div> - <div class="span6"> - <header> - <h4>Roles</h4> - <p>All users under the following role(s) will have <%- section %> access.</p> - </header> - - <form class="permission-item-form form-inline"> - <input data-section="<%- section %>" data-type="roles" type="text" class="item input-small" placeholder="Add Role"> - <button type="submit" class="btn btn-success"><i class="icon fonticon-plus-circled"></i> Add Role</button> - </form> - <ul class="unstyled permission-items span10" id="<%- (section) %>-items-roles"></ul> - </div> -</div> http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/tests/actionsSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/tests/actionsSpec.js b/app/addons/permissions/tests/actionsSpec.js new file mode 100644 index 0000000..ac3d4cd --- /dev/null +++ b/app/addons/permissions/tests/actionsSpec.js @@ -0,0 +1,124 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +define([ + 'api', + 'addons/databases/base', + 'addons/permissions/stores', + 'addons/permissions/resources', + 'addons/permissions/actions', + 'testUtils' +], function (FauxtonAPI, Databases, Stores, Permissions, Actions, testUtils) { + var assert = testUtils.assert; + var restore = testUtils.restore; + var store = Stores.permissionsStore; + + describe('Permissions Actions', function () { + var getSecuritystub; + + beforeEach(function () { + var databaseName = 'permissions-test'; + var database = new Databases.Model({ id: databaseName }); + Actions.editPermissions( + database, + new Permissions.Security(null, { + database: database + }) + ); + + + var promise = FauxtonAPI.Deferred(); + getSecuritystub = sinon.stub(store, 'getSecurity'); + getSecuritystub.returns({ + canAddItem: function () { return {error: true};}, + save: function () { + return promise; + } + }); + }); + + afterEach(function () { + restore(store.getSecurity); + }); + + describe('add Item', function () { + + afterEach(function () { + restore(FauxtonAPI.addNotification); + restore(Actions.savePermissions); + restore(store.getSecurity); + }); + + it('does not save item if cannot add it', function () { + var spy = sinon.spy(FauxtonAPI, 'addNotification'); + var spy2 = sinon.spy(Actions, 'savePermissions'); + + Actions.addItem({ + value: 'boom', + type: 'names', + section: 'members' + }); + + assert.ok(spy.calledOnce); + assert.notOk(spy2.calledOnce); + }); + + it('save items', function () { + var spy = sinon.spy(FauxtonAPI, 'addNotification'); + var spy2 = sinon.spy(Actions, 'savePermissions'); + + var promise = FauxtonAPI.Deferred(); + getSecuritystub.returns({ + canAddItem: function () { return {error: false};}, + save: function () { + return promise; + } + }); + + Actions.addItem({ + value: 'boom', + type: 'names', + section: 'members' + }); + + assert.ok(spy2.calledOnce); + assert.notOk(spy.calledOnce); + }); + }); + + describe('remove item', function () { + + afterEach(function () { + restore(Actions.savePermissions); + }); + + it('saves item', function () { + Actions.addItem({ + value: 'boom', + type: 'names', + section: 'members' + }); + + var spy = sinon.spy(Actions, 'savePermissions'); + + Actions.removeItem({ + value: 'boom', + type: 'names', + section: 'members' + }); + + assert.ok(spy.calledOnce); + }); + + }); + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/tests/componentsSpec.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/permissions/tests/componentsSpec.react.jsx b/app/addons/permissions/tests/componentsSpec.react.jsx new file mode 100644 index 0000000..acbb668 --- /dev/null +++ b/app/addons/permissions/tests/componentsSpec.react.jsx @@ -0,0 +1,157 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +define([ + 'api', + 'addons/databases/base', + 'addons/permissions/resources', + 'addons/permissions/components.react', + 'addons/permissions/actions', + 'testUtils', + "react" + ], function (FauxtonAPI, Databases, Permissions, Views, Actions, utils, React) { + var assert = utils.assert; + var restore = utils.restore; + var TestUtils = React.addons.TestUtils; + + describe('Permissions Components', function () { + + beforeEach(function () { + var databaseName = 'permissions-test'; + var database = new Databases.Model({ id: databaseName }); + Actions.editPermissions( + database, + new Permissions.Security(null, { + database: database + }) + ); + + var savePermissionsStub = sinon.stub(Actions, 'savePermissions'); + }); + + afterEach(function () { + restore(Actions.savePermissions); + }); + + describe('Permissions Controller', function () { + var el, container; + + beforeEach(function () { + container = document.createElement('div'); + el = TestUtils.renderIntoDocument(<Views.PermissionsController />, container); + }); + + afterEach(function () { + React.unmountComponentAtNode(container); + }); + + it('on Add triggers add action', function () { + var spy = sinon.spy(Actions, 'addItem'); + el.addItem({}); + assert.ok(spy.calledOnce); + }); + + it('on Remove triggers remove action', function () { + var spy = sinon.spy(Actions, 'removeItem'); + el.removeItem({ + value: 'boom', + type: 'names', + section: 'members' + }); + assert.ok(spy.calledOnce); + }); + + }); + + describe('PermissionsSection', function () { + var el, container, addSpy; + + beforeEach(function () { + addSpy = sinon.spy(); + container = document.createElement('div'); + el = TestUtils.renderIntoDocument(<Views.PermissionsSection section={'members'} roles={[]} names={[]} addItem={addSpy} />, container); + }); + + afterEach(function () { + React.unmountComponentAtNode(container); + }); + + it('adds user on submit', function () { + var dom = $(el.getDOMNode()).find('.permission-item-form')[0]; + TestUtils.Simulate.submit(dom); + + var options = addSpy.args[0][0]; + assert.ok(addSpy.calledOnce); + assert.equal(options.type, "names"); + assert.equal(options.section, "members"); + }); + + it('adds role on submit', function () { + var dom = $(el.getDOMNode()).find('.permission-item-form')[1]; + TestUtils.Simulate.submit(dom); + + var options = addSpy.args[0][0]; + assert.ok(addSpy.calledOnce); + assert.equal(options.type, "roles"); + assert.equal(options.section, "members"); + }); + + it('stores new name on change', function () { + var newName = 'newName'; + var dom = $(el.getDOMNode()).find('.item')[0]; + + TestUtils.Simulate.change(dom, { + target: { + value: newName + } + }); + + assert.equal(el.state.newName, newName); + }); + + it('stores new role on change', function () { + var newRole = 'newRole'; + var dom = $(el.getDOMNode()).find('.item')[1]; + + TestUtils.Simulate.change(dom, { + target: { + value: newRole + } + }); + + assert.equal(el.state.newRole, newRole); + }); + }); + + describe('PermissionsItem', function () { + var el, container, removeSpy; + + beforeEach(function () { + removeSpy = sinon.spy(); + container = document.createElement('div'); + el = TestUtils.renderIntoDocument(<Views.PermissionsItem section={'members'} item={'test-item'} removeItem={removeSpy} />, container); + }); + + afterEach(function () { + React.unmountComponentAtNode(container); + }); + + it('triggers remove on click', function () { + var dom = $(el.getDOMNode()).find('.close')[0]; + TestUtils.Simulate.click(dom); + + assert.ok(removeSpy.calledOnce); + + }); + + }); + }); + }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/tests/resourceSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/tests/resourceSpec.js b/app/addons/permissions/tests/resourceSpec.js index 99bb745..04489c2 100644 --- a/app/addons/permissions/tests/resourceSpec.js +++ b/app/addons/permissions/tests/resourceSpec.js @@ -46,6 +46,28 @@ define([ }); }); + describe('#removeItem', function () { + var security; + + beforeEach(function () { + security = new Models.Security(null, {database: 'fakedb'}); + }); + + it('removes value from section', function () { + security.addItem('_user', 'names', 'admins'); + security.removeItem('_user', 'names', 'admins'); + + assert.equal(security.get('admins').names.length, 0); + }); + + it('ignores non-existing value', function () { + security.addItem('_user', 'names', 'admins'); + security.removeItem('wrong_user', 'names', 'admins'); + assert.equal(security.get('admins').names.length, 1); + }); + + }); + }); }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/tests/viewsSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/tests/viewsSpec.js b/app/addons/permissions/tests/viewsSpec.js deleted file mode 100644 index c0d0b9b..0000000 --- a/app/addons/permissions/tests/viewsSpec.js +++ /dev/null @@ -1,164 +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([ - 'api', - 'addons/permissions/views', - 'addons/permissions/resources', - 'testUtils' -], function (FauxtonAPI, Views, Models, testUtils) { - var assert = testUtils.assert, - ViewSandbox = testUtils.ViewSandbox; - - describe('Permission View', function () { - var security, section, viewSandbox; - - beforeEach(function (done) { - security = new Models.Security({'admins': { - 'names': ['_user'], - 'roles': [] - } - }, {database: {id: 'fakedb', safeID: function () { return this.id; }}}); - - section = new Views.Permissions({ - database: 'fakedb', - model: security - }); - - viewSandbox = new ViewSandbox(); - viewSandbox.renderView(section, done); - }); - - afterEach(function () { - viewSandbox.remove(); - }); - - describe('itemRemoved', function () { - - it('Should set model', function () { - var saveMock = sinon.spy(security, 'set'); - Views.events.trigger('itemRemoved'); - - assert.ok(saveMock.calledOnce); - var args = saveMock.args; - assert.deepEqual(args[0][0], - {"admins": {"names": ["_user"], "roles":[]}, "members": {"names":[], "roles":[]}}); - }); - - it('Should save model', function () { - var saveMock = sinon.spy(security, 'save'); - Views.events.trigger('itemRemoved'); - - assert.ok(saveMock.calledOnce); - }); - }); - - }); - - describe('PermissionsSection', function () { - var section, - security, - viewSandbox; - - beforeEach(function (done) { - security = new Models.Security({'admins': { - 'names': ['_user'], - 'roles': [] - } - }, {database: 'fakedb'}); - - section = new Views.PermissionSection({ - section: 'admins', - model: security - }); - - viewSandbox = new ViewSandbox(); - viewSandbox.renderView(section, done); - }); - - afterEach(function () { - viewSandbox.remove(); - }); - - describe('#discardRemovedViews', function () { - it('Should not filter out active views', function () { - section.discardRemovedViews(); - - assert.equal(section.nameViews.length, 1); - - }); - - it('Should filter out removed views', function () { - section.nameViews[0].removed = true; - section.discardRemovedViews(); - - assert.equal(section.nameViews.length, 0); - - }); - - }); - - describe('#getItemFromView', function () { - - it('Should return item list', function () { - var items = section.getItemFromView(section.nameViews); - - assert.deepEqual(items, ['_user']); - }); - - }); - - describe('#addItems', function () { - - it('Should add item to model', function () { - //todo add a test here - - }); - - }); - - }); - - describe('PermissionItem', function () { - var item, - viewSandbox; - - beforeEach(function (done) { - item = new Views.PermissionItem({ - item: '_user' - }); - - viewSandbox = new ViewSandbox(); - viewSandbox.renderView(item, done); - }); - - afterEach(function () { - viewSandbox.remove(); - }); - - it('should trigger event on remove item', function () { - var eventSpy = sinon.spy(); - - Views.events.on('itemRemoved', eventSpy); - - item.$('.close').click(); - - assert.ok(eventSpy.calledOnce); - }); - - it('should set removed to true', function () { - item.$('.close').click(); - - assert.ok(item.removed); - }); - }); - -}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/1c6ffe91/app/addons/permissions/views.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/views.js b/app/addons/permissions/views.js deleted file mode 100644 index 7a111ca..0000000 --- a/app/addons/permissions/views.js +++ /dev/null @@ -1,198 +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([ - 'app', - 'api', - 'addons/permissions/resources' -], -function (app, FauxtonAPI, Permissions) { - - var events = {}; - Permissions.events = _.extend(events, Backbone.Events); - - Permissions.Permissions = FauxtonAPI.View.extend({ - template: 'addons/permissions/templates/permissions', - - initialize: function (options) { - this.database = options.database; - this.listenTo(Permissions.events, 'itemRemoved', this.itemRemoved); - }, - - itemRemoved: function () { - this.model.set({ - admins: this.adminsView.items(), - members: this.membersView.items() - }); - - this.model.save().then(function () { - FauxtonAPI.addNotification({ - msg: 'Database permissions has been updated.' - }); - }, function (xhr) { - FauxtonAPI.addNotification({ - msg: 'Could not update permissions - reason: ' + xhr.responseText, - type: 'error' - }); - }); - }, - - beforeRender: function () { - this.adminsView = this.insertView('#sections', new Permissions.PermissionSection({ - model: this.model, - section: 'admins', - help: 'Database admins can update design documents and edit the admin and member lists.' - })); - - this.membersView = this.insertView('#sections', new Permissions.PermissionSection({ - model: this.model, - section: 'members', - help: 'Database members can access the database. If no members are defined, the database is public.' - })); - }, - - serialize: function () { - return { - databaseName: this.database.id - }; - } - }); - - Permissions.PermissionSection = FauxtonAPI.View.extend({ - template: 'addons/permissions/templates/section', - initialize: function (options) { - this.section = options.section; - this.help = options.help; - }, - - events: { - 'submit .permission-item-form': 'addItem', - 'click button.close': 'removeItem' - }, - - beforeRender: function () { - var section = this.model.get(this.section); - - this.nameViews = []; - this.roleViews = []; - - _.each(section.names, function (name) { - var nameView = this.insertView('#' + this.section + '-items-names', new Permissions.PermissionItem({ - item: name - })); - this.nameViews.push(nameView); - }, this); - - _.each(section.roles, function (role) { - var roleView = this.insertView('#' + this.section + '-items-roles', new Permissions.PermissionItem({ - item: role - })); - this.roleViews.push(roleView); - }, this); - }, - - getItemFromView: function (viewList) { - return _.map(viewList, function (view) { - return view.item; - }); - }, - - discardRemovedViews: function () { - this.nameViews = _.filter(this.nameViews, function (view) { - return !view.removed; - }); - - this.roleViews = _.filter(this.roleViews, function (view) { - return !view.removed; - }); - }, - - items: function () { - this.discardRemovedViews(); - - return { - names: this.getItemFromView(this.nameViews), - roles: this.getItemFromView(this.roleViews) - }; - }, - - addItem: function (event) { - event.preventDefault(); - var $item = this.$(event.currentTarget).find('.item'), - value = $item.val(), - section = $item.data('section'), - type = $item.data('type'), - that = this; - - var resp = this.model.addItem(value, type, section); - - if (resp && resp.error) { - return FauxtonAPI.addNotification({ - msg: resp.msg, - type: 'error' - }); - } - - this.model.save().then(function () { - that.render(); - FauxtonAPI.addNotification({ - msg: 'Database permissions has been updated.' - }); - }, function (xhr) { - FauxtonAPI.addNotification({ - msg: 'Could not update permissions - reason: ' + xhr.responseJSON.reason, - type: 'error' - }); - }); - }, - - serialize: function () { - return { - section: this.section, - help: this.help - }; - } - }); - - Permissions.PermissionItem = FauxtonAPI.View.extend({ - tagName: 'li', - template: 'addons/permissions/templates/item', - initialize: function (options) { - this.item = options.item; - this.viewsList = options.viewsList; - }, - - events: { - 'click .close': 'removeItem' - }, - - removeItem: function (event) { - var that = this; - event.preventDefault(); - - this.removed = true; - Permissions.events.trigger('itemRemoved'); - - this.$el.hide('fast', function () { - that.remove(); - }); - }, - - serialize: function () { - return { - item: this.item - }; - } - }); - - return Permissions; -});
