Ahmad Kholid 3 yıl önce
ebeveyn
işleme
ad3dff746f
68 değiştirilmiş dosya ile 1294 ekleme ve 335 silme
  1. 2 1
      .gitignore
  2. 5 3
      package.json
  3. 0 0
      secrets.blank.js
  4. 0 13
      src/assets/css/prism-editor.css
  5. 13 0
      src/assets/css/tailwind.css
  6. 19 1
      src/background/index.js
  7. 1 1
      src/background/workflow-engine/blocks-handler/handler-active-tab.js
  8. 46 0
      src/background/workflow-engine/blocks-handler/handler-blocks-group.js
  9. 19 1
      src/background/workflow-engine/blocks-handler/handler-interaction-block.js
  10. 1 1
      src/background/workflow-engine/blocks-handler/handler-new-tab.js
  11. 22 6
      src/background/workflow-engine/blocks-handler/handler-switch-to.js
  12. 11 3
      src/background/workflow-engine/blocks-handler/handler-take-screenshot.js
  13. 1 1
      src/background/workflow-engine/blocks-handler/handler-trigger.js
  14. 15 27
      src/background/workflow-engine/blocks-handler/handler-webhook.js
  15. 2 2
      src/background/workflow-engine/engine.js
  16. 25 34
      src/background/workflow-engine/execute-content-script.js
  17. 1 1
      src/components/block/BlockConditions.vue
  18. 175 0
      src/components/block/BlockGroup.vue
  19. 5 6
      src/components/newtab/logs/LogsDataViewer.vue
  20. 3 3
      src/components/newtab/shared/SharedCard.vue
  21. 101 0
      src/components/newtab/shared/SharedCodemirror.vue
  22. 7 1
      src/components/newtab/workflow/WorkflowBuilder.vue
  23. 3 5
      src/components/newtab/workflow/WorkflowGlobalData.vue
  24. 40 0
      src/components/newtab/workflow/edit/EditAttributeValue.vue
  25. 19 0
      src/components/newtab/workflow/edit/EditElementExists.vue
  26. 8 18
      src/components/newtab/workflow/edit/EditExecuteWorkflow.vue
  27. 39 1
      src/components/newtab/workflow/edit/EditGetText.vue
  28. 6 11
      src/components/newtab/workflow/edit/EditJavascriptCode.vue
  29. 16 29
      src/components/newtab/workflow/edit/EditLoopData.vue
  30. 76 35
      src/components/newtab/workflow/edit/EditTakeScreenshot.vue
  31. 19 23
      src/components/newtab/workflow/edit/EditWebhook.vue
  32. 9 2
      src/components/newtab/workflow/edit/TriggerEventKeyboard.vue
  33. 1 1
      src/components/popup/home/HomeWorkflowCard.vue
  34. 1 0
      src/components/ui/UiInput.vue
  35. 1 1
      src/components/ui/UiPagination.vue
  36. 3 0
      src/composable/editorBlock.js
  37. 17 6
      src/content/blocks-handler/handler-element-exists.js
  38. 21 6
      src/content/blocks-handler/handler-javascript-code.js
  39. 4 5
      src/content/blocks-handler/handler-switch-to.js
  40. 5 5
      src/content/element-selector/AppBlocks.vue
  41. 2 2
      src/content/element-selector/AppSelector.vue
  42. 16 2
      src/content/helper.js
  43. 6 0
      src/content/index.js
  44. 43 9
      src/content/shortcut.js
  45. 0 13
      src/lib/prism.js
  46. 4 0
      src/lib/v-remixicon.js
  47. 31 6
      src/locales/en/blocks.json
  48. 6 0
      src/locales/en/newtab.json
  49. 13 2
      src/locales/fr/blocks.json
  50. 1 1
      src/locales/vi/blocks.json
  51. 1 1
      src/locales/zh-TW/blocks.json
  52. 1 1
      src/locales/zh/blocks.json
  53. 1 0
      src/manifest.json
  54. 1 1
      src/models/workflow.js
  55. 3 0
      src/newtab/App.vue
  56. 35 0
      src/newtab/pages/Workflows.vue
  57. 5 6
      src/newtab/pages/collections/[id].vue
  58. 23 5
      src/newtab/pages/workflows/[id].vue
  59. 1 1
      src/popup/App.vue
  60. 6 6
      src/utils/find-element.js
  61. 14 8
      src/utils/handle-form-element.js
  62. 1 0
      src/utils/reference-data.js
  63. 31 5
      src/utils/shared.js
  64. 4 2
      src/utils/webhookUtil.js
  65. 3 1
      src/utils/workflow-data.js
  66. 1 0
      utils/build-zip.js
  67. 1 0
      webpack.config.js
  68. 278 10
      yarn.lock

+ 2 - 1
.gitignore

@@ -21,6 +21,7 @@
 *.log
 *.log
 
 
 # secrets
 # secrets
-secrets.*.js
+secrets.production.js
+secrets.development.js
 
 
 .idea
 .idea

+ 5 - 3
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "automa",
   "name": "automa",
-  "version": "0.9.7",
+  "version": "0.10.0",
   "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": {
@@ -21,6 +21,10 @@
     "*.{js,ts,vue}": "eslint --fix"
     "*.{js,ts,vue}": "eslint --fix"
   },
   },
   "dependencies": {
   "dependencies": {
+    "@codemirror/basic-setup": "^0.19.1",
+    "@codemirror/lang-javascript": "^0.19.3",
+    "@codemirror/lang-json": "^0.19.1",
+    "@codemirror/theme-one-dark": "^0.19.1",
     "@medv/finder": "^2.1.0",
     "@medv/finder": "^2.1.0",
     "@vuex-orm/core": "^0.36.4",
     "@vuex-orm/core": "^0.36.4",
     "dayjs": "^1.10.7",
     "dayjs": "^1.10.7",
@@ -30,13 +34,11 @@
     "nanoid": "3.1.28",
     "nanoid": "3.1.28",
     "object-path-immutable": "^4.1.2",
     "object-path-immutable": "^4.1.2",
     "papaparse": "^5.3.1",
     "papaparse": "^5.3.1",
-    "prismjs": "^1.25.0",
     "tiny-emitter": "^2.1.0",
     "tiny-emitter": "^2.1.0",
     "tippy.js": "^6.3.1",
     "tippy.js": "^6.3.1",
     "v-remixicon": "^0.1.1",
     "v-remixicon": "^0.1.1",
     "vue": "3.2.19",
     "vue": "3.2.19",
     "vue-i18n": "^9.2.0-beta.20",
     "vue-i18n": "^9.2.0-beta.20",
-    "vue-prism-editor": "^2.0.0-alpha.2",
     "vue-router": "^4.0.11",
     "vue-router": "^4.0.11",
     "vuedraggable": "^4.1.0",
     "vuedraggable": "^4.1.0",
     "vuex": "^4.0.2",
     "vuex": "^4.0.2",

+ 0 - 0
secrets.blank.js


+ 0 - 13
src/assets/css/prism-editor.css

@@ -1,13 +0,0 @@
-.my-editor,
-.prism-editor-wrapper {
-  color: #ccc;
-  font-family: JetBrains Mono, Fira code, Fira Mono, Consolas, Menlo, Courier,
-    monospace;
-  font-size: 14px;
-  line-height: 1.5;
-  padding: 5px;
-  @apply bg-gray-900 rounded-lg;
-}
-.prism-editor__textarea:focus {
-  outline: none;
-}

+ 13 - 0
src/assets/css/tailwind.css

@@ -33,6 +33,9 @@ select:focus,
   overflow: hidden;
   overflow: hidden;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
 }
 }
