Add workflow menu to thread lists

This CL adds a MD3 menu button to the bulk actions bar in thread lists.
The menu will display the available workflows, but as of now it just
contains random items, and clicking the items doesn't work (this will be
implemented later).

This implementation has been done using the newly added @material/web
package, so the elix package has been removed from the project. This is
because @material/web also uses Material Design like the Community
Console.

Bug: twpowertools:74
Change-Id: Ifb3712c7afc024e21c7ff83fb77658cd8d08bc8a
diff --git a/package-lock.json b/package-lock.json
index 5b8f478..38826bd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,8 +10,8 @@
       "license": "MIT",
       "dependencies": {
         "@material/tooltip": "^12.0.0",
+        "@material/web": "^0.1.0-alpha.0",
         "async-mutex": "^0.3.2",
-        "elix": "^15.0.1",
         "google-protobuf": "^3.19.3",
         "grpc-web": "^1.2.1",
         "idb": "^6.1.2",
@@ -207,6 +207,15 @@
         "tslib": "^2.1.0"
       }
     },
+    "node_modules/@material/web": {
+      "version": "0.1.0-alpha.0",
+      "resolved": "https://registry.npmjs.org/@material/web/-/web-0.1.0-alpha.0.tgz",
+      "integrity": "sha512-GNsNyWOtYKezXwvrD1ZBfnOHClVya31ZEQH1O83+PsPqlVZK6cBfnf1cwJmEFQ6FezB6q6q+cHGGqx0lb90Jug==",
+      "dependencies": {
+        "lit": "^2.3.0",
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -769,11 +778,6 @@
       "integrity": "sha512-X/6VRCXWALzdX+RjCtBU6cyg8WZgoxm9YA02COmDOiNJEZ59WkQggDbWZ4t/giHi/3GS+cvdrP6gbLISANAGYA==",
       "dev": true
     },
-    "node_modules/elix": {
-      "version": "15.0.1",
-      "resolved": "https://registry.npmjs.org/elix/-/elix-15.0.1.tgz",
-      "integrity": "sha512-hgL6EDdMO/JBJLmDfaM8AL0f3zQXDIwkjQEhaLi9OTLV+3q445ErkBZuHGK9UzAoIq3I062ldDXRX48l1vfqCg=="
-    },
     "node_modules/enhanced-resolve": {
       "version": "5.8.2",
       "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz",
@@ -2132,9 +2136,9 @@
       }
     },
     "node_modules/tslib": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
-      "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+      "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
     },
     "node_modules/uri-js": {
       "version": "4.4.1",
@@ -2523,6 +2527,15 @@
         "tslib": "^2.1.0"
       }
     },
+    "@material/web": {
+      "version": "0.1.0-alpha.0",
+      "resolved": "https://registry.npmjs.org/@material/web/-/web-0.1.0-alpha.0.tgz",
+      "integrity": "sha512-GNsNyWOtYKezXwvrD1ZBfnOHClVya31ZEQH1O83+PsPqlVZK6cBfnf1cwJmEFQ6FezB6q6q+cHGGqx0lb90Jug==",
+      "requires": {
+        "lit": "^2.3.0",
+        "tslib": "^2.4.0"
+      }
+    },
     "@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2981,11 +2994,6 @@
       "integrity": "sha512-X/6VRCXWALzdX+RjCtBU6cyg8WZgoxm9YA02COmDOiNJEZ59WkQggDbWZ4t/giHi/3GS+cvdrP6gbLISANAGYA==",
       "dev": true
     },
-    "elix": {
-      "version": "15.0.1",
-      "resolved": "https://registry.npmjs.org/elix/-/elix-15.0.1.tgz",
-      "integrity": "sha512-hgL6EDdMO/JBJLmDfaM8AL0f3zQXDIwkjQEhaLi9OTLV+3q445ErkBZuHGK9UzAoIq3I062ldDXRX48l1vfqCg=="
-    },
     "enhanced-resolve": {
       "version": "5.8.2",
       "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz",
@@ -3939,9 +3947,9 @@
       }
     },
     "tslib": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
