Ahmad Kholid 3 years ago
parent
commit
4682beb80d
43 changed files with 868 additions and 218 deletions
  1. 1 0
      .gitignore
  2. 4 3
      package.json
  3. 41 3
      src/background/index.js
  4. 28 4
      src/background/workflow-engine/blocks-handler/handler-execute-workflow.js
  5. 16 0
      src/background/workflow-engine/blocks-handler/handler-interaction-block.js
  6. 0 2
      src/background/workflow-engine/blocks-handler/handler-new-tab.js
  7. 16 3
      src/background/workflow-engine/blocks-handler/handler-take-screenshot.js
  8. 1 0
      src/background/workflow-engine/engine.js
  9. 1 0
      src/components/block/BlockBase.vue
  10. 28 0
      src/components/block/BlockBasic.vue
  11. 30 3
      src/components/block/BlockGroup.vue
  12. 11 1
      src/components/newtab/workflow/WorkflowActions.vue
  13. 22 13
      src/components/newtab/workflow/WorkflowBuilder.vue
  14. 71 0
      src/components/newtab/workflow/WorkflowProtect.vue
  15. 7 0
      src/components/newtab/workflow/edit/EditExecuteWorkflow.vue
  16. 11 7
      src/components/newtab/workflow/edit/EditGetText.vue
  17. 6 3
      src/components/newtab/workflow/edit/EditGoogleSheets.vue
  18. 7 0
      src/components/newtab/workflow/edit/EditTakeScreenshot.vue
  19. 8 1
      src/components/newtab/workflow/edit/EditTrigger.vue
  20. 6 1
      src/components/popup/home/HomeWorkflowCard.vue
  21. 44 35
      src/components/ui/UiInput.vue
  22. 1 1
      src/content/blocks-handler/handler-event-click.js
  23. 2 1
      src/content/blocks-handler/handler-get-text.js
  24. 139 0
      src/content/blocks-handler/handler-take-screenshot.js
  25. 15 11
      src/content/index.js
  26. 2 13
      src/content/services/shortcut-listener.js
  27. 2 0
      src/lib/v-remixicon.js
  28. 6 0
      src/locales/en/blocks.json
  29. 2 1
      src/locales/en/common.json
  30. 16 1
      src/locales/en/newtab.json
  31. 3 0
      src/models/workflow.js
  32. 0 3
      src/newtab/App.vue
  33. 6 1
      src/newtab/pages/Workflows.vue
  34. 124 17
      src/newtab/pages/workflows/[id].vue
  35. 29 0
      src/utils/decrypt-flow.js
  36. 12 15
      src/utils/reference-data/index.js
  37. 10 12
      src/utils/reference-data/key-parser.js
  38. 3 7
      src/utils/reference-data/mustache-replacer.js
  39. 3 0
      src/utils/shared.js
  40. 67 3
      src/utils/workflow-data.js
  41. 26 5
      src/utils/workflow-trigger.js
  42. 0 12
      webpack.config.js
  43. 41 36
      yarn.lock

+ 1 - 0
.gitignore

@@ -23,5 +23,6 @@
 # secrets
 # secrets
 secrets.production.js
 secrets.production.js
 secrets.development.js
 secrets.development.js
+get-pass-key.js
 
 
 .idea
 .idea

+ 4 - 3
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "automa",
   "name": "automa",
-  "version": "0.16.2",
+  "version": "0.17.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": {
@@ -29,8 +29,9 @@
     "@medv/finder": "^2.1.0",
     "@medv/finder": "^2.1.0",
     "@vuex-orm/core": "^0.36.4",
     "@vuex-orm/core": "^0.36.4",
     "compare-versions": "^4.1.2",
     "compare-versions": "^4.1.2",
+    "crypto-js": "^4.1.1",
     "dayjs": "^1.10.7",
     "dayjs": "^1.10.7",
-    "defu": "^5.0.0",
+    "defu": "^5.0.1",
     "drawflow": "^0.0.51",
     "drawflow": "^0.0.51",
     "idb": "^7.0.0",
     "idb": "^7.0.0",
     "mitt": "^3.0.0",
     "mitt": "^3.0.0",
@@ -41,7 +42,7 @@
     "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.29",
     "vue-router": "^4.0.11",
     "vue-router": "^4.0.11",
     "vue-toastification": "^2.0.0-rc.5",
     "vue-toastification": "^2.0.0-rc.5",
     "vuedraggable": "^4.1.0",
     "vuedraggable": "^4.1.0",

+ 41 - 3
src/background/index.js

@@ -1,11 +1,13 @@
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
 import { MessageListener } from '@/utils/message';
 import { MessageListener } from '@/utils/message';
 import { registerSpecificDay } from '../utils/workflow-trigger';
 import { registerSpecificDay } from '../utils/workflow-trigger';
+import { parseJSON } from '@/utils/helper';
 import WorkflowState from './workflow-state';
 import WorkflowState from './workflow-state';
 import CollectionEngine from './collection-engine';
 import CollectionEngine from './collection-engine';
 import WorkflowEngine from './workflow-engine/engine';
 import WorkflowEngine from './workflow-engine/engine';
 import blocksHandler from './workflow-engine/blocks-handler';
 import blocksHandler from './workflow-engine/blocks-handler';
 import WorkflowLogger from './workflow-logger';
 import WorkflowLogger from './workflow-logger';
+import decryptFlow, { getWorkflowPass } from '@/utils/decrypt-flow';
 
 
 const storage = {
 const storage = {
   async get(key) {
   async get(key) {
@@ -36,6 +38,16 @@ const workflow = {
     return findWorkflow;
     return findWorkflow;
   },
   },
   execute(workflowData, options) {
   execute(workflowData, options) {
+    if (workflowData.isProtected) {
+      const flow = parseJSON(workflowData.drawflow, null);
+
+      if (!flow) {
+        const pass = getWorkflowPass(workflowData.pass);
+
+        workflowData.drawflow = decryptFlow(workflowData, pass);
+      }
+    }
+
     const engine = new WorkflowEngine(workflowData, {
     const engine = new WorkflowEngine(workflowData, {
       ...options,
       ...options,
       blocksHandler,
       blocksHandler,
@@ -136,6 +148,23 @@ chrome.runtime.onInstalled.addListener((details) => {
       });
       });
   }
   }
 });
 });
+chrome.runtime.onStartup.addListener(async () => {
+  const { onStartupTriggers, workflows } = await browser.storage.local.get([
+    'onStartupTriggers',
+    'workflows',
+  ]);
+
+  (onStartupTriggers || []).forEach((workflowId, index) => {
+    const findWorkflow = workflows.find(({ id }) => id === workflowId);
+
+    if (findWorkflow) {
+      workflow.execute(findWorkflow);
+    } else {
+      onStartupTriggers.splice(index, 1);
+    }
+  });
+  await browser.storage.local.set({ onStartupTriggers });
+});
 
 
 const message = new MessageListener('background');
 const message = new MessageListener('background');
 
 
@@ -165,9 +194,16 @@ message.on('open:dashboard', async (url) => {
     console.error(error);
     console.error(error);
   }
   }
 });
 });
+message.on('set:active-tab', (tabId) => {
+  return browser.tabs.update(tabId, { active: true });
+});
+
 message.on('get:sender', (_, sender) => {
 message.on('get:sender', (_, sender) => {
   return sender;
   return sender;
 });
 });
