Add initial version of the workflows manager

The workflows manager can be accessed from the extension options, and
will let the user add custom workflows, edit and view the already
created ones, and delete them.

As of now, the only feature implemented is a functional prototype of the
"new workflow" dialog, which is missing the workflow title field and the
functionality to persist the designed workflows. The only action
supported in the workflows editor is the "reply with canned response"
action.

Bug: twpowertools:74
Change-Id: I0d93bf0fdcda92a449855c1f8470f8b7068839aa
diff --git a/src/workflows/manager/components/ActionEditor.js b/src/workflows/manager/components/ActionEditor.js
new file mode 100644
index 0000000..fbd0bed
--- /dev/null
+++ b/src/workflows/manager/components/ActionEditor.js
@@ -0,0 +1,253 @@
+import './actions/ReplyWithCR.js';
+
+import {css, html, LitElement, nothing} from 'lit';
+import {map} from 'lit/directives/map.js';
+import {createRef, ref} from 'lit/directives/ref.js';
+
+import * as pb from '../../proto/main_pb.js';
+
+// TODO: remove this and substitute it with proper localization.
+const kActionHeadings = {
+  0: 'Unknown action',
+  1: 'Reply',
+  2: 'Move to a forum',
+  3: 'Mark as duplicate of a thread',
+  4: 'Unmark duplicate',
+  5: 'Change thread attributes',
+  6: 'Reply with canned response',
+  16: 'Star/unstar thread',
+  17: 'Subscribe/unsubscribe to thread',
+  18: 'Vote thread',
+  19: 'Report thread',
+};
+const kSupportedActions = new Set([6]);
+const actionCases = Object.entries(pb.workflows.Action.ActionCase);
+
+export default class WFActionEditor extends LitElement {
+  static properties = {
+    action: {type: Object},
+    readOnly: {type: Boolean},
+    disableRemoveButton: {type: Boolean},
+    step: {type: Number},
+  };
+
+  static styles = css`
+    .action {
+      margin-bottom: 20px;
+    }
+
+    .header {
+      display: flex;
+      align-items: center;
+      margin-bottom: 8px;
+    }
+
+    .step {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      min-height: 30px;
+      min-width: 30px;
+      margin-inline-end: 8px;
+
+      font-weight: 500;
+      font-size: 18px;
+
+      border-radius: 50%;
+      color: white;
+      background-color: #018786;
+    }
+
+    .title {
+      font-weight: 500;
+      margin: 0;
+      flex-grow: 1;
+    }
+
+    .header .select {
+      flex-grow: 1;
+      width: 300px;
+      padding: 4px;
+      margin-inline-end: 8px;
+      font-size: 16px;
+    }
+  `;
+
+  selectRef = createRef();
+
+  constructor() {
+    super();
+    this.action = new pb.workflows.Action();
+    this.readOnly = false;
+  }
+
+  renderActionTitle() {
+    if (this.readOnly) return html`<h3 class="title">${this._stepTitle()}</h3>`;
+
+    let selectedActionCase = this._actionCase;
+
+    return html`
+      <select ${ref(this.selectRef)}
+          class="select"
+          @change=${this._actionCaseChanged}>
+        ${map(actionCases, ([actionName, num]) => {
+      if (num == 0) return nothing;
+      return html`
+          <option value=${num} ?selected=${selectedActionCase == num}>
+            ${kActionHeadings[num] ?? actionName}
+          </option>
+        `;
+    })}
+      </select>
+    `;
+  }
+
+  renderSpecificActionEditor() {
+    switch (this._actionCase) {
+      case pb.workflows.Action.ActionCase.REPLY_WITH_CR_ACTION:
+        return html`
+          <wf-action-reply-with-cr
+              ?readOnly=${this.readOnly}
+              .action=${this.action.getReplyWithCrAction()}>
+          </wf-action-reply-with-cr>
+        `;
+
+      default:
+        return html`<p>This action has not yet been implemented.</p>`;
+    }
+  }
+
+  render() {
+    return [
+      html`
+        <div class="action">
+          <div class="header">
+            <div class="step">${this.step}</div>
+            ${this.renderActionTitle()}
+            ${
+          !this.readOnly ?
+              html`
+                <button
+                    ?disabled=${this.disableRemoveButton}
+                    @click=${this._remove}>
+                  Remove
+                </button>
+              ` :
+              nothing}
+          </div>
+          ${this.renderSpecificActionEditor()}
+        </div>
+      `,
+    ];
+  }
+
+  checkValidity() {
+    if (this.readOnly || !kSupportedActions.has(this._actionCase)) return true;
+    return this._specificActionEditor().checkValidity();
+  }
+
+  _actionCaseChanged() {
+    this._actionCaseString = this.selectRef.value.value;
+  }
+
+  _dispatchUpdateEvent() {
+    // Transmit to other components that the action has changed
+    const e = new Event('action-updated', {bubbles: true, composed: true});
+    this.renderRoot.dispatchEvent(e);
+  }
+
+  _remove() {
+    // Transmit to other components that the action has to be removed
+    const e = new Event('action-removed', {bubbles: true, composed: true});
+    this.renderRoot.dispatchEvent(e);
+  }
+
+  _stepTitle() {
+    return kActionHeadings[this._actionCase] ?? this._actionCase;
+  }
+
+  get _actionCase() {
+    return this.action.getActionCase();
+  }
+
+  set _actionCase(newCase) {
+    let value;
+    switch (newCase) {
+      case pb.workflows.Action.ActionCase.REPLY_ACTION:
+        value = new pb.workflows.Action.ReplyAction;
+        this.action.setReplyAction(value);
+        break;
+      case pb.workflows.Action.ActionCase.MOVE_ACTION:
+        value = new pb.workflows.Action.MoveAction;
+        this.action.setMoveAction(value);
+        break;
+      case pb.workflows.Action.ActionCase.MARK_DUPLICATE_ACTION:
+        value = new pb.workflows.Action.MarkDuplicateAction;
+        this.action.setMarkDuplicateAction(value);
+        break;
+      case pb.workflows.Action.ActionCase.UNMARK_DUPLICATE_ACTION:
+        value = new pb.workflows.Action.UnmarkDuplicateAction;
+        this.action.setUnmarkDuplicateAction(value);
+        break;
+      case pb.workflows.Action.ActionCase.ATTRIBUTE_ACTION:
+        value = new pb.workflows.Action.AttributeAction;
+        this.action.setAttributeAction(value);
+        break;
+      case pb.workflows.Action.ActionCase.REPLY_WITH_CR_ACTION:
+        value = new pb.workflows.Action.ReplyWithCRAction;
+        this.action.setReplyWithCrAction(value);
+        break;
+      case pb.workflows.Action.ActionCase.STAR_ACTION:
+        value = new pb.workflows.Action.StarAction;
+        this.action.setStarAction(value);
+        break;
+      case pb.workflows.Action.ActionCase.SUBSCRIBE_ACTION:
+        value = new pb.workflows.Action.SubscribeAction;
+        this.action.setSubscribeAction(value);
+        break;
+      case pb.workflows.Action.ActionCase.VOTE_ACTION:
+        value = new pb.workflows.Action.VoteAction;
+        this.action.setVoteAction(value);
+        break;
+      case pb.workflows.Action.ActionCase.REPORT_ACTION:
+        value = new pb.workflows.Action.ReportAction;
+        this.action.setReportAction(value);
+        break;
+      default:
+        this.action.clearReplyAction();
+        this.action.clearMoveAction();
+        this.action.clearMarkDuplicateAction();
+        this.action.clearUnmarkDuplicateAction();
+        this.action.clearAttributeAction();
+        this.action.clearReplyWithCrAction();
+        this.action.clearStarAction();
+        this.action.clearSubscribeAction();
+        this.action.clearVoteAction();
+        this.action.clearReportAction();
+    }
+
+    this.requestUpdate();
+    this._dispatchUpdateEvent();
+  }
+
+  // The same as _actionCase, but represented as a String instead of a Number
+  get _actionCaseString() {
+    return this._actionCase.toString();
+  }
+
+  set _actionCaseString(newCase) {
+    this._actionCase = parseInt(newCase);
+  }
+
+  _specificActionEditor() {
+    switch (this._actionCase) {
+      case pb.workflows.Action.ActionCase.REPLY_WITH_CR_ACTION:
+        return this.renderRoot.querySelector('wf-action-reply-with-cr');
+
+      default:
+        return null;
+    }
+  }
+}
+window.customElements.define('wf-action-editor', WFActionEditor);