Merge branch 'stable-3.2' into stable-3.3

* stable-3.2: (27 commits)
  Use shallow clone while expanding definitions
  Move match() and matchOrNull() to MatchCache
  fixup! Cache Task definition lists for ChangeNodes
  Cache Task definition lists for ChangeNodes
  Split SubNodeFactory out of SubNodeAdder
  Moving caching logic out of loadSubNodes()
  Use a TaskTree.Node.Invalid instead of nulls
  Use overloading for special cases instead of nulls
  Add a TaskTree ApplicableNodeFilter
  Add an isCacheableByBranch() to the PredicateCache
  Track whether Task.applicable needs to be refreshed
  Allow Expander to expand a single Task's field
  Minor cleanup of task Properties.expandText()
  Remove cached Change subNodes on Node completion
  Explicitly signal end of Task Properties expansion
  Use a SubNodeAdder to add TaskTree SubNodes
  Avoid hard coding the refs/meta/config ref
  Inject All-Projects in TaskConfigFactory
  Change TaskExpressions to iterate over TaskKeys
  Avoid passing isTrusted in TaskConfigFactory.getTaskConfig()
  ...

Change-Id: I8551b1ba75c9f0ad35b211325f5dec7788d3005d
diff --git a/BUILD b/BUILD
index e53588b..06297ac 100644
--- a/BUILD
+++ b/BUILD
@@ -4,8 +4,7 @@
     "PLUGIN_TEST_DEPS",
     "gerrit_plugin",
 )
-load("//tools/bzl:genrule2.bzl", "genrule2")
-load("//tools/bzl:js.bzl", "polygerrit_plugin")
+load("//tools/bzl:js.bzl", "gerrit_js_bundle")
 load("//tools/js:eslint.bzl", "eslint")
 load("//tools/bzl:junit.bzl", "junit_tests")
 load("@rules_java//java:defs.bzl", "java_library", "java_plugin")
@@ -39,31 +38,16 @@
         "Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/" + plugin_name,
         "Gerrit-Module: com.googlesource.gerrit.plugins.task.Modules$Module",
     ],
-    resource_jars = [":gr-task-plugin-static"],
+    resource_jars = [":gr-task-plugin"],
     resources = glob(["src/main/resources/**/*"]),
     deps = [":auto-value"],
     javacopts = [ "-Werror", "-Xlint:all", "-Xlint:-classfile", "-Xlint:-processing"],
 )
 
-genrule2(
-    name = "gr-task-plugin-static",
-    srcs = [":gr-task-plugin"],
-    outs = ["gr-task-plugin-static.jar"],
-    cmd = " && ".join([
-        "mkdir $$TMP/static",
-        "cp -r $(locations :gr-task-plugin) $$TMP/static",
-        "cd $$TMP",
-        "zip -Drq $$ROOT/$@ -g .",
-    ]),
-)
-
-polygerrit_plugin(
+gerrit_js_bundle(
     name = "gr-task-plugin",
-    srcs = glob([
-        "gr-task-plugin/*.html",
-        "gr-task-plugin/*.js",
-    ]),
-    app = "plugin.html",
+    srcs = glob(["gr-task-plugin/*.js"]),
+    entry_point = "gr-task-plugin/plugin.js",
 )
 
 junit_tests(
diff --git a/WORKSPACE b/WORKSPACE
index 4951e8d..10af712 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -8,7 +8,7 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "1dd03e38f46e56defc91b6ab4a4d9879f6083de1",
+    commit = "b4324e30289c2f6a2a07a4f0a9df6b1fce85ef1a",
     #local_path = "/home/<user>/projects/bazlets",
 )
 
@@ -29,26 +29,6 @@
     yarn_lock = "//:yarn.lock",
 )
 
-# Load closure compiler with transitive dependencies
-load("@io_bazel_rules_closure//closure:repositories.bzl", "rules_closure_dependencies", "rules_closure_toolchains")
-
-rules_closure_dependencies()
-
-rules_closure_toolchains()
-
-# Load Gerrit npm_binary toolchain
-load("@com_googlesource_gerrit_bazlets//tools:js.bzl", "GERRIT", "npm_binary")
-
-npm_binary(
-    name = "polymer-bundler",
-    repository = GERRIT,
-)
-
-npm_binary(
-    name = "crisper",
-    repository = GERRIT,
-)
-
 load("//tools/bzl:maven_jar.bzl", "maven_jar")
 
 AUTO_VALUE_VERSION = "1.7.4"
