Implement workflow execution UI and logic

This CL adds a provisional workflow execution UI (which will need to be
thoroughly improved in the future), and most importantly the logic for
running workflows and actions inside workflows.

Bug: twpowertools:74
Change-Id: I94944a623a2411bef9d2b5244fea707e69a49790
diff --git a/src/contentScripts/communityConsole/workflows/actionRunners/replyWithCR.js b/src/contentScripts/communityConsole/workflows/actionRunners/replyWithCR.js
new file mode 100644
index 0000000..e8f665c
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/actionRunners/replyWithCR.js
@@ -0,0 +1,66 @@
+import {CCApi} from '../../../../common/api.js';
+import {getAuthUser} from '../../../../common/communityConsoleUtils.js';
+
+const kPiiScanType_ScanNone = 0;
+const kType_Reply = 1;
+const kType_RecommendedAnswer = 3;
+const kPostMethodCommunityConsole = 4;
+
+export default class CRRunner {
+  constructor() {
+    this._CRs = [];
+    this._haveCRsBeenLoaded = false;
+  }
+
+  loadCRs() {
+    return CCApi(
+               'ListCannedResponses', {}, /* authenticated = */ true,
+               getAuthUser())
+        .then(res => {
+          this._CRs = res?.[1] ?? [];
+          this._haveCRsBeenLoaded = true;
+        });
+  }
+
+  _getCRPayload(id) {
+    let maybeLoadCRsPromise;
+    if (!this._haveCRsBeenLoaded)
+      maybeLoadCRsPromise = this.loadCRs();
+    else
+      maybeLoadCRsPromise = Promise.resolve();
+
+    return maybeLoadCRsPromise.then(() => {
+      let cr = this._CRs.find(cr => cr?.[1]?.[1] == id);
+      if (!cr) throw new Error(`Couldn't find CR with id ${id}.`);
+      return cr?.[3];
+    });
+  }
+
+  execute(action, thread) {
+    let crId = action?.getCannedResponseId?.();
+    if (!crId)
+      return Promise.reject(
+          new Error('The action doesn\'t contain a valid CR id.'));
+
+    return this._getCRPayload(crId).then(payload => {
+      let subscribe = action?.getSubscribe?.() ?? false;
+      let markAsAnswer = action?.getMarkAsAnswer?.() ?? false;
+      return CCApi(
+          'CreateMessage', {
+            1: thread.forum,   // forumId
+            2: thread.thread,  // threadId
+            // message
+            3: {
+              4: payload,
+              6: {
+                1: markAsAnswer ? kType_RecommendedAnswer : kType_Reply,
+              },
+              11: kPostMethodCommunityConsole,
+            },
+            4: subscribe,
+            6: kPiiScanType_ScanNone,
+          },
+          /* authenticated = */ true, getAuthUser());
+    });
+  }
+}