UI: Add chips to ease navigation to the Tasks tab

On changes with a lot of label scores or changed files, users
have to scroll to view status of their tasks. This change adds
a chip (under the commit message, similar to comment chips)
each for 'needs' (aka ready) and 'blocked' (aka failed) tasks
to ease navigation and to show a quick summary of tasks. Here
are some highlights:

 - Chips are shown only after the tasks have loaded
 - Clicking the chips changes focus to the Tasks tab
 - Each chip shows the count of respective tasks
 - Chips are hidden if the count of respective tasks is 0
 - Warning style is used for 'needs' chip and error for 'blocked'

Also, update the eslint ecmaVersion[1] to 2020 to support optional
chaining.

[1] https://eslint.org/blog/2020/07/eslint-v7.5.0-released/#optional-chaining-support

Change-Id: I3e69232661577b64064ce03627cf7efb86728c81
diff --git a/.eslintrc.json b/.eslintrc.json
index 0c290fa..358c95d 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -4,7 +4,7 @@
       "google"
   ],
   "parserOptions": {
-      "ecmaVersion": 8,
+      "ecmaVersion": 2020,
       "sourceType": "module"
   },
   "env": {
diff --git a/gr-task-plugin/gr-task-chip.js b/gr-task-plugin/gr-task-chip.js
new file mode 100644
index 0000000..8e46bd0
--- /dev/null
+++ b/gr-task-plugin/gr-task-chip.js
@@ -0,0 +1,66 @@
+/**
+ * @license
+ * Copyright (C) 2023 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';
+import {htmlTemplate} from './gr-task-chip_html.js';
+
+class GrTaskChip extends Polymer.Element {
+  static get is() {
+    return 'gr-task-chip';
+  }
+
+  static get template() {
+    return htmlTemplate;
+  }
+
+  static get properties() {
+    return {
+      chip_style: {
+        type: String,
+        notify: true,
+        value: 'ready',
+      },
+    };
+  }
+
+  _setTasksTabActive() {
+    // TODO: Identify a better way as current implementation is fragile
+    const endPointDecorators = document.querySelector('gr-app')
+        .shadowRoot.querySelector('gr-app-element')
+        .shadowRoot.querySelector('main')
+        .querySelector('gr-change-view')
+        .shadowRoot.querySelector('#mainContent')
+        .getElementsByTagName('gr-endpoint-decorator');
+    if (endPointDecorators) {
+      for (let i = 0; i <= endPointDecorators.length; i++) {
+        const el = endPointDecorators[i]
+            ?.shadowRoot?.querySelector('gr-task-plugin');
+        if (el) {
+          el.shadowRoot.querySelector('paper-tabs')
+              .querySelector('paper-tab').scrollIntoView();
+          break;
+        }
+      }
+    }
+  }
+
+  _onChipClick() {
+    this._setTasksTabActive();
+  }
+}
+
+customElements.define(GrTaskChip.is, GrTaskChip);
\ No newline at end of file
diff --git a/gr-task-plugin/gr-task-chip_html.js b/gr-task-plugin/gr-task-chip_html.js
new file mode 100644
index 0000000..af7338f
--- /dev/null
+++ b/gr-task-plugin/gr-task-chip_html.js
@@ -0,0 +1,72 @@
+/**
+ * @license
+ * Copyright (C) 2023 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="gr-a11y-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
+  <style include="shared-styles">
+    .taskSummaryChip {
+      color: var(--chip-color);
+      cursor: pointer;
+      display: inline-block;
+      padding: var(--spacing-xxs) var(--spacing-m) var(--spacing-xxs)
+        var(--spacing-s);
+      margin-right: var(--spacing-s);
+      border-radius: 12px;
+      border: 1px solid gray;
+      vertical-align: top;
+      /* centered position of 20px chips in 24px line-height inline flow */
+      vertical-align: top;
+      position: relative;
+      top: 2px;
+    }
+    .taskSummaryChip.ready {
+      border-color: var(--warning-foreground);
+      background: var(--warning-background);
+    }
+    .taskSummaryChip.ready:hover {
+      background: var(--warning-background-hover);
+      box-shadow: var(--elevation-level-1);
+    }
+    .taskSummaryChip.ready:focus-within {
+      background: var(--warning-background-focus);
+    }
+    .taskSummaryChip.fail {
+      color: var(--error-foreground);
+      border-color: var(--error-foreground);
+      background: var(--error-background);
+    }
+    .taskSummaryChip.fail:hover {
+      background: var(--error-background-hover);
+      box-shadow: var(--elevation-level-1);
+    }
+    .taskSummaryChip.fail:focus-within {
+      background: var(--error-background-focus);
+    }
+    .font-small {
+      font-size: var(--font-size-small);
+      font-weight: var(--font-weight-normal);
+      line-height: var(--line-height-small);
+    }
+  </style>
+  <button
+    class$="taskSummaryChip font-small [[chip_style]]"
+    on-click="_onChipClick">
+    <slot></slot>
+  </button>
+`;
diff --git a/gr-task-plugin/gr-task-plugin.js b/gr-task-plugin/gr-task-plugin.js
index aa0e5f3..4813d10 100644
--- a/gr-task-plugin/gr-task-plugin.js
+++ b/gr-task-plugin/gr-task-plugin.js
@@ -123,6 +123,13 @@
 
           if (taskPluginInfo) {
             this._tasks = this._addTasks(taskPluginInfo.roots);
+            document.dispatchEvent(new CustomEvent('tasks-loaded', {
+              detail: {
+                ready_count: this._ready_count,
+                fail_count: this._fail_count,
+              },
+              composed: true, bubbles: true,
+            }));
           }
         }
       }
diff --git a/gr-task-plugin/gr-task-summary.js b/gr-task-plugin/gr-task-summary.js
new file mode 100644
index 0000000..6e54268
--- /dev/null
+++ b/gr-task-plugin/gr-task-summary.js
@@ -0,0 +1,60 @@
+/**
+ * @license
+ * Copyright (C) 2023 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-summary_html.js';
+import './gr-task-chip.js';
+
+class GrTaskSummary extends Polymer.Element {
+  static get is() {
+    return 'gr-task-summary';
+  }
+
+  static get template() {
+    return htmlTemplate;
+  }
+
+  static get properties() {
+    return {
+      ready_count: {
+        type: Number,
+        notify: true,
+        value: 0,
+      },
+
+      fail_count: {
+        type: Number,
+        notify: true,
+        value: 0,
+      },
+    };
+  }
+
+  /** @override */
+  ready() {
+    super.ready();
+    document.addEventListener('tasks-loaded', e => {
+      this.ready_count = e.detail.ready_count;
+      this.fail_count = e.detail.fail_count;
+    });
+  }
+
+  _can_show(ready_count, fail_count) {
+    return ready_count || fail_count;
+  }
+}
+
+customElements.define(GrTaskSummary.is, GrTaskSummary);
\ No newline at end of file
diff --git a/gr-task-plugin/gr-task-summary_html.js b/gr-task-plugin/gr-task-summary_html.js
new file mode 100644
index 0000000..18afc66
--- /dev/null
+++ b/gr-task-plugin/gr-task-summary_html.js
@@ -0,0 +1,80 @@
+/**
+ * @license
+ * Copyright (C) 2023 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="gr-a11y-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
+  <style include="shared-styles">
+    :host {
+      display: block;
+      color: var(--deemphasized-text-color);
+      max-width: 625px;
+      margin-bottom: var(--spacing-m);
+    }
+    .zeroState {
+      color: var(--deemphasized-text-color);
+    }
+    .loading.zeroState {
+      margin-right: var(--spacing-m);
+    }
+    div.error,
+    .login {
+      display: flex;
+      color: var(--primary-text-color);
+      padding: 0 var(--spacing-s);
+      margin: var(--spacing-xs) 0;
+      width: 490px;
+    }
+    div.error {
+      background-color: var(--error-background);
+    }
+    div.error .right {
+      overflow: hidden;
+    }
+    div.error .right .message {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    td.key {
+      padding-right: var(--spacing-l);
+      padding-bottom: var(--spacing-s);
+      line-height: calc(var(--line-height-normal) + var(--spacing-s));
+    }
+    td.value {
+      padding-right: var(--spacing-l);
+      padding-bottom: var(--spacing-s);
+      line-height: calc(var(--line-height-normal) + var(--spacing-s));
+      display: flex;
+    }
+    div {
+      margin-left: var(--spacing-m);
+    }
+  </style>
+  <div class="task_summary" hidden$="[[!_can_show(ready_count, fail_count)]]">
+    <table>
+      <tr>
+        <td class="key">Tasks</td>
+        <td class="value">
+          <gr-task-chip chip_style="ready" hidden$="[[!ready_count]]">[[ready_count]] needs</gr-task-chip>
+          <gr-task-chip chip_style="fail" hidden$="[[!fail_count]]">[[fail_count]] blocked</gr-task-chip>
+        </td>
+      </tr>
+    </table>
+  </div>
+`;
diff --git a/gr-task-plugin/plugin.js b/gr-task-plugin/plugin.js
index 59d05b0..2219f12 100644
--- a/gr-task-plugin/plugin.js
+++ b/gr-task-plugin/plugin.js
@@ -16,8 +16,11 @@
  */
 
 import './gr-task-plugin.js';
+import './gr-task-summary.js';
 
 Gerrit.install(plugin => {
   plugin.registerCustomComponent(
       'change-view-integration', 'gr-task-plugin');
+  plugin.registerCustomComponent(
+      'commit-container', 'gr-task-summary');
 });