feat(workflows): add "attribute action" action

This action lets users perform several actions on threads, such as
(un)lock, (un)set as trending, (un)pin, set as non-issue, obsolete, etc.

The action selector shows the action with the codename it has in the
Protobuf enum. We will show a friendly string when we localize the
feature.

Bug: twpowertools:74

Change-Id: I95f9f1904ffe559c92a786cbdb327613c8ca32ca
diff --git a/src/workflows/manager/components/ActionEditor.js b/src/workflows/manager/components/ActionEditor.js
index 9a57509..cdcb85a 100644
--- a/src/workflows/manager/components/ActionEditor.js
+++ b/src/workflows/manager/components/ActionEditor.js
@@ -1,5 +1,5 @@
+import './actions/Attribute.js';
 import './actions/ReplyWithCR.js';
-
 import '@material/mwc-circular-progress/mwc-circular-progress.js';
 
 import {html, LitElement, nothing} from 'lit';
@@ -61,6 +61,14 @@
           </wf-action-reply-with-cr>
         `;
 
+      case pb.workflows.Action.ActionCase.ATTRIBUTE_ACTION:
+        return html`
+          <wf-action-attribute
+              ?readOnly=${this.readOnly}
+              .action=${this.action.getAttributeAction()}>
+          </wf-action-attribute>
+        `;
+
       case pb.workflows.Action.ActionCase.MARK_AS_READ_ACTION:
       case pb.workflows.Action.ActionCase.MARK_AS_UNREAD_ACTION:
         return nothing;
@@ -216,6 +224,9 @@
       case pb.workflows.Action.ActionCase.REPLY_WITH_CR_ACTION:
         return this.renderRoot.querySelector('wf-action-reply-with-cr');
 
+      case pb.workflows.Action.ActionCase.ATTRIBUTE_ACTION:
+        return this.renderRoot.querySelector('wf-action-attribute');
+
       default:
         return null;
     }
diff --git a/src/workflows/manager/components/actions/Attribute.js b/src/workflows/manager/components/actions/Attribute.js
new file mode 100644
index 0000000..150e7da
--- /dev/null
+++ b/src/workflows/manager/components/actions/Attribute.js
@@ -0,0 +1,89 @@
+import '@material/web/select/outlined-select.js';
+import '@material/web/select/select-option.js';
+
+import {html, LitElement} from 'lit';
+import {createRef, ref} from 'lit/directives/ref.js';
+
+import {SHARED_MD3_STYLES} from '../../../../common/styles/md3.js';
+import * as pb from '../../../proto/main_pb.js';
+
+import {FORM_STYLES} from './common.js';
+
+const kHiddenActions = [
+  pb.workflows.Action.AttributeAction.AttributeAction.AA_NONE,
+  pb.workflows.Action.AttributeAction.AttributeAction.AA_SOFT_LOCK,
+  pb.workflows.Action.AttributeAction.AttributeAction.AA_UNSOFT_LOCK,
+];
+
+export default class WFActionAttribute extends LitElement {
+  static properties = {
+    action: {type: Object},
+    readOnly: {type: Boolean},
+  };
+
+  static styles = [
+    SHARED_MD3_STYLES,
+    FORM_STYLES,
+  ];
+
+  attributeActionRef = createRef();
+
+  constructor() {
+    super();
+    this.action = new pb.workflows.Action.AttributeAction;
+  }
+
+  render() {
+    return html`
+      <div class="form-line">
+        <md-outlined-select ${ref(this.attributeActionRef)}
+            required
+            label="Action"
+            value=${this.action}
+            ?disabled=${this.readOnly}
+            @change=${this._attributeActionChanged}>
+          ${this.renderAttributeActions()}
+        </md-outlined-select>
+      </div>
+    `;
+  }
+
+  renderAttributeActions() {
+    const attributeActions =
+        Object.entries(pb.workflows.Action.AttributeAction.AttributeAction);
+    return attributeActions.filter(([, id]) => !kHiddenActions.includes(id))
+        .map(([actionCodename, id]) => html`
+      <md-select-option value=${id}>
+        <div slot="headline">${actionCodename}</div>
+      </md-select-option>
+    `);
+  }
+
+  checkValidity() {
+    return this.attributeActionRef.value.reportValidity();
+  }
+
+  _dispatchUpdateEvent() {
+    // Request an update for this component
+    this.requestUpdate();
+
+    // Transmit to other components that the action has changed
+    const e =
+        new Event('attribute-action-updated', {bubbles: true, composed: true});
+    this.renderRoot.dispatchEvent(e);
+  }
+
+  _attributeActionChanged() {
+    this.attributeAction = this.attributeActionRef.value.value;
+  }
+
+  get attributeAction() {
+    return this.action.getAttributeAction();
+  }
+
+  set attributeAction(value) {
+    this.action.setAttributeAction(value);
+    this._dispatchUpdateEvent();
+  }
+}
+window.customElements.define('wf-action-attribute', WFActionAttribute);
diff --git a/src/workflows/manager/components/actions/ReplyWithCR.js b/src/workflows/manager/components/actions/ReplyWithCR.js
index 40a6b9c..e97f6f4 100644
--- a/src/workflows/manager/components/actions/ReplyWithCR.js
+++ b/src/workflows/manager/components/actions/ReplyWithCR.js
@@ -8,6 +8,7 @@
 
 import {SHARED_MD3_STYLES} from '../../../../common/styles/md3.js';
 import * as pb from '../../../proto/main_pb.js';
+import { FORM_STYLES } from './common.js';
 
 export default class WFActionReplyWithCR extends LitElement {
   static properties = {
@@ -18,15 +19,8 @@
 
   static styles = [
     SHARED_MD3_STYLES,
+    FORM_STYLES,
     css`
-      .form-line {
-        display: flex;
-        flex-direction: row;
-        align-items: center;
-        margin-block: 1em;
-        gap: .5rem;
-      }
-
       .select-cr-btn {
         --md-outlined-button-icon-size: 24px;
       }
diff --git a/src/workflows/manager/components/actions/common.js b/src/workflows/manager/components/actions/common.js
new file mode 100644
index 0000000..3d5be56
--- /dev/null
+++ b/src/workflows/manager/components/actions/common.js
@@ -0,0 +1,11 @@
+import {css} from 'lit';
+
+export const FORM_STYLES = css`
+  .form-line {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    margin-block: 1em;
+    gap: .5rem;
+  }
+`;