+pre {
+  font-size: 15px;
+}
 .scroll::-webkit-scrollbar {
 .scroll::-webkit-scrollbar {
   width: 7px;
   width: 7px;
   height: 9px;
   height: 9px;
@@ -47,6 +50,16 @@ select:focus,
 .tippy-box[data-theme~='tooltip-theme'] {
 .tippy-box[data-theme~='tooltip-theme'] {
   @apply px-2 py-1 bg-gray-900 text-sm text-gray-200 rounded-md;
   @apply px-2 py-1 bg-gray-900 text-sm text-gray-200 rounded-md;
 }
 }
+.ProseMirror > * + * {
+  margin-top: 0.75em;
+}
+.ProseMirror img {
+  max-width: 100%;
+  height: auto;
+}
+.ProseMirror img.ProseMirror-selectednode {
+  outline: 3px solid #68CEF8;
+}
 
 
 @layer utilities {
 @layer utilities {
   .hoverable {
   .hoverable {

+ 19 - 1
src/background/index.js

@@ -1,9 +1,10 @@
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
+import { objectHasKey } from '@/utils/helper';
 import { MessageListener } from '@/utils/message';
 import { MessageListener } from '@/utils/message';
+import { registerSpecificDay } from '../utils/workflow-trigger';
 import workflowState from './workflow-state';
 import workflowState from './workflow-state';
 import workflowEngine from './workflow-engine';
 import workflowEngine from './workflow-engine';
 import CollectionEngine from './collection-engine';
 import CollectionEngine from './collection-engine';
-import { registerSpecificDay } from '../utils/workflow-trigger';
 
 
 function getWorkflow(workflowId) {
 function getWorkflow(workflowId) {
   return new Promise((resolve) => {
   return new Promise((resolve) => {
@@ -47,6 +48,22 @@ function executeCollection(collection) {
 
 
   return true;
   return true;
 }
 }
+async function checkRunnigWorkflows() {
+  try {
+    const result = await browser.storage.local.get('workflowState');
+
+    result.workflowState.forEach(({ id }, index) => {
+      if (objectHasKey(runningWorkflows, id)) return;
+
+      result.workflowState.splice(index, 1);
+    });
+
+    browser.storage.local.set({ workflowState: result.workflowState });
+  } catch (error) {
+    console.error(error);
+  }
+}
+checkRunnigWorkflows();
 
 
 browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
 browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
   if (changeInfo.status === 'complete') {
   if (changeInfo.status === 'complete') {
@@ -151,6 +168,7 @@ message.on('collection:stop', (id) => {
   collection.stop();
   collection.stop();
 });
 });
 
 
+message.on('workflow:check-state', checkRunnigWorkflows);
 message.on('workflow:execute', (workflow) => executeWorkflow(workflow));
 message.on('workflow:execute', (workflow) => executeWorkflow(workflow));
 message.on('workflow:stop', (id) => {
 message.on('workflow:stop', (id) => {
   const workflow = runningWorkflows[id];
   const workflow = runningWorkflows[id];

+ 1 - 1
src/background/workflow-engine/blocks-handler/handler-active-tab.js

@@ -22,7 +22,7 @@ async function activeTab(block) {
       currentWindow: true,
       currentWindow: true,
     });
     });
 
 
-    this.frames = await executeContentScript(tab.id, 'activetab');
+    this.frames = await executeContentScript(tab.id);
 
 
     this.frameId = 0;
     this.frameId = 0;
     this.tabId = tab.id;
     this.tabId = tab.id;

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

@@ -0,0 +1,46 @@
+import { getBlockConnection } from '../helper';
+
+function blocksGroup({ data, outputs }, { prevBlockData }) {
+  return new Promise((resolve) => {
+    const nextBlockId = getBlockConnection({ outputs });
+
+    if (data.blocks.length === 0) {
+      resolve({
+        nextBlockId,
+        data: prevBlockData,
+      });
+
+      return;
+    }
+
+    const blocks = data.blocks.reduce((acc, block, index) => {
+      let nextBlock = data.blocks[index + 1]?.itemId;
+
+      if (index === data.blocks.length - 1) {
+        nextBlock = nextBlockId;
+      }
+
+      acc[block.itemId] = {
+        ...block,
+        id: block.itemId,
+        name: block.id,
+        outputs: {
+          output_1: {
+            connections: [{ node: nextBlock }],
+          },
+        },
+      };
+
+      return acc;
+    }, {});
+
+    Object.assign(this.blocks, blocks);
+
+    resolve({
+      data: prevBlockData,
+      nextBlockId: data.blocks[0].itemId,
+    });
+  });
+}
+
+export default blocksGroup;

+ 19 - 1
src/background/workflow-engine/blocks-handler/handler-interaction-block.js

@@ -3,7 +3,11 @@ import { getBlockConnection } from '../helper';
 
 
 async function interactionHandler(block, { refData }) {
 async function interactionHandler(block, { refData }) {
   const nextBlockId = getBlockConnection(block);
   const nextBlockId = getBlockConnection(block);
-  const messagePayload = { ...block, refData };
+  const messagePayload = {
+    ...block,
+    refData,
+    frameSelector: this.frameSelector,
+  };
 
 
   try {
   try {
     const data = await this._sendMessageToTab(messagePayload, {
     const data = await this._sendMessageToTab(messagePayload, {
@@ -26,9 +30,23 @@ async function interactionHandler(block, { refData }) {
       if (Array.isArray(data) && currentColumnType !== 'array') {
       if (Array.isArray(data) && currentColumnType !== 'array') {
         data.forEach((item) => {
         data.forEach((item) => {
           this.addData(block.data.dataColumn, item);
           this.addData(block.data.dataColumn, item);
+          if (objectHasKey(block.data, 'extraRowDataColumn')) {
+            if (block.data.addExtraRow)
+              this.addData(
+                block.data.extraRowDataColumn,
+                block.data.extraRowValue
+              );
+          }
         });
         });
       } else {
       } else {
         this.addData(block.data.dataColumn, data);
         this.addData(block.data.dataColumn, data);
+        if (objectHasKey(block.data, 'extraRowDataColumn')) {
+          if (block.data.addExtraRow)
+            this.addData(
+              block.data.extraRowDataColumn,
+              block.data.extraRowValue
+            );
+        }
       }
       }
     } else if (block.name === 'javascript-code') {
     } else if (block.name === 'javascript-code') {
       const arrData = Array.isArray(data) ? data : [data];
       const arrData = Array.isArray(data) ? data : [data];

+ 1 - 1
src/background/workflow-engine/blocks-handler/handler-new-tab.js

@@ -10,7 +10,7 @@ function tabUpdatedListener(tab) {
       callback: async (tabId, changeInfo, deleteListener) => {
       callback: async (tabId, changeInfo, deleteListener) => {
         if (changeInfo.status !== 'complete') return;
         if (changeInfo.status !== 'complete') return;
 
 
-        const frames = await executeContentScript(tabId, 'newtab');
+        const frames = await executeContentScript(tabId);
 
 
         deleteListener();
         deleteListener();
 
 

+ 22 - 6
src/background/workflow-engine/blocks-handler/handler-switch-to.js

@@ -1,5 +1,6 @@
 import { objectHasKey } from '@/utils/helper';
 import { objectHasKey } from '@/utils/helper';
 import { getBlockConnection } from '../helper';
 import { getBlockConnection } from '../helper';
+import executeContentScript, { getFrames } from '../execute-content-script';
 
 
 async function switchTo(block) {
 async function switchTo(block) {
   const nextBlockId = getBlockConnection(block);
   const nextBlockId = getBlockConnection(block);
@@ -8,28 +9,43 @@ async function switchTo(block) {
     if (block.data.windowType === 'main-window') {
     if (block.data.windowType === 'main-window') {
       this.frameId = 0;
       this.frameId = 0;
 
 
+      delete this.frameSelector;
+
       return {
       return {
         data: '',
         data: '',
         nextBlockId,
         nextBlockId,
       };
       };
     }
     }
 
 
-    const { url } = await this._sendMessageToTab(block, { frameId: 0 });
+    const frames = await getFrames(this.tabId);
+    const { url, isSameOrigin } = await this._sendMessageToTab(block, {
+      frameId: 0,
+    });
+
+    if (isSameOrigin) {
+      this.frameSelector = block.data.selector;
+
+      return {
+        data: block.data.selector,
+        nextBlockId,
+      };
+    }
 
 
-    if (objectHasKey(this.frames, url)) {
+    if (objectHasKey(frames, url)) {
       this.frameId = this.frames[url];
       this.frameId = this.frames[url];
 
 
+      await executeContentScript(this.tabId, this.frameId);
+      await new Promise((resolve) => setTimeout(resolve, 1000));
+
       return {
       return {
         data: this.frameId,
         data: this.frameId,
         nextBlockId,
         nextBlockId,
       };
       };
     }
     }
 
 
-    const error = new Error('no-iframe-id');
-    error.data = { selector: block.selector };
-
-    throw error;
+    throw new Error('no-iframe-id');
   } catch (error) {
   } catch (error) {
+    error.data = { selector: block.data.selector };
     error.nextBlockId = nextBlockId;
     error.nextBlockId = nextBlockId;
 
 
     throw error;
     throw error;

+ 11 - 3
src/background/workflow-engine/blocks-handler/handler-take-screenshot.js

@@ -22,7 +22,13 @@ function saveImage({ fileName, uri, ext }) {
 
 
 async function takeScreenshot(block) {
 async function takeScreenshot(block) {
   const nextBlockId = getBlockConnection(block);
   const nextBlockId = getBlockConnection(block);
-  const { ext, quality, captureActiveTab, fileName } = block.data;
+  const { ext, quality, captureActiveTab, fileName, saveToColumn, dataColumn } =
+    block.data;
+
+  const saveToComputer =
+    typeof block.data.saveToComputer === 'undefined'
+      ? true
+      : block.data.saveToComputer;
 
 
   try {
   try {
     const options = {
     const options = {
@@ -52,11 +58,13 @@ async function takeScreenshot(block) {
         await browser.tabs.update(tab.id, { active: true });
         await browser.tabs.update(tab.id, { active: true });
       }
       }
 
 
-      saveImage({ fileName, uri, ext });
+      if (saveToColumn) this.addData(dataColumn, uri);
+      if (saveToComputer) saveImage({ fileName, uri, ext });
     } else {
     } else {
       const uri = await browser.tabs.captureVisibleTab(options);
       const uri = await browser.tabs.captureVisibleTab(options);
 
 
-      saveImage({ fileName, uri, ext });
+      if (saveToColumn) this.addData(dataColumn, uri);
+      if (saveToComputer) saveImage({ fileName, uri, ext });
     }
     }
 
 
     return { data: '', nextBlockId };
     return { data: '', nextBlockId };

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

@@ -6,7 +6,7 @@ async function trigger(block) {
 
 
   try {
   try {
     if (block.data.type === 'visit-web' && this.tabId) {
     if (block.data.type === 'visit-web' && this.tabId) {
-      this.frames = await executeContentScript(this.tabId, 'trigger');
+      this.frames = await executeContentScript(this.tabId);
     }
     }
 
 
     return { nextBlockId, data: '' };
     return { nextBlockId, data: '' };

+ 15 - 27
src/background/workflow-engine/blocks-handler/handler-webhook.js

@@ -1,37 +1,25 @@
 import { getBlockConnection } from '../helper';
 import { getBlockConnection } from '../helper';
+import { isWhitespace } from '@/utils/helper';
 import { executeWebhook } from '@/utils/webhookUtil';
 import { executeWebhook } from '@/utils/webhookUtil';
 
 
-function webhook({ data, outputs }) {
-  return new Promise((resolve, reject) => {
-    const nextBlockId = getBlockConnection({ outputs });
+export async function webhook({ data, outputs }) {
+  const nextBlockId = getBlockConnection({ outputs });
 
 
-    if (!data.url) {
-      const error = new Error('URL is empty');
-      error.nextBlockId = nextBlockId;
+  try {
+    if (isWhitespace(data.url)) throw new Error('url-empty');
+    if (!data.url.startsWith('http')) throw new Error('invalid-url');
 
 
-      reject(error);
-      return;
-    }
+    await executeWebhook(data);
 
 
-    if (!data.url.startsWith('http')) {
-      const error = new Error('URL is not valid');
-      error.nextBlockId = nextBlockId;
+    return {
+      data: '',
+      nextBlockId,
+    };
+  } catch (error) {
+    error.nextBlockId = nextBlockId;
 
 
-      reject(error);
-      return;
-    }
-
-    executeWebhook(data)
-      .then(() => {
-        resolve({
-          data: '',
-          nextBlockId: getBlockConnection({ outputs }),
-        });
-      })
-      .catch((error) => {
-        reject(error);
-      });
-  });
+    throw error;
+  }
 }
 }
 
 
 export default webhook;
 export default webhook;

+ 2 - 2
src/background/workflow-engine/engine.js

@@ -183,7 +183,7 @@ class WorkflowEngine {
   }
   }
 
 
   addLog(detail) {
   addLog(detail) {
-    if (this.logs.length >= 1001) return;
+    if (this.logs.length >= 1001 || detail.name === 'blocks-group') return;
 
 
     this.logs.push(detail);
     this.logs.push(detail);
   }
   }
@@ -312,7 +312,7 @@ class WorkflowEngine {
       return;
       return;
     }
     }
 
 
-    const disableTimeoutKeys = ['delay', 'javascript-code'];
+    const disableTimeoutKeys = ['delay', 'javascript-code', 'webhook'];
 
 
     if (!disableTimeoutKeys.includes(block.name)) {
     if (!disableTimeoutKeys.includes(block.name)) {
       this.workflowTimeout = setTimeout(() => {
       this.workflowTimeout = setTimeout(() => {

+ 25 - 34
src/background/workflow-engine/execute-content-script.js

@@ -1,37 +1,30 @@
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
 
 
-function getFrames(tabId) {
-  return new Promise((resolve) => {
-    const frames = {};
-    let frameTimeout;
-    let timeout;
-
-    const onMessageListener = (_, sender) => {
-      if (sender.frameId !== 0) frames[sender.url] = sender.frameId;
-
-      clearTimeout(frameTimeout);
-      frameTimeout = setTimeout(() => {
-        clearTimeout(timeout);
-        browser.runtime.onMessage.removeListener(onMessageListener);
-        resolve(frames);
-      }, 250);
-    };
-
-    browser.tabs.sendMessage(tabId, {
-      type: 'give-me-the-frame-id',
-    });
-    browser.runtime.onMessage.addListener(onMessageListener);
-
-    timeout = setTimeout(() => {
-      clearTimeout(frameTimeout);
-      resolve(frames);
-    }, 5000);
-  });
+export async function getFrames(tabId) {
+  try {
+    const frames = await browser.webNavigation.getAllFrames({ tabId });
+    const framesObj = frames.reduce((acc, { frameId, url }) => {
+      const key = url === 'about:blank' ? '' : url;
+
+      acc[key] = frameId;
+
+      return acc;
+    }, {});
+
+    return framesObj;
+  } catch (error) {
+    console.error(error);
+    return {};
+  }
 }
 }
 
 
-async function contentScriptExist(tabId) {
+async function contentScriptExist(tabId, frameId = 0) {
   try {
   try {
-    await browser.tabs.sendMessage(tabId, { type: 'content-script-exists' });
+    await browser.tabs.sendMessage(
+      tabId,
+      { type: 'content-script-exists' },
+      { frameId }
+    );
 
 
     return true;
     return true;
   } catch (error) {
   } catch (error) {
@@ -39,19 +32,17 @@ async function contentScriptExist(tabId) {
   }
   }
 }
 }
 
 
-export default async function (tabId) {
+export default async function (tabId, frameId = 0) {
   try {
   try {
-    const isScriptExists = await contentScriptExist(tabId);
+    const isScriptExists = await contentScriptExist(tabId, frameId);
 
 
     if (!isScriptExists) {
     if (!isScriptExists) {
       await browser.tabs.executeScript(tabId, {
       await browser.tabs.executeScript(tabId, {
+        frameId,
         file: './contentScript.bundle.js',
         file: './contentScript.bundle.js',
-        allFrames: true,
       });
       });
     }
     }
 
 
-    await new Promise((resolve) => setTimeout(resolve, 1000));
-
     const frames = await getFrames(tabId);
     const frames = await getFrames(tabId);
 
 
     return frames;
     return frames;

+ 1 - 1
src/components/block/BlockConditions.vue

@@ -101,7 +101,7 @@ function addConditionEmit({ id }) {
   const { length } = block.data.conditions;
   const { length } = block.data.conditions;
 
 
   if (length >= 10) return;
   if (length >= 10) return;
-  if (length === 1) props.editor.addNodeOutput(block.id);
+  if (length === 0) props.editor.addNodeOutput(block.id);
 
 
   props.editor.addNodeOutput(block.id);
   props.editor.addNodeOutput(block.id);
 }
 }

+ 175 - 0
src/components/block/BlockGroup.vue

@@ -0,0 +1,175 @@
+<template>
+  <div :id="componentId" class="w-64">
+    <div class="p-4">
+      <div class="flex items-center mb-2">
+        <div
+          :class="block.category.color"
+          class="inline-flex items-center text-sm mr-4 p-2 rounded-lg"
+        >
+          <v-remixicon
+            :name="block.details.icon || 'riFolderZipLine'"
+            size="20"
+            class="inline-block mr-2"
+          />
+          <span>{{ t('workflow.blocks.blocks-group.name') }}</span>
+        </div>
+        <div class="flex-grow"></div>
+        <v-remixicon
+          name="riDeleteBin7Line"
+          class="cursor-pointer"
+          @click="editor.removeNodeId(`node-${block.id}`)"
+        />
+      </div>
+      <input
+        v-model="block.data.name"
+        :placeholder="t('workflow.blocks.blocks-group.groupName')"
+        type="text"
+        class="bg-transparent w-full focus:ring-0"
+      />
+    </div>
+    <draggable
+      v-model="block.data.blocks"
+      item-key="itemId"
+      class="px-4 mb-4 overflow-auto scroll text-sm space-y-1 max-h-60"
+      @mousedown.stop
+      @dragover.prevent
+      @drop="handleDrop"
+    >
+      <template #item="{ element, index }">
+        <div
+          class="p-2 rounded-lg bg-input space-x-2 flex items-center group"
+          style="cursor: grab"
+        >
+          <v-remixicon
+            :name="tasks[element.id].icon"
+            size="20"
+            class="flex-shrink-0"
+          />
+          <div class="leading-tight flex-1 overflow-hidden">
+            <p class="text-overflow">
+              {{ t(`workflow.blocks.${element.id}.name`) }}
+            </p>
+            <p
+              :title="element.data.description"
+              class="text-gray-600 dark:text-gray-200 text-overflow"
+            >
+              {{ element.data.description }}
+            </p>
+          </div>
+          <div class="invisible group-hover:visible">
+            <v-remixicon
+              name="riPencilLine"
+              size="20"
+              class="cursor-pointer inline-block mr-2"
+              @click="editBlock(element)"
+            />
+            <v-remixicon
+              name="riDeleteBin7Line"
+              size="20"
+              class="cursor-pointer inline-block"
+              @click="deleteItem(index, element.itemId)"
+            />
+          </div>
+        </div>
+      </template>
+      <template #footer>
+        <div
+          class="
+            p-2
+            rounded-lg
+            text-gray-600
+            dark:text-gray-200
+            border
+            text-center
+            border-dashed
+          "
+        >
+          {{ t('workflow.blocks.blocks-group.dropText') }}
+        </div>
+      </template>
+    </draggable>
+    <input class="hidden trigger" @change="handleDataChange" />
+  </div>
+</template>
+<script setup>
+import { watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { nanoid } from 'nanoid';
+import draggable from 'vuedraggable';
+import emitter from 'tiny-emitter/instance';
+import { tasks } from '@/utils/shared';
+import { useComponentId } from '@/composable/componentId';
+import { useEditorBlock } from '@/composable/editorBlock';
+
+const props = defineProps({
+  editor: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+
+const { t } = useI18n();
+const componentId = useComponentId('blocks-group');
+const block = useEditorBlock(`#${componentId}`, props.editor);
+
+const excludeBlocks = [
+  'trigger',
+  'repeat-task',
+  'export-data',
+  'loop-data',
+  'loop-breakpoint',
+  'blocks-group',
+  'conditions',
+  'element-exists',
+  'delay',
+];
+
+function handleDataChange({ detail }) {
+  if (!detail) return;
+
+  const itemIndex = block.data.blocks.findIndex(
+    ({ itemId }) => itemId === detail.itemId
+  );
+
+  if (itemIndex === -1) return;
+
+  block.data.blocks[itemIndex].data = detail.data;
+}
+function editBlock(payload) {
+  emitter.emit('editor:edit-block', {
+    ...tasks[payload.id],
+    ...payload,
+    isInGroup: true,
+    blockId: block.id,
+  });
+}
+function deleteItem(index, itemId) {
+  emitter.emit('editor:delete-block', { itemId, isInGroup: true });
+  block.data.blocks.splice(index, 1);
+}
+function handleDrop(event) {
+  event.preventDefault();
+  event.stopPropagation();
+
+  const droppedBlock = JSON.parse(event.dataTransfer.getData('block') || null);
+
+  if (!droppedBlock) return;
+
+  const { id, data } = droppedBlock;
+
+  if (excludeBlocks.includes(id)) return;
+
+  block.data.blocks.push({ id, data, itemId: nanoid(5) });
+}
+
+watch(
+  () => block.data,
+  (value, oldValue) => {
+    if (Object.keys(oldValue).length === 0) return;
+
+    props.editor.updateNodeDataFromId(block.id, value);
+    emitter.emit('editor:data-changed', block.id);
+  },
+  { deep: true }
+);
+</script>

+ 5 - 6
src/components/newtab/logs/LogsDataViewer.vue

@@ -26,21 +26,20 @@
       </ui-list>
       </ui-list>
     </ui-popover>
     </ui-popover>
   </div>
   </div>
-  <prism-editor
+  <shared-codemirror
     :model-value="jsonData"
     :model-value="jsonData"
-    :highlight="highlighter('json')"
     :class="editorClass"
     :class="editorClass"
+    lang="json"
     readonly
     readonly
-    class="my-editor p-4 bg-gray-900 rounded-lg mt-4"
-  ></prism-editor>
+    class="mt-4"
+  />
 </template>
 </template>
 <script setup>
 <script setup>
 import { ref } from 'vue';
 import { ref } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
-import { PrismEditor } from 'vue-prism-editor';
-import { highlighter } from '@/lib/prism';
 import { dataExportTypes } from '@/utils/shared';
 import { dataExportTypes } from '@/utils/shared';
 import dataExporter, { generateJSON } from '@/utils/data-exporter';
 import dataExporter, { generateJSON } from '@/utils/data-exporter';
+import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
 
 
 const props = defineProps({
 const props = defineProps({
   log: {
   log: {

+ 3 - 3
src/components/newtab/shared/SharedCard.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <ui-card class="hover:ring-2 group hover:ring-accent">
+  <ui-card class="hover:ring-2 flex flex-col group hover:ring-accent">
     <slot name="header">
     <slot name="header">
       <div class="flex items-center mb-4">
       <div class="flex items-center mb-4">
         <ui-img
         <ui-img
@@ -40,7 +40,7 @@
         </ui-popover>
         </ui-popover>
       </div>
       </div>
     </slot>
     </slot>
-    <div class="cursor-pointer" @click="$emit('click', data)">
+    <div class="cursor-pointer flex-1" @click="$emit('click', data)">
       <p class="line-clamp font-semibold leading-tight">
       <p class="line-clamp font-semibold leading-tight">
         {{ data.name }}
         {{ data.name }}
       </p>
       </p>
@@ -50,8 +50,8 @@
       >
       >
         {{ data.description }}
         {{ data.description }}
       </p>
       </p>
-      <p class="text-gray-600 dark:text-gray-200">{{ formatDate() }}</p>
     </div>
     </div>
+    <p class="text-gray-600 dark:text-gray-200">{{ formatDate() }}</p>
   </ui-card>
   </ui-card>
 </template>
 </template>
 <script setup>
 <script setup>

+ 101 - 0
src/components/newtab/shared/SharedCodemirror.vue

@@ -0,0 +1,101 @@
+<template>
+  <div
+    ref="containerEl"
+    :class="{ 'hide-gutters': !lineNumbers }"
+    class="codemirror relative"
+  ></div>
+</template>
+<script setup>
+import { onMounted, ref, onBeforeUnmount, watch } from 'vue';
+import { json } from '@codemirror/lang-json';
+import { indentWithTab } from '@codemirror/commands';
+import { oneDark } from '@codemirror/theme-one-dark';
+import { EditorView, keymap } from '@codemirror/view';
+import { javascript } from '@codemirror/lang-javascript';
+import { EditorState, basicSetup } from '@codemirror/basic-setup';
+
+const props = defineProps({
+  lang: {
+    type: String,
+    default: 'javascript',
+  },
+  modelValue: {
+    type: String,
+    default: '',
+  },
+  readonly: {
+    type: Boolean,
+    default: false,
+  },
+  lineNumbers: {
+    type: Boolean,
+    default: true,
+  },
+});
+const emit = defineEmits(['change', 'update:modelValue']);
+
+let view = null;
+const containerEl = ref(null);
+
+const updateListener = EditorView.updateListener.of((event) => {
+  if (event.docChanged) {
+    event.state.sliceDoc(0, 20);
+
+    const newValue = event.state.doc.toString();
+
+    emit('change', newValue);
+    emit('update:modelValue', newValue);
+  }
+});
+
+const state = EditorState.create({
+  doc: props.modelValue,
+  extensions: [
+    oneDark,
+    basicSetup,
+    updateListener,
+    EditorState.tabSize.of(2),
+    keymap.of([indentWithTab]),
+    EditorState.readOnly.of(props.readonly),
+    props.lang === 'javascript' ? javascript() : json(),
+  ],
+});
+
+watch(
+  () => props.modelValue,
+  (value) => {
+    if (value === view.state.doc.toString()) return;
+
+    view.dispatch({
+      changes: { from: 0, to: view.state.doc.length, insert: value },
+    });
+  }
+);
+
+onMounted(() => {
+  view = new EditorView({
+    state,
+    parent: containerEl.value,
+  });
+});
+onBeforeUnmount(() => {
+  view?.destroy();
+});
+</script>
+<style>
+.codemirror.hide-gutters .cm-gutters {
+  display: none !important;
+}
+
+.cm-editor {
+  height: 100%;
+  font-size: 15px;
+}
+
+.cm-editor .cm-gutters,
+.cm-editor .cm-content,
+.cm-tooltip.cm-tooltip-autocomplete > ul {
+  font-family: JetBrains Mono, Fira code, Fira Mono, Consolas, Menlo, Courier,
+    monospace !important;
+}
+</style>

+ 7 - 1
src/components/newtab/workflow/WorkflowBuilder.vue

@@ -58,6 +58,7 @@ import { onMounted, shallowRef, reactive, getCurrentInstance } from 'vue';
 import emitter from 'tiny-emitter/instance';
 import emitter from 'tiny-emitter/instance';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import { tasks } from '@/utils/shared';
 import { tasks } from '@/utils/shared';
+import { parseJSON } from '@/utils/helper';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import drawflow from '@/lib/drawflow';
 import drawflow from '@/lib/drawflow';
 
 
@@ -100,6 +101,9 @@ export default {
 
 
     function dropHandler({ dataTransfer, clientX, clientY }) {
     function dropHandler({ dataTransfer, clientX, clientY }) {
       const block = JSON.parse(dataTransfer.getData('block') || null);
       const block = JSON.parse(dataTransfer.getData('block') || null);
+
+      if (!block) return;
+
       const isTriggerExists =
       const isTriggerExists =
         block.id === 'trigger' &&
         block.id === 'trigger' &&
         editor.value.getNodesFromName('trigger').length !== 0;
         editor.value.getNodesFromName('trigger').length !== 0;
@@ -182,9 +186,11 @@ export default {
       if (props.data) {
       if (props.data) {
         const data =
         const data =
           typeof props.data === 'string'
           typeof props.data === 'string'
-            ? JSON.parse(props.data.replace(/BlockNewTab/g, 'BlockBasic'))
+            ? parseJSON(props.data.replace(/BlockNewTab/g, 'BlockBasic'), null)
             : props.data;
             : props.data;
 
 
+        if (!data) return;
+
         editor.value.import(data);
         editor.value.import(data);
       } else {
       } else {
         editor.value.addNode(
         editor.value.addNode(

+ 3 - 5
src/components/newtab/workflow/WorkflowGlobalData.vue

@@ -11,20 +11,18 @@
     <p class="float-right clear-both" title="Characters limit">
     <p class="float-right clear-both" title="Characters limit">
       {{ globalData.length }}/{{ maxLength.toLocaleString() }}
       {{ globalData.length }}/{{ maxLength.toLocaleString() }}
     </p>
     </p>
-    <prism-editor
+    <shared-codemirror
       v-model="globalData"
       v-model="globalData"
-      :highlight="highlighter('json')"
-      class="h-full scroll mt-2"
       style="height: calc(100vh - 10rem)"
       style="height: calc(100vh - 10rem)"
+      lang="json"
     />
     />
   </div>
   </div>
 </template>
 </template>
 <script setup>
 <script setup>
 import { ref, watch } from 'vue';
 import { ref, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
-import { PrismEditor } from 'vue-prism-editor';
-import { highlighter } from '@/lib/prism';
 import { debounce } from '@/utils/helper';
 import { debounce } from '@/utils/helper';
+import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
 
 
 const props = defineProps({
 const props = defineProps({
   workflow: {
   workflow: {

+ 40 - 0
src/components/newtab/workflow/edit/EditAttributeValue.vue

@@ -32,6 +32,46 @@
         <v-remixicon name="riKey2Line" />
         <v-remixicon name="riKey2Line" />
       </ui-button>
       </ui-button>
     </div>
     </div>
+    <ui-checkbox
+      :model-value="data.addExtraRow"
+      class="mt-3"
+      @change="updateData({ addExtraRow: $event })"
+    >
+      {{ t('workflow.blocks.attribute-value.forms.extraRow.checkbox') }}
+    </ui-checkbox>
+    <ui-input
+      v-if="data.addExtraRow"
+      :model-value="data.extraRowValue"
+      :title="t('workflow.blocks.attribute-value.forms.extraRow.title')"
+      :placeholder="
+        t('workflow.blocks.attribute-value.forms.extraRow.placeholder')
+      "
+      class="w-full mt-3 mb-2"
+      @change="updateData({ extraRowValue: $event })"
+    />
+    <div v-if="data.addExtraRow" class="flex items-center mt-1">
+      <ui-select
+        :model-value="data.extraRowDataColumn"
+        placeholder="Data column"
+        class="mr-2 flex-1"
+        @change="updateData({ extraRowDataColumn: $event })"
+      >
+        <option
+          v-for="column in workflow.data.value.dataColumns"
+          :key="column.name"
+          :value="column.name"
+        >
+          {{ column.name }}
+        </option>
+      </ui-select>
+      <ui-button
+        icon
+        title="Data columns"
+        @click="workflow.showDataColumnsModal(true)"
+      >
+        <v-remixicon name="riKey2Line" />
+      </ui-button>
+    </div>
   </edit-interaction-base>
   </edit-interaction-base>
 </template>
 </template>
 <script setup>
 <script setup>

+ 19 - 0
src/components/newtab/workflow/edit/EditElementExists.vue

@@ -1,5 +1,15 @@
 <template>
 <template>
   <div>
   <div>
+    <ui-select
+      :model-value="data.findBy || 'cssSelector'"
+      :placeholder="t('workflow.blocks.base.findElement.placeholder')"
+      class="w-full mb-2"
+      @change="updateData({ findBy: $event })"
+    >
+      <option v-for="type in selectorTypes" :key="type" :value="type">
+        {{ t(`workflow.blocks.base.findElement.options.${type}`) }}
+      </option>
+    </ui-select>
     <ui-input
     <ui-input
       :model-value="data.selector"
       :model-value="data.selector"
       :label="t('workflow.blocks.element-exists.selector')"
       :label="t('workflow.blocks.element-exists.selector')"
@@ -27,6 +37,7 @@
   </div>
   </div>
 </template>
 </template>
 <script setup>
 <script setup>
+import { onMounted } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 
 
 const props = defineProps({
 const props = defineProps({
@@ -39,7 +50,15 @@ const emit = defineEmits(['update:data']);
 
 
 const { t } = useI18n();
 const { t } = useI18n();
 
 
+const selectorTypes = ['cssSelector', 'xpath'];
+
 function updateData(value) {
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
   emit('update:data', { ...props.data, ...value });
 }
 }
+
+onMounted(() => {
+  if (!props.data.findBy) {
+    updateData({ findBy: 'cssSelector' });
+  }
+});
 </script>
 </script>

+ 8 - 18
src/components/newtab/workflow/edit/EditExecuteWorkflow.vue

@@ -15,13 +15,11 @@
       </option>
       </option>
     </ui-select>
     </ui-select>
     <p>{{ t('common.globalData') }}</p>
     <p>{{ t('common.globalData') }}</p>
-    <prism-editor
+    <pre
       v-if="!state.showGlobalData"
       v-if="!state.showGlobalData"
-      :model-value="data.globalData"
-      :highlight="highlighter('json')"
-      readonly
-      class="p-4 max-h-80"
+      class="rounded-lg text-gray-200 p-4 max-h-80 bg-gray-900 overflow-auto"
       @click="state.showGlobalData = true"
       @click="state.showGlobalData = true"
+      v-text="data.globalData"
     />
     />
     <ui-modal
     <ui-modal
       v-model="state.showGlobalData"
       v-model="state.showGlobalData"
@@ -29,12 +27,12 @@
       content-class="max-w-xl"
       content-class="max-w-xl"
     >
     >
       <p>{{ t('workflow.blocks.execute-workflow.overwriteNote') }}</p>
       <p>{{ t('workflow.blocks.execute-workflow.overwriteNote') }}</p>
-      <prism-editor
-        :model-value="state.globalData"
-        :highlight="highlighter('json')"
+      <shared-codemirror
+        :model-value="data.globalData"
+        lang="json"
         class="w-full scroll"
         class="w-full scroll"
         style="height: calc(100vh - 10rem)"
         style="height: calc(100vh - 10rem)"
-        @input="updateGlobalData"
+        @change="updateData({ globalData: $event })"
       />
       />
     </ui-modal>
     </ui-modal>
   </div>
   </div>
@@ -43,9 +41,8 @@
 import { computed, shallowReactive } from 'vue';
 import { computed, shallowReactive } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import { useRoute } from 'vue-router';
 import { useRoute } from 'vue-router';
-import { PrismEditor } from 'vue-prism-editor';
-import { highlighter } from '@/lib/prism';
 import Workflow from '@/models/workflow';
 import Workflow from '@/models/workflow';
+import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
 
 
 const props = defineProps({
 const props = defineProps({
   data: {
   data: {
@@ -64,7 +61,6 @@ const route = useRoute();
 
 
 const state = shallowReactive({
 const state = shallowReactive({
   showGlobalData: false,
   showGlobalData: false,
-  globalData: `${props.data.globalData}`,
 });
 });
 
 
 const workflows = computed(() =>
 const workflows = computed(() =>
@@ -80,10 +76,4 @@ const workflows = computed(() =>
 function updateData(value) {
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
   emit('update:data', { ...props.data, ...value });
 }
 }
-function updateGlobalData(event) {
-  const { value } = event.target;
-
-  state.globalData = value;
-  updateData({ globalData: value });
-}
 </script>
 </script>

+ 39 - 1
src/components/newtab/workflow/edit/EditGetText.vue

@@ -30,7 +30,7 @@
       class="mt-3"
       class="mt-3"
       @change="updateData({ saveData: $event })"
       @change="updateData({ saveData: $event })"
     >
     >
-      Save data
+      {{ t('workflow.blocks.get-text.checkbox') }}
     </ui-checkbox>
     </ui-checkbox>
     <div v-if="data.saveData" class="flex items-center mt-1">
     <div v-if="data.saveData" class="flex items-center mt-1">
       <ui-select
       <ui-select
@@ -69,6 +69,44 @@
       class="w-full"
       class="w-full"
       @change="updateData({ suffixText: $event })"
       @change="updateData({ suffixText: $event })"
     />
     />
+    <ui-checkbox
+      :model-value="data.addExtraRow"
+      class="mt-3"
+      @change="updateData({ addExtraRow: $event })"
+    >
+      {{ t('workflow.blocks.get-text.extraRow.checkbox') }}
+    </ui-checkbox>
+    <ui-input
+      v-if="data.addExtraRow"
+      :model-value="data.extraRowValue"
+      :title="t('workflow.blocks.get-text.extraRow.title')"
+      :placeholder="t('workflow.blocks.get-text.extraRow.placeholder')"
+      class="w-full mt-3 mb-2"
+      @change="updateData({ extraRowValue: $event })"
+    />
+    <div v-if="data.addExtraRow" class="flex items-center mt-1">
+      <ui-select
+        :model-value="data.extraRowDataColumn"
+        placeholder="Data column"
+        class="mr-2 flex-1"
+        @change="updateData({ extraRowDataColumn: $event })"
+      >
+        <option
+          v-for="column in workflow.data.value.dataColumns"
+          :key="column.name"
+          :value="column.name"
+        >
+          {{ column.name }}
+        </option>
+      </ui-select>
+      <ui-button
+        icon
+        title="Data columns"
+        @click="workflow.showDataColumnsModal(true)"
+      >
+        <v-remixicon name="riKey2Line" />
+      </ui-button>
+    </div>
   </edit-interaction-base>
   </edit-interaction-base>
 </template>
 </template>
 <script setup>
 <script setup>

+ 6 - 11
src/components/newtab/workflow/edit/EditJavascriptCode.vue

@@ -15,13 +15,11 @@
       :title="t('workflow.blocks.javascript-code.timeout.title')"
       :title="t('workflow.blocks.javascript-code.timeout.title')"
       @change="updateData({ timeout: +$event })"
       @change="updateData({ timeout: +$event })"
     />
     />
-    <prism-editor
+    <pre
       v-if="!state.showCodeModal"
       v-if="!state.showCodeModal"
-      :model-value="data.code"
-      :highlight="highlighter('javascript')"
-      readonly
-      class="p-4 max-h-80"
+      class="rounded-lg overflow-auto text-gray-200 p-4 max-h-80 bg-gray-900"
       @click="state.showCodeModal = true"
       @click="state.showCodeModal = true"
+      v-text="data.code"
     />
     />
     <ui-modal v-model="state.showCodeModal" content-class="max-w-3xl">
     <ui-modal v-model="state.showCodeModal" content-class="max-w-3xl">
       <template #header>
       <template #header>
@@ -40,11 +38,9 @@
         style="height: calc(100vh - 9rem)"
         style="height: calc(100vh - 9rem)"
       >
       >
         <ui-tab-panel value="code" class="h-full">
         <ui-tab-panel value="code" class="h-full">
-          <prism-editor
+          <shared-codemirror
             v-model="state.code"
             v-model="state.code"
-            :highlight="highlighter('javascript')"
-            line-numbers
-            class="py-4 overflow-auto"
+            class="overflow-auto"
             style="height: 87%"
             style="height: 87%"
           />
           />
           <p class="mt-1">
           <p class="mt-1">
@@ -96,8 +92,7 @@
 <script setup>
 <script setup>
 import { watch, reactive } from 'vue';
 import { watch, reactive } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
-import { PrismEditor } from 'vue-prism-editor';
-import { highlighter } from '@/lib/prism';
+import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
 
 
 const props = defineProps({
 const props = defineProps({
   data: {
   data: {

+ 16 - 29
src/components/newtab/workflow/edit/EditLoopData.vue

@@ -82,7 +82,7 @@
           {{ t('workflow.blocks.loop-data.buttons.import') }}
           {{ t('workflow.blocks.loop-data.buttons.import') }}
         </ui-button>
         </ui-button>
         <ui-button
         <ui-button
-          v-tooltip="t('commons.options')"
+          v-tooltip="t('common.options')"
           :class="{ 'text-primary': state.showOptions }"
           :class="{ 'text-primary': state.showOptions }"
           icon
           icon
           class="ml-2"
           class="ml-2"
@@ -91,24 +91,15 @@
           <v-remixicon name="riSettings3Line" />
           <v-remixicon name="riSettings3Line" />
         </ui-button>
         </ui-button>
         <p class="flex-1 text-overflow mx-4">{{ file.name }}</p>
         <p class="flex-1 text-overflow mx-4">{{ file.name }}</p>
-        <template v-if="data.loopData.length > maxStrLength">
-          <p class="mr-2">
-            {{ t('workflow.blocks.loop-data.modal.fileTooLarge') }}
-          </p>
-          <ui-button @click="updateData({ loopData: '[]' })">
-            {{ t('workflow.blocks.loop-data.buttons.clear') }}
-          </ui-button>
-        </template>
-        <p v-else>{{ t('workflow.blocks.loop-data.modal.maxFile') }}</p>
+        <p>{{ t('workflow.blocks.loop-data.modal.maxFile') }}</p>
       </div>
       </div>
       <div style="height: calc(100vh - 11rem)">
       <div style="height: calc(100vh - 11rem)">
-        <prism-editor
+        <shared-codemirror
           v-show="!state.showOptions"
           v-show="!state.showOptions"
-          v-model="state.tempLoopData"
-          :highlight="highlighter('json')"
-          :readonly="data.loopData.length > maxStrLength"
-          class="py-4"
-          @input="updateData({ loopData: $event.target.value })"
+          :model-value="data.loopData"
+          lang="json"
+          class="h-full"
+          @change="updateLoopData"
         />
         />
         <div v-show="state.showOptions">
         <div v-show="state.showOptions">
           <p class="font-semibold mb-2">CSV</p>
           <p class="font-semibold mb-2">CSV</p>
@@ -124,11 +115,10 @@
 /* eslint-disable no-alert */
 /* eslint-disable no-alert */
 import { onMounted, shallowReactive } from 'vue';
 import { onMounted, shallowReactive } from 'vue';
 import { nanoid } from 'nanoid';
 import { nanoid } from 'nanoid';
-import { PrismEditor } from 'vue-prism-editor';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import Papa from 'papaparse';
 import Papa from 'papaparse';
-import { highlighter } from '@/lib/prism';
 import { openFilePicker } from '@/utils/helper';
 import { openFilePicker } from '@/utils/helper';
+import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
 
 
 const props = defineProps({
 const props = defineProps({
   blockId: {
   blockId: {
@@ -144,16 +134,10 @@ const emit = defineEmits(['update:data']);
 
 
 const { t } = useI18n();
 const { t } = useI18n();
 
 
-const maxStrLength = 5e4;
 const maxFileSize = 1024 * 1024;
 const maxFileSize = 1024 * 1024;
 const loopTypes = ['data-columns', 'numbers', 'custom-data'];
 const loopTypes = ['data-columns', 'numbers', 'custom-data'];
-const tempLoopData =
-  props.data.loopData.length > maxStrLength
-    ? props.data.loopData.slice(0, maxStrLength)
-    : props.data.loopData;
 
 
 const state = shallowReactive({
 const state = shallowReactive({
-  tempLoopData,
   showOptions: false,
   showOptions: false,
   showDataModal: false,
   showDataModal: false,
   workflowLoopData: {},
   workflowLoopData: {},
@@ -170,6 +154,13 @@ const file = shallowReactive({
 function updateData(value) {
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
   emit('update:data', { ...props.data, ...value });
 }
 }
+function updateLoopData(value) {
+  if (value.length > maxFileSize) {
+    alert(t('message.maxSizeExceeded'));
+  }
+
+  updateData({ loopData: value.slice(0, maxFileSize) });
+}
 function updateLoopID(id) {
 function updateLoopID(id) {
   let loopId = id.replace(/\s/g, '');
   let loopId = id.replace(/\s/g, '');
 
 
@@ -205,12 +196,8 @@ function importFile() {
         }
         }
 
 
         if (Array.isArray(loopData)) {
         if (Array.isArray(loopData)) {
-          const loopDataStr = JSON.stringify(loopData);
+          const loopDataStr = JSON.stringify(loopData, null, 2);
 
 
-          state.tempLoopData =
-            loopDataStr.length > maxStrLength
-              ? loopDataStr.slice(0, maxStrLength)
-              : loopDataStr;
           updateData({ loopData: loopDataStr });
           updateData({ loopData: loopDataStr });
         }
         }
       };
       };

+ 76 - 35
src/components/newtab/workflow/edit/EditTakeScreenshot.vue

@@ -1,44 +1,78 @@
 <template>
 <template>
-  <div class="flex items-center mb-2 mt-8">
-    <ui-input
-      :model-value="data.fileName"
-      :placeholder="t('common.fileName')"
-      class="flex-1 mr-2"
-      title="File name"
-      @change="updateData({ fileName: $event })"
-    />
-    <ui-select
-      :model-value="data.ext || 'png'"
-      placeholder="Type"
-      @change="updateData({ ext: $event })"
+  <div class="take-screenshot">
+    <ui-checkbox
+      :model-value="data.saveToComputer"
+      class="mb-2"
+      @change="updateData({ saveToComputer: $event })"
     >
     >
-      <option value="png">PNG</option>
-      <option value="jpeg">JPEG</option>
-    </ui-select>
-  </div>
-  <p class="text-sm text-gray-600 ml-2">Image quality:</p>
-  <div class="bg-box-transparent px-4 mb-4 py-2 rounded-lg flex items-center">
-    <input
-      :value="data.quality"
-      :title="t('workflow.blocks.loop.take-screenshot.imageQuality')"
-      class="focus:outline-none flex-1"
-      type="range"
-      min="0"
-      max="100"
-      @change="updateQuality"
-    />
-    <span class="w-12 text-right">{{ data.quality }}%</span>
+      {{ t('workflow.blocks.take-screenshot.saveToComputer') }}
+    </ui-checkbox>
+    <div v-if="data.saveToComputer" class="flex items-center my-2">
+      <ui-input
+        :model-value="data.fileName"
+        :placeholder="t('common.fileName')"
+        class="flex-1 mr-2"
+        title="File name"
+        @change="updateData({ fileName: $event })"
+      />
+      <ui-select
+        :model-value="data.ext || 'png'"
+        placeholder="Type"
+        @change="updateData({ ext: $event })"
+      >
+        <option value="png">PNG</option>
+        <option value="jpeg">JPEG</option>
+      </ui-select>
+    </div>
+    <p class="text-sm text-gray-600 ml-2">Image quality:</p>
+    <div class="bg-box-transparent px-4 mb-2 py-2 rounded-lg flex items-center">
+      <input
+        :value="data.quality"
+        :title="t('workflow.blocks.take-screenshot.imageQuality')"
+        class="focus:outline-none flex-1"
+        type="range"
+        min="0"
+        max="100"
+        @change="updateQuality"
+      />
+      <span class="w-12 text-right">{{ data.quality }}%</span>
+    </div>
+    <ui-checkbox
+      :model-value="data.saveToColumn"
+      class="mt-3"
+      @change="updateData({ saveToColumn: $event })"
+    >
+      {{ t('workflow.blocks.take-screenshot.saveToColumn') }}
+    </ui-checkbox>
+    <div v-if="data.saveToColumn" class="flex items-center mt-1">
+      <ui-select
+        :model-value="data.dataColumn"
+        placeholder="Data column"
+        class="mr-2 flex-1"
+        @change="updateData({ dataColumn: $event })"
+      >
+        <option
+          v-for="column in workflow.data.value.dataColumns"
+          :key="column.name"
+          :value="column.name"
+        >
+          {{ column.name }}
+        </option>
+      </ui-select>
+      <ui-button
+        icon
+        title="Data columns"
+        @click="workflow.showDataColumnsModal(true)"
+      >
+        <v-remixicon name="riKey2Line" />
+      </ui-button>
+    </div>
   </div>
   </div>
-  <ui-checkbox
-    v-if="false"
-    :model-value="data.captureActiveTab"
-    @change="updateData({ captureActiveTab: $event })"
-  >
-    Take screenshoot of active tab
-  </ui-checkbox>
 </template>
 </template>
 <script setup>
 <script setup>
+import { inject, onMounted } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
+import { objectHasKey } from '@/utils/helper';
 
 
 const props = defineProps({
 const props = defineProps({
   data: {
   data: {
@@ -49,6 +83,7 @@ const props = defineProps({
 const emit = defineEmits(['update:data']);
 const emit = defineEmits(['update:data']);
 
 
 const { t } = useI18n();
 const { t } = useI18n();
+const workflow = inject('workflow');
 
 
 function updateData(value) {
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
   emit('update:data', { ...props.data, ...value });
@@ -61,4 +96,10 @@ function updateQuality({ target }) {
 
 
   updateData({ quality });
   updateData({ quality });
 }
 }
+
+onMounted(() => {
+  if (objectHasKey(props.data, 'saveToComputer')) return;
+
+  updateData({ saveToComputer: true, saveToColumn: false });
+});
 </script>
 </script>

+ 19 - 23
src/components/newtab/workflow/edit/EditWebhook.vue

@@ -38,9 +38,9 @@
       @change="updateData({ timeout: +$event })"
       @change="updateData({ timeout: +$event })"
     />
     />
     <ui-tabs v-model="activeTab" fill class="mb-4">
     <ui-tabs v-model="activeTab" fill class="mb-4">
-      <ui-tab value="headers">{{
-        t('workflow.blocks.webhook.tabs.headers')
-      }}</ui-tab>
+      <ui-tab value="headers">
+        {{ t('workflow.blocks.webhook.tabs.headers') }}
+      </ui-tab>
       <ui-tab value="body">{{ t('workflow.blocks.webhook.tabs.body') }}</ui-tab>
       <ui-tab value="body">{{ t('workflow.blocks.webhook.tabs.body') }}</ui-tab>
     </ui-tabs>
     </ui-tabs>
     <ui-tab-panels :model-value="activeTab">
     <ui-tab-panels :model-value="activeTab">
@@ -74,13 +74,18 @@
         </ui-button>
         </ui-button>
       </ui-tab-panel>
       </ui-tab-panel>
       <ui-tab-panel value="body">
       <ui-tab-panel value="body">
-        <prism-editor
+        <pre
           v-if="!showContentModalRef"
           v-if="!showContentModalRef"
-          :highlight="highlighter('json')"
-          :model-value="data.body"
-          class="p-4 max-h-80 mb-2"
-          readonly
+          class="
+            rounded-lg
+            text-gray-200
+            p-4
+            max-h-80
+            bg-gray-900
+            overflow-auto
+          "
           @click="showContentModalRef = true"
           @click="showContentModalRef = true"
+          v-text="data.body"
         />
         />
       </ui-tab-panel>
       </ui-tab-panel>
     </ui-tab-panels>
     </ui-tab-panels>
@@ -89,12 +94,11 @@
       content-class="max-w-3xl"
       content-class="max-w-3xl"
       :title="t('workflow.blocks.webhook.tabs.body')"
       :title="t('workflow.blocks.webhook.tabs.body')"
     >
     >
-      <prism-editor
-        v-model="contentRef"
-        :highlight="highlighter('json')"
-        class="py-4"
-        line-numbers
-        style="height: calc(100vh - 18rem)"
+      <shared-codemirror
+        :model-value="data.body"
+        lang="json"
+        style="height: calc(100vh - 10rem)"
+        @change="updateData({ body: $event })"
       />
       />
       <div class="mt-3">
       <div class="mt-3">
         <a
         <a
@@ -112,9 +116,8 @@
 <script setup>
 <script setup>
 import { ref, watch } from 'vue';
 import { ref, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
-import { PrismEditor } from 'vue-prism-editor';
-import { highlighter } from '@/lib/prism';
 import { contentTypes } from '@/utils/shared';
 import { contentTypes } from '@/utils/shared';
+import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
 
 
 const props = defineProps({
 const props = defineProps({
   data: {
   data: {
@@ -127,22 +130,15 @@ const emit = defineEmits(['update:data']);
 const { t } = useI18n();
 const { t } = useI18n();
 
 
 const activeTab = ref('headers');
 const activeTab = ref('headers');
-const contentRef = ref(props.data.body);
 const headerRef = ref(props.data.headers);
 const headerRef = ref(props.data.headers);
 const showContentModalRef = ref(false);
 const showContentModalRef = ref(false);
 
 
 function updateData(value) {
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
   emit('update:data', { ...props.data, ...value });
 }
 }
-
-watch(contentRef, (value) => {
-  updateData({ body: value });
-});
-
 function removeHeader(index) {
 function removeHeader(index) {
   headerRef.value.splice(index, 1);
   headerRef.value.splice(index, 1);
 }
 }
-
 function addHeader() {
 function addHeader() {
   headerRef.value.push({ name: '', value: '' });
   headerRef.value.push({ name: '', value: '' });
 }
 }

+ 9 - 2
src/components/newtab/workflow/edit/TriggerEventKeyboard.vue

@@ -8,9 +8,15 @@
       {{ item }}
       {{ item }}
     </ui-checkbox>
     </ui-checkbox>
   </div>
   </div>
-  <div class="flex items-center mt-3 space-x-2">
-    <ui-input v-model="defaultParams.code" class="flex-1" label="code" />
+  <ui-input v-model="defaultParams.code" class="w-full mt-2" label="code" />
+  <div class="flex items-center mt-1 space-x-2">
     <ui-input v-model="defaultParams.key" class="flex-1" label="key" />
     <ui-input v-model="defaultParams.key" class="flex-1" label="key" />
+    <ui-input
+      v-model.number="defaultParams.keyCode"
+      type="number"
+      class="flex-1"
+      label="keyCode"
+    />
   </div>
   </div>
   <ui-checkbox v-model="defaultParams.repeat" class="mt-4">
   <ui-checkbox v-model="defaultParams.repeat" class="mt-4">
     Repeat
     Repeat
@@ -35,6 +41,7 @@ const defaultParams = shallowReactive({
   shiftKey: false,
   shiftKey: false,
   code: '',
   code: '',
   key: '',
   key: '',
+  keyCode: 0,
   repat: false,
   repat: false,
 });
 });
 
 

+ 1 - 1
src/components/popup/home/HomeWorkflowCard.vue

@@ -7,7 +7,7 @@
       @click="$emit('details', workflow)"
       @click="$emit('details', workflow)"
     >
     >
       <p class="leading-tight text-overflow">{{ workflow.name }}</p>
       <p class="leading-tight text-overflow">{{ workflow.name }}</p>
-      <p class="leading-none text-gray-500">
+      <p class="leading-tight text-gray-500">
         {{ dayjs(workflow.createdAt).fromNow() }}
         {{ dayjs(workflow.createdAt).fromNow() }}
       </p>
       </p>
     </div>
     </div>

+ 1 - 0
src/components/ui/UiInput.vue

@@ -43,6 +43,7 @@
   </div>
   </div>
 </template>
 </template>
 <script>
 <script>
+/* eslint-disable vue/require-prop-types */
 export default {
 export default {
   props: {
   props: {
     modelModifiers: {
     modelModifiers: {

+ 1 - 1
src/components/ui/UiPagination.vue

@@ -63,7 +63,7 @@ const emit = defineEmits(['update:modelValue', 'paginate']);
 const { t } = useI18n();
 const { t } = useI18n();
 
 
 const inputEl = ref(null);
 const inputEl = ref(null);
-const maxPage = computed(() => Math.round(props.records / props.perPage));
+const maxPage = computed(() => Math.round(props.records / props.perPage) + 1);
 
 
 function emitEvent(page) {
 function emitEvent(page) {
   emit('update:modelValue', page);
   emit('update:modelValue', page);

+ 3 - 0
src/composable/editorBlock.js

@@ -7,6 +7,7 @@ export function useEditorBlock(selector, editor) {
     data: {},
     data: {},
     details: {},
     details: {},
     category: {},
     category: {},
+    retrieved: false,
   });
   });
 
 
   nextTick(() => {
   nextTick(() => {
@@ -30,5 +31,7 @@ export function useEditorBlock(selector, editor) {
     }, 200);
     }, 200);
   });
   });
 
 
+  block.retrieved = true;
+
   return block;
   return block;
 }
 }

+ 17 - 6
src/content/blocks-handler/handler-element-exists.js

@@ -1,21 +1,32 @@
-function elementExists({ data }) {
+import { handleElement } from '../helper';
+
+function elementExists(block) {
   return new Promise((resolve) => {
   return new Promise((resolve) => {
     let trying = 0;
     let trying = 0;
 
 
+    const isExists = () => {
+      try {
+        const element = handleElement(block, { returnElement: true });
+
+        return !!element;
+      } catch (error) {
+        console.error(error);
+        return false;
+      }
+    };
+
     function checkElement() {
     function checkElement() {
-      if (trying >= (data.tryCount || 1)) {
+      if (trying > (block.data.tryCount || 1)) {
         resolve(false);
         resolve(false);
         return;
         return;
       }
       }
 
 
-      const element = document.querySelector(data.selector);
-
-      if (element) {
+      if (isExists()) {
         resolve(true);
         resolve(true);
       } else {
       } else {
         trying += 1;
         trying += 1;
 
 
-        setTimeout(checkElement, data.timeout || 500);
+        setTimeout(checkElement, block.data.timeout || 500);
       }
       }
     }
     }
 
 

+ 21 - 6
src/content/blocks-handler/handler-javascript-code.js

@@ -40,8 +40,23 @@ function javascriptCode(block) {
   sessionStorage.setItem(`automa--${block.id}`, JSON.stringify(block.refData));
   sessionStorage.setItem(`automa--${block.id}`, JSON.stringify(block.refData));
   const automaScript = getAutomaScript(block.id);
   const automaScript = getAutomaScript(block.id);
 
 
-  return new Promise((resolve) => {
-    const isScriptExists = document.getElementById('automa-custom-js');
+  return new Promise((resolve, reject) => {
+    let documentCtx = document;
+
+    if (block.frameSelector) {
+      const iframeCtx = document.querySelector(
+        block.frameSelector
+      )?.contentDocument;
+
+      if (!iframeCtx) {
+        reject(new Error('iframe-not-found'));
+        return;
+      }
+
+      documentCtx = iframeCtx;
+    }
+
+    const isScriptExists = documentCtx.getElementById('automa-custom-js');
     const scriptAttr = `block--${block.id}`;
     const scriptAttr = `block--${block.id}`;
 
 
     if (isScriptExists && isScriptExists.hasAttribute(scriptAttr)) {
     if (isScriptExists && isScriptExists.hasAttribute(scriptAttr)) {
@@ -62,7 +77,7 @@ function javascriptCode(block) {
             item.src,
             item.src,
             'background'
             'background'
           );
           );
-          const scriptEl = document.createElement('script');
+          const scriptEl = documentCtx.createElement('script');
 
 
           scriptEl.type = 'text/javascript';
           scriptEl.type = 'text/javascript';
           scriptEl.innerHTML = script;
           scriptEl.innerHTML = script;
@@ -74,14 +89,14 @@ function javascriptCode(block) {
         } catch (error) {
         } catch (error) {
           return null;
           return null;
         }
         }
-      }, []) || [];
+      }) || [];
 
 
     Promise.allSettled(promisePreloadScripts).then((result) => {
     Promise.allSettled(promisePreloadScripts).then((result) => {
       const preloadScripts = result.reduce((acc, { status, value }) => {
       const preloadScripts = result.reduce((acc, { status, value }) => {
         if (status !== 'fulfilled' || !value) return acc;
         if (status !== 'fulfilled' || !value) return acc;
 
 
         acc.push(value);
         acc.push(value);
-        document.body.appendChild(value.script);
+        documentCtx.body.appendChild(value.script);
 
 
         return acc;
         return acc;
       }, []);
       }, []);
@@ -112,7 +127,7 @@ function javascriptCode(block) {
         timeout = setTimeout(cleanUp, block.data.timeout);
         timeout = setTimeout(cleanUp, block.data.timeout);
       });
       });
 
 
-      document.body.appendChild(script);
+      documentCtx.body.appendChild(script);
 
 
       timeout = setTimeout(cleanUp, block.data.timeout);
       timeout = setTimeout(cleanUp, block.data.timeout);
     });
     });

+ 4 - 5
src/content/blocks-handler/handler-switch-to.js

@@ -5,14 +5,13 @@ function switchTo(block) {
     handleElement(block, {
     handleElement(block, {
       onSelected(element) {
       onSelected(element) {
         if (element.tagName !== 'IFRAME') {
         if (element.tagName !== 'IFRAME') {
-          resolve('');
+          reject(new Error('not-iframe'));
           return;
           return;
         }
         }
 
 
-        resolve({ url: element.src });
-      },
-      onSuccess() {
-        resolve('');
+        const isSameOrigin = element.contentDocument !== null;
+
+        resolve({ url: element.src, isSameOrigin });
       },
       },
       onError(error) {
       onError(error) {
         reject(error);
         reject(error);

+ 5 - 5
src/content/element-selector/AppBlocks.vue

@@ -26,26 +26,26 @@
       :hide-base="true"
       :hide-base="true"
       @update:data="updateParams"
       @update:data="updateParams"
     />
     />
-    <prism-editor
+    <shared-codemirror
       v-if="state.blockResult"
       v-if="state.blockResult"
       v-model="state.blockResult"
       v-model="state.blockResult"
+      :line-numbers="false"
       readonly
       readonly
-      :highlight="highlighter('json')"
-      class="h-full scroll mt-2"
+      lang="json"
+      class="h-full mt-2"
     />
     />
   </div>
   </div>
 </template>
 </template>
 <script setup>
 <script setup>
 import { shallowReactive } from 'vue';
 import { shallowReactive } from 'vue';
-import { PrismEditor } from 'vue-prism-editor';
 import { tasks } from '@/utils/shared';
 import { tasks } from '@/utils/shared';
-import { highlighter } from '@/lib/prism';
 import handleForms from '../blocks-handler/handler-forms';
 import handleForms from '../blocks-handler/handler-forms';
 import handleGetText from '../blocks-handler/handler-get-text';
 import handleGetText from '../blocks-handler/handler-get-text';
 import handleEventClick from '../blocks-handler/handler-event-click';
 import handleEventClick from '../blocks-handler/handler-event-click';
 import handelTriggerEvent from '../blocks-handler/handler-trigger-event';
 import handelTriggerEvent from '../blocks-handler/handler-trigger-event';
 import handleElementScroll from '../blocks-handler/handler-element-scroll';
 import handleElementScroll from '../blocks-handler/handler-element-scroll';
 import EditForms from '@/components/newtab/workflow/edit/EditForms.vue';
 import EditForms from '@/components/newtab/workflow/edit/EditForms.vue';
+import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
 import EditTriggerEvent from '@/components/newtab/workflow/edit/EditTriggerEvent.vue';
 import EditTriggerEvent from '@/components/newtab/workflow/edit/EditTriggerEvent.vue';
 import EditScrollElement from '@/components/newtab/workflow/edit/EditScrollElement.vue';
 import EditScrollElement from '@/components/newtab/workflow/edit/EditScrollElement.vue';
 
 

+ 2 - 2
src/content/element-selector/AppSelector.vue

@@ -13,10 +13,10 @@
       </template>
       </template>
     </ui-input>
     </ui-input>
     <template v-if="selectedCount === 1">
     <template v-if="selectedCount === 1">
-      <button class="mr-2 ml-4" @click="$emit('parent')">
+      <button class="mr-2 ml-4" title="Parent element" @click="$emit('parent')">
         <v-remixicon rotate="90" name="riArrowLeftLine" />
         <v-remixicon rotate="90" name="riArrowLeftLine" />
       </button>
       </button>
-      <button @click="$emit('child')">
+      <button title="Child element" @click="$emit('child')">
         <v-remixicon rotate="-90" name="riArrowLeftLine" />
         <v-remixicon rotate="-90" name="riArrowLeftLine" />
       </button>
       </button>
     </template>
     </template>

+ 16 - 2
src/content/helper.js

@@ -9,7 +9,7 @@ export function markElement(el, { id, data }) {
 }
 }
 
 
 export function handleElement(
 export function handleElement(
-  { data, id },
+  { data, id, frameSelector },
   { onSelected, onError, onSuccess, returnElement }
   { onSelected, onError, onSuccess, returnElement }
 ) {
 ) {
   if (!data || !data.selector) {
   if (!data || !data.selector) {
@@ -17,11 +17,25 @@ export function handleElement(
     return null;
     return null;
   }
   }
 
 
+  let documentCtx = document;
+
+  if (frameSelector) {
+    const iframeCtx = document.querySelector(frameSelector)?.contentDocument;
+
+    if (!iframeCtx && returnElement) return null;
+    if (!iframeCtx && onError) {
+      onError(new Error('iframe-not-found'));
+      return;
+    }
+
+    documentCtx = iframeCtx;
+  }
+
   try {
   try {
     data.blockIdAttr = `block--${id}`;
     data.blockIdAttr = `block--${id}`;
 
 
     const selectorType = data.findBy || 'cssSelector';
     const selectorType = data.findBy || 'cssSelector';
-    const element = FindElement[selectorType](data);
+    const element = FindElement[selectorType](data, documentCtx);
 
 
     if (returnElement) return element;
     if (returnElement) return element;
 
 

+ 6 - 0
src/content/index.js

@@ -4,6 +4,10 @@ import elementSelector from './element-selector';
 import blocksHandler from './blocks-handler';
 import blocksHandler from './blocks-handler';
 
 
 (() => {
 (() => {
+  if (window.isAutomaInjected) return;
+
+  window.isAutomaInjected = true;
+
   browser.runtime.onMessage.addListener((data) => {
   browser.runtime.onMessage.addListener((data) => {
     if (data.isBlock) {
     if (data.isBlock) {
       const handler = blocksHandler[toCamelCase(data.name)];
       const handler = blocksHandler[toCamelCase(data.name)];
@@ -26,6 +30,8 @@ import blocksHandler from './blocks-handler';
         browser.runtime.sendMessage({
         browser.runtime.sendMessage({
           type: 'this-is-the-frame-id',
           type: 'this-is-the-frame-id',
         });
         });
+
+        resolve();
       }
       }
     });
     });
   });
   });

+ 43 - 9
src/content/shortcut.js

@@ -1,7 +1,9 @@
 import { openDB } from 'idb';
 import { openDB } from 'idb';
+import { nanoid } from 'nanoid';
 import Mousetrap from 'mousetrap';
 import Mousetrap from 'mousetrap';
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
 import secrets from 'secrets';
 import secrets from 'secrets';
+import { objectHasKey } from '@/utils/helper';
 import { sendMessage } from '@/utils/message';
 import { sendMessage } from '@/utils/message';
 
 
 Mousetrap.prototype.stopCallback = function () {
 Mousetrap.prototype.stopCallback = function () {
@@ -19,6 +21,23 @@ function getTriggerBlock(workflow) {
   return trigger;
   return trigger;
 }
 }
 
 
+function initWebListener() {
+  const listeners = {};
+
+  function on(name, callback) {
+    (listeners[name] = listeners[name] || []).push(callback);
+  }
+
+  window.addEventListener('__automa-ext__', ({ detail }) => {
+    if (!detail || !objectHasKey(listeners, detail.type)) return;
+
+    listeners[detail.type].forEach((listener) => {
+      listener(detail.data);
+    });
+  });
+
+  return { on };
+}
 async function listenWindowMessage(workflows) {
 async function listenWindowMessage(workflows) {
   try {
   try {
     if (secrets?.webOrigin !== window.location.origin) return;
     if (secrets?.webOrigin !== window.location.origin) return;
@@ -29,17 +48,29 @@ async function listenWindowMessage(workflows) {
       },
       },
     });
     });
 
 
-    db.put('store', workflows, 'workflows');
+    await db.put('store', workflows, 'workflows');
 
 
-    window.addEventListener('__automa-ext__', async ({ detail }) => {
-      if (detail.type === 'open-workflow') {
-        if (!detail.workflowId) return;
+    const webListener = initWebListener();
+    webListener.on('open-workflow', ({ workflowId }) => {
+      if (!workflowId) return;
 
 
-        sendMessage(
-          'open:dashboard',
-          `/workflows/${detail.workflowId}`,
-          'background'
+      sendMessage('open:dashboard', `/workflows/${workflowId}`, 'background');
+    });
+    webListener.on('add-workflow', async ({ workflow }) => {
+      try {
+        const { workflows: workflowsStorage } = await browser.storage.local.get(
+          'workflows'
         );
         );
+
+        workflowsStorage.push({
+          ...workflow,
+          id: nanoid(),
+          createdAt: Date.now(),
+        });
+
+        await browser.storage.local.set({ workflows: workflowsStorage });
+      } catch (error) {
+        console.error(error);
       }
       }
     });
     });
   } catch (error) {
   } catch (error) {
@@ -57,7 +88,10 @@ async function listenWindowMessage(workflows) {
 
 
     listenWindowMessage(workflows);
     listenWindowMessage(workflows);
 
 
-    document.body.setAttribute('data-atm-ext-installed', '');
+    document.body.setAttribute(
+      'data-atm-ext-installed',
+      browser.runtime.getManifest().version
+    );
 
 
     if (shortcutsArr.length === 0) return;
     if (shortcutsArr.length === 0) return;
 
 

+ 0 - 13
src/lib/prism.js

@@ -1,13 +0,0 @@
-import { highlight, languages } from 'prismjs/components/prism-core';
-import 'vue-prism-editor/dist/prismeditor.min.css';
-import 'prismjs/components/prism-clike';
-import 'prismjs/components/prism-javascript';
-import 'prismjs/components/prism-json';
-import 'prismjs/themes/prism-tomorrow.css';
-import '@/assets/css/prism-editor.css';
-
-export function highlighter(language) {
-  return function (code) {
-    return highlight(code, languages[language]);
-  };
-}

+ 4 - 0
src/lib/v-remixicon.js

@@ -1,7 +1,9 @@
 import vRemixicon from 'v-remixicon';
 import vRemixicon from 'v-remixicon';
 import {
 import {
   riHome5Line,
   riHome5Line,
+  riFolderZipLine,
   riHandHeartLine,
   riHandHeartLine,
+  riCompass3Line,
   riFileCopyLine,
   riFileCopyLine,
   riShieldKeyholeLine,
   riShieldKeyholeLine,
   riToggleLine,
   riToggleLine,
@@ -77,7 +79,9 @@ import {
 
 
 export const icons = {
 export const icons = {
   riHome5Line,
   riHome5Line,
+  riFolderZipLine,
   riHandHeartLine,
   riHandHeartLine,
+  riCompass3Line,
   riFileCopyLine,
   riFileCopyLine,
   riShieldKeyholeLine,
   riShieldKeyholeLine,
   riToggleLine,
   riToggleLine,

+ 31 - 6
src/locales/en/blocks.json

@@ -27,6 +27,12 @@
           "text": "Multiple"
           "text": "Multiple"
         }
         }
       },
       },
+      "blocks-group": {
+        "name": "Blocks group",
+        "groupName": "Group name",
+        "description": "Grouping blocks",
+        "dropText": "Drag & drop a block here"
+      },
       "trigger": {
       "trigger": {
         "name": "Trigger",
         "name": "Trigger",
         "description": "Block where the workflow will start executing",
         "description": "Block where the workflow will start executing",
@@ -41,7 +47,7 @@
         ],
         ],
         "useRegex": "Use regex",
         "useRegex": "Use regex",
         "shortcut": {
         "shortcut": {
-          "tootlip": "Record shortcut",
+          "tooltip": "Record shortcut",
           "checkboxTitle": "Execute shortcut even when you're in an input element",
           "checkboxTitle": "Execute shortcut even when you're in an input element",
           "checkbox": "Active while in input",
           "checkbox": "Active while in input",
           "note": "Note: keyboard shortcut only working when you're on a webpage"
           "note": "Note: keyboard shortcut only working when you're on a webpage"
@@ -129,6 +135,7 @@
       "get-text": {
       "get-text": {
         "name": "Get text",
         "name": "Get text",
         "description": "Get text from an element",
         "description": "Get text from an element",
+        "checkbox": "Save data",
         "prefixText": {
         "prefixText": {
           "placeholder": "Text prefix",
           "placeholder": "Text prefix",
           "title": "Add prefix to the text"
           "title": "Add prefix to the text"
@@ -136,6 +143,11 @@
         "suffixText": {
         "suffixText": {
           "placeholder": "Text suffix",
           "placeholder": "Text suffix",
           "title": "Add suffix to the text"
           "title": "Add suffix to the text"
+        },
+        "extraRow": {
+          "checkbox": "Add extra row",
+          "placeholder": "Value",
+          "title": "Value of the extra row"
         }
         }
       },
       },
       "export-data": {
       "export-data": {
@@ -172,7 +184,12 @@
         "forms": {
         "forms": {
           "name": "Attribute name",
           "name": "Attribute name",
           "checkbox": "Save data",
           "checkbox": "Save data",
-          "column": "Select column"
+          "column": "Select column",
+          "extraRow": {
+            "checkbox": "Add extra row",
+            "placeholder": "Value",
+            "title": "Value of the extra row"
+          }
         }
         }
       },
       },
       "forms": {
       "forms": {
@@ -190,9 +207,15 @@
             "label": "Typing delay (millisecond)(0 to disable)"
             "label": "Typing delay (millisecond)(0 to disable)"
           }
           }
         },
         },
-        "select": { "name": "Select" },
-        "radio": { "name": "Radio" },
-        "checkbox": { "name": "Checkbox" }
+        "select": {
+          "name": "Select"
+        },
+        "radio": {
+          "name": "Radio"
+        },
+        "checkbox": {
+          "name": "Checkbox"
+        }
       },
       },
       "repeat-task": {
       "repeat-task": {
         "name": "Repeat task",
         "name": "Repeat task",
@@ -303,7 +326,9 @@
       "take-screenshot": {
       "take-screenshot": {
         "name": "Take screenshot",
         "name": "Take screenshot",
         "description": "Take a screenshot of current active tab",
         "description": "Take a screenshot of current active tab",
-        "imageQuality": "Image quality"
+        "imageQuality": "Image quality",
+        "saveToColumn": "Save screenshot to column",
+        "saveToComputer": "Save screenshot to computer"
       },
       },
       "switch-to": {
       "switch-to": {
         "name": "Switch frame",
         "name": "Switch frame",

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

@@ -13,6 +13,7 @@
     "import": "Import workflow",
     "import": "Import workflow",
     "new": "New workflow",
     "new": "New workflow",
     "delete": "Delete workflow",
     "delete": "Delete workflow",
+    "browse": "Browse workflows",
     "name": "Workflow name",
     "name": "Workflow name",
     "rename": "Rename workflow",
     "rename": "Rename workflow",
     "add": "Add workflow",
     "add": "Add workflow",
@@ -84,8 +85,11 @@
       "finish": "Finish"
       "finish": "Finish"
     },
     },
     "messages": {
     "messages": {
+      "url-empty": "URL is empty",
+      "invalid-url": "URL is not valid",
       "conditions-empty": "Conditions is empty",
       "conditions-empty": "Conditions is empty",
       "invalid-proxy-host": "Invalid proxy host",
       "invalid-proxy-host": "Invalid proxy host",
+      "invalid-body": "Content body is not valid",
       "workflow-disabled": "Workflow is disabled",
       "workflow-disabled": "Workflow is disabled",
       "selector-empty": "Element selector is empty",
       "selector-empty": "Element selector is empty",
       "empty-workflow": "You must select a workflow first",
       "empty-workflow": "You must select a workflow first",
@@ -93,6 +97,8 @@
       "stop-timeout": "Workflow is stopped because of timeout",
       "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",
       "element-not-found": "Can't find an element with \"{selector}\" selector.",
       "element-not-found": "Can't find an element with \"{selector}\" selector.",
+      "not-iframe": "Element with \"{selector}\" selector is not an Iframe element",
+      "iframe-not-found": "Can't find an Iframe element with \"{selector}\" selector.",
       "workflow-infinite-loop": "Can't execute the workflow to prevent an infinite loop",
       "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-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."
       "no-tab": "Can't connect to a tab, use \"New tab\" or \"Active tab\" block before using the \"{name}\" block."

+ 13 - 2
src/locales/fr/blocks.json

@@ -41,7 +41,7 @@
         ],
         ],
         "useRegex": "Utiliser une Regex",
         "useRegex": "Utiliser une Regex",
         "shortcut": {
         "shortcut": {
-          "tootlip": "Enregistrer un raccourci",
+          "tooltip": "Enregistrer un raccourci",
           "checkboxTitle": "Exécuter le raccourci même lorsque vous êtes dans un élément de saisie",
           "checkboxTitle": "Exécuter le raccourci même lorsque vous êtes dans un élément de saisie",
           "checkbox": "Actif dans un élément de saisie",
           "checkbox": "Actif dans un élément de saisie",
           "note": "Note: le raccourci clavier ne fonctionne que lorsque vous êtes sur une page web"
           "note": "Note: le raccourci clavier ne fonctionne que lorsque vous êtes sur une page web"
@@ -129,6 +129,7 @@
       "get-text": {
       "get-text": {
         "name": "Obtenir le texte",
         "name": "Obtenir le texte",
         "description": "Obtenir le texte d'un élément",
         "description": "Obtenir le texte d'un élément",
+        "checkbox": "Enregistrer les données",
         "prefixText": {
         "prefixText": {
           "placeholder": "Préfixe du texte",
           "placeholder": "Préfixe du texte",
           "title": "Ajouter un préfixe au texte"
           "title": "Ajouter un préfixe au texte"
@@ -136,6 +137,11 @@
         "suffixText": {
         "suffixText": {
           "placeholder": "Suffixe du texte",
           "placeholder": "Suffixe du texte",
           "title": "Ajouter un suffixe au texte"
           "title": "Ajouter un suffixe au texte"
+        },
+        "extraRow": {
+          "checkbox": "Ajouter une ligne supplémentaire",
+          "placeholder": "Valeur",
+          "title": "Valeur de la ligne supplémentaire"
         }
         }
       },
       },
       "export-data": {
       "export-data": {
@@ -172,7 +178,12 @@
         "forms": {
         "forms": {
           "name": "Nom de l'attribut",
           "name": "Nom de l'attribut",
           "checkbox": "Enregistrer des données",
           "checkbox": "Enregistrer des données",
-          "column": "Sélectionnez la colonne"
+          "column": "Sélectionnez la colonne",
+          "extraRow": {
+            "checkbox": "Ajouter une ligne supplémentaire",
+            "placeholder": "Valeur",
+            "title": "Valeur de la ligne supplémentaire"
+          }
         }
         }
       },
       },
       "forms": {
       "forms": {

+ 1 - 1
src/locales/vi/blocks.json

@@ -41,7 +41,7 @@
         ],
         ],
         "useRegex": "Dùng regex",
         "useRegex": "Dùng regex",
         "shortcut": {
         "shortcut": {
-          "tootlip": "Ghi lại lối tắt",
+          "tooltip": "Ghi lại lối tắt",
           "checkboxTitle": "Execute shortcut even when you're in an input element",
           "checkboxTitle": "Execute shortcut even when you're in an input element",
           "checkbox": "Hoạt động khi nhập liệu",
           "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"
           "note": "Lưu ý: phím tắt chỉ hoạt động khi bạn đang truy cập một trang web"

+ 1 - 1
src/locales/zh-TW/blocks.json

@@ -34,7 +34,7 @@
         ],
         ],
         "useRegex": "使用正則表達式",
         "useRegex": "使用正則表達式",
         "shortcut": {
         "shortcut": {
-          "tootlip": "設定快捷鍵",
+          "tooltip": "設定快捷鍵",
           "checkboxTitle": "允許快捷鍵在文字輸入框內執行",
           "checkboxTitle": "允許快捷鍵在文字輸入框內執行",
           "checkbox": "啟用輸入框內執行快捷鍵",
           "checkbox": "啟用輸入框內執行快捷鍵",
           "note": "Note: 鍵盤快捷鍵只在當前網頁有效"
           "note": "Note: 鍵盤快捷鍵只在當前網頁有效"

+ 1 - 1
src/locales/zh/blocks.json

@@ -34,7 +34,7 @@
         ],
         ],
         "useRegex": "使用正则表达式",
         "useRegex": "使用正则表达式",
         "shortcut": {
         "shortcut": {
-          "tootlip": "录制快捷键",
+          "tooltip": "录制快捷键",
           "checkboxTitle": "即使在输入框中也执行快捷键",
           "checkboxTitle": "即使在输入框中也执行快捷键",
           "checkbox": "在输入框中执行快捷键",
           "checkbox": "在输入框中执行快捷键",
           "note": "提示: 键盘快捷键仅在你访问网页时有效"
           "note": "提示: 键盘快捷键仅在你访问网页时有效"

+ 1 - 0
src/manifest.json

@@ -31,6 +31,7 @@
     "proxy",
     "proxy",
     "alarms",
     "alarms",
     "storage",
     "storage",
+    "webNavigation",
     "unlimitedStorage",
     "unlimitedStorage",
     "<all_urls>"
     "<all_urls>"
   ],
   ],

+ 1 - 1
src/models/workflow.js

@@ -16,7 +16,7 @@ class Workflow extends Model {
       name: this.string(''),
       name: this.string(''),
       icon: this.string('riGlobalLine'),
       icon: this.string('riGlobalLine'),
       data: this.attr(null),
       data: this.attr(null),
-      drawflow: this.string(''),
+      drawflow: this.attr(''),
       dataColumns: this.attr([]),
       dataColumns: this.attr([]),
       description: this.string(''),
       description: this.string(''),
       globalData: this.string('[{ "key": "value" }]'),
       globalData: this.string('[{ "key": "value" }]'),

+ 3 - 0
src/newtab/App.vue

@@ -13,6 +13,7 @@ import { useStore } from 'vuex';
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
 import { loadLocaleMessages, setI18nLanguage } from '@/lib/vue-i18n';
 import { loadLocaleMessages, setI18nLanguage } from '@/lib/vue-i18n';
 import AppSidebar from '@/components/newtab/app/AppSidebar.vue';
 import AppSidebar from '@/components/newtab/app/AppSidebar.vue';
+import { sendMessage } from '@/utils/message';
 
 
 const store = useStore();
 const store = useStore();
 const retrieved = ref(false);
 const retrieved = ref(false);
@@ -47,6 +48,8 @@ onMounted(async () => {
     await loadLocaleMessages(store.state.settings.locale, 'newtab');
     await loadLocaleMessages(store.state.settings.locale, 'newtab');
     await setI18nLanguage(store.state.settings.locale);
     await setI18nLanguage(store.state.settings.locale);
 
 
+    await sendMessage('workflow:check-state', {}, 'background');
+
     retrieved.value = true;
     retrieved.value = true;
   } catch (error) {
   } catch (error) {
     retrieved.value = true;
     retrieved.value = true;

+ 35 - 0
src/newtab/pages/Workflows.vue

@@ -26,6 +26,36 @@
           </option>
           </option>
         </ui-select>
         </ui-select>
       </div>
       </div>
+      <ui-button
+        tag="a"
+        href="https://automa.vercel.app/workflows"
+        target="_blank"
+        class="inline-block relative"
+        @click="browseWorkflow"
+      >
+        <span
+          v-if="state.highlightBrowse"
+          class="flex h-3 w-3 absolute top-0 right-0 -mr-1 -mt-1"
+        >
+          <span
+            class="
+              animate-ping
+              absolute
+              inline-flex
+              h-full
+              w-full
+              rounded-full
+              bg-primary
+              opacity-75
+            "
+          ></span>
+          <span
+            class="relative inline-flex rounded-full h-3 w-3 bg-blue-600"
+          ></span>
+        </span>
+        <v-remixicon name="riCompass3Line" class="mr-2 -ml-1" />
+        {{ t('workflow.browse') }}
+      </ui-button>
       <ui-button @click="importWorkflow">
       <ui-button @click="importWorkflow">
         <v-remixicon name="riUploadLine" class="mr-2 -ml-1" />
         <v-remixicon name="riUploadLine" class="mr-2 -ml-1" />
         {{ t('workflow.import') }}
         {{ t('workflow.import') }}
@@ -168,6 +198,7 @@ const menu = [
 const savedSorts = JSON.parse(localStorage.getItem('workflow-sorts') || '{}');
 const savedSorts = JSON.parse(localStorage.getItem('workflow-sorts') || '{}');
 const state = shallowReactive({
 const state = shallowReactive({
   query: '',
   query: '',
+  highlightBrowse: !localStorage.getItem('first-time-browse'),
   sortBy: savedSorts.sortBy || 'createdAt',
   sortBy: savedSorts.sortBy || 'createdAt',
   sortOrder: savedSorts.sortOrder || 'desc',
   sortOrder: savedSorts.sortOrder || 'desc',
 });
 });
@@ -186,6 +217,10 @@ const workflows = computed(() =>
     .get()
     .get()
 );
 );
 
 
+function browseWorkflow() {
+  state.highlightBrowse = false;
+  localStorage.setItem('first-time-browse', false);
+}
 function executeWorkflow(workflow) {
 function executeWorkflow(workflow) {
   sendMessage('workflow:execute', workflow, 'background');
   sendMessage('workflow:execute', workflow, 'background');
 }
 }

+ 5 - 6
src/newtab/pages/collections/[id].vue

@@ -218,12 +218,12 @@
     <p class="float-right clear-both" title="Characters limit">
     <p class="float-right clear-both" title="Characters limit">
       {{ collection.globalData.length }}/{{ (1e4).toLocaleString() }}
       {{ collection.globalData.length }}/{{ (1e4).toLocaleString() }}
     </p>
     </p>
-    <prism-editor
+    <shared-codemirror
       :model-value="collection.globalData"
       :model-value="collection.globalData"
-      :highlight="highlighter('json')"
-      class="h-full scroll mt-2"
+      lang="json"
+      class="mt-2"
       style="height: calc(100vh - 10rem)"
       style="height: calc(100vh - 10rem)"
-      @update:modelValue="updateGlobalData"
+      @change="updateGlobalData"
     />
     />
   </ui-modal>
   </ui-modal>
 </template>
 </template>
@@ -232,16 +232,15 @@ import { computed, shallowReactive, onMounted, watch } from 'vue';
 import { nanoid } from 'nanoid';
 import { nanoid } from 'nanoid';
 import { useStore } from 'vuex';
 import { useStore } from 'vuex';
 import { useRoute, useRouter } from 'vue-router';
 import { useRoute, useRouter } from 'vue-router';
-import { PrismEditor } from 'vue-prism-editor';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import Draggable from 'vuedraggable';
 import Draggable from 'vuedraggable';
-import { highlighter } from '@/lib/prism';
 import { useDialog } from '@/composable/dialog';
 import { useDialog } from '@/composable/dialog';
 import { sendMessage } from '@/utils/message';
 import { sendMessage } from '@/utils/message';
 import Log from '@/models/log';
 import Log from '@/models/log';
 import Workflow from '@/models/workflow';
 import Workflow from '@/models/workflow';
 import Collection from '@/models/collection';
 import Collection from '@/models/collection';
 import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
 import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
+import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
 import SharedWorkflowState from '@/components/newtab/shared/SharedWorkflowState.vue';
 import SharedWorkflowState from '@/components/newtab/shared/SharedWorkflowState.vue';
 
 
 const { t } = useI18n();
 const { t } = useI18n();

+ 23 - 5
src/newtab/pages/workflows/[id].vue

@@ -150,13 +150,14 @@ import {
   provide,
   provide,
   onMounted,
   onMounted,
   onUnmounted,
   onUnmounted,
+  toRaw,
 } from 'vue';
 } from 'vue';
 import { useStore } from 'vuex';
 import { useStore } from 'vuex';
 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import emitter from 'tiny-emitter/instance';
 import emitter from 'tiny-emitter/instance';
 import { sendMessage } from '@/utils/message';
 import { sendMessage } from '@/utils/message';
-import { debounce } from '@/utils/helper';
+import { debounce, isObject } from '@/utils/helper';
 import { useDialog } from '@/composable/dialog';
 import { useDialog } from '@/composable/dialog';
 import { exportWorkflow } from '@/utils/workflow-data';
 import { exportWorkflow } from '@/utils/workflow-data';
 import Log from '@/models/log';
 import Log from '@/models/log';
@@ -226,17 +227,26 @@ const logs = computed(() =>
     .orderBy('startedAt', 'desc')
     .orderBy('startedAt', 'desc')
     .get()
     .get()
 );
 );
-
 const updateBlockData = debounce((data) => {
 const updateBlockData = debounce((data) => {
+  let payload = data;
+
   state.blockData.data = data;
   state.blockData.data = data;
   state.isDataChanged = true;
   state.isDataChanged = true;
-  editor.value.updateNodeDataFromId(state.blockData.blockId, data);
+
+  if (state.blockData.isInGroup) {
+    payload = { itemId: state.blockData.itemId, data };
+  } else {
+    editor.value.updateNodeDataFromId(state.blockData.blockId, data);
+  }
 
 
   const inputEl = document.querySelector(
   const inputEl = document.querySelector(
     `#node-${state.blockData.blockId} input.trigger`
     `#node-${state.blockData.blockId} input.trigger`
   );
   );
 
 
-  if (inputEl) inputEl.dispatchEvent(new Event('change'));
+  if (inputEl)
+    inputEl.dispatchEvent(
+      new CustomEvent('change', { detail: toRaw(payload) })
+    );
 }, 250);
 }, 250);
 function deleteLog(logId) {
 function deleteLog(logId) {
   Log.delete(logId).then(() => {
   Log.delete(logId).then(() => {
@@ -244,7 +254,13 @@ function deleteLog(logId) {
   });
   });
 }
 }
 function deleteBlock(id) {
 function deleteBlock(id) {
-  if (state.isEditBlock && state.blockData.blockId === id) {
+  if (!state.isEditBlock) return;
+
+  const isGroupBlock =
+    isObject(id) && id.isInGroup && id.itemId === state.blockData.itemId;
+  const isEditedBlock = state.blockData.blockId === id;
+
+  if (isEditedBlock || isGroupBlock) {
     state.isEditBlock = false;
     state.isEditBlock = false;
     state.blockData = {};
     state.blockData = {};
   }
   }
@@ -358,11 +374,13 @@ onMounted(() => {
   };
   };
 
 
   emitter.on('editor:edit-block', editBlock);
   emitter.on('editor:edit-block', editBlock);
+  emitter.on('editor:delete-block', deleteBlock);
   emitter.on('editor:data-changed', handleEditorDataChanged);
   emitter.on('editor:data-changed', handleEditorDataChanged);
 });
 });
 onUnmounted(() => {
 onUnmounted(() => {
   window.onbeforeunload = null;
   window.onbeforeunload = null;
   emitter.off('editor:edit-block', editBlock);
   emitter.off('editor:edit-block', editBlock);
+  emitter.off('editor:delete-block', deleteBlock);
   emitter.off('editor:data-changed', handleEditorDataChanged);
   emitter.off('editor:data-changed', handleEditorDataChanged);
 });
 });
 </script>
 </script>

+ 1 - 1
src/popup/App.vue

@@ -28,7 +28,7 @@ onMounted(async () => {
 <style>
 <style>
 body {
 body {
   height: 500px;
   height: 500px;
-  width: 330px;
+  width: 350px;
   font-size: 16px;
   font-size: 16px;
 }
 }
 </style>
 </style>

+ 6 - 6
src/utils/find-element.js

@@ -1,24 +1,24 @@
 class FindElement {
 class FindElement {
-  static cssSelector(data) {
+  static cssSelector(data, documentCtx) {
     const selector = data.markEl
     const selector = data.markEl
       ? `${data.selector.trim()}:not([${data.blockIdAttr}])`
       ? `${data.selector.trim()}:not([${data.blockIdAttr}])`
       : data.selector;
       : data.selector;
 
 
     if (data.multiple) {
     if (data.multiple) {
-      const elements = document.querySelectorAll(selector);
+      const elements = documentCtx.querySelectorAll(selector);
 
 
       if (elements.length === 0) return null;
       if (elements.length === 0) return null;
 
 
       return elements;
       return elements;
     }
     }
 
 
-    return document.querySelector(selector);
+    return documentCtx.querySelector(selector);
   }
   }
 
 
-  static xpath(data) {
-    return document.evaluate(
+  static xpath(data, documentCtx) {
+    return documentCtx.evaluate(
       data.selector,
       data.selector,
-      document,
+      documentCtx,
       null,
       null,
       XPathResult.FIRST_ORDERED_NODE_TYPE,
       XPathResult.FIRST_ORDERED_NODE_TYPE,
       null
       null

+ 14 - 8
src/utils/handle-form-element.js

@@ -31,21 +31,18 @@ function formEvent(element, data) {
     new Event('change', { bubbles: true, cancelable: true })
     new Event('change', { bubbles: true, cancelable: true })
   );
   );
 }
 }
-function inputText({ data, element, index = 0, callback }) {
+function inputText({ data, element, isEditable, index = 0, callback }) {
   const noDelay = data.delay === 0;
   const noDelay = data.delay === 0;
   const currentChar = data.value[index] ?? '';
   const currentChar = data.value[index] ?? '';
+  const elementKey = isEditable ? 'textContent' : 'value';
 
 
-  if (noDelay) {
-    element.value += data.value;
-  } else {
-    element.value += currentChar;
-  }
+  element[elementKey] += noDelay ? data.value : currentChar;
 
 
-  formEvent(element, { ...data, value: currentChar });
+  formEvent(element, { type: 'text-field', value: currentChar, isEditable });
 
 
   if (!noDelay && index + 1 !== data.value.length) {
   if (!noDelay && index + 1 !== data.value.length) {
     setTimeout(() => {
     setTimeout(() => {
-      inputText({ data, element, callback, index: index + 1 });
+      inputText({ data, element, callback, isEditable, index: index + 1 });
     }, data.delay);
     }, data.delay);
   } else {
   } else {
     callback();
     callback();
@@ -54,6 +51,15 @@ function inputText({ data, element, index = 0, callback }) {
 
 
 export default function (element, data, callback) {
 export default function (element, data, callback) {
   const textFields = ['INPUT', 'TEXTAREA'];
   const textFields = ['INPUT', 'TEXTAREA'];
+  const isEditable =
+    element.hasAttribute('contenteditable') && element.isContentEditable;
+
+  if (isEditable) {
+    if (data.clearValue) element.innerText = '';
+
+    inputText({ data, element, callback, isEditable });
+    return;
+  }
 
 
   if (data.type === 'text-field' && textFields.includes(element.tagName)) {
   if (data.type === 'text-field' && textFields.includes(element.tagName)) {
     if (data.clearValue) element.value = '';
     if (data.clearValue) element.value = '';

+ 1 - 0
src/utils/reference-data.js

@@ -50,6 +50,7 @@ export default function (block, data) {
     'selector',
     'selector',
     'prefixText',
     'prefixText',
     'suffixText',
     'suffixText',
+    'extraRowValue',
   ];
   ];
   let replacedBlock = block;
   let replacedBlock = block;
 
 

+ 31 - 5
src/utils/shared.js

@@ -1,4 +1,5 @@
-/* to-do screenshot, looping, cookies, assets, tab loaded, opened tab, and run workflow block? */
+/* to-do execute multiple blocks simultaneously, keyboard shortcut */
+import { nanoid } from 'nanoid';
 
 
 export const tasks = {
 export const tasks = {
   trigger: {
   trigger: {
@@ -167,6 +168,9 @@ export const tasks = {
       fileName: '',
       fileName: '',
       ext: 'png',
       ext: 'png',
       quality: 100,
       quality: 100,
+      dataColumn: '',
+      saveToColumn: false,
+      saveToComputer: true,
       captureActiveTab: true,
       captureActiveTab: true,
     },
     },
   },
   },
@@ -226,6 +230,9 @@ export const tasks = {
       regexExp: ['g'],
       regexExp: ['g'],
       dataColumn: '',
       dataColumn: '',
       saveData: true,
       saveData: true,
+      addExtraRow: false,
+      extraRowValue: '',
+      extraRowDataColumn: '',
     },
     },
   },
   },
   'export-data': {
   'export-data': {
@@ -306,6 +313,9 @@ export const tasks = {
       attributeName: '',
       attributeName: '',
       dataColumn: '',
       dataColumn: '',
       saveData: true,
       saveData: true,
+      addExtraRow: false,
+      extraRowValue: '',
+      extraRowDataColumn: '',
     },
     },
   },
   },
   forms: {
   forms: {
@@ -377,7 +387,7 @@ export const tasks = {
     data: {
     data: {
       description: '',
       description: '',
       timeout: 20000,
       timeout: 20000,
-      code: 'console.log("Hello world!")',
+      code: 'console.log("Hello world!");\nautomaNextBlock()',
       preloadScripts: [],
       preloadScripts: [],
     },
     },
   },
   },
@@ -454,7 +464,7 @@ export const tasks = {
       contentType: 'json',
       contentType: 'json',
       timeout: 10000,
       timeout: 10000,
       headers: [{ name: '', value: '' }],
       headers: [{ name: '', value: '' }],
-      body: '{\n "key": {{ dataColumns@0.key }} \n}',
+      body: '{}',
     },
     },
   },
   },
   'loop-data': {
   'loop-data': {
@@ -494,6 +504,22 @@ export const tasks = {
       loopId: '',
       loopId: '',
     },
     },
   },
   },
+  'blocks-group': {
+    name: 'Blocks group',
+    description: 'Grouping blocks',
+    icon: 'riFolderZipLine',
+    component: 'BlockGroup',
+    category: 'general',
+    disableEdit: true,
+    inputs: 1,
+    outputs: 1,
+    allowedInputs: true,
+    maxConnection: 1,
+    data: {
+      name: '',
+      blocks: [],
+    },
+  },
   'switch-to': {
   'switch-to': {
     name: 'Switch frame',
     name: 'Switch frame',
     description: 'Switch between main window and iframe',
     description: 'Switch between main window and iframe',
@@ -564,13 +590,13 @@ export const dataExportTypes = [
 
 
 export const firstWorkflows = [
 export const firstWorkflows = [
   {
   {
-    id: 'google-search',
+    id: nanoid(),
     name: 'Google search',
     name: 'Google search',
     createdAt: Date.now(),
     createdAt: Date.now(),
     drawflow: `{"drawflow":{"Home":{"data":{"d634ff22-5dfe-44dc-83d2-842412bd9fbf":{"id":"d634ff22-5dfe-44dc-83d2-842412bd9fbf","name":"trigger","data":{"type":"manual","interval":10},"class":"trigger","html":"BlockBasic","typenode":"vue","inputs":{},"outputs":{"output_1":{"connections":[{"node":"b9e7e0d4-e86a-4635-a352-31c63723fef4","output":"input_1"}]}},"pos_x":50,"pos_y":300},"b9e7e0d4-e86a-4635-a352-31c63723fef4":{"id":"b9e7e0d4-e86a-4635-a352-31c63723fef4","name":"new-tab","data":{"url":"https://google.com","active":true},"class":"new-tab","html":"BlockBasic","typenode":"vue","inputs":{"input_1":{"connections":[{"node":"d634ff22-5dfe-44dc-83d2-842412bd9fbf","input":"output_1"}]}},"outputs":{"output_1":{"connections":[{"node":"09f3a14c-0514-4287-93b0-aa92b0064fba","output":"input_1"}]}},"pos_x":278,"pos_y":268},"09f3a14c-0514-4287-93b0-aa92b0064fba":{"id":"09f3a14c-0514-4287-93b0-aa92b0064fba","name":"forms","data":{"description":"Type query","selector":"[name='q']","markEl":false,"multiple":false,"selected":true,"type":"text-field","value":"Stackoverflow","delay":"120","events":[]},"class":"forms","html":"BlockBasic","typenode":"vue","inputs":{"input_1":{"connections":[{"node":"b9e7e0d4-e86a-4635-a352-31c63723fef4","input":"output_1"}]}},"outputs":{"output_1":{"connections":[{"node":"5f76370d-aa3d-4258-8319-230fcfc49a3a","output":"input_1"}]}},"pos_x":551,"pos_y":290},"5f76370d-aa3d-4258-8319-230fcfc49a3a":{"id":"5f76370d-aa3d-4258-8319-230fcfc49a3a","name":"event-click","data":{"description":"Click search","selector":"center:nth-child(1) > .gNO89b","markEl":false,"multiple":false},"class":"event-click","html":"BlockBasic","typenode":"vue","inputs":{"input_1":{"connections":[{"node":"09f3a14c-0514-4287-93b0-aa92b0064fba","input":"output_1"}]}},"outputs":{"output_1":{"connections":[]}},"pos_x":794,"pos_y":308}}}}}`,
     drawflow: `{"drawflow":{"Home":{"data":{"d634ff22-5dfe-44dc-83d2-842412bd9fbf":{"id":"d634ff22-5dfe-44dc-83d2-842412bd9fbf","name":"trigger","data":{"type":"manual","interval":10},"class":"trigger","html":"BlockBasic","typenode":"vue","inputs":{},"outputs":{"output_1":{"connections":[{"node":"b9e7e0d4-e86a-4635-a352-31c63723fef4","output":"input_1"}]}},"pos_x":50,"pos_y":300},"b9e7e0d4-e86a-4635-a352-31c63723fef4":{"id":"b9e7e0d4-e86a-4635-a352-31c63723fef4","name":"new-tab","data":{"url":"https://google.com","active":true},"class":"new-tab","html":"BlockBasic","typenode":"vue","inputs":{"input_1":{"connections":[{"node":"d634ff22-5dfe-44dc-83d2-842412bd9fbf","input":"output_1"}]}},"outputs":{"output_1":{"connections":[{"node":"09f3a14c-0514-4287-93b0-aa92b0064fba","output":"input_1"}]}},"pos_x":278,"pos_y":268},"09f3a14c-0514-4287-93b0-aa92b0064fba":{"id":"09f3a14c-0514-4287-93b0-aa92b0064fba","name":"forms","data":{"description":"Type query","selector":"[name='q']","markEl":false,"multiple":false,"selected":true,"type":"text-field","value":"Stackoverflow","delay":"120","events":[]},"class":"forms","html":"BlockBasic","typenode":"vue","inputs":{"input_1":{"connections":[{"node":"b9e7e0d4-e86a-4635-a352-31c63723fef4","input":"output_1"}]}},"outputs":{"output_1":{"connections":[{"node":"5f76370d-aa3d-4258-8319-230fcfc49a3a","output":"input_1"}]}},"pos_x":551,"pos_y":290},"5f76370d-aa3d-4258-8319-230fcfc49a3a":{"id":"5f76370d-aa3d-4258-8319-230fcfc49a3a","name":"event-click","data":{"description":"Click search","selector":"center:nth-child(1) > .gNO89b","markEl":false,"multiple":false},"class":"event-click","html":"BlockBasic","typenode":"vue","inputs":{"input_1":{"connections":[{"node":"09f3a14c-0514-4287-93b0-aa92b0064fba","input":"output_1"}]}},"outputs":{"output_1":{"connections":[]}},"pos_x":794,"pos_y":308}}}}}`,
   },
   },
   {
   {
-    id: 'lorem-ipsum',
+    id: nanoid(),
     name: 'Generate lorem ipsum',
     name: 'Generate lorem ipsum',
     createdAt: Date.now(),
     createdAt: Date.now(),
     drawflow:
     drawflow:

+ 4 - 2
src/utils/webhookUtil.js

@@ -1,10 +1,12 @@
-import { isObject } from './helper';
+import { isObject, parseJSON } from './helper';
 
 
 const renderContent = (content, contentType) => {
 const renderContent = (content, contentType) => {
   // 1. render the content
   // 1. render the content
   // 2. if the content type is json then parse the json
   // 2. if the content type is json then parse the json
   // 3. else parse to form data
   // 3. else parse to form data
-  const renderedJson = JSON.parse(content);
+  const renderedJson = parseJSON(content, new Error('invalid-body'));
+
+  if (renderedJson instanceof Error) throw renderedJson;
 
 
   if (contentType === 'form') {
   if (contentType === 'form') {
     return Object.keys(renderedJson)
     return Object.keys(renderedJson)

+ 3 - 1
src/utils/workflow-data.js

@@ -29,7 +29,9 @@ export function exportWorkflow(workflow) {
     'globalData',
     'globalData',
     'description',
     'description',
   ];
   ];
-  const content = {};
+  const content = {
+    extVersion: chrome.runtime.getManifest().version,
+  };
 
 
   keys.forEach((key) => {
   keys.forEach((key) => {
     content[key] = workflow[key];
     content[key] = workflow[key];

+ 1 - 0
utils/build-zip.js

@@ -1,3 +1,4 @@
+/* eslint-disable no-console */
 const fs = require('fs');
 const fs = require('fs');
 const path = require('path');
 const path = require('path');
 const archiver = require('archiver');
 const archiver = require('archiver');

+ 1 - 0
webpack.config.js

@@ -13,6 +13,7 @@ const ASSET_PATH = process.env.ASSET_PATH || '/';
 
 
 const alias = {
 const alias = {
   '@': path.resolve(__dirname, 'src/'),
   '@': path.resolve(__dirname, 'src/'),
+  secrets: path.join(__dirname, 'secrets.blank.js'),
 };
 };
 
 
 // load the secrets
 // load the secrets

+ 278 - 10
yarn.lock

@@ -884,6 +884,243 @@
     "@babel/helper-validator-identifier" "^7.14.9"
     "@babel/helper-validator-identifier" "^7.14.9"
     to-fast-properties "^2.0.0"
     to-fast-properties "^2.0.0"
 
 
+"@codemirror/autocomplete@^0.19.0":
+  version "0.19.9"
+  resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-0.19.9.tgz#28b6600ad617bdc8dfeb0102a1df8cc61883d87c"
+  integrity sha512-Ph1LWHtFFqNUIqEVrws6I263ihe5TH+TRBPwxQ78j7st7Q67FDAmgKX6mNbUPh02dxfqQrc9qxlo5JIqKeiVdg==
+  dependencies:
+    "@codemirror/language" "^0.19.0"
+    "@codemirror/state" "^0.19.4"
+    "@codemirror/text" "^0.19.2"
+    "@codemirror/tooltip" "^0.19.0"
+    "@codemirror/view" "^0.19.0"
+    "@lezer/common" "^0.15.0"
+
+"@codemirror/basic-setup@^0.19.1":
+  version "0.19.1"
+  resolved "https://registry.yarnpkg.com/@codemirror/basic-setup/-/basic-setup-0.19.1.tgz#17b27d02f15c628eb62a85d01e3e1b1958933eb4"
+  integrity sha512-gLjD7YgZU/we6BzS/ecCmD3viw83dsgv5ZUaSydYbYx9X4w4w9RqYnckcJ+0GDyHfNr5Jtfv2Z5ZtFQnBj0UDA==
+  dependencies:
+    "@codemirror/autocomplete" "^0.19.0"
+    "@codemirror/closebrackets" "^0.19.0"
+    "@codemirror/commands" "^0.19.0"
+    "@codemirror/comment" "^0.19.0"
+    "@codemirror/fold" "^0.19.0"
+    "@codemirror/gutter" "^0.19.0"
+    "@codemirror/highlight" "^0.19.0"
+    "@codemirror/history" "^0.19.0"
+    "@codemirror/language" "^0.19.0"
+    "@codemirror/lint" "^0.19.0"
+    "@codemirror/matchbrackets" "^0.19.0"
+    "@codemirror/rectangular-selection" "^0.19.0"
+    "@codemirror/search" "^0.19.0"
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/view" "^0.19.31"
+
+"@codemirror/closebrackets@^0.19.0":
+  version "0.19.0"
+  resolved "https://registry.yarnpkg.com/@codemirror/closebrackets/-/closebrackets-0.19.0.tgz#69fdcee85779d638a00a42becd9f53a33a26d77f"
+  integrity sha512-dFWX5OEVYWRNtGaifSbwIAlymnRRjxWMiMbffbAjF7p0zfGHDbdGkiT56q3Xud63h5/tQdSo5dK1iyNTzHz5vg==
+  dependencies:
+    "@codemirror/language" "^0.19.0"
+    "@codemirror/rangeset" "^0.19.0"
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/text" "^0.19.0"
+    "@codemirror/view" "^0.19.0"
+
+"@codemirror/commands@^0.19.0":
+  version "0.19.6"
+  resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-0.19.6.tgz#1568cf2c45a05864c1a4229575c5ac367ebbad9f"
+  integrity sha512-Mjc3ZfTifOn0h5499xI3MfCVIZvO2I0ochgzxfRtPOFwfXX/k7HTgnK0/KzuGDINyxUVeDaFCkf53TyyWjdxMQ==
+  dependencies:
+    "@codemirror/language" "^0.19.0"
+    "@codemirror/matchbrackets" "^0.19.0"
+    "@codemirror/state" "^0.19.2"
+    "@codemirror/text" "^0.19.0"
+    "@codemirror/view" "^0.19.22"
+    "@lezer/common" "^0.15.0"
+
+"@codemirror/comment@^0.19.0":
+  version "0.19.0"
+  resolved "https://registry.yarnpkg.com/@codemirror/comment/-/comment-0.19.0.tgz#4f23497924e9346898c2e0123011acc535a0bea6"
+  integrity sha512-3hqAd0548fxqOBm4khFMcXVIivX8p0bSlbAuZJ6PNoUn/0wXhxkxowPp0FmFzU2+y37Z+ZQF5cRB5EREWPRIiQ==
+  dependencies:
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/text" "^0.19.0"
+    "@codemirror/view" "^0.19.0"
+
+"@codemirror/fold@^0.19.0":
+  version "0.19.2"
+  resolved "https://registry.yarnpkg.com/@codemirror/fold/-/fold-0.19.2.tgz#9d4e0c0f9f3bb2fcded7d82bea14ce39310e8e2a"
+  integrity sha512-FLi6RBhHPBnSbKZEu1S98z+VYSP5678cMdYVqhR58OWWTkEiLRVPeCTj8FhRKNL9B8Gx+lBQhGq3uwr3KtSs8w==
+  dependencies:
+    "@codemirror/gutter" "^0.19.0"
+    "@codemirror/language" "^0.19.0"
+    "@codemirror/rangeset" "^0.19.0"
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/view" "^0.19.22"
+
+"@codemirror/gutter@^0.19.0", "@codemirror/gutter@^0.19.4":
+  version "0.19.9"
+  resolved "https://registry.yarnpkg.com/@codemirror/gutter/-/gutter-0.19.9.tgz#bbb69f4d49570d9c1b3f3df5d134980c516cd42b"
+  integrity sha512-PFrtmilahin1g6uL27aG5tM/rqR9DZzZYZsIrCXA5Uc2OFTFqx4owuhoU9hqfYxHp5ovfvBwQ+txFzqS4vog6Q==
+  dependencies:
+    "@codemirror/rangeset" "^0.19.0"
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/view" "^0.19.23"
+
+"@codemirror/highlight@^0.19.0", "@codemirror/highlight@^0.19.6":
+  version "0.19.6"
+  resolved "https://registry.yarnpkg.com/@codemirror/highlight/-/highlight-0.19.6.tgz#7f2e066f83f5649e8e0748a3abe0aaeaf64b8ac2"
+  integrity sha512-+eibu6on9quY8uN3xJ/n3rH+YIDLlpX7YulVmFvqAIz/ukRQ5tWaBmB7fMixHmnmRIRBRZgB8rNtonuMwZSAHQ==
+  dependencies:
+    "@codemirror/language" "^0.19.0"
+    "@codemirror/rangeset" "^0.19.0"
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/view" "^0.19.0"
+    "@lezer/common" "^0.15.0"
+    style-mod "^4.0.0"
+
+"@codemirror/history@^0.19.0":
+  version "0.19.0"
+  resolved "https://registry.yarnpkg.com/@codemirror/history/-/history-0.19.0.tgz#cc8095c927c9566f7b69fa404074edde4c54d39c"
+  integrity sha512-E0H+lncH66IMDhaND9jgkjE7s0dhYfjCPmS+Ig2Yes9I8+UIEecIdObj8c8HPCFGctGg3fxXqRAw2mdHl2Wouw==
+  dependencies:
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/view" "^0.19.0"
+
+"@codemirror/lang-javascript@^0.19.3":
+  version "0.19.3"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-javascript/-/lang-javascript-0.19.3.tgz#281b3447d8b65d98311ebf25893ef5c30a11418b"
+  integrity sha512-2NE5z98Nz9Rv4OS5UtgehCSnyQjac+P85+evzy1D/4wllp/EPaHIEEtSP1daBvrLy49SdI/9vES3ZJu6rSv4/w==
+  dependencies:
+    "@codemirror/autocomplete" "^0.19.0"
+    "@codemirror/highlight" "^0.19.6"
+    "@codemirror/language" "^0.19.0"
+    "@codemirror/lint" "^0.19.0"
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/view" "^0.19.0"
+    "@lezer/javascript" "^0.15.1"
+
+"@codemirror/lang-json@^0.19.1":
+  version "0.19.1"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-json/-/lang-json-0.19.1.tgz#616588d1422529965243c10af6c44ad0b9134fb0"
+  integrity sha512-66K5TT9HO0ODtpjY+3Ub6t3r0OB1d27P+Kl5oygk4tDavHUBpsyHTJRFw/CdeRM2VwjbpBfctGm/cTrSthFDZg==
+  dependencies:
+    "@codemirror/highlight" "^0.19.0"
+    "@codemirror/language" "^0.19.0"
+    "@lezer/json" "^0.15.0"
+
+"@codemirror/language@^0.19.0":
+  version "0.19.7"
+  resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-0.19.7.tgz#9eef8e827692d93a701b18db9d46a42be34ecca6"
+  integrity sha512-pNNUtYWMIMG0lUSKyUXJr8U0rFiCKsKFXbA2Oj17PC+S1FY99hV0z1vcntW67ekAIZw9DMEUQnLsKBuIbAUX7Q==
+  dependencies:
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/text" "^0.19.0"
+    "@codemirror/view" "^0.19.0"
+    "@lezer/common" "^0.15.5"
+    "@lezer/lr" "^0.15.0"
+
+"@codemirror/lint@^0.19.0":
+  version "0.19.3"
+  resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-0.19.3.tgz#84101d0967fea8df114a8f0f79965c22ccd3b3cc"
+  integrity sha512-+c39s05ybD2NjghxkPFsUbH/qBL0cdzKmtHbzUm0RVspeL2OiP7uHYJ6J5+Qr9RjMIPWzcqSauRqxfmCrctUfg==
+  dependencies:
+    "@codemirror/gutter" "^0.19.4"
+    "@codemirror/panel" "^0.19.0"
+    "@codemirror/rangeset" "^0.19.1"
+    "@codemirror/state" "^0.19.4"
+    "@codemirror/tooltip" "^0.19.5"
+    "@codemirror/view" "^0.19.0"
+    crelt "^1.0.5"
+
+"@codemirror/matchbrackets@^0.19.0":
+  version "0.19.3"
+  resolved "https://registry.yarnpkg.com/@codemirror/matchbrackets/-/matchbrackets-0.19.3.tgz#1f430ada6fa21af2205280ff344ef57bb95dd3cb"
+  integrity sha512-ljkrBxaLgh8jesroUiBa57pdEwqJamxkukXrJpL9LdyFZVJaF+9TldhztRaMsMZO1XnCSSHQ9sg32iuHo7Sc2g==
+  dependencies:
+    "@codemirror/language" "^0.19.0"
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/view" "^0.19.0"
+    "@lezer/common" "^0.15.0"
+
+"@codemirror/panel@^0.19.0":
+  version "0.19.1"
+  resolved "https://registry.yarnpkg.com/@codemirror/panel/-/panel-0.19.1.tgz#bf77d27b962cf16357139e50864d0eb69d634441"
+  integrity sha512-sYeOCMA3KRYxZYJYn5PNlt9yNsjy3zTNTrbYSfVgjgL9QomIVgOJWPO5hZ2sTN8lufO6lw0vTBsIPL9MSidmBg==
+  dependencies:
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/view" "^0.19.0"
+
+"@codemirror/rangeset@^0.19.0", "@codemirror/rangeset@^0.19.1", "@codemirror/rangeset@^0.19.5":
+  version "0.19.5"
+  resolved "https://registry.yarnpkg.com/@codemirror/rangeset/-/rangeset-0.19.5.tgz#82dd2583324f5d5ffacf58922170bc5f3010e076"
+  integrity sha512-L3b+RIwIRKOJ3pJLOtpkxCUjGnxZKFyPb0CjYWKnVLuzEIaEExWWK7sp6rsejxOy8RjYzfCHlFhYB4UdQN7brw==
+  dependencies:
+    "@codemirror/state" "^0.19.0"
+
+"@codemirror/rectangular-selection@^0.19.0":
+  version "0.19.1"
+  resolved "https://registry.yarnpkg.com/@codemirror/rectangular-selection/-/rectangular-selection-0.19.1.tgz#5a88ece4fb68ce5682539497db8a64fc015aae63"
+  integrity sha512-9ElnqOg3mpZIWe0prPRd1SZ48Q9QB3bR8Aocq8UtjboJSUG8ABhRrbuTZMW/rMqpBPSjVpCe9xkCCkEQMYQVmw==
+  dependencies:
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/text" "^0.19.4"
+    "@codemirror/view" "^0.19.0"
+
+"@codemirror/search@^0.19.0":
+  version "0.19.5"
+  resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-0.19.5.tgz#cae88292a6b4a6d6e6a8b6218fe62355cf7f6055"
+  integrity sha512-9kbtCBpMDlzcj7AptMRBx9BZpC5wz+/tG8nIe4vdpOszP08ZY2AcxN5nlhCoUSZu+pd0b6fYiwjLXOf000rRpw==
+  dependencies:
+    "@codemirror/panel" "^0.19.0"
+    "@codemirror/rangeset" "^0.19.0"
+    "@codemirror/state" "^0.19.3"
+    "@codemirror/text" "^0.19.0"
+    "@codemirror/view" "^0.19.0"
+    crelt "^1.0.5"
+
+"@codemirror/state@^0.19.0", "@codemirror/state@^0.19.2", "@codemirror/state@^0.19.3", "@codemirror/state@^0.19.4":
+  version "0.19.6"
+  resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-0.19.6.tgz#d631f041d39ce41b7891b099fca26cb1fdb9763e"
+  integrity sha512-sqIQZE9VqwQj7D4c2oz9mfLhlT1ElAzGB5lO1lE33BPyrdNy1cJyCIOecT4cn4VeJOFrnjOeu+IftZ3zqdFETw==
+  dependencies:
+    "@codemirror/text" "^0.19.0"
+
+"@codemirror/text@^0.19.0", "@codemirror/text@^0.19.2", "@codemirror/text@^0.19.4":
+  version "0.19.5"
+  resolved "https://registry.yarnpkg.com/@codemirror/text/-/text-0.19.5.tgz#75033af2476214e79eae22b81ada618815441c18"
+  integrity sha512-Syu5Xc7tZzeUAM/y4fETkT0zgGr48rDG+w4U38bPwSIUr+L9S/7w2wDE1WGNzjaZPz12F6gb1gxWiSTg9ocLow==
+
+"@codemirror/theme-one-dark@^0.19.1":
+  version "0.19.1"
+  resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-0.19.1.tgz#648b9cbe37186a2b7bd2a83fb483dc7aa18ce218"
+  integrity sha512-8gc4c2k2o/EhyHoWkghCxp5vyDT96JaFGtRy35PHwIom0LZdx7aU4AbDUnITvwiFB+0+i54VO+WQjBqgTyJvqg==
+  dependencies:
+    "@codemirror/highlight" "^0.19.0"
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/view" "^0.19.0"
+
+"@codemirror/tooltip@^0.19.0", "@codemirror/tooltip@^0.19.5":
+  version "0.19.10"
+  resolved "https://registry.yarnpkg.com/@codemirror/tooltip/-/tooltip-0.19.10.tgz#c9ce5f8844ef28ab24d4a5adab0fc7ed85c44b4a"
+  integrity sha512-xqIhCHr+IYoamdNLvBnU/oDh92zPnsbT1zLaFtKTFi9GI9SxOfBhWY3jfMENlK0j1C9rk8+AvwpXblPGvY/O6w==
+  dependencies:
+    "@codemirror/state" "^0.19.0"
+    "@codemirror/view" "^0.19.0"
+
+"@codemirror/view@^0.19.0", "@codemirror/view@^0.19.22", "@codemirror/view@^0.19.23", "@codemirror/view@^0.19.31":
+  version "0.19.37"
+  resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-0.19.37.tgz#36fe17c774525c775af57e7dde2867b3b7cb400f"
+  integrity sha512-SLuLx9p0O1ZHXLehvl5MwSvUrQRcsNGemzTgJ0zRajmc3BBsNigI1PXxdo7tvBhO5DcAzRRBXoke9DZFUR6Qqg==
+  dependencies:
+    "@codemirror/rangeset" "^0.19.5"
+    "@codemirror/state" "^0.19.3"
+    "@codemirror/text" "^0.19.0"
+    style-mod "^4.0.0"
+    w3c-keyname "^2.2.4"
+
 "@discoveryjs/json-ext@^0.5.0":
 "@discoveryjs/json-ext@^0.5.0":
   version "0.5.5"
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz#9283c9ce5b289a3c4f61c12757469e59377f81f3"
   resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz#9283c9ce5b289a3c4f61c12757469e59377f81f3"
@@ -991,6 +1228,32 @@
     json5 "^2.2.0"
     json5 "^2.2.0"
     loader-utils "^2.0.0"
     loader-utils "^2.0.0"
 
 
+"@lezer/common@^0.15.0", "@lezer/common@^0.15.5":
+  version "0.15.11"
+  resolved "https://registry.yarnpkg.com/@lezer/common/-/common-0.15.11.tgz#965b5067036305f12e8a3efc344076850be1d3a8"
+  integrity sha512-vv0nSdIaVCRcJ8rPuDdsrNVfBOYe/4Szr/LhF929XyDmBndLDuWiCCHooGlGlJfzELyO608AyDhVsuX/ZG36NA==
+
+"@lezer/javascript@^0.15.1":
+  version "0.15.2"
+  resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-0.15.2.tgz#50b70a02561b047947e050e0619b1aea7131dc5f"
+  integrity sha512-ytWvdJ1NAc0pfrNipGQs8otJVfjVibpIiFKH0fl99rKSA6cVlyQN/XTj/dEAQCfBfCBPAFdc30cuUe5CGZ0odA==
+  dependencies:
+    "@lezer/lr" "^0.15.0"
+
+"@lezer/json@^0.15.0":
+  version "0.15.0"
+  resolved "https://registry.yarnpkg.com/@lezer/json/-/json-0.15.0.tgz#b96c1161eb8514e05f4eaaec95c68376e76e539f"
+  integrity sha512-OsMjjBkTkeQ15iMCu5U1OiBubRC4V9Wm03zdIlUgNZ20aUPx5DWDRqUc5wG41JXVSj7Lxmo+idlFCfBBdxB8sw==
+  dependencies:
+    "@lezer/lr" "^0.15.0"
+
+"@lezer/lr@^0.15.0":
+  version "0.15.5"
+  resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-0.15.5.tgz#4bce44169c441d9dda7be398f5202ea65c5f1138"
+  integrity sha512-DEcLyhdmBxD1foQe7RegLrSlfS/XaTMGLkO5evkzHWAQKh/JnFWp7j7iNB7s2EpxzRrBCh0U+W7JDCeFhv2mng==
+  dependencies:
+    "@lezer/common" "^0.15.0"
+
 "@medv/finder@^2.1.0":
 "@medv/finder@^2.1.0":
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/@medv/finder/-/finder-2.1.0.tgz#5c53cdaac3b87057b9e5579ca1282b2397624016"
   resolved "https://registry.yarnpkg.com/@medv/finder/-/finder-2.1.0.tgz#5c53cdaac3b87057b9e5579ca1282b2397624016"
@@ -2295,6 +2558,11 @@ crc32-stream@^4.0.2:
     crc-32 "^1.2.0"
     crc-32 "^1.2.0"
     readable-stream "^3.4.0"
     readable-stream "^3.4.0"
 
 
+crelt@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94"
+  integrity sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==
+
 cross-spawn@^6.0.0:
 cross-spawn@^6.0.0:
   version "6.0.5"
   version "6.0.5"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@@ -5448,11 +5716,6 @@ printj@~1.1.0:
   resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
   resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
   integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
   integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
 
 
-prismjs@^1.25.0:
-  version "1.25.0"
-  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756"
-  integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==
-
 process-nextick-args@~2.0.0:
 process-nextick-args@~2.0.0:
   version "2.0.1"
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -6411,6 +6674,11 @@ style-loader@3.3.0:
   resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.0.tgz#d66ea95fc50b22f8b79b69a9e414760fcf58d8d8"
   resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.0.tgz#d66ea95fc50b22f8b79b69a9e414760fcf58d8d8"
   integrity sha512-szANub7ksJtQioJYtpbWwh1hUl99uK15n5HDlikeCRil/zYMZgSxucHddyF/4A3qJMUiAjPhFowrrQuNMA7jwQ==
   integrity sha512-szANub7ksJtQioJYtpbWwh1hUl99uK15n5HDlikeCRil/zYMZgSxucHddyF/4A3qJMUiAjPhFowrrQuNMA7jwQ==
 
 
+style-mod@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.0.0.tgz#97e7c2d68b592975f2ca7a63d0dd6fcacfe35a01"
+  integrity sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==
+
 supports-color@^5.3.0:
 supports-color@^5.3.0:
   version "5.5.0"
   version "5.5.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -6835,11 +7103,6 @@ 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-prism-editor@^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"
-  integrity sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==
-
 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"
@@ -6872,6 +7135,11 @@ vuex@^4.0.2:
   dependencies:
   dependencies:
     "@vue/devtools-api" "^6.0.0-beta.11"
     "@vue/devtools-api" "^6.0.0-beta.11"
 
 
+w3c-keyname@^2.2.4:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.4.tgz#4ade6916f6290224cdbd1db8ac49eab03d0eef6b"
+  integrity sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==
+
 watchpack@^2.2.0:
 watchpack@^2.2.0:
   version "2.2.0"
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce"
   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce"