Browse Source

feat: update workflow engine

Ahmad Kholid 3 years ago
parent
commit
4af43d2dca
50 changed files with 633 additions and 762 deletions
  1. 9 5
      src/background/index.js
  2. 2 5
      src/background/workflowEngine/blocksHandler/handlerActiveTab.js
  3. 30 26
      src/background/workflowEngine/blocksHandler/handlerBlocksGroup.js
  4. 14 23
      src/background/workflowEngine/blocksHandler/handlerBrowserEvent.js
  5. 23 32
      src/background/workflowEngine/blocksHandler/handlerClipboard.js
  6. 13 22
      src/background/workflowEngine/blocksHandler/handlerCloseTab.js
  7. 7 8
      src/background/workflowEngine/blocksHandler/handlerConditions.js
  8. 1 3
      src/background/workflowEngine/blocksHandler/handlerDelay.js
  9. 2 4
      src/background/workflowEngine/blocksHandler/handlerDeleteData.js
  10. 4 9
      src/background/workflowEngine/blocksHandler/handlerElementExists.js
  11. 58 60
      src/background/workflowEngine/blocksHandler/handlerExecuteWorkflow.js
  12. 36 45
      src/background/workflowEngine/blocksHandler/handlerExportData.js
  13. 7 16
      src/background/workflowEngine/blocksHandler/handlerForwardPage.js
  14. 7 16
      src/background/workflowEngine/blocksHandler/handlerGoBack.js
  15. 34 44
      src/background/workflowEngine/blocksHandler/handlerGoogleSheets.js
  16. 3 5
      src/background/workflowEngine/blocksHandler/handlerHandleDialog.js
  17. 2 3
      src/background/workflowEngine/blocksHandler/handlerHandleDownload.js
  18. 28 36
      src/background/workflowEngine/blocksHandler/handlerHoverElement.js
  19. 2 3
      src/background/workflowEngine/blocksHandler/handlerInsertData.js
  20. 1 5
      src/background/workflowEngine/blocksHandler/handlerInteractionBlock.js
  21. 36 46
      src/background/workflowEngine/blocksHandler/handlerJavascriptCode.js
  22. 2 4
      src/background/workflowEngine/blocksHandler/handlerLoopBreakpoint.js
  23. 3 8
      src/background/workflowEngine/blocksHandler/handlerLoopData.js
  24. 75 88
      src/background/workflowEngine/blocksHandler/handlerNewTab.js
  25. 14 23
      src/background/workflowEngine/blocksHandler/handlerNewWindow.js
  26. 27 36
      src/background/workflowEngine/blocksHandler/handlerNotification.js
  27. 2 3
      src/background/workflowEngine/blocksHandler/handlerProxy.js
  28. 7 16
      src/background/workflowEngine/blocksHandler/handlerReloadTab.js
  29. 3 5
      src/background/workflowEngine/blocksHandler/handlerRepeatTask.js
  30. 35 44
      src/background/workflowEngine/blocksHandler/handlerSaveAssets.js
  31. 3 3
      src/background/workflowEngine/blocksHandler/handlerSwitchTab.js
  32. 2 2
      src/background/workflowEngine/blocksHandler/handlerSwitchTo.js
  33. 7 7
      src/background/workflowEngine/blocksHandler/handlerTakeScreenshot.js
  34. 1 5
      src/background/workflowEngine/blocksHandler/handlerTrigger.js
  35. 9 6
      src/background/workflowEngine/blocksHandler/handlerWaitConnections.js
  36. 3 4
      src/background/workflowEngine/blocksHandler/handlerWebhook.js
  37. 6 4
      src/background/workflowEngine/blocksHandler/handlerWhileLoop.js
  38. 26 12
      src/background/workflowEngine/engine.js
  39. 2 4
      src/background/workflowEngine/helper.js
  40. 26 30
      src/background/workflowEngine/worker.js
  41. 2 4
      src/components/block/BlockRepeatTask.vue
  42. 6 10
      src/components/newtab/workflow/editor/EditorLocalActions.vue
  43. 4 0
      src/components/newtab/workflows/WorkflowsHosted.vue
  44. 4 0
      src/components/newtab/workflows/WorkflowsLocal.vue
  45. 5 0
      src/components/newtab/workflows/WorkflowsShared.vue
  46. 2 2
      src/content/index.js
  47. 13 16
      src/newtab/pages/logs/[id].vue
  48. 18 7
      src/newtab/pages/workflows/[id].vue
  49. 6 2
      src/utils/convertWorkflowData.js
  50. 1 1
      src/utils/helper.js

+ 9 - 5
src/background/index.js

@@ -5,6 +5,7 @@ import { parseJSON, findTriggerBlock, sleep } from '@/utils/helper';
 import { fetchApi } from '@/utils/api';
 import getFile from '@/utils/getFile';
 import decryptFlow, { getWorkflowPass } from '@/utils/decryptFlow';
