Sfoglia il codice sorgente

refactor: blocks-handler

Ahmad Kholid 3 anni fa
parent
commit
751f85abad
43 ha cambiato i file con 1498 aggiunte e 1359 eliminazioni
  1. 2 2
      src/background/collection-engine/flow-handler.js
  2. 2 2
      src/background/collection-engine/index.js
  3. 2 2
      src/background/index.js
  4. 8 622
      src/background/workflow-engine/blocks-handler.js
  5. 43 0
      src/background/workflow-engine/blocks-handler/handler-active-tab.js
  6. 33 0
      src/background/workflow-engine/blocks-handler/handler-close-tab.js
  7. 33 0
      src/background/workflow-engine/blocks-handler/handler-condition.js
  8. 14 0
      src/background/workflow-engine/blocks-handler/handler-delay.js
  9. 20 0
      src/background/workflow-engine/blocks-handler/handler-element-exists.js
  10. 15 0
      src/background/workflow-engine/blocks-handler/handler-export-data.js
  11. 32 0
      src/background/workflow-engine/blocks-handler/handler-forward-page.js
  12. 32 0
      src/background/workflow-engine/blocks-handler/handler-go-back.js
  13. 92 0
      src/background/workflow-engine/blocks-handler/handler-interaction-handler.js
  14. 24 0
      src/background/workflow-engine/blocks-handler/handler-loop-breakpoint.js
  15. 36 0
      src/background/workflow-engine/blocks-handler/handler-loop-data.js
  16. 79 0
      src/background/workflow-engine/blocks-handler/handler-new-tab.js
  17. 27 0
      src/background/workflow-engine/blocks-handler/handler-new-window.js
  18. 21 0
      src/background/workflow-engine/blocks-handler/handler-repeat-task.js
  19. 39 0
      src/background/workflow-engine/blocks-handler/handler-switch-to.js
  20. 70 0
      src/background/workflow-engine/blocks-handler/handler-take-screenshot.js
  21. 20 0
      src/background/workflow-engine/blocks-handler/handler-trigger.js
  22. 37 0
      src/background/workflow-engine/blocks-handler/handler-webhook.js
  23. 358 0
      src/background/workflow-engine/engine.js
  24. 0 0
      src/background/workflow-engine/execute-content-script.js
  25. 24 0
      src/background/workflow-engine/helper.js
  26. 5 355
      src/background/workflow-engine/index.js
  27. 22 12
      src/components/newtab/app/AppSidebar.vue
  28. 8 350
      src/content/blocks-handler.js
  29. 27 0
      src/content/blocks-handler/handler-attribute-value.js
  30. 26 0
      src/content/blocks-handler/handler-element-exists.js
  31. 38 0
      src/content/blocks-handler/handler-element-scroll.js
  32. 13 0
      src/content/blocks-handler/handler-event-click.js
  33. 29 0
      src/content/blocks-handler/handler-forms.js
  34. 27 0
      src/content/blocks-handler/handler-get-text.js
  35. 128 0
      src/content/blocks-handler/handler-javascript-code.js
  36. 22 0
      src/content/blocks-handler/handler-link.js
  37. 22 0
      src/content/blocks-handler/handler-switch-to.js
  38. 16 0
      src/content/blocks-handler/handler-trigger-event.js
  39. 10 12
      src/content/element-selector/AppBlocks.vue
  40. 38 0
      src/content/helper.js
  41. 1 1
      src/content/index.js
  42. 2 0
      src/lib/v-remixicon.js
  43. 1 1
      src/lib/vue-i18n.js

+ 2 - 2
src/background/collection-engine/flow-handler.js

