Ahmad Kholid 3 年之前
父节点
当前提交
4ad032e3ee
共有 43 个文件被更改,包括 1197 次插入400 次删除
  1. 1 2
      .babelrc
  2. 2 4
      package.json
  3. 22 0
      src/background/index.js
  4. 0 33
      src/background/workflow-engine/blocks-handler/handler-condition.js
  5. 46 0
      src/background/workflow-engine/blocks-handler/handler-conditions.js
  6. 24 60
      src/background/workflow-engine/blocks-handler/handler-interaction-block.js
  7. 1 1
      src/background/workflow-engine/blocks-handler/handler-loop-breakpoint.js
  8. 61 24
      src/background/workflow-engine/engine.js
  9. 1 1
      src/background/workflow-engine/helper.js
  10. 61 66
      src/components/block/BlockConditions.vue
  11. 8 2
      src/components/newtab/shared/SharedLogsTable.vue
  12. 17 2
      src/components/newtab/workflow/WorkflowSettings.vue
  13. 133 0
      src/components/newtab/workflow/edit/EditConditions.vue
  14. 9 9
      src/components/newtab/workflow/edit/EditElementExists.vue
  15. 16 10
      src/content/blocks-handler/handler-attribute-value.js
  16. 20 15
      src/content/blocks-handler/handler-element-scroll.js
  17. 11 3
      src/content/blocks-handler/handler-event-click.js
  18. 8 2
      src/content/blocks-handler/handler-forms.js
  19. 29 13
      src/content/blocks-handler/handler-get-text.js
  20. 2 2
      src/content/blocks-handler/handler-link.js
  21. 9 7
      src/content/blocks-handler/handler-switch-to.js
  22. 11 3
      src/content/blocks-handler/handler-trigger-event.js
  23. 1 0
      src/content/element-selector/AppBlocks.vue
  24. 23 9
      src/content/helper.js
  25. 34 0
      src/content/shortcut.js
  26. 1 0
      src/lib/dayjs.js
  27. 1 0
      src/locales/en/blocks.json
  28. 14 6
      src/locales/en/newtab.json
  29. 317 0
      src/locales/vi/blocks.json
  30. 56 0
      src/locales/vi/common.json
  31. 135 0
      src/locales/vi/newtab.json
  32. 13 0
      src/locales/vi/popup.json
  33. 1 0
      src/models/log.js
  34. 1 0
      src/models/workflow.js
  35. 1 18
      src/popup/pages/Home.vue
  36. 1 0
      src/utils/compare-block-value.js
  37. 7 1
      src/utils/data-exporter.js
  38. 9 3
      src/utils/find-element.js
  39. 23 32
      src/utils/reference-data.js
  40. 7 5
      src/utils/shared.js
  41. 1 1
      src/utils/simulate-event.js
  42. 1 1
      utils/env.js
  43. 58 65
      yarn.lock

+ 1 - 2
.babelrc

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

+ 2 - 4
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "automa",
   "name": "automa",
