From 8410aef03be0df81fc771784f3b19351a7fd1ad9 Mon Sep 17 00:00:00 2001 From: Ahmad Kholid Date: Sat, 9 Oct 2021 19:26:15 +0800 Subject: [PATCH] feat: add workflow-engine --- src/background/index.js | 22 ++++++++- src/content/index.js | 7 +++ src/manifest.json | 2 +- src/newtab/pages/workflows/[id].vue | 17 ++++++- src/utils/blocks-handler.js | 54 ++++++++++++++++++++++ src/utils/helper.js | 12 +++++ src/utils/message.js | 71 +++++++++++++++++++++++++++++ src/utils/workflow-engine.js | 59 ++++++++++++++++++++++++ webpack.config.js | 7 +-- 9 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 src/utils/blocks-handler.js create mode 100644 src/utils/message.js create mode 100644 src/utils/workflow-engine.js diff --git a/src/background/index.js b/src/background/index.js index 5e5057763..50a64ad1f 100644 --- a/src/background/index.js +++ b/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()); diff --git a/src/content/index.js b/src/content/index.js index d9e44cee1..d00e52486 100644 --- a/src/content/index.js +++ b/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); + }); +})(); diff --git a/src/manifest.json b/src/manifest.json index c6ce32519..550e30758 100644 --- a/src/manifest.json +++ b/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"] } diff --git a/src/newtab/pages/workflows/[id].vue b/src/newtab/pages/workflows/[id].vue index ceaeceffb..89c9f7a96 100644 --- a/src/newtab/pages/workflows/[id].vue +++ b/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; diff --git a/src/utils/blocks-handler.js b/src/utils/blocks-handler.js new file mode 100644 index 000000000..6f7665b28 --- /dev/null +++ b/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); + }); + }); +} diff --git a/src/utils/helper.js b/src/utils/helper.js index 9e7540dbf..f54a433d2 100644 --- a/src/utils/helper.js +++ b/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); } diff --git a/src/utils/message.js b/src/utils/message.js new file mode 100644 index 000000000..3556baa53 --- /dev/null +++ b/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); + }); + }); +} diff --git a/src/utils/workflow-engine.js b/src/utils/workflow-engine.js new file mode 100644 index 000000000..e275bff78 --- /dev/null +++ b/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; diff --git a/webpack.config.js b/webpack.config.js index 00efe0125..6052c01c8 100644 --- a/webpack.config.js +++ b/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', },