@@ -1,4 +1,4 @@
-import WorkflowEngine from '../workflow-engine';
+import workflowEngine from '../workflow-engine';
 import dataExporter from '@/utils/data-exporter';
 
 export function workflow(flow) {
@@ -26,7 +26,7 @@ export function workflow(flow) {
     const { globalData } = this.collection;
     this.currentWorkflow = currentWorkflow;
 
-    const engine = new WorkflowEngine(currentWorkflow, {
+    const engine = workflowEngine(currentWorkflow, {
       isInCollection: true,
       collectionLogId: this.id,
       collectionId: this.collection.id,

+ 2 - 2
src/background/collection-engine/index.js

@@ -3,7 +3,7 @@ import browser from 'webextension-polyfill';
 import { toCamelCase } from '@/utils/helper';
 import * as flowHandler from './flow-handler';
 import workflowState from '../workflow-state';
-import WorkflowEngine from '../workflow-engine';
+import workflowEngine from '../workflow-engine';
 
 class CollectionEngine {
   constructor(collection) {
@@ -37,7 +37,7 @@ class CollectionEngine {
           const currentWorkflow = workflows.find(({ id }) => id === itemId);
 
           if (currentWorkflow) {
-            const engine = new WorkflowEngine(currentWorkflow, {});
+            const engine = workflowEngine(currentWorkflow, {});
 
             engine.init();
           }

+ 2 - 2
src/background/index.js

@@ -1,7 +1,7 @@
 import browser from 'webextension-polyfill';
 import { MessageListener } from '@/utils/message';
 import workflowState from './workflow-state';
-import WorkflowEngine from './workflow-engine';
+import workflowEngine from './workflow-engine';
 import CollectionEngine from './collection-engine';
 import { registerSpecificDay } from '../utils/workflow-trigger';
 
@@ -20,7 +20,7 @@ const runningCollections = {};
 
 async function executeWorkflow(workflow, tabId) {
   try {
-    const engine = new WorkflowEngine(workflow, { tabId });
+    const engine = workflowEngine(workflow, { tabId });
 
     runningWorkflows[engine.id] = engine;
 

+ 8 - 622
src/background/workflow-engine/blocks-handler.js

@@ -1,626 +1,12 @@
-/* eslint-disable no-underscore-dangle */
-import browser from 'webextension-polyfill';
-import { objectHasKey, fileSaver, isObject } from '@/utils/helper';
-import { executeWebhook } from '@/utils/webhookUtil';
-import executeContentScript from '@/utils/execute-content-script';
-import dataExporter, { generateJSON } from '@/utils/data-exporter';
-import compareBlockValue from '@/utils/compare-block-value';
+import { toCamelCase } from '@/utils/helper';
 
-function getBlockConnection(block, index = 1) {
-  const blockId = block.outputs[`output_${index}`]?.connections[0]?.node;
+const blocksHandler = require.context('./blocks-handler', false, /\.js$/);
+const handlers = blocksHandler.keys().reduce((acc, key) => {
+  const name = key.replace(/^\.\/handler-|\.js/g, '');
 
-  return blockId;
-}
-function convertData(data, type) {
-  let result = data;
+  acc[toCamelCase(name)] = blocksHandler(key).default;
 
-  switch (type) {
-    case 'integer':
-      result = +data.replace(/\D+/g, '');
-      break;
-    case 'boolean':
-      result = Boolean(data);
-      break;
-    case 'array':
-      result = Array.from(data);
-      break;
-    default:
-  }
+  return acc;
+}, {});
 
-  return result;
-}
-
-export async function closeTab(block) {
-  const nextBlockId = getBlockConnection(block);
-
-  try {
-    const { data } = block;
-    let tabIds;
-
-    if (data.activeTab && this.tabId) {
-      tabIds = this.tabId;
-    } else if (data.url) {
-      tabIds = (await browser.tabs.query({ url: data.url })).map(
-        (tab) => tab.id
-      );
-    }
-
-    if (tabIds) await browser.tabs.remove(tabIds);
-
-    return {
-      nextBlockId,
-      data: '',
-    };
-  } catch (error) {
-    const errorInstance = typeof error === 'string' ? new Error(error) : error;
-    errorInstance.nextBlockId = nextBlockId;
-
-    throw error;
-  }
-}
-export async function trigger(block) {
-  const nextBlockId = getBlockConnection(block);
-  try {
-    if (block.data.type === 'visit-web' && this.tabId) {
-      this.frames = await executeContentScript(this.tabId, 'trigger');
-    }
-
-    return { nextBlockId, data: '' };
-  } catch (error) {
-    const errorInstance = new Error(error);
-    errorInstance.nextBlockId = nextBlockId;
-
-    throw errorInstance;
-  }
-}
-
-export function loopBreakpoint(block, prevBlockData) {
-  return new Promise((resolve) => {
-    const currentLoop = this.loopList[block.data.loopId];
-
-    if (
-      currentLoop &&
-      currentLoop.index < currentLoop.maxLoop - 1 &&
-      currentLoop.index <= currentLoop.data.length - 1
-    ) {
-      resolve({
-        data: '',
-        nextBlockId: currentLoop.blockId,
-      });
-    } else {
-      resolve({
-        data: prevBlockData,
-        nextBlockId: getBlockConnection(block),
-      });
-    }
-  });
-}
-
-export function loopData(block) {
-  return new Promise((resolve) => {
-    const { data } = block;
-
-    if (this.loopList[data.loopId]) {
-      this.loopList[data.loopId].index += 1;
-      this.loopData[data.loopId] =
-        this.loopList[data.loopId].data[this.loopList[data.loopId].index];
-    } else {
-      const currLoopData =
-        data.loopThrough === 'data-columns'
-          ? generateJSON(Object.keys(this.data), this.data)
-          : JSON.parse(data.loopData);
-
-      this.loopList[data.loopId] = {
-        index: 0,
-        data: currLoopData,
-        id: data.loopId,
-        blockId: block.id,
-        maxLoop: data.maxLoop || currLoopData.length,
-      };
-      /* eslint-disable-next-line */
-      this.loopData[data.loopId] = currLoopData[0];
-    }
-
-    resolve({
-      data: this.loopData[data.loopId],
-      nextBlockId: getBlockConnection(block),
-    });
-  });
-}
-
-export function goBack(block) {
-  return new Promise((resolve, reject) => {
-    const nextBlockId = getBlockConnection(block);
-
-    if (!this.tabId) {
-      const error = new Error('no-tab');
-      error.nextBlockId = nextBlockId;
-
-      reject(error);
-
-      return;
-    }
-
-    browser.tabs
-      .goBack(this.tabId)
-      .then(() => {
-        resolve({
-          nextBlockId,
-          data: '',
-        });
-      })
-      .catch((error) => {
-        error.nextBlockId = nextBlockId;
-        reject(error);
-      });
-  });
-}
-
-export function forwardPage(block) {
-  return new Promise((resolve, reject) => {
-    const nextBlockId = getBlockConnection(block);
-
-    if (!this.tabId) {
-      const error = new Error('no-tab');
-      error.nextBlockId = nextBlockId;
-
-      reject(nextBlockId);
-
-      return;
-    }
-
-    browser.tabs
-      .goForward(this.tabId)
-      .then(() => {
-        resolve({
-          nextBlockId,
-          data: '',
-        });
-      })
-      .catch((error) => {
-        error.nextBlockId = nextBlockId;
-        reject(error);
-      });
-  });
-}
-
-export async function newWindow(block) {
-  const nextBlockId = getBlockConnection(block);
-
-  try {
-    const { incognito, windowState } = block.data;
-    const { id } = await browser.windows.create({
-      incognito,
-      state: windowState,
-    });
-
-    this.windowId = id;
-
-    return {
-      data: id,
-      nextBlockId,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
-
-    throw error;
-  }
-}
-
-function tabUpdatedListener(tab) {
-  return new Promise((resolve) => {
-    this._listener({
-      name: 'tab-updated',
-      id: tab.id,
-      callback: async (tabId, changeInfo, deleteListener) => {
-        if (changeInfo.status !== 'complete') return;
-
-        const frames = await executeContentScript(tabId, 'newtab');
-
-        deleteListener();
-
-        resolve(frames);
-      },
-    });
-  });
-}
-export async function newTab(block) {
-  if (this.windowId) {
-    try {
-      await browser.windows.get(this.windowId);
-    } catch (error) {
-      this.windowId = null;
-    }
-  }
-
-  try {
-    const { updatePrevTab, url, active, inGroup } = block.data;
-
-    if (updatePrevTab && this.tabId) {
-      await browser.tabs.update(this.tabId, { url, active });
-    } else {
-      const tab = await browser.tabs.create({
-        url,
-        active,
-        windowId: this.windowId,
-      });
-
-      this.tabId = tab.id;
-      this.activeTabUrl = url;
-      this.windowId = tab.windowId;
-    }
-
-    if (inGroup && !updatePrevTab) {
-      const options = {
-        groupId: this.tabGroupId,
-        tabIds: this.tabId,
-      };
-
-      if (!this.tabGroupId) {
-        options.createProperties = {
-          windowId: this.windowId,
-        };
-      }
-
-      chrome.tabs.group(options, (tabGroupId) => {
-        this.tabGroupId = tabGroupId;
-      });
-    }
-
-    this.frameId = 0;
-    this.frames = await tabUpdatedListener.call(this, { id: this.tabId });
-
-    return {
-      data: url,
-      nextBlockId: getBlockConnection(block),
-    };
-  } catch (error) {
-    console.error(error);
-    throw error;
-  }
-}
-
-export async function activeTab(block) {
-  const nextBlockId = getBlockConnection(block);
-
-  try {
-    const data = {
-      nextBlockId,
-      data: '',
-    };
-
-    if (this.tabId) {
-      await browser.tabs.update(this.tabId, { active: true });
-
-      return data;
-    }
-
-    const [tab] = await browser.tabs.query({
-      active: true,
-      currentWindow: true,
-    });
-
-    this.frames = await executeContentScript(tab.id, 'activetab');
-
-    this.frameId = 0;
-    this.tabId = tab.id;
-    this.activeTabUrl = tab.url;
-    this.windowId = tab.windowId;
-
-    return data;
-  } catch (error) {
-    console.error(error);
-    return {
-      data: '',
-      message: error.message || error,
-      nextBlockId,
-    };
-  }
-}
-
-export async function takeScreenshot(block) {
-  const nextBlockId = getBlockConnection(block);
-  const { ext, quality, captureActiveTab, fileName } = block.data;
-
-  function saveImage(uri) {
-    const image = new Image();
-
-    image.onload = () => {
-      const name = `${fileName || 'Screenshot'}.${ext || 'png'}`;
-      const canvas = document.createElement('canvas');
-      canvas.width = image.width;
-      canvas.height = image.height;
-
-      const context = canvas.getContext('2d');
-      context.drawImage(image, 0, 0);
-
-      fileSaver(name, canvas.toDataURL());
-    };
-
-    image.src = uri;
-  }
-
-  try {
-    const options = {
-      quality,
-      format: ext || 'png',
-    };
-
-    if (captureActiveTab) {
-      if (!this.tabId) {
-        throw new Error('no-tab');
-      }
-
-      const [tab] = await browser.tabs.query({
-        active: true,
-        currentWindow: true,
-      });
-
-      await browser.windows.update(this.windowId, { focused: true });
-      await browser.tabs.update(this.tabId, { active: true });
-
-      await new Promise((resolve) => setTimeout(resolve, 500));
-
-      const uri = await browser.tabs.captureVisibleTab(options);
-
-      if (tab) {
-        await browser.windows.update(tab.windowId, { focused: true });
-        await browser.tabs.update(tab.id, { active: true });
-      }
-
-      saveImage(uri);
-    } else {
-      const uri = await browser.tabs.captureVisibleTab(options);
-
-      saveImage(uri);
-    }
-
-    return { data: '', nextBlockId };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
-
-    throw error;
-  }
-}
-
-export async function switchTo(block) {
-  const nextBlockId = getBlockConnection(block);
-
-  try {
-    if (block.data.windowType === 'main-window') {
-      this.frameId = 0;
-
-      return {
-        data: '',
-        nextBlockId,
-      };
-    }
-
-    const { url } = await this._sendMessageToTab(block, { frameId: 0 });
-
-    if (objectHasKey(this.frames, url)) {
-      this.frameId = this.frames[url];
-
-      return {
-        data: this.frameId,
-        nextBlockId,
-      };
-    }
-
-    const error = new Error('no-iframe-id');
-    error.data = { selector: block.selector };
-
-    throw error;
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
-
-    throw error;
-  }
-}
-
-export async function interactionHandler(block, prevBlockData) {
-  const nextBlockId = getBlockConnection(block);
-
-  try {
-    const refData = {
-      prevBlockData,
-      dataColumns: this.data,
-      loopData: this.loopData,
-      globalData: this.globalData,
-      activeTabUrl: this.activeTabUrl,
-    };
-    const data = await this._sendMessageToTab(
-      { ...block, refData },
-      {
-        frameId: this.frameId || 0,
-      }
-    );
-
-    if (block.name === 'link')
-      await new Promise((resolve) => setTimeout(resolve, 5000));
-
-    if (data?.isError) {
-      const error = new Error(data.message);
-      error.nextBlockId = nextBlockId;
-
-      throw error;
-    }
-
-    const getColumn = (name) =>
-      this.workflow.dataColumns.find((item) => item.name === name) || {
-        name: 'column',
-        type: 'text',
-      };
-    const pushData = (column, value) => {
-      this.data[column.name]?.push(convertData(value, column.type));
-    };
-
-    if (objectHasKey(block.data, 'dataColumn')) {
-      const column = getColumn(block.data.dataColumn);
-
-      if (block.data.saveData) {
-        if (Array.isArray(data) && column.type !== 'array') {
-          data.forEach((item) => {
-            pushData(column, item);
-          });
-        } else {
-          pushData(column, data);
-        }
-      }
-    } else if (block.name === 'javascript-code') {
-      const memoColumn = {};
-      const pushObjectData = (obj) => {
-        Object.entries(obj).forEach(([key, value]) => {
-          let column;
-
-          if (memoColumn[key]) {
-            column = memoColumn[key];
-          } else {
-            const currentColumn = getColumn(key);
-
-            column = currentColumn;
-            memoColumn[key] = currentColumn;
-          }
-
-          pushData(column, value);
-        });
-      };
-
-      if (Array.isArray(data)) {
-        data.forEach((obj) => {
-          if (isObject(obj)) pushObjectData(obj);
-        });
-      } else if (isObject(data)) {
-        pushObjectData(data);
-      }
-    }
-
-    return {
-      data,
-      nextBlockId,
-    };
-  } catch (error) {
-    error.nextBlockId = nextBlockId;
-
-    throw error;
-  }
-}
-
-export function delay(block) {
-  return new Promise((resolve) => {
-    setTimeout(() => {
-      resolve({
-        nextBlockId: getBlockConnection(block),
-        data: '',
-      });
-    }, block.data.time);
-  });
-}
-
-export function exportData(block) {
-  return new Promise((resolve) => {
-    dataExporter(this.data, block.data);
-
-    resolve({
-      data: '',
-      nextBlockId: getBlockConnection(block),
-    });
-  });
-}
-
-export function elementExists(block) {
-  return new Promise((resolve, reject) => {
-    this._sendMessageToTab(block)
-      .then((data) => {
-        resolve({
-          data,
-          nextBlockId: getBlockConnection(block, data ? 1 : 2),
-        });
-      })
-      .catch((error) => {
-        error.nextBlockId = getBlockConnection(block);
-
-        reject(error);
-      });
-  });
-}
-
-export function conditions({ data, outputs }, prevBlockData) {
-  return new Promise((resolve, reject) => {
-    if (data.conditions.length === 0) {
-      reject(new Error('Conditions is empty'));
-      return;
-    }
-
-    let outputIndex = data.conditions.length + 1;
-    let resultData = '';
-    const prevData = Array.isArray(prevBlockData)
-      ? prevBlockData[0]
-      : prevBlockData;
-
-    data.conditions.forEach(({ type, value }, index) => {
-      const result = compareBlockValue(type, prevData, value);
-
-      if (result) {
-        resultData = value;
-        outputIndex = index + 1;
-      }
-    });
-
-    resolve({
-      data: resultData,
-      nextBlockId: getBlockConnection({ outputs }, outputIndex),
-    });
-  });
-}
-
-export function repeatTask({ data, id, outputs }) {
-  return new Promise((resolve) => {
-    if (this.repeatedTasks[id] >= data.repeatFor) {
-      resolve({
-        data: data.repeatFor,
-        nextBlockId: getBlockConnection({ outputs }),
-      });
-    } else {
-      this.repeatedTasks[id] = (this.repeatedTasks[id] || 1) + 1;
-
-      resolve({
-        data: data.repeatFor,
-        nextBlockId: getBlockConnection({ outputs }, 2),
-      });
-    }
-  });
-}
-
-export function webhook({ data, outputs }) {
-  return new Promise((resolve, reject) => {
-    const nextBlockId = getBlockConnection({ outputs });
-
-    if (!data.url) {
-      const error = new Error('URL is empty');
-      error.nextBlockId = nextBlockId;
-
-      reject(error);
-      return;
-    }
-
-    if (!data.url.startsWith('http')) {
-      const error = new Error('URL is not valid');
-      error.nextBlockId = nextBlockId;
-
-      reject(error);
-      return;
-    }
-
-    executeWebhook(data)
-      .then(() => {
-        resolve({
-          data: '',
-          nextBlockId: getBlockConnection({ outputs }),
-        });
-      })
-      .catch((error) => {
-        reject(error);
-      });
-  });
-}
+export default handlers;

+ 43 - 0
src/background/workflow-engine/blocks-handler/handler-active-tab.js

@@ -0,0 +1,43 @@
+import browser from 'webextension-polyfill';
+import { getBlockConnection } from '../helper';
+import executeContentScript from '../execute-content-script';
+
+async function activeTab(block) {
+  const nextBlockId = getBlockConnection(block);
+
+  try {
+    const data = {
+      nextBlockId,
+      data: '',
+    };
+
+    if (this.tabId) {
+      await browser.tabs.update(this.tabId, { active: true });
+
+      return data;
+    }
+
+    const [tab] = await browser.tabs.query({
+      active: true,
+      currentWindow: true,
+    });
+
+    this.frames = await executeContentScript(tab.id, 'activetab');
+
+    this.frameId = 0;
+    this.tabId = tab.id;
+    this.activeTabUrl = tab.url;
+    this.windowId = tab.windowId;
+
+    return data;
+  } catch (error) {
+    console.error(error);
+    return {
+      data: '',
+      message: error.message || error,
+      nextBlockId,
+    };
+  }
+}
+
+export default activeTab;

+ 33 - 0
src/background/workflow-engine/blocks-handler/handler-close-tab.js

@@ -0,0 +1,33 @@
+import browser from 'webextension-polyfill';
+import { getBlockConnection } from '../helper';
+
+async function closeTab(block) {
+  const nextBlockId = getBlockConnection(block);
+
+  try {
+    const { data } = block;
+    let tabIds;
+
+    if (data.activeTab && this.tabId) {
+      tabIds = this.tabId;
+    } else if (data.url) {
+      tabIds = (await browser.tabs.query({ url: data.url })).map(
+        (tab) => tab.id
+      );
+    }
+
+    if (tabIds) await browser.tabs.remove(tabIds);
+
+    return {
+      nextBlockId,
+      data: '',
+    };
+  } catch (error) {
+    const errorInstance = typeof error === 'string' ? new Error(error) : error;
+    errorInstance.nextBlockId = nextBlockId;
+
+    throw error;
+  }
+}
+
+export default closeTab;

+ 33 - 0
src/background/workflow-engine/blocks-handler/handler-condition.js

@@ -0,0 +1,33 @@
+import { getBlockConnection } from '../helper';
+import compareBlockValue from '@/utils/compare-block-value';
+
+function conditions({ data, outputs }, prevBlockData) {
+  return new Promise((resolve, reject) => {
+    if (data.conditions.length === 0) {
+      reject(new Error('Conditions is empty'));
+      return;
+    }
+
+    let outputIndex = data.conditions.length + 1;
+    let resultData = '';
+    const prevData = Array.isArray(prevBlockData)
+      ? prevBlockData[0]
+      : prevBlockData;
+
+    data.conditions.forEach(({ type, value }, index) => {
+      const result = compareBlockValue(type, prevData, value);
+
+      if (result) {
+        resultData = value;
+        outputIndex = index + 1;
+      }
+    });
+
+    resolve({
+      data: resultData,
+      nextBlockId: getBlockConnection({ outputs }, outputIndex),
+    });
+  });
+}
+
+export default conditions;

+ 14 - 0
src/background/workflow-engine/blocks-handler/handler-delay.js

@@ -0,0 +1,14 @@
+import { getBlockConnection } from '../helper';
+
+function delay(block) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve({
+        nextBlockId: getBlockConnection(block),
+        data: '',
+      });
+    }, block.data.time);
+  });
+}
+
+export default delay;

+ 20 - 0
src/background/workflow-engine/blocks-handler/handler-element-exists.js

@@ -0,0 +1,20 @@
+import { getBlockConnection } from '../helper';
+
+function elementExists(block) {
+  return new Promise((resolve, reject) => {
+    this._sendMessageToTab(block)
+      .then((data) => {
+        resolve({
+          data,
+          nextBlockId: getBlockConnection(block, data ? 1 : 2),
+        });
+      })
+      .catch((error) => {
+        error.nextBlockId = getBlockConnection(block);
+
+        reject(error);
+      });
+  });
+}
+
+export default elementExists;

+ 15 - 0
src/background/workflow-engine/blocks-handler/handler-export-data.js

@@ -0,0 +1,15 @@
+import { getBlockConnection } from '../helper';
+import dataExporter from '@/utils/data-exporter';
+
+function exportData(block) {
+  return new Promise((resolve) => {
+    dataExporter(this.data, block.data);
+
+    resolve({
+      data: '',
+      nextBlockId: getBlockConnection(block),
+    });
+  });
+}
+
+export default exportData;

+ 32 - 0
src/background/workflow-engine/blocks-handler/handler-forward-page.js

@@ -0,0 +1,32 @@
+import browser from 'webextension-polyfill';
+import { getBlockConnection } from '../helper';
+
+function forwardPage(block) {
+  return new Promise((resolve, reject) => {
+    const nextBlockId = getBlockConnection(block);
+
+    if (!this.tabId) {
+      const error = new Error('no-tab');
+      error.nextBlockId = nextBlockId;
+
+      reject(nextBlockId);
+
+      return;
+    }
+
+    browser.tabs
+      .goForward(this.tabId)
+      .then(() => {
+        resolve({
+          nextBlockId,
+          data: '',
+        });
+      })
+      .catch((error) => {
+        error.nextBlockId = nextBlockId;
+        reject(error);
+      });
+  });
+}
+
+export default forwardPage;

+ 32 - 0
src/background/workflow-engine/blocks-handler/handler-go-back.js

@@ -0,0 +1,32 @@
+import browser from 'webextension-polyfill';
+import { getBlockConnection } from '../helper';
+
+export function goBack(block) {
+  return new Promise((resolve, reject) => {
+    const nextBlockId = getBlockConnection(block);
+
+    if (!this.tabId) {
+      const error = new Error('no-tab');
+      error.nextBlockId = nextBlockId;
+
+      reject(error);
+
+      return;
+    }
+
+    browser.tabs
+      .goBack(this.tabId)
+      .then(() => {
+        resolve({
+          nextBlockId,
+          data: '',
+        });
+      })
+      .catch((error) => {
+        error.nextBlockId = nextBlockId;
+        reject(error);
+      });
+  });
+}
+
+export default goBack;

+ 92 - 0
src/background/workflow-engine/blocks-handler/handler-interaction-handler.js

@@ -0,0 +1,92 @@
+import { objectHasKey } from '@/utils/helper';
+import { getBlockConnection, convertData } from '../helper';
+
+async function interactionHandler(block, prevBlockData) {
+  const nextBlockId = getBlockConnection(block);
+
+  try {
+    const refData = {
+      prevBlockData,
+      dataColumns: this.data,
+      loopData: this.loopData,
+      globalData: this.globalData,
+      activeTabUrl: this.activeTabUrl,
+    };
+    const data = await this._sendMessageToTab(
+      { ...block, refData },
+      {
+        frameId: this.frameId || 0,
+      }
+    );
+
+    if (block.name === 'link')
+      await new Promise((resolve) => setTimeout(resolve, 5000));
+
+    if (data?.isError) {
+      const error = new Error(data.message);
+      error.nextBlockId = nextBlockId;
+
+      throw error;
+    }
+
+    const getColumn = (name) =>
+      this.workflow.dataColumns.find((item) => item.name === name) || {
+        name: 'column',
+        type: 'text',
+      };
+    const pushData = (column, value) => {
+      this.data[column.name]?.push(convertData(value, column.type));
+    };
+
+    if (objectHasKey(block.data, 'dataColumn')) {
+      const column = getColumn(block.data.dataColumn);
+
+      if (block.data.saveData) {
+        if (Array.isArray(data) && column.type !== 'array') {
+          data.forEach((item) => {
+            pushData(column, item);
+          });
+        } else {
+          pushData(column, data);
+        }
+      }
+    } else if (block.name === 'javascript-code') {
+      const memoColumn = {};
+      const pushObjectData = (obj) => {
+        Object.entries(obj).forEach(([key, value]) => {
+          let column;
+
+          if (memoColumn[key]) {
+            column = memoColumn[key];
+          } else {
+            const currentColumn = getColumn(key);
+
+            column = currentColumn;
+            memoColumn[key] = currentColumn;
+          }
+
+          pushData(column, value);
+        });
+      };
+
+      if (Array.isArray(data)) {
+        data.forEach((obj) => {
+          if (isObject(obj)) pushObjectData(obj);
+        });
+      } else if (isObject(data)) {
+        pushObjectData(data);
+      }
+    }
+
+    return {
+      data,
+      nextBlockId,
+    };
+  } catch (error) {
+    error.nextBlockId = nextBlockId;
+
+    throw error;
+  }
+}
+
+export default interactionHandler;

+ 24 - 0
src/background/workflow-engine/blocks-handler/handler-loop-breakpoint.js

@@ -0,0 +1,24 @@
+import { getBlockConnection } from '../helper';
+
+function loopBreakpoint(block, prevBlockData) {
+  const currentLoop = this.loopList[block.data.loopId];
+  return new Promise((resolve) => {
+    if (
+      currentLoop &&
+      currentLoop.index < currentLoop.maxLoop - 1 &&
+      currentLoop.index <= currentLoop.data.length - 1
+    ) {
+      resolve({
+        data: '',
+        nextBlockId: currentLoop.blockId,
+      });
+    } else {
+      resolve({
+        data: prevBlockData,
+        nextBlockId: getBlockConnection(block),
+      });
+    }
+  });
+}
+
+export default loopBreakpoint;

+ 36 - 0
src/background/workflow-engine/blocks-handler/handler-loop-data.js

@@ -0,0 +1,36 @@
+import { generateJSON } from '@/utils/data-exporter';
+import { getBlockConnection } from '../helper';
+
+function loopData(block) {
+  return new Promise((resolve) => {
+    const { data } = block;
+
+    if (this.loopList[data.loopId]) {
+      this.loopList[data.loopId].index += 1;
+      this.loopData[data.loopId] =
+        this.loopList[data.loopId].data[this.loopList[data.loopId].index];
+    } else {
+      const currLoopData =
+        data.loopThrough === 'data-columns'
+          ? generateJSON(Object.keys(this.data), this.data)
+          : JSON.parse(data.loopData);
+
+      this.loopList[data.loopId] = {
+        index: 0,
+        data: currLoopData,
+        id: data.loopId,
+        blockId: block.id,
+        maxLoop: data.maxLoop || currLoopData.length,
+      };
+      /* eslint-disable-next-line */
+      this.loopData[data.loopId] = currLoopData[0];
+    }
+
+    resolve({
+      data: this.loopData[data.loopId],
+      nextBlockId: getBlockConnection(block),
+    });
+  });
+}
+
+export default loopData;

+ 79 - 0
src/background/workflow-engine/blocks-handler/handler-new-tab.js

@@ -0,0 +1,79 @@
+import browser from 'webextension-polyfill';
+import { getBlockConnection } from '../helper';
+import executeContentScript from '../execute-content-script';
+
+function tabUpdatedListener(tab) {
+  return new Promise((resolve) => {
+    this._listener({
+      name: 'tab-updated',
+      id: tab.id,
+      callback: async (tabId, changeInfo, deleteListener) => {
+        if (changeInfo.status !== 'complete') return;
+
+        const frames = await executeContentScript(tabId, 'newtab');
+
+        deleteListener();
+
+        resolve(frames);
+      },
+    });
+  });
+}
+
+async function newTab(block) {
+  if (this.windowId) {
+    try {
+      await browser.windows.get(this.windowId);
+    } catch (error) {
+      this.windowId = null;
+    }
+  }
+
+  try {
+    const { updatePrevTab, url, active, inGroup } = block.data;
+
+    if (updatePrevTab && this.tabId) {
+      await browser.tabs.update(this.tabId, { url, active });
+    } else {
+      const tab = await browser.tabs.create({
+        url,
+        active,
+        windowId: this.windowId,
+      });
+
+      this.tabId = tab.id;
+      this.activeTabUrl = url;
+      this.windowId = tab.windowId;
+    }
+
+    if (inGroup && !updatePrevTab) {
+      const options = {
+        groupId: this.tabGroupId,
+        tabIds: this.tabId,
+      };
+
+      if (!this.tabGroupId) {
+        options.createProperties = {
+          windowId: this.windowId,
+        };
+      }
+
+      chrome.tabs.group(options, (tabGroupId) => {
+        this.tabGroupId = tabGroupId;
+      });
+    }
+
+    this.frameId = 0;
+    this.frames = await tabUpdatedListener.call(this, { id: this.tabId });
+
+    return {
+      data: url,
+      nextBlockId: getBlockConnection(block),
+    };
+  } catch (error) {
+    console.error(error);
+    throw error;
+  }
+}
+
+export default newTab;

+ 27 - 0
src/background/workflow-engine/blocks-handler/handler-new-window.js

@@ -0,0 +1,27 @@
+import browser from 'webextension-polyfill';
+import { getBlockConnection } from '../helper';
+
+export async function newWindow(block) {
+  const nextBlockId = getBlockConnection(block);
+
+  try {
+    const { incognito, windowState } = block.data;
+    const { id } = await browser.windows.create({
+      incognito,
+      state: windowState,
+    });
+
+    this.windowId = id;
+
+    return {
+      data: id,
+      nextBlockId,
+    };
+  } catch (error) {
+    error.nextBlockId = nextBlockId;
+
+    throw error;
+  }
+}
+
+export default newWindow;

+ 21 - 0
src/background/workflow-engine/blocks-handler/handler-repeat-task.js

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

+ 39 - 0
src/background/workflow-engine/blocks-handler/handler-switch-to.js

@@ -0,0 +1,39 @@
+import { objectHasKey } from '@/utils/helper';
+import { getBlockConnection } from '../helper';
+
+async function switchTo(block) {
+  const nextBlockId = getBlockConnection(block);
+
+  try {
+    if (block.data.windowType === 'main-window') {
+      this.frameId = 0;
+
+      return {
+        data: '',
+        nextBlockId,
+      };
+    }
+
+    const { url } = await this._sendMessageToTab(block, { frameId: 0 });
+
+    if (objectHasKey(this.frames, url)) {
+      this.frameId = this.frames[url];
+
+      return {
+        data: this.frameId,
+        nextBlockId,
+      };
+    }
+
+    const error = new Error('no-iframe-id');
+    error.data = { selector: block.selector };
+
+    throw error;
+  } catch (error) {
+    error.nextBlockId = nextBlockId;
+
+    throw error;
+  }
+}
+
+export default switchTo;

+ 70 - 0
src/background/workflow-engine/blocks-handler/handler-take-screenshot.js

@@ -0,0 +1,70 @@
+import browser from 'webextension-polyfill';
+import { getBlockConnection } from '../helper';
+import { fileSaver } from '@/utils/helper';
+
+function saveImage(filename, fileName, uri) {
+  const image = new Image();
+
+  image.onload = () => {
+    const name = `${fileName || 'Screenshot'}.${ext || 'png'}`;
+    const canvas = document.createElement('canvas');
+    canvas.width = image.width;
+    canvas.height = image.height;
+
+    const context = canvas.getContext('2d');
+    context.drawImage(image, 0, 0);
+
+    fileSaver(name, canvas.toDataURL());
+  };
+
+  image.src = uri;
+}
+
+async function takeScreenshot(block) {
+  const nextBlockId = getBlockConnection(block);
+  const { ext, quality, captureActiveTab, fileName } = block.data;
+
+  try {
+    const options = {
+      quality,
+      format: ext || 'png',
+    };
+
+    if (captureActiveTab) {
+      if (!this.tabId) {
+        throw new Error('no-tab');
+      }
+
+      const [tab] = await browser.tabs.query({
+        active: true,
+        currentWindow: true,
+      });
+
+      await browser.windows.update(this.windowId, { focused: true });
+      await browser.tabs.update(this.tabId, { active: true });
+
+      await new Promise((resolve) => setTimeout(resolve, 500));
+
+      const uri = await browser.tabs.captureVisibleTab(options);
+
+      if (tab) {
+        await browser.windows.update(tab.windowId, { focused: true });
+        await browser.tabs.update(tab.id, { active: true });
+      }
+
+      saveImage(fileName, uri);
+    } else {
+      const uri = await browser.tabs.captureVisibleTab(options);
+
+      saveImage(fileName, uri);
+    }
+
+    return { data: '', nextBlockId };
+  } catch (error) {
+    error.nextBlockId = nextBlockId;
+
+    throw error;
+  }
+}
+
+export default takeScreenshot;

+ 20 - 0
src/background/workflow-engine/blocks-handler/handler-trigger.js

@@ -0,0 +1,20 @@
+import { getBlockConnection } from '../helper';
+
+async function trigger(block) {
+  const nextBlockId = getBlockConnection(block);
+
+  try {
+    if (block.data.type === 'visit-web' && this.tabId) {
+      this.frames = await executeContentScript(this.tabId, 'trigger');
+    }
+
+    return { nextBlockId, data: '' };
+  } catch (error) {
+    const errorInstance = new Error(error);
+    errorInstance.nextBlockId = nextBlockId;
+
+    throw errorInstance;
+  }
+}
+
+export default trigger;

+ 37 - 0
src/background/workflow-engine/blocks-handler/handler-webhook.js

@@ -0,0 +1,37 @@
+import { getBlockConnection } from '../helper';
+import { executeWebhook } from '@/utils/webhookUtil';
+
+function webhook({ data, outputs }) {
+  return new Promise((resolve, reject) => {
+    const nextBlockId = getBlockConnection({ outputs });
+
+    if (!data.url) {
+      const error = new Error('URL is empty');
+      error.nextBlockId = nextBlockId;
+
+      reject(error);
+      return;
+    }
+
+    if (!data.url.startsWith('http')) {
+      const error = new Error('URL is not valid');
+      error.nextBlockId = nextBlockId;
+
+      reject(error);
+      return;
+    }
+
+    executeWebhook(data)
+      .then(() => {
+        resolve({
+          data: '',
+          nextBlockId: getBlockConnection({ outputs }),
+        });
+      })
+      .catch((error) => {
+        reject(error);
+      });
+  });
+}
+
+export default webhook;

+ 358 - 0
src/background/workflow-engine/engine.js

@@ -0,0 +1,358 @@
+/* eslint-disable no-underscore-dangle */
+import browser from 'webextension-polyfill';
+import { nanoid } from 'nanoid';
+import { tasks } from '@/utils/shared';
+import { toCamelCase, parseJSON } from '@/utils/helper';
+import { generateJSON } from '@/utils/data-exporter';
+import errorMessage from './error-message';
+import referenceData from '@/utils/reference-data';
+import workflowState from '../workflow-state';
+import executeContentScript from './execute-content-script';
+
+let reloadTimeout;
+
+function tabRemovedHandler(tabId) {
+  if (tabId !== this.tabId) return;
+
+  delete this.tabId;
+
+  if (
+    this.currentBlock.name === 'new-tab' ||
+    tasks[this.currentBlock.name].category === 'interaction'
+  ) {
+    this.destroy('error', 'Current active tab is removed');
+  }
+
+  workflowState.update(this.id, this.state);
+}
+function tabUpdatedHandler(tabId, changeInfo, tab) {
+  const listener = this.tabUpdatedListeners[tabId];
+
+  if (listener) {
+    listener.callback(tabId, changeInfo, () => {
+      delete this.tabUpdatedListeners[tabId];
+    });
+  } else if (this.tabId === tabId) {
+    if (!reloadTimeout) {
+      reloadTimeout = setTimeout(() => {
+        this.isPaused = false;
+      }, 15000);
+    }
+
+    this.isPaused = true;
+
+    if (changeInfo.status === 'complete') {
+      clearTimeout(reloadTimeout);
+      reloadTimeout = null;
+
+      executeContentScript(tabId, 'update tab')
+        .then((frames) => {
+          this.tabId = tabId;
+          this.frames = frames;
+          this.isPaused = false;
+          this.activeTabUrl = tab?.url || '';
+        })
+        .catch((error) => {
+          console.error(error);
+          this.isPaused = false;
+        });
+    }
+  }
+}
+
+class WorkflowEngine {
+  constructor(
+    workflow,
+    { globalData, tabId = null, isInCollection, collectionLogId, blocksHandler }
+  ) {
+    const globalDataVal = globalData || workflow.globalData;
+
+    this.id = nanoid();
+    this.tabId = tabId;
+    this.workflow = workflow;
+    this.blocksHandler = blocksHandler;
+    this.isInCollection = isInCollection;
+    this.collectionLogId = collectionLogId;
+    this.globalData = parseJSON(globalDataVal, globalDataVal);
+    this.activeTabUrl = '';
+    this.data = {};
+    this.logs = [];
+    this.blocks = {};
+    this.frames = {};
+    this.loopList = {};
+    this.loopData = {};
+    this.repeatedTasks = {};
+    this.eventListeners = {};
+    this.isPaused = false;
+    this.isDestroyed = false;
+    this.frameId = null;
+    this.windowId = null;
+    this.tabGroupId = null;
+    this.currentBlock = null;
+    this.workflowTimeout = null;
+
+    this.tabUpdatedListeners = {};
+    this.tabUpdatedHandler = tabUpdatedHandler.bind(this);
+    this.tabRemovedHandler = tabRemovedHandler.bind(this);
+  }
+
+  init() {
+    if (this.workflow.isDisabled) return;
+
+    const drawflowData =
+      typeof this.workflow.drawflow === 'string'
+        ? JSON.parse(this.workflow.drawflow || '{}')
+        : this.workflow.drawflow;
+    const blocks = drawflowData?.drawflow.Home.data;
+
+    if (!blocks) {
+      console.error(errorMessage('no-block', this.workflow));
+      return;
+    }
+
+    const blocksArr = Object.values(blocks);
+    const triggerBlock = blocksArr.find(({ name }) => name === 'trigger');
+
+    if (!triggerBlock) {
+      console.error(errorMessage('no-trigger-block', this.workflow));
+      return;
+    }
+
+    browser.tabs.onUpdated.addListener(this.tabUpdatedHandler);
+    browser.tabs.onRemoved.addListener(this.tabRemovedHandler);
+
+    const dataColumns = Array.isArray(this.workflow.dataColumns)
+      ? this.workflow.dataColumns
+      : Object.values(this.workflow.dataColumns);
+
+    this.blocks = blocks;
+    this.startedTimestamp = Date.now();
+    this.workflow.dataColumns = dataColumns;
+    this.data = dataColumns.reduce(
+      (acc, column) => {
+        acc[column.name] = [];
+
+        return acc;
+      },
+      { column: [] }
+    );
+
+    workflowState
+      .add(this.id, {
+        state: this.state,
+        workflowId: this.workflow.id,
+        isInCollection: this.isInCollection,
+      })
+      .then(() => {
+        this._blockHandler(triggerBlock);
+      });
+  }
+
+  on(name, listener) {
+    (this.eventListeners[name] = this.eventListeners[name] || []).push(
+      listener
+    );
+  }
+
+  pause(pause = true) {
+    this.isPaused = pause;
+
+    workflowState.update(this.id, this.state);
+  }
+
+  stop(message) {
+    this.logs.push({
+      message,
+      type: 'stop',
+      name: 'stop',
+    });
+    this.destroy('stopped');
+  }
+
+  async destroy(status, message) {
+    try {
+      this.dispatchEvent('destroyed', { id: this.id, status, message });
+
+      this.eventListeners = {};
+      this.tabUpdatedListeners = {};
+
+      await browser.tabs.onRemoved.removeListener(this.tabRemovedHandler);
+      await browser.tabs.onUpdated.removeListener(this.tabUpdatedHandler);
+
+      await workflowState.delete(this.id);
+
+      clearTimeout(this.workflowTimeout);
+      this.isDestroyed = true;
+      this.endedTimestamp = Date.now();
+
+      if (!this.workflow.isTesting) {
+        const { logs } = await browser.storage.local.get('logs');
+        const { name, icon, id } = this.workflow;
+        const jsonData = generateJSON(Object.keys(this.data), this.data);
+
+        logs.push({
+          name,
+          icon,
+          status,
+          id: this.id,
+          workflowId: id,
+          data: jsonData,
+          history: this.logs,
+          endedAt: this.endedTimestamp,
+          startedAt: this.startedTimestamp,
+          isInCollection: this.isInCollection,
+          collectionLogId: this.collectionLogId,
+        });
+
+        await browser.storage.local.set({ logs });
+      }
+    } catch (error) {
+      console.error(error);
+    }
+  }
+
+  dispatchEvent(name, params) {
+    const listeners = this.eventListeners[name];
+
+    if (!listeners) return;
+
+    listeners.forEach((callback) => {
+      callback(params);
+    });
+  }
+
+  get state() {
+    const keys = [
+      'tabId',
+      'isPaused',
+      'isDestroyed',
+      'currentBlock',
+      'isInCollection',
+      'startedTimestamp',
+    ];
+    const state = keys.reduce((acc, key) => {
+      acc[key] = this[key];
+
+      return acc;
+    }, {});
+
+    state.name = this.workflow.name;
+    state.icon = this.workflow.icon;
+
+    return state;
+  }
+
+  _blockHandler(block, prevBlockData) {
+    if (this.isDestroyed) return;
+    if (this.isPaused) {
+      setTimeout(() => {
+        this._blockHandler(block, prevBlockData);
+      }, 1000);
+
+      return;
+    }
+
+    const disableTimeoutKeys = ['delay', 'javascript-code'];
+
+    if (!disableTimeoutKeys.includes(block.name)) {
+      this.workflowTimeout = setTimeout(() => {
+        if (!this.isDestroyed) this.stop('stop-timeout');
+      }, this.workflow.settings.timeout || 120000);
+    }
+
+    this.currentBlock = block;
+
+    workflowState.update(this.id, this.state);
+    this.dispatchEvent('update', this.state);
+
+    const started = Date.now();
+    const blockHandler = this.blocksHandler[toCamelCase(block?.name)];
+    const handler =
+      !blockHandler && tasks[block.name].category === 'interaction'
+        ? this.blocksHandler.interactionHandler
+        : blockHandler;
+
+    if (handler) {
+      const replacedBlock = referenceData(block, {
+        prevBlockData,
+        data: this.data,
+        loopData: this.loopData,
+        globalData: this.globalData,
+        activeTabUrl: this.activeTabUrl,
+      });
+
+      handler
+        .call(this, replacedBlock, prevBlockData)
+        .then((result) => {
+          clearTimeout(this.workflowTimeout);
+          this.workflowTimeout = null;
+          this.logs.push({
+            type: 'success',
+            name: block.name,
+            duration: Math.round(Date.now() - started),
+          });
+
+          if (result.nextBlockId) {
+            this._blockHandler(this.blocks[result.nextBlockId], result.data);
+          } else {
+            this.logs.push({
+              type: 'finish',
+              name: 'finish',
+            });
+            this.dispatchEvent('finish');
+            this.destroy('success');
+          }
+        })
+        .catch((error) => {
+          this.logs.push({
+            type: 'error',
+            message: error.message,
+            name: block.name,
+          });
+
+          if (
+            this.workflow.settings.onError === 'keep-running' &&
+            error.nextBlockId
+          ) {
+            this._blockHandler(
+              this.blocks[error.nextBlockId],
+              error.data || ''
+            );
+          } else {
+            this.destroy('error', error.message);
+          }
+
+          clearTimeout(this.workflowTimeout);
+          this.workflowTimeout = null;
+
+          console.error(error);
+        });
+    } else {
+      console.error(`"${block.name}" block doesn't have a handler`);
+    }
+  }
+
+  _sendMessageToTab(payload, options = {}) {
+    return new Promise((resolve, reject) => {
+      if (!this.tabId) {
+        reject(new Error('no-tab'));
+        return;
+      }
+
+      browser.tabs
+        .sendMessage(this.tabId, { isBlock: true, ...payload }, options)
+        .then(resolve)
+        .catch(reject);
+    });
+  }
+
+  _listener({ id, name, callback, once = true, ...options }) {
+    const listenerNames = {
+      event: 'eventListener',
+      'tab-updated': 'tabUpdatedListeners',
+    };
+    this[listenerNames[name]][id] = { callback, once, ...options };
+  }
+}
+
+export default WorkflowEngine;

+ 0 - 0
src/utils/execute-content-script.js → src/background/workflow-engine/execute-content-script.js


+ 24 - 0
src/background/workflow-engine/helper.js

@@ -0,0 +1,24 @@
+export function convertData(data, type) {
+  let result = data;
+
+  switch (type) {
+    case 'integer':
+      result = +data.replace(/\D+/g, '');
+      break;
+    case 'boolean':
+      result = Boolean(data);
+      break;
+    case 'array':
+      result = Array.from(data);
+      break;
+    default:
+  }
+
+  return result;
+}
+
+export function getBlockConnection(block, index = 1) {
+  const blockId = block.outputs[`output_${index}`]?.connections[0]?.node;
+
+  return blockId;
+}

+ 5 - 355
src/background/workflow-engine/index.js

@@ -1,358 +1,8 @@
-/* eslint-disable no-underscore-dangle */
-import browser from 'webextension-polyfill';
-import { nanoid } from 'nanoid';
-import { tasks } from '@/utils/shared';
-import { toCamelCase, parseJSON } from '@/utils/helper';
-import { generateJSON } from '@/utils/data-exporter';
-import errorMessage from './error-message';
-import referenceData from '@/utils/reference-data';
-import workflowState from '../workflow-state';
-import * as blocksHandler from './blocks-handler';
-import executeContentScript from '@/utils/execute-content-script';
+import Engine from './engine';
+import blocksHandler from './blocks-handler';
 
-let reloadTimeout;
+export default function (workflow, options = {}) {
+  const engine = new Engine(workflow, { ...options, blocksHandler });
 
-function tabRemovedHandler(tabId) {
-  if (tabId !== this.tabId) return;
-
-  delete this.tabId;
-
-  if (
-    this.currentBlock.name === 'new-tab' ||
-    tasks[this.currentBlock.name].category === 'interaction'
-  ) {
-    this.destroy('error', 'Current active tab is removed');
-  }
-
-  workflowState.update(this.id, this.state);
-}
-function tabUpdatedHandler(tabId, changeInfo, tab) {
-  const listener = this.tabUpdatedListeners[tabId];
-
-  if (listener) {
-    listener.callback(tabId, changeInfo, () => {
-      delete this.tabUpdatedListeners[tabId];
-    });
-  } else if (this.tabId === tabId) {
-    if (!reloadTimeout) {
-      reloadTimeout = setTimeout(() => {
-        this.isPaused = false;
-      }, 15000);
-    }
-
-    this.isPaused = true;
-
-    if (changeInfo.status === 'complete') {
-      clearTimeout(reloadTimeout);
-      reloadTimeout = null;
-
-      executeContentScript(tabId, 'update tab')
-        .then((frames) => {
-          this.tabId = tabId;
-          this.frames = frames;
-          this.isPaused = false;
-          this.activeTabUrl = tab?.url || '';
-        })
-        .catch((error) => {
-          console.error(error);
-          this.isPaused = false;
-        });
-    }
-  }
-}
-
-class WorkflowEngine {
-  constructor(
-    workflow,
-    { globalData, tabId = null, isInCollection, collectionLogId }
-  ) {
-    const globalDataVal = globalData || workflow.globalData;
-
-    this.id = nanoid();
-    this.tabId = tabId;
-    this.workflow = workflow;
-    this.isInCollection = isInCollection;
-    this.collectionLogId = collectionLogId;
-    this.globalData = parseJSON(globalDataVal, globalDataVal);
-    this.activeTabUrl = '';
-    this.data = {};
-    this.logs = [];
-    this.blocks = {};
-    this.frames = {};
-    this.loopList = {};
-    this.loopData = {};
-    this.repeatedTasks = {};
-    this.eventListeners = {};
-    this.isPaused = false;
-    this.isDestroyed = false;
-    this.frameId = null;
-    this.windowId = null;
-    this.tabGroupId = null;
-    this.currentBlock = null;
-    this.workflowTimeout = null;
-
-    this.tabUpdatedListeners = {};
-    this.tabUpdatedHandler = tabUpdatedHandler.bind(this);
-    this.tabRemovedHandler = tabRemovedHandler.bind(this);
-  }
-
-  init() {
-    if (this.workflow.isDisabled) return;
-
-    const drawflowData =
-      typeof this.workflow.drawflow === 'string'
-        ? JSON.parse(this.workflow.drawflow || '{}')
-        : this.workflow.drawflow;
-    const blocks = drawflowData?.drawflow.Home.data;
-
-    if (!blocks) {
-      console.error(errorMessage('no-block', this.workflow));
-      return;
-    }
-
-    const blocksArr = Object.values(blocks);
-    const triggerBlock = blocksArr.find(({ name }) => name === 'trigger');
-
-    if (!triggerBlock) {
-      console.error(errorMessage('no-trigger-block', this.workflow));
-      return;
-    }
-
-    browser.tabs.onUpdated.addListener(this.tabUpdatedHandler);
-    browser.tabs.onRemoved.addListener(this.tabRemovedHandler);
-
-    const dataColumns = Array.isArray(this.workflow.dataColumns)
-      ? this.workflow.dataColumns
-      : Object.values(this.workflow.dataColumns);
-
-    this.blocks = blocks;
-    this.startedTimestamp = Date.now();
-    this.workflow.dataColumns = dataColumns;
-    this.data = dataColumns.reduce(
-      (acc, column) => {
-        acc[column.name] = [];
-
-        return acc;
-      },
-      { column: [] }
-    );
-
-    workflowState
-      .add(this.id, {
-        state: this.state,
-        workflowId: this.workflow.id,
-        isInCollection: this.isInCollection,
-      })
-      .then(() => {
-        this._blockHandler(triggerBlock);
-      });
-  }
-
-  on(name, listener) {
-    (this.eventListeners[name] = this.eventListeners[name] || []).push(
-      listener
-    );
-  }
-
-  pause(pause = true) {
-    this.isPaused = pause;
-
-    workflowState.update(this.id, this.state);
-  }
-
-  stop(message) {
-    this.logs.push({
-      message,
-      type: 'stop',
-      name: 'stop',
-    });
-    this.destroy('stopped');
-  }
-
-  async destroy(status, message) {
-    try {
-      this.dispatchEvent('destroyed', { id: this.id, status, message });
-
-      this.eventListeners = {};
-      this.tabUpdatedListeners = {};
-
-      await browser.tabs.onRemoved.removeListener(this.tabRemovedHandler);
-      await browser.tabs.onUpdated.removeListener(this.tabUpdatedHandler);
-
-      await workflowState.delete(this.id);
-
-      clearTimeout(this.workflowTimeout);
-      this.isDestroyed = true;
-      this.endedTimestamp = Date.now();
-
-      if (!this.workflow.isTesting) {
-        const { logs } = await browser.storage.local.get('logs');
-        const { name, icon, id } = this.workflow;
-        const jsonData = generateJSON(Object.keys(this.data), this.data);
-
-        logs.push({
-          name,
-          icon,
-          status,
-          id: this.id,
-          workflowId: id,
-          data: jsonData,
-          history: this.logs,
-          endedAt: this.endedTimestamp,
-          startedAt: this.startedTimestamp,
-          isInCollection: this.isInCollection,
-          collectionLogId: this.collectionLogId,
-        });
-
-        await browser.storage.local.set({ logs });
-      }
-    } catch (error) {
-      console.error(error);
-    }
-  }
-
-  dispatchEvent(name, params) {
-    const listeners = this.eventListeners[name];
-
-    if (!listeners) return;
-
-    listeners.forEach((callback) => {
-      callback(params);
-    });
-  }
-
-  get state() {
-    const keys = [
-      'tabId',
-      'isPaused',
-      'isDestroyed',
-      'currentBlock',
-      'isInCollection',
-      'startedTimestamp',
-    ];
-    const state = keys.reduce((acc, key) => {
-      acc[key] = this[key];
-
-      return acc;
-    }, {});
-
-    state.name = this.workflow.name;
-    state.icon = this.workflow.icon;
-
-    return state;
-  }
-
-  _blockHandler(block, prevBlockData) {
-    if (this.isDestroyed) return;
-    if (this.isPaused) {
-      setTimeout(() => {
-        this._blockHandler(block, prevBlockData);
-      }, 1000);
-
-      return;
-    }
-
-    const disableTimeoutKeys = ['delay', 'javascript-code'];
-
-    if (!disableTimeoutKeys.includes(block.name)) {
-      this.workflowTimeout = setTimeout(() => {
-        if (!this.isDestroyed) this.stop('stop-timeout');
-      }, this.workflow.settings.timeout || 120000);
-    }
-
-    this.currentBlock = block;
-
-    workflowState.update(this.id, this.state);
-    this.dispatchEvent('update', this.state);
-
-    const started = Date.now();
-    const blockHandler = blocksHandler[toCamelCase(block?.name)];
-    const handler =
-      !blockHandler && tasks[block.name].category === 'interaction'
-        ? blocksHandler.interactionHandler
-        : blockHandler;
-
-    if (handler) {
-      const replacedBlock = referenceData(block, {
-        prevBlockData,
-        data: this.data,
-        loopData: this.loopData,
-        globalData: this.globalData,
-        activeTabUrl: this.activeTabUrl,
-      });
-
-      handler
-        .call(this, replacedBlock, prevBlockData)
-        .then((result) => {
-          clearTimeout(this.workflowTimeout);
-          this.workflowTimeout = null;
-          this.logs.push({
-            type: 'success',
-            name: block.name,
-            duration: Math.round(Date.now() - started),
-          });
-
-          if (result.nextBlockId) {
-            this._blockHandler(this.blocks[result.nextBlockId], result.data);
-          } else {
-            this.logs.push({
-              type: 'finish',
-              name: 'finish',
-            });
-            this.dispatchEvent('finish');
-            this.destroy('success');
-          }
-        })
-        .catch((error) => {
-          this.logs.push({
-            type: 'error',
-            message: error.message,
-            name: block.name,
-          });
-
-          if (
-            this.workflow.settings.onError === 'keep-running' &&
-            error.nextBlockId
-          ) {
-            this._blockHandler(
-              this.blocks[error.nextBlockId],
-              error.data || ''
-            );
-          } else {
-            this.destroy('error', error.message);
-          }
-
-          clearTimeout(this.workflowTimeout);
-          this.workflowTimeout = null;
-
-          console.error(error);
-        });
-    } else {
-      console.error(`"${block.name}" block doesn't have a handler`);
-    }
-  }
-
-  _sendMessageToTab(payload, options = {}) {
-    return new Promise((resolve, reject) => {
-      if (!this.tabId) {
-        reject(new Error('no-tab'));
-        return;
-      }
-
-      browser.tabs
-        .sendMessage(this.tabId, { isBlock: true, ...payload }, options)
-        .then(resolve)
-        .catch(reject);
-    });
-  }
-
-  _listener({ id, name, callback, once = true, ...options }) {
-    const listenerNames = {
-      event: 'eventListener',
-      'tab-updated': 'tabUpdatedListeners',
-    };
-    this[listenerNames[name]][id] = { callback, once, ...options };
-  }
+  return engine;
 }
-
-export default WorkflowEngine;

+ 22 - 12
src/components/newtab/app/AppSidebar.vue

@@ -70,22 +70,15 @@
       </template>
       <ui-list class="space-y-1">
         <ui-list-item
+          v-for="item in links"
+          :key="item.name"
+          :href="item.url"
           tag="a"
-          href="https://github.com/kholid060/automa/wiki"
           rel="noopener"
           target="_blank"
         >
-          <v-remixicon name="riBookOpenLine" class="-ml-1 mr-2" />
-          <span>{{ t('common.docs', 2) }}</span>
-        </ui-list-item>
-        <ui-list-item
-          tag="a"
-          href="https://github.com/kholid060/automa"
-          rel="noopener"
-          target="_blank"
-        >
-          <v-remixicon name="riGithubFill" class="-ml-1 mr-2" />
-          <span>GitHub</span>
+          <v-remixicon :name="item.icon" class="-ml-1 mr-2" />
+          <span>{{ item.name }}</span>
         </ui-list-item>
       </ui-list>
     </ui-popover>
@@ -99,6 +92,23 @@ import { useGroupTooltip } from '@/composable/groupTooltip';
 useGroupTooltip();
 const { t } = useI18n();
 
+const links = [
+  {
+    name: 'Donate',
+    icon: 'riHandHeartLine',
+    url: 'https://paypal.me/akholid060',
+  },
+  {
+    name: t('common.docs', 2),
+    icon: 'riBookOpenLine',
+    url: 'https://github.com/kholid060/automa/wiki',
+  },
+  {
+    name: 'GitHub',
+    icon: 'riGithubFill',
+    url: 'https://github.com/kholid060/automa',
+  },
+];
 const tabs = [
   {
     id: 'dashboard',

+ 8 - 350
src/content/blocks-handler.js

@@ -1,354 +1,12 @@
-/* eslint-disable consistent-return, no-param-reassign */
-import simulateEvent from '@/utils/simulate-event';
-import handleFormElement from '@/utils/handle-form-element';
-import { generateJSON } from '@/utils/data-exporter';
-import { sendMessage } from '@/utils/message';
+import { toCamelCase } from '@/utils/helper';
 
-function markElement(el, { id, data }) {
-  if (data.markEl) {
-    el.setAttribute(`block--${id}`, '');
-  }
-}
-function handleElement({ data, id }, callback, errCallback) {
-  if (!data || !data.selector) return null;
+const blocksHandler = require.context('./blocks-handler', false, /\.js$/);
+const handlers = blocksHandler.keys().reduce((acc, key) => {
+  const name = key.replace(/^\.\/handler-|\.js/g, '');
 
-  try {
-    const blockIdAttr = `block--${id}`;
-    const selector = data.markEl
-      ? `${data.selector.trim()}:not([${blockIdAttr}])`
-      : data.selector;
+  acc[toCamelCase(name)] = blocksHandler(key).default;
 
-    const element = data.multiple
-      ? document.querySelectorAll(selector)
-      : document.querySelector(selector);
+  return acc;
+}, {});
 
-    if (typeof callback === 'boolean' && callback) return element;
-
-    if (data.multiple) {
-      element.forEach((el) => {
-        markElement(el, { id, data });
-        callback(el);
-      });
-    } else if (element) {
-      markElement(element, { id, data });
-      callback(element);
-    } else if (errCallback) {
-      errCallback();
-    }
-  } catch (error) {
-    console.error(error);
-  }
-}
-
-export function switchTo(block) {
-  return new Promise((resolve) => {
-    handleElement(
-      block,
-      (element) => {
-        if (element.tagName !== 'IFRAME') {
-          resolve('');
-          return;
-        }
-
-        resolve({ url: element.src });
-      },
-      () => {
-        resolve('');
-      }
-    );
-  });
-}
-
-export function eventClick(block) {
-  return new Promise((resolve) => {
-    handleElement(block, (element) => {
-      element.click();
-    });
-
-    resolve('');
-  });
-}
-
-export function getText(block) {
-  return new Promise((resolve) => {
-    let regex;
-    const { regex: regexData, regexExp, prefixText, suffixText } = block.data;
-    const textResult = [];
-
-    if (regexData) {
-      regex = new RegExp(regexData, regexExp.join(''));
-    }
-
-    handleElement(block, (element) => {
-      let text = element.innerText;
-
-      if (regex) text = text.match(regex).join(' ');
-
-      text = (prefixText || '') + text + (suffixText || '');
-
-      textResult.push(text);
-    });
-
-    resolve(textResult);
-  });
-}
-
-export function javascriptCode(block) {
-  block.refData.dataColumns = generateJSON(
-    Object.keys(block.refData.dataColumns),
-    block.refData.dataColumns
-  );
-
-  sessionStorage.setItem(`automa--${block.id}`, JSON.stringify(block.refData));
-  const automaScript = `
-function automaNextBlock(data) {
-  window.dispatchEvent(new CustomEvent('__automa-next-block__', { detail: data }));
-}
-function automaResetTimeout() {
- window.dispatchEvent(new CustomEvent('__automa-reset-timeout__'));
-}
-function findData(obj, path) {
-  const paths = path.split('.');
-  const isWhitespace = paths.length === 1 && !/\\S/.test(paths[0]);
-
-  if (paths.length === 0 || isWhitespace) return obj;
-
-  let current = obj;
-
-  for (let i = 0; i < paths.length; i++) {
-    if (current[paths[i]] == undefined) {
-      return undefined;
-    } else {
-      current = current[paths[i]];
-    }
-  }
-
-  return current;
-}
-function automaRefData(keyword, path = '') {
-  const data = JSON.parse(sessionStorage.getItem('automa--${block.id}')) || null;
-
-  if (data === null) return null;
-
-  return findData(data[keyword], path);
-}
-`;
-
-  return new Promise((resolve) => {
-    const isScriptExists = document.getElementById('automa-custom-js');
-    const scriptAttr = `block--${block.id}`;
-
-    if (isScriptExists && isScriptExists.hasAttribute(scriptAttr)) {
-      resolve('');
-      return;
-    }
-
-    const promisePreloadScripts =
-      block.data?.preloadScripts.map(async (item) => {
-        try {
-          const { protocol, pathname } = new URL(item.src);
-          const isValidUrl = /https?/.test(protocol) && /\.js$/.test(pathname);
-
-          if (!isValidUrl) return null;
-
-          const script = await sendMessage(
-            'fetch:text',
-            item.src,
-            'background'
-          );
-          const scriptEl = document.createElement('script');
-
-          scriptEl.type = 'text/javascript';
-          scriptEl.innerHTML = script;
-
-          return {
-            ...item,
-            script: scriptEl,
-          };
-        } catch (error) {
-          return null;
-        }
-      }, []) || [];
-
-    Promise.allSettled(promisePreloadScripts).then((result) => {
-      const preloadScripts = result.reduce((acc, { status, value }) => {
-        if (status !== 'fulfilled' || !value) return acc;
-
-        acc.push(value);
-        document.body.appendChild(value.script);
-
-        return acc;
-      }, []);
-
-      const script = document.createElement('script');
-      let timeout;
-
-      script.setAttribute(scriptAttr, '');
-      script.id = 'automa-custom-js';
-      script.innerHTML = `(() => {\n${automaScript} ${block.data.code}\n})()`;
-
-      const cleanUp = (data = '') => {
-        script.remove();
-        preloadScripts.forEach((item) => {
-          if (item.removeAfterExec) item.script.remove();
-        });
-        sessionStorage.removeItem(`automa--${block.id}`);
-        resolve(data);
-      };
-
-      window.addEventListener('__automa-next-block__', ({ detail }) => {
-        clearTimeout(timeout);
-        cleanUp(detail || {});
-      });
-      window.addEventListener('__automa-reset-timeout__', () => {
-        clearTimeout(timeout);
-
-        timeout = setTimeout(cleanUp, block.data.timeout);
-      });
-
-      document.body.appendChild(script);
-
-      timeout = setTimeout(cleanUp, block.data.timeout);
-    });
-  });
-}
-
-export function elementScroll(block) {
-  function incScrollPos(element, data, vertical = true) {
-    let currentPos = vertical ? element.scrollTop : element.scrollLeft;
-
-    if (data.incY) {
-      currentPos += data.scrollY;
-    } else if (data.incX) {
-      currentPos += data.scrollX;
-    }
-
-    return currentPos;
-  }
-
-  return new Promise((resolve) => {
-    const { data } = block;
-    const behavior = data.smooth ? 'smooth' : 'auto';
-
-    handleElement(block, (element) => {
-      if (data.scrollIntoView) {
-        element.scrollIntoView({ behavior, block: 'center' });
-      } else {
-        element.scroll({
-          behavior,
-          top: data.incY ? incScrollPos(element, data) : data.scrollY,
-          left: data.incX ? incScrollPos(element, data, false) : data.scrollX,
-        });
-      }
-    });
-
-    window.dispatchEvent(new Event('scroll'));
-
-    resolve('');
-  });
-}
-
-export function attributeValue(block) {
-  return new Promise((resolve) => {
-    let result = [];
-    const { attributeName, multiple } = block.data;
-    const isCheckboxOrRadio = (element) => {
-      if (element.tagName !== 'INPUT') return false;
-
-      return ['checkbox', 'radio'].includes(element.getAttribute('type'));
-    };
-
-    handleElement(block, (element) => {
-      const value =
-        attributeName === 'checked' && isCheckboxOrRadio(element)
-          ? element.checked
-          : element.getAttribute(attributeName);
-
-      if (multiple) result.push(value);
-      else result = value;
-    });
-
-    resolve(result);
-  });
-}
-
-export function forms(block) {
-  return new Promise((resolve) => {
-    const { data } = block;
-    const elements = handleElement(block, true);
-
-    if (data.multiple) {
-      const promises = Array.from(elements).map((element) => {
-        return new Promise((eventResolve) => {
-          markElement(element, block);
-          handleFormElement(element, data, eventResolve);
-        });
-      });
-
-      Promise.allSettled(promises).then(() => {
-        resolve('');
-      });
-    } else if (elements) {
-      markElement(elements, block);
-      handleFormElement(elements, data, resolve);
-    } else {
-      resolve('');
-    }
-  });
-}
-
-export function triggerEvent(block) {
-  return new Promise((resolve) => {
-    const { data } = block;
-
-    handleElement(block, (element) => {
-      simulateEvent(element, data.eventName, data.eventParams);
-    });
-
-    resolve(data.eventName);
-  });
-}
-
-export function link(block) {
-  return new Promise((resolve) => {
-    const element = document.querySelector(block.data.selector);
-
-    if (!element) {
-      resolve('');
-      return;
-    }
-
-    markElement(element, block);
-
-    const url = element.href;
-
-    if (url) window.location.href = url;
-
-    resolve(url);
-  });
-}
-
-export function elementExists({ data }) {
-  return new Promise((resolve) => {
-    let trying = 0;
-
-    function checkElement() {
-      if (trying >= (data.tryCount || 1)) {
-        resolve(false);
-        return;
-      }
-
-      const element = document.querySelector(data.selector);
-
-      if (element) {
-        resolve(true);
-      } else {
-        trying += 1;
-
-        setTimeout(checkElement, data.timeout || 500);
-      }
-    }
-
-    checkElement();
-  });
-}
+export default handlers;

+ 27 - 0
src/content/blocks-handler/handler-attribute-value.js

@@ -0,0 +1,27 @@
+import { handleElement } from '../helper';
+
+function attributeValue(block) {
+  return new Promise((resolve) => {
+    let result = [];
+    const { attributeName, multiple } = block.data;
+    const isCheckboxOrRadio = (element) => {
+      if (element.tagName !== 'INPUT') return false;
+
+      return ['checkbox', 'radio'].includes(element.getAttribute('type'));
+    };
+
+    handleElement(block, (element) => {
+      const value =
+        attributeName === 'checked' && isCheckboxOrRadio(element)
+          ? element.checked
+          : element.getAttribute(attributeName);
+
+      if (multiple) result.push(value);
+      else result = value;
+    });
+
+    resolve(result);
+  });
+}
+
+export default attributeValue;

+ 26 - 0
src/content/blocks-handler/handler-element-exists.js

@@ -0,0 +1,26 @@
+function elementExists({ data }) {
+  return new Promise((resolve) => {
+    let trying = 0;
+
+    function checkElement() {
+      if (trying >= (data.tryCount || 1)) {
+        resolve(false);
+        return;
+      }
+
+      const element = document.querySelector(data.selector);
+
+      if (element) {
+        resolve(true);
+      } else {
+        trying += 1;
+
+        setTimeout(checkElement, data.timeout || 500);
+      }
+    }
+
+    checkElement();
+  });
+}
+
+export default elementExists;

+ 38 - 0
src/content/blocks-handler/handler-element-scroll.js

@@ -0,0 +1,38 @@
+import { handleElement } from '../helper';
+
+function elementScroll(block) {
+  function incScrollPos(element, data, vertical = true) {
+    let currentPos = vertical ? element.scrollTop : element.scrollLeft;
+
+    if (data.incY) {
+      currentPos += data.scrollY;
+    } else if (data.incX) {
+      currentPos += data.scrollX;
+    }
+
+    return currentPos;
+  }
+
+  return new Promise((resolve) => {
+    const { data } = block;
+    const behavior = data.smooth ? 'smooth' : 'auto';
+
+    handleElement(block, (element) => {
+      if (data.scrollIntoView) {
+        element.scrollIntoView({ behavior, block: 'center' });
+      } else {
+        element.scroll({
+          behavior,
+          top: data.incY ? incScrollPos(element, data) : data.scrollY,
+          left: data.incX ? incScrollPos(element, data, false) : data.scrollX,
+        });
+      }
+    });
+
+    window.dispatchEvent(new Event('scroll'));
+
+    resolve('');
+  });
+}
+
+export default elementScroll;

+ 13 - 0
src/content/blocks-handler/handler-event-click.js

@@ -0,0 +1,13 @@
+import { handleElement } from '../helper';
+
+function eventClick(block) {
+  return new Promise((resolve) => {
+    handleElement(block, (element) => {
+      element.click();
+    });
+
+    resolve('');
+  });
+}
+
+export default eventClick;

+ 29 - 0
src/content/blocks-handler/handler-forms.js

@@ -0,0 +1,29 @@
+import { handleElement, markElement } from '../helper';
+import handleFormElement from '@/utils/handle-form-element';
+
+function forms(block) {
+  return new Promise((resolve) => {
+    const { data } = block;
+    const elements = handleElement(block, true);
+
+    if (data.multiple) {
+      const promises = Array.from(elements).map((element) => {
+        return new Promise((eventResolve) => {
+          markElement(element, block);
+          handleFormElement(element, data, eventResolve);
+        });
+      });
+
+      Promise.allSettled(promises).then(() => {
+        resolve('');
+      });
+    } else if (elements) {
+      markElement(elements, block);
+      handleFormElement(elements, data, resolve);
+    } else {
+      resolve('');
+    }
+  });
+}
+
+export default forms;

+ 27 - 0
src/content/blocks-handler/handler-get-text.js

@@ -0,0 +1,27 @@
+import { handleElement } from '../helper';
+
+function getText(block) {
+  return new Promise((resolve) => {
+    let regex;
+    const { regex: regexData, regexExp, prefixText, suffixText } = block.data;
+    const textResult = [];
+
+    if (regexData) {
+      regex = new RegExp(regexData, regexExp.join(''));
+    }
+
+    handleElement(block, (element) => {
+      let text = element.innerText;
+
+      if (regex) text = text.match(regex).join(' ');
+
+      text = (prefixText || '') + text + (suffixText || '');
+
+      textResult.push(text);
+    });
+
+    resolve(textResult);
+  });
+}
+
+export default getText;

+ 128 - 0
src/content/blocks-handler/handler-javascript-code.js

@@ -0,0 +1,128 @@
+import { sendMessage } from '@/utils/message';
+import { generateJSON } from '@/utils/data-exporter';
+
+function getAutomaScript(blockId) {
+  return `
+function automaNextBlock(data) {
+  window.dispatchEvent(new CustomEvent('__automa-next-block__', { detail: data }));
+}
+function automaResetTimeout() {
+ window.dispatchEvent(new CustomEvent('__automa-reset-timeout__'));
+}
+function findData(obj, path) {
+  const paths = path.split('.');
+  const isWhitespace = paths.length === 1 && !/\\S/.test(paths[0]);
+
+  if (paths.length === 0 || isWhitespace) return obj;
+
+  let current = obj;
+
+  for (let i = 0; i < paths.length; i++) {
+    if (current[paths[i]] == undefined) {
+      return undefined;
+    } else {
+      current = current[paths[i]];
+    }
+  }
+
+  return current;
+}
+function automaRefData(keyword, path = '') {
+  const data = JSON.parse(sessionStorage.getItem('automa--${blockId}')) || null;
+
+  if (data === null) return null;
+
+  return findData(data[keyword], path);
+}
+  `;
+}
+
+function javascriptCode(block) {
+  block.refData.dataColumns = generateJSON(
+    Object.keys(block.refData.dataColumns),
+    block.refData.dataColumns
+  );
+
+  sessionStorage.setItem(`automa--${block.id}`, JSON.stringify(block.refData));
+  const automaScript = getAutomaScript(block.id);
+
+  return new Promise((resolve) => {
+    const isScriptExists = document.getElementById('automa-custom-js');
+    const scriptAttr = `block--${block.id}`;
+
+    if (isScriptExists && isScriptExists.hasAttribute(scriptAttr)) {
+      resolve('');
+      return;
+    }
+
+    const promisePreloadScripts =
+      block.data?.preloadScripts.map(async (item) => {
+        try {
+          const { protocol, pathname } = new URL(item.src);
+          const isValidUrl = /https?/.test(protocol) && /\.js$/.test(pathname);
+
+          if (!isValidUrl) return null;
+
+          const script = await sendMessage(
+            'fetch:text',
+            item.src,
+            'background'
+          );
+          const scriptEl = document.createElement('script');
+
+          scriptEl.type = 'text/javascript';
+          scriptEl.innerHTML = script;
+
+          return {
+            ...item,
+            script: scriptEl,
+          };
+        } catch (error) {
+          return null;
+        }
+      }, []) || [];
+
+    Promise.allSettled(promisePreloadScripts).then((result) => {
+      const preloadScripts = result.reduce((acc, { status, value }) => {
+        if (status !== 'fulfilled' || !value) return acc;
+
+        acc.push(value);
+        document.body.appendChild(value.script);
+
+        return acc;
+      }, []);
+
+      const script = document.createElement('script');
+      let timeout;
+
+      script.setAttribute(scriptAttr, '');
+      script.id = 'automa-custom-js';
+      script.innerHTML = `(() => {\n${automaScript} ${block.data.code}\n})()`;
+
+      const cleanUp = (data = '') => {
+        script.remove();
+        preloadScripts.forEach((item) => {
+          if (item.removeAfterExec) item.script.remove();
+        });
+        sessionStorage.removeItem(`automa--${block.id}`);
+        resolve(data);
+      };
+
+      window.addEventListener('__automa-next-block__', ({ detail }) => {
+        clearTimeout(timeout);
+        cleanUp(detail || {});
+      });
+      window.addEventListener('__automa-reset-timeout__', () => {
+        clearTimeout(timeout);
+
+        timeout = setTimeout(cleanUp, block.data.timeout);
+      });
+
+      document.body.appendChild(script);
+
+      timeout = setTimeout(cleanUp, block.data.timeout);
+    });
+  });
+}
+
+export default javascriptCode;

+ 22 - 0
src/content/blocks-handler/handler-link.js

@@ -0,0 +1,22 @@
+import { markElement } from '../helper';
+
+function link(block) {
+  return new Promise((resolve) => {
+    const element = document.querySelector(block.data.selector);
+
+    if (!element) {
+      resolve('');
+      return;
+    }
+
+    markElement(element, block);
+
+    const url = element.href;
+
+    if (url) window.location.href = url;
+
+    resolve(url);
+  });
+}
+
+export default link;

+ 22 - 0
src/content/blocks-handler/handler-switch-to.js

@@ -0,0 +1,22 @@
+import { handleElement } from '../helper';
+
+function switchTo(block) {
+  return new Promise((resolve) => {
+    handleElement(
+      block,
+      (element) => {
+        if (element.tagName !== 'IFRAME') {
+          resolve('');
+          return;
+        }
+
+        resolve({ url: element.src });
+      },
+      () => {
+        resolve('');
+      }
+    );
+  });
+}
+
+export default switchTo;

+ 16 - 0
src/content/blocks-handler/handler-trigger-event.js

@@ -0,0 +1,16 @@
+import { handleElement } from '../helper';
+import simulateEvent from '@/utils/simulate-event';
+
+function triggerEvent(block) {
+  return new Promise((resolve) => {
+    const { data } = block;
+
+    handleElement(block, (element) => {
+      simulateEvent(element, data.eventName, data.eventParams);
+    });
+
+    resolve(data.eventName);
+  });
+}
+
+export default triggerEvent;

+ 10 - 12
src/content/element-selector/AppBlocks.vue

@@ -31,13 +31,11 @@
 <script setup>
 import { shallowReactive } from 'vue';
 import { tasks } from '@/utils/shared';
-import {
-  forms,
-  getText,
-  eventClick,
-  triggerEvent,
-  elementScroll,
-} from '../blocks-handler';
+import handleForms from '../blocks-handler/handler-forms';
+import handleGetText from '../blocks-handler/handler-get-text';
+import handleEventClick from '../blocks-handler/handler-event-click';
+import handelTriggerEvent from '../blocks-handler/handler-trigger-event';
+import handleElementScroll from '../blocks-handler/handler-element-scroll';
 import EditForms from '@/components/newtab/workflow/edit/EditForms.vue';
 import EditTriggerEvent from '@/components/newtab/workflow/edit/EditTriggerEvent.vue';
 import EditScrollElement from '@/components/newtab/workflow/edit/EditScrollElement.vue';
@@ -54,27 +52,27 @@ const blocks = {
   forms: {
     ...tasks.forms,
     component: EditForms,
-    handler: forms,
+    handler: handleForms,
   },
   'get-text': {
     ...tasks['get-text'],
     component: '',
-    handler: getText,
+    handler: handleGetText,
   },
   'event-click': {
     ...tasks['event-click'],
     component: '',
-    handler: eventClick,
+    handler: handleEventClick,
   },
   'trigger-event': {
     ...tasks['trigger-event'],
     component: EditTriggerEvent,
-    handler: triggerEvent,
+    handler: handelTriggerEvent,
   },
   'element-scroll': {
     ...tasks['element-scroll'],
     component: EditScrollElement,
-    handler: elementScroll,
+    handler: handleElementScroll,
   },
 };
 

+ 38 - 0
src/content/helper.js

@@ -0,0 +1,38 @@
+/* eslint-disable consistent-return */
+
+export function markElement(el, { id, data }) {
+  if (data.markEl) {
+    el.setAttribute(`block--${id}`, '');
+  }
+}
+
+export function handleElement({ data, id }, callback, errCallback) {
+  if (!data || !data.selector) return null;
+
+  try {
+    const blockIdAttr = `block--${id}`;
+    const selector = data.markEl
+      ? `${data.selector.trim()}:not([${blockIdAttr}])`
+      : data.selector;
+
+    const element = data.multiple
+      ? document.querySelectorAll(selector)
+      : document.querySelector(selector);
+
+    if (typeof callback === 'boolean' && callback) return element;
+
+    if (data.multiple) {
+      element.forEach((el) => {
+        markElement(el, { id, data });
+        callback(el);
+      });
+    } else if (element) {
+      markElement(element, { id, data });
+      callback(element);
+    } else if (errCallback) {
+      errCallback();
+    }
+  } catch (error) {
+    console.error(error);
+  }
+}

+ 1 - 1
src/content/index.js

@@ -1,7 +1,7 @@
 import browser from 'webextension-polyfill';
 import { toCamelCase } from '@/utils/helper';
 import elementSelector from './element-selector';
-import * as blocksHandler from './blocks-handler';
+import blocksHandler from './blocks-handler';
 
 (() => {
   browser.runtime.onMessage.addListener((data) => {

+ 2 - 0
src/lib/v-remixicon.js

@@ -1,6 +1,7 @@
 import vRemixicon from 'v-remixicon';
 import {
   riHome5Line,
+  riHandHeartLine,
   riFileCopyLine,
   riToggleLine,
   riFolderLine,
@@ -75,6 +76,7 @@ import {
 
 export const icons = {
   riHome5Line,
+  riHandHeartLine,
   riFileCopyLine,
   riToggleLine,
   riFolderLine,

+ 1 - 1
src/lib/vue-i18n.js

@@ -10,7 +10,7 @@ const i18n = createI18n({
 
 export function setI18nLanguage(locale) {
   i18n.global.locale.value = locale;
-  console.log(i18n.global);
+
   document.querySelector('html').setAttribute('lang', locale);
 }