+message.on('get:tab-screenshot', (options) => {
+  return browser.tabs.captureVisibleTab(options);
+});
 message.on('get:file', (path) => {
 message.on('get:file', (path) => {
   return new Promise((resolve, reject) => {
   return new Promise((resolve, reject) => {
     const isFile = /\.(.*)/.test(path);
     const isFile = /\.(.*)/.test(path);
@@ -193,7 +229,9 @@ message.on('get:file', (path) => {
       }
       }
     };
     };
     xhr.onerror = function () {
     xhr.onerror = function () {
-      reject(new Error(xhr.statusText));
+      reject(
+        new Error(xhr.statusText || `Can't find a file with "${path}" path`)
+      );
     };
     };
     xhr.open('GET', fileUrl);
     xhr.open('GET', fileUrl);
     xhr.send();
     xhr.send();
@@ -208,8 +246,8 @@ message.on('collection:execute', (collection) => {
   engine.init();
   engine.init();
 });
 });
 
 
-message.on('workflow:execute', (param) => {
-  workflow.execute(param);
+message.on('workflow:execute', (workflowData) => {
+  workflow.execute(workflowData);
 });
 });
 message.on('workflow:stop', async (id) => {
 message.on('workflow:stop', async (id) => {
   await workflow.states.stop(id);
   await workflow.states.stop(id);

+ 28 - 4
src/background/workflow-engine/blocks-handler/handler-execute-workflow.js

@@ -1,13 +1,26 @@
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
 import WorkflowEngine from '../engine';
 import WorkflowEngine from '../engine';
 import { getBlockConnection } from '../helper';
 import { getBlockConnection } from '../helper';
-import { isWhitespace } from '@/utils/helper';
+import { isWhitespace, parseJSON } from '@/utils/helper';
+import decryptFlow, { getWorkflowPass } from '@/utils/decrypt-flow';
 
 
 function workflowListener(workflow, options) {
 function workflowListener(workflow, options) {
   return new Promise((resolve, reject) => {
   return new Promise((resolve, reject) => {
+    if (workflow.isProtected) {
+      const flow = parseJSON(workflow.drawflow, null);
+
+      if (!flow) {
+        const pass = getWorkflowPass(workflow.pass);
+
+        workflow.drawflow = decryptFlow(workflow, pass);
+      }
+    }
+
     const engine = new WorkflowEngine(workflow, options);
     const engine = new WorkflowEngine(workflow, options);
     engine.init();
     engine.init();
     engine.on('destroyed', ({ id, status, message }) => {
     engine.on('destroyed', ({ id, status, message }) => {
+      options.events.onDestroyed(engine);
+
       if (status === 'error') {
       if (status === 'error') {
         const error = new Error(message);
         const error = new Error(message);
         error.data = { logId: id };
         error.data = { logId: id };
@@ -23,9 +36,8 @@ function workflowListener(workflow, options) {
   });
   });
 }
 }
 
 
-async function executeWorkflow(block) {
-  const nextBlockId = getBlockConnection(block);
-  const { data } = block;
+async function executeWorkflow({ outputs, data }) {
+  const nextBlockId = getBlockConnection({ outputs });
 
 
   try {
   try {
     if (data.workflowId === '') throw new Error('empty-workflow');
     if (data.workflowId === '') throw new Error('empty-workflow');
@@ -48,6 +60,18 @@ async function executeWorkflow(block) {
         onInit: (engine) => {
         onInit: (engine) => {
           this.childWorkflowId = engine.id;
           this.childWorkflowId = engine.id;
         },
         },
+        onDestroyed: (engine) => {
+          if (data.executeId) {
+            const { dataColumns, globalData, googleSheets } =
+              engine.referenceData;
+
+            this.referenceData.workflow[data.executeId] = {
+              dataColumns,
+              globalData,
+              googleSheets,
+            };
+          }
+        },
       },
       },
       states: this.states,
       states: this.states,
       logger: this.logger,
       logger: this.logger,

+ 16 - 0
src/background/workflow-engine/blocks-handler/handler-interaction-block.js

@@ -1,7 +1,23 @@
 import { objectHasKey } from '@/utils/helper';
 import { objectHasKey } from '@/utils/helper';
 import { getBlockConnection } from '../helper';
 import { getBlockConnection } from '../helper';
 
 
+async function checkAccess(blockName) {
+  if (blockName === 'upload-file') {
+    const hasFileAccess = await new Promise((resolve) =>
+      chrome.extension.isAllowedFileSchemeAccess(resolve)
+    );
+
+    if (hasFileAccess) return true;
+
+    throw new Error('no-file-access');
+  }
+
+  return true;
+}
+
 async function interactionHandler(block, { refData }) {
 async function interactionHandler(block, { refData }) {
+  await checkAccess(block.name);
+
   const { executedBlockOnWeb, debugMode } = this.workflow.settings;
   const { executedBlockOnWeb, debugMode } = this.workflow.settings;
 
 
   const nextBlockId = getBlockConnection(block);
   const nextBlockId = getBlockConnection(block);

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

@@ -1,6 +1,5 @@
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
 import { getBlockConnection } from '../helper';
 import { getBlockConnection } from '../helper';
-import executeContentScript from '../execute-content-script';
 
 
 async function newTab(block) {
 async function newTab(block) {
   if (this.windowId) {
   if (this.windowId) {
@@ -48,7 +47,6 @@ async function newTab(block) {
     }
     }
 
 
     this.activeTab.frameId = 0;
     this.activeTab.frameId = 0;
-    await executeContentScript(this.activeTab.id);
 
 
     return {
     return {
       data: url,
       data: url,

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

@@ -22,8 +22,15 @@ 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, saveToColumn, dataColumn } =
-    block.data;
+  const {
+    ext,
+    quality,
+    captureActiveTab,
+    fileName,
+    saveToColumn,
+    dataColumn,
+    fullPage,
+  } = block.data;
 
 
   const saveToComputer =
   const saveToComputer =
     typeof block.data.saveToComputer === 'undefined'
     typeof block.data.saveToComputer === 'undefined'
@@ -51,7 +58,13 @@ async function takeScreenshot(block) {
 
 
       await new Promise((resolve) => setTimeout(resolve, 500));
       await new Promise((resolve) => setTimeout(resolve, 500));
 
 
-      const uri = await browser.tabs.captureVisibleTab(options);
+      const uri = await (fullPage
+        ? this._sendMessageToTab({
+            tabId: this.activeTab.id,
+            options,
+            name: block.name,
+          })
+        : browser.tabs.captureVisibleTab(options));
 
 
       if (tab) {
       if (tab) {
         await browser.windows.update(tab.windowId, { focused: true });
         await browser.windows.update(tab.windowId, { focused: true });

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

@@ -45,6 +45,7 @@ class WorkflowEngine {
     };
     };
     this.referenceData = {
     this.referenceData = {
       loopData: {},
       loopData: {},
+      workflow: {},
       dataColumns: [],
       dataColumns: [],
       googleSheets: {},
       googleSheets: {},
       globalData: parseJSON(globalDataValue, globalDataValue),
       globalData: parseJSON(globalDataValue, globalDataValue),

+ 1 - 0
src/components/block/BlockBase.vue

@@ -1,5 +1,6 @@
 <template>
 <template>
   <div class="block-base relative" @dblclick="$emit('edit')">
   <div class="block-base relative" @dblclick="$emit('edit')">
+    <slot name="prepend" />
     <div
     <div
       :class="contentClass"
       :class="contentClass"
       class="z-10 bg-white relative rounded-lg overflow-hidden w-full p-4"
       class="z-10 bg-white relative rounded-lg overflow-hidden w-full p-4"

+ 28 - 0
src/components/block/BlockBasic.vue

@@ -4,6 +4,7 @@
     :hide-edit="block.details.disableEdit"
     :hide-edit="block.details.disableEdit"
     :hide-delete="block.details.disableDelete"
     :hide-delete="block.details.disableDelete"
     content-class="flex items-center"
     content-class="flex items-center"
+    class="block-basic"
     @edit="editBlock"
     @edit="editBlock"
     @delete="editor.removeNodeId(`node-${block.id}`)"
     @delete="editor.removeNodeId(`node-${block.id}`)"
   >
   >
@@ -30,6 +31,18 @@
         @change="handleDataChange"
         @change="handleDataChange"
       />
       />
     </div>
     </div>
+    <template #prepend>
+      <div
+        v-if="block.details.id !== 'trigger'"
+        :title="t('workflow.blocks.base.moveToGroup')"
+        draggable="true"
+        class="bg-white invisible move-to-group z-50 absolute -top-2 -right-2 rounded-md p-1 shadow-md"
+        @dragstart="handleStartDrag"
+        @mousedown.stop
+      >
+        <v-remixicon name="riDragDropLine" size="20" />
+      </div>
+    </template>
   </block-base>
   </block-base>
 </template>
 </template>
 <script setup>
 <script setup>
@@ -62,4 +75,19 @@ function handleDataChange() {
 
 
   block.data = data;
   block.data = data;
 }
 }
+function handleStartDrag(event) {
+  const payload = {
+    data: block.data,
+    id: block.details.id,
+    blockId: block.id,
+  };
+
+  event.dataTransfer.setData('block', JSON.stringify(payload));
+}
 </script>
 </script>
+<style>
+.drawflow-node.selected .move-to-group,
+.block-basic:hover .move-to-group {
+  visibility: visible;
+}
+</style>

+ 30 - 3
src/components/block/BlockGroup.vue

@@ -39,6 +39,8 @@
         <div
         <div
           class="p-2 rounded-lg bg-input space-x-2 flex items-center group"
           class="p-2 rounded-lg bg-input space-x-2 flex items-center group"
           style="cursor: grab"
           style="cursor: grab"
+          @dragstart="onDragStart(element, $event)"
+          @dragend="onDragEnd(element.itemId)"
         >
         >
           <v-remixicon
           <v-remixicon
             :name="tasks[element.id].icon"
             :name="tasks[element.id].icon"
@@ -109,7 +111,6 @@ const block = useEditorBlock(`#${componentId}`, props.editor);
 const excludeBlocks = [
 const excludeBlocks = [
   'trigger',
   'trigger',
   'repeat-task',
   'repeat-task',
-  'export-data',
   'loop-data',
   'loop-data',
   'loop-breakpoint',
   'loop-breakpoint',
   'blocks-group',
   'blocks-group',
@@ -118,6 +119,28 @@ const excludeBlocks = [
   'delay',
   'delay',
 ];
 ];
 
 
+function onDragStart(item, event) {
+  event.dataTransfer.setData(
+    'block',
+    JSON.stringify({ ...tasks[item.id], ...item, fromGroup: true })
+  );
+}
+function onDragEnd(itemId) {
+  setTimeout(() => {
+    const blockEl = document.querySelector(`[group-item-id="${itemId}"]`);
+
+    if (blockEl) {
+      const blockIndex = block.data.blocks.findIndex(
+        (item) => item.itemId === itemId
+      );
+
+      if (blockIndex !== -1) {
+        emitter.emit('editor:delete-block', { itemId, isInGroup: true });
+        block.data.blocks.splice(blockIndex, 1);
+      }
+    }
+  }, 200);
+}
 function handleDataChange({ detail }) {
 function handleDataChange({ detail }) {
   if (!detail) return;
   if (!detail) return;
 
 
@@ -147,9 +170,9 @@ function handleDrop(event) {
 
 
   const droppedBlock = JSON.parse(event.dataTransfer.getData('block') || null);
   const droppedBlock = JSON.parse(event.dataTransfer.getData('block') || null);
 
 
-  if (!droppedBlock) return;
+  if (!droppedBlock || droppedBlock.fromGroup) return;
 
 
-  const { id, data } = droppedBlock;
+  const { id, data, blockId } = droppedBlock;
 
 
   if (excludeBlocks.includes(id)) {
   if (excludeBlocks.includes(id)) {
     toast.error(
     toast.error(
@@ -161,6 +184,10 @@ function handleDrop(event) {
     return;
     return;
   }
   }
 
 
+  if (blockId) {
+    props.editor.removeNodeId(`node-${blockId}`);
+  }
+
   block.data.blocks.push({ id, data, itemId: nanoid(5) });
   block.data.blocks.push({ id, data, itemId: nanoid(5) });
 }
 }
 
 

+ 11 - 1
src/components/newtab/workflow/WorkflowActions.vue

@@ -11,11 +11,20 @@
     </button>
     </button>
   </ui-card>
   </ui-card>
   <ui-card padding="p-1 ml-4">
   <ui-card padding="p-1 ml-4">
+    <button
+      v-tooltip.group="
+        t(`workflow.protect.${workflow.isProtected ? 'remove' : 'title'}`)
+      "
+      :class="{ 'text-green-600': workflow.isProtected }"
+      class="hoverable p-2 rounded-lg"
+      @click="$emit('protect')"
+    >
+      <v-remixicon name="riShieldKeyholeLine" />
+    </button>
     <button
     <button
       v-if="!workflow.isDisabled"
       v-if="!workflow.isDisabled"
       v-tooltip.group="t('common.execute')"
       v-tooltip.group="t('common.execute')"
       :title="shortcuts['editor:execute-workflow'].readable"
       :title="shortcuts['editor:execute-workflow'].readable"
-      icon
       class="hoverable p-2 rounded-lg"
       class="hoverable p-2 rounded-lg"
       @click="$emit('execute')"
       @click="$emit('execute')"
     >
     >
@@ -100,6 +109,7 @@ const emit = defineEmits([
   'rename',
   'rename',
   'delete',
   'delete',
   'save',
   'save',
+  'protect',
   'export',
   'export',
   'update',
   'update',
 ]);
 ]);

+ 22 - 13
src/components/newtab/workflow/WorkflowBuilder.vue

@@ -83,7 +83,7 @@ export default {
       default: '',
       default: '',
     },
     },
   },
   },
-  emits: ['load', 'deleteBlock', 'update'],
+  emits: ['load', 'deleteBlock', 'update', 'save'],
   setup(props, { emit }) {
   setup(props, { emit }) {
     useGroupTooltip();
     useGroupTooltip();
     const { t } = useI18n();
     const { t } = useI18n();
@@ -160,7 +160,7 @@ export default {
         block.id === 'trigger' &&
         block.id === 'trigger' &&
         editor.value.getNodesFromName('trigger').length !== 0;
         editor.value.getNodesFromName('trigger').length !== 0;
 
 
-      if (!block || isTriggerExists) return;
+      if (isTriggerExists) return;
 
 
       const xPosition =
       const xPosition =
         clientX *
         clientX *
@@ -189,6 +189,12 @@ export default {
         'vue'
         'vue'
       );
       );
 
 
+      if (block.fromGroup) {
+        const blockEl = document.getElementById(`node-${blockId}`);
+
+        blockEl.setAttribute('group-item-id', block.itemId);
+      }
+
       if (isConnectionEl(target)) {
       if (isConnectionEl(target)) {
         target.classList.remove('selected');
         target.classList.remove('selected');
 
 
@@ -316,7 +322,7 @@ export default {
       if (props.data) {
       if (props.data) {
         let data =
         let data =
           typeof props.data === 'string'
           typeof props.data === 'string'
-            ? parseJSON(props.data.replace(/BlockNewTab/g, 'BlockBasic'), null)
+            ? parseJSON(props.data, null)
             : props.data;
             : props.data;
 
 
         if (!data) return;
         if (!data) return;
@@ -332,26 +338,29 @@ export default {
           const newDrawflowData = Object.entries(
           const newDrawflowData = Object.entries(
             data.drawflow.Home.data
             data.drawflow.Home.data
           ).reduce((obj, [key, value]) => {
           ).reduce((obj, [key, value]) => {
-            const newBlockData = defu(value, tasks[value.name]);
-
-            obj[key] = newBlockData;
+            obj[key] = {
+              ...value,
+              html: tasks[value.name].component,
+              data: defu({}, value.data, tasks[value.name].data),
+            };
 
 
             return obj;
             return obj;
           }, {});
           }, {});
 
 
-          const drawflowData = {
+          data = {
             drawflow: { Home: { data: newDrawflowData } },
             drawflow: { Home: { data: newDrawflowData } },
           };
           };
 
 
-          data = drawflowData;
-
-          emit('update', {
-            version: currentExtVersion,
-            drawflow: JSON.stringify(drawflowData),
-          });
+          emit('update', { version: currentExtVersion });
         }
         }
 
 
         editor.value.import(data);
         editor.value.import(data);
+
+        if (isOldWorkflow) {
+          setTimeout(() => {
+            emit('save');
+          }, 200);
+        }
       } else {
       } else {
         editor.value.addNode(
         editor.value.addNode(
           'trigger',
           'trigger',

+ 71 - 0
src/components/newtab/workflow/WorkflowProtect.vue

@@ -0,0 +1,71 @@
+<template>
+  <div>
+    <form
+      class="mb-4 flex items-center w-full"
+      @submit.prevent="protectWorkflow"
+    >
+      <ui-input
+        v-model="state.password"
+        :placeholder="t('common.password')"
+        :type="state.showPassword ? 'text' : 'password'"
+        input-class="pr-10"
+        autofocus
+        class="flex-1 mr-6"
+      >
+        <template #append>
+          <v-remixicon
+            :name="state.showPassword ? 'riEyeOffLine' : 'riEyeLine'"
+            class="absolute right-2"
+            @click="state.showPassword = !state.showPassword"
+          />
+        </template>
+      </ui-input>
+      <ui-button variant="accent">
+        {{ t('workflow.protect.button') }}
+      </ui-button>
+    </form>
+    <p>
+      {{ t('workflow.protect.note') }}
+    </p>
+  </div>
+</template>
+<script setup>
+import { shallowReactive } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { nanoid } from 'nanoid';
+import AES from 'crypto-js/aes';
+import hmacSHA256 from 'crypto-js/hmac-sha256';
+import getPassKey from '@/utils/get-pass-key';
+
+const props = defineProps({
+  workflow: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['update', 'close']);
+
+const { t } = useI18n();
+
+const state = shallowReactive({
+  password: '',
+  showPassword: false,
+});
+
+async function protectWorkflow() {
+  const key = getPassKey(nanoid());
+  const encryptedPass = AES.encrypt(state.password, key).toString();
+  const hmac = hmacSHA256(encryptedPass, state.password).toString();
+
+  const { drawflow } = props.workflow;
+  const flow =
+    typeof drawflow === 'string' ? drawflow : JSON.stringify(drawflow);
+
+  emit('update', {
+    isProtected: true,
+    pass: hmac + encryptedPass,
+    drawflow: AES.encrypt(flow, state.password).toString(),
+  });
+  emit('close');
+}
+</script>

+ 7 - 0
src/components/newtab/workflow/edit/EditExecuteWorkflow.vue

@@ -14,6 +14,13 @@
         {{ workflow.name }}
         {{ workflow.name }}
       </option>
       </option>
     </ui-select>
     </ui-select>
+    <ui-input
+      :model-value="data.executeId"
+      :placeholder="t('workflow.blocks.execute-workflow.executeId')"
+      :title="t('workflow.blocks.execute-workflow.executeId')"
+      class="mb-2 w-full"
+      @change="updateData({ executeId: $event })"
+    />
     <p>{{ t('common.globalData') }}</p>
     <p>{{ t('common.globalData') }}</p>
     <pre
     <pre
       v-if="!state.showGlobalData"
       v-if="!state.showGlobalData"

+ 11 - 7
src/components/newtab/workflow/edit/EditGetText.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
   <edit-interaction-base v-bind="{ data }" @change="updateData">
   <edit-interaction-base v-bind="{ data }" @change="updateData">
-    <div class="flex rounded-lg bg-input px-4 items-center transition mt-2">
+    <div class="flex rounded-lg bg-input px-4 items-center transition mt-3">
       <span>/</span>
       <span>/</span>
       <input
       <input
         :value="data.regex"
         :value="data.regex"
@@ -69,6 +69,13 @@
       class="w-full"
       class="w-full"
       @change="updateData({ suffixText: $event })"
       @change="updateData({ suffixText: $event })"
     />
     />
+    <ui-checkbox
+      :model-value="data.includeTags"
+      class="mt-3"
+      @change="updateData({ includeTags: $event })"
+    >
+      {{ t('workflow.blocks.get-text.includeTags') }}
+    </ui-checkbox>
     <ui-checkbox
     <ui-checkbox
       :model-value="data.addExtraRow"
       :model-value="data.addExtraRow"
       class="mt-3"
       class="mt-3"
@@ -125,7 +132,7 @@ const emit = defineEmits(['update:data']);
 const { t } = useI18n();
 const { t } = useI18n();
 
 
 const workflow = inject('workflow');
 const workflow = inject('workflow');
-const regexExp = ref(props.data.regexExp);
+const regexExp = ref([...new Set(props.data.regexExp)]);
 
 
 const exps = [
 const exps = [
   { id: 'g', name: 'global' },
   { id: 'g', name: 'global' },
@@ -137,15 +144,12 @@ function updateData(value) {
   emit('update:data', { ...props.data, ...value });
   emit('update:data', { ...props.data, ...value });
 }
 }
 function handleExpCheckbox(id, value) {
 function handleExpCheckbox(id, value) {
-  const copy = [...new Set(regexExp.value)];
-
   if (value) {
   if (value) {
-    copy.push(id);
+    regexExp.value.push(id);
   } else {
   } else {
-    copy.splice(copy.indexOf(id), 1);
+    regexExp.value.splice(regexExp.value.indexOf(id), 1);
   }
   }
 
 
-  regexExp.value = copy;
   updateData({ regexExp: regexExp.value });
   updateData({ regexExp: regexExp.value });
 }
 }
 </script>
 </script>

+ 6 - 3
src/components/newtab/workflow/edit/EditGoogleSheets.vue

@@ -81,7 +81,7 @@
         {{ previewDataState.errorMessage }}
         {{ previewDataState.errorMessage }}
       </p>
       </p>
       <shared-codemirror
       <shared-codemirror
-        v-if="previewDataState.data"
+        v-if="previewDataState.data && previewDataState.status !== 'error'"
         :model-value="previewDataState.data"
         :model-value="previewDataState.data"
         readonly
         readonly
         class="mt-4 max-h-96"
         class="mt-4 max-h-96"
@@ -194,7 +194,9 @@ async function previewData() {
     });
     });
 
 
     if (response.status !== 200) {
     if (response.status !== 200) {
-      throw new Error(response.statusText);
+      const error = await response.json();
+
+      throw new Error(response.statusText || error.statusMessage);
     }
     }
 
 
     const { values } = await response.json();
     const { values } = await response.json();
@@ -206,7 +208,8 @@ async function previewData() {
 
 
     previewDataState.status = 'idle';
     previewDataState.status = 'idle';
   } catch (error) {
   } catch (error) {
-    console.error(error);
+    console.dir(error);
+    previewDataState.data = '';
     previewDataState.status = 'error';
     previewDataState.status = 'error';
     previewDataState.errorMessage = error.message;
     previewDataState.errorMessage = error.message;
   }
   }

+ 7 - 0
src/components/newtab/workflow/edit/EditTakeScreenshot.vue

@@ -1,5 +1,12 @@
 <template>
 <template>
   <div class="take-screenshot">
   <div class="take-screenshot">
+    <ui-checkbox
+      :model-value="data.fullPage"
+      class="mb-2"
+      @change="updateData({ fullPage: $event })"
+    >
+      {{ t('workflow.blocks.take-screenshot.fullPage') }}
+    </ui-checkbox>
     <ui-checkbox
     <ui-checkbox
       :model-value="data.saveToComputer"
       :model-value="data.saveToComputer"
       class="mb-2"
       class="mb-2"

+ 8 - 1
src/components/newtab/workflow/edit/EditTrigger.vue

@@ -164,7 +164,13 @@
             :placeholder="t('workflow.blocks.trigger.forms.shortcut')"
             :placeholder="t('workflow.blocks.trigger.forms.shortcut')"
           />
           />
           <ui-button
           <ui-button
-            v-tooltip="t('workflow.blocks.trigger.shortcut.tooltip')"
+            v-tooltip="
+              t(
+                `workflow.blocks.trigger.shortcut.${
+                  recordKeys.isRecording ? 'stopRecord' : 'tooltip'
+                }`
+              )
+            "
             icon
             icon
             @click="toggleRecordKeys"
             @click="toggleRecordKeys"
           >
           >
@@ -213,6 +219,7 @@ const triggers = [
   'interval',
   'interval',
   'date',
   'date',
   'specific-day',
   'specific-day',
+  'on-startup',
   'visit-web',
   'visit-web',
   'keyboard-shortcut',
   'keyboard-shortcut',
 ];
 ];

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

@@ -15,7 +15,12 @@
     <button v-else title="Execute" @click="$emit('execute', workflow)">
     <button v-else title="Execute" @click="$emit('execute', workflow)">
       <v-remixicon name="riPlayLine" />
       <v-remixicon name="riPlayLine" />
     </button>
     </button>
-    <ui-popover class="h-6">
+    <v-remixicon
+      v-if="workflow.isProtected"
+      name="riShieldKeyholeLine"
+      class="text-green-600"
+    />
+    <ui-popover v-else class="h-6">
       <template #trigger>
       <template #trigger>
         <button>
         <button>
           <v-remixicon name="riMoreLine" />
           <v-remixicon name="riMoreLine" />

+ 44 - 35
src/components/ui/UiInput.vue

@@ -1,48 +1,50 @@
 <template>
 <template>
   <div class="inline-block input-ui">
   <div class="inline-block input-ui">
-    <label class="relative">
-      <span
-        v-if="label || $slots.label"
-        class="text-sm dark:text-gray-200 text-gray-600 mb-1 ml-1"
-      >
+    <label v-if="label || $slots.label" :for="componentId">
+      <span class="text-sm dark:text-gray-200 text-gray-600 mb-1 ml-1">
         <slot name="label">{{ label }}</slot>
         <slot name="label">{{ label }}</slot>
       </span>
       </span>
-      <div class="flex items-center">
-        <slot name="prepend">
-          <v-remixicon
-            v-if="prependIcon"
-            class="ml-2 dark:text-gray-200 text-gray-600 absolute left-0"
-            :name="prependIcon"
-          ></v-remixicon>
-        </slot>
-        <input
-          v-autofocus="autofocus"
-          v-bind="{
-            readonly: disabled || readonly || null,
-            placeholder,
-            type,
-            autofocus,
-            min,
-            max,
-            list,
-          }"
-          :class="{
+    </label>
+    <div class="flex items-center relative w-full">
+      <slot name="prepend">
+        <v-remixicon
+          v-if="prependIcon"
+          class="ml-2 dark:text-gray-200 text-gray-600 absolute left-0"
+          :name="prependIcon"
+        ></v-remixicon>
+      </slot>
+      <input
+        v-bind="{
+          readonly: disabled || readonly || null,
+          placeholder,
+          type,
+          autofocus,
+          min,
+          max,
+          list,
+        }"
+        :id="componentId"
+        v-autofocus="autofocus"
+        :class="[
+          inputClass,
+          {
             'opacity-75 pointer-events-none': disabled,
             'opacity-75 pointer-events-none': disabled,
             'pl-10': prependIcon || $slots.prepend,
             'pl-10': prependIcon || $slots.prepend,
             'appearance-none': list,
             'appearance-none': list,
-          }"
-          :value="modelValue"
-          class="py-2 px-4 rounded-lg w-full bg-input bg-transparent transition"
-          @keydown="$emit('keydown', $event)"
-          @blur="$emit('blur', $event)"
-          @input="emitValue"
-        />
-        <slot name="append" />
-      </div>
-    </label>
+          },
+        ]"
+        :value="modelValue"
+        class="py-2 px-4 rounded-lg w-full bg-input bg-transparent transition"
+        @keydown="$emit('keydown', $event)"
+        @blur="$emit('blur', $event)"
+        @input="emitValue"
+      />
+      <slot name="append" />
+    </div>
   </div>
   </div>
 </template>
 </template>
 <script>
 <script>
+import { useComponentId } from '@/composable/componentId';
 /* eslint-disable vue/require-prop-types */
 /* eslint-disable vue/require-prop-types */
 export default {
 export default {
   props: {
   props: {
@@ -65,6 +67,10 @@ export default {
       type: [String, Number],
       type: [String, Number],
       default: '',
       default: '',
     },
     },
+    inputClass: {
+      type: String,
+      default: '',
+    },
     prependIcon: {
     prependIcon: {
       type: String,
       type: String,
       default: '',
       default: '',
@@ -96,6 +102,8 @@ export default {
   },
   },
   emits: ['update:modelValue', 'change', 'keydown', 'blur'],
   emits: ['update:modelValue', 'change', 'keydown', 'blur'],
   setup(props, { emit }) {
   setup(props, { emit }) {
+    const componentId = useComponentId('ui-input');
+
     function emitValue(event) {
     function emitValue(event) {
       let { value } = event.target;
       let { value } = event.target;
 
 
@@ -111,6 +119,7 @@ export default {
 
 
     return {
     return {
       emitValue,
       emitValue,
+      componentId,
     };
     };
   },
   },
 };
 };

+ 1 - 1
src/content/blocks-handler/handler-event-click.js

@@ -7,7 +7,7 @@ function eventClick(block) {
         if (element.click) {
         if (element.click) {
           element.click();
           element.click();
         } else {
         } else {
-          element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+          element.dispatchEvent(new PointerEvent('click', { bubbles: true }));
         }
         }
       },
       },
       onError(error) {
       onError(error) {

+ 2 - 1
src/content/blocks-handler/handler-get-text.js

@@ -10,6 +10,7 @@ function getText(block) {
       prefixText,
       prefixText,
       suffixText,
       suffixText,
       multiple,
       multiple,
+      includeTags,
     } = block.data;
     } = block.data;
 
 
     if (regexData) {
     if (regexData) {
@@ -18,7 +19,7 @@ function getText(block) {
 
 
     handleElement(block, {
     handleElement(block, {
       onSelected(element) {
       onSelected(element) {
-        let text = element.innerText;
+        let text = includeTags ? element.outerHTML : element.innerText;
 
 
         if (regex) text = text.match(regex).join(' ');
         if (regex) text = text.match(regex).join(' ');
 
 

+ 139 - 0
src/content/blocks-handler/handler-take-screenshot.js

@@ -0,0 +1,139 @@
+/* eslint-disable no-await-in-loop */
+import { sendMessage } from '@/utils/message';
+
+function findScrollableElement(
+  element = document.documentElement,
+  maxDepth = 5
+) {
+  if (maxDepth === 0) return null;
+
+  const excludeTags = ['SCRIPT', 'STYLE', 'SVG', 'HEAD'];
+  const isScrollable = element.scrollHeight > window.innerHeight;
+
+  if (isScrollable) return element;
+
+  for (let index = 0; index < element.childElementCount; index += 1) {
+    const currentChild = element.children.item(index);
+    const isExcluded =
+      currentChild.tagName.includes('-') ||
+      excludeTags.includes(currentChild.tagName);
+
+    if (!isExcluded) {
+      const scrollableElement = findScrollableElement(
+        currentChild,
+        maxDepth - 1
+      );
+
+      if (scrollableElement) return scrollableElement;
+    }
+  }
+
+  return null;
+}
+function injectStyle() {
+  const style = document.createElement('style');
+  style.innerText =
+    'html::-webkit-scrollbar, body::-webkit-scrollbar, .automa-scrollable-el::-webkit-scrollbar{ width: 0 !important; height: 0 !important } body.is-screenshotting [is-sticky] { position: relative !important; } .hide-fixed [is-fixed] {visibility: hidden !important; opacity: 0 !important;}';
+  style.id = 'automa-css-scroll';
+  document.body.appendChild(style);
+
+  return style;
+}
+
+const loadAsyncImg = (src) =>
+  new Promise((resolve) => {
+    const image = new Image();
+    image.onload = () => {
+      resolve(image);
+    };
+    image.src = src;
+  });
+
+export default async function ({ tabId, options }) {
+  document.body.classList.add('is-screenshotting');
+
+  const canvas = document.createElement('canvas');
+  const context = canvas.getContext('2d');
+  const maxCanvasSize = 32767;
+
+  const scrollableElement = findScrollableElement();
+  const takeScreenshot = async () => {
+    await sendMessage('set:active-tab', tabId, 'background');
+    const imageUrl = await sendMessage(
+      'get:tab-screenshot',
+      options,
+      'background'
+    );
+
+    return imageUrl;
+  };
+
+  if (!scrollableElement) {
+    const imageUrl = await takeScreenshot();
+
+    return imageUrl;
+  }
+
+  scrollableElement.classList.add('automa-scrollable-el');
+
+  const style = injectStyle();
+  const originalYPosition = window.scrollY;
+  const originalScrollHeight = scrollableElement.scrollHeight;
+
+  canvas.height =
+    scrollableElement.scrollHeight > maxCanvasSize
+      ? maxCanvasSize
+      : scrollableElement.scrollHeight;
+  canvas.width = window.innerWidth;
+
+  document.body
+    .querySelectorAll('*:not([is-sticky], [is-fixed])')
+    .forEach((el) => {
+      const { position } = getComputedStyle(el);
+
+      if (position === 'sticky') el.setAttribute('is-sticky', '');
+      else if (position === 'fixed') el.setAttribute('is-fixed', '');
+    });
+
+  let scrollPosition = 0;
+
+  while (scrollPosition <= originalScrollHeight) {
+    const imageUrl = await takeScreenshot();
+
+    if (scrollPosition > 0 && !document.body.classList.contains('hide-fixed')) {
+      document.body.classList.add('hide-fixed');
+    }
+
+    const image = await loadAsyncImg(imageUrl);
+    const newScrollPos = scrollPosition + window.innerHeight;
+
+    if (newScrollPos - originalScrollHeight > 0) {
+      context.drawImage(
+        image,
+        0,
+        newScrollPos - originalScrollHeight,
+        image.width,
+        image.height,
+        0,
+        scrollPosition,
+        image.width,
+        image.height
+      );
+    } else {
+      context.drawImage(image, 0, scrollPosition);
+    }
+
+    scrollPosition = newScrollPos;
+    scrollableElement.scrollTo(0, newScrollPos);
+
+    await new Promise((resolve) => setTimeout(resolve, 1000));
+  }
+
+  style.remove();
+  document.body.classList.remove('hide-fixed');
+  document.body.classList.remove('is-screenshotting');
+
+  scrollableElement.scrollTo(0, originalYPosition);
+
+  return canvas.toDataURL(`image/${options.format}`, options.quality / 100);
+}

+ 15 - 11
src/content/index.js

@@ -35,17 +35,21 @@ import blocksHandler from './blocks-handler';
         return;
         return;
       }
       }
 
 
-      if (data.type === 'content-script-exists') {
-        resolve(true);
-      } else if (data.type === 'select-element') {
-        elementSelector();
-        resolve(true);
-      } else if (data.type === 'give-me-the-frame-id') {
-        browser.runtime.sendMessage({
-          type: 'this-is-the-frame-id',
-        });
-
-        resolve();
+      switch (data.type) {
+        case 'content-script-exists':
+          resolve(true);
+          break;
+        case 'select-element':
+          elementSelector();
+          resolve(true);
+          break;
+        case 'give-me-the-frame-id':
+          browser.runtime.sendMessage({
+            type: 'this-is-the-frame-id',
+          });
+          resolve();
+          break;
+        default:
       }
       }
     });
     });
   });
   });

+ 2 - 13
src/content/services/shortcut-listener.js

@@ -6,17 +6,6 @@ Mousetrap.prototype.stopCallback = function () {
   return false;
   return false;
 };
 };
 
 
-function getTriggerBlock(workflow) {
-  const drawflow = JSON.parse(workflow?.drawflow || '{}');
-
-  if (!drawflow?.drawflow?.Home?.data) return null;
-
-  const blocks = Object.values(drawflow.drawflow.Home.data);
-  const trigger = blocks.find(({ name }) => name === 'trigger');
-
-  return trigger;
-}
-
 (async () => {
 (async () => {
   try {
   try {
     const { shortcuts, workflows } = await browser.storage.local.get([
     const { shortcuts, workflows } = await browser.storage.local.get([
@@ -28,12 +17,12 @@ function getTriggerBlock(workflow) {
     if (shortcutsArr.length === 0) return;
     if (shortcutsArr.length === 0) return;
 
 
     const keyboardShortcuts = shortcutsArr.reduce((acc, [id, value]) => {
     const keyboardShortcuts = shortcutsArr.reduce((acc, [id, value]) => {
-      const workflow = [...workflows].find((item) => item.id === id);
+      const workflow = workflows.find((item) => item.id === id);
 
 
       (acc[value] = acc[value] || []).push({
       (acc[value] = acc[value] || []).push({
         id,
         id,
         workflow,
         workflow,
-        activeInInput: getTriggerBlock(workflow)?.data?.activeInInput,
+        activeInInput: workflow.trigger?.activeInInput || false,
       });
       });
 
 
       return acc;
       return acc;

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

@@ -22,6 +22,7 @@ import {
   riRecordCircleLine,
   riRecordCircleLine,
   riErrorWarningLine,
   riErrorWarningLine,
   riEyeLine,
   riEyeLine,
+  riEyeOffLine,
   riCalendarLine,
   riCalendarLine,
   riFileTextLine,
   riFileTextLine,
   riFilter2Line,
   riFilter2Line,
@@ -104,6 +105,7 @@ export const icons = {
   riRecordCircleLine,
   riRecordCircleLine,
   riErrorWarningLine,
   riErrorWarningLine,
   riEyeLine,
   riEyeLine,
+  riEyeOffLine,
   riCalendarLine,
   riCalendarLine,
   riFileTextLine,
   riFileTextLine,
   riFilter2Line,
   riFilter2Line,

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

@@ -10,6 +10,7 @@
   "workflow": {
   "workflow": {
     "blocks": {
     "blocks": {
       "base": {
       "base": {
+        "moveToGroup": "Move block to blocks group",
         "selector": "Element selector",
         "selector": "Element selector",
         "findElement": {
         "findElement": {
           "placeholder": "Find element by",
           "placeholder": "Find element by",
@@ -66,6 +67,7 @@
         "useRegex": "Use regex",
         "useRegex": "Use regex",
         "shortcut": {
         "shortcut": {
           "tooltip": "Record shortcut",
           "tooltip": "Record shortcut",
+          "stopRecord": "Stop record",
           "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"
@@ -85,6 +87,7 @@
           "date": "On a specific date",
           "date": "On a specific date",
           "specific-day": "On a specific day",
           "specific-day": "On a specific day",
           "visit-web": "When visit a website",
           "visit-web": "When visit a website",
+          "on-startup": "On browser startup",
           "keyboard-shortcut": "Keyboard shortcut"
           "keyboard-shortcut": "Keyboard shortcut"
         }
         }
       },
       },
@@ -92,6 +95,7 @@
         "name": "Execute workflow",
         "name": "Execute workflow",
         "overwriteNote": "This will overwrite the global data of the selected workflow",
         "overwriteNote": "This will overwrite the global data of the selected workflow",
         "select": "Select workflow",
         "select": "Select workflow",
+        "executeId": "Execute Id",
         "description": ""
         "description": ""
       },
       },
       "google-sheets": {
       "google-sheets": {
@@ -187,6 +191,7 @@
         "name": "Get text",
         "name": "Get text",
         "description": "Get text from an element",
         "description": "Get text from an element",
         "checkbox": "Save data",
         "checkbox": "Save data",
+        "includeTags": "Include HTML tags",
         "prefixText": {
         "prefixText": {
           "placeholder": "Text prefix",
           "placeholder": "Text prefix",
           "title": "Add prefix to the text"
           "title": "Add prefix to the text"
@@ -387,6 +392,7 @@
       },
       },
       "take-screenshot": {
       "take-screenshot": {
         "name": "Take screenshot",
         "name": "Take screenshot",
+        "fullPage": "Take full page 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",
         "saveToColumn": "Save screenshot to column",

+ 2 - 1
src/locales/en/common.json

@@ -32,7 +32,8 @@
     "enable": "Enable",
     "enable": "Enable",
     "fallback": "Fallback",
     "fallback": "Fallback",
     "update": "Update",
     "update": "Update",
-    "duplicate": "Duplicate"
+    "duplicate": "Duplicate",
+    "password": "Password"
   },
   },
   "message": {
   "message": {
     "noBlock": "No block",
     "noBlock": "No block",

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

@@ -31,6 +31,20 @@
     "add": "Add workflow",
     "add": "Add workflow",
     "clickToEnable": "Click to enable",
     "clickToEnable": "Click to enable",
     "toggleSidebar": "Toggle sidebar",
     "toggleSidebar": "Toggle sidebar",
+    "protect": {
+      "title": "Protect workflow",
+      "remove": "Remove protection",
+      "button": "Protect",
+      "note": "Note: you must remember this password, this password will be required to edit and delete the workflow later on."
+    },
+    "locked": {
+      "title": "This Workflow is Protected",
+      "body": "Input the password to unlock it",
+      "unlock": "Unlock",
+      "messages": {
+        "incorrect-password": "Incorrect password"
+      }
+    },
     "state": {
     "state": {
       "executeBy": "Executed by: \"{name}\""
       "executeBy": "Executed by: \"{name}\""
     },
     },
@@ -112,9 +126,10 @@
       "empty-spreadsheet-id": "Spreadsheet Id is empty",
       "empty-spreadsheet-id": "Spreadsheet Id is empty",
       "invalid-loop-data": "Invalid data to loop through",
       "invalid-loop-data": "Invalid data to loop through",
       "empty-workflow": "You must select a workflow first",
       "empty-workflow": "You must select a workflow first",
-      "empty-spreadsheet-range": "Spreadsheet range is empty",
       "active-tab-removed": "Workflow active tab is removed",
       "active-tab-removed": "Workflow active tab is removed",
+      "empty-spreadsheet-range": "Spreadsheet range is empty",
       "stop-timeout": "Workflow is stopped because of timeout",
       "stop-timeout": "Workflow is stopped because of timeout",
+      "no-file-access": "Automa doesn't have access to the file",
       "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",
       "not-iframe": "Element with \"{selector}\" selector is not an Iframe element",

+ 3 - 0
src/models/workflow.js

@@ -19,6 +19,9 @@ class Workflow extends Model {
       drawflow: this.attr(''),
       drawflow: this.attr(''),
       dataColumns: this.attr([]),
       dataColumns: this.attr([]),
       description: this.string(''),
       description: this.string(''),
+      pass: this.string(''),
+      trigger: this.attr(null),
+      isProtected: this.boolean(false),
       version: this.string(''),
       version: this.string(''),
       globalData: this.string('[{ "key": "value" }]'),
       globalData: this.string('[{ "key": "value" }]'),
       createdAt: this.number(),
       createdAt: this.number(),

+ 0 - 3
src/newtab/App.vue

@@ -33,7 +33,6 @@ import { compare } from 'compare-versions';
 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 { t } = useI18n();
 const { t } = useI18n();
@@ -81,8 +80,6 @@ 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;

+ 6 - 1
src/newtab/pages/Workflows.vue

@@ -103,7 +103,12 @@
             >
             >
               <v-remixicon name="riPlayLine" />
               <v-remixicon name="riPlayLine" />
             </button>
             </button>
-            <ui-popover class="h-6 ml-2">
+            <v-remixicon
+              v-if="workflow.isProtected"
+              name="riShieldKeyholeLine"
+              class="text-green-600 ml-2"
+            />
+            <ui-popover v-if="!workflow.isProtected" class="h-6 ml-2">
               <template #trigger>
               <template #trigger>
                 <button>
                 <button>
                   <v-remixicon name="riMoreLine" />
                   <v-remixicon name="riMoreLine" />

+ 124 - 17
src/newtab/pages/workflows/[id].vue

@@ -1,5 +1,39 @@
 <template>
 <template>
-  <div class="flex h-screen">
+  <div v-if="protectionState.needed" class="my-12 mx-auto max-w-md w-full">
+    <div class="inline-block p-4 bg-green-200 mb-4 rounded-full">
+      <v-remixicon name="riShieldKeyholeLine" size="52" />
+    </div>
+    <h1 class="text-2xl font-semibold">
+      {{ t('workflow.locked.title') }}
+    </h1>
+    <p class="text-gray-600 text-lg">{{ t('workflow.locked.body') }}</p>
+    <form class="flex items-center mt-6" @submit.prevent="unlockWorkflow">
+      <ui-input
+        v-model="protectionState.password"
+        :placeholder="t('common.password')"
+        :type="protectionState.showPassword ? 'text' : 'password'"
+        autofocus
+        class="flex-1 mr-4"
+      >
+        <template #append>
+          <v-remixicon
+            :name="protectionState.showPassword ? 'riEyeOffLine' : 'riEyeLine'"
+            class="absolute right-2"
+            @click="
+              protectionState.showPassword = !protectionState.showPassword
+            "
+          />
+        </template>
+      </ui-input>
+      <ui-button variant="accent">
+        {{ t('workflow.locked.unlock') }}
+      </ui-button>
+    </form>
+    <p v-if="protectionState.message" class="ml-2 text-red-500">
+      {{ t(`workflow.locked.messages.${protectionState.message}`) }}
+    </p>
+  </div>
+  <div v-else class="flex h-screen">
     <div
     <div
       v-if="state.showSidebar"
       v-if="state.showSidebar"
       class="w-80 bg-white py-6 relative border-l border-gray-100 flex flex-col"
       class="w-80 bg-white py-6 relative border-l border-gray-100 flex flex-col"
@@ -55,19 +89,21 @@
           :is-data-changed="state.isDataChanged"
           :is-data-changed="state.isDataChanged"
           @showModal="(state.modalName = $event), (state.showModal = true)"
           @showModal="(state.modalName = $event), (state.showModal = true)"
           @save="saveWorkflow"
           @save="saveWorkflow"
-          @export="exportWorkflow(workflow)"
+          @export="workflowExporter"
           @execute="executeWorkflow"
           @execute="executeWorkflow"
           @rename="renameWorkflow"
           @rename="renameWorkflow"
           @update="updateWorkflow"
           @update="updateWorkflow"
           @delete="deleteWorkflow"
           @delete="deleteWorkflow"
+          @protect="toggleProtection"
         />
         />
       </div>
       </div>
       <keep-alive>
       <keep-alive>
         <workflow-builder
         <workflow-builder
-          v-if="activeTab === 'editor'"
+          v-if="activeTab === 'editor' && state.drawflow !== null"
           class="h-full w-full"
           class="h-full w-full"
-          :data="workflow.drawflow"
+          :data="state.drawflow"
           :version="workflow.version"
           :version="workflow.version"
+          @save="saveWorkflow"
           @update="updateWorkflow"
           @update="updateWorkflow"
           @load="editor = $event"
           @load="editor = $event"
           @deleteBlock="deleteBlock"
           @deleteBlock="deleteBlock"
@@ -165,18 +201,21 @@ import { useToast } from 'vue-toastification';
 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 defu from 'defu';
 import defu from 'defu';
+import AES from 'crypto-js/aes';
 import emitter from '@/lib/mitt';
 import emitter from '@/lib/mitt';
 import { useDialog } from '@/composable/dialog';
 import { useDialog } from '@/composable/dialog';
 import { useShortcut } from '@/composable/shortcut';
 import { useShortcut } from '@/composable/shortcut';
-import { tasks } from '@/utils/shared';
 import { sendMessage } from '@/utils/message';
 import { sendMessage } from '@/utils/message';
 import { debounce, isObject } from '@/utils/helper';
 import { debounce, isObject } from '@/utils/helper';
 import { exportWorkflow } from '@/utils/workflow-data';
 import { exportWorkflow } from '@/utils/workflow-data';
+import { tasks } from '@/utils/shared';
 import Log from '@/models/log';
 import Log from '@/models/log';
+import decryptFlow, { getWorkflowPass } from '@/utils/decrypt-flow';
 import Workflow from '@/models/workflow';
 import Workflow from '@/models/workflow';
 import workflowTrigger from '@/utils/workflow-trigger';
 import workflowTrigger from '@/utils/workflow-trigger';
 import WorkflowActions from '@/components/newtab/workflow/WorkflowActions.vue';
 import WorkflowActions from '@/components/newtab/workflow/WorkflowActions.vue';
 import WorkflowBuilder from '@/components/newtab/workflow/WorkflowBuilder.vue';
 import WorkflowBuilder from '@/components/newtab/workflow/WorkflowBuilder.vue';
+import WorkflowProtect from '@/components/newtab/workflow/WorkflowProtect.vue';
 import WorkflowSettings from '@/components/newtab/workflow/WorkflowSettings.vue';
 import WorkflowSettings from '@/components/newtab/workflow/WorkflowSettings.vue';
 import WorkflowEditBlock from '@/components/newtab/workflow/WorkflowEditBlock.vue';
 import WorkflowEditBlock from '@/components/newtab/workflow/WorkflowEditBlock.vue';
 import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
 import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
@@ -205,6 +244,11 @@ const workflowModals = {
     component: WorkflowGlobalData,
     component: WorkflowGlobalData,
     title: t('common.globalData'),
     title: t('common.globalData'),
   },
   },
+  'protect-workflow': {
+    icon: 'riShieldKeyholeLine',
+    component: WorkflowProtect,
+    title: t('workflow.protect.title'),
+  },
   settings: {
   settings: {
     icon: 'riSettings3Line',
     icon: 'riSettings3Line',
     component: WorkflowSettings,
     component: WorkflowSettings,
@@ -217,6 +261,7 @@ const activeTab = shallowRef('editor');
 const state = reactive({
 const state = reactive({
   blockData: {},
   blockData: {},
   modalName: '',
   modalName: '',
+  drawflow: null,
   showModal: false,
   showModal: false,
   showSidebar: true,
   showSidebar: true,
   isEditBlock: false,
   isEditBlock: false,
@@ -227,6 +272,12 @@ const renameModal = reactive({
   name: '',
   name: '',
   description: '',
   description: '',
 });
 });
+const protectionState = reactive({
+  message: '',
+  password: '',
+  needed: false,
+  showPassword: false,
+});
 
 
 const workflowState = computed(() =>
 const workflowState = computed(() =>
   store.getters.getWorkflowState(workflowId)
   store.getters.getWorkflowState(workflowId)
@@ -268,6 +319,50 @@ function deleteLog(logId) {
     store.dispatch('saveToStorage', 'logs');
     store.dispatch('saveToStorage', 'logs');
   });
   });
 }
 }
+function toggleProtection() {
+  if (workflow.value.isProtected) {
+    const decryptedFlow = decryptFlow(
+      workflow.value,
+      getWorkflowPass(workflow.value.pass)
+    );
+
+    updateWorkflow({
+      pass: '',
+      isProtected: false,
+      drawflow: decryptedFlow,
+    });
+  } else {
+    state.showModal = true;
+    state.modalName = 'protect-workflow';
+  }
+}
+function workflowExporter() {
+  const currentWorkflow = { ...workflow.value };
+
+  if (currentWorkflow.isProtected) {
+    currentWorkflow.drawflow = decryptFlow(
+      workflow.value,
+      getWorkflowPass(workflow.value.pass)
+    );
+    delete currentWorkflow.isProtected;
+  }
+
+  exportWorkflow(currentWorkflow);
+}
+function unlockWorkflow() {
+  protectionState.message = '';
+
+  const decryptedFlow = decryptFlow(workflow.value, protectionState.password);
+
+  if (decryptedFlow.isError) {
+    protectionState.message = decryptedFlow.message;
+    return;
+  }
+
+  state.drawflow = decryptedFlow;
+  protectionState.password = '';
+  protectionState.needed = false;
+}
 function toggleSidebar() {
 function toggleSidebar() {
   state.showSidebar = !state.showSidebar;
   state.showSidebar = !state.showSidebar;
   localStorage.setItem('workflow:sidebar', state.showSidebar);
   localStorage.setItem('workflow:sidebar', state.showSidebar);
@@ -304,21 +399,26 @@ function updateNameAndDesc() {
     });
     });
   });
   });
 }
 }
-function saveWorkflow() {
-  const data = editor.value.export();
-
-  updateWorkflow({ drawflow: JSON.stringify(data) }).then(() => {
+async function saveWorkflow() {
+  try {
+    let flow = JSON.stringify(editor.value.export());
     const [triggerBlockId] = editor.value.getNodesFromName('trigger');
     const [triggerBlockId] = editor.value.getNodesFromName('trigger');
+    const triggerBlock = editor.value.getNodeFromId(triggerBlockId);
 
 
-    if (triggerBlockId) {
-      workflowTrigger.register(
-        workflowId,
-        editor.value.getNodeFromId(triggerBlockId)
-      );
+    if (workflow.value.isProtected) {
+      flow = AES.encrypt(flow, getWorkflowPass(workflow.value.pass)).toString();
     }
     }
 
 
-    state.isDataChanged = false;
-  });
+    updateWorkflow({ drawflow: flow, trigger: triggerBlock?.data }).then(() => {
+      if (triggerBlock) {
+        workflowTrigger.register(workflowId, triggerBlock);
+      }
+
+      state.isDataChanged = false;
+    });
+  } catch (error) {
+    console.error(error);
+  }
 }
 }
 function editBlock(data) {
 function editBlock(data) {
   state.isEditBlock = true;
   state.isEditBlock = true;
@@ -333,8 +433,8 @@ function executeWorkflow() {
 
 
   const payload = {
   const payload = {
     ...workflow.value,
     ...workflow.value,
-    drawflow: editor.value.export(),
     isTesting: state.isDataChanged,
     isTesting: state.isDataChanged,
+    drawflow: JSON.stringify(editor.value.export()),
   };
   };
 
 
   sendMessage('workflow:execute', payload, 'background');
   sendMessage('workflow:execute', payload, 'background');
@@ -384,6 +484,13 @@ onMounted(() => {
 
 
   if (!isWorkflowExists) {
   if (!isWorkflowExists) {
     router.push('/workflows');
     router.push('/workflows');
+    return;
+  }
+
+  if (workflow.value.isProtected) {
+    protectionState.needed = true;
+  } else {
+    state.drawflow = workflow.value.drawflow;
   }
   }
 
 
   state.showSidebar =
   state.showSidebar =

+ 29 - 0
src/utils/decrypt-flow.js

@@ -0,0 +1,29 @@
+import { nanoid } from 'nanoid';
+import hmacSHA256 from 'crypto-js/hmac-sha256';
+import AES from 'crypto-js/aes';
+import encUtf8 from 'crypto-js/enc-utf8';
+import { parseJSON } from './helper';
+import getPassKey from './get-pass-key';
+
+export function getWorkflowPass(pass) {
+  const key = getPassKey(nanoid());
+  const decryptedPass = AES.decrypt(pass.substring(64), key).toString(encUtf8);
+
+  return decryptedPass;
+}
+
+export default function ({ pass, drawflow }, password) {
+  const hmac = pass.substring(0, 64);
+  const decryptedHmac = hmacSHA256(pass.substring(64), password).toString();
+
+  if (hmac !== decryptedHmac)
+    return {
+      isError: true,
+      message: 'incorrect-password',
+    };
+
+  const isDecrypted = parseJSON(drawflow, null);
+  if (isDecrypted) return isDecrypted;
+
+  return AES.decrypt(drawflow, password).toString(encUtf8);
+}

+ 12 - 15
src/utils/reference-data/index.js

@@ -1,4 +1,3 @@
-import { set as setObjectPath } from 'object-path-immutable';
 import dayjs from '@/lib/dayjs';
 import dayjs from '@/lib/dayjs';
 import { objectHasKey } from '@/utils/helper';
 import { objectHasKey } from '@/utils/helper';
 import mustacheReplacer from './mustache-replacer';
 import mustacheReplacer from './mustache-replacer';
@@ -35,24 +34,22 @@ export const funcs = {
 export default function ({ block, refKeys, data: refData }) {
 export default function ({ block, refKeys, data: refData }) {
   if (!refKeys || refKeys.length === 0) return block;
   if (!refKeys || refKeys.length === 0) return block;
 
 
-  let replacedBlock = { ...block };
-  const data = Object.assign(refData, { funcs });
+  const data = { ...refData, funcs };
+  const copyBlock = JSON.parse(JSON.stringify(block));
 
 
   refKeys.forEach((blockDataKey) => {
   refKeys.forEach((blockDataKey) => {
     if (!objectHasKey(block.data, blockDataKey)) return;
     if (!objectHasKey(block.data, blockDataKey)) return;
 
 
-    const newDataValue = mustacheReplacer({
-      data,
-      block,
-      str: replacedBlock.data[blockDataKey],
-    });
-
-    replacedBlock = setObjectPath(
-      replacedBlock,
-      `data.${blockDataKey}`,
-      newDataValue
-    );
+    const currentData = copyBlock.data[blockDataKey];
+
+    if (Array.isArray(currentData)) {
+      currentData.forEach((str, index) => {
+        currentData[index] = mustacheReplacer(str, data);
+      });
+    } else {
+      copyBlock.data[blockDataKey] = mustacheReplacer(currentData, data);
+    }
   });
   });
 
 
-  return replacedBlock;
+  return copyBlock;
 }
 }

+ 10 - 12
src/utils/reference-data/key-parser.js

@@ -1,15 +1,14 @@
-import { objectHasKey } from '../helper';
-
 const refKeys = {
 const refKeys = {
   dataColumn: 'dataColumns',
   dataColumn: 'dataColumns',
   dataColumns: 'dataColumns',
   dataColumns: 'dataColumns',
 };
 };
 
 
 export default function (key) {
 export default function (key) {
-  /* eslint-disable-next-line */
-  let [dataKey, path] = key.split('@');
+  let [dataKey, path] = key.split(/@(.+)/);
+
+  dataKey = refKeys[dataKey] ?? dataKey;
 
 
-  dataKey = objectHasKey(refKeys, dataKey) ? refKeys[dataKey] : dataKey;
+  if (!path) return { dataKey, path: '' };
 
 
   if (dataKey !== 'dataColumns') {
   if (dataKey !== 'dataColumns') {
     if (dataKey === 'loopData' && !path.endsWith('.$index')) {
     if (dataKey === 'loopData' && !path.endsWith('.$index')) {
@@ -19,19 +18,18 @@ export default function (key) {
       path = pathArr.join('.');
       path = pathArr.join('.');
     }
     }
 
 
-    return { dataKey, path: path || '' };
+    return { dataKey, path };
   }
   }
 
 
-  const pathArr = path?.split('.') ?? [];
-  let dataPath = path;
+  const pathArr = path.split('.');
 
 
   if (pathArr.length === 1) {
   if (pathArr.length === 1) {
-    dataPath = `0.${pathArr[0]}`;
+    path = `0.${pathArr[0]}`;
   } else if (typeof +pathArr[0] !== 'number' || Number.isNaN(+pathArr[0])) {
   } else if (typeof +pathArr[0] !== 'number' || Number.isNaN(+pathArr[0])) {
-    dataPath = `0.${pathArr.join('.')}`;
+    path = `0.${pathArr.join('.')}`;
   }
   }
 
 
-  if (dataPath.endsWith('.')) dataPath = dataPath.slice(0, -1);
+  if (path.endsWith('.')) path = path.slice(0, -1);
 
 
-  return { dataKey: 'dataColumns', path: dataPath };
+  return { dataKey: 'dataColumns', path };
 }
 }

+ 3 - 7
src/utils/reference-data/mustache-replacer.js

@@ -1,5 +1,4 @@
 import { get as getObjectPath } from 'object-path-immutable';
 import { get as getObjectPath } from 'object-path-immutable';
-import { replaceMustache } from '../helper';
 import keyParser from './key-parser';
 import keyParser from './key-parser';
 
 
 export function extractStrFunction(str) {
 export function extractStrFunction(str) {
@@ -8,7 +7,6 @@ export function extractStrFunction(str) {
   if (!extractedStr) return null;
   if (!extractedStr) return null;
 
 
   const { 1: name, 2: funcParams } = extractedStr;
   const { 1: name, 2: funcParams } = extractedStr;
-
   const params = funcParams
   const params = funcParams
     .split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/)
     .split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/)
     .map((param) => param?.trim().replace(/^['"]|['"]$/g, '') || '');
     .map((param) => param?.trim().replace(/^['"]|['"]$/g, '') || '');
@@ -19,8 +17,8 @@ export function extractStrFunction(str) {
   };
   };
 }
 }
 
 
-export default function ({ str, data, block }) {
-  const replacedStr = replaceMustache(str, (match) => {
+export default function (str, data) {
+  const replacedStr = str.replace(/\{\{(.*?)\}\}/g, (match) => {
     const key = match.slice(2, -2).trim();
     const key = match.slice(2, -2).trim();
 
 
     if (!key) return '';
     if (!key) return '';
@@ -37,9 +35,7 @@ export default function ({ str, data, block }) {
       const { dataKey, path } = keyParser(key);
       const { dataKey, path } = keyParser(key);
       result = getObjectPath(data[dataKey], path) ?? match;
       result = getObjectPath(data[dataKey], path) ?? match;
     }
     }
-    if (block && block.name === 'webhook') {
-      return JSON.stringify(result);
-    }
+
     return typeof result === 'string' ? result : JSON.stringify(result);
     return typeof result === 'string' ? result : JSON.stringify(result);
   });
   });
 
 

+ 3 - 0
src/utils/shared.js

@@ -43,6 +43,7 @@ export const tasks = {
     refDataKeys: ['globalData'],
     refDataKeys: ['globalData'],
     data: {
     data: {
       workflowId: '',
       workflowId: '',
+      executeId: '',
       globalData: '',
       globalData: '',
     },
     },
   },
   },
@@ -176,6 +177,7 @@ export const tasks = {
       ext: 'png',
       ext: 'png',
       quality: 100,
       quality: 100,
       dataColumn: '',
       dataColumn: '',
+      fullPage: false,
       saveToColumn: false,
       saveToColumn: false,
       saveToComputer: true,
       saveToComputer: true,
       captureActiveTab: true,
       captureActiveTab: true,
@@ -261,6 +263,7 @@ export const tasks = {
       regexExp: ['g'],
       regexExp: ['g'],
       dataColumn: '',
       dataColumn: '',
       saveData: true,
       saveData: true,
+      includeTags: false,
       addExtraRow: false,
       addExtraRow: false,
       extraRowValue: '',
       extraRowValue: '',
       extraRowDataColumn: '',
       extraRowDataColumn: '',

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

@@ -1,15 +1,47 @@
-import { fileSaver, openFilePicker } from './helper';
+import { parseJSON, fileSaver, openFilePicker, isObject } from './helper';
 import Workflow from '@/models/workflow';
 import Workflow from '@/models/workflow';
 
 
 export function importWorkflow() {
 export function importWorkflow() {
   openFilePicker(['application/json'])
   openFilePicker(['application/json'])
     .then((file) => {
     .then((file) => {
       const reader = new FileReader();
       const reader = new FileReader();
+      const getDrawflow = ({ drawflow }) => {
+        if (isObject(drawflow)) return JSON.stringify(drawflow);
+
+        return drawflow;
+      };
 
 
       reader.onload = ({ target }) => {
       reader.onload = ({ target }) => {
         const workflow = JSON.parse(target.result);
         const workflow = JSON.parse(target.result);
 
 
-        Workflow.insert({ data: { ...workflow, createdAt: Date.now() } });
+        if (workflow.includedWorkflows) {
+          Object.keys(workflow.includedWorkflows).forEach((workflowId) => {
+            const isWorkflowExists = Workflow.query()
+              .where('id', workflowId)
+              .exists();
+
+            if (isWorkflowExists) return;
+
+            Workflow.insert({
+              data: {
+                ...workflow.includedWorkflows[workflowId],
+                drawflow: getDrawflow(workflow.includedWorkflows[workflowId]),
+                id: workflowId,
+                createdAt: Date.now(),
+              },
+            });
+          });
+
+          delete workflow.includedWorkflows;
+        }
+
+        Workflow.insert({
+          data: {
+            ...workflow,
+            drawflow: getDrawflow(workflow),
+            createdAt: Date.now(),
+          },
+        });
       };
       };
 
 
       reader.readAsText(file);
       reader.readAsText(file);
@@ -19,7 +51,9 @@ export function importWorkflow() {
     });
     });
 }
 }
 
 
-export function exportWorkflow(workflow) {
+function convertWorkflow(workflow) {
+  if (!workflow) return null;
+
   const keys = [
   const keys = [
     'name',
     'name',
     'icon',
     'icon',
@@ -38,6 +72,36 @@ export function exportWorkflow(workflow) {
     content[key] = workflow[key];
     content[key] = workflow[key];
   });
   });
 
 
+  return content;
+}
+function findIncludedWorkflows({ drawflow }, maxDepth = 3, workflows = {}) {
+  if (maxDepth === 0) return workflows;
+
+  const blocks = parseJSON(drawflow, null)?.drawflow.Home.data;
+
+  if (!blocks) return workflows;
+
+  Object.values(blocks).forEach(({ data, name }) => {
+    if (name !== 'execute-workflow' || workflows[data.workflowId]) return;
+
+    const workflow = Workflow.find(data.workflowId);
+
+    if (workflow && !workflow.isProtected) {
+      workflows[data.workflowId] = convertWorkflow(workflow);
+      findIncludedWorkflows(workflow, maxDepth - 1, workflows);
+    }
+  });
+
+  return workflows;
+}
+export function exportWorkflow(workflow) {
+  if (workflow.isProtected) return;
+
+  const includedWorkflows = findIncludedWorkflows(workflow);
+  const content = convertWorkflow(workflow);
+
+  content.includedWorkflows = includedWorkflows;
+
   const blob = new Blob([JSON.stringify(content)], {
   const blob = new Blob([JSON.stringify(content)], {
     type: 'application/json',
     type: 'application/json',
   });
   });

+ 26 - 5
src/utils/workflow-trigger.js

@@ -6,14 +6,22 @@ export async function cleanWorkflowTriggers(workflowId) {
   try {
   try {
     await browser.alarms.clear(workflowId);
     await browser.alarms.clear(workflowId);
 
 
-    const { visitWebTriggers, shortcuts } = await browser.storage.local.get([
-      'visitWebTriggers',
-      'shortcuts',
-    ]);
+    const { visitWebTriggers, onStartupTriggers, shortcuts } =
+      await browser.storage.local.get([
+        'shortcuts',
+        'visitWebTriggers',
+        'onStartupTriggers',
+      ]);
 
 
     const keyboardShortcuts = Array.isArray(shortcuts) ? {} : shortcuts || {};
     const keyboardShortcuts = Array.isArray(shortcuts) ? {} : shortcuts || {};
     delete keyboardShortcuts[workflowId];
     delete keyboardShortcuts[workflowId];
 
 
+    const startupTriggers = onStartupTriggers || [];
+    const startupTriggerIndex = startupTriggers.indexOf(workflowId);
+    if (startupTriggerIndex !== -1) {
+      startupTriggers.splice(startupTriggerIndex, 1);
+    }
+
     const visitWebTriggerIndex = visitWebTriggers.findIndex(
     const visitWebTriggerIndex = visitWebTriggers.findIndex(
       (item) => item.id === workflowId
       (item) => item.id === workflowId
     );
     );
@@ -24,6 +32,7 @@ export async function cleanWorkflowTriggers(workflowId) {
     await browser.storage.local.set({
     await browser.storage.local.set({
       visitWebTriggers,
       visitWebTriggers,
       shortcuts: keyboardShortcuts,
       shortcuts: keyboardShortcuts,
+      onStartupTriggers: startupTriggers,
     });
     });
   } catch (error) {
   } catch (error) {
     console.error(error);
     console.error(error);
@@ -126,14 +135,26 @@ export async function registerKeyboardShortcut(workflowId, data) {
   }
   }
 }
 }
 
 
+export async function registerOnStartup(workflowId) {
+  const { onStartupTriggers } = await browser.storage.local.get(
+    'onStartupTriggers'
+  );
+  const startupTriggers = onStartupTriggers || [];
+
+  startupTriggers.push(workflowId);
+
+  await browser.storage.local.set({ onStartupTriggers: startupTriggers });
+}
+
 export async function registerWorkflowTrigger(workflowId, { data }) {
 export async function registerWorkflowTrigger(workflowId, { data }) {
   try {
   try {
     await cleanWorkflowTriggers(workflowId);
     await cleanWorkflowTriggers(workflowId);
 
 
     const triggersHandler = {
     const triggersHandler = {
-      date: registerSpecificDate,
       interval: registerInterval,
       interval: registerInterval,
+      date: registerSpecificDate,
       'visit-web': registerVisitWeb,
       'visit-web': registerVisitWeb,
+      'on-startup': registerOnStartup,
       'specific-day': registerSpecificDay,
       'specific-day': registerSpecificDay,
       'keyboard-shortcut': registerKeyboardShortcut,
       'keyboard-shortcut': registerKeyboardShortcut,
     };
     };

+ 0 - 12
webpack.config.js

@@ -199,18 +199,6 @@ if (env.NODE_ENV === 'development') {
         extractComments: false,
         extractComments: false,
       }),
       }),
     ],
     ],
-    // runtimeChunk: 'single',
-    // splitChunks: {
-    //   chunks: 'all',
-    //   maxInitialRequests: Infinity,
-    //   minSize: 0,
-    //   cacheGroups: {
-    //     vendor: {
-    //       test: /[\\/]node_modules[\\/]/,
-    //       name: 'vendor',
-    //     },
-    //   },
-    // },
   };
   };
 }
 }
 
 

+ 41 - 36
yarn.lock

@@ -1229,29 +1229,29 @@
     source-map "^0.6.1"
     source-map "^0.6.1"
     yaml-eslint-parser "^0.3.2"
     yaml-eslint-parser "^0.3.2"
 
 
-"@intlify/core-base@9.2.0-beta.28":
-  version "9.2.0-beta.28"
-  resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.2.0-beta.28.tgz#e8b1e4adfa7a262c6ed169ad7b15dbe2a173cb27"
-  integrity sha512-p7iXwVQFyBmEo65KoqRCbT6Ig3OI6rnaS/zeMCKtp6Bjsbg35VGAaiN05Eyrq78BCh2Ir1S6nl+Cz3y00D0yoQ==
+"@intlify/core-base@9.2.0-beta.29":
+  version "9.2.0-beta.29"
+  resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.2.0-beta.29.tgz#2e14c998a75f90e86f193d821deb5e54c236ec6f"
+  integrity sha512-4HzfpBC0pBjud26vWXhy+HRrcJR+KsQPpGbIbS/FVbqeUTOX90+RsBC5HOpIVmdeVPpzQUyzKAZdj+hkTQYQ3Q==
   dependencies:
   dependencies:
-    "@intlify/devtools-if" "9.2.0-beta.28"
-    "@intlify/message-compiler" "9.2.0-beta.28"
-    "@intlify/shared" "9.2.0-beta.28"
-    "@intlify/vue-devtools" "9.2.0-beta.28"
+    "@intlify/devtools-if" "9.2.0-beta.29"
+    "@intlify/message-compiler" "9.2.0-beta.29"
+    "@intlify/shared" "9.2.0-beta.29"
+    "@intlify/vue-devtools" "9.2.0-beta.29"
 
 
-"@intlify/devtools-if@9.2.0-beta.28":
-  version "9.2.0-beta.28"
-  resolved "https://registry.yarnpkg.com/@intlify/devtools-if/-/devtools-if-9.2.0-beta.28.tgz#daca7b4348a59109778558e7f5769e5f6b422d4e"
-  integrity sha512-3RL38hDBRipipoYRl4Ggu98M4/XqDKm0jW8kcOWpuocB/aZBBEGzoQfeaq09Xa9SA46podjntBlYDAOGQyXqqg==
+"@intlify/devtools-if@9.2.0-beta.29":
+  version "9.2.0-beta.29"
+  resolved "https://registry.yarnpkg.com/@intlify/devtools-if/-/devtools-if-9.2.0-beta.29.tgz#cfb21294251a4faa2f3e769b6033691d821cdac7"
+  integrity sha512-AEF+K/VYTR1FkakCi8ZQOARcla2wOdX4khS5X4zEcUwl06VSaN0ilBnLX4727zcVU5jh8YkOM0uERLLyf2lxeg==
   dependencies:
   dependencies:
-    "@intlify/shared" "9.2.0-beta.28"
+    "@intlify/shared" "9.2.0-beta.29"
 
 
-"@intlify/message-compiler@9.2.0-beta.28":
-  version "9.2.0-beta.28"
-  resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.2.0-beta.28.tgz#caae08ead8c6c02e2d0de39e1e8bdbbb99683c83"
-  integrity sha512-NBH9fZyitN2cijGt8bmU1W7ZPdhKbgW01L1RxJKFJW0cRaCmknJq63Aif1Q6xcxKt9ZhPbvIKHgPGzg1nWMfeA==
+"@intlify/message-compiler@9.2.0-beta.29":
+  version "9.2.0-beta.29"
+  resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.2.0-beta.29.tgz#078a44a46ee08320d27e40278ba6708e9e6e0e23"
+  integrity sha512-FvMDwe57VvupujvNYUY90J8wv26wKu6j7I93dLwBOo/PTg7nQqFrmYQAF23UfDAdXO4FTdgHfFyb5ecYrN+n3g==
   dependencies:
   dependencies:
-    "@intlify/shared" "9.2.0-beta.28"
+    "@intlify/shared" "9.2.0-beta.29"
     source-map "0.6.1"
     source-map "0.6.1"
 
 
 "@intlify/message-compiler@beta":
 "@intlify/message-compiler@beta":
@@ -1267,18 +1267,18 @@
   resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.2.0-beta.25.tgz#a178975e77dcca59203f46269037ea1d4b858899"
   resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.2.0-beta.25.tgz#a178975e77dcca59203f46269037ea1d4b858899"
   integrity sha512-I2L05aWh0azr5KwQjLV7gMTN0SrdglgMAfpJniT53Pvvc8l+OTs8IEhdPCQwsbgOravpWt14O7m3deOzw3ln6w==
   integrity sha512-I2L05aWh0azr5KwQjLV7gMTN0SrdglgMAfpJniT53Pvvc8l+OTs8IEhdPCQwsbgOravpWt14O7m3deOzw3ln6w==
 
 
-"@intlify/shared@9.2.0-beta.28":
-  version "9.2.0-beta.28"
-  resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.2.0-beta.28.tgz#50bd3f769bcab6f80e00027761b3397e268f9d02"
-  integrity sha512-JBMcoj1D4kSAma7Vb0+d8z6lPLIn7hIdZJPxbU8bgeMMniwKLoIS/jGlEfrZihsB5+otckPeQp203z8skwVS0w==
+"@intlify/shared@9.2.0-beta.29":
+  version "9.2.0-beta.29"
+  resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.2.0-beta.29.tgz#447036dc9c085ba45aaaffb639d75485b9c858bf"
+  integrity sha512-blMW14WBr3fiCEk/XO4IbSxM8WMAhQOzEgWzP1aqbkeXbIMiHeyFI0ZexwyTKsvDZz0wEWlhupQi+9udrJsozA==
 
 
-"@intlify/vue-devtools@9.2.0-beta.28":
-  version "9.2.0-beta.28"
-  resolved "https://registry.yarnpkg.com/@intlify/vue-devtools/-/vue-devtools-9.2.0-beta.28.tgz#60113c137a380433961934045b9cf046c8773341"
-  integrity sha512-kf9Gt64sjP1fJQHUlB3m/RFDeJBcrvRImcEl6g0BV13K/xyA9u9RGM89YpR16F5KKTXdhpkvroLWh2uo4pc6jg==
+"@intlify/vue-devtools@9.2.0-beta.29":
+  version "9.2.0-beta.29"
+  resolved "https://registry.yarnpkg.com/@intlify/vue-devtools/-/vue-devtools-9.2.0-beta.29.tgz#52a1930f8f9acfde91aaa748e9272ebc5969e98e"
+  integrity sha512-VkeSxU4RLiY89MT5POET+szYJbfmRG1rR2Ndw7Rgqgl7UqiL4ayTdT/VN6I9lw41nK98deK0QXv/FqWmUOyJGg==
   dependencies:
   dependencies:
-    "@intlify/core-base" "9.2.0-beta.28"
-    "@intlify/shared" "9.2.0-beta.28"
+    "@intlify/core-base" "9.2.0-beta.29"
+    "@intlify/shared" "9.2.0-beta.29"
 
 
 "@intlify/vue-i18n-loader@^4.0.1":
 "@intlify/vue-i18n-loader@^4.0.1":
   version "4.1.0"
   version "4.1.0"
@@ -2659,6 +2659,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     shebang-command "^2.0.0"
     which "^2.0.1"
     which "^2.0.1"
 
 
+crypto-js@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf"
+  integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
+
 css-loader@5.2.7:
 css-loader@5.2.7:
   version "5.2.7"
   version "5.2.7"
   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.7.tgz#9b9f111edf6fb2be5dc62525644cbc9c232064ae"
   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.7.tgz#9b9f111edf6fb2be5dc62525644cbc9c232064ae"
@@ -2803,7 +2808,7 @@ defined@^1.0.0:
   resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
   resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
   integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
   integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
 
 
-defu@^5.0.0:
+defu@^5.0.1:
   version "5.0.1"
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/defu/-/defu-5.0.1.tgz#a034278f9b032bf0845d261aa75e9ad98da878ac"
   resolved "https://registry.yarnpkg.com/defu/-/defu-5.0.1.tgz#a034278f9b032bf0845d261aa75e9ad98da878ac"
   integrity sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==
   integrity sha512-EPS1carKg+dkEVy3qNTqIdp2qV7mUP08nIsupfwQpz++slCVRw7qbQyWvSTig+kFPwz2XXp5/kIIkH+CwrJKkQ==
@@ -6950,14 +6955,14 @@ vue-eslint-parser@^7.10.0:
     lodash "^4.17.21"
     lodash "^4.17.21"
     semver "^6.3.0"
     semver "^6.3.0"
 
 
-vue-i18n@^9.2.0-beta.20:
-  version "9.2.0-beta.28"
-  resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.2.0-beta.28.tgz#fcfa1d2deafb0914817fb338ed8e5deb54ba4e44"
-  integrity sha512-Jn7DHA3JgOYaB6ahqmuW0wQ2zZx0ivastVDUul8325geyT0Q4PblJvXvfWHi2L0eb+YjWMZvf30MQYJ1FWDlfQ==
+vue-i18n@^9.2.0-beta.29:
+  version "9.2.0-beta.29"
+  resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.2.0-beta.29.tgz#87c0bba8453f38380c0704847ad35cc57b224226"
+  integrity sha512-Cbs7qwlTXI/B5XjueGFLUYoS7Mh+9ZB3RgV1pQYYHVC1xPVbWDaskOB/YkLiyff2ZdssViX93xQ/KLkcZ3DhFw==
   dependencies:
   dependencies:
-    "@intlify/core-base" "9.2.0-beta.28"
-    "@intlify/shared" "9.2.0-beta.28"
-    "@intlify/vue-devtools" "9.2.0-beta.28"
+    "@intlify/core-base" "9.2.0-beta.29"
+    "@intlify/shared" "9.2.0-beta.29"
+    "@intlify/vue-devtools" "9.2.0-beta.29"
     "@vue/devtools-api" "^6.0.0-beta.13"
     "@vue/devtools-api" "^6.0.0-beta.13"
 
 
 vue-loader@16.8.1:
 vue-loader@16.8.1: