Browse Source

feat: add workflow-engine

Ahmad Kholid 3 years ago
parent
commit
8410aef03b

+ 21 - 1
src/background/index.js

@@ -1,8 +1,28 @@
+import { MessageListener } from '@/utils/message';
+import WorkflowEngine from '@/utils/workflow-engine';
+
 chrome.runtime.onInstalled.addListener((details) => {
   if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {
-    chrome.storage.set({
+    chrome.storage.local.set({
       workflows: [],
       tasks: [],
     });
   }
 });
+
+const message = new MessageListener('background');
+
+message.on('workflow:execute', (workflow) => {
+  try {
+    const engine = new WorkflowEngine(workflow);
+    console.log('execute');
+    engine.init();
+
+    return true;
+  } catch (error) {
+    console.error(error);
+    return error;
+  }
+});
+
+chrome.runtime.onMessage.addListener(message.listener());

+ 7 - 0
src/content/index.js

@@ -1,6 +1,13 @@
+import browser from 'webextension-polyfill';
 import { printLine } from './modules/print';
 
 console.log('Content script works!');
 console.log('Must reload extension for modifications to take effect.');
 
 printLine("Using the 'printLine' function from the Print Module");
+
+(() => {
+  browser.runtime.onConnect.addListener((a, b) => {
+    console.log(a, b);
+  });
+})();

+ 1 - 1
src/manifest.json

@@ -14,6 +14,6 @@
   "icons": {
     "128": "icon-128.png"
   },
-  "permissions": ["scripting", "storage", "unlimitedStorage", "tabs"],
+  "permissions": ["storage", "unlimitedStorage", "tabs", "activeTab", "http://*/*", "https://*/*"],
   "web_accessible_resources": ["content.styles.css", "icon-128.png", "icon-34.png"]
 }

+ 16 - 1
src/newtab/pages/workflows/[id].vue

@@ -48,6 +48,7 @@ import {
 } from 'vue';
 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
 import emitter from 'tiny-emitter/instance';
+import { sendMessage } from '@/utils/message';
 import { debounce } from '@/utils/helper';
 import { useDialog } from '@/composable/dialog';
 import Workflow from '@/models/workflow';
@@ -107,7 +108,21 @@ function editBlock(data) {
   state.blockData = data;
 }
 function executeWorkflow() {
-  console.log(editor.value);
+  if (editor.value.getNodesFromName('trigger').length === 0) {
+    /* eslint-disable-next-line */
+    alert("Can't find a trigger block");
+    return;
+  }
+
+  const payload = {
+    ...workflow.value,
+    drawflow: editor.value.export(),
+    isTesting: state.isDataChanged,
+  };
+
+  sendMessage('workflow:execute', payload, 'background').then(() => {
+    console.log('the fuck');
+  });
 }
 function handleEditorDataChanged() {
   state.isDataChanged = true;

+ 54 - 0
src/utils/blocks-handler.js

@@ -0,0 +1,54 @@
+import browser from 'webextension-polyfill';
+
+function getBlockConnection(block, index = 1) {
+  const blockId = block.outputs[`output_${index}`].connections[0]?.node;
+
+  return blockId;
+}
+
+export function trigger(block) {
+  return new Promise((resolve) => {
+    const nextBlockId = getBlockConnection(block);
+
+    resolve({ nextBlockId });
+  });
+}
+
+export function openWebsite(block) {
+  return new Promise((resolve) => {
+    browser.tabs
+      .create({
+        active: true,
+        url: block.data.url,
+      })
+      .then((tab) => {
+        const tabListener = (tabId, changeInfo) => {
+          if (changeInfo.status === 'complete' && tabId === tab.id) {
+            browser.tabs.onUpdated.removeListener(tabListener);
+
+            browser.tabs
+              .executeScript(tabId, {
+                file: './contentScript.bundle.js',
+              })
+              .then(() => {
+                this.connectedTab = browser.tabs.connect(tabId, {
+                  name: `${this.workflow.id}--${this.workflow.name.slice(
+                    0,
+                    10
+                  )}`,
+                });
+                this.tabId = tabId;
+
+                resolve({ nextBlockId: getBlockConnection(block) });
+              });
+          }
+        };
+
+        browser.tabs.onUpdated.addListener(tabListener);
+      })
+      .catch((error) => {
+        console.error(error, 'nnnaa');
+        reject(error);
+      });
+  });
+}

+ 12 - 0
src/utils/helper.js

@@ -1,3 +1,15 @@
+export function toCamelCase(str) {
+  const result = str.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => {
+    return index === 0 ? letter.toLowerCase() : letter.toUpperCase();
+  });
+
+  return result.replace(/\s+|[-]/g, '');
+}
+
+export function isObject(obj) {
+  return typeof obj === 'object' && object !== null;
+}
+
 export function objectHasKey(obj, key) {
   return Object.prototype.hasOwnProperty.call(obj, key);
 }

+ 71 - 0
src/utils/message.js

@@ -0,0 +1,71 @@
+import browser from 'webextension-polyfill';
+import { objectHasKey } from './helper';
+
+const nameBuilder = (prefix, name) => (prefix ? `${prefix}--${name}` : name);
+
+export class MessageListener {
+  constructor(prefix = '') {
+    this.listeners = {};
+    this.prefix = prefix;
+  }
+
+  on(name, listener) {
+    if (objectHasKey(this.listeners, 'name')) {
+      console.error(`You already added ${name}`);
+      return;
+    }
+
+    this.listeners[nameBuilder(this.prefix, name)] = listener;
+  }
+
+  listener() {
+    return this.listen.bind(this);
+  }
+
+  listen(message, sender, sendResponse) {
+    try {
+      const listener = this.listeners[message.name];
+
+      const response =
+        listener && listener.call({ message, sender }, message.data, sender);
+
+      if (!response) {
+        // Do nothing
+      } else if (!(response instanceof Promise)) {
+        sendResponse(response);
+      } else {
+        response
+          .then((res) => {
+            sendResponse(res);
+          })
+          .catch((res) => {
+            sendResponse(res);
+          });
+      }
+    } catch (err) {
+      sendResponse({
+        error: true,
+        message: `Unhandled Background Error: ${String(err)}`,
+      });
+    }
+  }
+}
+
+export function sendMessage(name = '', data = {}, prefix = '') {
+  return new Promise((resolve, reject) => {
+    const payload = {
+      name: nameBuilder(prefix, name),
+      data,
+    };
+
+    browser.runtime
+      .sendMessage(payload)
+      .then((response) => {
+        if (response.error) reject(new Error(response.message));
+        else resolve(response);
+      })
+      .catch((error) => {
+        reject(error);
+      });
+  });
+}

+ 59 - 0
src/utils/workflow-engine.js

@@ -0,0 +1,59 @@
+/* eslint-disable no-underscore-dangle */
+import { toCamelCase } from './helper';
+import * as blocksHandler from './blocks-handler';
+
+class WorkflowEngine {
+  constructor(workflow) {
+    this.workflow = workflow;
+    this.blocks = {};
+    this.blocksArr = [];
+    this.data = [];
+  }
+
+  init() {
+    const drawflowData =
+      typeof this.workflow.drawflow === 'string'
+        ? JSON.parse(this.workflow.drawflow || '{}')
+        : this.workflow.drawflow;
+    const blocks = drawflowData?.drawflow.Home.data;
+
+    if (!blocks) return;
+
+    const blocksArr = Object.values(blocks);
+    const triggerBlock = blocksArr.find(({ name }) => name === 'trigger');
+
+    if (!triggerBlock) {
+      console.error('A trigger block is required');
+      return;
+    }
+
+    this.blocks = blocks;
+    this.blocksArr = blocksArr;
+
+    this._blockHandler(triggerBlock);
+  }
+
+  _blockHandler(block) {
+    console.log(`${block.name}(${toCamelCase(block.name)}):`, block);
+    const handler = blocksHandler[toCamelCase(block?.name)];
+
+    if (handler) {
+      handler
+        .call(this, block)
+        .then((result) => {
+          if (result.nextBlockId) {
+            this._blockHandler(this.blocks[result.nextBlockId]);
+          } else {
+            console.log('Done');
+          }
+        })
+        .catch((error) => {
+          console.error(error, 'new');
+        });
+    } else {
+      console.error(`"${block.name}" block doesn't have a handler`);
+    }
+  }
+}
+
+export default WorkflowEngine;

+ 4 - 3
webpack.config.js

@@ -44,7 +44,7 @@ const options = {
     contentScript: path.join(__dirname, 'src', 'content', 'index.js'),
   },
   chromeExtensionBoilerplate: {
-    notHotReload: ['contentScript', 'devtools'],
+    notHotReload: ['contentScript'],
   },
   output: {
     path: path.resolve(__dirname, 'build'),
@@ -58,11 +58,12 @@ const options = {
         loader: 'vue-loader',
       },
       {
-        // look for .css or .scss files
         test: /\.css$/,
-        // in the `src` directory
         use: [
           MiniCssExtractPlugin.loader,
+          // {
+          //   loader: 'style-loader',
+          // },
           {
             loader: 'css-loader',
           },