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">&times;</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;
-});

Reply via email to