소스 검색

refactor: workflow engine

Ahmad Kholid 3 년 전
부모
커밋
928caddcaa

+ 1 - 2
.babelrc

@@ -5,8 +5,7 @@
       "useBuiltIns": "usage",
       "corejs": 3,
       "targets": {
-        // https://jamie.build/last-2-versions
-        "browsers": ["> 0.25%", "not ie 11", "not op_mini all"]
+        "browsers": "last 2 Chrome versions"
       }
     }]
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "automa",
-  "version": "0.8.3",
+  "version": "0.8.4",
   "description": "An extension for automating your browser by connecting blocks",
   "license": "MIT",
   "repository": {

+ 18 - 44
src/background/workflow-engine/blocks-handler/handler-interaction-block.js

@@ -1,5 +1,5 @@
-import { objectHasKey, isObject } from '@/utils/helper';
-import { getBlockConnection, convertData } from '../helper';
+import { objectHasKey } from '@/utils/helper';
+import { getBlockConnection } from '../helper';
 
 async function interactionHandler(block, prevBlockData) {
   const nextBlockId = getBlockConnection(block);
@@ -29,53 +29,27 @@ async function interactionHandler(block, prevBlockData) {
       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 (!block.data.saveData)
+        return {
+          data,
+          nextBlockId,
+        };
 
-          if (memoColumn[key]) {
-            column = memoColumn[key];
-          } else {
-            const currentColumn = getColumn(key);
+      const currentColumnType =
+        this.columns[block.data.dataColumn]?.type || 'any';
 
-            column = currentColumn;
-            memoColumn[key] = currentColumn;
-          }
-
-          pushData(column, value);
-        });
-      };
-
-      if (Array.isArray(data)) {
-        data.forEach((obj) => {
-          if (isObject(obj)) pushObjectData(obj);
+      if (Array.isArray(data) && currentColumnType !== 'array') {
+        data.forEach((item) => {
+          this.addData(block.data.dataColumn, item);
         });
-      } else if (isObject(data)) {
-        pushObjectData(data);
+      } else {
+        this.addData(block.data.dataColumn, data);
       }
+    } else if (block.name === 'javascript-code') {
+      const arrData = Array.isArray(data) ? data : [data];
+
+      this.addData(arrData);
     }
 
     return {

+ 46 - 16
src/background/workflow-engine/engine.js

@@ -2,8 +2,9 @@
 import browser from 'webextension-polyfill';
 import { nanoid } from 'nanoid';
 import { tasks } from '@/utils/shared';
-import { toCamelCase, parseJSON } from '@/utils/helper';
+import { convertData } from './helper';
 import { generateJSON } from '@/utils/data-exporter';
+import { toCamelCase, parseJSON, isObject, objectHasKey } from '@/utils/helper';
 import errorMessage from './error-message';
 import referenceData from '@/utils/reference-data';
 import workflowState from '../workflow-state';
@@ -20,7 +21,7 @@ function tabRemovedHandler(tabId) {
     this.currentBlock.name === 'new-tab' ||
     tasks[this.currentBlock.name].category === 'interaction'
   ) {
-    this.destroy('error', 'Current active tab is removed');
+    this.destroy('error', 'active-tab-removed');
   }
 
   workflowState.update(this.id, this.state);
@@ -83,7 +84,8 @@ class WorkflowEngine {
     this.collectionLogId = collectionLogId;
     this.globalData = parseJSON(globalDataVal, globalDataVal);
     this.activeTabUrl = '';
-    this.data = {};
+    this.columns = { column: { index: 0, type: 'any' } };
+    this.data = [];
     this.logs = [];
     this.blocks = {};
     this.frames = {};
@@ -113,7 +115,7 @@ class WorkflowEngine {
       typeof this.workflow.drawflow === 'string'
         ? JSON.parse(this.workflow.drawflow || '{}')
         : this.workflow.drawflow;
-    const blocks = drawflowData?.drawflow.Home.data;
+    const blocks = drawflowData?.drawflow?.Home.data;
 
     if (!blocks) {
       console.error(errorMessage('no-block', this.workflow));
@@ -138,14 +140,10 @@ class WorkflowEngine {
     this.blocks = blocks;
     this.startedTimestamp = Date.now();
     this.workflow.dataColumns = dataColumns;
-    this.data = dataColumns.reduce(
-      (acc, column) => {
-        acc[column.name] = [];
 
-        return acc;
-      },
-      { column: [] }
-    );
+    dataColumns.forEach(({ name, type }) => {
+      this.columns[name] = { index: 0, type };
+    });
 
     workflowState
       .add(this.id, {
@@ -158,6 +156,38 @@ class WorkflowEngine {
       });
   }
 
+  addData(key, value) {
+    if (Array.isArray(key)) {
+      key.forEach((item) => {
+        if (!isObject(item)) return;
+
+        Object.entries(item).forEach(([itemKey, itemValue]) => {
+          this.addData(itemKey, itemValue);
+        });
+      });
+
+      return;
+    }
+
+    const columnName = objectHasKey(this.columns, key) ? key : 'column';
+    const currentColumn = this.columns[columnName];
+    const convertedValue = convertData(value, currentColumn.type);
+
+    if (objectHasKey(this.data, currentColumn.index)) {
+      this.data[currentColumn.index][columnName] = convertedValue;
+    } else {
+      this.data.push({ [columnName]: convertedValue });
+    }
+
+    currentColumn.index += 1;
+  }
+
+  addLog(detail) {
+    if (this.logs.length >= 1001) return;
+
+    this.logs.push(detail);
+  }
+
   on(name, listener) {
     (this.eventListeners[name] = this.eventListeners[name] || []).push(
       listener
@@ -176,7 +206,7 @@ class WorkflowEngine {
         await this.childWorkflow.stop();
       }
 
-      this.logs.push({
+      this.addLog({
         message,
         type: 'stop',
         name: 'stop',
@@ -210,6 +240,7 @@ class WorkflowEngine {
           name,
           icon,
           status,
+          message,
           id: this.id,
           workflowId: id,
           data: jsonData,
@@ -285,7 +316,6 @@ class WorkflowEngine {
 
     if (!disableTimeoutKeys.includes(block.name)) {
       this.workflowTimeout = setTimeout(() => {
-        alert('timeout');
         if (!this.isDestroyed) this.stop('stop-timeout');
       }, this.workflow.settings.timeout || 120000);
     }
@@ -316,7 +346,7 @@ class WorkflowEngine {
         .then((result) => {
           clearTimeout(this.workflowTimeout);
           this.workflowTimeout = null;
-          this.logs.push({
+          this.addLog({
             type: 'success',
             name: block.name,
             logId: result.logId,
@@ -326,7 +356,7 @@ class WorkflowEngine {
           if (result.nextBlockId) {
             this._blockHandler(this.blocks[result.nextBlockId], result.data);
           } else {
-            this.logs.push({
+            this.addLog({
               type: 'finish',
               name: 'finish',
             });
@@ -335,7 +365,7 @@ class WorkflowEngine {
           }
         })
         .catch((error) => {
-          this.logs.push({
+          this.addLog({
             type: 'error',
             message: error.message,
             name: block.name,

+ 1 - 1
src/background/workflow-engine/helper.js

@@ -3,7 +3,7 @@ export function convertData(data, type) {
 
   switch (type) {
     case 'integer':
-      result = +data.replace(/\D+/g, '');
+      result = Number.isNaN(data) ? +data?.replace(/\D+/g, '') : data;
       break;
     case 'boolean':
       result = Boolean(data);

+ 8 - 2
src/components/newtab/shared/SharedLogsTable.vue

@@ -51,7 +51,7 @@ defineProps({
   },
 });
 
-const { t } = useI18n();
+const { t, te } = useI18n();
 
 const statusColors = {
   error: 'bg-red-200',
@@ -64,7 +64,13 @@ function formatDate(date, format) {
 
   return dayjs(date).format(format);
 }
-function getErrorMessage({ history }) {
+function getErrorMessage({ history, message }) {
+  const messagePath = `log.messages.${message}`;
+
+  if (message && te(messagePath)) {
+    return t(messagePath);
+  }
+
   const lastHistory = history[history.length - 1];
 
   return lastHistory && lastHistory.type === 'error'

+ 1 - 0
src/locales/en/newtab.json

@@ -86,6 +86,7 @@
       "no-iframe-id": "Can't find Frame ID for the iframe element with \"{selector}\" selector",
       "no-tab": "Can't connect to a tab, use \"New tab\" or \"Active tab\" block before using the \"{name}\" block.",
       "empty-workflow": "You must select a workflow first",
+      "active-tab-removed": "Workflow active tab is removed",
       "no-workflow": "Can't find workflow with \"{workflowId}\" ID",
       "workflow-infinite-loop": "Can't execute the workflow to prevent an infinite loop"
     },

+ 1 - 0
src/models/log.js

@@ -11,6 +11,7 @@ class Log extends Model {
       name: this.string(''),
       history: this.attr([]),
       endedAt: this.number(0),
+      message: this.string(''),
       startedAt: this.number(0),
       workflowId: this.attr(null),
       collectionId: this.attr(null),

+ 7 - 1
src/utils/data-exporter.js

@@ -17,6 +17,8 @@ const files = {
 };
 
 export function generateJSON(keys, data) {
+  if (Array.isArray(data)) return data;
+
   const result = [];
 
   keys.forEach((key) => {
@@ -45,7 +47,11 @@ export default function (data, { name, type }, converted) {
         ? Papa.unparse(jsonData)
         : JSON.stringify(jsonData, null, 2);
   } else if (type === 'plain-text') {
-    result = Object.values(data).join(' ');
+    result = (
+      Array.isArray(data)
+        ? data.map((item) => Object.values(item)).flat()
+        : Object.values(data)
+    ).join(' ');
   }
 
   const { mime, ext } = files[type];

+ 12 - 14
src/utils/reference-data.js

@@ -2,31 +2,29 @@ import { get, set } from 'object-path-immutable';
 import { isObject, objectHasKey, replaceMustache } from '@/utils/helper';
 
 const objectPath = { get, set };
+const refKeys = [
+  { name: 'dataColumn', key: 'dataColumns' },
+  { name: 'dataColumns', key: 'dataColumns' },
+];
 
 export function parseKey(key) {
-  const [dataKey, path] = key.split('@');
+  /* eslint-disable-next-line */
+  let [dataKey, path] = key.split('@');
 
-  if (
-    ['prevBlockData', 'loopData', 'globalData', 'activeTabUrl'].includes(
-      dataKey
-    )
-  )
-    return { dataKey, path: path || '' };
+  dataKey =
+    (refKeys.find((item) => item.name === dataKey) || {}).key || dataKey;
+
+  if (dataKey !== 'dataColumns') return { dataKey, path: path || '' };
 
   const pathArr = path?.split('.') ?? [];
   let dataPath = '';
 
   if (pathArr.length === 1) {
-    dataPath = `${pathArr[0]}.0`;
+    dataPath = `0.${pathArr[0]}`;
   } else if (typeof +pathArr[0] !== 'number') {
     const firstPath = pathArr.shift();
 
-    dataPath = `${firstPath}.0.${pathArr.join('.')}`;
-  } else {
-    const index = pathArr.shift();
-    const firstPath = pathArr.shift();
-
-    dataPath = `${firstPath}.${index}.${pathArr.join('.')}`;
+    dataPath = `0.${firstPath}.${pathArr.join('.')}`;
   }
 
   if (dataPath.endsWith('.')) dataPath = dataPath.slice(0, -1);