diff --git a/gr-task-plugin/gr-task-plugin-tasks.js b/gr-task-plugin/gr-task-plugin-tasks.js
new file mode 100644
index 0000000..54585d1
--- /dev/null
+++ b/gr-task-plugin/gr-task-plugin-tasks.js
@@ -0,0 +1,49 @@
+/**
+ * @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.
+ */
+
+import {htmlTemplate} from './gr-task-plugin-tasks_html.js';
+
+class GrTaskPluginTasks extends Polymer.Element {
+  static get is() {
+    return 'gr-task-plugin-tasks';
+  }
+
+  static get template() {
+    return htmlTemplate;
+  }
+
+  static get properties() {
+    return {
+      tasks: {
+        type: Array,
+        notify: true,
+        value() { return []; },
+      },
+
+      show_all: {
+        type: String,
+        notify: true,
+      },
+    };
+  }
+
+  _can_show(show, task) {
+    return show === 'true' || task.showOnFilter;
+  }
+}
+
+customElements.define(GrTaskPluginTasks.is, GrTaskPluginTasks);
diff --git a/gr-task-plugin/gr-task-plugin-tasks_html.js b/gr-task-plugin/gr-task-plugin-tasks_html.js
new file mode 100644
index 0000000..df1644f
--- /dev/null
+++ b/gr-task-plugin/gr-task-plugin-tasks_html.js
@@ -0,0 +1,58 @@
+/**
+ * @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 const htmlTemplate = Polymer.html`
+<template is="dom-repeat" as="task" items="[[tasks]]">
+  <template is="dom-if" if="[[_can_show(show_all, task)]]">
+    <li>
+      <style>
+        /* Matching colors with core code. */
+        .green {
+          color: #9fcc6b;
+        }
+        .red {
+          color: #FFA62F;
+        }
+      </style>
+      <template is="dom-if" if="[[task.icon.id]]">
+        <gr-tooltip-content
+            has-tooltip
+            title="In Progress">
+            <iron-icon
+              icon="gr-icons:hourglass"
+              class="green"
+              hidden$="[[!task.in_progress]]">
+            </iron-icon>
+        </gr-tooltip-content>
+        <gr-tooltip-content
+            has-tooltip
+            title$="[[task.icon.tooltip]]">
+            <iron-icon
+              icon="[[task.icon.id]]"
+              class$="[[task.icon.color]]">
+            </iron-icon>
+        </gr-tooltip-content>
+      </template>
+      [[task.message]]
+    </li>
+  </template>
+  <ul style="list-style-type:none; margin: 0 0 0 0; padding: 0 0 0 2em;">
+    <gr-task-plugin-tasks
+        tasks="[[task.sub_tasks]]"
+        show_all$="[[show_all]]"> </gr-task-plugin-tasks>
+  </ul>
+</template>`;
diff --git a/gr-task-plugin/gr-task-plugin.html b/gr-task-plugin/gr-task-plugin.html
deleted file mode 100644
index c6c3746..0000000
--- a/gr-task-plugin/gr-task-plugin.html
+++ /dev/null
@@ -1,146 +0,0 @@
-<!--
-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="gr-task-plugin">
-  <template>
-      <style>
-        ul {
-          padding-left: 0.5em;
-          margin-top: 0;
-        }
-        h3 { padding-left: 0.1em; }
-        .cursor { cursor: pointer; }
-        #tasks_header {
-          align-items: center;
-          background-color: #fafafa;
-          border-top: 1px solid #ddd;
-          display: flex;
-          padding: 6px 1rem;
-        }
-        .links {
-          color: blue;
-          cursor: pointer;
-          text-decoration: underline;
-        }
-        .no-margins { margin: 0 0 0 0; }
-      </style>
-
-      <div id="tasks" hidden$="[[!_tasks.length]]">
-        <div id="tasks_header" style="display: flex;">
-          <iron-icon
-              icon="gr-icons:expand-less"
-              hidden$="[[!_expand_all]]"
-              on-tap="_switch_expand"
-              class="cursor"> </iron-icon>
-          <iron-icon
-              icon="gr-icons:expand-more"
-              hidden$="[[_expand_all]]"
-              on-tap="_switch_expand"
-              class="cursor"> </iron-icon>
-          <div style="display: flex; align-items: center; column-gap: 1em;">
-          <h3 class="no-margins" on-tap="_switch_expand" class="cursor"> Tasks </h3>
-          <template is="dom-if" if="[[_is_show_all(_show_all)]]">
-            <p class="no-margins">All ([[_all_count]]) |&nbsp;
-              <span
-                  on-click="_needs_and_blocked_tap"
-                  class="links">Needs ([[_ready_count]]) + Blocked ([[_fail_count]])</span>
-            <p>
-          </template>
-          <template is="dom-if" if="[[!_is_show_all(_show_all)]]">
-            <p class="no-margins"> <span
-                  class="links"
-                  on-click="_show_all_tap">All ([[_all_count]])</span>
-              &nbsp;| Needs ([[_ready_count]]) + Blocked ([[_fail_count]])</p>
-          </template>
-        </div>
-        </div>
-        <div hidden$="[[!_expand_all]]">
-          <ul style="list-style-type:none;">
-            <gr-task-plugin-tasks
-                tasks="[[_tasks]]"
-                show_all$="[[_show_all]]"> </gr-task-plugin-tasks>
-          </ul>
-        </div>
-      </div>
-  </template>
-  <script src="gr-task-plugin.js"></script>
-</dom-module>
-
-<dom-module id="gr-task-plugin-tasks">
-  <template>
-    <template is="dom-repeat" as="task" items="[[tasks]]">
-      <template is="dom-if" if="[[_can_show(show_all, task)]]">
-        <li style="padding: 0.2em;">
-          <style>
-            /* Matching colors with core code. */
-            .green {
-              color: #9fcc6b;
-            }
-            .red {
-              color: #FFA62F;
-            }
-          </style>
-          <template is="dom-if" if="[[task.icon.id]]">
-            <gr-tooltip-content
-                has-tooltip
-                title="In Progress">
-                <iron-icon
-                  icon="gr-icons:hourglass"
-                  class="green"
-                  hidden$="[[!task.in_progress]]">
-                </iron-icon>
-            </gr-tooltip-content>
-            <gr-tooltip-content
-                has-tooltip
-                title$="[[task.icon.tooltip]]">
-                <iron-icon
-                  icon="[[task.icon.id]]"
-                  class$="[[task.icon.color]]">
-                </iron-icon>
-            </gr-tooltip-content>
-          </template>
-          [[task.message]]
-        </li>
-      </template>
-      <ul style="list-style-type:none; margin: 0 0 0 0; padding: 0 0 0 2em;">
-      <gr-task-plugin-tasks
-          tasks="[[task.sub_tasks]]"
-          show_all$="[[show_all]]"> </gr-task-plugin-tasks>
-      </ul>
-    </template>
-  </template>
-  <script>
-      Polymer({
-        is: 'gr-task-plugin-tasks',
-        properties: {
-          tasks: {
-            type: Array,
-            notify: true,
-            value() { return []; },
-          },
-
-          show_all: {
-            type: String,
-            notify: true,
-          },
-        },
-
-        _can_show(show, task) {
-          return show === 'true' || task.showOnFilter;
-        },
-      });
-  </script>
-</dom-module>
diff --git a/gr-task-plugin/gr-task-plugin.js b/gr-task-plugin/gr-task-plugin.js
index e6d1ed0..7e1add6 100644
--- a/gr-task-plugin/gr-task-plugin.js
+++ b/gr-task-plugin/gr-task-plugin.js
@@ -1,6 +1,6 @@
 /**
  * @license
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,24 +14,34 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
 
-  const Defs = {};
-  /**
-   * @typedef {{
-   *  message: string,
-   *  sub_tasks: Array<Defs.Task>,
-   *  hint: ?string,
-   *  name: string,
-   *  status: string
-   * }}
-   */
-  Defs.Task;
+import './gr-task-plugin-tasks.js';
 
-  Polymer({
-    is: 'gr-task-plugin',
-    properties: {
+import {htmlTemplate} from './gr-task-plugin_html.js';
+
+const Defs = {};
+/**
+ * @typedef {{
+ *  message: string,
+ *  sub_tasks: Array<Defs.Task>,
+ *  hint: ?string,
+ *  name: string,
+ *  status: string
+ * }}
+ */
+Defs.Task;
+
+class GrTaskPlugin extends Polymer.Element {
+  static get is() {
+    return 'gr-task-plugin';
+  }
+
+  static get template() {
+    return htmlTemplate;
+  }
+
+  static get properties() {
+    return {
       change: {
         type: Object,
       },
@@ -72,124 +82,132 @@
         notify: true,
         value: 0,
       },
-    },
+    };
+  }
 
-    _is_show_all(show_all) {
-      return show_all === 'true';
-    },
+  _is_show_all(show_all) {
+    return show_all === 'true';
+  }
 
-    attached() {
-      this._getTasks();
-    },
+  connectedCallback() {
+    super.connectedCallback();
 
-    _getTasks() {
-      const endpoint =
-          `/changes/?q=change:${this.change._number}&--task--applicable`;
+    this._getTasks();
+  }
 
-      return this.plugin.restApi().get(endpoint).then(response => {
-        if (response && response.length === 1) {
-          const cinfo = response[0];
-          if (cinfo.plugins) {
-            const taskPluginInfo = cinfo.plugins.find(
-                pluginInfo => pluginInfo.name === 'task');
+  _getTasks() {
+    if (!this.change) {
+      return;
+    }
 
-            if (taskPluginInfo) {
-              this._tasks = this._addTasks(taskPluginInfo.roots);
-            }
+    const endpoint =
+        `/changes/?q=change:${this.change._number}&--task--applicable`;
+
+    return this.plugin.restApi().get(endpoint).then(response => {
+      if (response && response.length === 1) {
+        const cinfo = response[0];
+        if (cinfo.plugins) {
+          const taskPluginInfo = cinfo.plugins.find(
+              pluginInfo => pluginInfo.name === 'task');
+
+          if (taskPluginInfo) {
+            this._tasks = this._addTasks(taskPluginInfo.roots);
           }
         }
-      });
-    },
-
-    _computeIcon(task) {
-      const icon = {};
-      switch (task.status) {
-        case 'FAIL':
-          icon.id = 'gr-icons:close';
-          icon.color = 'red';
-          icon.tooltip = 'Failed';
-          break;
-        case 'READY':
-          icon.id = 'gr-icons:playArrow';
-          icon.color = 'green';
-          icon.tooltip = 'Ready';
-          break;
-        case 'INVALID':
-          icon.id = 'gr-icons:abandon';
-          icon.color = 'red';
-          icon.tooltip = 'Invalid';
-          break;
-        case 'WAITING':
-          icon.id = 'gr-icons:pause';
-          icon.color = 'red';
-          icon.tooltip = 'Waiting';
-          break;
-        case 'DUPLICATE':
-          icon.id = 'gr-icons:check';
-          icon.color = 'green';
-          icon.tooltip = 'Duplicate';
-          break;
-        case 'PASS':
-          icon.id = 'gr-icons:check-circle';
-          icon.color = 'green';
-          icon.tooltip = 'Passed';
-          break;
       }
-      return icon;
-    },
+    });
+  }
 
-    _isFailOrReadyOrInvalid(task) {
-      switch (task.status) {
-        case 'FAIL':
-        case 'READY':
-        case 'INVALID':
-          return true;
-      }
-      return false;
-    },
+  _computeIcon(task) {
+    const icon = {};
+    switch (task.status) {
+      case 'FAIL':
+        icon.id = 'gr-icons:close';
+        icon.color = 'red';
+        icon.tooltip = 'Failed';
+        break;
+      case 'READY':
+        icon.id = 'gr-icons:playArrow';
+        icon.color = 'green';
+        icon.tooltip = 'Ready';
+        break;
+      case 'INVALID':
+        icon.id = 'gr-icons:abandon';
+        icon.color = 'red';
+        icon.tooltip = 'Invalid';
+        break;
+      case 'WAITING':
+        icon.id = 'gr-icons:pause';
+        icon.color = 'red';
+        icon.tooltip = 'Waiting';
+        break;
+      case 'DUPLICATE':
+        icon.id = 'gr-icons:check';
+        icon.color = 'green';
+        icon.tooltip = 'Duplicate';
+        break;
+      case 'PASS':
+        icon.id = 'gr-icons:check-circle';
+        icon.color = 'green';
+        icon.tooltip = 'Passed';
+        break;
+    }
+    return icon;
+  }
 
-    _computeShowOnNeedsAndBlockedFilter(task) {
-      return this._isFailOrReadyOrInvalid(task) ||
-        (task.sub_tasks && task.sub_tasks.some(t =>
-          this._computeShowOnNeedsAndBlockedFilter(t)));
-    },
+  _isFailOrReadyOrInvalid(task) {
+    switch (task.status) {
+      case 'FAIL':
+      case 'READY':
+      case 'INVALID':
+        return true;
+    }
+    return false;
+  }
 
-    _compute_counts(task) {
-      this._all_count++;
-      switch (task.status) {
-        case 'FAIL':
-          this._fail_count++;
-          break;
-        case 'READY':
-          this._ready_count++;
-          break;
-      }
-    },
+  _computeShowOnNeedsAndBlockedFilter(task) {
+    return this._isFailOrReadyOrInvalid(task) ||
+      (task.sub_tasks && task.sub_tasks.some(t =>
+        this._computeShowOnNeedsAndBlockedFilter(t)));
+  }
 
-    _addTasks(tasks) { // rename to process, remove DOM bits
-      if (!tasks) return [];
-      tasks.forEach(task => {
-        task.message = task.hint || task.name;
-        task.icon = this._computeIcon(task);
-        task.showOnFilter = this._computeShowOnNeedsAndBlockedFilter(task);
-        this._compute_counts(task);
-        this._addTasks(task.sub_tasks);
-      });
-      return tasks;
-    },
+  _compute_counts(task) {
+    this._all_count++;
+    switch (task.status) {
+      case 'FAIL':
+        this._fail_count++;
+        break;
+      case 'READY':
+        this._ready_count++;
+        break;
+    }
+  }
 
-    _show_all_tap() {
-      this._show_all = 'true';
-      this._expand_all = true;
-    },
+  _addTasks(tasks) { // rename to process, remove DOM bits
+    if (!tasks) return [];
+    tasks.forEach(task => {
+      task.message = task.hint || task.name;
+      task.icon = this._computeIcon(task);
+      task.showOnFilter = this._computeShowOnNeedsAndBlockedFilter(task);
+      this._compute_counts(task);
+      this._addTasks(task.sub_tasks);
+    });
+    return tasks;
+  }
 
-    _needs_and_blocked_tap() {
-      this._show_all = 'false';
-      this._expand_all = true;
-    },
+  _show_all_tap() {
+    this._show_all = 'true';
+    this._expand_all = 'true';
+  }
 
-    _switch_expand() {
-      this._expand_all = !this._expand_all;
-    },
-  });
-})();
+  _needs_and_blocked_tap() {
+    this._show_all = 'false';
+    this._expand_all = 'true';
+  }
+
+  _switch_expand() {
+    this._expand_all = !this._expand_all;
+  }
+}
+
+customElements.define(GrTaskPlugin.is, GrTaskPlugin);
diff --git a/gr-task-plugin/gr-task-plugin_html.js b/gr-task-plugin/gr-task-plugin_html.js
new file mode 100644
index 0000000..71c995c
--- /dev/null
+++ b/gr-task-plugin/gr-task-plugin_html.js
@@ -0,0 +1,80 @@
+/**
+ * @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 const htmlTemplate = Polymer.html`
+<style>
+  ul {
+    padding-left: 0.5em;
+    margin-top: 0;
+  }
+  h3 {
+    padding-left: 0.1em;
+    margin: 0 0 0 0;
+  }
+  .cursor { cursor: pointer; }
+  .links {
+    color: blue;
+    cursor: pointer;
+    text-decoration: underline;
+  }
+  #tasks_header {
+    align-items: center;
+    background-color: #fafafa;
+    border-top: 1px solid #ddd;
+    display: flex;
+    padding: 6px 1rem;
+  }
+  .no-margins { margin: 0 0 0 0; }
+</style>
+
+<div id="tasks" hidden$="[[!_tasks.length]]">
+  <div id="tasks_header" style="display: flex;">
+    <iron-icon
+        icon="gr-icons:expand-less"
+        hidden$="[[!_expand_all]]"
+        on-tap="_switch_expand"
+        class="cursor"> </iron-icon>
+    <iron-icon
+        icon="gr-icons:expand-more"
+        hidden$="[[_expand_all]]"
+        on-tap="_switch_expand"
+        class="cursor"> </iron-icon>
+    <div style="display: flex; align-items: center; column-gap: 1em;">
+    <h3 class="no-margins" on-tap="_switch_expand" class="cursor"> Tasks </h3>
+    <template is="dom-if" if="[[_is_show_all(_show_all)]]">
+      <p class="no-margins" >All ([[_all_count]]) |&nbsp;
+        <span
+            on-click="_needs_and_blocked_tap"
+            class="links">Needs + Blocked ([[_ready_count]], [[_fail_count]])</span>
+      <p>
+    </template>
+    <template is="dom-if" if="[[!_is_show_all(_show_all)]]">
+      <p class="no-margins" > <span
+            class="links"
+            on-click="_show_all_tap">All ([[_all_count]])</span>
+        &nbsp;| Needs + Blocked ([[_ready_count]], [[_fail_count]])</p>
+    </template>
+  </div>
+  </div>
+  <div hidden$="[[!_expand_all]]">
+    <ul style="list-style-type:none;">
+      <gr-task-plugin-tasks
+          tasks="[[_tasks]]"
+          show_all$="[[_show_all]]"> </gr-task-plugin-tasks>
+    </ul>
+  </div>
+</div>`;
diff --git a/gr-task-plugin/plugin.js b/gr-task-plugin/plugin.js
new file mode 100644
index 0000000..59d05b0
--- /dev/null
+++ b/gr-task-plugin/plugin.js
@@ -0,0 +1,23 @@
+/**
+ * @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.
+ */
+
+import './gr-task-plugin.js';
+
+Gerrit.install(plugin => {
+  plugin.registerCustomComponent(
+      'change-view-integration', 'gr-task-plugin');
+});
diff --git a/package.json b/package.json
index fe5067c..ff90a5b 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,19 @@
 {
   "name": "task",
-  "version": "2.16.13-SNAPSHOT",
+  "version": "3.2.6-SNAPSHOT",
   "description": "Task Plugin",
-  "dependencies": {},
+  "dependencies": {
+    "@bazel/rollup": "^3.4.0",
+    "@bazel/terser": "^3.4.0"
+  },
   "devDependencies": {
     "eslint": "^6.6.0",
     "eslint-config-google": "^0.13.0",
     "eslint-plugin-html": "^6.0.0",
     "eslint-plugin-import": "^2.20.1",
-    "eslint-plugin-jsdoc": "^19.2.0"
+    "eslint-plugin-jsdoc": "^19.2.0",
+    "rollup": "^2.45.2",
+    "terser": "^5.6.1"
   },
   "scripts": {
     "eslint": "./node_modules/eslint/bin/eslint.js --ext .html,.js gr-task-plugin || exit 0",
diff --git a/plugin.html b/plugin.html
deleted file mode 100644
index 9d3144c..0000000
--- a/plugin.html
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-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="./gr-task-plugin/gr-task-plugin.html">
-
-<dom-module id="task-plugin">
-  <script>
-    if (window.Polymer) {
-      Gerrit.install(function(plugin) {
-          plugin.registerCustomComponent(
-              'change-view-integration', 'gr-task-plugin');
-      });
-    }
-  </script>
-</dom-module>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
index 11786e6..3a8d903 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.server.DynamicOptions.DynamicBean;
-import com.google.gerrit.server.change.ChangeAttributeFactory;
+import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
 import com.google.gerrit.server.restapi.change.GetChange;
 import com.google.gerrit.server.restapi.change.QueryChanges;
 import com.google.gerrit.sshd.commands.Query;
@@ -34,7 +34,7 @@
   public static class Module extends AbstractModule {
     @Override
     protected void configure() {
-      bind(ChangeAttributeFactory.class)
+      bind(ChangePluginDefinedInfoFactory.class)
           .annotatedWith(Exports.named("task"))
           .to(TaskAttributeFactory.class);
 
@@ -42,7 +42,7 @@
       bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(MyOptions.class);
       bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class);
       DynamicSet.bind(binder(), WebUiPlugin.class)
-          .toInstance(new JavaScriptPlugin("gr-task-plugin.html"));
+          .toInstance(new JavaScriptPlugin("gr-task-plugin.js"));
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
index 9367d23..688faa2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -15,11 +15,12 @@
 package com.googlesource.gerrit.plugins.task;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.PluginDefinedInfo;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.DynamicOptions.BeanProvider;
-import com.google.gerrit.server.change.ChangeAttributeFactory;
+import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
@@ -27,14 +28,16 @@
 import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
-public class TaskAttributeFactory implements ChangeAttributeFactory {
+public class TaskAttributeFactory implements ChangePluginDefinedInfoFactory {
   private static final FluentLogger log = FluentLogger.forEnclosingClass();
 
   public enum Status {
@@ -80,15 +83,17 @@
   }
 
   @Override
-  public PluginDefinedInfo create(ChangeData c, BeanProvider beanProvider, String plugin) {
+  public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
+      Collection<ChangeData> cds, BeanProvider beanProvider, String plugin) {
+    Map<Change.Id, PluginDefinedInfo> pluginInfosByChange = new HashMap<>();
     options = (Modules.MyOptions) beanProvider.getDynamicBean(plugin);
     if (options.all || options.onlyApplicable || options.onlyInvalid) {
       for (PatchSetArgument psa : options.patchSetArguments) {
         definitions.masquerade(psa);
       }
-      return createWithExceptions(c);
+      cds.forEach(cd -> pluginInfosByChange.put(cd.getId(), createWithExceptions(cd)));
     }
-    return null;
+    return pluginInfosByChange;
   }
 
   protected PluginDefinedInfo createWithExceptions(ChangeData c) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/cli/PatchSetArgument.java b/src/main/java/com/googlesource/gerrit/plugins/task/cli/PatchSetArgument.java
index e0e2317..6fb4e69 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/cli/PatchSetArgument.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/cli/PatchSetArgument.java
@@ -48,7 +48,7 @@
     public PatchSetArgument createForArgument(String token) {
       try {
         PatchSet.Id patchSetId = parsePatchSet(token);
-        ChangeNotes changeNotes = notesFactory.createChecked(patchSetId.changeId());
+        ChangeNotes changeNotes = notesFactory.createCheckedUsingIndexLookup(patchSetId.changeId());
         permissionBackend.user(user).change(changeNotes).check(ChangePermission.READ);
         return new PatchSetArgument(changeNotes.getChange(), psUtil.get(changeNotes, patchSetId));
       } catch (PermissionBackendException | AuthException e) {
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
index 6d65974..b15542e 100755
--- a/test/docker/gerrit/Dockerfile
+++ b/test/docker/gerrit/Dockerfile
@@ -1,6 +1,4 @@
-FROM gerritcodereview/gerrit:3.2.10-ubuntu20
-
-USER root
+FROM gerritcodereview/gerrit:3.3.4-ubuntu20
 
 ENV GERRIT_SITE /var/gerrit
 RUN git config -f "$GERRIT_SITE/etc/gerrit.config" auth.type \
@@ -9,5 +7,3 @@
 COPY artifacts /tmp/
 RUN cp /tmp/task.jar "$GERRIT_SITE/plugins/task.jar"
 RUN { [ -e /tmp/gerrit.war ] && cp /tmp/gerrit.war "$GERRIT_SITE/bin/gerrit.war" ; } || true
-
-USER gerrit
diff --git a/tools/bzl/genrule2.bzl b/tools/bzl/genrule2.bzl
deleted file mode 100644
index e223dcd..0000000
--- a/tools/bzl/genrule2.bzl
+++ /dev/null
@@ -1,2 +0,0 @@
-load("@com_googlesource_gerrit_bazlets//tools:genrule2.bzl", _genrule2 = "genrule2")
-genrule2 = _genrule2
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index ec1e08a..e36a535 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -1,2 +1,2 @@
-load("@com_googlesource_gerrit_bazlets//tools:js.bzl", _polygerrit_plugin = "polygerrit_plugin")
-polygerrit_plugin = _polygerrit_plugin
+load("@com_googlesource_gerrit_bazlets//tools:js.bzl", _gerrit_js_bundle = "gerrit_js_bundle")
+gerrit_js_bundle = _gerrit_js_bundle
diff --git a/yarn.lock b/yarn.lock
index 0664ef6..0bdc3f3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -23,6 +23,16 @@
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
+"@bazel/rollup@^3.4.0":
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.5.0.tgz#3de2db08cbc62c3cffbbabaa4517ec250cf6419a"
+  integrity sha512-sFPqbzSbIn6h66uuZdXgK5oitSmEGtnDPfL3TwTS4ZWy75SpYvk9X1TFGlvkralEkVnFfdH15sq80/1t+YgQow==
+
+"@bazel/terser@^3.4.0":
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-3.5.0.tgz#4b1c3a3b781e65547694aa05bc600c251e4d8c0b"
+  integrity sha512-dpWHn1Iu+w0uA/kvPb0pP+4Io0PrVuzCCbVg2Ow4uRt/gTFKQJJWp4EiTitEZlPA2dHlW7PHThAb93lGo2c8qA==
+
 "@types/json5@^0.0.29":
   version "0.0.29"
   resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@@ -124,6 +134,11 @@
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
+buffer-from@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+  integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
 call-bind@^1.0.0, call-bind@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -195,6 +210,11 @@
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
+commander@^2.20.0:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
 comment-parser@^0.7.2:
   version "0.7.6"
   resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.7.6.tgz#0e743a53c8e646c899a1323db31f6cd337b10f12"
@@ -323,21 +343,22 @@
     is-arrayish "^0.2.1"
 
 es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
-  version "1.18.5"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.5.tgz#9b10de7d4c206a3581fd5b2124233e04db49ae19"
-  integrity sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==
+  version "1.18.6"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.6.tgz#2c44e3ea7a6255039164d26559777a6d978cb456"
+  integrity sha512-kAeIT4cku5eNLNuUKhlmtuk1/TRZvQoYccn6TO0cSVdf1kzB0T7+dYuVK9MWM7l+/53W2Q8M7N2c6MQvhXFcUQ==
   dependencies:
     call-bind "^1.0.2"
     es-to-primitive "^1.2.1"
     function-bind "^1.1.1"
     get-intrinsic "^1.1.1"
+    get-symbol-description "^1.0.0"
     has "^1.0.3"
     has-symbols "^1.0.2"
     internal-slot "^1.0.3"
-    is-callable "^1.2.3"
+    is-callable "^1.2.4"
     is-negative-zero "^2.0.1"
-    is-regex "^1.1.3"
-    is-string "^1.0.6"
+    is-regex "^1.1.4"
+    is-string "^1.0.7"
     object-inspect "^1.11.0"
     object-keys "^1.1.1"
     object.assign "^4.1.2"
@@ -592,6 +613,11 @@
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
+fsevents@~2.3.1:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -611,6 +637,14 @@
     has "^1.0.3"
     has-symbols "^1.0.1"
 
+get-symbol-description@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
+  integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
+  dependencies:
+    call-bind "^1.0.2"
+    get-intrinsic "^1.1.1"
+
 glob-parent@^5.0.0:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@@ -777,7 +811,7 @@
     call-bind "^1.0.2"
     has-tostringtag "^1.0.0"
 
-is-callable@^1.1.4, is-callable@^1.2.3:
+is-callable@^1.1.4, is-callable@^1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
   integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
@@ -830,7 +864,7 @@
   dependencies:
     has-tostringtag "^1.0.0"
 
-is-regex@^1.1.3:
+is-regex@^1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
   integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
@@ -838,7 +872,7 @@
     call-bind "^1.0.2"
     has-tostringtag "^1.0.0"
 
-is-string@^1.0.5, is-string@^1.0.6:
+is-string@^1.0.5, is-string@^1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
   integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
@@ -1207,6 +1241,13 @@
   dependencies:
     glob "^7.1.3"
 
+rollup@^2.45.2:
+  version "2.47.0"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.47.0.tgz#9d958aeb2c0f6a383cacc0401dff02b6e252664d"
+  integrity sha512-rqBjgq9hQfW0vRmz+0S062ORRNJXvwRpzxhFXORvar/maZqY6za3rgQ/p1Glg+j1hnc1GtYyQCPiAei95uTElg==
+  optionalDependencies:
+    fsevents "~2.3.1"
+
 run-async@^2.4.0:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
@@ -1269,6 +1310,24 @@
     astral-regex "^1.0.0"
     is-fullwidth-code-point "^2.0.0"
 
+source-map-support@~0.5.19:
+  version "0.5.19"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
+  integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+source-map@~0.7.2:
+  version "0.7.3"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+  integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+
 spdx-correct@^3.0.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
@@ -1382,6 +1441,15 @@
     slice-ansi "^2.1.0"
     string-width "^3.0.0"
 
+terser@^5.6.1:
+  version "5.7.0"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.0.tgz#a761eeec206bc87b605ab13029876ead938ae693"
+  integrity sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.7.2"
+    source-map-support "~0.5.19"
+
 text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"