feat(workflows): add reorder buttons to the workflow manager

This change allows users to reorder workflows in the workflow manager by
exposing move up/down buttons next to each workflow.

Fixed: twpowertools:152
Change-Id: I4298344ec7a296f125c30ba5762fb5f0c8e632a0
diff --git a/src/features/workflows/core/manager/components/List.js b/src/features/workflows/core/manager/components/List.js
index 9f7ed5f..4621a8e 100644
--- a/src/features/workflows/core/manager/components/List.js
+++ b/src/features/workflows/core/manager/components/List.js
@@ -36,13 +36,26 @@
   dialogRef = createRef();
 
   renderListItems() {
-    return map(this.workflows, w => html`
+    return map(this.workflows, (w, index) => html`
       <md-list-item
           type="button"
           @click=${() => this._show(w)}>
         <div slot="headline">${w.proto?.getName?.()}</div>
         <div slot="end" class="end">
           <md-icon-button
+              aria-label="Move up"
+              ?soft-disabled=${index === 0}
+              @click=${e => this._moveUp(w.uuid, e)}>
+            <md-icon>arrow_upward</md-icon>
+          </md-icon-button>
+          <md-icon-button
+              aria-label="Move down"
+              ?soft-disabled=${index === this.workflows.length - 1}
+              @click=${e => this._moveDown(w.uuid, e)}>
+            <md-icon>arrow_downward</md-icon>
+          </md-icon-button>
+          <md-icon-button
+              aria-label="Delete"
               @click=${e => this._showDelete(w.uuid, e)}>
             <md-icon>delete</md-icon>
           </md-icon-button>
@@ -87,6 +100,16 @@
     this.dialogRef.value.open = true;
   }
 
+  _moveUp(uuid, e) {
+    e.stopPropagation();
+    WorkflowsStorage.moveUp(uuid);
+  }
+
+  _moveDown(uuid, e) {
+    e.stopPropagation();
+    WorkflowsStorage.moveDown(uuid);
+  }
+
   _showDelete(uuid, e) {
     e.stopPropagation();
     const proceed = window.confirm(
diff --git a/src/features/workflows/core/workflowsStorage.js b/src/features/workflows/core/workflowsStorage.js
index 3fc6f95..43ffd2b 100644
--- a/src/features/workflows/core/workflowsStorage.js
+++ b/src/features/workflows/core/workflowsStorage.js
@@ -97,6 +97,38 @@
     });
   }
 
+  static async moveUp(uuid) {
+    await this.moveToRelativePosition(uuid, -1);
+  }
+
+  static async moveDown(uuid) {
+    await this.moveToRelativePosition(uuid, 1);
+  }
+
+  /**
+   * Swaps the workflow with the one saved in the specified position relative to
+   * the workflow's position. Example: position = 1 swaps the workflow with the
+   * next workflow, so it appears afterwards.
+   */
+  static async moveToRelativePosition(uuid, relativePosition) {
+    const workflows = await this.getAll();
+    const index = workflows.findIndex((workflow) => workflow.uuid === uuid);
+    if (index === -1) {
+      throw new Error(
+          'Couldn\'t move the workflow because it couldn\'t be found.',
+      );
+    }
+    if (workflows[index + relativePosition] === undefined) {
+      throw new Error(
+          'Couldn\'t move the workflow because the specified relative position is out of bounds.',
+      );
+    }
+    [workflows[index], workflows[index + relativePosition]] =
+        [workflows[index + relativePosition], workflows[index]];
+    const items = {[kWorkflowsDataKey]: workflows};
+    await chrome.storage.local.set(items);
+  }
+
   static _proto2Base64(workflow) {
     const binaryWorkflow = workflow.serializeBinary();
     return arrayBufferToBase64(binaryWorkflow);