+import convertWorkflowData from '@/utils/convertWorkflowData';
 import {
   registerSpecificDay,
   registerContextMenu,
@@ -72,7 +73,6 @@ const workflow = {
   },
   execute(workflowData, options) {
     if (workflowData.isDisabled) return null;
-
     if (workflowData.isProtected) {
       const flow = parseJSON(workflowData.drawflow, null);
 
@@ -83,7 +83,8 @@ const workflow = {
       }
     }
 
-    const engine = new WorkflowEngine(workflowData, {
+    const convertedWorkflow = convertWorkflowData(workflowData);
+    const engine = new WorkflowEngine(convertedWorkflow, {
       options,
       blocksHandler,
       logger: this.logger,
@@ -384,10 +385,13 @@ browser.runtime.onInstalled.addListener(async ({ reason }) => {
     }
 
     if (reason === 'update') {
-      const { workflows } = await browser.storage.local.get('workflows');
+      let { workflows } = await browser.storage.local.get('workflows');
       const alarmTypes = ['specific-day', 'date', 'interval'];
 
-      for (const { trigger, drawflow, id } of workflows) {
+      workflows = Array.isArray(workflows)
+        ? workflows
+        : Object.values(workflows);
+      workflows.forEach(({ trigger, drawflow, id }) => {
         let workflowTrigger = trigger?.data || trigger;
 
         if (!trigger) {
@@ -402,7 +406,7 @@ browser.runtime.onInstalled.addListener(async ({ reason }) => {
         } else if (triggerType === 'context-menu') {
           registerContextMenu(id, workflowTrigger);
         }
-      }
+      });
     }
   } catch (error) {
     console.error(error);

+ 2 - 5
src/background/workflowEngine/blocksHandler/handlerActiveTab.js

@@ -1,13 +1,11 @@
 import browser from 'webextension-polyfill';
-import { getBlockConnection, attachDebugger } from '../helper';
+import { attachDebugger } from '../helper';
 
 async function activeTab(block) {
-  const nextBlockId = getBlockConnection(block);
-
   try {
     const data = {
-      nextBlockId,
       data: '',
+      nextBlockId: this.getBlockConnections(block.id),
     };
 
     if (this.activeTab.id) {
@@ -51,7 +49,6 @@ async function activeTab(block) {
     return data;
   } catch (error) {
     console.error(error);
-    error.nextBlockId = nextBlockId;
     error.data = error.data || {};
 
     throw error;

+ 30 - 26
src/background/workflowEngine/blocksHandler/handlerBlocksGroup.js

@@ -1,8 +1,6 @@
-import { getBlockConnection } from '../helper';
-
-function blocksGroup({ data, outputs }, { prevBlockData }) {
+function blocksGroup({ data, id }, { prevBlockData }) {
   return new Promise((resolve) => {
-    const nextBlockId = getBlockConnection({ outputs });
+    const nextBlockId = this.getBlockConnections(id);
 
     if (data.blocks.length === 0) {
       resolve({
@@ -13,34 +11,40 @@ function blocksGroup({ data, outputs }, { prevBlockData }) {
       return;
     }
 
-    const blocks = data.blocks.reduce((acc, block, index) => {
-      let nextBlock = {
-        connections: [{ node: data.blocks[index + 1]?.itemId }],
-      };
-
-      if (index === data.blocks.length - 1) {
-        nextBlock = nextBlockId;
-      }
-
-      acc[block.itemId] = {
-        ...block,
-        id: block.itemId,
-        name: block.id,
-        outputs: {
-          output_1: nextBlock,
+    if (!this.engine.extractedGroup[id]) {
+      const { blocks, connections } = data.blocks.reduce(
+        (acc, block, index) => {
+          const nextBlock = data.blocks[index + 1]?.itemId;
+
+          acc.blocks[block.itemId] = {
+            label: block.id,
+            data: block.data,
+            id: nextBlock ? block.itemId : id,
+          };
+
+          if (nextBlock) {
+            const outputId = `${block.itemId}-output-1`;
+
+            if (!acc.connections[outputId]) {
+              acc.connections[outputId] = [];
+            }
+            acc.connections[outputId].push(nextBlock);
+          }
+
+          return acc;
         },
-      };
+        { blocks: {}, connections: {} }
+      );
 
-      return acc;
-    }, {});
+      Object.assign(this.engine.blocks, blocks);
+      Object.assign(this.engine.connectionsMap, connections);
 
-    Object.assign(this.engine.blocks, blocks);
+      this.engine.extractedGroup[id] = true;
+    }
 
     resolve({
       data: prevBlockData,
-      nextBlockId: {
-        connections: [{ node: data.blocks[0].itemId }],
-      },
+      nextBlockId: this.getBlockConnections(data.blocks[0].itemId),
     });
   });
 }

+ 14 - 23
src/background/workflowEngine/blocksHandler/handlerBrowserEvent.js

@@ -1,6 +1,5 @@
 import browser from 'webextension-polyfill';
 import { isWhitespace } from '@/utils/helper';
-import { getBlockConnection } from '../helper';
 
 function handleEventListener(target, validate) {
   return (data, activeTab) => {
@@ -103,30 +102,22 @@ const events = {
   'window:close': handleEventListener(browser.windows.onRemoved),
 };
 
-export default async function ({ data, outputs }) {
-  const nextBlockId = getBlockConnection({ outputs });
+export default async function ({ data, id }) {
+  const currentEvent = events[data.eventName];
 
-  try {
-    const currentEvent = events[data.eventName];
-
-    if (!currentEvent) {
-      throw new Error(`Can't find ${data.eventName} event`);
-    }
-
-    const result = await currentEvent(data, this.activeTab);
+  if (!currentEvent) {
+    throw new Error(`Can't find ${data.eventName} event`);
+  }
 
-    if (data.eventName === 'tab:create' && data.setAsActiveTab) {
-      this.activeTab.id = result.tabId;
-      this.activeTab.url = result.url;
-    }
+  const result = await currentEvent(data, this.activeTab);
 
-    return {
-      nextBlockId,
-      data: result || '',
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
-
-    throw error;
+  if (data.eventName === 'tab:create' && data.setAsActiveTab) {
+    this.activeTab.id = result.tabId;
+    this.activeTab.url = result.url;
   }
+
+  return {
+    data: result || '',
+    nextBlockId: this.getBlockConnections(id),
+  };
 }

+ 23 - 32
src/background/workflowEngine/blocksHandler/handlerClipboard.js

@@ -1,41 +1,32 @@
 import browser from 'webextension-polyfill';
-import { getBlockConnection } from '../helper';
 
-export default async function ({ data, outputs }) {
-  const nextBlockId = getBlockConnection({ outputs });
+export default async function ({ data, id }) {
+  const hasPermission = await browser.permissions.contains({
+    permissions: ['clipboardRead'],
+  });
 
-  try {
-    const hasPermission = await browser.permissions.contains({
-      permissions: ['clipboardRead'],
-    });
-
-    if (!hasPermission) {
-      throw new Error('no-clipboard-acces');
-    }
-
-    const textarea = document.createElement('textarea');
-    document.body.appendChild(textarea);
-    textarea.focus();
-    document.execCommand('paste');
+  if (!hasPermission) {
+    throw new Error('no-clipboard-acces');
+  }
 
-    const copiedText = textarea.value;
+  const textarea = document.createElement('textarea');
+  document.body.appendChild(textarea);
+  textarea.focus();
+  document.execCommand('paste');
 
-    if (data.assignVariable) {
-      this.setVariable(data.variableName, copiedText);
-    }
-    if (data.saveData) {
-      this.addDataToColumn(data.dataColumn, copiedText);
-    }
+  const copiedText = textarea.value;
 
-    document.body.removeChild(textarea);
+  if (data.assignVariable) {
+    this.setVariable(data.variableName, copiedText);
+  }
+  if (data.saveData) {
+    this.addDataToColumn(data.dataColumn, copiedText);
+  }
 
-    return {
-      nextBlockId,
-      data: copiedText,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
+  document.body.removeChild(textarea);
 
-    throw error;
-  }
+  return {
+    data: copiedText,
+    nextBlockId: this.getBlockConnections(id),
+  };
 }

+ 13 - 22
src/background/workflowEngine/blocksHandler/handlerCloseTab.js

@@ -1,5 +1,4 @@
 import browser from 'webextension-polyfill';
-import { getBlockConnection } from '../helper';
 
 async function closeWindow(data, windowId) {
   const windowIds = [];
@@ -37,29 +36,21 @@ async function closeTab(data, tabId) {
   if (tabIds) await browser.tabs.remove(tabIds);
 }
 
-export default async function ({ data, outputs }) {
-  const nextBlockId = getBlockConnection({ outputs });
+export default async function ({ data, id }) {
+  if (data.closeType === 'window') {
+    await closeWindow(data, this.windowId);
 
-  try {
-    if (data.closeType === 'window') {
-      await closeWindow(data, this.windowId);
-
-      this.windowId = null;
-    } else {
-      await closeTab(data, this.activeTab.id);
+    this.windowId = null;
+  } else {
+    await closeTab(data, this.activeTab.id);
 
-      if (data.activeTab) {
-        this.activeTab.id = null;
-      }
+    if (data.activeTab) {
+      this.activeTab.id = null;
     }
-
-    return {
-      nextBlockId,
-      data: '',
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
-
-    throw error;
   }
+
+  return {
+    data: '',
+    nextBlockId: this.getBlockConnections(id),
+  };
 }

+ 7 - 8
src/background/workflowEngine/blocksHandler/handlerConditions.js

@@ -1,7 +1,6 @@
 import compareBlockValue from '@/utils/compareBlockValue';
 import mustacheReplacer from '@/utils/referenceData/mustacheReplacer';
 import testConditions from '@/utils/testConditions';
-import { getBlockConnection } from '../helper';
 
 function checkConditions(data, conditionOptions) {
   return new Promise((resolve, reject) => {
@@ -42,14 +41,14 @@ function checkConditions(data, conditionOptions) {
   });
 }
 
-async function conditions({ data, outputs, id }, { prevBlockData, refData }) {
+async function conditions({ data, id }, { prevBlockData, refData }) {
   if (data.conditions.length === 0) {
     throw new Error('conditions-empty');
   }
 
   let resultData = '';
   let isConditionMet = false;
-  let outputIndex = data.conditions.length + 1;
+  let outputId = 'fallback';
 
   const replacedValue = {};
   const condition = data.conditions[0];
@@ -62,7 +61,7 @@ async function conditions({ data, outputs, id }, { prevBlockData, refData }) {
       refData,
       activeTab: this.activeTab.id,
       sendMessage: (payload) =>
-        this._sendMessageToTab({ ...payload.data, name: 'conditions', id }),
+        this._sendMessageToTab({ ...payload.data, label: 'conditions', id }),
     };
 
     const conditionsResult = await checkConditions(data, conditionPayload);
@@ -72,10 +71,10 @@ async function conditions({ data, outputs, id }, { prevBlockData, refData }) {
     }
     if (conditionsResult.match) {
       isConditionMet = true;
-      outputIndex = conditionsResult.index + 1;
+      outputId = data.conditions[conditionsResult.index].id;
     }
   } else {
-    data.conditions.forEach(({ type, value, compareValue }, index) => {
+    data.conditions.forEach(({ type, value, compareValue, id: itemId }) => {
       if (isConditionMet) return;
 
       const firstValue = mustacheReplacer(
@@ -93,8 +92,8 @@ async function conditions({ data, outputs, id }, { prevBlockData, refData }) {
       );
 
       if (isMatch) {
+        outputId = itemId;
         resultData = value;
-        outputIndex = index + 1;
         isConditionMet = true;
       }
     });
@@ -103,7 +102,7 @@ async function conditions({ data, outputs, id }, { prevBlockData, refData }) {
   return {
     replacedValue,
     data: resultData,
-    nextBlockId: getBlockConnection({ outputs }, outputIndex),
+    nextBlockId: this.getBlockConnections(id, outputId),
   };
 }
 

+ 1 - 3
src/background/workflowEngine/blocksHandler/handlerDelay.js

@@ -1,5 +1,3 @@
-import { getBlockConnection } from '../helper';
-
 function delay(block) {
   return new Promise((resolve) => {
     const delayTime = +block.data.time || 500;
@@ -7,7 +5,7 @@ function delay(block) {
     setTimeout(() => {
       resolve({
         data: '',
-        nextBlockId: getBlockConnection(block),
+        nextBlockId: this.getBlockConnections(block.id),
       });
     }, delayTime);
   });

+ 2 - 4
src/background/workflowEngine/blocksHandler/handlerDeleteData.js

@@ -1,6 +1,4 @@
-import { getBlockConnection } from '../helper';
-
-function deleteData({ data, outputs }) {
+function deleteData({ data, id }) {
   return new Promise((resolve) => {
     data.deleteList.forEach((item) => {
       if (item.type === 'table') {
@@ -31,7 +29,7 @@ function deleteData({ data, outputs }) {
 
     resolve({
       data: '',
-      nextBlockId: getBlockConnection({ outputs }),
+      nextBlockId: this.getBlockConnections(id),
     });
   });
 }

+ 4 - 9
src/background/workflowEngine/blocksHandler/handlerElementExists.js

@@ -1,29 +1,24 @@
-import { getBlockConnection } from '../helper';
-
 function elementExists(block) {
   return new Promise((resolve, reject) => {
     this._sendMessageToTab(block)
       .then((data) => {
-        const nextBlockId = getBlockConnection(block, data ? 1 : 2);
-
         if (!data && block.data.throwError) {
           const error = new Error('element-not-found');
-          error.nextBlockId = nextBlockId;
           error.data = { selector: block.data.selector };
 
           reject(error);
-
           return;
         }
 
         resolve({
           data,
-          nextBlockId,
+          nextBlockId: this.getBlockConnections(
+            block.id,
+            data ? 1 : 'fallback'
+          ),
         });
       })
       .catch((error) => {
-        error.nextBlockId = getBlockConnection(block);
-
         reject(error);
       });
   });

+ 58 - 60
src/background/workflowEngine/blocksHandler/handlerExecuteWorkflow.js

@@ -1,8 +1,8 @@
 import browser from 'webextension-polyfill';
 import { isWhitespace, parseJSON } from '@/utils/helper';
 import decryptFlow, { getWorkflowPass } from '@/utils/decryptFlow';
+import convertWorkflowData from '@/utils/convertWorkflowData';
 import WorkflowEngine from '../engine';
-import { getBlockConnection } from '../helper';
 
 function workflowListener(workflow, options) {
   return new Promise((resolve, reject) => {
@@ -36,73 +36,71 @@ function workflowListener(workflow, options) {
   });
 }
 
-async function executeWorkflow({ outputs, data }) {
-  const nextBlockId = getBlockConnection({ outputs });
+async function executeWorkflow({ id: blockId, data }) {
+  if (data.workflowId === '') throw new Error('empty-workflow');
 
-  try {
-    if (data.workflowId === '') throw new Error('empty-workflow');
+  const { workflows } = await browser.storage.local.get('workflows');
+  let workflow = Array.isArray(workflows)
+    ? workflows.find(({ id }) => id === data.workflowId)
+    : workflows[data.workflowId];
+  if (!workflow) {
+    const errorInstance = new Error('no-workflow');
+    errorInstance.data = { workflowId: data.workflowId };
 
-    const { workflows } = await browser.storage.local.get('workflows');
-    const workflowsArr = Array.isArray(workflows)
-      ? workflows
-      : Object.values(workflows);
-    const workflow = workflowsArr.find(({ id }) => id === data.workflowId);
+    throw errorInstance;
+  }
 
-    if (!workflow) {
-      const errorInstance = new Error('no-workflow');
-      errorInstance.data = { workflowId: data.workflowId };
+  workflow = convertWorkflowData(workflow);
 
-      throw errorInstance;
-    }
-    const options = {
-      options: {
-        data: {
-          globalData: isWhitespace(data.globalData) ? null : data.globalData,
-        },
-        parentWorkflow: {
-          id: this.engine.id,
-          name: this.engine.workflow.name,
-        },
+  const options = {
+    options: {
+      data: {
+        globalData: isWhitespace(data.globalData) ? null : data.globalData,
       },
-      events: {
-        onInit: (engine) => {
-          this.childWorkflowId = engine.id;
-        },
-        onDestroyed: (engine) => {
-          if (data.executeId) {
-            const { dataColumns, globalData, googleSheets, table } =
-              engine.referenceData;
-
-            this.engine.referenceData.workflow[data.executeId] = {
-              globalData,
-              dataColumns,
-              googleSheets,
-              table: table || dataColumns,
-            };
-          }
-        },
+      parentWorkflow: {
+        id: this.engine.id,
+        name: this.engine.workflow.name,
       },
-      states: this.engine.states,
-      logger: this.engine.logger,
-      blocksHandler: this.engine.blocksHandler,
-    };
-
-    if (workflow.drawflow.includes(this.engine.workflow.id)) {
-      throw new Error('workflow-infinite-loop');
-    }
-
-    const result = await workflowListener(workflow, options);
+    },
+    events: {
+      onInit: (engine) => {
+        this.childWorkflowId = engine.id;
+      },
+      onDestroyed: (engine) => {
+        if (data.executeId) {
+          const { dataColumns, globalData, googleSheets, table } =
+            engine.referenceData;
+
+          this.engine.referenceData.workflow[data.executeId] = {
+            globalData,
+            dataColumns,
+            googleSheets,
+            table: table || dataColumns,
+          };
+        }
+      },
+    },
+    states: this.engine.states,
+    logger: this.engine.logger,
+    blocksHandler: this.engine.blocksHandler,
+  };
+
+  const isWorkflowIncluded = workflow.nodes.some(
+    (node) =>
+      node.label === 'execute-workflow' &&
+      node.data.workflowId === this.engine.workflow.id
+  );
+  if (isWorkflowIncluded) {
+    throw new Error('workflow-infinite-loop');
+  }
 
-    return {
-      data: '',
-      logId: result.id,
-      nextBlockId,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
+  const result = await workflowListener(workflow, options);
 
-    throw error;
-  }
+  return {
+    data: '',
+    logId: result.id,
+    nextBlockId: this.getBlockConnections(blockId),
+  };
 }
 
 export default executeWorkflow;

+ 36 - 45
src/background/workflowEngine/blocksHandler/handlerExportData.js

@@ -1,60 +1,51 @@
 import browser from 'webextension-polyfill';
 import { default as dataExporter, files } from '@/utils/dataExporter';
-import { getBlockConnection } from '../helper';
 
-async function exportData({ data, outputs }, { refData }) {
-  const nextBlockId = getBlockConnection({ outputs });
+async function exportData({ data, id }, { refData }) {
+  const dataToExport = data.dataToExport || 'data-columns';
+  let payload = refData.table;
 
-  try {
-    const dataToExport = data.dataToExport || 'data-columns';
-    let payload = refData.table;
+  if (dataToExport === 'google-sheets') {
+    payload = refData.googleSheets[data.refKey] || [];
+  } else if (dataToExport === 'variable') {
+    payload = refData.variables[data.variableName] || [];
 
-    if (dataToExport === 'google-sheets') {
-      payload = refData.googleSheets[data.refKey] || [];
-    } else if (dataToExport === 'variable') {
-      payload = refData.variables[data.variableName] || [];
+    if (!Array.isArray(payload)) {
+      payload = [payload];
 
-      if (!Array.isArray(payload)) {
+      if (data.type === 'csv' && typeof payload[0] !== 'object')
         payload = [payload];
-
-        if (data.type === 'csv' && typeof payload[0] !== 'object')
-          payload = [payload];
-      }
-    }
-
-    const hasDownloadAccess = await browser.permissions.contains({
-      permissions: ['downloads'],
-    });
-    const blobUrl = dataExporter(payload, {
-      ...data,
-      csvOptions: {
-        delimiter: data.csvDelimiter || ',',
-      },
-      returnUrl: hasDownloadAccess,
-    });
-
-    if (hasDownloadAccess) {
-      const filename = `${data.name || 'unnamed'}${files[data.type].ext}`;
-      const options = {
-        filename,
-        conflictAction: data.onConflict || 'uniquify',
-      };
-
-      await browser.downloads.download({
-        ...options,
-        url: blobUrl,
-      });
     }
+  }
 
-    return {
-      data: '',
-      nextBlockId,
+  const hasDownloadAccess = await browser.permissions.contains({
+    permissions: ['downloads'],
+  });
+  const blobUrl = dataExporter(payload, {
+    ...data,
+    csvOptions: {
+      delimiter: data.csvDelimiter || ',',
+    },
+    returnUrl: hasDownloadAccess,
+  });
+
+  if (hasDownloadAccess) {
+    const filename = `${data.name || 'unnamed'}${files[data.type].ext}`;
+    const options = {
+      filename,
+      conflictAction: data.onConflict || 'uniquify',
     };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
 
-    throw error;
+    await browser.downloads.download({
+      ...options,
+      url: blobUrl,
+    });
   }
+
+  return {
+    data: '',
+    nextBlockId: this.getBlockConnections(id),
+  };
 }
 
 export default exportData;

+ 7 - 16
src/background/workflowEngine/blocksHandler/handlerForwardPage.js

@@ -1,23 +1,14 @@
 import browser from 'webextension-polyfill';
-import { getBlockConnection } from '../helper';
 
-export async function goBack({ outputs }) {
-  const nextBlockId = getBlockConnection({ outputs });
+export async function goBack({ id }) {
+  if (!this.activeTab.id) throw new Error('no-tab');
 
-  try {
-    if (!this.activeTab.id) throw new Error('no-tab');
+  await browser.tabs.goForward(this.activeTab.id);
 
-    await browser.tabs.goForward(this.activeTab.id);
-
-    return {
-      data: '',
-      nextBlockId,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
-
-    throw error;
-  }
+  return {
+    data: '',
+    nextBlockId: this.getBlockConnections(id),
+  };
 }
 
 export default goBack;

+ 7 - 16
src/background/workflowEngine/blocksHandler/handlerGoBack.js

@@ -1,23 +1,14 @@
 import browser from 'webextension-polyfill';
-import { getBlockConnection } from '../helper';
 
-export async function goBack({ outputs }) {
-  const nextBlockId = getBlockConnection({ outputs });
+export async function goBack({ id }) {
+  if (!this.activeTab.id) throw new Error('no-tab');
 
-  try {
-    if (!this.activeTab.id) throw new Error('no-tab');
+  await browser.tabs.goBack(this.activeTab.id);
 
-    await browser.tabs.goBack(this.activeTab.id);
-
-    return {
-      data: '',
-      nextBlockId,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
-
-    throw error;
-  }
+  return {
+    data: '',
+    nextBlockId: this.getBlockConnections(id),
+  };
 }
 
 export default goBack;

+ 34 - 44
src/background/workflowEngine/blocksHandler/handlerGoogleSheets.js

@@ -5,7 +5,6 @@ import {
   isWhitespace,
   parseJSON,
 } from '@/utils/helper';
-import { getBlockConnection } from '../helper';
 
 async function getSpreadsheetValues({ spreadsheetId, range, firstRowAsKey }) {
   const response = await googleSheets.getValues({ spreadsheetId, range });
@@ -93,50 +92,41 @@ async function updateSpreadsheetValues(
   }
 }
 
-export default async function ({ data, outputs }, { refData }) {
-  const nextBlockId = getBlockConnection({ outputs });
-
-  try {
-    if (isWhitespace(data.spreadsheetId))
-      throw new Error('empty-spreadsheet-id');
-    if (isWhitespace(data.range)) throw new Error('empty-spreadsheet-range');
-
-    let result = [];
-
-    if (data.type === 'get') {
-      const spreadsheetValues = await getSpreadsheetValues(data);
-
-      result = spreadsheetValues;
-
-      if (data.refKey && !isWhitespace(data.refKey)) {
-        refData.googleSheets[data.refKey] = spreadsheetValues;
-      }
-    } else if (data.type === 'getRange') {
-      result = await getSpreadsheetRange(data);
-
-      if (data.assignVariable) {
-        this.setVariable(data.variableName, result);
-      }
-      if (data.saveData) {
-        this.addDataToColumn(data.dataColumn, result);
-      }
-    } else if (['update', 'append'].includes(data.type)) {
-      result = await updateSpreadsheetValues(
-        {
-          ...data,
-          append: data.type === 'append',
-        },
-        refData.table
-      );
-    }
+export default async function ({ data, id }, { refData }) {
+  if (isWhitespace(data.spreadsheetId)) throw new Error('empty-spreadsheet-id');
+  if (isWhitespace(data.range)) throw new Error('empty-spreadsheet-range');
+
+  let result = [];
+
+  if (data.type === 'get') {
+    const spreadsheetValues = await getSpreadsheetValues(data);
+
+    result = spreadsheetValues;
 
-    return {
-      nextBlockId,
-      data: result,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
+    if (data.refKey && !isWhitespace(data.refKey)) {
+      refData.googleSheets[data.refKey] = spreadsheetValues;
+    }
+  } else if (data.type === 'getRange') {
+    result = await getSpreadsheetRange(data);
 
-    throw error;
+    if (data.assignVariable) {
+      this.setVariable(data.variableName, result);
+    }
+    if (data.saveData) {
+      this.addDataToColumn(data.dataColumn, result);
+    }
+  } else if (['update', 'append'].includes(data.type)) {
+    result = await updateSpreadsheetValues(
+      {
+        ...data,
+        append: data.type === 'append',
+      },
+      refData.table
+    );
   }
+
+  return {
+    data: result,
+    nextBlockId: this.getBlockConnections(id),
+  };
 }

+ 3 - 5
src/background/workflowEngine/blocksHandler/handlerHandleDialog.js

@@ -1,4 +1,4 @@
-import { getBlockConnection, sendDebugCommand } from '../helper';
+import { sendDebugCommand } from '../helper';
 
 const overwriteDialog = (accept, promptText) => `
   const realConfirm = window.confirm;
@@ -17,9 +17,7 @@ const overwriteDialog = (accept, promptText) => `
   }
 `;
 
-function handleDialog({ data, outputs, id: blockId }) {
-  const nextBlockId = getBlockConnection({ outputs });
-
+function handleDialog({ data, id: blockId }) {
   return new Promise((resolve) => {
     if (!this.settings.debugMode || BROWSER_TYPE !== 'chrome') {
       const isScriptExist = this.preloadScripts.find(
@@ -57,7 +55,7 @@ function handleDialog({ data, outputs, id: blockId }) {
 
     resolve({
       data: '',
-      nextBlockId,
+      nextBlockId: this.getBlockConnections(blockId),
     });
   });
 }

+ 2 - 3
src/background/workflowEngine/blocksHandler/handlerHandleDownload.js

@@ -1,5 +1,4 @@
 import browser from 'webextension-polyfill';
-import { getBlockConnection } from '../helper';
 
 const getFileExtension = (str) => /(?:\.([^.]+))?$/.exec(str)[1];
 function determineFilenameListener(item, suggest) {
@@ -28,8 +27,8 @@ function determineFilenameListener(item, suggest) {
   return false;
 }
 
-function handleDownload({ data, outputs }) {
-  const nextBlockId = getBlockConnection({ outputs });
+function handleDownload({ data, id: blockId }) {
+  const nextBlockId = this.getBlockConnections(blockId);
   const getFilesname = () =>
     JSON.parse(sessionStorage.getItem('rename-downloaded-files')) || {};
 

+ 28 - 36
src/background/workflowEngine/blocksHandler/handlerHoverElement.js

@@ -1,44 +1,36 @@
-import { getBlockConnection, attachDebugger } from '../helper';
+import { attachDebugger } from '../helper';
 
 export async function hoverElement(block) {
-  const nextBlockId = getBlockConnection(block);
-
-  try {
-    if (!this.activeTab.id) throw new Error('no-tab');
-    if (BROWSER_TYPE !== 'chrome') {
-      const error = new Error('browser-not-supported');
-      error.data = { browser: BROWSER_TYPE };
-
-      throw error;
-    }
-
-    const { debugMode, executedBlockOnWeb } = this.settings;
-
-    if (!debugMode) {
-      await attachDebugger(this.activeTab.id);
-    }
-
-    await this._sendMessageToTab({
-      ...block,
-      debugMode,
-      executedBlockOnWeb,
-      activeTabId: this.activeTab.id,
-      frameSelector: this.frameSelector,
-    });
-
-    if (!debugMode) {
-      chrome.debugger.detach({ tabId: this.activeTab.id });
-    }
-
-    return {
-      data: '',
-      nextBlockId,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
+  if (!this.activeTab.id) throw new Error('no-tab');
+  if (BROWSER_TYPE !== 'chrome') {
+    const error = new Error('browser-not-supported');
+    error.data = { browser: BROWSER_TYPE };
 
     throw error;
   }
+
+  const { debugMode, executedBlockOnWeb } = this.settings;
+
+  if (!debugMode) {
+    await attachDebugger(this.activeTab.id);
+  }
+
+  await this._sendMessageToTab({
+    ...block,
+    debugMode,
+    executedBlockOnWeb,
+    activeTabId: this.activeTab.id,
+    frameSelector: this.frameSelector,
+  });
+
+  if (!debugMode) {
+    chrome.debugger.detach({ tabId: this.activeTab.id });
+  }
+
+  return {
+    data: '',
+    nextBlockId: this.getBlockConnections(block.id),
+  };
 }
 
 export default hoverElement;

+ 2 - 3
src/background/workflowEngine/blocksHandler/handlerInsertData.js

@@ -1,8 +1,7 @@
 import { parseJSON } from '@/utils/helper';
 import mustacheReplacer from '@/utils/referenceData/mustacheReplacer';
-import { getBlockConnection } from '../helper';
 
-function insertData({ outputs, data }, { refData }) {
+function insertData({ id, data }, { refData }) {
   return new Promise((resolve) => {
     const replacedValueList = {};
     data.dataList.forEach(({ name, value, type }) => {
@@ -21,7 +20,7 @@ function insertData({ outputs, data }, { refData }) {
     resolve({
       data: '',
       replacedValue: replacedValueList,
-      nextBlockId: getBlockConnection({ outputs }),
+      nextBlockId: this.getBlockConnections(id),
     });
   });
 }

+ 1 - 5
src/background/workflowEngine/blocksHandler/handlerInteractionBlock.js

@@ -1,6 +1,5 @@
 import browser from 'webextension-polyfill';
 import { objectHasKey } from '@/utils/helper';
-import { getBlockConnection } from '../helper';
 
 async function checkAccess(blockName) {
   if (blockName === 'upload-file') {
@@ -25,8 +24,6 @@ async function checkAccess(blockName) {
 async function interactionHandler(block) {
   await checkAccess(block.name);
 
-  const nextBlockId = getBlockConnection(block);
-
   try {
     const data = await this._sendMessageToTab(block, {
       frameId: this.activeTab.frameId || 0,
@@ -67,10 +64,9 @@ async function interactionHandler(block) {
 
     return {
       data,
-      nextBlockId,
+      nextBlockId: this.getBlockConnections(block.id),
     };
   } catch (error) {
-    error.nextBlockId = nextBlockId;
     error.data = {
       name: block.name,
       selector: block.data.selector,

+ 36 - 46
src/background/workflowEngine/blocksHandler/handlerJavascriptCode.js

@@ -1,62 +1,52 @@
-import { getBlockConnection } from '../helper';
-
 export async function javascriptCode({ outputs, data, ...block }, { refData }) {
-  const nextBlockId = getBlockConnection({ outputs });
+  const nextBlockId = this.getBlockConnections(block.id);
 
-  try {
-    if (data.everyNewTab) {
-      const isScriptExist = this.preloadScripts.find(
-        ({ id }) => id === block.id
-      );
+  if (data.everyNewTab) {
+    const isScriptExist = this.preloadScripts.find(({ id }) => id === block.id);
 
-      if (!isScriptExist) {
-        this.preloadScripts.push({ ...block, data });
-      }
+    if (!isScriptExist) {
+      this.preloadScripts.push({ ...block, data });
     }
-    if (!this.activeTab.id) {
-      if (!data.everyNewTab) {
-        throw new Error('no-tab');
-      } else {
-        return { data: '', nextBlockId };
-      }
+  }
+  if (!this.activeTab.id) {
+    if (!data.everyNewTab) {
+      throw new Error('no-tab');
+    } else {
+      return { data: '', nextBlockId };
     }
+  }
 
-    const payload = { ...block, data, refData: { variables: {} } };
-    if (data.code.includes('automaRefData')) payload.refData = refData;
-
-    if (!data.code.includes('automaNextBlock'))
-      payload.data.code += `\nautomaNextBlock()`;
-
-    const result = await this._sendMessageToTab(payload);
-    if (result) {
-      if (result.columns.data?.$error) {
-        throw new Error(result.columns.data.message);
-      }
+  const payload = { ...block, data, refData: { variables: {} } };
+  if (data.code.includes('automaRefData')) payload.refData = refData;
 
-      if (result.variables) {
-        Object.keys(result.variables).forEach((varName) => {
-          this.setVariable(varName, result.variables[varName]);
-        });
-      }
+  if (!data.code.includes('automaNextBlock'))
+    payload.data.code += `\nautomaNextBlock()`;
 
-      if (result.columns.insert && result.columns.data) {
-        const params = Array.isArray(result.columns.data)
-          ? result.columns.data
-          : [result.columns.data];
+  const result = await this._sendMessageToTab(payload);
+  if (result) {
+    if (result.columns.data?.$error) {
+      throw new Error(result.columns.data.message);
+    }
 
-        this.addDataToColumn(params);
-      }
+    if (result.variables) {
+      Object.keys(result.variables).forEach((varName) => {
+        this.setVariable(varName, result.variables[varName]);
+      });
     }
 
-    return {
-      nextBlockId,
-      data: result?.columns.data || {},
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
+    if (result.columns.insert && result.columns.data) {
+      const params = Array.isArray(result.columns.data)
+        ? result.columns.data
+        : [result.columns.data];
 
-    throw error;
+      this.addDataToColumn(params);
+    }
   }
+
+  return {
+    nextBlockId,
+    data: result?.columns.data || {},
+  };
 }
 
 export default javascriptCode;

+ 2 - 4
src/background/workflowEngine/blocksHandler/handlerLoopBreakpoint.js

@@ -1,5 +1,3 @@
-import { getBlockConnection } from '../helper';
-
 function loopBreakpoint(block, { prevBlockData }) {
   const currentLoop = this.loopList[block.data.loopId];
 
@@ -20,7 +18,7 @@ function loopBreakpoint(block, { prevBlockData }) {
     ) {
       resolve({
         data: '',
-        nextBlockId: currentLoop.blockId,
+        nextBlockId: [currentLoop.blockId],
       });
     } else {
       if (currentLoop.type === 'elements') {
@@ -36,7 +34,7 @@ function loopBreakpoint(block, { prevBlockData }) {
 
       resolve({
         data: prevBlockData,
-        nextBlockId: getBlockConnection(block),
+        nextBlockId: this.getBlockConnections(block.id),
       });
     }
   });

+ 3 - 8
src/background/workflowEngine/blocksHandler/handlerLoopData.js

@@ -1,9 +1,6 @@
 import { parseJSON, isXPath } from '@/utils/helper';
-import { getBlockConnection } from '../helper';
-
-async function loopData({ data, id, outputs }, { refData }) {
-  const nextBlockId = getBlockConnection({ outputs });
 
+async function loopData({ data, id }, { refData }) {
   try {
     if (this.loopList[data.loopId]) {
       const index = this.loopList[data.loopId].index + 1;
@@ -43,7 +40,7 @@ async function loopData({ data, id, outputs }, { refData }) {
             : 'cssSelector';
           const { elements, url, loopId } = await this._sendMessageToTab({
             id,
-            name: 'loop-data',
+            label: 'loop-data',
             data: {
               max,
               findBy,
@@ -103,12 +100,10 @@ async function loopData({ data, id, outputs }, { refData }) {
     localStorage.setItem(`index:${id}`, this.loopList[data.loopId].index);
 
     return {
-      nextBlockId,
       data: refData.loopData[data.loopId],
+      nextBlockId: this.getBlockConnections(id),
     };
   } catch (error) {
-    error.nextBlockId = nextBlockId;
-
     if (data.loopThrough === 'elements') {
       error.data = { selector: data.elementSelector };
     }

+ 75 - 88
src/background/workflowEngine/blocksHandler/handlerNewTab.js

@@ -1,13 +1,8 @@
 import browser from 'webextension-polyfill';
 import { isWhitespace, sleep } from '@/utils/helper';
-import {
-  waitTabLoaded,
-  attachDebugger,
-  sendDebugCommand,
-  getBlockConnection,
-} from '../helper';
-
-async function newTab({ outputs, data }) {
+import { waitTabLoaded, attachDebugger, sendDebugCommand } from '../helper';
+
+async function newTab({ id, data }) {
   if (this.windowId) {
     try {
       await browser.windows.get(this.windowId);
@@ -16,104 +11,96 @@ async function newTab({ outputs, data }) {
     }
   }
 
-  const nextBlockId = getBlockConnection({ outputs });
-
-  try {
-    const isInvalidUrl = !/^https?/.test(data.url);
-
-    if (isInvalidUrl) {
-      const error = new Error(
-        isWhitespace(data.url) ? 'url-empty' : 'invalid-active-tab'
-      );
-      error.data = { url: data.url };
-
-      throw error;
-    }
-
-    let tab = null;
-    const isChrome = BROWSER_TYPE === 'chrome';
-
-    if (data.updatePrevTab && this.activeTab.id) {
-      tab = await browser.tabs.update(this.activeTab.id, {
-        url: data.url,
-        active: data.active,
-      });
-    } else {
-      tab = await browser.tabs.create({
-        url: data.url,
-        active: data.active,
-        windowId: this.windowId,
-      });
-    }
+  const isInvalidUrl = !/^https?/.test(data.url);
 
-    this.activeTab.url = data.url;
-    if (tab) {
-      if (this.settings.debugMode || data.customUserAgent) {
-        await attachDebugger(tab.id, this.activeTab.id);
-        this.debugAttached = true;
-
-        if (data.customUserAgent && isChrome) {
-          await sendDebugCommand(tab.id, 'Network.setUserAgentOverride', {
-            userAgent: data.userAgent,
-          });
-          await browser.tabs.reload(tab.id);
-          await sleep(1000);
-        }
-      }
+  if (isInvalidUrl) {
+    const error = new Error(
+      isWhitespace(data.url) ? 'url-empty' : 'invalid-active-tab'
+    );
+    error.data = { url: data.url };
 
-      this.activeTab.id = tab.id;
-      this.windowId = tab.windowId;
-    }
+    throw error;
+  }
 
-    if (data.inGroup && !data.updatePrevTab) {
-      const options = {
-        groupId: this.activeTab.groupId,
-        tabIds: this.activeTab.id,
-      };
+  let tab = null;
+  const isChrome = BROWSER_TYPE === 'chrome';
+
+  if (data.updatePrevTab && this.activeTab.id) {
+    tab = await browser.tabs.update(this.activeTab.id, {
+      url: data.url,
+      active: data.active,
+    });
+  } else {
+    tab = await browser.tabs.create({
+      url: data.url,
+      active: data.active,
+      windowId: this.windowId,
+    });
+  }
 
-      if (!this.activeTab.groupId) {
-        options.createProperties = {
-          windowId: this.windowId,
-        };
-      }
+  this.activeTab.url = data.url;
+  if (tab) {
+    if (this.settings.debugMode || data.customUserAgent) {
+      await attachDebugger(tab.id, this.activeTab.id);
+      this.debugAttached = true;
 
-      if (isChrome) {
-        chrome.tabs.group(options, (tabGroupId) => {
-          this.activeTab.groupId = tabGroupId;
+      if (data.customUserAgent && isChrome) {
+        await sendDebugCommand(tab.id, 'Network.setUserAgentOverride', {
+          userAgent: data.userAgent,
         });
+        await browser.tabs.reload(tab.id);
+        await sleep(1000);
       }
     }
 
-    this.activeTab.frameId = 0;
+    this.activeTab.id = tab.id;
+    this.windowId = tab.windowId;
+  }
 
-    if (isChrome && !this.settings.debugMode && data.customUserAgent) {
-      chrome.debugger.detach({ tabId: tab.id });
-    }
+  if (data.inGroup && !data.updatePrevTab) {
+    const options = {
+      groupId: this.activeTab.groupId,
+      tabIds: this.activeTab.id,
+    };
 
-    if (this.preloadScripts.length > 0) {
-      const preloadScripts = this.preloadScripts.map((script) =>
-        this._sendMessageToTab(script)
-      );
-      await Promise.allSettled(preloadScripts);
+    if (!this.activeTab.groupId) {
+      options.createProperties = {
+        windowId: this.windowId,
+      };
     }
 
-    if (data.waitTabLoaded) {
-      await waitTabLoaded({
-        listenError: true,
-        tabId: this.activeTab.id,
-        ms: this.settings?.tabLoadTimeout ?? 30000,
+    if (isChrome) {
+      chrome.tabs.group(options, (tabGroupId) => {
+        this.activeTab.groupId = tabGroupId;
       });
     }
+  }
 
-    return {
-      nextBlockId,
-      data: data.url,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
+  this.activeTab.frameId = 0;
 
-    throw error;
+  if (isChrome && !this.settings.debugMode && data.customUserAgent) {
+    chrome.debugger.detach({ tabId: tab.id });
+  }
+
+  if (this.preloadScripts.length > 0) {
+    const preloadScripts = this.preloadScripts.map((script) =>
+      this._sendMessageToTab(script)
+    );
+    await Promise.allSettled(preloadScripts);
   }
+
+  if (data.waitTabLoaded) {
+    await waitTabLoaded({
+      listenError: true,
+      tabId: this.activeTab.id,
+      ms: this.settings?.tabLoadTimeout ?? 30000,
+    });
+  }
+
+  return {
+    data: data.url,
+    nextBlockId: this.getBlockConnections(id),
+  };
 }
 
 export default newTab;

+ 14 - 23
src/background/workflowEngine/blocksHandler/handlerNewWindow.js

@@ -1,33 +1,24 @@
 import browser from 'webextension-polyfill';
-import { getBlockConnection } from '../helper';
 
 export async function newWindow(block) {
-  const nextBlockId = getBlockConnection(block);
+  const { incognito, windowState } = block.data;
+  const windowOptions = { incognito, state: windowState };
 
-  try {
-    const { incognito, windowState } = block.data;
-    const windowOptions = { incognito, state: windowState };
+  if (windowState === 'normal') {
+    ['top', 'left', 'height', 'width'].forEach((key) => {
+      if (block.data[key] <= 0) return;
 
-    if (windowState === 'normal') {
-      ['top', 'left', 'height', 'width'].forEach((key) => {
-        if (block.data[key] <= 0) return;
-
-        windowOptions[key] = block.data[key];
-      });
-    }
-
-    const { id } = await browser.windows.create(windowOptions);
-    this.windowId = id;
+      windowOptions[key] = block.data[key];
+    });
+  }
 
-    return {
-      data: id,
-      nextBlockId,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
+  const { id } = await browser.windows.create(windowOptions);
+  this.windowId = id;
 
-    throw error;
-  }
+  return {
+    data: id,
+    nextBlockId: this.getBlockConnections(block.id),
+  };
 }
 
 export default newWindow;

+ 27 - 36
src/background/workflowEngine/blocksHandler/handlerNotification.js

@@ -1,47 +1,38 @@
 import { nanoid } from 'nanoid';
 import browser from 'webextension-polyfill';
-import { getBlockConnection } from '../helper';
 
-export default async function ({ data, outputs }) {
-  const nextBlockId = getBlockConnection({ outputs });
+export default async function ({ data, id }) {
+  const hasPermission = await browser.permissions.contains({
+    permissions: ['notifications'],
+  });
 
-  try {
-    const hasPermission = await browser.permissions.contains({
-      permissions: ['notifications'],
-    });
+  if (!hasPermission) {
+    const error = new Error('no-permission');
+    error.data = { permission: 'notifications' };
 
-    if (!hasPermission) {
-      const error = new Error('no-permission');
-      error.data = { permission: 'notifications' };
-
-      throw error;
-    }
-
-    const options = {
-      title: data.title,
-      message: data.message,
-      iconUrl: browser.runtime.getURL('icon-128.png'),
-    };
+    throw error;
+  }
 
-    ['iconUrl', 'imageUrl'].forEach((key) => {
-      const url = data[key];
-      if (!url || !url.startsWith('http')) return;
+  const options = {
+    title: data.title,
+    message: data.message,
+    iconUrl: browser.runtime.getURL('icon-128.png'),
+  };
 
-      options[key] = url;
-    });
+  ['iconUrl', 'imageUrl'].forEach((key) => {
+    const url = data[key];
+    if (!url || !url.startsWith('http')) return;
 
-    await browser.notifications.create(nanoid(), {
-      ...options,
-      type: options.imageUrl ? 'image' : 'basic',
-    });
+    options[key] = url;
+  });
 
-    return {
-      data: '',
-      nextBlockId,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
+  await browser.notifications.create(nanoid(), {
+    ...options,
+    type: options.imageUrl ? 'image' : 'basic',
+  });
 
-    throw error;
-  }
+  return {
+    data: '',
+    nextBlockId: this.getBlockConnections(id),
+  };
 }

+ 2 - 3
src/background/workflowEngine/blocksHandler/handlerProxy.js

@@ -1,9 +1,8 @@
 import browser from 'webextension-polyfill';
 import { isWhitespace } from '@/utils/helper';
-import { getBlockConnection } from '../helper';
 
-function setProxy({ data, outputs }) {
-  const nextBlockId = getBlockConnection({ outputs });
+function setProxy({ data, id }) {
+  const nextBlockId = this.getBlockConnections(id);
 
   return new Promise((resolve, reject) => {
     if (data.clearProxy) {

+ 7 - 16
src/background/workflowEngine/blocksHandler/handlerReloadTab.js

@@ -1,23 +1,14 @@
 import browser from 'webextension-polyfill';
-import { getBlockConnection } from '../helper';
 
-export async function reloadTab({ outputs }) {
-  const nextBlockId = getBlockConnection({ outputs });
+export async function reloadTab({ id }) {
+  if (!this.activeTab.id) throw new Error('no-tab');
 
-  try {
-    if (!this.activeTab.id) throw new Error('no-tab');
+  await browser.tabs.reload(this.activeTab.id);
 
-    await browser.tabs.reload(this.activeTab.id);
-
-    return {
-      data: '',
-      nextBlockId,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
-
-    throw error;
-  }
+  return {
+    data: '',
+    nextBlockId: this.getBlockConnections(id),
+  };
 }
 
 export default reloadTab;

+ 3 - 5
src/background/workflowEngine/blocksHandler/handlerRepeatTask.js

@@ -1,20 +1,18 @@
-import { getBlockConnection } from '../helper';
-
-function repeatTask({ data, id, outputs }) {
+function repeatTask({ data, id }) {
   return new Promise((resolve) => {
     if (this.repeatedTasks[id] >= data.repeatFor) {
       delete this.repeatedTasks[id];
 
       resolve({
         data: data.repeatFor,
-        nextBlockId: getBlockConnection({ outputs }),
+        nextBlockId: this.getBlockConnections(id),
       });
     } else {
       this.repeatedTasks[id] = (this.repeatedTasks[id] || 1) + 1;
 
       resolve({
         data: data.repeatFor,
-        nextBlockId: getBlockConnection({ outputs }, 2),
+        nextBlockId: this.getBlockConnections(id, 2),
       });
     }
   });

+ 35 - 44
src/background/workflowEngine/blocksHandler/handlerSaveAssets.js

@@ -1,5 +1,4 @@
 import browser from 'webextension-polyfill';
-import { getBlockConnection } from '../helper';
 
 function getFilename(url) {
   try {
@@ -14,56 +13,48 @@ function getFilename(url) {
   }
 }
 
-export default async function ({ data, id, name, outputs }) {
-  const nextBlockId = getBlockConnection({ outputs });
+export default async function ({ data, id, name }) {
+  const hasPermission = await browser.permissions.contains({
+    permissions: ['downloads'],
+  });
 
-  try {
-    const hasPermission = await browser.permissions.contains({
-      permissions: ['downloads'],
-    });
-
-    if (!hasPermission) {
-      throw new Error('no-permission');
-    }
-
-    let sources = [data.url];
-    let index = 0;
-    const downloadFile = (url) => {
-      const options = { url, conflictAction: data.onConflict };
-      let filename = data.filename || getFilename(url);
+  if (!hasPermission) {
+    throw new Error('no-permission');
+  }
 
-      if (filename) {
-        if (data.onConflict === 'overwrite' && index !== 0) {
-          filename = `(${index}) ${filename}`;
-        }
+  let sources = [data.url];
+  let index = 0;
+  const downloadFile = (url) => {
+    const options = { url, conflictAction: data.onConflict };
+    let filename = data.filename || getFilename(url);
 
-        options.filename = filename;
-        index += 1;
+    if (filename) {
+      if (data.onConflict === 'overwrite' && index !== 0) {
+        filename = `(${index}) ${filename}`;
       }
 
-      return browser.downloads.download(options);
-    };
-
-    if (data.type === 'element') {
-      sources = await this._sendMessageToTab({
-        id,
-        name,
-        data,
-        tabId: this.activeTab.id,
-      });
-
-      await Promise.all(sources.map((url) => downloadFile(url)));
-    } else if (data.type === 'url') {
-      await downloadFile(data.url);
+      options.filename = filename;
+      index += 1;
     }
 
-    return {
-      nextBlockId,
-      data: sources,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
+    return browser.downloads.download(options);
+  };
 
-    throw error;
+  if (data.type === 'element') {
+    sources = await this._sendMessageToTab({
+      id,
+      name,
+      data,
+      tabId: this.activeTab.id,
+    });
+
+    await Promise.all(sources.map((url) => downloadFile(url)));
+  } else if (data.type === 'url') {
+    await downloadFile(data.url);
   }
+
+  return {
+    data: sources,
+    nextBlockId: this.getBlockConnections(id),
+  };
 }

+ 3 - 3
src/background/workflowEngine/blocksHandler/handlerSwitchTab.js

@@ -1,8 +1,8 @@
 import browser from 'webextension-polyfill';
-import { getBlockConnection, attachDebugger } from '../helper';
+import { attachDebugger } from '../helper';
 
-export default async function ({ data, outputs }) {
-  const nextBlockId = getBlockConnection({ outputs });
+export default async function ({ data, id }) {
+  const nextBlockId = this.getBlockConnections(id);
   const generateError = (message, errorData) => {
     const error = new Error(message);
     error.nextBlockId = nextBlockId;

+ 2 - 2
src/background/workflowEngine/blocksHandler/handlerSwitchTo.js

@@ -1,8 +1,8 @@
 import { objectHasKey, sleep } from '@/utils/helper';
-import { getBlockConnection, getFrames } from '../helper';
+import { getFrames } from '../helper';
 
 async function switchTo(block) {
-  const nextBlockId = getBlockConnection(block);
+  const nextBlockId = this.getBlockConnections(block.id);
 
   try {
     if (block.data.windowType === 'main-window') {

+ 7 - 7
src/background/workflowEngine/blocksHandler/handlerTakeScreenshot.js

@@ -1,6 +1,6 @@
 import browser from 'webextension-polyfill';
 import { fileSaver } from '@/utils/helper';
-import { getBlockConnection, waitTabLoaded } from '../helper';
+import { waitTabLoaded } from '../helper';
 
 async function saveImage({ filename, uri, ext }) {
   const hasDownloadAccess = await browser.permissions.contains({
@@ -33,8 +33,7 @@ async function saveImage({ filename, uri, ext }) {
   image.src = uri;
 }
 
-async function takeScreenshot({ data, outputs, name }) {
-  const nextBlockId = getBlockConnection({ outputs });
+async function takeScreenshot({ data, id, label }) {
   const saveToComputer =
     typeof data.saveToComputer === 'undefined' || data.saveToComputer;
 
@@ -85,7 +84,7 @@ async function takeScreenshot({ data, outputs, name }) {
       screenshot = await (data.fullPage ||
       ['element', 'fullpage'].includes(data.type)
         ? this._sendMessageToTab({
-            name,
+            label,
             options,
             data: {
               type: data.type,
@@ -107,10 +106,11 @@ async function takeScreenshot({ data, outputs, name }) {
       await saveScreenshot(screenshot);
     }
 
-    return { data: screenshot, nextBlockId };
+    return {
+      data: screenshot,
+      nextBlockId: this.getBlockConnections(id),
+    };
   } catch (error) {
-    error.nextBlockId = nextBlockId;
-
     if (data.type === 'element') error.data = { selector: data.selector };
 
     throw error;

+ 1 - 5
src/background/workflowEngine/blocksHandler/handlerTrigger.js

@@ -1,12 +1,8 @@
-import { getBlockConnection } from '../helper';
-
 async function trigger(block) {
   return new Promise((resolve) => {
-    const nextBlockId = getBlockConnection(block);
-
     resolve({
       data: '',
-      nextBlockId,
+      nextBlockId: this.getBlockConnections(block.id),
     });
   });
 }

+ 9 - 6
src/background/workflowEngine/blocksHandler/handlerWaitConnections.js

@@ -1,17 +1,20 @@
-import { getBlockConnection } from '../helper';
-
-async function waitConnections({ data, outputs, inputs, id }, { prevBlock }) {
+async function waitConnections({ data, id }, { prevBlock }) {
   return new Promise((resolve) => {
     let timeout;
     let resolved = false;
 
-    const nextBlockId = getBlockConnection({ outputs });
+    const nextBlockId = this.getBlockConnections(id);
     const destroyWorker =
       data.specificFlow && prevBlock?.id !== data.flowBlockId;
 
     const registerConnections = () => {
-      inputs.input_1.connections.forEach(({ node }) => {
-        this.engine.waitConnections[id][node] = {
+      const connections = this.engine.connectionsMap;
+      Object.keys(connections).forEach((key) => {
+        const isConnected = connections[key].includes(id);
+        if (!isConnected) return;
+
+        const prevBlockId = key.slice(0, key.indexOf('-output'));
+        this.engine.waitConnections[id][prevBlockId] = {
           isHere: false,
           isContinue: false,
         };

+ 3 - 4
src/background/workflowEngine/blocksHandler/handlerWebhook.js

@@ -2,11 +2,10 @@ import objectPath from 'object-path';
 import { isWhitespace } from '@/utils/helper';
 import { executeWebhook } from '@/utils/webhookUtil';
 import mustacheReplacer from '@/utils/referenceData/mustacheReplacer';
-import { getBlockConnection } from '../helper';
 
-export async function webhook({ data, outputs }, { refData }) {
-  const nextBlockId = getBlockConnection({ outputs });
-  const fallbackOutput = getBlockConnection({ outputs }, 2);
+export async function webhook({ data, id }, { refData }) {
+  const nextBlockId = this.getBlockConnections(id);
+  const fallbackOutput = this.getBlockConnections(id, 'fallback');
 
   try {
     if (isWhitespace(data.url)) throw new Error('url-empty');

+ 6 - 4
src/background/workflowEngine/blocksHandler/handlerWhileLoop.js

@@ -1,15 +1,17 @@
 import testConditions from '@/utils/testConditions';
-import { getBlockConnection } from '../helper';
 
-async function whileLoop({ data, outputs, id }, { refData }) {
+async function whileLoop({ data, id }, { refData }) {
   const conditionPayload = {
     refData,
     activeTab: this.activeTab.id,
     sendMessage: (payload) =>
-      this._sendMessageToTab({ ...payload.data, name: 'conditions', id }),
+      this._sendMessageToTab({ ...payload.data, label: 'conditions', id }),
   };
   const result = await testConditions(data.conditions, conditionPayload);
-  const nextBlockId = getBlockConnection({ outputs }, result.isMatch ? 1 : 2);
+  const nextBlockId = this.getBlockConnections(
+    id,
+    result.isMatch ? 1 : 'fallback'
+  );
 
   return {
     data: '',

+ 26 - 12
src/background/workflowEngine/engine.js

@@ -16,11 +16,16 @@ class WorkflowEngine {
 
     this.workerId = 0;
     this.workers = new Map();
+
+    this.extractedGroup = {};
+    this.connectionsMap = {};
     this.waitConnections = {};
 
     this.isDestroyed = false;
     this.isUsingProxy = false;
 
+    this.triggerBlockId = null;
+
     this.blocks = {};
     this.history = [];
     this.columnsId = {};
@@ -89,23 +94,33 @@ class WorkflowEngine {
       return;
     }
 
-    const flow = this.workflow.drawflow;
-    const parsedFlow = typeof flow === 'string' ? parseJSON(flow, {}) : flow;
-    const blocks = parsedFlow?.drawflow?.Home.data;
-
-    if (!blocks) {
+    const { nodes, edges } = this.workflow.drawflow;
+    if (!nodes || nodes.length === 0) {
       console.error(`${this.workflow.name} doesn't have blocks`);
       return;
     }
 
-    const triggerBlock = Object.values(blocks).find(
-      ({ name }) => name === 'trigger'
-    );
+    const triggerBlock = nodes.find((node) => node.label === 'trigger');
     if (!triggerBlock) {
       console.error(`${this.workflow.name} doesn't have a trigger block`);
       return;
     }
 
+    this.triggerBlockId = triggerBlock.id;
+
+    this.blocks = nodes.reduce((acc, node) => {
+      acc[node.id] = node;
+
+      return acc;
+    }, {});
+    this.connectionsMap = edges.reduce((acc, { sourceHandle, target }) => {
+      if (!acc[sourceHandle]) acc[sourceHandle] = [];
+
+      acc[sourceHandle].push(target);
+
+      return acc;
+    }, {});
+
     const workflowTable = this.workflow.table || this.workflow.dataColumns;
     const columns = Array.isArray(workflowTable)
       ? workflowTable
@@ -135,9 +150,8 @@ class WorkflowEngine {
       });
     }
 
-    this.blocks = blocks;
-    this.startedTimestamp = Date.now();
     this.workflow.table = columns;
+    this.startedTimestamp = Date.now();
 
     this.states.on('stop', this.onWorkflowStopped);
 
@@ -348,9 +362,9 @@ class WorkflowEngine {
     };
 
     this.workers.forEach((worker) => {
-      const { id, name, startedAt } = worker.currentBlock;
+      const { id, label, startedAt } = worker.currentBlock;
 
-      state.currentBlock.push({ id, name, startedAt });
+      state.currentBlock.push({ id, name: label, startedAt });
       state.tabIds.push(worker.activeTab.id);
     });
 

+ 2 - 4
src/background/workflowEngine/helper.js

@@ -111,8 +111,6 @@ export function convertData(data, type) {
   return result;
 }
 
-export function getBlockConnection(block, index = 1) {
-  const blockId = block.outputs[`output_${index}`];
-
-  return blockId;
+export function getBlockConnection(blockId, outputId = 1) {
+  return `${blockId}-output-${outputId}`;
 }

+ 26 - 30
src/background/workflowEngine/worker.js

@@ -3,7 +3,7 @@ import { toCamelCase, sleep, objectHasKey, isObject } from '@/utils/helper';
 import { tasks } from '@/utils/shared';
 import referenceData from '@/utils/referenceData';
 import injectContentScript from './injectContentScript';
-import { convertData, waitTabLoaded, getBlockConnection } from './helper';
+import { convertData, waitTabLoaded } from './helper';
 
 class Worker {
   constructor(id, engine) {
@@ -79,10 +79,15 @@ class Worker {
     this.engine.referenceData.variables[name] = value;
   }
 
+  getBlockConnections(blockId, outputIndex = 1) {
+    const outputId = `${blockId}-output-${outputIndex}`;
+    return this.engine.connectionsMap[outputId] || null;
+  }
+
   executeNextBlocks(connections, prevBlockData) {
-    connections.forEach(({ node }, index) => {
+    connections.forEach((nodeId, index) => {
       if (index === 0) {
-        this.executeBlock(this.engine.blocks[node], prevBlockData);
+        this.executeBlock(this.engine.blocks[nodeId], prevBlockData);
       } else {
         const state = structuredClone({
           windowId: this.windowId,
@@ -96,7 +101,7 @@ class Worker {
         this.engine.addWorker({
           state,
           prevBlockData,
-          blockId: node,
+          blockId: nodeId,
         });
       }
     });
@@ -123,9 +128,9 @@ class Worker {
       });
     }
 
-    const blockHandler = this.engine.blocksHandler[toCamelCase(block.name)];
+    const blockHandler = this.engine.blocksHandler[toCamelCase(block.label)];
     const handler =
-      !blockHandler && tasks[block.name].category === 'interaction'
+      !blockHandler && tasks[block.label].category === 'interaction'
         ? this.engine.blocksHandler.interactionBlock
         : blockHandler;
 
@@ -139,20 +144,21 @@ class Worker {
       ...this.engine.referenceData,
       activeTabUrl: this.activeTab.url,
     };
+
     const replacedBlock = referenceData({
       block,
       data: refData,
       refKeys:
         isRetry || block.data.disableBlock
           ? null
-          : tasks[block.name].refDataKeys,
+          : tasks[block.label].refDataKeys,
     });
     const blockDelay = this.settings?.blockDelay || 0;
     const addBlockLog = (status, obj = {}) => {
       this.engine.addLogHistory({
         prevBlockData,
         type: status,
-        name: block.name,
+        name: block.label,
         blockId: block.id,
         workerId: this.id,
         description: block.data.description,
@@ -168,7 +174,7 @@ class Worker {
       if (block.data.disableBlock) {
         result = {
           data: '',
-          nextBlockId: getBlockConnection(block),
+          nextBlockId: this.getBlockConnections(block.id),
         };
       } else {
         result = await handler.call(this, replacedBlock, {
@@ -186,17 +192,9 @@ class Worker {
         });
       }
 
-      let nodeConnections = null;
-
-      if (typeof result.nextBlockId === 'string') {
-        nodeConnections = [{ node: result.nextBlockId }];
-      } else {
-        nodeConnections = result.nextBlockId.connections;
-      }
-
-      if (nodeConnections.length > 0 && !result.destroyWorker) {
+      if (result.nextBlockId && !result.destroyWorker) {
         setTimeout(() => {
-          this.executeNextBlocks(nodeConnections, result.data);
+          this.executeNextBlocks(result.nextBlockId, result.data);
         }, blockDelay);
       } else {
         this.engine.destroyWorker(this.id);
@@ -213,17 +211,17 @@ class Worker {
           return;
         }
 
-        const nextBlocks = getBlockConnection(
-          block,
-          blockOnError.toDo === 'continue' ? 1 : 2
+        const nextBlocks = this.getBlockConnections(
+          block.id,
+          blockOnError.toDo === 'continue' ? 1 : 'fallback'
         );
-        if (blockOnError.toDo !== 'error' && nextBlocks?.connections) {
+        if (blockOnError.toDo !== 'error' && nextBlocks) {
           addBlockLog('error', {
             message: error.message,
             ...(error.data || {}),
           });
 
-          this.executeNextBlocks(nextBlocks.connections, prevBlockData);
+          this.executeNextBlocks(nextBlocks, prevBlockData);
 
           return;
         }
@@ -235,7 +233,7 @@ class Worker {
       });
 
       const { onError } = this.settings;
-      const nodeConnections = error.nextBlockId?.connections;
+      const nodeConnections = this.getBlockConnections(block.id);
 
       if (onError === 'keep-running' && nodeConnections) {
         setTimeout(() => {
@@ -248,15 +246,13 @@ class Worker {
 
         if (restartCount >= maxRestart) {
           localStorage.removeItem(restartKey);
-          this.engine.destroy();
+          this.engine.destroy('error');
           return;
         }
 
         this.reset();
 
-        const triggerBlock = Object.values(this.engine.blocks).find(
-          ({ name }) => name === 'trigger'
-        );
+        const triggerBlock = this.engine.blocks[this.engine.triggerBlockId];
         this.executeBlock(triggerBlock);
 
         localStorage.setItem(restartKey, restartCount + 1);
@@ -296,7 +292,7 @@ class Worker {
       loopData: {},
       workflow: {},
       googleSheets: {},
-      variables: this.engine.options.variables,
+      variables: this.engine.options?.variables || {},
       globalData: this.engine.referenceData.globalData,
     };
   }

+ 2 - 4
src/components/block/BlockRepeatTask.vue

@@ -46,7 +46,6 @@
 <script setup>
 import { useI18n } from 'vue-i18n';
 import { Handle, Position } from '@braks/vue-flow';
-import emitter from '@/lib/mitt';
 import { useComponentId } from '@/composable/componentId';
 import { useEditorBlock } from '@/composable/editorBlock';
 
@@ -65,7 +64,7 @@ const props = defineProps({
     default: () => ({}),
   },
 });
-defineEmits(['delete']);
+const emit = defineEmits(['delete', 'update']);
 
 const block = useEditorBlock(props.label);
 const componentId = useComponentId('block-delay');
@@ -77,8 +76,7 @@ function handleInput({ target }) {
 
   if (repeatFor < 0) return;
 
-  props.editor.updateNodeDataFromId(props.id, { repeatFor });
-  emitter.emit('editor:data-changed', props.id);
+  emit('update', { repeatFor });
 }
 </script>
 <style>

+ 6 - 10
src/components/newtab/workflow/editor/EditorLocalActions.vue

@@ -212,16 +212,12 @@ const dialog = useDialog();
 const userStore = useUserStore();
 const workflowStore = useWorkflowStore();
 const sharedWorkflowStore = useSharedWorkflowStore();
-const shortcuts = useShortcut(
-  [
-    /* eslint-disable-next-line */
-    getShortcut('editor:save', saveWorkflow),
-    getShortcut('editor:execute-workflow', 'execute'),
-  ],
-  ({ data }) => {
-    emit(data);
-  }
-);
+const shortcuts = useShortcut([
+  /* eslint-disable-next-line */
+  getShortcut('editor:save', saveWorkflow),
+  /* eslint-disable-next-line */
+  getShortcut('editor:execute-workflow', executeWorkflow),
+]);
 
 const state = reactive({
   isUploadingHost: false,

+ 4 - 0
src/components/newtab/workflows/WorkflowsHosted.vue

@@ -13,6 +13,7 @@
 import { computed } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useDialog } from '@/composable/dialog';
+import { sendMessage } from '@/utils/message';
 import { arraySorter } from '@/utils/helper';
 import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
@@ -65,4 +66,7 @@ async function deleteWorkflow(workflow) {
     },
   });
 }
+function executeWorkflow(workflow) {
+  sendMessage('workflow:execute', workflow, 'background');
+}
 </script>

+ 4 - 0
src/components/newtab/workflows/WorkflowsLocal.vue

@@ -163,6 +163,7 @@ import { shallowReactive, computed, onMounted, onBeforeUnmount } from 'vue';
 import { useI18n } from 'vue-i18n';
 import SelectionArea from '@viselect/vanilla';
 import { arraySorter } from '@/utils/helper';
+import { sendMessage } from '@/utils/message';
 import { useUserStore } from '@/stores/user';
 import { useDialog } from '@/composable/dialog';
 import { useWorkflowStore } from '@/stores/workflow';
@@ -263,6 +264,9 @@ const workflows = computed(() =>
   )
 );
 
+function executeWorkflow(workflow) {
+  sendMessage('workflow:execute', workflow, 'background');
+}
 function toggleDisableWorkflow({ id, isDisabled }) {
   workflowStore.update({
     id,

+ 5 - 0
src/components/newtab/workflows/WorkflowsShared.vue

@@ -11,6 +11,7 @@
 <script setup>
 import { computed } from 'vue';
 import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
+import { sendMessage } from '@/utils/message';
 import { arraySorter } from '@/utils/helper';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 
@@ -41,4 +42,8 @@ const workflows = computed(() => {
     order: props.sort.order,
   });
 });
+
+function executeWorkflow(workflow) {
+  sendMessage('workflow:execute', workflow, 'background');
+}
 </script>

+ 2 - 2
src/content/index.js

@@ -65,7 +65,7 @@ async function executeBlock(data) {
     }
   }
 
-  const handler = blocksHandler[toCamelCase(data.name)];
+  const handler = blocksHandler[toCamelCase(data.name || data.label)];
 
   if (handler) {
     const result = await handler(data);
@@ -74,7 +74,7 @@ async function executeBlock(data) {
     return result;
   }
 
-  const error = new Error(`"${data.name}" doesn't have a handler`);
+  const error = new Error(`"${data.label}" doesn't have a handler`);
   console.error(error);
 
   throw error;

+ 13 - 16
src/newtab/pages/logs/[id].vue

@@ -1,21 +1,14 @@
 <template>
   <div v-if="currentLog.id" class="container pt-8 pb-4">
     <div class="flex items-center">
-      <router-link
-        v-if="state.goBackBtn"
-        v-slot="{ navigate }"
-        :to="backHistory"
-        custom
+      <button
+        v-tooltip:bottom="t('workflow.blocks.go-back.name')"
+        role="button"
+        class="h-12 px-1 transition mr-2 bg-input rounded-lg dark:text-gray-300 text-gray-600"
+        @click="goBack"
       >
-        <button
-          v-tooltip:bottom="t('workflow.blocks.go-back.name')"
-          role="button"
-          class="h-12 px-1 transition mr-2 bg-input rounded-lg dark:text-gray-300 text-gray-600"
-          @click="navigate"
-        >
-          <v-remixicon name="riArrowLeftSLine" />
-        </button>
-      </router-link>
+        <v-remixicon name="riArrowLeftSLine" />
+      </button>
       <div>
         <h1 class="text-2xl max-w-md text-overflow font-semibold">
           {{ currentLog.name }}
@@ -23,7 +16,9 @@
         <p class="text-gray-600 dark:text-gray-200">
           {{
             t(`log.description.text`, {
-              status: t(`log.description.status.${currentLog.status}`),
+              status: t(
+                `log.description.status.${currentLog.status || 'success'}`
+              ),
               date: dayjs(currentLog.startedAt).format('DD MMM'),
               duration: countDuration(currentLog.startedAt, currentLog.endedAt),
             })
@@ -100,7 +95,6 @@ const tabs = [
 const state = shallowReactive({
   activeTab: 'logs',
   workflowExists: false,
-  goBackBtn: ['/logs', '/workflows'].some((str) => backHistory?.includes(str)),
 });
 const tableData = shallowReactive({
   converted: false,
@@ -115,6 +109,9 @@ const currentLog = shallowRef({
   },
 });
 
+function goBack() {
+  router.go(-1);
+}
 function deleteLog() {
   dbLogs.items
     .where('id')

+ 18 - 7
src/newtab/pages/workflows/[id].vue

@@ -88,7 +88,7 @@
             @duplicate="duplicateElements"
           />
         </ui-tab-panel>
-        <ui-tab-panel value="logs" class="mt-24">
+        <ui-tab-panel value="logs" class="mt-24 container">
           <editor-logs
             :workflow-id="route.params.id"
             :workflow-states="workflowStore.states"
@@ -139,7 +139,7 @@ import defu from 'defu';
 import { useStore } from '@/stores/main';
 import { useUserStore } from '@/stores/user';
 import { useWorkflowStore } from '@/stores/workflow';
-import { useShortcut } from '@/composable/shortcut';
+import { useShortcut, getShortcut } from '@/composable/shortcut';
 import { tasks } from '@/utils/shared';
 import { debounce, parseJSON, throttle } from '@/utils/helper';
 import { fetchApi } from '@/utils/api';
@@ -165,8 +165,6 @@ const route = useRoute();
 const router = useRouter();
 const userStore = useUserStore();
 const workflowStore = useWorkflowStore();
-/* eslint-disable-next-line */
-const shortcut = useShortcut('editor:toggle-sidebar', toggleSidebar);
 
 const editor = shallowRef(null);
 
@@ -339,6 +337,8 @@ const onNodesChange = debounce((changes) => {
         editState.editing = false;
         editState.blockData = {};
       }
+
+      state.dataChanged = true;
     }
   });
 }, 250);
@@ -568,10 +568,16 @@ function copyElements(nodes, edges, initialPos) {
   };
 }
 function duplicateElements({ nodes, edges }) {
-  editor.value.removeSelectedNodes(editor.value.getSelectedNodes.value);
-  editor.value.removeSelectedEdges(editor.value.getSelectedEdges.value);
+  const selectedNodes = editor.value.getSelectedNodes.value;
+  const selectedEdges = editor.value.getSelectedEdges.value;
 
-  const { edges: newEdges, nodes: newNodes } = copyElements(nodes, edges);
+  const { edges: newEdges, nodes: newNodes } = copyElements(
+    nodes || selectedNodes,
+    edges || selectedEdges
+  );
+
+  editor.value.removeSelectedNodes(selectedNodes);
+  editor.value.removeSelectedEdges(selectedEdges);
 
   editor.value.addNodes(newNodes);
   editor.value.addEdges(newEdges);
@@ -602,6 +608,11 @@ function onKeydown({ ctrlKey, metaKey, key }) {
   }
 }
 
+const shortcut = useShortcut([
+  getShortcut('editor:toggle-sidebar', toggleSidebar),
+  getShortcut('editor:duplicate-block', duplicateElements),
+]);
+
 /* eslint-disable consistent-return */
 onBeforeRouteLeave(() => {
   updateHostedWorkflow();

+ 6 - 2
src/utils/convertWorkflowData.js

@@ -41,16 +41,20 @@ export default function (workflow) {
       let outputName = outputIndex + 1;
 
       const isLastIndex = outputs.length - 1 === outputIndex;
-      const isConditionsFallback = block.name === 'conditions';
+      const isConditionsBlock = block.name === 'conditions';
       const isFallbackBlock = block.html === 'BlockBasicWithFallback';
       const isBlockFallback = block.html === 'BlockBasic' && outputName >= 2;
       if (
-        (isConditionsFallback || isFallbackBlock || isBlockFallback) &&
+        (isConditionsBlock || isFallbackBlock || isBlockFallback) &&
         isLastIndex
       ) {
         outputName = 'fallback';
       }
 
+      if (isConditionsBlock && !isLastIndex) {
+        outputName = block.data.conditions[outputIndex].id;
+      }
+
       connections.forEach(({ node: outputId, output }) => {
         const sourceHandle = `${block.id}-output-${outputName}`;
         const targetHandle = `${outputId}-${output.replace('_', '-')}`;

+ 1 - 1
src/utils/helper.js

@@ -151,7 +151,7 @@ export function openFilePicker(acceptedFileTypes = [], attrs = {}) {
       const { files } = event.target;
       const validFiles = [];
 
-      files.forEach((file) => {
+      Array.from(files).forEach((file) => {
         if (!acceptedFileTypes.includes(file.type)) return;
 
         validFiles.push(file);