-      "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+      "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
     },
     "uri-js": {
       "version": "4.4.1",
diff --git a/package.json b/package.json
index a460742..4c90fd3 100644
--- a/package.json
+++ b/package.json
@@ -40,8 +40,8 @@
   "private": true,
   "dependencies": {
     "@material/tooltip": "^12.0.0",
+    "@material/web": "^0.1.0-alpha.0",
     "async-mutex": "^0.3.2",
-    "elix": "^15.0.1",
     "google-protobuf": "^3.19.3",
     "grpc-web": "^1.2.1",
     "idb": "^6.1.2",
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index 95045a4..964a647 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -10,8 +10,9 @@
 // #!endif
 import InfiniteScroll from './infiniteScroll.js';
 import {unifiedProfilesFix} from './unifiedProfiles.js';
+import Workflows from './workflows/workflows.js';
 
-var mutationObserver, options, avatars, infiniteScroll;
+var mutationObserver, options, avatars, infiniteScroll, workflows;
 
 const watchedNodesSelectors = [
   // App container (used to set up the intersection observer and inject the dark
@@ -128,6 +129,12 @@
     }
     // #!endif
 
+    // Inject the worflows menu in the thread list if the option is currently
+    // enabled.
+    if (workflows.shouldAddThreadListBtn(node)) {
+      workflows.addThreadListBtnIfEnabled(node);
+    }
+
     // Inject the batch lock button in the thread list if the option is
     // currently enabled.
     if (batchLock.shouldAddButton(node)) {
@@ -227,6 +234,7 @@
   // Initialize classes needed by the mutation observer
   avatars = new AvatarsHandler();
   infiniteScroll = new InfiniteScroll();
+  workflows = new Workflows();
 
   // autoRefresh, extraInfo and threadPageDesignWarning are initialized in
   // start.js
@@ -287,4 +295,6 @@
   // Extra info
   injectStylesheet(chrome.runtime.getURL('css/extrainfo.css'));
   injectStylesheet(chrome.runtime.getURL('css/extrainfo_perforumstats.css'));
+  // Workflows
+  injectScript(chrome.runtime.getURL('workflowComponentsInject.bundle.js'));
 });
diff --git a/src/contentScripts/communityConsole/utils/common.js b/src/contentScripts/communityConsole/utils/common.js
index 2e33f99..052b580 100644
--- a/src/contentScripts/communityConsole/utils/common.js
+++ b/src/contentScripts/communityConsole/utils/common.js
@@ -31,6 +31,19 @@
   return [badge, badgeTooltip];
 }
 
+// Adds an element to the thread list actions bar next to the button given by
+// |originalBtn|.
+export function addElementToThreadListActions(originalBtn, element) {
+  var duplicateBtn =
+      originalBtn.parentNode.querySelector('[debugid="mark-duplicate-button"]');
+  if (duplicateBtn)
+    duplicateBtn.parentNode.insertBefore(
+        element, (duplicateBtn.nextSibling || duplicateBtn));
+  else
+    originalBtn.parentNode.insertBefore(
+        element, (originalBtn.nextSibling || originalBtn));
+}
+
 // Adds a button to the thread list actions bar next to the button given by
 // |originalBtn|. The button will have icon |icon|, when hovered it will display
 // |tooltip|, and will have a debugid attribute with value |debugId|.
@@ -46,14 +59,7 @@
   [badge, badgeTooltip] = createExtBadge();
   clone.append(badge);
 
-  var duplicateBtn =
-      originalBtn.parentNode.querySelector('[debugid="mark-duplicate-button"]');
-  if (duplicateBtn)
-    duplicateBtn.parentNode.insertBefore(
-        clone, (duplicateBtn.nextSibling || duplicateBtn));
-  else
-    originalBtn.parentNode.insertBefore(
-        clone, (originalBtn.nextSibling || originalBtn));
+  addElementToThreadListActions(originalBtn, clone);
 
   createPlainTooltip(clone, tooltip);
   new MDCTooltip(badgeTooltip);
