Polygerrit UI
TODO:
* Autocompletion when adding reviewers.
Bug: Issue 8351
Change-Id: I1fa2f03f95e3b1b4217ec7a418931f5e3fdcf7cf
diff --git a/BUILD b/BUILD
index b9bd435..faa2550 100644
--- a/BUILD
+++ b/BUILD
@@ -5,6 +5,8 @@
"PLUGIN_TEST_DEPS",
"gerrit_plugin",
)
+load("//tools/bzl:genrule2.bzl", "genrule2")
+load("//tools/bzl:js.bzl", "polygerrit_plugin")
SRC = "src/main/java/com/googlesource/gerrit/plugins/reviewers/"
@@ -37,6 +39,28 @@
"Gerrit-Module: com.googlesource.gerrit.plugins.reviewers.Module",
],
resources = glob(["src/main/**/*"]),
+ resource_jars = [":rv-reviewers-static"],
+)
+
+genrule2(
+ name = "rv-reviewers-static",
+ srcs = [":rv-reviewers"],
+ outs = ["rv-reviewers-static.jar"],
+ cmd = " && ".join([
+ "mkdir $$TMP/static",
+ "cp -r $(locations :rv-reviewers) $$TMP/static",
+ "cd $$TMP",
+ "zip -Drq $$ROOT/$@ -g .",
+ ]),
+)
+
+polygerrit_plugin(
+ name = "rv-reviewers",
+ srcs = glob([
+ "rv-reviewers/*.html",
+ "rv-reviewers/*.js",
+ ]),
+ app = "plugin.html",
)
junit_tests(
diff --git a/plugin.html b/plugin.html
new file mode 100644
index 0000000..29c10cb
--- /dev/null
+++ b/plugin.html
@@ -0,0 +1,27 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+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.
+-->
+<link rel="import" href="./rv-reviewers/rv-reviewers.html">
+
+<dom-module id="reviewers">
+ <script>
+ if (window.Polymer) {
+ Gerrit.install(plugin => {
+ plugin.registerCustomComponent('repo-command', 'rv-reviewers');
+ });
+ }
+ </script>
+</dom-module>
diff --git a/rv-reviewers/rv-edit-screen.html b/rv-reviewers/rv-edit-screen.html
new file mode 100644
index 0000000..ca32849
--- /dev/null
+++ b/rv-reviewers/rv-edit-screen.html
@@ -0,0 +1,76 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+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.
+-->
+<link rel="import" href="./rv-filter-section.html">
+
+<dom-module id="rv-edit-screen">
+ <template>
+ <style include="shared-styles"></style>
+ <style include="gr-menu-page-styles"></style>
+ <style include="gr-subpage-styles">
+ .bottomButtons {
+ display: flex;
+ }
+ #closeButton {
+ float: right;
+ }
+ #filterSections {
+ width: 100%;
+ }
+ header {
+ border-bottom: 1px solid var(--border-colo);
+ flex-shrink: 0;
+ font-weight: var(--font-weight-bold)
+ }
+ </style>
+ <div>
+ <header>Reviewers Config</header>
+ <table id="filterSections">
+ <tr>
+ <th>Filter Sections</th>
+ </tr>
+ <tr id="loading" class$="loadingMsg [[_computeLoadingClass(loading)]]">
+ <td>Loading...</td>
+ </tr>
+ <tbody class$="[[_computeLoadingClass(loading)]]">
+ <tr>
+ <template
+ is="dom-repeat"
+ items="[[_filterSections]]"
+ as="section">
+ <rv-filter-section
+ filter="[[section.filter]]"
+ reviewers="[[section.reviewers]]"
+ editing="[[section.editing]]"
+ reviewers-url="[[_getReviewersUrl(repoName)]]"
+ plugin-rest-api="[[pluginRestApi]]"
+ can-modify-config="[[canModifyConfig]]"
+ on-reviewer-changed="_handleReviewerChanged"></rv-filter-section>
+ </template>
+ </tr>
+ </tbody>
+ </table>
+ <div class="bottomButtons">
+ <gr-button id="closeButton" on-tap="_handleCloseTap">Close</gr-button>
+ <gr-button
+ id="addFilterBtn"
+ on-tap="_handleCreateSection"
+ hidden="[[_computeAddFilterBtnHidden(canModifyConfig, _editingFilter)]]">Add New Filter</gr-button>
+ </div>
+ </div>
+ </template>
+ <script src="./rv-edit-screen.js"></script>
+</dom-module>
diff --git a/rv-reviewers/rv-edit-screen.js b/rv-reviewers/rv-edit-screen.js
new file mode 100644
index 0000000..f25f696
--- /dev/null
+++ b/rv-reviewers/rv-edit-screen.js
@@ -0,0 +1,69 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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.
+(function() {
+ Polymer({
+ is: 'rv-edit-screen',
+
+ properties: {
+ pluginRestApi: Object,
+ repoName: String,
+ loading: Boolean,
+ canModifyConfig: Boolean,
+ _editingFilter: {
+ type: Boolean,
+ value: false,
+ },
+ _filterSections: Array,
+ },
+
+ attached() {
+ this._loadFilterSections();
+ },
+
+ _loadFilterSections() {
+ this.pluginRestApi.get(this._getReviewersUrl(this.repoName))
+ .then(filterSections => {
+ this._filterSections = filterSections;
+ });
+ },
+
+ _computeAddFilterBtnHidden(canModifyConfig, editingFilter) {
+ return !canModifyConfig || editingFilter;
+ },
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ },
+
+ _getReviewersUrl(repoName) {
+ return `/projects/${repoName}/reviewers`;
+ },
+
+ _handleCreateSection() {
+ const section = {filter: '', reviewers: [], editing: true};
+ this._editingFilter = true;
+ this.push('_filterSections', section);
+ },
+
+ _handleCloseTap(e) {
+ e.preventDefault();
+ this.fire('close', null, {bubbles: false});
+ },
+
+ _handleReviewerChanged(e) {
+ this._filterSections = e.detail.result;
+ this._editingFilter = false;
+ },
+ });
+})();
diff --git a/rv-reviewers/rv-filter-section.html b/rv-reviewers/rv-filter-section.html
new file mode 100644
index 0000000..d4784cc
--- /dev/null
+++ b/rv-reviewers/rv-filter-section.html
@@ -0,0 +1,96 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+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.
+-->
+<link rel="import" href="./rv-reviewer.html">
+
+<dom-module id="rv-filter-section">
+ <template>
+ <style include="shared-styles">
+ :host {
+ display: block;
+ margin-bottom: 1em;
+ }
+ fieldset {
+ border: 1px solid var(--border-color);
+ }
+ .name {
+ align-items: center;
+ display: flex;
+ }
+ .header {
+ align-items: center;
+ background: var(--table-header-background-color);
+ border-bottom: 1px dotted var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ min-height: 3em;
+ padding: 0 .7em;
+ }
+ #addReviewer {
+ display: flex;
+ }
+ #editFilterInput {
+ width: 30vw;
+ max-width: 500px;
+ margin-left: 3px;
+ }
+ #mainContainer {
+ display: block;
+ }
+ </style>
+ <style include="gr-form-styles"></style>
+ <fieldset id="section"
+ class$="gr-form-styles">
+ <div id="mainContainer">
+ <div class="header">
+ <div class="name">
+ <h3>Filter:</h3>
+ <input
+ id="editFilterInput"
+ bind-value="{{filter}}"
+ is="iron-input"
+ type="text"
+ disabled="[[_computeFilterInputDisabled(canModifyConfig, _originalFilter)]]">
+ <gr-button
+ id="cancelBtn"
+ on-tap="_handleCancel"
+ hidden$="[[_computeCancelHidden(filter, _originalFilter)]]">Cancel</gr-button>
+ </div><!-- name -->
+ </div><!-- header -->
+ <div class="reviewers">
+ <template
+ is="dom-repeat"
+ items="{{reviewers}}">
+ <rv-reviewer
+ reviewer="{{item}}"
+ can-modify-config="[[canModifyConfig]]"
+ on-reviewer-deleted="_handleReviewerDeleted"
+ on-reviewer-added="_handleReviewerAdded">
+ </rv-reviewer>
+ </template>
+ <div id="addReviewer">
+ <gr-button
+ link
+ id="addBtn"
+ on-tap="_handleAddReviewer"
+ hidden="[[_computeAddBtnHidden(canModifyConfig, _editingReviewer)]]">Add Reviewer</gr-button>
+ </div><!-- addReviewer -->
+ </div><!-- reviewers -->
+ </div>
+ </fieldset>
+ </template>
+ <script src="./rv-filter-section.js"></script>
+</dom-module>
diff --git a/rv-reviewers/rv-filter-section.js b/rv-reviewers/rv-filter-section.js
new file mode 100644
index 0000000..0ca36dc
--- /dev/null
+++ b/rv-reviewers/rv-filter-section.js
@@ -0,0 +1,98 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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.
+(function() {
+ Polymer({
+ is: 'rv-filter-section',
+
+ properties: {
+ pluginRestApi: Object,
+ reviewers: Array,
+ filter: String,
+ canModifyConfig: Boolean,
+ _originalFilter: String,
+ _editingReviewer: {
+ type: Boolean,
+ value: false,
+ },
+ reviewersUrl: String,
+ },
+
+ attached() {
+ this._updateSection();
+ },
+
+ _updateSection() {
+ this._originalFilter = this.filter;
+ },
+
+ _computeEditing(filter, _originalFilter) {
+ if (_originalFilter === '') {
+ return true;
+ }
+ return filter === '';
+ },
+
+ _computeCancelHidden(filter, _originalFilter) {
+ return !this._computeEditing(filter, _originalFilter);
+ },
+
+ _computeAddBtnHidden(canModifyConfig, editingReviewer) {
+ return !(canModifyConfig && !editingReviewer);
+ },
+
+ _computeFilterInputDisabled(canModifyConfig, originalFilter) {
+ return !canModifyConfig || originalFilter !== '';
+ },
+
+ _handleCancel() {
+ this.remove();
+ },
+
+ _handleReviewerDeleted(e) {
+ if (e.detail.editing) {
+ this.reviewers.pop();
+ this._editingReviewer = false;
+ } else {
+ const index = e.model.index;
+ const deleted = this.reviewers[index];
+ this._putReviewer(deleted, 'DELETE');
+ }
+ },
+
+ _handleReviewerAdded(e) {
+ this._editingReviewer = false;
+ this._putReviewer(e.detail.reviewer, 'ADD').catch(err => {
+ this.fire('show-alert', {message: err});
+ throw err;
+ });
+ },
+
+ _putReviewer(reviewer, action) {
+ return this.pluginRestApi.put(this.reviewersUrl, {
+ action,
+ reviewer,
+ filter: this.filter,
+ }).then(result => {
+ const detail = {result};
+ this.dispatchEvent(
+ new CustomEvent('reviewer-changed', {detail, bubbles: true}));
+ });
+ },
+
+ _handleAddReviewer() {
+ this.push('reviewers', '');
+ this._editingReviewer = true;
+ },
+ });
+})();
diff --git a/rv-reviewers/rv-reviewer.html b/rv-reviewers/rv-reviewer.html
new file mode 100644
index 0000000..3609612
--- /dev/null
+++ b/rv-reviewers/rv-reviewer.html
@@ -0,0 +1,52 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+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.
+-->
+<dom-module id="rv-reviewer">
+ <template>
+ <style include="shared-styles">
+ #editReviewerInput {
+ display: block;
+ }
+ .reviewerRow {
+ align-items: center;
+ display: flex;
+ }
+ #reviewerHeader, #editReviewerInput, #deleteCancelBtn, #addBtn {
+ margin-left: 3px;
+ }
+ </style>
+ <style include="gr-form-styles"></style>
+ <div class="reviewerRow">
+ <h4 id="reviewerHeader">Reviewer:</h4>
+ <input
+ id="editReviewerInput"
+ bind-value="{{reviewer}}"
+ is="iron-input"
+ type="text"
+ disabled="[[_computeReviewerDisabled(reviewer, _originalReviewer)]]">
+ <gr-button
+ id="deleteCancelBtn"
+ on-tap="_handleDeleteCancel"
+ hidden$="[[_computeHideDeleteButton(canModifyConfig)]]"
+ >[[_computeDeleteCancel(reviewer, _originalReviewer)]]</gr-button>
+ <gr-button
+ id="addBtn"
+ on-tap="_handleAddReviewer"
+ hidden$="[[_computeHideAddButton(reviewer, _originalReviewer)]]">Add</gr-button>
+ </div> <!-- reviewerRow -->
+ </template>
+ <script src="./rv-reviewer.js"></script>
+</dom-module>
diff --git a/rv-reviewers/rv-reviewer.js b/rv-reviewers/rv-reviewer.js
new file mode 100644
index 0000000..9698a22
--- /dev/null
+++ b/rv-reviewers/rv-reviewer.js
@@ -0,0 +1,74 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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.
+(function() {
+ Polymer({
+ is: 'rv-reviewer',
+
+ properties: {
+ reviewer: String,
+ canModifyConfig: Boolean,
+ _originalReviewer: String,
+ _deleted: Boolean,
+ _editing: {
+ type: Boolean,
+ computed: '_computeEditing(reviewer, _originalReviewer)',
+ },
+ },
+
+ attached() {
+ this._originalReviewer = this.reviewer;
+ },
+
+ _computeReviewerDisabled(reviewer, _originalReviewer) {
+ return !this._computeEditing(reviewer, _originalReviewer);
+ },
+
+ _computeEditing(reviewer, _originalReviewer) {
+ if (_originalReviewer === '') {
+ return true;
+ }
+ return reviewer === '';
+ },
+
+ _computeDeleteCancel(reviewer, _originalReviewer) {
+ return this._computeEditing(reviewer, _originalReviewer) ?
+ 'Cancel' : 'Delete';
+ },
+
+ _computeHideAddButton(reviewer, _originalReviewer) {
+ return !(this._computeEditing(reviewer, _originalReviewer)
+ && this.$.editReviewerInput.value);
+ },
+
+ _computeHideDeleteButton(canModifyConfig) {
+ return !canModifyConfig;
+ },
+
+ _handleDeleteCancel() {
+ const detail = {editing: this._editing};
+ if (this._editing) {
+ this.remove();
+ }
+ this.dispatchEvent(
+ new CustomEvent('reviewer-deleted', {detail, bubbles: true}));
+ },
+
+ _handleAddReviewer() {
+ const detail = {reviewer: this.reviewer};
+ this._originalReviewer = this.reviewer;
+ this.dispatchEvent(
+ new CustomEvent('reviewer-added', {detail, bubbles: true}));
+ },
+ });
+})();
diff --git a/rv-reviewers/rv-reviewers.html b/rv-reviewers/rv-reviewers.html
new file mode 100644
index 0000000..dcf890d
--- /dev/null
+++ b/rv-reviewers/rv-reviewers.html
@@ -0,0 +1,40 @@
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+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.
+-->
+<link rel="import" href="./rv-edit-screen.html">
+
+<dom-module id="rv-reviewers">
+ <template>
+ <style include="shared-styles">
+ #rvScreenOverlay {
+ width: 50em;
+ }
+ </style>
+ <gr-repo-command
+ title="Reviewers Config"
+ on-command-tap="_handleCommandTap">
+ </gr-repo-command>
+ <gr-overlay id="rvScreenOverlay" with-backdrop>
+ <rv-edit-screen
+ plugin-rest-api="[[pluginRestApi]]"
+ repo-name="[[repoName]]"
+ loading="[[_loading]]"
+ can-modify-config="[[_canModifyConfig]]"
+ on-close="_handleRvEditScreenClose"></rv-edit-screen>
+ </gr-overlay>
+ </template>
+ <script src="./rv-reviewers.js"></script>
+</dom-module>
diff --git a/rv-reviewers/rv-reviewers.js b/rv-reviewers/rv-reviewers.js
new file mode 100644
index 0000000..e40e03a
--- /dev/null
+++ b/rv-reviewers/rv-reviewers.js
@@ -0,0 +1,82 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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.
+(function() {
+ Polymer({
+ is: 'rv-reviewers',
+
+ properties: {
+ pluginRestApi: Object,
+ repoName: String,
+ _canModifyConfig: {
+ type: Boolean,
+ computed: '_computeCanModifyConfig(_isOwner, _hasModifyCapability)',
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _isOwner: {
+ type: Boolean,
+ value: false,
+ },
+ _hasModifyCapability: {
+ type: Boolean,
+ value: false,
+ },
+ },
+
+ attached() {
+ this.pluginRestApi = this.plugin.restApi();
+ this._setCanModifyConfig();
+ },
+
+ _handleCommandTap() {
+ this.$.rvScreenOverlay.open();
+ },
+
+ _handleRvEditScreenClose() {
+ this.$.rvScreenOverlay.close();
+ },
+
+ _setCanModifyConfig() {
+ const promises = [];
+ promises.push(this._getRepoAccess(this.repoName).then( access => {
+ if (access && access[this.repoName] && access[this.repoName].is_owner) {
+ this._isOwner = true;
+ }
+ }));
+ promises.push(this._getCapabilities().then(capabilities => {
+ if (capabilities['reviewers-modifyReviewersConfig']) {
+ this._hasModifyCapability = true;
+ }
+ }));
+ Promise.all(promises).then(() => {
+ this._loading = false;
+ });
+ },
+
+ _computeCanModifyConfig(isOwner, hasModifyCapability) {
+ return isOwner || hasModifyCapability;
+ },
+
+ _getRepoAccess(repoName) {
+ return this.pluginRestApi.get(
+ '/access/?project=' + encodeURIComponent(repoName));
+ },
+
+ _getCapabilities() {
+ return this.pluginRestApi.get('/accounts/self/capabilities');
+ },
+ });
+})();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ClientModule.java b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ClientModule.java
index 4aac38f..bf6acbb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/reviewers/ClientModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/reviewers/ClientModule.java
@@ -16,6 +16,7 @@
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.webui.GwtPlugin;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
import com.google.gerrit.extensions.webui.TopMenu;
import com.google.gerrit.extensions.webui.WebUiPlugin;
import com.google.inject.AbstractModule;
@@ -25,5 +26,7 @@
protected void configure() {
DynamicSet.bind(binder(), TopMenu.class).to(ReviewersTopMenu.class);
DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(new GwtPlugin("reviewers"));
+ DynamicSet.bind(binder(), WebUiPlugin.class)
+ .toInstance(new JavaScriptPlugin("rv-reviewers.html"));
}
}