Workflows manager: add user-friendly CR selector

This CL lets users select CRs for the "Reply with CR" action in a
user-friendly manner.

A "Select CR" button next to the CR field has been added, which opens a
popup with an adapted version of the Community Console CR list with
buttons next to each CR which lets the user select one of them.

Fixed: twpowertools:148
Change-Id: I9799d671e7440b66435b30c540adc3f050c9f4e2
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index 964a647..e5d9af3 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -53,7 +53,8 @@
   // Unified profile iframe
   'iframe',
 
-  // Canned response tags or toolbelt (for the extra info feature)
+  // Canned response tags or toolbelt (for the extra info feature and the
+  // "import CR" popup for the workflows feature)
   'ec-canned-response-row .tags',
   'ec-canned-response-row .main .toolbelt',
 
@@ -170,9 +171,11 @@
       unifiedProfilesFix.fixIframe(node);
     }
 
-    // Show additional details in the canned responses view.
+    // Show additional details in the canned responses view (and add the
+    // "import" button if applicable for the workflows feature).
     if (node.matches('ec-canned-response-row .tags')) {
       window.TWPTExtraInfo.injectAtCRIfEnabled(node, /* isExpanded = */ false);
+      window.TWPTWorkflowsImport.addButtonIfEnabled(node);
     }
     if (node.matches('ec-canned-response-row .main .toolbelt')) {
       const tags = node.parentNode?.querySelector?.('.tags');
@@ -236,8 +239,8 @@
   infiniteScroll = new InfiniteScroll();
   workflows = new Workflows();
 
-  // autoRefresh, extraInfo and threadPageDesignWarning are initialized in
-  // start.js
+  // autoRefresh, extraInfo, threadPageDesignWarning and workflowsImport are
+  // initialized in start.js
 
   // Before starting the mutation Observer, check whether we missed any
   // mutations by manually checking whether some watched nodes already
diff --git a/src/contentScripts/communityConsole/start.js b/src/contentScripts/communityConsole/start.js
index f0c43d8..b3417fe 100644
--- a/src/contentScripts/communityConsole/start.js
+++ b/src/contentScripts/communityConsole/start.js
@@ -4,6 +4,7 @@
 import AutoRefresh from './autoRefresh.js';
 import ExtraInfo from './extraInfo.js';
 import ThreadPageDesignWarning from './threadPageDesignWarning.js';
+import WorkflowsImport from './workflows/import.js';
 
 const SMEI_NESTED_REPLIES = 15;
 const SMEI_RCE_THREAD_INTEROP = 22;
@@ -50,6 +51,7 @@
   window.TWPTAutoRefresh = new AutoRefresh();
   window.TWPTExtraInfo = new ExtraInfo();
   window.TWPTThreadPageDesignWarning = new ThreadPageDesignWarning();
+  window.TWPTWorkflowsImport = new WorkflowsImport();
 
   if (options.ccdarktheme) {
     switch (options.ccdarktheme_mode) {
diff --git a/src/contentScripts/communityConsole/workflows/components/TwptCRImportButton.js b/src/contentScripts/communityConsole/workflows/components/TwptCRImportButton.js
new file mode 100644
index 0000000..935926f
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/components/TwptCRImportButton.js
@@ -0,0 +1,40 @@
+import '@material/web/button/outlined-button.js';
+
+import {html, LitElement} from 'lit';
+
+import {SHARED_MD3_STYLES} from '../../../../common/styles/md3.js';
+
+export default class TwptCRImportButton extends LitElement {
+  static properties = {
+    cannedResponseId: {type: String},
+    selected: {type: Boolean},
+  };
+
+  static styles = [
+    SHARED_MD3_STYLES,
+  ];
+
+  render() {
+    const icon = this.selected ? 'done' : 'post_add';
+    const label = this.selected ? 'Selected' : 'Select';
+
+    return html`
+      <md-outlined-button
+          icon=${icon}
+          label=${label}
+          ?disabled=${this.selected}
+          @click=${this._importCR}>
+      </md-outlined-button>
+    `;
+  }
+
+  _importCR() {
+    window.opener?.postMessage?.(
+        {
+          action: 'importCannedResponse',
+          cannedResponseId: this.cannedResponseId,
+        },
+        '*');
+  }
+}
+window.customElements.define('twpt-cr-import-button', TwptCRImportButton);
diff --git a/src/contentScripts/communityConsole/workflows/components/index.js b/src/contentScripts/communityConsole/workflows/components/index.js
index 05cf839..f7651f0 100644
--- a/src/contentScripts/communityConsole/workflows/components/index.js
+++ b/src/contentScripts/communityConsole/workflows/components/index.js
@@ -1,4 +1,5 @@
 import './TwptConfirmDialog.js';
+import './TwptCRImportButton.js';
 import './TwptWorkflowDialog.js';
 import './TwptWorkflowsMenu.js';
 
@@ -53,7 +54,8 @@
   }
 
   _startWorkflow() {
-    this.workflowDialogRef.value.workflow = this._selectedWorkflow.cloneMessage();
+    this.workflowDialogRef.value.workflow =
+        this._selectedWorkflow.cloneMessage();
     this.workflowDialogRef.value.start();
   }
 
diff --git a/src/contentScripts/communityConsole/workflows/import.js b/src/contentScripts/communityConsole/workflows/import.js
new file mode 100644
index 0000000..8172b62
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/import.js
@@ -0,0 +1,108 @@
+import {waitFor} from 'poll-until-promise';
+
+import {recursiveParentElement} from '../../../common/commonUtils.js';
+import {injectStylesheet} from '../../../common/contentScriptsUtils.js';
+import {isOptionEnabled} from '../../../common/optionsUtils.js';
+
+const kListCannedResponsesResponse = 'TWPT_ListCannedResponsesResponse';
+
+const kImportParam = 'TWPTImportToWorkflow';
+const kSelectedIdParam = 'TWPTSelectedId';
+
+// Class which is used to inject a "select" button in the CRs list when loading
+// the canned response list for this purpose from the workflows manager.
+export default class WorkflowsImport {
+  constructor() {
+    // Only set this class up if the Community Console was opened with the
+    // purpose of importing CRs to the workflow manager.
+    const searchParams = new URLSearchParams(document.location.search);
+    if (!searchParams.has(kImportParam)) return;
+
+    this.selectedId = searchParams.get(kSelectedIdParam);
+
+    this.lastCRsList = {
+      body: {},
+      id: -1,
+      duplicateNames: new Set(),
+    };
+
+    this.setUpHandler();
+    this.addCustomStyles();
+  }
+
+  setUpHandler() {
+    window.addEventListener(kListCannedResponsesResponse, e => {
+      if (e.detail.id < this.lastCRsList.id) return;
+
+      // Look if there are duplicate names
+      const crs = e.detail.body?.['1'] ?? [];
+      const names = crs.map(cr => cr?.['7']).slice().sort();
+      let duplicateNames = new Set();
+      for (let i = 1; i < names.length; i++)
+        if (names[i - 1] == names[i]) duplicateNames.add(names[i]);
+
+      this.lastCRsList = {
+        body: e.detail.body,
+        id: e.detail.id,
+        duplicateNames,
+      };
+    });
+  }
+
+  addCustomStyles() {
+    injectStylesheet(chrome.runtime.getURL('css/workflow_import.css'));
+  }
+
+  addButton(tags) {
+    const cr = recursiveParentElement(tags, 'EC-CANNED-RESPONSE-ROW');
+    if (!cr) return;
+
+    const name = cr.querySelector('.text .name').textContent;
+    if (!name) return;
+
+    const toolbar = cr.querySelector('.action .toolbar');
+    if (!toolbar) return console.error(`Can't find toolbar.`);
+
+    // If it has already been injected, exit.
+    if (toolbar.querySelector('twpt-cr-import-button')) return;
+
+    waitFor(() => {
+      if (this.lastCRsList.id != -1) return Promise.resolve(this.lastCRsList);
+      return Promise.reject(new Error('Didn\'t receive canned responses list'));
+    }, {
+      interval: 500,
+      timeout: 15 * 1000,
+    }).then(crs => {
+      // If another CR has the same name, there's no easy way to distinguish
+      // them, so don't inject the button.
+      if (crs.duplicateNames.has(name)) {
+        console.info(
+            'CR "' + name +
+            '" is duplicate, so skipping the injection of the button.');
+        return;
+      }
+
+      for (const cr of (crs.body?.[1] ?? [])) {
+        if (cr[7] == name) {
+          const id = cr?.[1]?.[1];
+          if (!id) {
+            console.error('Can\'t find ID for canned response', cr);
+            break;
+          }
+
+          const button = document.createElement('twpt-cr-import-button');
+          button.setAttribute('cannedResponseId', id);
+          if (this.selectedId == id) button.setAttribute('selected', '');
+          toolbar.prepend(button);
+          break;
+        }
+      }
+    });
+  }
+
+  addButtonIfEnabled(tags) {
+    isOptionEnabled('workflows').then(isEnabled => {
+      if (isEnabled) this.addButton(tags);
+    });
+  }
+}
diff --git a/src/static/css/workflow_import.css b/src/static/css/workflow_import.css
new file mode 100644
index 0000000..e11c9d3
--- /dev/null
+++ b/src/static/css/workflow_import.css
@@ -0,0 +1,41 @@
+/**
+ * Styles used to hide some UI components in order to adapt the Community
+ * Console to be shown like an importer popup for the workflow manager.
+ */
+header.material-header {
+  visibility: hidden;
+}
+
+.material-content {
+  margin-top: 0!important;
+}
+
+.material-content > .scrollable-content {
+  height: 100vh!important;
+}
+
+material-drawer {
+  visibility: hidden;
+}
+
+.material-content > .scrollable-content > main.shifted-right {
+  margin-left: 32px!important;
+}
+
+ec-back-button {
+  visibility: hidden;
+}
+
+/**
+ * Don't allow creating/modifying/deleting CRs because this breaks the logic
+ * which adds the "Select" buttons.
+ */
+ec-canned-response-row .action .toolbar :is(
+    [debugid="edit-button"],
+    [debugid="delete-button"]) {
+  display: none;
+}
+
+material-fab.create-button {
+  display: none;
+}
diff --git a/src/workflows/manager/components/actions/ReplyWithCR.js b/src/workflows/manager/components/actions/ReplyWithCR.js
index 614a042..f1bdeeb 100644
--- a/src/workflows/manager/components/actions/ReplyWithCR.js
+++ b/src/workflows/manager/components/actions/ReplyWithCR.js
@@ -1,8 +1,8 @@
-import '@material/web/textfield/outlined-text-field.js';
-import '@material/web/switch/switch.js';
 import '@material/web/formfield/formfield.js';
+import '@material/web/switch/switch.js';
+import '@material/web/textfield/outlined-text-field.js';
 
-import {html, LitElement} from 'lit';
+import {css, html, LitElement} from 'lit';
 import {createRef, ref} from 'lit/directives/ref.js';
 
 import {CCApi} from '../../../../common/api.js';
@@ -12,8 +12,19 @@
   static properties = {
     action: {type: Object},
     readOnly: {type: Boolean},
+    _importerWindow: {type: Object, state: true},
   };
 
+  static styles = css`
+    .form-line {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      margin-block: 1em;
+      gap: .5rem;
+    }
+  `;
+
   cannedResponseRef = createRef();
   subscribeRef = createRef();
   markAsAnswerRef = createRef();
@@ -21,12 +32,20 @@
   constructor() {
     super();
     this.action = new pb.workflows.Action.ReplyWithCRAction;
-    // this._loadUserCannedResponses();
+    this._importerWindow = undefined;
+
+    window.addEventListener('message', e => {
+      if (e.source === this._importerWindow &&
+          e.data?.action === 'importCannedResponse') {
+        this._cannedResponseIdString = e.data?.cannedResponseId;
+        this._importerWindow?.close?.();
+      }
+    });
   }
 
   render() {
     return html`
-      <p>
+      <div class="form-line">
         <md-outlined-text-field ${ref(this.cannedResponseRef)}
             type="number"
             label="Canned response ID"
@@ -35,44 +54,40 @@
             ?readonly=${this.readOnly}
             @input=${this._cannedResponseIdChanged}>
         </md-outlined-text-field>
-      </p>
-      <p>
+        <md-outlined-button
+            icon="more"
+            label="Select CR"
+            @click=${this._openCRImporter}>
+        </md-outlined-button>
+      </div>
+      <div class="form-line">
         <md-formfield label="Subscribe to thread">
           <md-switch ${ref(this.subscribeRef)}
               ?selected=${this.subscribe}
               ?disabled=${this.readOnly}
               @click=${this._subscribeChanged}/>
         </md-formfield>
-      </p>
-      <p>
+      </div>
+      <div class="form-line">
         <md-formfield label="Mark as answer">
           <md-switch ${ref(this.markAsAnswerRef)}
               ?selected=${this.markAsAnswer}
               ?disabled=${this.readOnly}
               @click=${this._markAsAnswerChanged}/>
         </md-formfield>
-      </p>
+      </div>
     `;
   }
 
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    this._importerWindow?.close?.();
+  }
+
   checkValidity() {
     return this.cannedResponseRef.value.reportValidity();
   }
 
-  _loadUserCannedResponses() {
-    if (window.USER_CANNED_RESPONSES_STARTED_TO_LOAD) return;
-
-    window.USER_CANNED_RESPONSES_STARTED_TO_LOAD = true;
-    let searchParams = new URLSearchParams(document.location.search);
-    let authuser = searchParams.get('authuser') ?? 0;
-
-    // @TODO: This isn't as simple as doing this because the request contains
-    // the wrong origin and fails.
-    CCApi('ListCannedResponses', {}, true, authuser).then(res => {
-      console.log(res);
-    });
-  }
-
   _dispatchUpdateEvent() {
     // Request an update for this component
     this.requestUpdate();
@@ -95,6 +110,16 @@
     this.markAsAnswer = this.markAsAnswerRef.value.selected;
   }
 
+  _openCRImporter() {
+    if (!(this._importerWindow?.closed ?? true))
+      this._importerWindow?.close?.();
+
+    this._importerWindow = window.open(
+        'https://support.google.com/s/community/cannedresponses?TWPTImportToWorkflow&TWPTSelectedId=' +
+            encodeURIComponent(this._cannedResponseIdString),
+        '', 'popup,width=720,height=540');
+  }
+
   get cannedResponseId() {
     return this.action.getCannedResponseId() ?? 0;
   }
diff --git a/templates/manifest.gjson b/templates/manifest.gjson
index 1a23c85..41d39e2 100644
--- a/templates/manifest.gjson
+++ b/templates/manifest.gjson
@@ -96,6 +96,7 @@
         "css/image_max_height.css",
         "css/extrainfo.css",
         "css/extrainfo_perforumstats.css",
+        "css/workflow_import.css",
         "css/ui_spacing/shared.css",
         "css/ui_spacing/console.css",
         "css/ui_spacing/twbasic.css",