diff --git a/src/contentScripts/communityConsole/workflows/components/TwptWorkflowsMenu.js b/src/contentScripts/communityConsole/workflows/components/TwptWorkflowsMenu.js
new file mode 100644
index 0000000..d36e730
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/components/TwptWorkflowsMenu.js
@@ -0,0 +1,40 @@
+import '@material/web/iconbutton/standard-icon-button.js';
+import '@material/web/menu/menu-button.js';
+import '@material/web/menu/menu.js';
+import '@material/web/menu/menu-item.js';
+
+import {css, html, LitElement} from 'lit';
+import {map} from 'lit/directives/map.js';
+import {range} from 'lit/directives/range.js';
+
+export default class TwptWorkflowsMenu extends LitElement {
+  static styles = css`
+    .workflow-item {
+      padding-inline: 1em;
+    }
+  `;
+
+  renderMenuItems() {
+    return map(range(8), i => html`
+      <md-menu-item @click="${this._showWorkflow}" data-workflow-id="${i}"><span class="workflow-item" slot="start">Workflow ${i}</span></md-menu-item>
+    `);
+  }
+
+  render() {
+    // The button is based in the button created in the
+    // addButtonToThreadListActions function in file ../../utils/common.js.
+    return html`
+      <md-menu-button>
+        <md-standard-icon-button slot="button" icon="more_vert"></md-standard-icon-button>
+        <md-menu slot="menu">
+          ${this.renderMenuItems()}
+        </md-menu>
+      </md-menu-button>
+    `;
+  }
+
+  _showWorkflow(e) {
+    console.log(`Clicked workflow ${e.target.getAttribute('data-workflow-id')}.`);
+  }
+}
+window.customElements.define('twpt-workflows-menu', TwptWorkflowsMenu);
diff --git a/src/contentScripts/communityConsole/workflows/workflows.js b/src/contentScripts/communityConsole/workflows/workflows.js
new file mode 100644
index 0000000..1c99729
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/workflows.js
@@ -0,0 +1,21 @@
+import {isOptionEnabled} from '../../../common/optionsUtils.js';
+import {addElementToThreadListActions, shouldAddBtnToActionBar} from '../utils/common.js';
+
+const wfDebugId = 'twpt-workflows';
+
+export default class Workflows {
+  constructor() {}
+
+  addThreadListBtnIfEnabled(readToggle) {
+    isOptionEnabled('workflows').then(isEnabled => {
+      if (isEnabled) {
+        const menu = document.createElement('twpt-workflows-menu');
+        addElementToThreadListActions(readToggle, menu);
+      }
+    });
+  }
+
+  shouldAddThreadListBtn(node) {
+    return shouldAddBtnToActionBar(wfDebugId, node);
+  }
+};
diff --git a/src/injections/workflowComponentsInject.js b/src/injections/workflowComponentsInject.js
new file mode 100644
index 0000000..c15111e
--- /dev/null
+++ b/src/injections/workflowComponentsInject.js
@@ -0,0 +1,12 @@
+// This file imports necessary web components used for the workflows feature.
+// This is done by injecting this javascript file instead of placing this code
+// directly in the content script because `window.customElements` doesn't exist
+// in content scripts.
+import '../contentScripts/communityConsole/workflows/components/TwptWorkflowsMenu.js';
+
+import {injectStylesheet} from '../common/contentScriptsUtils.js';
+
+// Also, we import Material Icons since the Community Console uses "Google
+// Material Icons" instead of "Material Icons". This is necessary for the MD3
+// components.
+injectStylesheet('https://fonts.googleapis.com/icon?family=Material+Icons');
diff --git a/templates/manifest.gjson b/templates/manifest.gjson
index e3eb47f..1a23c85 100644
--- a/templates/manifest.gjson
+++ b/templates/manifest.gjson
@@ -84,6 +84,7 @@
         "batchLockInject.bundle.js",
         "xhrInterceptorInject.bundle.js",
         "extraInfoInject.bundle.js",
+        "workflowComponentsInject.bundle.js",
 
         "css/profileindicator_inject.css",
         "css/ccdarktheme.css",
diff --git a/webpack.config.js b/webpack.config.js
index 3591c97..a86a8fd 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -52,6 +52,7 @@
     batchLockInject: './src/injections/batchLock.js',
     xhrInterceptorInject: './src/injections/xhrInterceptor.js',
     extraInfoInject: './src/injections/extraInfo.js',
+    workflowComponentsInject: './src/injections/workflowComponentsInject.js',
 
     // Options page
     optionsCommon: './src/options/optionsCommon.js',