Browse Source

feat: validate workflow before executing

Ahmad Kholid 3 years ago
parent
commit
efc8b025c8

+ 21 - 16
src/background/index.js

@@ -1,19 +1,20 @@
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
+import { nanoid } from 'nanoid';
 import { MessageListener } from '@/utils/message';
 import { MessageListener } from '@/utils/message';
+import executingWorkflow from '@/utils/executing-workflow';
 import WorkflowEngine from './workflow-engine';
 import WorkflowEngine from './workflow-engine';
 
 
-chrome.runtime.onInstalled.addListener((details) => {
+browser.runtime.onInstalled.addListener((details) => {
   if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {
   if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {
-    chrome.storage.local.set({
+    browser.storage.local.set({
+      logs: [],
       workflows: [],
       workflows: [],
       visitWebTriggers: [],
       visitWebTriggers: [],
-      tasks: [],
+      executingWorkflow: [],
     });
     });
   }
   }
 });
 });
 
 
-const executingWorkflow = {};
-
 function getWorkflow(workflowId) {
 function getWorkflow(workflowId) {
   return new Promise((resolve) => {
   return new Promise((resolve) => {
     browser.storage.local.get('workflows').then(({ workflows }) => {
     browser.storage.local.get('workflows').then(({ workflows }) => {
@@ -23,17 +24,18 @@ function getWorkflow(workflowId) {
     });
     });
   });
   });
 }
 }
-function executeWorkflow(workflow) {
+async function executeWorkflow(workflow) {
   try {
   try {
-    /* to-do handle running workflow & validate if a tab is using by workflow */
-    console.log(executingWorkflow[workflow.id]);
-    if (executingWorkflow[workflow.id]) return true;
+    const id = nanoid();
+    const engine = new WorkflowEngine(id, workflow);
 
 
-    const engine = new WorkflowEngine(workflow);
-    console.log('execute');
-    engine.init();
+    executingWorkflow.set(id, {});
 
 
-    executingWorkflow[workflow.id] = engine;
+    engine.init();
+    engine.on('destroyed', () => {
+      console.log('destroyed...');
+      executingWorkflow.delete(workflow.id);
+    });
 
 
     return true;
     return true;
   } catch (error) {
   } catch (error) {
@@ -44,7 +46,7 @@ function executeWorkflow(workflow) {
 
 
 browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
 browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
   if (changeInfo.status === 'complete') {
   if (changeInfo.status === 'complete') {
-    const { visitWebTriggers = [] } = await browser.storage.local.get(
+    const { visitWebTriggers } = await browser.storage.local.get(
       'visitWebTriggers'
       'visitWebTriggers'
     );
     );
     const trigger = visitWebTriggers.find(({ url, isRegex }) => {
     const trigger = visitWebTriggers.find(({ url, isRegex }) => {
@@ -52,8 +54,11 @@ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
 
 
       return tab.url.match(isRegex ? new RegExp(url, 'g') : url);
       return tab.url.match(isRegex ? new RegExp(url, 'g') : url);
     });
     });
-
-    if (trigger) {
+    const executedWorkflow = await executingWorkflow.find(
+      ({ workflow }) => workflow?.tabId === tabId
+    );
+    console.log(executedWorkflow, 'wo');
+    if (trigger && !executedWorkflow) {
       const workflow = await getWorkflow(trigger.id);
       const workflow = await getWorkflow(trigger.id);
 
 
       executeWorkflow(workflow);
       executeWorkflow(workflow);

+ 49 - 5
src/background/workflow-engine.js

@@ -3,6 +3,7 @@ import browser from 'webextension-polyfill';
 import { toCamelCase } from '@/utils/helper';
 import { toCamelCase } from '@/utils/helper';
 import { tasks } from '@/utils/shared';
 import { tasks } from '@/utils/shared';
 import * as blocksHandler from './blocks-handler';
 import * as blocksHandler from './blocks-handler';
+import executingWorkflow from '@/utils/executing-workflow';
 
 
 function tabMessageHandler({ type, data }) {
 function tabMessageHandler({ type, data }) {
   const listener = this.tabMessageListeners[type];
   const listener = this.tabMessageListeners[type];
@@ -56,17 +57,19 @@ function tabUpdatedHandler(tabId, changeInfo) {
 }
 }
 
 
 class WorkflowEngine {
 class WorkflowEngine {
-  constructor(workflow) {
+  constructor(id, workflow) {
+    this.id = id;
     this.workflow = workflow;
     this.workflow = workflow;
+    this.data = {};
     this.blocks = {};
     this.blocks = {};
+    this.eventListeners = {};
+    this.repeatedTasks = {};
+    this.logs = [];
     this.blocksArr = [];
     this.blocksArr = [];
-    this.data = {};
-    this.isDestroyed = false;
     this.isPaused = false;
     this.isPaused = false;
+    this.isDestroyed = false;
     this.isInsidePaused = false;
     this.isInsidePaused = false;
-    this.logs = [];
     this.currentBlock = null;
     this.currentBlock = null;
-    this.repeatedTasks = {};
 
 
     this.tabMessageListeners = {};
     this.tabMessageListeners = {};
     this.tabUpdatedListeners = {};
     this.tabUpdatedListeners = {};
@@ -100,16 +103,40 @@ class WorkflowEngine {
 
 
     this.blocks = blocks;
     this.blocks = blocks;
     this.blocksArr = blocksArr;
     this.blocksArr = blocksArr;
+    this.startedTimestamp = Date.now();
 
 
     this._blockHandler(triggerBlock);
     this._blockHandler(triggerBlock);
   }
   }
 
 
+  on(name, listener) {
+    (this.eventListeners[name] = this.eventListeners[name] || []).push(
+      listener
+    );
+  }
+
   destroy() {
   destroy() {
     // save log
     // save log
+    this._dispatchEvent('destroyed', this.workflow.id);
+
+    this.eventListeners = {};
+    this.tabMessageListeners = {};
+    this.tabUpdatedListeners = {};
+
     browser.tabs.onRemoved.removeListener(this.tabRemovedHandler);
     browser.tabs.onRemoved.removeListener(this.tabRemovedHandler);
     browser.tabs.onUpdated.removeListener(this.tabUpdatedHandler);
     browser.tabs.onUpdated.removeListener(this.tabUpdatedHandler);
 
 
     this.isDestroyed = true;
     this.isDestroyed = true;
+    this.endedTimestamp = Date.now();
+  }
+
+  _dispatchEvent(name, params) {
+    const listeners = this.eventListeners[name];
+    console.log(name, this.eventListeners);
+    if (!listeners) return;
+
+    listeners.forEach((callback) => {
+      callback(params);
+    });
   }
   }
 
 
   _blockHandler(block, prevBlockData) {
   _blockHandler(block, prevBlockData) {
@@ -120,6 +147,20 @@ class WorkflowEngine {
       );
       );
       return;
       return;
     }
     }
+
+    const executedWorkflowData = [
+      'data',
+      'isPaused',
+      'isDestroyed',
+      'currentBlock',
+    ].reduce((acc, key) => {
+      acc[key] = this[key];
+
+      return acc;
+    }, {});
+    console.log(executedWorkflowData);
+    executingWorkflow.update(this.id, { workflow: executedWorkflowData });
+
     if (this.isPaused || this.isInsidePaused) {
     if (this.isPaused || this.isInsidePaused) {
       setTimeout(() => {
       setTimeout(() => {
         this._blockHandler(block, prevBlockData);
         this._blockHandler(block, prevBlockData);
@@ -144,6 +185,8 @@ class WorkflowEngine {
           if (result.nextBlockId) {
           if (result.nextBlockId) {
             this._blockHandler(this.blocks[result.nextBlockId], result.data);
             this._blockHandler(this.blocks[result.nextBlockId], result.data);
           } else {
           } else {
+            this._dispatchEvent('finish');
+            this.destroy();
             console.log('Done', this);
             console.log('Done', this);
           }
           }
         })
         })
@@ -175,6 +218,7 @@ class WorkflowEngine {
 
 
   _listener({ id, name, callback, once = true, ...options }) {
   _listener({ id, name, callback, once = true, ...options }) {
     const listenerNames = {
     const listenerNames = {
+      event: 'eventListener',
       'tab-updated': 'tabUpdatedListeners',
       'tab-updated': 'tabUpdatedListeners',
       'tab-message': 'tabMessageListeners',
       'tab-message': 'tabMessageListeners',
     };
     };

+ 19 - 10
src/components/newtab/workflow/edit/EditScrollElement.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <edit-interaction-base v-bind="{ data }" @change="updateData">
   <edit-interaction-base v-bind="{ data }" @change="updateData">
-    <div class="flex items-center mt-3 space-x-2">
+    <div v-if="!data.scrollIntoView" class="flex items-center mt-3 space-x-2">
       <ui-input
       <ui-input
         :model-value="data.scrollX || 0"
         :model-value="data.scrollX || 0"
         type="number"
         type="number"
@@ -16,16 +16,11 @@
     </div>
     </div>
     <div class="mt-3 space-y-2">
     <div class="mt-3 space-y-2">
       <ui-checkbox
       <ui-checkbox
-        :model-value="data.incX"
-        @change="updateData({ incX: $event })"
+        class="w-full"
+        :model-value="data.scrollIntoView"
+        @change="updateData({ scrollIntoView: $event })"
       >
       >
-        Increment horizontal scroll
-      </ui-checkbox>
-      <ui-checkbox
-        :model-value="data.incY"
-        @change="updateData({ incY: $event })"
-      >
-        Increment vertical scroll
+        Scroll into view
       </ui-checkbox>
       </ui-checkbox>
       <ui-checkbox
       <ui-checkbox
         :model-value="data.smooth"
         :model-value="data.smooth"
@@ -33,6 +28,20 @@
       >
       >
         Smooth scroll
         Smooth scroll
       </ui-checkbox>
       </ui-checkbox>
+      <template v-if="!data.scrollIntoView">
+        <ui-checkbox
+          :model-value="data.incX"
+          @change="updateData({ incX: $event })"
+        >
+          Increment horizontal scroll
+        </ui-checkbox>
+        <ui-checkbox
+          :model-value="data.incY"
+          @change="updateData({ incY: $event })"
+        >
+          Increment vertical scroll
+        </ui-checkbox>
+      </template>
     </div>
     </div>
   </edit-interaction-base>
   </edit-interaction-base>
 </template>
 </template>

+ 4 - 1
src/composable/editorBlock.js

@@ -24,7 +24,10 @@ export function useEditorBlock(selector, editor) {
       block.data = data || details.data;
       block.data = data || details.data;
       block.category = categories[details.category];
       block.category = categories[details.category];
     }
     }
-    editor.updateConnectionNodes(`node-${block.id}`);
+
+    setTimeout(() => {
+      editor.updateConnectionNodes(`node-${block.id}`);
+    }, 200);
   });
   });
 
 
   return block;
   return block;

+ 11 - 5
src/content/blocks-handler.js

@@ -76,12 +76,18 @@ function getScrollPos(element, data, vertical = true) {
 }
 }
 export function elementScroll(block) {
 export function elementScroll(block) {
   return new Promise((resolve) => {
   return new Promise((resolve) => {
+    const behavior = block.data.smooth ? 'smooth' : 'auto';
+
     handleElement(block, (element) => {
     handleElement(block, (element) => {
-      element.scroll({
-        top: getScrollPos(element, block.data),
-        left: getScrollPos(element, block.data, false),
-        behavior: block.data.smooth ? 'smooth' : 'auto',
-      });
+      if (block.data.scrollIntoView) {
+        element.scrollIntoView({ behavior, block: 'center' });
+      } else {
+        element.scroll({
+          behavior,
+          top: getScrollPos(element, block.data),
+          left: getScrollPos(element, block.data, false),
+        });
+      }
     });
     });
 
 
     window.dispatchEvent(new Event('scroll'));
     window.dispatchEvent(new Event('scroll'));

+ 4 - 2
src/content/element-selector/ElementSelector.ce.vue

@@ -132,6 +132,8 @@ function selectParentElement() {
   selectedEl = activeEl;
   selectedEl = activeEl;
 }
 }
 function handleClick(event) {
 function handleClick(event) {
+  if (event.target === root) return;
+
   event.preventDefault();
   event.preventDefault();
   event.stopPropagation();
   event.stopPropagation();
 
 
@@ -163,7 +165,7 @@ function handleScroll() {
 function destroy() {
 function destroy() {
   window.removeEventListener('keyup', handleKeyup);
   window.removeEventListener('keyup', handleKeyup);
   window.removeEventListener('scroll', handleScroll);
   window.removeEventListener('scroll', handleScroll);
-  document.body.removeEventListener('click', handleClick);
+  document.removeEventListener('click', handleClick, true);
   window.removeEventListener('mousemove', handleMouseMove);
   window.removeEventListener('mousemove', handleMouseMove);
 
 
   root.remove();
   root.remove();
@@ -171,7 +173,7 @@ function destroy() {
 
 
 window.addEventListener('keyup', handleKeyup);
 window.addEventListener('keyup', handleKeyup);
 window.addEventListener('scroll', handleScroll);
 window.addEventListener('scroll', handleScroll);
-document.body.addEventListener('click', handleClick);
+document.addEventListener('click', handleClick, true);
 window.addEventListener('mousemove', handleMouseMove);
 window.addEventListener('mousemove', handleMouseMove);
 </script>
 </script>
 <style>
 <style>

+ 69 - 0
src/utils/executing-workflow.js

@@ -0,0 +1,69 @@
+import browser from 'webextension-polyfill';
+
+function getWorkflows() {
+  return new Promise((resolve) => {
+    browser.storage.local
+      .get('executingWorkflow')
+      .then(({ executingWorkflow }) => {
+        console.log(executingWorkflow);
+        resolve(executingWorkflow || []);
+      });
+  });
+}
+async function updater(callback) {
+  try {
+    const executingWorkflow = await getWorkflows();
+    const items = callback(executingWorkflow);
+
+    await browser.storage.local.set({ executingWorkflow: items });
+
+    return items;
+  } catch (error) {
+    console.error(error);
+    return [];
+  }
+}
+function setWorkflow(id, workflow) {
+  return updater((items) => {
+    items.push({ id, workflow });
+
+    return items;
+  });
+}
+async function findWorkflows(callback) {
+  try {
+    const workflows = await getWorkflows();
+
+    return workflows.find(callback);
+  } catch (error) {
+    console.error(error);
+    return [];
+  }
+}
+function deleteWorkflow(id) {
+  return updater((items) => {
+    const index = items.findIndex((item) => item.id === id);
+
+    items.splice(index, 1);
+
+    return items;
+  });
+}
+function update(id, data = {}) {
+  return updater((items) => {
+    const index = items.findIndex((item) => item.id === id);
+
+    /* eslint-disable-next-line */
+    if (index !== -1) items[index] = { ...data, id };
+
+    return items;
+  });
+}
+
+export default {
+  update,
+  set: setWorkflow,
+  get: getWorkflows,
+  find: findWorkflows,
+  delete: deleteWorkflow,
+};

+ 1 - 0
src/utils/shared.js

@@ -141,6 +141,7 @@ export const tasks = {
       incX: false,
       incX: false,
       incY: false,
       incY: false,
       smooth: false,
       smooth: false,
+      scrollIntoView: false,
     },
     },
   },
   },
   link: {
   link: {