Convert the reviewers plugin to TypeScript and Lit
Change-Id: I41ec46590e4b44a9219e786e5f51fbde50f2c329
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index e69de29..0000000
--- a/.eslintignore
+++ /dev/null
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..749f7c4
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright (C) 2021 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.
+ */
+__plugindir = 'reviewers';
+module.exports = {
+ extends: '../.eslintrc.js',
+};
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index da33ea2..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,165 +0,0 @@
-{
- "extends": ["eslint:recommended", "google"],
- "parserOptions": {
- "ecmaVersion": 8,
- "sourceType": "module"
- },
- "env": {
- "browser": true,
- "es6": true
- },
- "globals": {
- "__dirname": false,
- "app": false,
- "page": false,
- "Polymer": false,
- "process": false,
- "require": false,
- "Gerrit": false,
- "Promise": false,
- "assert": false,
- "test": false,
- "flushAsynchronousOperations": false
- },
- "rules": {
- "arrow-parens": ["error", "as-needed"],
- "block-spacing": ["error", "always"],
- "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
- "camelcase": "off",
- "comma-dangle": ["error", {
- "arrays": "always-multiline",
- "objects": "always-multiline",
- "imports": "always-multiline",
- "exports": "always-multiline",
- "functions": "never"
- }],
- "eol-last": "off",
- "indent": ["error", 2, {
- "MemberExpression": 2,
- "FunctionDeclaration": {"body": 1, "parameters": 2},
- "FunctionExpression": {"body": 1, "parameters": 2},
- "CallExpression": {"arguments": 2 },
- "ArrayExpression": 1,
- "ObjectExpression": 1,
- "SwitchCase": 1
- }],
- "keyword-spacing": ["error", { "after": true, "before": true }],
- "lines-between-class-members": ["error", "always"],
- "max-len": [
- "error",
- 80,
- 2,
- {
- "ignoreComments": true,
- "ignorePattern": "^import .*;$"
- }
- ],
- "new-cap": ["error", { "capIsNewExceptions": ["Polymer", "LegacyElementMixin", "GestureEventListeners", "LegacyDataMixin"] }],
- "no-console": "off",
- "no-multiple-empty-lines": [ "error", { "max": 1 } ],
- "no-prototype-builtins": "off",
- "no-redeclare": "off",
- "no-restricted-syntax": [
- "error",
- {
- "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='test'][property.name='only']",
- "message": "Remove test.only."
- },
- {
- "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='suite'][property.name='only']",
- "message": "Remove suite.only."
- }
- ],
- "no-undef": "off",
- "no-useless-escape": "off",
- "no-var": "error",
- "object-shorthand": ["error", "always"],
- "padding-line-between-statements": [
- "error",
- {
- "blankLine": "always",
- "prev": "class",
- "next": "*"
- },
- {
- "blankLine": "always",
- "prev": "*",
- "next": "class"
- }
- ],
- "prefer-arrow-callback": "error",
- "prefer-const": "error",
- "prefer-spread": "error",
- "quote-props": ["error", "consistent-as-needed"],
- "semi": [2, "always"],
- "template-curly-spacing": "error",
- "valid-jsdoc": "off",
- "require-jsdoc": 0,
- "valid-jsdoc": 0,
- "jsdoc/check-alignment": 2,
- "jsdoc/check-examples": 0,
- "jsdoc/check-indentation": 0,
- "jsdoc/check-param-names": 0,
- "jsdoc/check-syntax": 0,
- "jsdoc/check-tag-names": 0,
- "jsdoc/check-types": 0,
- "jsdoc/implements-on-classes": 2,
- "jsdoc/match-description": 0,
- "jsdoc/newline-after-description": 2,
- "jsdoc/no-types": 0,
- "jsdoc/no-undefined-types": 0,
- "jsdoc/require-description": 0,
- "jsdoc/require-description-complete-sentence": 0,
- "jsdoc/require-example": 0,
- "jsdoc/require-hyphen-before-param-description": 0,
- "jsdoc/require-jsdoc": 0,
- "jsdoc/require-param": 0,
- "jsdoc/require-param-description": 0,
- "jsdoc/require-param-name": 2,
- "jsdoc/require-param-type": 2,
- "jsdoc/require-returns": 0,
- "jsdoc/require-returns-check": 0,
- "jsdoc/require-returns-description": 0,
- "jsdoc/require-returns-type": 2,
- "jsdoc/valid-types": 2,
- "jsdoc/require-file-overview": ["error", {
- "tags": {
- "license": {
- "mustExist": true,
- "preventDuplicates": true
- }
- }
- }],
- "import/named": 2,
- "import/no-unresolved": 2,
- "import/no-self-import": 2,
- // The no-cycle rule is slow, because it doesn't cache dependencies.
- // Disable it.
- "import/no-cycle": 0,
- "import/no-useless-path-segments": 2,
- "import/no-unused-modules": 2,
- "import/no-default-export": 2
- },
- "plugins": [
- "html",
- "jsdoc",
- "import"
- ],
- "settings": {
- "html/report-bad-indent": "error"
- },
- "overrides": [
- {
- "files": ["*_html.js", "*-styles.js", "externs.js"],
- "rules": {
- "max-len": "off"
- }
- },
- {
- "files": ["*.html"],
- "rules": {
- "jsdoc/require-file-overview": "off"
- }
- }
- ]
-}
diff --git a/.gitignore b/.gitignore
index 1c805b7..cd156c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
/local.properties
*.pyc
/package-lock.json
+/node_modules
diff --git a/BUILD b/BUILD
index 52f43b1..6ad9310 100644
--- a/BUILD
+++ b/BUILD
@@ -1,6 +1,6 @@
load("@rules_java//java:defs.bzl", "java_library")
load("//tools/bzl:junit.bzl", "junit_tests")
-load("//tools/js:eslint.bzl", "eslint")
+load("//tools/js:eslint.bzl", "plugin_eslint")
load(
"//tools/bzl:plugin.bzl",
"PLUGIN_DEPS",
@@ -8,6 +8,7 @@
"gerrit_plugin",
)
load("//tools/bzl:js.bzl", "gerrit_js_bundle")
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
gerrit_plugin(
name = "reviewers",
@@ -20,9 +21,32 @@
resources = glob(["src/main/resources/**/*"]),
)
+ts_config(
+ name = "tsconfig",
+ src = "tsconfig.json",
+ deps = [
+ "//plugins:tsconfig-plugins-base.json",
+ ],
+)
+
+ts_project(
+ name = "rv-reviewers-ts",
+ srcs = glob([
+ "rv-reviewers/**/*.ts",
+ ]),
+ incremental = True,
+ supports_workers = True,
+ tsc = "//tools/node_tools:tsc-bin",
+ tsconfig = ":tsconfig",
+ deps = [
+ "@plugins_npm//@gerritcodereview/typescript-api",
+ "@plugins_npm//lit",
+ ],
+)
+
gerrit_js_bundle(
name = "rv-reviewers",
- srcs = glob(["rv-reviewers/*.js"]),
+ srcs = [":rv-reviewers-ts"],
entry_point = "rv-reviewers/plugin.js",
)
@@ -36,23 +60,4 @@
],
)
-# Define the eslinter for the plugin
-# The eslint macro creates 2 rules: lint_test and lint_bin
-eslint(
- name = "lint",
- srcs = glob([
- "rv-reviewers/*.js",
- ]),
- config = ".eslintrc.json",
- data = [],
- extensions = [
- ".js",
- ],
- ignore = ".eslintignore",
- plugins = [
- "@npm//eslint-config-google",
- "@npm//eslint-plugin-html",
- "@npm//eslint-plugin-import",
- "@npm//eslint-plugin-jsdoc",
- ],
-)
+plugin_eslint()
diff --git a/rv-reviewers/plugin.ts b/rv-reviewers/plugin.ts
index ba7e1d2..3b73980 100644
--- a/rv-reviewers/plugin.ts
+++ b/rv-reviewers/plugin.ts
@@ -14,9 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import './rv-reviewers.js';
+import '@gerritcodereview/typescript-api/gerrit';
+import './rv-reviewers';
-Gerrit.install(plugin => {
- plugin.registerCustomComponent(
- 'repo-command', 'rv-reviewers');
+window.Gerrit.install(plugin => {
+ plugin.registerCustomComponent('repo-command', 'rv-reviewers');
});
diff --git a/rv-reviewers/rv-edit-screen.ts b/rv-reviewers/rv-edit-screen.ts
index b0419bf..1d6dcc7 100644
--- a/rv-reviewers/rv-edit-screen.ts
+++ b/rv-reviewers/rv-edit-screen.ts
@@ -14,73 +14,148 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {htmlTemplate} from './rv-edit-screen_html.js';
+import {RepoName} from '@gerritcodereview/typescript-api/rest-api';
+import {RestPluginApi} from '@gerritcodereview/typescript-api/rest';
+import {css, html, LitElement} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import './rv-filter-section';
+import {Section} from './rv-filter-section';
+import {fire} from './util';
-class RvEditScreen extends Polymer.Element {
- /** @returns {string} name of the component */
- static get is() { return 'rv-edit-screen'; }
+function getReviewersUrl(repoName: RepoName) {
+ return `/projects/${encodeURIComponent(repoName)}/reviewers`;
+}
- /** @returns {?} template for this component */
- static get template() { return htmlTemplate; }
-
- /**
- * Defines properties of the component
- *
- * @returns {?}
- */
- static get properties() {
- return {
- pluginRestApi: {
- type: Object,
- observer: '_loadFilterSections',
- },
- repoName: String,
- loading: Boolean,
- canModifyConfig: Boolean,
- _editingFilter: {
- type: Boolean,
- value: false,
- },
- _filterSections: Array,
- };
- }
-
- _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/${encodeURIComponent(repoName)}/reviewers`;
- }
-
- _handleCreateSection() {
- const section = {filter: '', reviewers: [], ccs: [], editing: true};
- this._editingFilter = true;
- this.push('_filterSections', section);
- }
-
- _handleCloseTap(e) {
- e.preventDefault();
- this.dispatchEvent(new CustomEvent('close', {
- composed: true, bubbles: false,
- }));
- }
-
- _handleReviewerChanged(e) {
- this._filterSections = e.detail.result;
- this._editingFilter = false;
+declare global {
+ interface HTMLElementTagNameMap {
+ 'rv-edit-screen': RvEditScreen;
}
}
-customElements.define(RvEditScreen.is, RvEditScreen);
+@customElement('rv-edit-screen')
+export class RvEditScreen extends LitElement {
+ @property()
+ pluginRestApi!: RestPluginApi;
+
+ @property()
+ repoName!: RepoName;
+
+ @property()
+ loading = false;
+
+ @property()
+ canModifyConfig = false;
+
+ @state()
+ editingFilter = false;
+
+ @state()
+ filterSections: Section[] = [];
+
+ static override get styles() {
+ return [
+ css`
+ :host {
+ padding: var(--spacing-xl);
+ display: block;
+ }
+ .bottomButtons {
+ display: flex;
+ justify-content: flex-end;
+ }
+ gr-button {
+ margin-left: var(--spacing-m);
+ }
+ #filterSections {
+ width: 100%;
+ }
+ `,
+ ];
+ }
+
+ render() {
+ return html`
+ <div>
+ <h3 class="heading-3">Reviewers Config</h3>
+ <table id="filterSections">
+ <tbody>
+ ${this.renderEmpty()}
+ ${this.loading
+ ? html`<tr>
+ <td>Loading...</td>
+ </tr>`
+ : this.filterSections.map(s => this.renderSection(s))}
+ </tbody>
+ </table>
+ <div class="bottomButtons">
+ <gr-button
+ id="addFilterBtn"
+ @click="${this.handleCreateSection}"
+ ?hidden="${!this.canModifyConfig || this.editingFilter}"
+ >
+ Add New Filter
+ </gr-button>
+ <gr-button id="closeButton" @click="${this.handleCloseTap}">
+ Close
+ </gr-button>
+ </div>
+ </div>
+ `;
+ }
+
+ private renderEmpty() {
+ if (this.loading || this.filterSections.length > 0) return;
+ return html`<tr>
+ <td>No filter defined yet.</td>
+ </tr>`;
+ }
+
+ private renderSection(section: Section) {
+ return html`
+ <tr>
+ <td>
+ <rv-filter-section
+ .filter="${section.filter}"
+ .reviewers="${section.reviewers}"
+ .ccs="${section.ccs}"
+ .reviewersUrl="${getReviewersUrl(this.repoName)}"
+ .repoName="${this.repoName}"
+ .pluginRestApi="${this.pluginRestApi}"
+ .canModifyConfig="${this.canModifyConfig}"
+ @reviewer-changed="${this.handleReviewerChanged}"
+ >
+ </rv-filter-section>
+ </td>
+ </tr>
+ `;
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.pluginRestApi
+ .get<Section[]>(getReviewersUrl(this.repoName))
+ .then(sections => {
+ this.filterSections = sections;
+ });
+ }
+
+ private handleCreateSection() {
+ const section = {filter: '', reviewers: [], ccs: [], editing: true};
+ this.filterSections = [...this.filterSections, section];
+ this.editingFilter = true;
+ fire(this, 'fit');
+ }
+
+ private handleCloseTap(e: Event) {
+ e.preventDefault();
+ fire(this, 'close');
+ }
+
+ private handleReviewerChanged(e: CustomEvent<Section[]>) {
+ // Even if just one reviewer is changed or deleted, then we still completely
+ // re-render everything from scratch.
+ this.filterSections = e.detail;
+ this.editingFilter = false;
+ fire(this, 'fit');
+ }
+}
diff --git a/rv-reviewers/rv-edit-screen_html.ts b/rv-reviewers/rv-edit-screen_html.ts
deleted file mode 100644
index 954d46c..0000000
--- a/rv-reviewers/rv-edit-screen_html.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * @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.
- */
-import './rv-filter-section.js';
-
-export const htmlTemplate = Polymer.html`
- <style include="shared-styles"></style>
- <style include="gr-menu-page-styles"></style>
- <style include="gr-subpage-styles">
- :host {
- padding: var(--spacing-xl);
- }
- .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]]"
- ccs="[[section.ccs]]"
- editing="[[section.editing]]"
- reviewers-url="[[_getReviewersUrl(repoName)]]"
- repo-name="[[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>
-`;
diff --git a/rv-reviewers/rv-filter-section.ts b/rv-reviewers/rv-filter-section.ts
index ae43a62..cfde6b8 100644
--- a/rv-reviewers/rv-filter-section.ts
+++ b/rv-reviewers/rv-filter-section.ts
@@ -14,120 +14,239 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {htmlTemplate} from './rv-filter-section_html.js';
+import {css, html, LitElement} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {RestPluginApi} from '@gerritcodereview/typescript-api/rest';
+import {RepoName} from '@gerritcodereview/typescript-api/rest-api';
+import './rv-reviewer';
+import {
+ ReviewerAddedEventDetail,
+ ReviewerDeletedEventDetail,
+ Type,
+} from './rv-reviewer';
+import {fire} from './util';
-class RvFilterSection extends Polymer.Element {
- /** @returns {string} name of the component */
- static get is() { return 'rv-filter-section'; }
+enum Action {
+ ADD = 'ADD',
+ REMOVE = 'REMOVE',
+}
- /** @returns {?} template for this component */
- static get template() { return htmlTemplate; }
+export interface Section {
+ filter: string;
+ reviewers: string[];
+ ccs: string[];
+ editing: boolean;
+}
- /**
- * Defines properties of the component
- *
- * @returns {?}
- */
- static get properties() {
- return {
- pluginRestApi: Object,
- repoName: String,
- reviewers: Array,
- ccs: Array,
- filter: String,
- canModifyConfig: Boolean,
- _originalFilter: String,
- _editingReviewer: {
- type: Boolean,
- value: false,
- },
- reviewersUrl: String,
- };
- }
-
- connectedCallback() {
- super.connectedCallback();
- 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) {
- const type = e.detail.type;
- if (e.detail.editing) {
- if (type === 'CC') {
- this.ccs.pop();
- } else {
- this.reviewers.pop();
- }
- this._editingReviewer = false;
- } else {
- const index = e.model.index;
- const deleted = type === 'CC' ? this.ccs[index] : this.reviewers[index];
- this._putReviewer(deleted, 'REMOVE', type);
- }
- }
-
- _handleReviewerAdded(e) {
- this._editingReviewer = false;
- this._putReviewer(e.detail.reviewer, 'ADD', e.detail.type).catch(err => {
- this.dispatchEvent(new CustomEvent('show-alert', {
- detail: {
- message: err,
- },
- composed: true, bubbles: true,
- }));
- throw err;
- });
- }
-
- _putReviewer(reviewer, action, type) {
- return this.pluginRestApi.put(this.reviewersUrl, {
- action,
- reviewer,
- type,
- filter: this.filter,
- }).then(result => {
- const detail = {result};
- this.dispatchEvent(
- new CustomEvent('reviewer-changed', {detail, bubbles: true}));
- });
- }
-
- _handleAddReviewer() {
- this.push('reviewers', '');
- this._editingReviewer = true;
- }
-
- _handleAddCc() {
- this.push('ccs', '');
- this._editingReviewer = true;
+declare global {
+ interface HTMLElementTagNameMap {
+ 'rv-filter-section': RvFilterSection;
}
}
-customElements.define(RvFilterSection.is, RvFilterSection);
+@customElement('rv-filter-section')
+export class RvFilterSection extends LitElement {
+ @query('#filterInput')
+ filterInput?: HTMLInputElement;
+
+ @property()
+ pluginRestApi!: RestPluginApi;
+
+ @property()
+ repoName!: RepoName;
+
+ @property()
+ reviewers: string[] = [];
+
+ @property()
+ ccs: string[] = [];
+
+ @property()
+ filter = '';
+
+ @property()
+ canModifyConfig = false;
+
+ @property({type: String})
+ reviewersUrl = '';
+
+ /**
+ * If a filter was already set initially, then you cannot "cancel" creating
+ * this filter.
+ */
+ @state()
+ originalFilter = '';
+
+ /** While a reviewer is being edited you cannot add another. */
+ @state()
+ editingReviewer = false;
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.originalFilter = this.filter;
+ }
+
+ static override get styles() {
+ return [
+ css`
+ :host {
+ display: block;
+ margin-bottom: 1em;
+ }
+ #container {
+ display: block;
+ border: 1px solid var(--border-color);
+ }
+ #filter {
+ align-items: center;
+ background: var(--table-header-background-color);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ min-height: 3em;
+ padding: 0 var(--spacing-m);
+ }
+ #filterInput {
+ width: 30vw;
+ max-width: 500px;
+ margin-left: var(--spacing-s);
+ }
+ gr-button {
+ margin-left: var(--spacing-m);
+ }
+ #addReviewer {
+ display: flex;
+ padding: var(--spacing-s) 0;
+ }
+ `,
+ ];
+ }
+
+ render() {
+ return html`
+ <div id="container">
+ <div id="filter">
+ <span class="heading-3">Filter</span>
+ <input
+ id="filterInput"
+ value="${this.filter}"
+ @input="${this.onFilterInput}"
+ ?disabled="${!this.canModifyConfig || this.originalFilter !== ''}"
+ />
+ <gr-button
+ @click="${() => this.remove()}"
+ ?hidden="${this.originalFilter !== '' && this.filter !== ''}"
+ >
+ Cancel
+ </gr-button>
+ </div>
+ <div>
+ ${this.reviewers.map(item =>
+ this.renderReviewer(item, Type.REVIEWER)
+ )}
+ ${this.ccs.map(item => this.renderReviewer(item, Type.CC))}
+ <div id="addReviewer">
+ <gr-button
+ link
+ @click="${this.handleAddReviewer}"
+ ?disabled="${this.filter === ''}"
+ ?hidden="${!this.canModifyConfig || this.editingReviewer}"
+ >
+ Add Reviewer
+ </gr-button>
+ <gr-button
+ link
+ @click="${this.handleAddCc}"
+ ?disabled="${this.filter === ''}"
+ ?hidden="${!this.canModifyConfig || this.editingReviewer}"
+ >
+ Add CC
+ </gr-button>
+ </div>
+ </div>
+ </div>
+ `;
+ }
+
+ private renderReviewer(reviewer: string, type: Type) {
+ return html`
+ <rv-reviewer
+ .reviewer="${reviewer}"
+ .type="${type}"
+ .canModifyConfig="${this.canModifyConfig}"
+ .pluginRestApi="${this.pluginRestApi}"
+ .repoName="${this.repoName}"
+ @reviewer-deleted="${(e: CustomEvent<ReviewerDeletedEventDetail>) =>
+ this.handleReviewerDeleted(e, reviewer)}"
+ @reviewer-added="${(e: CustomEvent<ReviewerAddedEventDetail>) =>
+ this.handleReviewerAdded(e)}"
+ >
+ </rv-reviewer>
+ `;
+ }
+
+ private onFilterInput() {
+ this.filter = this.filterInput?.value ?? '';
+ }
+
+ private handleReviewerDeleted(
+ e: CustomEvent<ReviewerDeletedEventDetail>,
+ reviewer: string
+ ) {
+ const {type, editing} = e.detail;
+ if (editing) {
+ // Just cancelling edit. Nothing was persisted yet, so nothing to delete.
+ if (type === Type.CC) {
+ this.ccs = [...this.ccs.slice(0, -1)];
+ } else {
+ this.reviewers = [...this.reviewers.slice(0, -1)];
+ }
+ this.editingReviewer = false;
+ } else {
+ // The reviewer was not in edit mode, but DELETE was clicked.
+ this.putReviewer(reviewer, Action.REMOVE, type);
+ }
+ }
+
+ private handleReviewerAdded(e: CustomEvent<ReviewerAddedEventDetail>) {
+ this.editingReviewer = false;
+ this.putReviewer(e.detail.reviewer, Action.ADD, e.detail.type).catch(
+ err => {
+ fire(this, 'show-alert', {message: err});
+ throw err;
+ }
+ );
+ }
+
+ private putReviewer(reviewer: string, action: Action, type: Type) {
+ if (this.filter === '') throw new Error('empty filter');
+ if (reviewer === '') throw new Error('empty reviewer');
+ return this.pluginRestApi
+ .put<Section[]>(this.reviewersUrl, {
+ action,
+ reviewer,
+ type,
+ filter: this.filter,
+ })
+ .then((sections: Section[]) => {
+ // Even if just one reviewer is changed or deleted, we will get the
+ // the complete list of sections back from the server, and we dispatch
+ // this event such that the entire dialog re-renders from scratch.
+ // Lit is smart enough to re-use the component though, so we also want
+ // to re-initialize the state here:
+ this.editingReviewer = false;
+ this.originalFilter = this.filter;
+ fire(this, 'reviewer-changed', sections);
+ });
+ }
+
+ private handleAddReviewer() {
+ this.reviewers = [...this.reviewers, ''];
+ this.editingReviewer = true;
+ }
+
+ private handleAddCc() {
+ this.ccs = [...this.ccs, ''];
+ this.editingReviewer = true;
+ }
+}
diff --git a/rv-reviewers/rv-filter-section_html.ts b/rv-reviewers/rv-filter-section_html.ts
deleted file mode 100644
index 87cc7a3..0000000
--- a/rv-reviewers/rv-filter-section_html.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * @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.
- */
-import './rv-reviewer.js';
-
-export const htmlTemplate = Polymer.html`
- <style include="shared-styles">
- :host {
- display: block;
- margin-bottom: 1em;
- }
- fieldset {
- border: 1px solid var(--border-color);
- }
- .name {
- align-items: center;
- display: flex;
- }
- .name gr-button {
- margin-left: var(--spacing-m);
- }
- .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>
- <iron-input
- id="editFilterInput"
- bind-value="{{filter}}"
- type="text"
- disabled="[[_computeFilterInputDisabled(canModifyConfig, _originalFilter)]]">
- <input
- id="editFilterInput"
- bind-value="{{filter}}"
- is="iron-input"
- type="text"
- disabled="[[_computeFilterInputDisabled(canModifyConfig, _originalFilter)]]">
- </iron-input>
- <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}}"
- type="REVIEWER"
- can-modify-config="[[canModifyConfig]]"
- plugin-rest-api="[[pluginRestApi]]"
- repo-name="[[repoName]]"
- on-reviewer-deleted="_handleReviewerDeleted"
- on-reviewer-added="_handleReviewerAdded">
- </rv-reviewer>
- </template>
- <template
- is="dom-repeat"
- items="{{ccs}}">
- <rv-reviewer
- reviewer="{{item}}"
- type="CC"
- can-modify-config="[[canModifyConfig]]"
- plugin-rest-api="[[pluginRestApi]]"
- repo-name="[[repoName]]"
- on-reviewer-deleted="_handleReviewerDeleted"
- on-reviewer-added="_handleReviewerAdded">
- </rv-reviewer>
- </template>
- <div id="addReviewer">
- <gr-button
- link
- id="addRevBtn"
- on-tap="_handleAddReviewer"
- hidden="[[_computeAddBtnHidden(canModifyConfig, _editingReviewer)]]">Add Reviewer</gr-button>
- <gr-button
- link
- id="addCcBtn"
- on-tap="_handleAddCc"
- hidden="[[_computeAddBtnHidden(canModifyConfig, _editingReviewer)]]">Add Cc</gr-button>
- </div><!-- addReviewer -->
- </div><!-- reviewers -->
- </div>
- </fieldset>
-`;
diff --git a/rv-reviewers/rv-reviewer.ts b/rv-reviewers/rv-reviewer.ts
index 6145013..1158be7 100644
--- a/rv-reviewers/rv-reviewer.ts
+++ b/rv-reviewers/rv-reviewer.ts
@@ -14,154 +14,254 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {htmlTemplate} from './rv-reviewer_html.js';
+import {customElement, property, state} from 'lit/decorators';
+import {css, html, LitElement} from 'lit';
+import {RestPluginApi} from '@gerritcodereview/typescript-api/rest';
+import {
+ AccountInfo,
+ GroupInfo,
+ RepoName,
+} from '@gerritcodereview/typescript-api/rest-api';
+import {fire} from './util';
-class RvReviewer extends Polymer.Element {
- /** @returns {string} name of the component */
- static get is() { return 'rv-reviewer'; }
+declare global {
+ interface HTMLElementTagNameMap {
+ 'rv-reviewer': RvReviewer;
+ }
+}
- /** @returns {?} template for this component */
- static get template() { return htmlTemplate; }
+export enum Type {
+ REVIEWER = 'REVIEWER',
+ CC = 'CC',
+}
+
+export interface ReviewerDeletedEventDetail {
+ /**
+ * If true, then this means a reviewer addition was just canceled. Not server
+ * update required.
+ * If false, then the entry has to be deleted server side by the event
+ * handler.
+ */
+ editing: boolean;
+ type: Type;
+}
+
+export interface ReviewerAddedEventDetail {
+ reviewer: string;
+ type: Type;
+}
+
+type GroupNameToInfo = {[name: string]: GroupInfo};
+
+interface NameValue {
+ name: string;
+ value: string;
+}
+
+function computeValue(account: AccountInfo): string | undefined {
+ if (account.username) {
+ return account.username;
+ }
+ if (account.email) {
+ return account.email;
+ }
+ return String(account._account_id);
+}
+
+function computeName(account: AccountInfo): string | undefined {
+ if (account.email) {
+ return `${account.name} <${account.email}>`;
+ }
+ return account.name;
+}
+
+@customElement('rv-reviewer')
+export class RvReviewer extends LitElement {
+ /**
+ * Fired when the 'CANCEL' or 'DELETE' button for a reviewer was clicked.
+ *
+ * @event reviewer-deleted
+ */
/**
- * Defines properties of the component
+ * Fired when the 'ADD' button for a reviewer was clicked.
*
- * @returns {?}
+ * @event reviewer-added
*/
- static get properties() {
- return {
- canModifyConfig: Boolean,
- pluginRestAPi: Object,
- repoName: String,
- reviewer: String,
- type: String,
- _header: {
- type: String,
- computed: '_computeHeader(type)',
- },
- _reviewerSearchId: String,
- _queryReviewers: {
- type: Function,
- value() {
- return this._getReviewerSuggestions.bind(this);
- },
- },
- _originalReviewer: String,
- _deleted: Boolean,
- _editing: {
- type: Boolean,
- computed: '_computeEditing(reviewer, _originalReviewer)',
- },
- };
+
+ @property()
+ canModifyConfig = false;
+
+ @property()
+ pluginRestApi!: RestPluginApi;
+
+ @property()
+ repoName!: RepoName;
+
+ @property()
+ type = Type.REVIEWER;
+
+ /**
+ * This is the value that is persisted on the server side. For new reviewers
+ * this is empty until the user clicks "ADD" and the data was saved.
+ */
+ @property()
+ reviewer = '';
+
+ /**
+ * This is value that the user has picked from the auto-completion. It will
+ * be used for saving (when the user clicks "ADD") and then assigned to the
+ * `reviewer` property.
+ */
+ @state()
+ selectedReviewer = '';
+
+ static override get styles() {
+ return [
+ css`
+ :host {
+ display: block;
+ padding: var(--spacing-s) 0;
+ }
+ #editReviewerInput {
+ display: block;
+ width: 250px;
+ }
+ .reviewerRow {
+ align-items: center;
+ display: flex;
+ }
+ #reviewerHeader,
+ #editReviewerInput,
+ #deleteCancelBtn,
+ #addBtn,
+ #reviewerField {
+ margin-left: var(--spacing-m);
+ }
+ #reviewerField {
+ width: 250px;
+ text-indent: 1px;
+ border: 1px solid var(--border-color);
+ }
+ `,
+ ];
}
- connectedCallback() {
- super.connectedCallback();
- this._originalReviewer = this.reviewer;
+ render() {
+ return html`
+ <div class="reviewerRow">
+ <span class="heading-3" id="reviewerHeader">
+ ${this.type === Type.CC ? 'CC' : 'Reviewer'}
+ </span>
+ ${this.isEditing()
+ ? this.renderAutocomplete()
+ : html`<td id="reviewerField">${this.reviewer}</td>`}
+ <gr-button
+ id="deleteCancelBtn"
+ @click="${this.handleDeleteCancel}"
+ ?hidden="${!this.canModifyConfig}"
+ >
+ ${this.isEditing() ? 'Cancel' : 'Delete'}
+ </gr-button>
+ <gr-button
+ id="addBtn"
+ @click="${this.handleAddReviewer}"
+ ?hidden="${!this.isEditing() || !this.selectedReviewer}"
+ >
+ Add
+ </gr-button>
+ </div>
+ `;
}
- _computeHeader(type) {
- if (type === 'CC') {
- return 'Cc';
- }
- return 'Reviewer';
+ renderAutocomplete() {
+ return html`
+ <span class="value">
+ <!--
+ TODO:
+ Investigate whether we could reuse gr-account-list.
+ If the REST API returns AccountInfo instead of an account
+ identifier String we should be able to use gr-account-list(size=1)
+ for all reviewers, including those who are non-editable
+ (#reviewerField below) and align the plugin with how accounts
+ are displayed in core Gerrit's UI.
+ -->
+ <gr-autocomplete
+ id="editReviewerInput"
+ .query="${(input: string) => this.getReviewerSuggestions(input)}"
+ .placeholder="Name Or Email"
+ @value-changed="${this.onReviewerSelected}"
+ >
+ </gr-autocomplete>
+ </span>
+ `;
}
- _computeEditing(reviewer, _originalReviewer) {
- if (_originalReviewer === '') {
- return true;
- }
- return reviewer === '';
+ onReviewerSelected(e: CustomEvent<{value: string}>) {
+ if (!e.detail.value) return;
+ this.selectedReviewer = e.detail.value;
}
- _computeDeleteCancel(reviewer, _originalReviewer) {
- return this._computeEditing(reviewer, _originalReviewer) ?
- 'Cancel' : 'Delete';
+ /**
+ * "Editing" actually just means "adding". This component does not allow
+ * editing. You can only add new entries or delete existing ones.
+ */
+ isEditing() {
+ return this.reviewer === '';
}
- _computeHideAddButton(reviewer, _originalReviewer) {
- return !(this._computeEditing(reviewer, _originalReviewer) &&
- this._reviewerSearchId);
+ getReviewerSuggestions(input: string): Promise<NameValue[]> {
+ if (input.length === 0) return Promise.resolve([]);
+ const p1 = this.getSuggestedGroups(input);
+ const p2 = this.getSuggestedAccounts(input);
+ return Promise.all([p1, p2]).then(result => result.flat());
}
- _computeHideDeleteButton(canModifyConfig) {
- return !canModifyConfig;
- }
-
- _getReviewerSuggestions(input) {
- if (input.length === 0) { return Promise.resolve([]); }
- const promises = [];
- promises.push(this._getSuggestedGroups(input));
- promises.push(this._getSuggestedAccounts(input));
- return Promise.all(promises).then(result => {
- return result.flat();
- });
- }
-
- _getSuggestedGroups(input) {
+ getSuggestedGroups(input: string): Promise<NameValue[]> {
const suggestUrl = `/groups/?suggest=${input}&p=${this.repoName}`;
- return this.pluginRestApi.get(suggestUrl).then(groups => {
- if (!groups) { return []; }
- const groupSuggestions = [];
- for (const key in groups) {
- if (!groups.hasOwnProperty(key) || key.startsWith('user/')) {
- continue;
- }
- groupSuggestions.push({
- name: key,
- value: key,
+ return this.pluginRestApi.get<GroupNameToInfo>(suggestUrl).then(groups => {
+ if (!groups) return [];
+ return Object.keys(groups)
+ .filter(name => !name.startsWith('user/'))
+ .map(name => {
+ return {name, value: name};
});
- }
- return groupSuggestions;
});
}
- _getSuggestedAccounts(input) {
+ getSuggestedAccounts(input: string): Promise<NameValue[]> {
const suggestUrl = `/accounts/?suggest&q=${input}`;
- return this.pluginRestApi.get(suggestUrl).then(accounts => {
- const accountSuggestions = [];
- let nameAndEmail;
- let value;
- if (!accounts) { return []; }
- for (const key in accounts) {
- if (!accounts.hasOwnProperty(key)) { continue; }
- if (accounts[key].email) {
- nameAndEmail = accounts[key].name +
- ' <' + accounts[key].email + '>';
- } else {
- nameAndEmail = accounts[key].name;
- }
- if (accounts[key].username) {
- value = accounts[key].username;
- } else if (accounts[key].email) {
- value = accounts[key].email;
- } else {
- value = accounts[key]._account_id;
- }
- accountSuggestions.push({
- name: nameAndEmail,
- value,
- });
+ return this.pluginRestApi.get<AccountInfo[]>(suggestUrl).then(accounts => {
+ const accountSuggestions: NameValue[] = [];
+ if (!accounts) return [];
+ for (const account of accounts) {
+ const name = computeName(account);
+ const value = computeValue(account);
+ if (!name || !value) continue;
+ accountSuggestions.push({name, value});
}
return accountSuggestions;
});
}
- _handleDeleteCancel() {
- const detail = {editing: this._editing, type: this.type};
- if (this._editing) {
+ handleDeleteCancel() {
+ const detail: ReviewerDeletedEventDetail = {
+ editing: this.isEditing(),
+ type: this.type,
+ };
+ if (this.isEditing()) {
this.remove();
}
- this.dispatchEvent(
- new CustomEvent('reviewer-deleted', {detail, bubbles: true}));
+ fire(this, 'reviewer-deleted', detail);
}
- _handleAddReviewer() {
- const detail = {reviewer: this._reviewerSearchId, type: this.type};
- this._originalReviewer = this.reviewer;
- this.dispatchEvent(
- new CustomEvent('reviewer-added', {detail, bubbles: true}));
+ handleAddReviewer() {
+ const detail: ReviewerAddedEventDetail = {
+ reviewer: this.selectedReviewer,
+ type: this.type,
+ };
+ this.reviewer = this.selectedReviewer;
+ this.selectedReviewer = '';
+ fire(this, 'reviewer-added', detail);
}
}
-
-customElements.define(RvReviewer.is, RvReviewer);
\ No newline at end of file
diff --git a/rv-reviewers/rv-reviewer_html.ts b/rv-reviewers/rv-reviewer_html.ts
deleted file mode 100644
index 29b22de..0000000
--- a/rv-reviewers/rv-reviewer_html.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * @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.
- */
-export const htmlTemplate = Polymer.html`
- <style include="shared-styles">
- :host {
- display: block;
- padding: var(--spacing-s) 0;
- }
- #editReviewerInput {
- display: block;
- width: 250px;
- }
- .reviewerRow {
- align-items: center;
- display: flex;
- }
- #reviewerHeader,
- #editReviewerInput,
- #deleteCancelBtn,
- #addBtn,
- #reviewerField {
- margin-left: var(--spacing-m);
- }
- #reviewerField {
- width: 250px;
- text-indent: 1px;
- border: 1px solid var(--border-color);
- }
- </style>
- <style include="gr-form-styles"></style>
- <div class="reviewerRow">
- <h4 id="reviewerHeader">[[_header]]:</h4>
- <template is="dom-if" if="[[_computeEditing(reviewer, _originalReviewer)]]">
- <span class="value">
- <!--
- TODO:
- Investigate whether we could reuse gr-account-list.
- If the REST API returns AccountInfo instead of an account
- identifier String we should be able to use gr-account-list(size=1)
- for all reviewers, including those who are non-editable
- (#reviewerField below) and align the plugin with how accounts
- are displayed in core Gerrit's UI.
- -->
- <gr-autocomplete
- id="editReviewerInput"
- text="{{reviewer}}"
- value="{{_reviewerSearchId}}"
- query="[[_queryReviewers]]"
- placeholder="Name Or Email">
- </gr-autocomplete>
- </span>
- </template>
- <template is="dom-if" if="[[!_computeEditing(reviewer, _originalReviewer)]]">
- <td id="reviewerField">[[reviewer]]</td>
- </template>
- <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 -->
-`;
\ No newline at end of file
diff --git a/rv-reviewers/rv-reviewers.ts b/rv-reviewers/rv-reviewers.ts
index 9782b8c..28f4096 100644
--- a/rv-reviewers/rv-reviewers.ts
+++ b/rv-reviewers/rv-reviewers.ts
@@ -14,87 +14,118 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {htmlTemplate} from './rv-reviewers_html.js';
+import {RepoName} from '@gerritcodereview/typescript-api/rest-api';
+import {RestPluginApi} from '@gerritcodereview/typescript-api/rest';
+import {PluginApi} from '@gerritcodereview/typescript-api/plugin';
+import {customElement, property, query, state} from 'lit/decorators';
+import {css, html, LitElement} from 'lit';
+import './rv-edit-screen';
-class RvReviewers extends Polymer.Element {
- /** @returns {string} name of the component */
- static get is() { return 'rv-reviewers'; }
+// TODO: This should be defined and exposed by @gerritcodereview/typescript-api
+type GrOverlay = Element & {
+ open(): void;
+ close(): void;
+ fit(): void;
+};
- /** @returns {?} template for this component */
- static get template() { return htmlTemplate; }
+declare global {
+ interface HTMLElementTagNameMap {
+ 'rv-reviewers': RvReviewers;
+ }
+}
- /**
- * Defines properties of the component
- *
- * @returns {?}
- */
- static get properties() {
- return {
- 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,
- },
- };
+declare interface AccountCapabilityInfo {
+ 'reviewers-modifyReviewersConfig'?: boolean;
+}
+
+type ProjectAccessInfoMap = {[projectName: string]: ProjectAccessInfo};
+
+declare interface ProjectAccessInfo {
+ is_owner?: boolean;
+}
+
+@customElement('rv-reviewers')
+export class RvReviewers extends LitElement {
+ @query('#rvScreenOverlay')
+ rvScreenOverlay?: GrOverlay;
+
+ /** Guaranteed to be set by the `repo-command` endpoint. */
+ @property({type: Object})
+ plugin!: PluginApi;
+
+ /** Guaranteed to be set by the `repo-command` endpoint. */
+ @property({type: String})
+ repoName!: RepoName;
+
+ @state()
+ pluginRestApi!: RestPluginApi;
+
+ @state()
+ canModifyConfig = false;
+
+ @state()
+ loading = true;
+
+ static override get styles() {
+ return [
+ css`
+ :host {
+ display: block;
+ margin-bottom: var(--spacing-xxl);
+ }
+ #rvScreenOverlay {
+ width: 50em;
+ overflow: auto;
+ }
+ `,
+ ];
+ }
+
+ render() {
+ return html`
+ <h3 class="heading-3">Reviewers Config</h3>
+ <gr-button @click="${() => this.rvScreenOverlay?.open()}">
+ Reviewers Config
+ </gr-button>
+ <gr-overlay id="rvScreenOverlay" with-backdrop>
+ <rv-edit-screen
+ .pluginRestApi="${this.pluginRestApi}"
+ .repoName="${this.repoName}"
+ .loading="${this.loading}"
+ .canModifyConfig="${this.canModifyConfig}"
+ @close="${() => this.rvScreenOverlay?.close()}"
+ @fit="${() => this.rvScreenOverlay?.fit()}"
+ >
+ </rv-edit-screen>
+ </gr-overlay>
+ `;
}
connectedCallback() {
super.connectedCallback();
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;
+ const p1 = this.getRepoAccess(this.repoName).then(access => {
+ if (access[this.repoName]?.is_owner) {
+ this.canModifyConfig = true;
}
- }));
- promises.push(this._getCapabilities().then(capabilities => {
- if (capabilities['reviewers-modifyReviewersConfig']) {
- this._hasModifyCapability = true;
- }
- }));
- Promise.all(promises).then(() => {
- this._loading = false;
});
+ const p2 = this.getCapabilities().then(capabilities => {
+ if (capabilities['reviewers-modifyReviewersConfig']) {
+ this.canModifyConfig = true;
+ }
+ });
+ Promise.all([p1, p2]).then(() => (this.loading = false));
}
- _computeCanModifyConfig(isOwner, hasModifyCapability) {
- return isOwner || hasModifyCapability;
+ getRepoAccess(repoName: RepoName) {
+ return this.pluginRestApi.get<ProjectAccessInfoMap>(
+ '/access/?project=' + encodeURIComponent(repoName)
+ );
}
- _getRepoAccess(repoName) {
- return this.pluginRestApi.get(
- '/access/?project=' + encodeURIComponent(repoName));
- }
-
- _getCapabilities() {
- return this.pluginRestApi.get('/accounts/self/capabilities');
+ getCapabilities() {
+ return this.pluginRestApi.get<AccountCapabilityInfo>(
+ '/accounts/self/capabilities'
+ );
}
}
-
-customElements.define(RvReviewers.is, RvReviewers);
-
diff --git a/rv-reviewers/rv-reviewers_html.ts b/rv-reviewers/rv-reviewers_html.ts
deleted file mode 100644
index 53a8aec..0000000
--- a/rv-reviewers/rv-reviewers_html.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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.
- */
-import './rv-edit-screen.js';
-
-export const htmlTemplate = Polymer.html`
- <style include="shared-styles">
- :host {
- display: block;
- margin-bottom: var(--spacing-xxl);
- }
- #rvScreenOverlay {
- width: 50em;
- overflow: auto;
- }
- </style>
- <h3>Reviewers Config</h3>
- <gr-button
- on-click="_handleCommandTap"
- >
- Reviewers Config
- </gr-button>
- <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>
-`;
diff --git a/rv-reviewers/util.ts b/rv-reviewers/util.ts
new file mode 100644
index 0000000..c2559c5
--- /dev/null
+++ b/rv-reviewers/util.ts
@@ -0,0 +1,26 @@
+/**
+ * @license
+ * Copyright (C) 2021 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.
+ */
+
+export function fire<T>(target: EventTarget, type: string, detail?: T) {
+ target.dispatchEvent(
+ new CustomEvent<T>(type, {
+ detail,
+ composed: true,
+ bubbles: true,
+ })
+ );
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..f0a5ff3
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../tsconfig-plugins-base.json",
+ "include": [
+ "rv-reviewers/**/*",
+ ]
+}