Allow saving access for review

Note: The only things that can be modified at this time are existing
access rules.

Bug: Issue 6569
Change-Id: I16d21d71a6c832dd5638877c7f07e572e6ce343f
diff --git a/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access.js b/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access.js
index d736dac..00a6e28 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access.js
@@ -14,6 +14,47 @@
 (function() {
   'use strict';
 
+  const Defs = {};
+
+  /**
+   * @typedef {{
+   *    value: !Object,
+   * }}
+   */
+  Defs.rule;
+
+  /**
+   * @typedef {{
+   *    rules: !Object<string, Defs.rule>
+   * }}
+   */
+  Defs.permission;
+
+  /**
+   * Can be an empty object or consist of permissions.
+   *
+   * @typedef {{
+   *    permissions: !Object<string, Defs.permission>
+   * }}
+   */
+  Defs.permissions;
+
+  /**
+   * Can be an empty object or consist of permissions.
+   *
+   * @typedef {Object<string, Defs.permissions>}
+   */
+  Defs.sections;
+
+  /**
+   * @typedef {{
+   *    remove: Defs.sections,
+   *    add: Defs.sections,
+   * }}
+   */
+  Defs.projectAccessInput;
+
+
   Polymer({
     is: 'gr-project-access',
 
@@ -39,6 +80,10 @@
         type: Boolean,
         value: false,
       },
+      _modified: {
+        type: Boolean,
+        value: false,
+      },
       _sections: Array,
     },
 
@@ -48,6 +93,60 @@
       Gerrit.URLEncodingBehavior,
     ],
 
+    listeners: {
+      'access-modified': '_handleAccessModified',
+    },
+
+    /**
+     * Gets an array of gr-acces-section Polymer elements.
+     *
+     * @return {!Array}
+     */
+    _getSections() {
+      return Polymer.dom(this.root).querySelectorAll('gr-access-section');
+    },
+
+    /**
+     * Gets an array of the gr-permission polymer elements for a particular
+     * access section.
+     *
+     * @param {!Object} section
+     * @return {!Array}
+     */
+    _getPermissionsForSection(section) {
+      return Polymer.dom(section.root).querySelectorAll('gr-permission');
+    },
+
+    /**
+     * Gets an array of the gr-rule-editor polymer elements for a particular
+     * permission (within a section).
+     *
+     * @param {!Object} permission
+     * @return {!Array}
+     */
+    _getRulesForPermission(permission) {
+      return Polymer.dom(permission.root).querySelectorAll('gr-rule-editor');
+    },
+
+    /**
+     * Gets an array of all rules for the entire project access.
+     *
+     * @return {!Array}
+     */
+    _getAllRules() {
+      let rules = [];
+      for (const section of this._getSections()) {
+        for (const permission of this._getPermissionsForSection(section)) {
+          rules = rules.concat(this._getRulesForPermission(permission));
+        }
+      }
+      return rules;
+    },
+
+    _handleAccessModified() {
+      this._modified = true;
+    },
+
     /**
      * @param {string} project
      * @return {!Promise}
@@ -83,6 +182,116 @@
       });
     },
 
+    _handleEdit() {
+      this._editing = !this._editing;
+    },
+
+    _editOrCancel(editing) {
+      return editing ? 'Cancel' : 'Edit';
+    },
+
+    /**
+     * Returns whether or not a given permission exists in the remove object
+     *
+     * @param {!Object} remove
+     * @param {string} sectionId
+     * @param {string} permissionId
+     * @return {boolean}
+     */
+    _permissionInRemove(remove, sectionId, permissionId) {
+      return !!(remove[sectionId] &&
+          remove[sectionId].permissions[permissionId]);
+    },
+
+    /**
+     * Returns a projectAccessInput object that contains new permission Objects
+     * for a given permission in a section.
+     *
+     * @param {!Defs.projectAccessInput} addRemoveObj
+     * @param {string} sectionId
+     * @param {string} permissionId
+     *
+     * @return {!Defs.projectAccessInput}
+     */
+    _generatePermissionObject(addRemoveObj, sectionId, permissionId) {
+      const permissionObjAdd = {};
+      const permissionObjRemove = {};
+      permissionObjAdd[permissionId] = {rules: {}};
+      permissionObjRemove[permissionId] = {rules: {}};
+      addRemoveObj.add[sectionId] = {permissions: permissionObjAdd};
+      addRemoveObj.remove[sectionId] = {permissions: permissionObjRemove};
+      return addRemoveObj;
+    },
+
+    /**
+     * Returns an object formatted for saving or submitting access changes for
+     * review
+     *
+     * @return {!Defs.projectAccessInput}
+     */
+    _computeAddAndRemove() {
+      let addRemoveObj = {
+        add: {},
+        remove: {},
+      };
+      const sections = this._getSections();
+      for (const section of sections) {
+        const sectionId = section.section.id;
+        const permissions = this._getPermissionsForSection(section);
+        for (const permission of permissions) {
+          const permissionId = permission.permission.id;
+          const rules = this._getRulesForPermission(permission);
+          for (const rule of rules) {
+            // Find all rules that are changed. In the event that it has been
+            // modified.
+            if (!rule._modified) { continue; }
+            const ruleId = rule.rule.id;
+            const ruleValue = rule.rule.value;
+
+            // If the rule's parent permission has already been added to the
+            // remove object (don't need to check add, as they are always
+            // done to both at the same time). If it doesn't exist yet, it needs
+            // to be created.
+            if (!this._permissionInRemove(addRemoveObj.remove, sectionId,
+                permissionId)) {
+              addRemoveObj = this._generatePermissionObject(addRemoveObj,
+                  sectionId, permissionId);
+            }
+
+            // Remove the rule with a value of null
+            addRemoveObj.remove[sectionId].permissions[permissionId]
+                .rules[ruleId] = null;
+            // Add the rule with a value of the updated rule value.
+            addRemoveObj.add[sectionId].permissions[permissionId]
+                .rules[ruleId] = ruleValue;
+          }
+        }
+      }
+      return addRemoveObj;
+    },
+
+    _handleSaveForReview() {
+      // Use saving rather than editing here because rules have to handle
+      // save prior to toggling editing.
+      const addRemoveObj = this._computeAddAndRemove();
+      return this.$.restAPI.setProjectAccessRightsForReview(this.project, {
+        add: addRemoveObj.add,
+        remove: addRemoveObj.remove,
+      }).then(change => {
+        Gerrit.Nav.navigateToChange(change);
+      });
+    },
+
+    _computeShowEditClass(sections) {
+      if (!sections.length) { return ''; }
+      return 'visible';
+    },
+
+    _computeShowSaveClass(editing) {
+      if (!editing) { return ''; }
+      return 'visible';
+    },
+
     _computeAdminClass(isAdmin) {
       return isAdmin ? 'admin' : '';
     },