-  "version": "0.8.2",
+  "version": "0.9.3",
   "description": "An extension for automating your browser by connecting blocks",
   "description": "An extension for automating your browser by connecting blocks",
   "license": "MIT",
   "license": "MIT",
   "repository": {
   "repository": {
@@ -23,7 +23,6 @@
   "dependencies": {
   "dependencies": {
     "@medv/finder": "^2.1.0",
     "@medv/finder": "^2.1.0",
     "@vuex-orm/core": "^0.36.4",
     "@vuex-orm/core": "^0.36.4",
-    "@webcomponents/custom-elements": "^1.5.0",
     "dayjs": "^1.10.7",
     "dayjs": "^1.10.7",
     "drawflow": "^0.0.51",
     "drawflow": "^0.0.51",
     "idb": "^7.0.0",
     "idb": "^7.0.0",
@@ -39,7 +38,6 @@
     "vue-i18n": "^9.2.0-beta.20",
     "vue-i18n": "^9.2.0-beta.20",
     "vue-prism-editor": "^2.0.0-alpha.2",
     "vue-prism-editor": "^2.0.0-alpha.2",
     "vue-router": "^4.0.11",
     "vue-router": "^4.0.11",
-    "vue-virtual-scroller": "^2.0.0-alpha.1",
     "vuedraggable": "^4.1.0",
     "vuedraggable": "^4.1.0",
     "vuex": "^4.0.2",
     "vuex": "^4.0.2",
     "webextension-polyfill": "^0.8.0"
     "webextension-polyfill": "^0.8.0"
@@ -80,7 +78,7 @@
     "simple-git-hooks": "^2.6.1",
     "simple-git-hooks": "^2.6.1",
     "source-map-loader": "3.0.0",
     "source-map-loader": "3.0.0",
     "style-loader": "3.3.0",
     "style-loader": "3.3.0",
-    "tailwindcss": "2.2.16",
+    "tailwindcss": "2.2.19",
     "terser-webpack-plugin": "5.2.4",
     "terser-webpack-plugin": "5.2.4",
     "vue-loader": "16.8.1",
     "vue-loader": "16.8.1",
     "webpack": "5.55.1",
     "webpack": "5.55.1",

+ 22 - 0
src/background/index.js

@@ -113,7 +113,29 @@ const message = new MessageListener('background');
 message.on('fetch:text', (url) => {
 message.on('fetch:text', (url) => {
   return fetch(url).then((response) => response.text());
   return fetch(url).then((response) => response.text());
 });
 });
+message.on('open:dashboard', async (url) => {
+  const tabOptions = {
+    active: true,
+    url: browser.runtime.getURL(
+      `/newtab.html#${typeof url === 'string' ? url : ''}`
+    ),
+  };
 
 
+  try {
+    const [tab] = await browser.tabs.query({
+      url: browser.runtime.getURL('/newtab.html'),
+    });
+
+    if (tab) {
+      await browser.tabs.update(tab.id, tabOptions);
+      await browser.tabs.reload(tab.id);
+    } else {
+      browser.tabs.create(tabOptions);
+    }
+  } catch (error) {
+    console.error(error);
+  }
+});
 message.on('get:sender', (_, sender) => {
 message.on('get:sender', (_, sender) => {
   return sender;
   return sender;
 });
 });

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

@@ -1,33 +0,0 @@
-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;

+ 46 - 0
src/background/workflow-engine/blocks-handler/handler-conditions.js

@@ -0,0 +1,46 @@
+import { getBlockConnection } from '../helper';
+import { replaceMustache } from '@/utils/helper';
+import { replaceMustacheHandler } from '@/utils/reference-data';
+import compareBlockValue from '@/utils/compare-block-value';
+
+function conditions({ data, outputs }, { prevBlockData, refData }) {
+  return new Promise((resolve, reject) => {
+    if (data.conditions.length === 0) {
+      reject(new Error('conditions-empty'));
+      return;
+    }
+
+    let resultData = '';
+    let isConditionMatch = false;
+    let outputIndex = data.conditions.length + 1;
+    const handleMustache = (match) => replaceMustacheHandler(match, refData);
+    const prevData = Array.isArray(prevBlockData)
+      ? prevBlockData[0]
+      : prevBlockData;
+
+    data.conditions.forEach(({ type, value, compareValue }, index) => {
+      if (isConditionMatch) return;
+
+      const firstValue = replaceMustache(
+        compareValue ?? prevData,
+        handleMustache
+      );
+      const secondValue = replaceMustache(value, handleMustache);
+
+      const isMatch = compareBlockValue(type, firstValue, secondValue);
+
+      if (isMatch) {
+        resultData = value;
+        outputIndex = index + 1;
+        isConditionMatch = true;
+      }
+    });
+
+    resolve({
+      data: resultData,
+      nextBlockId: getBlockConnection({ outputs }, outputIndex),
+    });
+  });
+}
+
+export default conditions;

+ 24 - 60
src/background/workflow-engine/blocks-handler/handler-interaction-block.js

@@ -1,17 +1,10 @@
-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) {
+async function interactionHandler(block, { refData }) {
   const nextBlockId = getBlockConnection(block);
   const nextBlockId = getBlockConnection(block);
 
 
   try {
   try {
-    const refData = {
-      prevBlockData,
-      dataColumns: this.data,
-      loopData: this.loopData,
-      globalData: this.globalData,
-      activeTabUrl: this.activeTabUrl,
-    };
     const data = await this._sendMessageToTab(
     const data = await this._sendMessageToTab(
       { ...block, refData },
       { ...block, refData },
       {
       {
@@ -22,60 +15,27 @@ async function interactionHandler(block, prevBlockData) {
     if (block.name === 'link')
     if (block.name === 'link')
       await new Promise((resolve) => setTimeout(resolve, 5000));
       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')) {
     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);
-        }
+      if (!block.data.saveData)
+        return {
+          data,
+          nextBlockId,
+        };
+
+      const currentColumnType =
+        this.columns[block.data.dataColumn]?.type || 'any';
+
+      if (Array.isArray(data) && currentColumnType !== 'array') {
+        data.forEach((item) => {
+          this.addData(block.data.dataColumn, item);
+        });
+      } else {
+        this.addData(block.data.dataColumn, data);
       }
       }
     } else if (block.name === 'javascript-code') {
     } else if (block.name === 'javascript-code') {
-      const memoColumn = {};
-      const pushObjectData = (obj) => {
-        Object.entries(obj).forEach(([key, value]) => {
-          let column;
+      const arrData = Array.isArray(data) ? data : [data];
 
 
-          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);
-      }
+      this.addData(arrData);
     }
     }
 
 
     return {
     return {
@@ -84,6 +44,10 @@ async function interactionHandler(block, prevBlockData) {
     };
     };
   } catch (error) {
   } catch (error) {
     error.nextBlockId = nextBlockId;
     error.nextBlockId = nextBlockId;
+    error.data = {
+      name: block.name,
+      selector: block.data.selector,
+    };
 
 
     throw error;
     throw error;
   }
   }

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

@@ -1,6 +1,6 @@
 import { getBlockConnection } from '../helper';
 import { getBlockConnection } from '../helper';
 
 
-function loopBreakpoint(block, prevBlockData) {
+function loopBreakpoint(block, { prevBlockData }) {
   const currentLoop = this.loopList[block.data.loopId];
   const currentLoop = this.loopList[block.data.loopId];
 
 
   return new Promise((resolve) => {
   return new Promise((resolve) => {

+ 61 - 24
src/background/workflow-engine/engine.js

@@ -2,8 +2,9 @@
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
 import { nanoid } from 'nanoid';
 import { nanoid } from 'nanoid';
 import { tasks } from '@/utils/shared';
 import { tasks } from '@/utils/shared';
-import { toCamelCase, parseJSON } from '@/utils/helper';
+import { convertData } from './helper';
 import { generateJSON } from '@/utils/data-exporter';
 import { generateJSON } from '@/utils/data-exporter';
+import { toCamelCase, parseJSON, isObject, objectHasKey } from '@/utils/helper';
 import errorMessage from './error-message';
 import errorMessage from './error-message';
 import referenceData from '@/utils/reference-data';
 import referenceData from '@/utils/reference-data';
 import workflowState from '../workflow-state';
 import workflowState from '../workflow-state';
@@ -20,7 +21,7 @@ function tabRemovedHandler(tabId) {
     this.currentBlock.name === 'new-tab' ||
     this.currentBlock.name === 'new-tab' ||
     tasks[this.currentBlock.name].category === 'interaction'
     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);
   workflowState.update(this.id, this.state);
@@ -83,7 +84,8 @@ class WorkflowEngine {
     this.collectionLogId = collectionLogId;
     this.collectionLogId = collectionLogId;
     this.globalData = parseJSON(globalDataVal, globalDataVal);
     this.globalData = parseJSON(globalDataVal, globalDataVal);
     this.activeTabUrl = '';
     this.activeTabUrl = '';
-    this.data = {};
+    this.columns = { column: { index: 0, type: 'any' } };
+    this.data = [];
     this.logs = [];
     this.logs = [];
     this.blocks = {};
     this.blocks = {};
     this.frames = {};
     this.frames = {};
@@ -113,7 +115,7 @@ class WorkflowEngine {
       typeof this.workflow.drawflow === 'string'
       typeof this.workflow.drawflow === 'string'
         ? JSON.parse(this.workflow.drawflow || '{}')
         ? JSON.parse(this.workflow.drawflow || '{}')
         : this.workflow.drawflow;
         : this.workflow.drawflow;
-    const blocks = drawflowData?.drawflow.Home.data;
+    const blocks = drawflowData?.drawflow?.Home.data;
 
 
     if (!blocks) {
     if (!blocks) {
       console.error(errorMessage('no-block', this.workflow));
       console.error(errorMessage('no-block', this.workflow));
@@ -138,14 +140,10 @@ class WorkflowEngine {
     this.blocks = blocks;
     this.blocks = blocks;
     this.startedTimestamp = Date.now();
     this.startedTimestamp = Date.now();
     this.workflow.dataColumns = dataColumns;
     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
     workflowState
       .add(this.id, {
       .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) {
   on(name, listener) {
     (this.eventListeners[name] = this.eventListeners[name] || []).push(
     (this.eventListeners[name] = this.eventListeners[name] || []).push(
       listener
       listener
@@ -176,7 +206,7 @@ class WorkflowEngine {
         await this.childWorkflow.stop();
         await this.childWorkflow.stop();
       }
       }
 
 
-      this.logs.push({
+      this.addLog({
         message,
         message,
         type: 'stop',
         type: 'stop',
         name: 'stop',
         name: 'stop',
@@ -210,6 +240,7 @@ class WorkflowEngine {
           name,
           name,
           icon,
           icon,
           status,
           status,
+          message,
           id: this.id,
           id: this.id,
           workflowId: id,
           workflowId: id,
           data: jsonData,
           data: jsonData,
@@ -285,7 +316,6 @@ class WorkflowEngine {
 
 
     if (!disableTimeoutKeys.includes(block.name)) {
     if (!disableTimeoutKeys.includes(block.name)) {
       this.workflowTimeout = setTimeout(() => {
       this.workflowTimeout = setTimeout(() => {
-        alert('timeout');
         if (!this.isDestroyed) this.stop('stop-timeout');
         if (!this.isDestroyed) this.stop('stop-timeout');
       }, this.workflow.settings.timeout || 120000);
       }, this.workflow.settings.timeout || 120000);
     }
     }
@@ -303,20 +333,23 @@ class WorkflowEngine {
         : blockHandler;
         : blockHandler;
 
 
     if (handler) {
     if (handler) {
-      const replacedBlock = referenceData(block, {
+      const refData = {
         prevBlockData,
         prevBlockData,
         data: this.data,
         data: this.data,
         loopData: this.loopData,
         loopData: this.loopData,
         globalData: this.globalData,
         globalData: this.globalData,
         activeTabUrl: this.activeTabUrl,
         activeTabUrl: this.activeTabUrl,
-      });
+      };
+      const replacedBlock = referenceData(block, refData);
+      const blockDelay =
+        block.name === 'trigger' ? 0 : this.workflow.settings?.blockDelay || 0;
 
 
       handler
       handler
-        .call(this, replacedBlock, prevBlockData)
+        .call(this, replacedBlock, { prevBlockData, refData })
         .then((result) => {
         .then((result) => {
           clearTimeout(this.workflowTimeout);
           clearTimeout(this.workflowTimeout);
           this.workflowTimeout = null;
           this.workflowTimeout = null;
-          this.logs.push({
+          this.addLog({
             type: 'success',
             type: 'success',
             name: block.name,
             name: block.name,
             logId: result.logId,
             logId: result.logId,
@@ -324,9 +357,11 @@ class WorkflowEngine {
           });
           });
 
 
           if (result.nextBlockId) {
           if (result.nextBlockId) {
-            this._blockHandler(this.blocks[result.nextBlockId], result.data);
+            setTimeout(() => {
+              this._blockHandler(this.blocks[result.nextBlockId], result.data);
+            }, blockDelay);
           } else {
           } else {
-            this.logs.push({
+            this.addLog({
               type: 'finish',
               type: 'finish',
               name: 'finish',
               name: 'finish',
             });
             });
@@ -335,7 +370,7 @@ class WorkflowEngine {
           }
           }
         })
         })
         .catch((error) => {
         .catch((error) => {
-          this.logs.push({
+          this.addLog({
             type: 'error',
             type: 'error',
             message: error.message,
             message: error.message,
             name: block.name,
             name: block.name,
@@ -346,10 +381,12 @@ class WorkflowEngine {
             this.workflow.settings.onError === 'keep-running' &&
             this.workflow.settings.onError === 'keep-running' &&
             error.nextBlockId
             error.nextBlockId
           ) {
           ) {
-            this._blockHandler(
-              this.blocks[error.nextBlockId],
-              error.data || ''
-            );
+            setTimeout(() => {
+              this._blockHandler(
+                this.blocks[error.nextBlockId],
+                error.data || ''
+              );
+            }, blockDelay);
           } else {
           } else {
             this.destroy('error', error.message);
             this.destroy('error', error.message);
           }
           }

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

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

+ 61 - 66
src/components/block/BlockConditions.vue

@@ -14,15 +14,11 @@
         class="cursor-pointer mr-2"
         class="cursor-pointer mr-2"
         @click="editor.removeNodeId(`node-${block.id}`)"
         @click="editor.removeNodeId(`node-${block.id}`)"
       />
       />
-      <ui-button
-        :disabled="block.data.conditions && block.data.conditions.length > 4"
-        icon
-        variant="accent"
-        style="height: 37px; width: 37px"
-        @click="addComparison"
-      >
-        <v-remixicon name="riAddLine" class="inline-block" />
-      </ui-button>
+      <v-remixicon
+        name="riPencilLine"
+        class="inline-block cursor-pointer"
+        @click="editBlock"
+      />
     </div>
     </div>
     <div
     <div
       v-if="block.data.conditions && block.data.conditions.length !== 0"
       v-if="block.data.conditions && block.data.conditions.length !== 0"
@@ -35,39 +31,30 @@
       >
       >
         <v-remixicon
         <v-remixicon
           name="riDeleteBin7Line"
           name="riDeleteBin7Line"
-          class="mr-2 invisible group-hover:visible cursor-pointer"
-          @click="deleteComparison(index)"
+          class="mr-2 cursor-pointer group-hover:visible invisible"
+          @click="deleteCondition(index)"
         />
         />
-        <div class="flex items-center transition bg-input rounded-lg">
-          <select
-            v-model="block.data.conditions[index].type"
-            :title="getTitle(index)"
-            class="
-              bg-transparent
-              font-mono
-              z-10
-              p-2
-              text-center
-              transition
-              rounded-l-lg
-              appearance-none
-            "
-          >
-            <option
-              v-for="(name, type) in conditions"
-              :key="type"
-              :value="type"
-            >
-              {{ type }}
-            </option>
-          </select>
-          <div class="bg-gray-300 w-px" style="height: 30px"></div>
-          <input
-            v-model="block.data.conditions[index].value"
-            type="text"
-            placeholder="value"
-            class="p-2 flex-1 transition rounded-r-lg bg-transparent w-36"
-          />
+        <div
+          class="
+            flex
+            items-center
+            flex-1
+            p-2
+            bg-box-transparent
+            rounded-lg
+            overflow-hidden
+            w-44
+          "
+        >
+          <p class="w-5/12 text-overflow text-right">
+            {{ item.compareValue || '_____' }}
+          </p>
+          <p class="w-2/12 text-center mx-1 font-mono">
+            {{ item.type }}
+          </p>
+          <p class="w-5/12 text-overflow">
+            {{ item.value || '_____' }}
+          </p>
         </div>
         </div>
       </div>
       </div>
       <p
       <p
@@ -83,7 +70,7 @@
   </div>
   </div>
 </template>
 </template>
 <script setup>
 <script setup>
-import { watch, toRaw } from 'vue';
+import { watch, toRaw, onBeforeUnmount } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import emitter from 'tiny-emitter/instance';
 import emitter from 'tiny-emitter/instance';
 import { debounce } from '@/utils/helper';
 import { debounce } from '@/utils/helper';
@@ -101,36 +88,36 @@ const { t } = useI18n();
 const componentId = useComponentId('block-conditions');
 const componentId = useComponentId('block-conditions');
 const block = useEditorBlock(`#${componentId}`, props.editor);
 const block = useEditorBlock(`#${componentId}`, props.editor);
 
 
-const conditions = {
-  '==': 'equals',
-  '>': 'gt',
-  '>=': 'gte',
-  '<': 'lt',
-  '<=': 'lte',
-  '()': 'contains',
-};
-
-function getTitle(index) {
-  const type = conditions[block.data.conditions[index]?.type] || 'equals';
-
-  return t(`workflow.blocks.conditions.${type}`);
+function editBlock() {
+  emitter.emit('editor:edit-block', {
+    ...block.details,
+    data: block.data,
+    blockId: block.id,
+  });
 }
 }
-function addComparison() {
-  if (block.data.conditions.length >= 10) return;
+function addConditionEmit({ id }) {
+  if (id !== block.id) return;
 
 
-  block.data.conditions.push({ type: '==', value: '' });
+  const { length } = block.data.conditions;
 
 
-  if (block.data.conditions.length === 1) props.editor.addNodeOutput(block.id);
+  if (length >= 10) return;
+  if (length === 1) props.editor.addNodeOutput(block.id);
 
 
   props.editor.addNodeOutput(block.id);
   props.editor.addNodeOutput(block.id);
 }
 }
-function deleteComparison(index) {
-  block.data.conditions.splice(index, 1);
+function deleteConditionEmit({ index, id }) {
+  if (id !== block.id) return;
 
 
   props.editor.removeNodeOutput(block.id, `output_${index + 1}`);
   props.editor.removeNodeOutput(block.id, `output_${index + 1}`);
+
   if (block.data.conditions.length === 0)
   if (block.data.conditions.length === 0)
     props.editor.removeNodeOutput(block.id, `output_1`);
     props.editor.removeNodeOutput(block.id, `output_1`);
 }
 }
+function deleteCondition(index) {
+  block.data.conditions.splice(index, 1);
+
+  deleteConditionEmit({ index, id: block.id });
+}
 
 
 watch(
 watch(
   () => block.data.conditions,
   () => block.data.conditions,
@@ -141,12 +128,20 @@ watch(
 
 
     props.editor.updateConnectionNodes(`node-${block.id}`);
     props.editor.updateConnectionNodes(`node-${block.id}`);
 
 
-    if (oldValue) {
-      emitter.emit('editor:data-changed', block.id);
-    }
+    if (!oldValue) return;
+
+    emitter.emit('editor:data-changed', block.id);
   }, 250),
   }, 250),
   { deep: true }
   { deep: true }
 );
 );
+
+emitter.on('conditions-block:add', addConditionEmit);
+emitter.on('conditions-block:delete', deleteConditionEmit);
+
+onBeforeUnmount(() => {
+  emitter.off('conditions-block:add', addConditionEmit);
+  emitter.off('conditions-block:delete', deleteConditionEmit);
+});
 </script>
 </script>
 <style>
 <style>
 .drawflow .drawflow-node.conditions .outputs {
 .drawflow .drawflow-node.conditions .outputs {
@@ -154,9 +149,9 @@ watch(
   transform: none !important;
   transform: none !important;
 }
 }
 .drawflow .drawflow-node.conditions .output {
 .drawflow .drawflow-node.conditions .output {
-  margin-bottom: 32px;
+  margin-bottom: 30px;
 }
 }
 .drawflow .drawflow-node.conditions .output:nth-last-child(2) {
 .drawflow .drawflow-node.conditions .output:nth-last-child(2) {
-  margin-bottom: 20px;
+  margin-bottom: 22px;
 }
 }
 </style>
 </style>

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

@@ -51,7 +51,7 @@ defineProps({
   },
   },
 });
 });
 
 
-const { t } = useI18n();
+const { t, te } = useI18n();
 
 
 const statusColors = {
 const statusColors = {
   error: 'bg-red-200',
   error: 'bg-red-200',
@@ -64,7 +64,13 @@ function formatDate(date, format) {
 
 
   return dayjs(date).format(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];
   const lastHistory = history[history.length - 1];
 
 
   return lastHistory && lastHistory.type === 'error'
   return lastHistory && lastHistory.type === 'error'

+ 17 - 2
src/components/newtab/workflow/WorkflowSettings.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <div class="workflow-settings">
   <div class="workflow-settings">
-    <div class="mb-4">
+    <div class="mb-6">
       <p class="mb-1">{{ t('workflow.settings.onError.title') }}</p>
       <p class="mb-1">{{ t('workflow.settings.onError.title') }}</p>
       <div class="space-x-4">
       <div class="space-x-4">
         <ui-radio
         <ui-radio
@@ -15,14 +15,29 @@
         </ui-radio>
         </ui-radio>
       </div>
       </div>
     </div>
     </div>
-    <div>
+    <div class="mb-6">
       <p class="mb-1">{{ t('workflow.settings.timeout.title') }}</p>
       <p class="mb-1">{{ t('workflow.settings.timeout.title') }}</p>
       <ui-input
       <ui-input
         :model-value="workflow.settings.timeout"
         :model-value="workflow.settings.timeout"
         type="number"
         type="number"
+        class="w-full max-w-sm"
         @change="updateWorkflow({ timeout: +$event })"
         @change="updateWorkflow({ timeout: +$event })"
       />
       />
     </div>
     </div>
+    <div>
+      <p class="mb-1">
+        {{ t('workflow.settings.blockDelay.title') }}
+        <span :title="t('workflow.settings.blockDelay.description')">
+          &#128712;
+        </span>
+      </p>
+      <ui-input
+        :model-value="workflow.settings.blockDelay"
+        type="number"
+        class="w-full max-w-sm"
+        @change="updateWorkflow({ blockDelay: +$event })"
+      />
+    </div>
   </div>
   </div>
 </template>
 </template>
 <script setup>
 <script setup>

+ 133 - 0
src/components/newtab/workflow/edit/EditConditions.vue

@@ -0,0 +1,133 @@
+<template>
+  <div>
+    <ui-button variant="accent" class="mb-4" @click="addCondition">
+      Add condition
+    </ui-button>
+    <ul class="space-y-2">
+      <li
+        v-for="(condition, index) in conditions"
+        :key="index"
+        class="relative rounded-lg bg-input transition-colors group"
+      >
+        <input
+          v-model="condition.compareValue"
+          type="text"
+          placeholder="value"
+          class="py-2 px-4 w-full transition rounded-lg bg-transparent"
+        />
+        <button
+          class="
+            bg-white
+            absolute
+            top-1/2
+            right-4
+            p-2
+            rounded-lg
+            -translate-y-1/2
+            group-hover:right-14
+          "
+          @click="deleteCondition(index)"
+        >
+          <v-remixicon size="20" name="riDeleteBin7Line" />
+        </button>
+        <select
+          v-model="condition.type"
+          :title="getTitle(index)"
+          class="
+            bg-white
+            absolute
+            right-4
+            font-mono
+            z-10
+            p-2
+            top-1/2
+            leading-tight
+            -translate-y-1/2
+            text-center
+            transition
+            rounded-lg
+            appearance-none
+          "
+        >
+          <option
+            v-for="(name, type) in conditionTypes"
+            :key="type"
+            :value="type"
+          >
+            {{ type }}
+          </option>
+        </select>
+        <div
+          class="w-full bg-gray-300 h-px mx-auto"
+          style="max-width: 89%"
+        ></div>
+        <input
+          v-model="condition.value"
+          type="text"
+          placeholder="value"
+          class="py-2 px-4 w-full transition rounded-lg bg-transparent"
+        />
+      </li>
+    </ul>
+  </div>
+</template>
+<script setup>
+import { toRef } from 'vue';
+import { useI18n } from 'vue-i18n';
+import emitter from 'tiny-emitter/instance';
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+  blockId: {
+    type: String,
+    default: '',
+  },
+});
+defineEmits(['update:data']);
+
+const conditionTypes = {
+  '==': 'equals',
+  '!=': 'ne',
+  '>': 'gt',
+  '>=': 'gte',
+  '<': 'lt',
+  '<=': 'lte',
+  '()': 'contains',
+};
+const { t } = useI18n();
+
+const conditions = toRef(props.data, 'conditions');
+
+function getTitle(index) {
+  const type = conditionTypes[conditions.value[index]?.type] || 'equals';
+
+  return t(`workflow.blocks.conditions.${type}`);
+}
+function addCondition() {
+  if (conditions.value.length >= 10) return;
+
+  conditions.value.unshift({
+    compareValue: '',
+    value: '',
+    type: '==',
+  });
+
+  emitter.emit('conditions-block:add', {
+    id: props.blockId,
+  });
+}
+function deleteCondition(index) {
+  conditions.value.splice(index, 1);
+
+  emitter.emit('conditions-block:delete', {
+    index,
+    id: prps.blockId,
+  });
+}
+// function updateData(value) {
+//   emit('update:data', { ...props.data, ...value });
+// }
+</script>

+ 9 - 9
src/components/newtab/workflow/edit/EditElementExists.vue

@@ -1,16 +1,16 @@
 <template>
 <template>
-  <ui-input
-    :model-value="data.selector"
-    :label="t('workflow.blocks.element-exists.selector')"
-    class="mb-1 w-full"
-    @change="updateData({ selector: $event })"
-  />
-  <div class="flex space-x-2">
+  <div>
+    <ui-input
+      :model-value="data.selector"
+      :label="t('workflow.blocks.element-exists.selector')"
+      class="mb-1 w-full"
+      @change="updateData({ selector: $event })"
+    />
     <ui-input
     <ui-input
       :model-value="data.tryCount"
       :model-value="data.tryCount"
       :title="t('workflow.blocks.element-exists.tryFor.title')"
       :title="t('workflow.blocks.element-exists.tryFor.title')"
       :label="t('workflow.blocks.element-exists.tryFor.label')"
       :label="t('workflow.blocks.element-exists.tryFor.label')"
-      class="flex-1"
+      class="w-full mb-1"
       type="number"
       type="number"
       min="1"
       min="1"
       @change="updateData({ tryCount: +$event })"
       @change="updateData({ tryCount: +$event })"
@@ -19,7 +19,7 @@
       :model-value="data.timeout"
       :model-value="data.timeout"
       :label="t('workflow.blocks.element-exists.timeout.label')"
       :label="t('workflow.blocks.element-exists.timeout.label')"
       :title="t('workflow.blocks.element-exists.timeout.title')"
       :title="t('workflow.blocks.element-exists.timeout.title')"
-      class="flex-1"
+      class="w-full"
       type="number"
       type="number"
       min="200"
       min="200"
       @change="updateData({ timeout: +$event })"
       @change="updateData({ timeout: +$event })"

+ 16 - 10
src/content/blocks-handler/handler-attribute-value.js

@@ -1,7 +1,7 @@
 import { handleElement } from '../helper';
 import { handleElement } from '../helper';
 
 
 function attributeValue(block) {
 function attributeValue(block) {
-  return new Promise((resolve) => {
+  return new Promise((resolve, reject) => {
     let result = [];
     let result = [];
     const { attributeName, multiple } = block.data;
     const { attributeName, multiple } = block.data;
     const isCheckboxOrRadio = (element) => {
     const isCheckboxOrRadio = (element) => {
@@ -10,17 +10,23 @@ function attributeValue(block) {
       return ['checkbox', 'radio'].includes(element.getAttribute('type'));
       return ['checkbox', 'radio'].includes(element.getAttribute('type'));
     };
     };
 
 
-    handleElement(block, (element) => {
-      const value =
-        attributeName === 'checked' && isCheckboxOrRadio(element)
-          ? element.checked
-          : element.getAttribute(attributeName);
+    handleElement(block, {
+      onSelected(element) {
+        const value =
+          attributeName === 'checked' && isCheckboxOrRadio(element)
+            ? element.checked
+            : element.getAttribute(attributeName);
 
 
-      if (multiple) result.push(value);
-      else result = value;
+        if (multiple) result.push(value);
+        else result = value;
+      },
+      onError(error) {
+        reject(error);
+      },
+      onSuccess() {
+        resolve(result);
+      },
     });
     });
-
-    resolve(result);
   });
   });
 }
 }
 
 

+ 20 - 15
src/content/blocks-handler/handler-element-scroll.js

@@ -13,25 +13,30 @@ function elementScroll(block) {
     return currentPos;
     return currentPos;
   }
   }
 
 
-  return new Promise((resolve) => {
+  return new Promise((resolve, reject) => {
     const { data } = block;
     const { data } = block;
     const behavior = data.smooth ? 'smooth' : 'auto';
     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,
-        });
-      }
+    handleElement(block, {
+      onSelected(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,
+          });
+        }
+      },
+      onError(error) {
+        reject(error);
+      },
+      onSuccess() {
+        window.dispatchEvent(new Event('scroll'));
+        resolve('');
+      },
     });
     });
-
-    window.dispatchEvent(new Event('scroll'));
-
-    resolve('');
   });
   });
 }
 }
 
 

+ 11 - 3
src/content/blocks-handler/handler-event-click.js

@@ -1,9 +1,17 @@
 import { handleElement } from '../helper';
 import { handleElement } from '../helper';
 
 
 function eventClick(block) {
 function eventClick(block) {
-  return new Promise((resolve) => {
-    handleElement(block, (element) => {
-      element.click();
+  return new Promise((resolve, reject) => {
+    handleElement(block, {
+      onSelected(element) {
+        element.click();
+      },
+      onError(error) {
+        reject(error);
+      },
+      onSuccess() {
+        resolve('');
+      },
     });
     });
 
 
     resolve('');
     resolve('');

+ 8 - 2
src/content/blocks-handler/handler-forms.js

@@ -2,9 +2,15 @@ import { handleElement, markElement } from '../helper';
 import handleFormElement from '@/utils/handle-form-element';
 import handleFormElement from '@/utils/handle-form-element';
 
 
 function forms(block) {
 function forms(block) {
-  return new Promise((resolve) => {
+  return new Promise((resolve, reject) => {
     const { data } = block;
     const { data } = block;
-    const elements = handleElement(block, true);
+    const elements = handleElement(block, { returnElement: true });
+
+    if (!elements) {
+      reject(new Error('element-not-found'));
+
+      return;
+    }
 
 
     if (data.getValue) {
     if (data.getValue) {
       let result = '';
       let result = '';

+ 29 - 13
src/content/blocks-handler/handler-get-text.js

@@ -1,26 +1,42 @@
 import { handleElement } from '../helper';
 import { handleElement } from '../helper';
 
 
 function getText(block) {
 function getText(block) {
-  return new Promise((resolve) => {
+  return new Promise((resolve, reject) => {
     let regex;
     let regex;
-    const { regex: regexData, regexExp, prefixText, suffixText } = block.data;
-    const textResult = [];
+    let textResult = [];
+    const {
+      regex: regexData,
+      regexExp,
+      prefixText,
+      suffixText,
+      multiple,
+    } = block.data;
 
 
     if (regexData) {
     if (regexData) {
       regex = new RegExp(regexData, regexExp.join(''));
       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);
+    handleElement(block, {
+      onSelected(element) {
+        let text = element.innerText;
+
+        if (regex) text = text.match(regex).join(' ');
+
+        text = (prefixText || '') + text + (suffixText || '');
+
+        if (multiple) {
+          textResult.push(text);
+        } else {
+          textResult = text;
+        }
+      },
+      onError(error) {
+        reject(error);
+      },
+      onSuccess() {
+        resolve(textResult);
+      },
     });
     });
-
-    resolve(textResult);
   });
   });
 }
 }
 
 

+ 2 - 2
src/content/blocks-handler/handler-link.js

@@ -1,11 +1,11 @@
 import { markElement } from '../helper';
 import { markElement } from '../helper';
 
 
 function link(block) {
 function link(block) {
-  return new Promise((resolve) => {
+  return new Promise((resolve, reject) => {
     const element = document.querySelector(block.data.selector);
     const element = document.querySelector(block.data.selector);
 
 
     if (!element) {
     if (!element) {
-      resolve('');
+      reject(new Error('element-not-found'));
       return;
       return;
     }
     }
 
 

+ 9 - 7
src/content/blocks-handler/handler-switch-to.js

@@ -1,10 +1,9 @@
 import { handleElement } from '../helper';
 import { handleElement } from '../helper';
 
 
 function switchTo(block) {
 function switchTo(block) {
-  return new Promise((resolve) => {
-    handleElement(
-      block,
-      (element) => {
+  return new Promise((resolve, reject) => {
+    handleElement(block, {
+      onSelected(element) {
         if (element.tagName !== 'IFRAME') {
         if (element.tagName !== 'IFRAME') {
           resolve('');
           resolve('');
           return;
           return;
@@ -12,10 +11,13 @@ function switchTo(block) {
 
 
         resolve({ url: element.src });
         resolve({ url: element.src });
       },
       },
-      () => {
+      onSuccess() {
         resolve('');
         resolve('');
-      }
-    );
+      },
+      onError(error) {
+        reject(error);
+      },
+    });
   });
   });
 }
 }
 
 

+ 11 - 3
src/content/blocks-handler/handler-trigger-event.js

@@ -2,11 +2,19 @@ import { handleElement } from '../helper';
 import simulateEvent from '@/utils/simulate-event';
 import simulateEvent from '@/utils/simulate-event';
 
 
 function triggerEvent(block) {
 function triggerEvent(block) {
-  return new Promise((resolve) => {
+  return new Promise((resolve, reject) => {
     const { data } = block;
     const { data } = block;
 
 
-    handleElement(block, (element) => {
-      simulateEvent(element, data.eventName, data.eventParams);
+    handleElement(block, {
+      onSelected(element) {
+        simulateEvent(element, data.eventName, data.eventParams);
+      },
+      onSuccess() {
+        resolve(data.eventName);
+      },
+      onError(error) {
+        reject(error);
+      },
     });
     });
 
 
     resolve(data.eventName);
     resolve(data.eventName);

+ 1 - 0
src/content/element-selector/AppBlocks.vue

@@ -29,6 +29,7 @@
     <prism-editor
     <prism-editor
       v-if="state.blockResult"
       v-if="state.blockResult"
       v-model="state.blockResult"
       v-model="state.blockResult"
+      readonly
       :highlight="highlighter('json')"
       :highlight="highlighter('json')"
       class="h-full scroll mt-2"
       class="h-full scroll mt-2"
     />
     />

+ 23 - 9
src/content/helper.js

@@ -8,26 +8,40 @@ export function markElement(el, { id, data }) {
   }
   }
 }
 }
 
 
-export function handleElement({ data, id }, callback, errCallback) {
-  if (!data || !data.selector) return null;
+export function handleElement(
+  { data, id },
+  { onSelected, onError, onSuccess, returnElement }
+) {
+  if (!data || !data.selector) {
+    if (onError) onError(new Error('selector-empty'));
+    return null;
+  }
 
 
   try {
   try {
     data.blockIdAttr = `block--${id}`;
     data.blockIdAttr = `block--${id}`;
-    const element = FindElement[data.findBy || 'cssSelector'](data);
 
 
-    if (typeof callback === 'boolean' && callback) return element;
+    const selectorType = data.findBy || 'cssSelector';
+    const element = FindElement[selectorType](data);
+
+    if (returnElement) return element;
+
+    if (!element) {
+      if (onError) onError(new Error('element-not-found'));
 
 
-    if (data.multiple && (data.findBy || 'cssSelector') === 'cssSelector') {
+      return null;
+    }
+
+    if (data.multiple && selectorType === 'cssSelector') {
       element.forEach((el) => {
       element.forEach((el) => {
         markElement(el, { id, data });
         markElement(el, { id, data });
-        callback(el);
+        onSelected(el);
       });
       });
     } else if (element) {
     } else if (element) {
       markElement(element, { id, data });
       markElement(element, { id, data });
-      callback(element);
-    } else if (errCallback) {
-      errCallback();
+      onSelected(element);
     }
     }
+
+    if (onSuccess) onSuccess();
   } catch (error) {
   } catch (error) {
     console.error(error);
     console.error(error);
   }
   }

+ 34 - 0
src/content/shortcut.js

@@ -1,5 +1,7 @@
+import { openDB } from 'idb';
 import Mousetrap from 'mousetrap';
 import Mousetrap from 'mousetrap';
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
+import secrets from 'secrets';
 import { sendMessage } from '@/utils/message';
 import { sendMessage } from '@/utils/message';
 
 
 Mousetrap.prototype.stopCallback = function () {
 Mousetrap.prototype.stopCallback = function () {
@@ -17,6 +19,34 @@ function getTriggerBlock(workflow) {
   return trigger;
   return trigger;
 }
 }
 
 
+async function listenWindowMessage(workflows) {
+  try {
+    if (secrets?.webOrigin !== window.location.origin) return;
+
+    const db = await openDB('automa', 1, {
+      upgrade(event) {
+        event.createObjectStore('store');
+      },
+    });
+
+    db.put('store', workflows, 'workflows');
+
+    window.addEventListener('__automa-ext__', async ({ detail }) => {
+      if (detail.type === 'open-workflow') {
+        if (!detail.workflowId) return;
+
+        sendMessage(
+          'open:dashboard',
+          `/workflows/${detail.workflowId}`,
+          'background'
+        );
+      }
+    });
+  } catch (error) {
+    console.error(error);
+  }
+}
+
 (async () => {
 (async () => {
   try {
   try {
     const { shortcuts, workflows } = await browser.storage.local.get([
     const { shortcuts, workflows } = await browser.storage.local.get([
@@ -25,6 +55,10 @@ function getTriggerBlock(workflow) {
     ]);
     ]);
     const shortcutsArr = Object.entries(shortcuts || {});
     const shortcutsArr = Object.entries(shortcuts || {});
 
 
+    listenWindowMessage(workflows);
+
+    document.body.setAttribute('data-atm-ext-installed', '');
+
     if (shortcutsArr.length === 0) return;
     if (shortcutsArr.length === 0) return;
 
 
     const keyboardShortcuts = shortcutsArr.reduce((acc, [id, value]) => {
     const keyboardShortcuts = shortcutsArr.reduce((acc, [id, value]) => {

+ 1 - 0
src/lib/dayjs.js

@@ -2,6 +2,7 @@ import dayjs from 'dayjs';
 import relativeTime from 'dayjs/plugin/relativeTime';
 import relativeTime from 'dayjs/plugin/relativeTime';
 import 'dayjs/locale/zh';
 import 'dayjs/locale/zh';
 import 'dayjs/locale/zh-tw';
 import 'dayjs/locale/zh-tw';
+import 'dayjs/locale/vi';
 
 
 dayjs.extend(relativeTime);
 dayjs.extend(relativeTime);
 
 

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

@@ -230,6 +230,7 @@
         "gte": "Greater than or equal",
         "gte": "Greater than or equal",
         "lt": "Less than",
         "lt": "Less than",
         "lte": "Less than or equal",
         "lte": "Less than or equal",
+        "ne": "Not equals",
         "contains": "Contains"
         "contains": "Contains"
       },
       },
       "element-exists": {
       "element-exists": {

+ 14 - 6
src/locales/en/newtab.json

@@ -47,7 +47,11 @@
       },
       },
       "timeout": {
       "timeout": {
         "title": "Workflow timeout (milliseconds)"
         "title": "Workflow timeout (milliseconds)"
-      }
+      },
+      "blockDelay": {
+        "title": "Block delay (milliseconds)",
+        "description": "Add delay before executing each of the blocks"
+      },
     }
     }
   },
   },
   "collection": {
   "collection": {
@@ -80,14 +84,18 @@
       "finish": "Finish"
       "finish": "Finish"
     },
     },
     "messages": {
     "messages": {
-      "workflow-disabled": "Workflow is disabled",
-      "stop-timeout": "Workflow is stopped because of timeout",
+      "conditions-empty": "Conditions is empty",
       "invalid-proxy-host": "Invalid proxy host",
       "invalid-proxy-host": "Invalid proxy host",
-      "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.",
+      "workflow-disabled": "Workflow is disabled",
+      "selector-empty": "Element selector is empty",
       "empty-workflow": "You must select a workflow first",
       "empty-workflow": "You must select a workflow first",
+      "active-tab-removed": "Workflow active tab is removed",
+      "stop-timeout": "Workflow is stopped because of timeout",
       "no-workflow": "Can't find workflow with \"{workflowId}\" ID",
       "no-workflow": "Can't find workflow with \"{workflowId}\" ID",
-      "workflow-infinite-loop": "Can't execute the workflow to prevent an infinite loop"
+      "element-not-found": "Can't find an element with \"{selector}\" selector.",
+      "workflow-infinite-loop": "Can't execute the workflow to prevent an infinite loop",
+      "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."
     },
     },
     "description": {
     "description": {
       "text": "{status} on {date} in {duration}",
       "text": "{status} on {date} in {duration}",

+ 317 - 0
src/locales/vi/blocks.json

@@ -0,0 +1,317 @@
+{
+  "collection": {
+    "blocks": {
+      "export-result": {
+        "name": "Xuất kết quả",
+        "description": "Xuất kết quả thu thập dưới dạng JSON"
+      }
+    }
+  },
+  "workflow": {
+    "blocks": {
+      "base": {
+        "selector": "Bộ chọn phần tử",
+        "findElement": {
+          "placeholder": "Tìm phần tử bằng",
+          "options": {
+            "cssSelector": "Bộ chọn CSS",
+            "xpath": "XPath"
+          }
+        },
+        "markElement": {
+          "title": "Một phần tử sẽ không được chọn nếu nó đã được chọn trước đó",
+          "text": "Đánh dấu phần tử"
+        },
+        "multiple": {
+          "title": "Chọn nhiều phần tử",
+          "text": "Nhiều"
+        }
+      },
+      "trigger": {
+        "name": "Kích hoạt",
+        "description": "Quy trình bắt đầu được thực thi ở đây",
+        "days": [
+          "Chủ nhật",
+          "Thứ hai",
+          "Thứ ba",
+          "Thứ tư",
+          "Thứ năm",
+          "Thứ sáu",
+          "Thứ bảy"
+        ],
+        "useRegex": "Dùng regex",
+        "shortcut": {
+          "tootlip": "Ghi lại lối tắt",
+          "checkboxTitle": "Execute shortcut even when you're in an input element",
+          "checkbox": "Hoạt động khi nhập liệu",
+          "note": "Lưu ý: phím tắt chỉ hoạt động khi bạn đang truy cập một trang web"
+        },
+        "forms": {
+          "triggerWorkflow": "Quy trình kích hoạt",
+          "interval": "Chu kỳ (phút)",
+          "delay": "Độ trễ (phút)",
+          "date": "Ngày",
+          "time": "Giờ",
+          "url": "URL hoặc Regex",
+          "shortcut": "Phim tắt"
+        },
+        "items": {
+          "manual": "Thủ công",
+          "interval": "Chu kỳ",
+          "date": "Vào một ngày cụ thể",
+          "specific-day": "Vào một ngày cụ thể",
+          "visit-web": "Khi truy cập một trang web",
+          "keyboard-shortcut": "Phim tắt"
+        }
+      },
+      "execute-workflow": {
+        "name": "Thực thi quy trình",
+        "overwriteNote": "Thao tác này sẽ ghi đè lên dữ liệu chung của quy trình đã chọn",
+        "select": "Chọn quy trình",
+        "description": ""
+      },
+      "active-tab": {
+        "name": "Tab hoạt động",
+        "description": "Chỉ định tab hiện tại mà bạn đang truy cập thành tab đang hoạt động"
+      },
+      "proxy": {
+        "name": "Proxy",
+        "description": "Thiết lập proxy của trình duyệt",
+        "clear": "Xóa tất cả proxy",
+        "bypass": {
+          "label": "Danh sách bỏ qua",
+          "note": "Dùng dấu phẩy (,) để tách biệt URL"
+        }
+      },
+      "new-window": {
+        "name": "Cửa sổ mới",
+        "description": "Tạo cửa sổ mới",
+        "windowState": {
+          "placeholder": "Trạng thái cửa sổ",
+          "options": {
+            "normal": "Bình thường",
+            "minimized": "Thu gọn",
+            "maximized": "Mở rộng tối đa",
+            "fullscreen": "Toàn màn hình"
+          }
+        },
+        "incognito": {
+          "text": "Đặt làm cửa sổ ẩn danh",
+          "note": "Bạn cần bật 'Cho phép ở chế độ ẩn danh' cho tiện ích mở rộng này để sử dụng tùy chọn"
+        }
+      },
+      "go-back": {
+        "name": "Quay lại",
+        "description": "Quay trở lại trang trước"
+      },
+      "forward-page": {
+        "name": "Về trước",
+        "description": "Đi tới trang tiếp theo"
+      },
+      "close-tab": {
+        "name": "Đóng tab",
+        "description": "",
+        "activeTab": "Đóng activeTab",
+        "url": "URL hoặc match pattern"
+      },
+      "event-click": {
+        "name": "Nhấp vào phần tử",
+        "description": ""
+      },
+      "delay": {
+        "name": "Độ trễ",
+        "description": "Thêm độ trễ trước khi thực hiện khối tiếp theo",
+        "input": {
+          "title": "Độ trễ trong mili giây",
+          "placeholder": "(mili giây)"
+        }
+      },
+      "get-text": {
+        "name": "Trích văn bản",
+        "description": "Trích văn bản từ một phần tử",
+        "prefixText": {
+          "placeholder": "Tiền tố văn bản",
+          "title": "Thêm tiền tố vào văn bản"
+        },
+        "suffixText": {
+          "placeholder": "Hậu tố văn bản",
+          "title": "Thêm hậu tố vào văn bản"
+        }
+      },
+      "export-data": {
+        "name": "Xuất dữ liệu",
+        "description": "Xuất dữ liệu của quy trình"
+      },
+      "element-scroll": {
+        "name": "Cuộn",
+        "description": "",
+        "scrollY": "Cuộn thẳng",
+        "scrollX": "Cuộn ngang",
+        "intoView": "Scroll into view",
+        "smooth": "Cuộn mượt",
+        "incScrollX": "Cuộn ngang tăng dần",
+        "incScrollY": "Cuộn dọc tăng dần"
+      },
+      "new-tab": {
+        "name": "Tab mới",
+        "description": "",
+        "activeTab": "Đặt làm tab hoạt động",
+        "tabToGroup": "Thêm tab vào nhóm",
+        "updatePrevTab": {
+          "title": "Sử dụng tab mới đã mở trước đó thay vì tạo tab mới",
+          "text": "Cập nhật tab đã mở trước đó"
+        }
+      },
+      "link": {
+        "name": "Link",
+        "description": "Mở phần tử link"
+      },
+      "attribute-value": {
+        "name": "Giá trị thuộc tính",
+        "description": "Trích xuất giá trị từ một thuộc tính của phần tử",
+        "forms": {
+          "name": "Tên thuộc tính",
+          "checkbox": "Lưu dữ liệu",
+          "column": "Chọn cột"
+        }
+      },
+      "forms": {
+        "name": "Biểu mẫu",
+        "description": "",
+        "selected": "Đã chọn",
+        "type": "Loại biểu mẫu",
+        "getValue": "Trích xuất giá trị từ biểu mẫu",
+        "text-field": {
+          "name": "Trường văn bản",
+          "value": "Giá trị",
+          "clearValue": "Xóa giá trị biểu mẫu",
+          "delay": {
+            "placeholder": "Độ trễ",
+            "label": "Nhập độ trễ (mili giây)(0 là vô hiệu hóa)"
+          }
+        },
+        "select": { "name": "Select" },
+        "radio": { "name": "Radio" },
+        "checkbox": { "name": "Checkbox" }
+      },
+      "repeat-task": {
+        "name": "Nhiệm vụ lặp lại",
+        "description": "",
+        "times": "lần",
+        "repeatFrom": "Lặp lại từ"
+      },
+      "javascript-code": {
+        "name": "JavaScript code",
+        "description": "Thực thi code Javascript trong trang web",
+        "availabeFuncs": "Các hàm có sẵn:",
+        "removeAfterExec": "Xóa sau khi khối được thực thi",
+        "modal": {
+          "tabs": {
+            "code": "JavaScript code",
+            "preloadScript": "Preload script"
+          }
+        },
+        "timeout": {
+          "placeholder": "Thời gian chờ",
+          "title": "Thời gian thực thi code Javascript"
+        }
+      },
+      "trigger-event": {
+        "name": "Sự kiện kích hoạt",
+        "description": "",
+        "selectEvent": "Chọn sự kiện"
+      },
+      "conditions": {
+        "name": "Điều kiện",
+        "description": "Khối có điều kiện",
+        "fallbackTitle": "Thực thi khi tất cả các phép so sánh không đáp ứng yêu cầu",
+        "equals": "Ngang bằng",
+        "gt": "Lớn hơn",
+        "gte": "Lớn hơn hoặc ngang bằng",
+        "lt": "Nhỏ hơn",
+        "lte": "Nhỏ hơn hoặc ngang bằng",
+        "contains": "Bao hàm"
+      },
+      "element-exists": {
+        "name": "Phần tử tồn tại",
+        "description": "Kiểm tra xem một phần tử có tồn tại không",
+        "selector": "Bộ chọn phần tử",
+        "fallbackTitle": "Thực thi khi phần tử không tồn tại",
+        "tryFor": {
+          "title": "Cố gắng kiểm tra xem phần tử có tồn tại không",
+          "label": "Số lần thử"
+        },
+        "timeout": {
+          "label": "Giới hạn thời gian (mili giây)",
+          "title": "Thời gian cho mỗi lần thử"
+        }
+      },
+      "webhook": {
+        "name": "Webhook",
+        "description": "Webhook cho phép dịch vụ bên ngoài được thông báo",
+        "url": "The Post receive URL",
+        "contentType": "Chọn một loại nội dung",
+        "buttons": {
+          "header": "Thêm header"
+        },
+        "timeout": {
+          "placeholder": "Thời gian chờ",
+          "title": "Thời gian chờ thực hiện yêu cầu Http(ms)"
+        },
+        "tabs": {
+          "headers": "Headers",
+          "body": "Nội dung"
+        }
+      },
+      "loop-data": {
+        "name": "Loop data",
+        "description": "Lặp lại qua các cột dữ liệu hoặc dữ liệu tùy chỉnh của bạn",
+        "loopId": "Loop ID",
+        "modal": {
+          "fileTooLarge": "Tệp quá lớn để chỉnh sửa",
+          "maxFile": "Kích thước tệp tối đa là 1MB",
+          "options": {
+            "firstRow": "Use the first row as keys"
+          }
+        },
+        "buttons": {
+          "clear": "Xóa dữ liệu",
+          "insert": "Chèn dữ liệu",
+          "import": "Nhập tệp"
+        },
+        "maxLoop": {
+          "title": "Số lượng dữ liệu tối đa để lặp lại",
+          "label": "Dữ liệu tối đa cho vòng lặp (0 là vô hiệu hóa)"
+        },
+        "loopThrough": {
+          "placeholder": "Lặp lại",
+          "fromNumber": "Từ số",
+          "toNumber": "Đến số",
+          "options": {
+            "numbers": "Số liệu",
+            "data-columns": "Cột dữ liệu",
+            "custom-data": "Dữ liệu tùy chỉnh"
+          }
+        }
+      },
+      "loop-breakpoint": {
+        "name": "Điểm ngắt vòng lặp",
+        "description": "Để cho biết khối dữ liệu vòng lặp phải dừng ở đâu"
+      },
+      "take-screenshot": {
+        "name": "Chụp màn hình",
+        "description": "Chụp màn hình của tab đang hoạt động",
+        "imageQuality": "Chất lượng hình ảnh"
+      },
+      "switch-to": {
+        "name": "Chuyển đổi frame",
+        "description": "Chuyển đổi giữa cửa sổ chính và iframe",
+        "iframeSelector": "Bộ chọn phần tử Iframe",
+        "windowTypes": {
+          "main": "Cửa sổ chính",
+          "iframe": "Iframe"
+        }
+      }
+    }
+  }
+}

+ 56 - 0
src/locales/vi/common.json

@@ -0,0 +1,56 @@
+{
+  "common": {
+    "dashboard": "Bảng điều khiển",
+    "workflow": "Quy trình | Các quy trình",
+    "collection": "Bộ sưu tập | Các bộ sưu tập",
+    "log": "Nhật ký | Nhật ký",
+    "block": "Khối | Khối",
+    "docs": "Tài liệu",
+    "search": "Tìm kiếm",
+    "import": "Nhập",
+    "export": "Xuất",
+    "rename": "Đổi tên",
+    "execute": "Thực thi",
+    "delete": "Xóa",
+    "cancel": "Hủy",
+    "settings": "Cài đặt",
+    "options": "Tùy chọn",
+    "confirm": "Xác nhận",
+    "name": "Tên",
+    "all": "Tất cả",
+    "add": "Thêm",
+    "save": "Lưu",
+    "data": "dữ liệu",
+    "stop": "Dừng lại",
+    "editor": "Trình biên tập",
+    "running": "Đang chạy",
+    "globalData": "Dữ liệu chung",
+    "fileName": "Tên file",
+    "description": "Mô tả",
+    "disable": "Vô hiệu hóa",
+    "disabled": "Đã vô hiệu hóa",
+    "enable": "Kích hoạt",
+    "fallback": "Dự phòng",
+    "update": "Cập nhật"
+  },
+  "message": {
+    "noBlock": "Không có khối",
+    "noData": "Không có dữ liệu để hiển thị",
+    "noTriggerBlock": "Không thể tìm thấy khối kích hoạt",
+    "useDynamicData": "Tìm hiểu cách thêm dữ liệu động",
+    "delete": "Bạn có chắc chắn muốn xóa \"{name}\"?",
+    "empty": "Mục này của bạn có vẻ đang bị trống",
+    "notSaved": "Bạn thực sự muốn thoát? Bạn có một số thay đổi chưa được lưu!",
+    "maxSizeExceeded": "Kích thước tệp vượt quá mức tối đa cho phép",
+  },
+  "sort": {
+    "sortBy": "Sắp xếp theo",
+    "name": "Tên",
+    "createdAt": "Ngày tạo"
+  },
+  "logStatus": {
+    "stopped": "đã dừng lại",
+    "error": "bị lỗi",
+    "success": "thành công"
+  },
+}

+ 135 - 0
src/locales/vi/newtab.json

@@ -0,0 +1,135 @@
+{
+  "home": {
+    "viewAll": "Tất cả"
+  },
+  "settings": {
+    "language": {
+      "label": "Ngôn ngữ",
+      "helpTranslate": "Không tìm thấy ngôn ngữ của bạn? Hãy đóng góp bản dịch với chúng tôi.",
+      "reloadPage": "Tải lại trang để hoàn tất thao tác"
+    }
+  },
+  "workflow": {
+    "import": "Nhập quy trình",
+    "new": "Tạo quy trình mới",
+    "delete": "Xóa quy trình",
+    "name": "Tên quy trình",
+    "rename": "Sửa tên quy trình",
+    "add": "Thêm quy trình",
+    "clickToEnable": "Nhấn để kích hoạt",
+    "state": {
+      "executeBy": "Thực hiện bởi: \"{name}\""
+    },
+    "dataColumns": {
+      "title": "Cơ sở dữ liệu Cột",
+      "placeholder": "Tìm kiếm hoặc thêm cột",
+      "column": {
+        "name": "Tên cột",
+        "type": "Kiểu dữ liệu"
+      }
+    },
+    "sidebar": {
+      "workflowIcon": "Icon"
+    },
+    "editor": {
+      "zoomIn": "Phóng to",
+      "zoomOut": "Thu nhỏ",
+      "resetZoom": "Về mặc định",
+      "duplicate": "Nhân bản"
+    },
+    "settings": {
+      "onError": {
+        "title": "Khi quy trình gặp lỗi",
+        "items": {
+          "keepRunning": "Tiếp tục chạy",
+          "stopWorkflow": "Dừng quy trình"
+        }
+      },
+      "timeout": {
+        "title": "Thời lượng thực thi tối đa (Mili giây)"
+      }
+    }
+  },
+  "collection": {
+    "description": "Thực thi quy trình của bạn theo trình tự",
+    "new": "Bộ sưu tập mới",
+    "delete": "Xóa bộ sưu tập",
+    "add": "Thêm bộ sưu tập",
+    "rename": "Đổi tên bộ sưu tập",
+    "flow": "Trình tự",
+    "dragDropText": "Thả một quy trình hoặc khối vào đây",
+    "options": {
+      "atOnce": {
+        "title": "Thực thi tất cả quy trình trong bộ sưu tập cùng một lúc",
+        "description": "Khối sẽ không được thực thi khi kích hoạt tùy chọn này"
+      }
+    },
+    "globalData": {
+      "note": "Điều này sẽ ghi đè lên dữ liệu chung của quy trình"
+    }
+  },
+  "log": {
+    "goBack": "Trở lại nhật ký \"{name}\"",
+    "startedDate": "Ngày bắt đầu",
+    "duration": "Thời lượng",
+    "selectAll": "Chọn tất cả",
+    "deselectAll": "Bỏ chọn tất cả",
+    "deleteSelected": "Xóa nhật ký đã chọn",
+    "types": {
+      "stop": "Quy trình đã bị dừng",
+      "finish": "Hoàn thành"
+    },
+    "messages": {
+      "workflow-disabled": "Quy trình đã được vô hiệu hóa",
+      "stop-timeout": "Quy trình đã bị dừng vì hết thời lượng thực thi",
+      "invalid-proxy-host": "Máy chủ proxy không hợp lệ",
+      "no-iframe-id": "Không tìm thấy Frame ID cho iframe element với bộ chọn \"{selector}\"",
+      "no-tab": "Không thể kết nối với một tab, dùng \"New tab\" hoặc khối \"Active tab\" trước khi dùng khối \"{name}\".",
+      "empty-workflow": "Đầu tiên, bạn phải chọn một quy trình",
+      "no-workflow": "Không tìm thấy quy trình với ID \"{workflowId}\"",
+      "workflow-infinite-loop": "Quy trình không được thực thi để ngăn vòng lặp vô hạn"
+    },
+    "description": {
+      "text": "{status} vào {date} trong {duration}",
+      "status": {
+        "success": "Thành công",
+        "error": "Thất bại",
+        "stopped": "Đã dừng lại"
+      }
+    },
+    "delete": {
+      "title": "Xóa nhật ký",
+      "description": "Bạn muốn xóa tất cả các nhật ký đã chọn?"
+    },
+    "exportData": {
+      "title": "Xuất dữ liệu",
+      "types": {
+        "json": "JSON",
+        "csv": "CSV",
+        "plain-text": "Văn bản thuần túy"
+      }
+    },
+    "filter": {
+      "title": "Bộ lọc",
+      "byStatus": "Theo trang thái",
+      "byDate": {
+        "title": "Theo ngày",
+        "items": {
+          "lastDay": "Hôm nay",
+          "last7Days": "7 ngày qua",
+          "last30Days": "30 ngày qua"
+        }
+      }
+    }
+  },
+  "components": {
+    "pagination": {
+      "text1": "Hiển thị",
+      "text2": "trong tổng số {count}",
+      "nextPage": "Trang tiếp theo",
+      "currentPage": "Trang hiện tại",
+      "prevPage": "Trang trước",
+      "of": "trong tổng số {page}"
+    }
+  }
+}

+ 13 - 0
src/locales/vi/popup.json

@@ -0,0 +1,13 @@
+{
+  "home": {
+    "elementSelector": {
+      "name": "Bộ chọn phần tử",
+      "noAccess": "Không có quyền truy cập vào trang web này"
+    },
+    "workflow": {
+      "new": "Tạo quy trình mới",
+      "rename": "Đổi tên quy trình",
+      "delete": "Xóa quy trình"
+    },
+  }
+}

+ 1 - 0
src/models/log.js

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

+ 1 - 0
src/models/workflow.js

@@ -24,6 +24,7 @@ class Workflow extends Model {
       createdAt: this.number(),
       createdAt: this.number(),
       isDisabled: this.boolean(false),
       isDisabled: this.boolean(false),
       settings: this.attr({
       settings: this.attr({
+        blockDelay: 0,
         timeout: 120000,
         timeout: 120000,
         onError: 'stop-workflow',
         onError: 'stop-workflow',
       }),
       }),

+ 1 - 18
src/popup/pages/Home.vue

@@ -106,24 +106,7 @@ function deleteWorkflow({ id, name }) {
   });
   });
 }
 }
 function openDashboard(url) {
 function openDashboard(url) {
-  const tabOptions = {
-    active: true,
-    url: browser.runtime.getURL(
-      `/newtab.html#${typeof url === 'string' ? url : ''}`
-    ),
-  };
-
-  browser.tabs
-    .query({ url: browser.runtime.getURL('/newtab.html') })
-    .then(([tab]) => {
-      if (tab) {
-        browser.tabs.update(tab.id, tabOptions).then(() => {
-          browser.tabs.reload(tab.id);
-        });
-      } else {
-        browser.tabs.create(tabOptions);
-      }
-    });
+  sendMessage('open:dashboard', url, 'background');
 }
 }
 async function selectElement() {
 async function selectElement() {
   const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
   const [tab] = await browser.tabs.query({ active: true, currentWindow: true });

+ 1 - 0
src/utils/compare-block-value.js

@@ -1,5 +1,6 @@
 const handlers = {
 const handlers = {
   '==': (a, b) => a === b,
   '==': (a, b) => a === b,
+  '!=': (a, b) => a !== b,
   '>': (a, b) => a > b,
   '>': (a, b) => a > b,
   '>=': (a, b) => a >= b,
   '>=': (a, b) => a >= b,
   '<': (a, b) => a < b,
   '<': (a, b) => a < b,

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

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

+ 9 - 3
src/utils/find-element.js

@@ -4,9 +4,15 @@ class FindElement {
       ? `${data.selector.trim()}:not([${data.blockIdAttr}])`
       ? `${data.selector.trim()}:not([${data.blockIdAttr}])`
       : data.selector;
       : data.selector;
 
 
-    return data.multiple
-      ? document.querySelectorAll(selector)
-      : document.querySelector(selector);
+    if (data.multiple) {
+      const elements = document.querySelectorAll(selector);
+
+      if (elements.length === 0) return null;
+
+      return elements;
+    }
+
+    return document.querySelector(selector);
   }
   }
 
 
   static xpath(data) {
   static xpath(data) {

+ 23 - 32
src/utils/reference-data.js

@@ -2,37 +2,45 @@ import { get, set } from 'object-path-immutable';
 import { isObject, objectHasKey, replaceMustache } from '@/utils/helper';
 import { isObject, objectHasKey, replaceMustache } from '@/utils/helper';
 
 
 const objectPath = { get, set };
 const objectPath = { get, set };
+const refKeys = [
+  { name: 'dataColumn', key: 'dataColumns' },
+  { name: 'dataColumns', key: 'dataColumns' },
+];
 
 
 export function parseKey(key) {
 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('.') ?? [];
   const pathArr = path?.split('.') ?? [];
   let dataPath = '';
   let dataPath = '';
 
 
   if (pathArr.length === 1) {
   if (pathArr.length === 1) {
-    dataPath = `${pathArr[0]}.0`;
+    dataPath = `0.${pathArr[0]}`;
   } else if (typeof +pathArr[0] !== 'number') {
   } else if (typeof +pathArr[0] !== 'number') {
     const firstPath = pathArr.shift();
     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);
   if (dataPath.endsWith('.')) dataPath = dataPath.slice(0, -1);
 
 
   return { dataKey: 'data', path: dataPath };
   return { dataKey: 'data', path: dataPath };
 }
 }
+export function replaceMustacheHandler(match, data) {
+  const key = match.slice(2, -2).replace(/\s/g, '');
+
+  if (!key) return '';
+
+  const { dataKey, path } = parseKey(key);
+  const result = objectPath.get(data[dataKey], path) ?? match;
+
+  return isObject(result) ? JSON.stringify(result) : result;
+}
 
 
 export default function (block, data) {
 export default function (block, data) {
   const replaceKeys = [
   const replaceKeys = [
@@ -52,24 +60,7 @@ export default function (block, data) {
 
 
     const newDataValue = replaceMustache(
     const newDataValue = replaceMustache(
       replacedBlock.data[blockDataKey],
       replacedBlock.data[blockDataKey],
-      (match) => {
-        const key = match.slice(2, -2).replace(/\s/g, '');
-
-        if (!key) return '';
-
-        const { dataKey, path } = parseKey(key);
-
-        if (
-          dataKey === 'prevBlockData' &&
-          (!isObject(data.prevBlockData) || !Array.isArray(data.prevBlockData))
-        ) {
-          return data.prevBlockData;
-        }
-
-        const result = objectPath.get(data[dataKey], path) ?? match;
-
-        return isObject(result) ? JSON.stringify(result) : result;
-      }
+      (match) => replaceMustacheHandler(match, data)
     );
     );
 
 
     replacedBlock = objectPath.set(
     replacedBlock = objectPath.set(

+ 7 - 5
src/utils/shared.js

@@ -408,6 +408,7 @@ export const tasks = {
     description: 'Conditional block',
     description: 'Conditional block',
     icon: 'riAB',
     icon: 'riAB',
     component: 'BlockConditions',
     component: 'BlockConditions',
+    editComponent: 'EditConditions',
     category: 'conditions',
     category: 'conditions',
     inputs: 1,
     inputs: 1,
     outputs: 0,
     outputs: 0,
@@ -536,11 +537,11 @@ export const eventList = [
   { id: 'dblclick', name: 'Double Click', type: 'mouse-event' },
   { id: 'dblclick', name: 'Double Click', type: 'mouse-event' },
   { id: 'mouseup', name: 'Mouseup', type: 'mouse-event' },
   { id: 'mouseup', name: 'Mouseup', type: 'mouse-event' },
   { id: 'mousedown', name: 'Mousedown', type: 'mouse-event' },
   { id: 'mousedown', name: 'Mousedown', type: 'mouse-event' },
-  { id: 'mouseenter ', name: 'Mouseenter', type: 'mouse-event' },
-  { id: 'mouseleave ', name: 'Mouseleave', type: 'mouse-event' },
-  { id: 'mouseover ', name: 'Mouseover', type: 'mouse-event' },
-  { id: 'mouseout ', name: 'Mouseout', type: 'mouse-event' },
-  { id: 'mousemove ', name: 'Mousemove', type: 'mouse-event' },
+  { id: 'mouseenter', name: 'Mouseenter', type: 'mouse-event' },
+  { id: 'mouseleave', name: 'Mouseleave', type: 'mouse-event' },
+  { id: 'mouseover', name: 'Mouseover', type: 'mouse-event' },
+  { id: 'mouseout', name: 'Mouseout', type: 'mouse-event' },
+  { id: 'mousemove', name: 'Mousemove', type: 'mouse-event' },
   { id: 'focus', name: 'Focus', type: 'focus-event' },
   { id: 'focus', name: 'Focus', type: 'focus-event' },
   { id: 'blur', name: 'Blur', type: 'focus-event' },
   { id: 'blur', name: 'Blur', type: 'focus-event' },
   { id: 'input', name: 'Input', type: 'input-event' },
   { id: 'input', name: 'Input', type: 'input-event' },
@@ -586,4 +587,5 @@ export const supportLocales = [
   { id: 'en', name: 'English' },
   { id: 'en', name: 'English' },
   { id: 'zh', name: '简体中文' },
   { id: 'zh', name: '简体中文' },
   { id: 'zh-tw', name: '繁體中文' },
   { id: 'zh-tw', name: '繁體中文' },
+  { id: 'vi', name: 'Tiếng Việt' },
 ];
 ];

+ 1 - 1
src/utils/simulate-event.js

@@ -6,7 +6,7 @@ export function getEventObj(name, params) {
 
 
   switch (eventType) {
   switch (eventType) {
     case 'mouse-event':
     case 'mouse-event':
-      event = new MouseEvent(name, params);
+      event = new MouseEvent(name, { ...params, view: window });
       break;
       break;
     case 'focus-event':
     case 'focus-event':
       event = new FocusEvent(name, params);
       event = new FocusEvent(name, params);

+ 1 - 1
utils/env.js

@@ -1,5 +1,5 @@
 // tiny wrapper with default env vars
 // tiny wrapper with default env vars
 module.exports = {
 module.exports = {
   NODE_ENV: process.env.NODE_ENV || 'development',
   NODE_ENV: process.env.NODE_ENV || 'development',
-  PORT: process.env.PORT || 3000,
+  PORT: process.env.PORT || 3001,
 };
 };

+ 58 - 65
yarn.lock

@@ -1309,11 +1309,6 @@
     "@webassemblyjs/ast" "1.11.1"
     "@webassemblyjs/ast" "1.11.1"
     "@xtuc/long" "4.2.2"
     "@xtuc/long" "4.2.2"
 
 
-"@webcomponents/custom-elements@^1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.5.0.tgz#7d07ff4979312dda167cc0a2b7586e76dc1cf6ab"
-  integrity sha512-c+7jPQCs9h/BYVcZ2Kna/3tsl3A/9EyXfvWjp5RiTDm1OpTcbZaCa1z4RNcTe/hUtXaqn64JjNW1yrWT+rZ8gg==
-
 "@webpack-cli/configtest@^1.0.4":
 "@webpack-cli/configtest@^1.0.4":
   version "1.0.4"
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.4.tgz#f03ce6311c0883a83d04569e2c03c6238316d2aa"
   resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.4.tgz#f03ce6311c0883a83d04569e2c03c6238316d2aa"
@@ -1891,11 +1886,16 @@ bytes@3.0.0:
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
   integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
   integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
 
 
-bytes@3.1.0, bytes@^3.0.0:
+bytes@3.1.0:
   version "3.1.0"
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
 
 
+bytes@^3.0.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a"
+  integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==
+
 cache-base@^1.0.1:
 cache-base@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
   resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
@@ -2107,21 +2107,21 @@ color-name@^1.0.0, color-name@~1.1.4:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
 
-color-string@^1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312"
-  integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==
+color-string@^1.9.0:
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa"
+  integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==
   dependencies:
   dependencies:
     color-name "^1.0.0"
     color-name "^1.0.0"
     simple-swizzle "^0.2.2"
     simple-swizzle "^0.2.2"
 
 
 color@^4.0.1:
 color@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/color/-/color-4.0.1.tgz#21df44cd10245a91b1ccf5ba031609b0e10e7d67"
-  integrity sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/color/-/color-4.1.0.tgz#9502e6a2dcacb26adf4c60910a27628d010b3de3"
+  integrity sha512-o2rkkxyLGgYoeUy1OodXpbPAQNmlNBrirQ8ODO8QutzDiDMNdezSOZLNnusQ6pUpCQJUsaJIo9DZJKqa2HgH7A==
   dependencies:
   dependencies:
     color-convert "^2.0.1"
     color-convert "^2.0.1"
-    color-string "^1.6.0"
+    color-string "^1.9.0"
 
 
 colorette@^1.2.1, colorette@^1.2.2, colorette@^1.3.0, colorette@^1.4.0:
 colorette@^1.2.1, colorette@^1.2.2, colorette@^1.3.0, colorette@^1.4.0:
   version "1.4.0"
   version "1.4.0"
@@ -2138,16 +2138,16 @@ commander@^4.1.1:
   resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
   resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
   integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
   integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
 
 
-commander@^6.0.0:
-  version "6.2.1"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
-  integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
-
 commander@^7.0.0, commander@^7.2.0:
 commander@^7.0.0, commander@^7.2.0:
   version "7.2.0"
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
   resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
   integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
   integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
 
 
+commander@^8.0.0:
+  version "8.3.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
+  integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
+
 commondir@^1.0.1:
 commondir@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -3406,26 +3406,19 @@ glob-parent@^5.1.2, glob-parent@~5.1.2:
   dependencies:
   dependencies:
     is-glob "^4.0.1"
     is-glob "^4.0.1"
 
 
-glob-parent@^6.0.0:
+glob-parent@^6.0.0, glob-parent@^6.0.1:
   version "6.0.2"
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
   integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
   integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
   dependencies:
   dependencies:
     is-glob "^4.0.3"
     is-glob "^4.0.3"
 
 
-glob-parent@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.1.tgz#42054f685eb6a44e7a7d189a96efa40a54971aa7"
-  integrity sha512-kEVjS71mQazDBHKcsq4E9u/vUzaLcw1A8EtUeydawvIWQCJM0qQ08G1H7/XTjFUulla6XQiDOG6MXSaG0HDKog==
-  dependencies:
-    is-glob "^4.0.1"
-
 glob-to-regexp@^0.4.1:
 glob-to-regexp@^0.4.1:
   version "0.4.1"
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
   integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
   integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
 
 
-glob@^7.0.0, glob@^7.0.3, glob@^7.1.3:
+glob@^7.0.3, glob@^7.1.3:
   version "7.1.7"
   version "7.1.7"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
   integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
   integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
@@ -3437,7 +3430,7 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.3:
     once "^1.3.0"
     once "^1.3.0"
     path-is-absolute "^1.0.0"
     path-is-absolute "^1.0.0"
 
 
-glob@^7.1.4:
+glob@^7.1.4, glob@^7.1.7:
   version "7.2.0"
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
   integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
   integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
@@ -4677,11 +4670,6 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
 
-mitt@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230"
-  integrity sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==
-
 mixin-deep@^1.2.0:
 mixin-deep@^1.2.0:
   version "1.3.2"
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
   resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
@@ -4765,6 +4753,11 @@ nanoid@^3.1.23:
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152"
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152"
   integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==
   integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==
 
 
+nanoid@^3.1.30:
+  version "3.1.30"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
+  integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
+
 nanomatch@^1.2.9:
 nanomatch@^1.2.9:
   version "1.2.13"
   version "1.2.13"
   resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
   resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -5228,6 +5221,11 @@ path-type@^4.0.0:
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
   integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
   integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
 
 
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
 picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
 picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
   version "2.3.0"
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
@@ -5397,7 +5395,7 @@ postcss@8.3.8:
     nanoid "^3.1.25"
     nanoid "^3.1.25"
     source-map-js "^0.6.2"
     source-map-js "^0.6.2"
 
 
-postcss@^8.1.10, postcss@^8.1.6, postcss@^8.2.1, postcss@^8.2.15:
+postcss@^8.1.10, postcss@^8.1.6, postcss@^8.2.15:
   version "8.3.6"
   version "8.3.6"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea"
   integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==
   integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==
@@ -5406,6 +5404,15 @@ postcss@^8.1.10, postcss@^8.1.6, postcss@^8.2.1, postcss@^8.2.15:
     nanoid "^3.1.23"
     nanoid "^3.1.23"
     source-map-js "^0.6.2"
     source-map-js "^0.6.2"
 
 
+postcss@^8.3.5:
+  version "8.4.4"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.4.tgz#d53d4ec6a75fd62557a66bb41978bf47ff0c2869"
+  integrity sha512-joU6fBsN6EIer28Lj6GDFoC/5yOZzLCfn0zHAn/MYXI7aPt4m4hK5KC5ovEZXy+lnCjmYIbQWngvju2ddyEr8Q==
+  dependencies:
+    nanoid "^3.1.30"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.1"
+
 prelude-ls@^1.2.1:
 prelude-ls@^1.2.1:
   version "1.2.1"
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -5488,14 +5495,14 @@ punycode@^2.1.0:
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
 
 purgecss@^4.0.3:
 purgecss@^4.0.3:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-4.0.3.tgz#8147b429f9c09db719e05d64908ea8b672913742"
-  integrity sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-4.1.3.tgz#683f6a133c8c4de7aa82fe2746d1393b214918f7"
+  integrity sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==
   dependencies:
   dependencies:
-    commander "^6.0.0"
-    glob "^7.0.0"
-    postcss "^8.2.1"
-    postcss-selector-parser "^6.0.2"
+    commander "^8.0.0"
+    glob "^7.1.7"
+    postcss "^8.3.5"
+    postcss-selector-parser "^6.0.6"
 
 
 qs@6.7.0:
 qs@6.7.0:
   version "6.7.0"
   version "6.7.0"
@@ -6152,6 +6159,11 @@ source-map-js@^0.6.2:
   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
   integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
   integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
 
 
+source-map-js@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf"
+  integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
+
 source-map-loader@3.0.0:
 source-map-loader@3.0.0:
   version "3.0.0"
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.0.tgz#f2a04ee2808ad01c774dea6b7d2639839f3b3049"
   resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.0.tgz#f2a04ee2808ad01c774dea6b7d2639839f3b3049"
@@ -6439,10 +6451,10 @@ table@^6.0.9:
     string-width "^4.2.0"
     string-width "^4.2.0"
     strip-ansi "^6.0.0"
     strip-ansi "^6.0.0"
 
 
-tailwindcss@2.2.16:
-  version "2.2.16"
-  resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-2.2.16.tgz#32f81bdf1758b639cb83b9d30bf7cbecdda49e5e"
-  integrity sha512-EireCtpQyyJ4Xz8NYzHafBoy4baCOO96flM0+HgtsFcIQ9KFy/YBK3GEtlnD+rXen0e4xm8t3WiUcKBJmN6yjg==
+tailwindcss@2.2.19:
+  version "2.2.19"
+  resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-2.2.19.tgz#540e464832cd462bb9649c1484b0a38315c2653c"
+  integrity sha512-6Ui7JSVtXadtTUo2NtkBBacobzWiQYVjYW0ZnKaP9S1ZCKQ0w7KVNz+YSDI/j7O7KCMHbOkz94ZMQhbT9pOqjw==
   dependencies:
   dependencies:
     arg "^5.0.1"
     arg "^5.0.1"
     bytes "^3.0.0"
     bytes "^3.0.0"
@@ -6823,21 +6835,11 @@ vue-loader@16.8.1:
     hash-sum "^2.0.0"
     hash-sum "^2.0.0"
     loader-utils "^2.0.0"
     loader-utils "^2.0.0"
 
 
-vue-observe-visibility@^2.0.0-alpha.1:
-  version "2.0.0-alpha.1"
-  resolved "https://registry.yarnpkg.com/vue-observe-visibility/-/vue-observe-visibility-2.0.0-alpha.1.tgz#1e4eda7b12562161d58984b7e0dea676d83bdb13"
-  integrity sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==
-
 vue-prism-editor@^2.0.0-alpha.2:
 vue-prism-editor@^2.0.0-alpha.2:
   version "2.0.0-alpha.2"
   version "2.0.0-alpha.2"
   resolved "https://registry.yarnpkg.com/vue-prism-editor/-/vue-prism-editor-2.0.0-alpha.2.tgz#aa53a88efaaed628027cbb282c2b1d37fc7c5c69"
   resolved "https://registry.yarnpkg.com/vue-prism-editor/-/vue-prism-editor-2.0.0-alpha.2.tgz#aa53a88efaaed628027cbb282c2b1d37fc7c5c69"
   integrity sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==
   integrity sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==
 
 
-vue-resize@^2.0.0-alpha.1:
-  version "2.0.0-alpha.1"
-  resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz#43eeb79e74febe932b9b20c5c57e0ebc14e2df3a"
-  integrity sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==
-
 vue-router@^4.0.11:
 vue-router@^4.0.11:
   version "4.0.11"
   version "4.0.11"
   resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.11.tgz#cd649a0941c635281763a20965b599643ddc68ed"
   resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.11.tgz#cd649a0941c635281763a20965b599643ddc68ed"
@@ -6845,15 +6847,6 @@ vue-router@^4.0.11:
   dependencies:
   dependencies:
     "@vue/devtools-api" "^6.0.0-beta.14"
     "@vue/devtools-api" "^6.0.0-beta.14"
 
 
-vue-virtual-scroller@^2.0.0-alpha.1:
-  version "2.0.0-alpha.1"
-  resolved "https://registry.yarnpkg.com/vue-virtual-scroller/-/vue-virtual-scroller-2.0.0-alpha.1.tgz#5b5410105b8e60ca57bbd5f2faf5ad1d8108d046"
-  integrity sha512-Mn5w3Qe06t7c3Imm2RHD43RACab1CCWplpdgzq+/FWJcpQtcGKd5vDep8i+nIwFtzFLsWAqEK0RzM7KrfAcBng==
-  dependencies:
-    mitt "^2.1.0"
-    vue-observe-visibility "^2.0.0-alpha.1"
-    vue-resize "^2.0.0-alpha.1"
-
 vue@3.2.19:
 vue@3.2.19:
   version "3.2.19"
   version "3.2.19"
   resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.19.tgz#da2c80a6a0271c7097fee9e31692adfd9d569c8f"
   resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.19.tgz#da2c80a6a0271c7097fee9e31692adfd9d569c8f"