Implement per-project plugin configuration

Bug: Issue 8535
Change-Id: I2b2bf7e72e4b527b39043cfafd9427273a9c1e1b
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
index 6ce1a09..cd27322 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -26,7 +26,7 @@
 <link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/gr-subpage-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
-
+<link rel="import" href="../gr-repo-plugin-config/gr-repo-plugin-config.html">
 
 <dom-module id="gr-repo">
   <template>
@@ -37,7 +37,7 @@
         content: ' *';
       }
       .loading,
-      .hideDownload {
+      .hide {
         display: none;
       }
       #loading.loading {
@@ -67,7 +67,7 @@
       </div>
       <div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div>
       <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
-        <div id="downloadContent" class$="[[_computeDownloadClass(_schemes)]]">
+        <div id="downloadContent" class$="[[_computeHideClass(_schemes)]]">
           <h2 id="download">Download</h2>
           <fieldset>
             <gr-download-commands
@@ -346,7 +346,15 @@
                 </span>
               </section>
             </fieldset>
-            <!-- TODO @beckysiegel add plugin config widgets -->
+            <div
+                class$="pluginConfig [[_computeHideClass(_pluginData)]]"
+                on-plugin-config-changed="_handlePluginConfigChanged">
+              <h3>Plugins</h3>
+              <template is="dom-repeat" items="[[_pluginData]]" as="data">
+                <gr-repo-plugin-config
+                    plugin-data="[[data]]"></gr-repo-plugin-config>
+              </template>
+            </div>
             <gr-button
                 on-tap="_handleSaveRepoConfig"
                 disabled$="[[_computeButtonDisabled(_readOnly, _configChanged)]]">Save changes</gr-button>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
index 840a529..7a52476 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -74,6 +74,11 @@
       },
       /** @type {?} */
       _repoConfig: Object,
+      /** @type {?} */
+      _pluginData: {
+        type: Array,
+        computed: '_computePluginData(_repoConfig.plugin_config.*)',
+      },
       _readOnly: {
         type: Boolean,
         value: true,
@@ -118,6 +123,15 @@
       this.fire('title-change', {title: this.repo});
     },
 
+    _computePluginData(configRecord) {
+      if (!configRecord ||
+          !configRecord.base) { return []; }
+
+      const pluginConfig = configRecord.base;
+      return Object.keys(pluginConfig)
+          .map(name => ({name, config: pluginConfig[name]}));
+    },
+
     _loadRepo() {
       if (!this.repo) { return Promise.resolve(); }
 
@@ -173,8 +187,8 @@
       return loading ? 'loading' : '';
     },
 
-    _computeDownloadClass(schemes) {
-      return !schemes || !schemes.length ? 'hideDownload' : '';
+    _computeHideClass(arr) {
+      return !arr || !arr.length ? 'hide' : '';
     },
 
     _loggedInChanged(_loggedIn) {
@@ -246,20 +260,22 @@
       return this.$.restAPI.getLoggedIn();
     },
 
-    _formatRepoConfigForSave(p) {
+    _formatRepoConfigForSave(repoConfig) {
       const configInputObj = {};
-      for (const key in p) {
-        if (p.hasOwnProperty(key)) {
+      for (const key in repoConfig) {
+        if (repoConfig.hasOwnProperty(key)) {
           if (key === 'default_submit_type') {
             // default_submit_type is not in the input type, and the
             // configured value was already copied to submit_type by
             // _loadProject. Omit this property when saving.
             continue;
           }
-          if (typeof p[key] === 'object') {
-            configInputObj[key] = p[key].configured_value;
+          if (key === 'plugin_config') {
+            configInputObj.plugin_config_values = repoConfig[key];
+          } else if (typeof repoConfig[key] === 'object') {
+            configInputObj[key] = repoConfig[key].configured_value;
           } else {
-            configInputObj[key] = p[key];
+            configInputObj[key] = repoConfig[key];
           }
         }
       }
@@ -323,5 +339,10 @@
     _computeChangesUrl(name) {
       return Gerrit.Nav.getUrlForProjectChanges(name);
     },
+
+    _handlePluginConfigChanged({detail: {name, config, notifyPath}}) {
+      this._repoConfig.plugin_config[name] = config;
+      this.notifyPath('_repoConfig.plugin_config.' + notifyPath);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
index d6d4366..c987278 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
@@ -125,6 +125,29 @@
       sandbox.restore();
     });
 
+    test('_computePluginData', () => {
+      assert.deepEqual(element._computePluginData(), []);
+      assert.deepEqual(element._computePluginData({}), []);
+      assert.deepEqual(element._computePluginData({base: {}}), []);
+      assert.deepEqual(element._computePluginData({base: {plugin: 'data'}}),
+          [{name: 'plugin', config: 'data'}]);
+    });
+
+    test('_handlePluginConfigChanged', () => {
+      const notifyStub = sandbox.stub(element, 'notifyPath');
+      element._repoConfig = {plugin_config: {}};
+      element._handlePluginConfigChanged({detail: {
+        name: 'test',
+        config: 'data',
+        notifyPath: 'path',
+      }});
+      flushAsynchronousOperations();
+
+      assert.equal(element._repoConfig.plugin_config.test, 'data');
+      assert.equal(notifyStub.lastCall.args[0],
+          '_repoConfig.plugin_config.path');
+    });
+
     test('loading displays before repo config is loaded', () => {
       assert.isTrue(element.$.loading.classList.contains('loading'));
       assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
@@ -136,14 +159,12 @@
     test('download commands visibility', () => {
       element._loading = false;
       flushAsynchronousOperations();
-      assert.isTrue(element.$.downloadContent.classList
-          .contains('hideDownload'));
+      assert.isTrue(element.$.downloadContent.classList.contains('hide'));
       assert.isTrue(getComputedStyle(element.$.downloadContent)
           .display == 'none');
       element._schemesObj = SCHEMES;
       flushAsynchronousOperations();
-      assert.isFalse(element.$.downloadContent.classList
-          .contains('hideDownload'));
+      assert.isFalse(element.$.downloadContent.classList.contains('hide'));
       assert.isFalse(getComputedStyle(element.$.downloadContent)
           .display == 'none');
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 4b66a6e..cfd5bac 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -463,10 +463,11 @@
     saveRepoConfig(repo, config, opt_errFn) {
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      const encodeName = encodeURIComponent(repo);
+      const url = `/projects/${encodeURIComponent(repo)}/config`;
+      this._cache.delete(url);
       return this._send({
         method: 'PUT',
-        url: `/projects/${encodeName}/config`,
+        url,
         body: config,
         errFn: opt_errFn,
         anonymizedUrl: '/projects/*/config',