Convert from Polymer to Lit Change-Id: I4486450eef72d97edb9c9bb265ed9f41b2ca688f
diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index e69de29..0000000 --- a/.eslintignore +++ /dev/null
diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 6d9ae7c..0000000 --- a/.eslintrc.json +++ /dev/null
@@ -1,166 +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/BUILD b/BUILD index 3c5aaa8..4070f5a 100644 --- a/BUILD +++ b/BUILD
@@ -1,15 +1,11 @@ load("@rules_java//java:defs.bzl", "java_library") -load("@npm//@bazel/rollup:index.bzl", "rollup_bundle") load("//tools/bzl:junit.bzl", "junit_tests") -load("//tools/js:eslint.bzl", "eslint") load( "//tools/bzl:plugin.bzl", "PLUGIN_DEPS", "PLUGIN_TEST_DEPS", "gerrit_plugin", ) -load("//tools/bzl:genrule2.bzl", "genrule2") -load("//tools/bzl:js.bzl", "polygerrit_plugin") gerrit_plugin( name = "zuul", @@ -21,7 +17,7 @@ "Gerrit-Module: com.googlesource.gerrit.plugins.zuul.Module", "Gerrit-HttpModule: com.googlesource.gerrit.plugins.zuul.HttpModule", ], - resource_jars = [":gr-zuul-static"], + resource_jars = ["//plugins/zuul/web:zuul"], ) junit_tests( @@ -43,55 +39,3 @@ "@commons-lang3//jar", ], ) - -genrule2( - name = "gr-zuul-static", - srcs = [":gr-zuul"], - outs = ["gr-zuul-static.jar"], - cmd = " && ".join([ - "mkdir $$TMP/static", - "cp $(locations :gr-zuul) $$TMP/static", - "cd $$TMP", - "zip -Drq $$ROOT/$@ -g .", - ]), -) - -polygerrit_plugin( - name = "gr-zuul", - app = "zuul-bundle.js", - plugin_name = "zuul", -) - -rollup_bundle( - name = "zuul-bundle", - srcs = glob(["gr-zuul/*.js"]), - entry_point = "gr-zuul/plugin.js", - rollup_bin = "//tools/node_tools:rollup-bin", - sourcemap = "hidden", - format = "iife", - deps = [ - "@tools_npm//rollup-plugin-node-resolve", - ], -) - -# Define the eslinter for the plugin -# The eslint macro creates 2 rules: lint_test and lint_bin -eslint( - name = "lint", - srcs = glob([ - "gr-zuul/**/*.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", - ], -) -
diff --git a/gr-zuul/gr-zuul.js b/gr-zuul/gr-zuul.js deleted file mode 100644 index b6893ce..0000000 --- a/gr-zuul/gr-zuul.js +++ /dev/null
@@ -1,119 +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 {htmlTemplate} from './gr-zuul_html.js'; - -class GrZuul extends Polymer.Element { - /** @returns {string} name of the component */ - static get is() { return 'gr-zuul'; } - - /** @returns {?} template for this component */ - static get template() { return htmlTemplate; } - - static get properties() { - return { - change: { - type: Object, - observer: '_onChangeChanged', - }, - hidden: { - type: Boolean, - value: true, - reflectToAttribute: true, - }, - _crd: { - type: Object, - value: {}, - }, - _crd_loaded: { - type: Boolean, - value: false, - }, - }; - } - - _onChangeChanged() { - this._crd_loaded = false; - this.setHidden(true); - const url = '/changes/' + this.change.id + '/revisions/current/crd'; - return this.plugin.restApi().send('GET', url).then(crd => { - this._crd = crd; - this._crd_loaded = true; - this.setHidden(!(this._isDependsOnSectionVisible() - || crd.needed_by.length)); - }); - } - - // copied from gr-related-changes-list.js, which is inaccessible from here. - // Resolved uses of `this.ChangeStatus.[...]`, as that's inaccessible from here too. - // Removed _isIndirectAncestor check, as the needed data is inaccessible from here. - // Not all code paths are reachable, as we only have shallow ChangeInfo objects. We leave the - // code here nonetheless, to allow for easier updating from gr-related-changes-list.js. - _computeChangeStatusClass(change) { - const classes = ['status']; - if (change._revision_number != change._current_revision_number) { - classes.push('notCurrent'); - } else if (change.submittable) { - classes.push('submittable'); - } else if (change.status == 'NEW') { - classes.push('hidden'); - } - return classes.join(' '); - } - - // copied from gr-related-changes-list.js, which is inaccessible from here. - // Resolved uses of `this.ChangeStatus.[...]`, as that's inaccessible from here too. - // Removed _isIndirectAncestor check, as the needed data is inaccessible from here. - // Not all code paths are reachable, as we only have shallow ChangeInfo objects. We leave the - // code here nonetheless, to allow for easier updating from gr-related-changes-list.js. - _computeChangeStatus(change) { - switch (change.status) { - case 'MERGED': - return 'Merged'; - case 'ABANDONED': - return 'Abandoned'; - } - if (change._revision_number != change._current_revision_number) { - return 'Not current'; - } else if (change.submittable) { - return 'Submittable'; - } - return ''; - } - - setHidden(hidden) { - if (this.hidden != hidden) { - this.hidden = hidden; - - // Flag to parents that something changed - this.dispatchEvent(new CustomEvent('new-section-loaded', { - composed: true, bubbles: true, - })); - } - } - - _computeDependencyUrl(changeInfo) { - return `${window.CANONICAL_PATH || ''}/q/${changeInfo.change_id}`; - } - - _isDependsOnSectionVisible() { - return !!(this._crd.depends_on_found.length - + this._crd.depends_on_missing.length); - } -} - -customElements.define(GrZuul.is, GrZuul);
diff --git a/gr-zuul/gr-zuul_html.js b/gr-zuul/gr-zuul_html.js deleted file mode 100644 index 5e208d4..0000000 --- a/gr-zuul/gr-zuul_html.js +++ /dev/null
@@ -1,129 +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. - */ - -export const htmlTemplate = Polymer.html` - <style include="shared-styles"> - section.related-changes-section { - margin-bottom: 1.4em; /* Same as line height for collapse purposes */ - display: block; - } - div.foo { - margin-bottom: 1.4em; /* Same as line height for collapse purposes */ - } - a { - display: block; - } - .changeContainer, - a { - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .changeContainer { - display: flex; - } - .changeContainer.thisChange:before { - content: '➔'; - width: 1.2em; - } - h4, - section div { - display: flex; - } - h4:before, - section div:before { - content: ' '; - flex-shrink: 0; - width: 1.2em; - } - .status { - color: var(--deemphasized-text-color); - font-weight: var(--font-weight-bold); - margin-left: var(--spacing-xs); - } - /* The above styles are copy/paste from gr-related-changes-list_html.js */ - .dependencyCycleDetected { - color: #d17171; - } - .missingFromThisServer { - color: #d17171; - } - .hidden { - display: none; - } - </style> - <template is="dom-if" if="[[_crd_loaded]]"> - <template is="dom-if" if="[[_isDependsOnSectionVisible()]]"> - <section class="related-changes-section"> - <h4>Depends on</h4> - <template is="dom-repeat" items="[[_crd.depends_on_found]]"> - <div class="changeContainer zuulDependencyContainer"> - <a - href$="[[_computeDependencyUrl(item)]]" - title$="[[item.project]]: [[item.branch]]: [[item.subject]]" - > - [[item.project]]: [[item.branch]]: [[item.subject]] - </a> - <span class$="[[_computeChangeStatusClass(item)]]"> - ([[_computeChangeStatus(item)]]) - </span> - <template is="dom-if" if="[[_crd.cycle]]"> - <span class="status dependencyCycleDetected"> - (Dependency cycle detected) - </span> - </template> - </div> - </template> - <template is="dom-repeat" items="[[_crd.depends_on_missing]]"> - <div class="changeContainer zuulDependencyContainer"> - <span> - [[item]] - </span> - <span class="status missingFromThisServer"> - (Missing from this server) - </span> - </div> - </template> - </section> - </template> - <template is="dom-if" if="[[_crd.needed_by.length]]"> - <section class="related-changes-section"> - <h4>Needed by</h4> - <template is="dom-repeat" items="[[_crd.needed_by]]"> - <div class="changeContainer zuulDependencyContainer"> - <a - href$="[[_computeDependencyUrl(item)]]" - title$="[[item.project]]: [[item.branch]]: [[item.subject]]" - > - [[item.project]]: [[item.branch]]: [[item.subject]] - </a> - <span class$="[[_computeChangeStatusClass(item)]]"> - ([[_computeChangeStatus(item)]]) - </span> - <template is="dom-if" if="[[_crd.cycle]]"> - <span class="status dependencyCycleDetected"> - (Dependency cycle detected) - </span> - </template> - </div> - </template> - </section> - </template> - </template> -`; -
diff --git a/web/BUILD b/web/BUILD new file mode 100644 index 0000000..991390e --- /dev/null +++ b/web/BUILD
@@ -0,0 +1,42 @@ +load("//tools/js:eslint.bzl", "plugin_eslint") +load("//tools/bzl:js.bzl", "gerrit_js_bundle") +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") + +package_group( + name = "visibility", + packages = ["//plugins/zuul/..."], +) + +package(default_visibility = [":visibility"]) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//plugins:tsconfig-plugins-base.json", + ], +) + +ts_project( + name = "zuul-ts", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*test*"], + ), + incremental = True, + out_dir = "_bazel_ts_out", + tsc = "//tools/node_tools:tsc-bin", + tsconfig = ":tsconfig", + deps = [ + "@plugins_npm//@gerritcodereview/typescript-api", + "@plugins_npm//lit", + ], +) + +gerrit_js_bundle( + name = "zuul", + srcs = [":zuul-ts"], + entry_point = "_bazel_ts_out/plugin.js", +) + +plugin_eslint()
diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 0000000..54028d5 --- /dev/null +++ b/web/eslint.config.js
@@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2025 The Android Open Source Project + * SPDX-License-Identifier: Apache-2.0 + */ + +const {defineConfig} = require('eslint/config'); + +// eslint-disable-next-line no-undef +__plugindir = 'zuul/web'; + +const gerritEslint = require('../../eslint.config.js'); + +module.exports = defineConfig([ + { + extends: [gerritEslint], + }, +]);
diff --git a/web/gr-zuul.ts b/web/gr-zuul.ts new file mode 100644 index 0000000..9d28ab2 --- /dev/null +++ b/web/gr-zuul.ts
@@ -0,0 +1,250 @@ +/** + * @license + * Copyright (C) 2025 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 {ChangeInfo, NumericChangeId} from '@gerritcodereview/typescript-api/rest-api'; +import {LitElement, html, css, nothing} from 'lit'; +import {customElement, property, state} from 'lit/decorators.js'; + +declare global { + interface HTMLElementTagNameMap { + 'gr-zuul': GrZuul; + } +} + +interface CrdInfo { + depends_on_found?: ChangeInfo[]; + depends_on_missing?: string[]; + needed_by?: ChangeInfo[]; + cycle?: boolean; +} + +// Partial copy of https://github.com/GerritCodeReview/gerrit/blob/b42341c5cd9b1f1535df30b16f180a90617fd067/polygerrit-ui/app/types/common.ts#L1377 +interface RelatedChangeAndCommitInfo { + _change_number?: NumericChangeId; + _revision_number?: number; + _current_revision_number?: number; + status?: string; + submittable?: boolean; +} + + +@customElement('gr-zuul') +export class GrZuul extends LitElement { + @property({type: Object}) change?: ChangeInfo; + + @state() private _crd: CrdInfo = {}; + @state() private _crdLoaded = false; + + static override get styles() { + return [ + css` + section.related-changes-section { + margin-bottom: 1.4em; + display: block; + } + a { + display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .changeContainer { + display: flex; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .changeContainer.thisChange:before { + content: '➔'; + width: 1.2em; + } + h4, + section div { + display: flex; + } + h4:before, + section div:before { + content: ' '; + flex-shrink: 0; + width: 1.2em; + } + .status { + color: var(--deemphasized-text-color); + font-weight: var(--font-weight-bold); + margin-left: var(--spacing-xs); + } + .dependencyCycleDetected, + .missingFromThisServer { + color: #d17171; + } + .hidden { + display: none; + } + `, + ]; + } + + override render() { + if (!this._crdLoaded) return nothing; + + return html` + ${this._isDependsOnSectionVisible() + ? html` + <section class="related-changes-section"> + <h4>Depends on</h4> + ${this._crd.depends_on_found?.map( + item => html` + <div class="changeContainer zuulDependencyContainer"> + <a + href=${this._computeDependencyUrl(item)} + title="${item.project}: ${item.branch}: ${item.subject}" + > + ${item.project}: ${item.branch}: ${item.subject} + </a> + <span class=${this._computeChangeStatusClass(item)}> + (${this._computeChangeStatus(item)}) + </span> + ${this._crd.cycle + ? html` + <span class="status dependencyCycleDetected"> + (Dependency cycle detected) + </span> + ` + : nothing} + </div> + ` + )} + ${this._crd.depends_on_missing?.map( + item => html` + <div class="changeContainer zuulDependencyContainer"> + <span>${item}</span> + <span class="status missingFromThisServer"> + (Missing from this server) + </span> + </div> + ` + )} + </section> + ` + : nothing} + + ${this._crd.needed_by?.length + ? html` + <section class="related-changes-section"> + <h4>Needed by</h4> + ${this._crd.needed_by.map( + item => html` + <div class="changeContainer zuulDependencyContainer"> + <a + href=${this._computeDependencyUrl(item)} + title="${item.project}: ${item.branch}: ${item.subject}" + > + ${item.project}: ${item.branch}: ${item.subject} + </a> + <span class=${this._computeChangeStatusClass(item)}> + (${this._computeChangeStatus(item)}) + </span> + ${this._crd.cycle + ? html` + <span class="status dependencyCycleDetected"> + (Dependency cycle detected) + </span> + ` + : nothing} + </div> + ` + )} + </section> + ` + : nothing} + `; + } + + override updated(changedProperties: Map<string, unknown>) { + if (changedProperties.has('change')) { + void this._onChangeChanged(); + } + } + + private async _onChangeChanged(): Promise<void> { + this._crdLoaded = false; + this._setHidden(true); + if (!this.change?.id) return; + + const url = `/changes/${this.change.id}/revisions/current/crd`; + const plugin = (this as any).plugin; + + const crd: CrdInfo = await plugin.restApi().send('GET', url); + this._crd = crd; + this._crdLoaded = true; + + const visible = this._isDependsOnSectionVisible() || (crd.needed_by?.length ?? 0) > 0; + this._setHidden(!visible); + } + + private _setHidden(hidden: boolean): void { + if (this.hidden !== hidden) { + this.hidden = hidden; + this.dispatchEvent( + new CustomEvent('new-section-loaded', { + composed: true, + bubbles: true, + }) + ); + } + } + + private _computeChangeStatusClass(change: RelatedChangeAndCommitInfo): string { + const classes = ['status']; + if (change._revision_number !== change._current_revision_number) { + classes.push('notCurrent'); + } else if (change.submittable) { + classes.push('submittable'); + } else if (change.status === 'NEW') { + classes.push('hidden'); + } + return classes.join(' '); + } + + private _computeChangeStatus(change: RelatedChangeAndCommitInfo): string { + switch (change.status) { + case 'MERGED': + return 'Merged'; + case 'ABANDONED': + return 'Abandoned'; + default: + if (change._revision_number !== change._current_revision_number) { + return 'Not current'; + } else if (change.submittable) { + return 'Submittable'; + } + return ''; + } + } + + private _computeDependencyUrl(changeInfo: ChangeInfo): string { + const base = (window as any).CANONICAL_PATH || ''; + return `${base}/q/${changeInfo.change_id}`; + } + + private _isDependsOnSectionVisible(): boolean { + const {depends_on_found, depends_on_missing} = this._crd; + return (depends_on_found?.length ?? 0) + (depends_on_missing?.length ?? 0) > 0; + } +}
diff --git a/gr-zuul/plugin.js b/web/plugin.ts similarity index 80% rename from gr-zuul/plugin.js rename to web/plugin.ts index 475f053..d75cd7d 100644 --- a/gr-zuul/plugin.js +++ b/web/plugin.ts
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2025 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. @@ -14,9 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import './gr-zuul.js'; -Gerrit.install(plugin => { +import '@gerritcodereview/typescript-api/gerrit'; +import './gr-zuul'; + +window.Gerrit?.install(plugin => { plugin.registerCustomComponent( 'related-changes-section', 'gr-zuul', {slot: 'bottom'}); });
diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..78f5cc7 --- /dev/null +++ b/web/tsconfig.json
@@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig-plugins-base.json", + "compilerOptions": { + "outDir": "../../../.ts-out/plugins/zuul" /* overridden by bazel */ + }, + "include": [ + "**/*.ts" + ] +}