Ahmad Kholid 3 years ago
parent
commit
b27d54b020

+ 1 - 0
.eslintrc.js

@@ -30,6 +30,7 @@ module.exports = {
   // add your custom rules here
   rules: {
     'no-undef': 'off',
+    'no-console': ['warn', { allow: ['warn', 'error'] }],
     'no-underscore-dangle': 'off',
     'func-names': 'off',
     'import/extensions': [

+ 1 - 1
package.json

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

+ 4 - 0
src/assets/css/drawflow.css

@@ -124,6 +124,10 @@
   stroke: theme('colors.accent');
 }
 
+.drawflow-node .drawflow-delete {
+  display: none !important;
+}
+
 .drawflow-delete {
   position: absolute;
   display: block;

+ 8 - 1
src/background/collection-engine/flow-handler.js

@@ -13,12 +13,14 @@ export function workflow(flow) {
       return;
     }
 
+    const { globalData } = this.collection;
     this.currentWorkflow = currentWorkflow;
 
     const engine = new WorkflowEngine(currentWorkflow, {
       isInCollection: true,
       collectionLogId: this.id,
       collectionId: this.collection.id,
+      globalData: globalData.trim() === '' ? null : globalData,
     });
 
     this.workflowEngine = engine;
@@ -37,7 +39,12 @@ export function workflow(flow) {
         workflowName: currentWorkflow.name,
       });
 
-      resolve({ id, name: currentWorkflow.name, type: status, message });
+      resolve({
+        id,
+        message,
+        type: status,
+        name: currentWorkflow.name,
+      });
     });
   });
 }

+ 9 - 0
src/background/index.js

@@ -3,6 +3,7 @@ import { MessageListener } from '@/utils/message';
 import workflowState from './workflow-state';
 import WorkflowEngine from './workflow-engine';
 import CollectionEngine from './collection-engine';
+import { registerSpecificDay } from '../utils/workflow-trigger';
 
 function getWorkflow(workflowId) {
   return new Promise((resolve) => {
@@ -71,6 +72,14 @@ browser.alarms.onAlarm.addListener(({ name }) => {
     if (!workflow) return;
 
     executeWorkflow(workflow);
+
+    const triggerBlock = Object.values(
+      JSON.parse(workflow.drawflow).drawflow.Home.data
+    ).find((block) => block.name === 'trigger');
+
+    if (triggerBlock?.data.type === 'specific-day') {
+      registerSpecificDay(workflow.id, triggerBlock.data);
+    }
   });
 });
 

+ 5 - 2
src/background/workflow-engine/blocks-handler.js

@@ -23,6 +23,9 @@ function convertData(data, type) {
     case 'boolean':
       result = Boolean(data);
       break;
+    case 'array':
+      result = Array.from(data);
+      break;
     default:
   }
 
@@ -68,7 +71,7 @@ export async function trigger(block) {
   const nextBlockId = getBlockConnection(block);
   try {
     if (block.data.type === 'visit-web' && this.tabId) {
-      this.frames = executeContentScript(this.tabId, 'trigger');
+      this.frames = await executeContentScript(this.tabId, 'trigger');
     }
 
     return { nextBlockId, data: '' };
@@ -443,7 +446,7 @@ export async function interactionHandler(block) {
       const column = getColumn(block.data.dataColumn);
 
       if (block.data.saveData) {
-        if (Array.isArray(data)) {
+        if (Array.isArray(data) && column.type !== 'array') {
           data.forEach((item) => {
             pushData(column, item);
           });

+ 22 - 11
src/background/workflow-engine/index.js

@@ -1,8 +1,9 @@
 /* eslint-disable no-underscore-dangle */
 import browser from 'webextension-polyfill';
 import { nanoid } from 'nanoid';
-import { toCamelCase } from '@/utils/helper';
 import { tasks } from '@/utils/shared';
+import { toCamelCase, parseJSON } from '@/utils/helper';
+import { generateJSON } from '@/utils/data-exporter';
 import errorMessage from './error-message';
 import referenceData from '@/utils/reference-data';
 import workflowState from '../workflow-state';
@@ -16,7 +17,10 @@ function tabRemovedHandler(tabId) {
 
   delete this.tabId;
 
-  if (tasks[this.currentBlock.name].category === 'interaction') {
+  if (
+    this.currentBlock.name === 'new-tab' ||
+    tasks[this.currentBlock.name].category === 'interaction'
+  ) {
     this.destroy('error', 'Current active tab is removed');
   }
 
@@ -57,12 +61,18 @@ function tabUpdatedHandler(tabId, changeInfo) {
 }
 
 class WorkflowEngine {
-  constructor(workflow, { tabId = null, isInCollection, collectionLogId }) {
+  constructor(
+    workflow,
+    { globalData, tabId = null, isInCollection, collectionLogId }
+  ) {
+    const globalDataVal = globalData || workflow.globalData;
+
     this.id = nanoid();
     this.tabId = tabId;
     this.workflow = workflow;
     this.isInCollection = isInCollection;
     this.collectionLogId = collectionLogId;
+    this.globalData = parseJSON(globalDataVal, globalDataVal);
     this.data = {};
     this.logs = [];
     this.blocks = {};
@@ -174,6 +184,7 @@ class WorkflowEngine {
       if (!this.workflow.isTesting) {
         const { logs } = await browser.storage.local.get('logs');
         const { name, icon, id } = this.workflow;
+        const jsonData = generateJSON(Object.keys(this.data), this.data);
 
         logs.push({
           name,
@@ -181,7 +192,7 @@ class WorkflowEngine {
           status,
           id: this.id,
           workflowId: id,
-          data: this.data,
+          data: jsonData,
           history: this.logs,
           endedAt: this.endedTimestamp,
           startedAt: this.startedTimestamp,
@@ -262,6 +273,7 @@ class WorkflowEngine {
         prevBlockData,
         data: this.data,
         loopData: this.loopData,
+        globalData: this.globalData,
       });
 
       handler
@@ -269,15 +281,14 @@ class WorkflowEngine {
         .then((result) => {
           clearTimeout(this.workflowTimeout);
           this.workflowTimeout = null;
+          this.logs.push({
+            type: 'success',
+            name: tasks[block.name].name,
+            data: result.data,
+            duration: Math.round(Date.now() - started),
+          });
 
           if (result.nextBlockId) {
-            this.logs.push({
-              type: 'success',
-              name: tasks[block.name].name,
-              data: result.data,
-              duration: Math.round(Date.now() - started),
-            });
-
             this._blockHandler(this.blocks[result.nextBlockId], result.data);
           } else {
             this.logs.push({

+ 3 - 1
src/components/newtab/logs/LogsDataViewer.vue

@@ -23,7 +23,7 @@
     </ui-popover>
   </div>
   <prism-editor
-    :model-value="dataStr"
+    :model-value="jsonData"
     :highlight="highlighter('json')"
     :class="editorClass"
     readonly
@@ -52,6 +52,8 @@ const data = Array.isArray(props.log.data)
   ? props.log.data
   : generateJSON(Object.keys(props.log.data), props.log.data);
 const dataStr = JSON.stringify(data, null, 2);
+const jsonData =
+  dataStr.length >= 5e4 ? `${dataStr.slice(0, 5e4)}\n...` : dataStr;
 
 const fileName = ref(props.log.name);
 

+ 116 - 0
src/components/newtab/workflow/WorkflowActions.vue

@@ -0,0 +1,116 @@
+<template>
+  <ui-card padding="p-1">
+    <button
+      v-for="item in modalActions"
+      :key="item.id"
+      v-tooltip.group="item.name"
+      class="hoverable p-2 rounded-lg"
+      @click="$emit('showModal', item.id)"
+    >
+      <v-remixicon :name="item.icon" />
+    </button>
+  </ui-card>
+  <ui-card padding="p-1 ml-4">
+    <button
+      v-tooltip.group="'Execute'"
+      icon
+      class="hoverable p-2 rounded-lg"
+      @click="$emit('execute')"
+    >
+      <v-remixicon name="riPlayLine" />
+    </button>
+  </ui-card>
+  <ui-card padding="p-1 ml-4 space-x-1">
+    <ui-popover>
+      <template #trigger>
+        <button class="rounded-lg p-2 hoverable">
+          <v-remixicon name="riMore2Line" />
+        </button>
+      </template>
+      <ui-list class="w-36">
+        <ui-list-item
+          v-for="item in moreActions"
+          :key="item.id"
+          v-close-popover
+          class="cursor-pointer"
+          @click="$emit(item.id)"
+        >
+          <v-remixicon :name="item.icon" class="mr-2 -ml-1" />
+          {{ item.name }}
+        </ui-list-item>
+      </ui-list>
+    </ui-popover>
+    <ui-button variant="accent" class="relative" @click="$emit('save')">
+      <span
+        v-if="isDataChanged"
+        class="flex h-3 w-3 absolute top-0 left-0 -ml-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="riSaveLine" class="mr-2 -ml-1 my-1" />
+      Save
+    </ui-button>
+  </ui-card>
+</template>
+<script setup>
+import { useGroupTooltip } from '@/composable/groupTooltip';
+
+defineProps({
+  isDataChanged: {
+    type: Boolean,
+    default: false,
+  },
+});
+defineEmits(['showModal', 'execute', 'rename', 'delete', 'save', 'export']);
+
+useGroupTooltip();
+
+const modalActions = [
+  {
+    id: 'data-columns',
+    name: 'Data columns',
+    icon: 'riKey2Line',
+  },
+  {
+    id: 'global-data',
+    name: 'Global data',
+    icon: 'riDatabase2Line',
+  },
+  {
+    id: 'settings',
+    name: 'Settings',
+    icon: 'riSettings3Line',
+  },
+];
+const moreActions = [
+  {
+    id: 'export',
+    name: 'Export',
+    icon: 'riDownloadLine',
+  },
+  {
+    id: 'rename',
+    name: 'Rename',
+    icon: 'riPencilLine',
+  },
+  {
+    id: 'delete',
+    name: 'Delete',
+    icon: 'riDeleteBin7Line',
+  },
+];
+</script>

+ 93 - 3
src/components/newtab/workflow/WorkflowBuilder.vue

@@ -32,11 +32,29 @@
         </button>
       </div>
     </div>
+    <ui-popover
+      v-model="contextMenu.show"
+      :options="contextMenu.position"
+      padding="p-3"
+    >
+      <ui-list class="w-36 space-y-1">
+        <ui-list-item
+          v-for="item in contextMenu.items"
+          :key="item.id"
+          v-close-popover
+          class="cursor-pointer"
+          @click="contextMenuHandler[item.event]"
+        >
+          <v-remixicon :name="item.icon" class="mr-2 -ml-1" />
+          <span>{{ item.name }}</span>
+        </ui-list-item>
+      </ui-list>
+    </ui-popover>
   </div>
 </template>
 <script>
 /* eslint-disable camelcase */
-import { onMounted, shallowRef, getCurrentInstance } from 'vue';
+import { onMounted, shallowRef, reactive, getCurrentInstance } from 'vue';
 import emitter from 'tiny-emitter/instance';
 import { tasks } from '@/utils/shared';
 import { useGroupTooltip } from '@/composable/groupTooltip';
@@ -53,7 +71,30 @@ export default {
   setup(props, { emit }) {
     useGroupTooltip();
 
+    const contextMenuItems = {
+      block: [
+        {
+          id: 'duplicate',
+          name: 'Duplicate',
+          icon: 'riFileCopyLine',
+          event: 'duplicateBlock',
+        },
+        {
+          id: 'delete',
+          name: 'Delete',
+          icon: 'riDeleteBin7Line',
+          event: 'deleteBlock',
+        },
+      ],
+    };
+
     const editor = shallowRef(null);
+    const contextMenu = reactive({
+      items: [],
+      data: null,
+      show: false,
+      position: {},
+    });
 
     function dropHandler({ dataTransfer, clientX, clientY }) {
       const block = JSON.parse(dataTransfer.getData('block') || null);
@@ -102,9 +143,32 @@ export default {
         return item === input;
       });
     }
+    function deleteBlock() {
+      editor.value.removeNodeId(contextMenu.data);
+    }
+    function duplicateBlock() {
+      const { name, pos_x, pos_y, data, html } = editor.value.getNodeFromId(
+        contextMenu.data.substr(5)
+      );
+
+      if (name === 'trigger') return;
+
+      const { outputs, inputs } = tasks[name];
+
+      editor.value.addNode(
+        name,
+        inputs,
+        outputs,
+        pos_x + 50,
+        pos_y + 100,
+        name,
+        data,
+        html,
+        'vue'
+      );
+    }
 
     onMounted(() => {
-      /* eslint-disable-next-line */
       const context = getCurrentInstance().appContext.app._context;
       const element = document.querySelector('#drawflow');
 
@@ -128,7 +192,7 @@ export default {
           50,
           300,
           'trigger',
-          { type: 'manual' },
+          tasks.trigger.data,
           'BlockBasic',
           'vue'
         );
@@ -162,6 +226,27 @@ export default {
       editor.value.on('connectionRemoved', () => {
         emitter.emit('editor:data-changed');
       });
+      editor.value.on('contextmenu', ({ clientY, clientX, target }) => {
+        const isBlock = target.closest('.drawflow .drawflow-node');
+
+        if (isBlock) {
+          const virtualEl = {
+            getReferenceClientRect: () => ({
+              width: 0,
+              height: 0,
+              top: clientY,
+              right: clientX,
+              bottom: clientY,
+              left: clientX,
+            }),
+          };
+
+          contextMenu.data = isBlock.id;
+          contextMenu.position = virtualEl;
+          contextMenu.items = contextMenuItems.block;
+          contextMenu.show = true;
+        }
+      });
 
       setTimeout(() => {
         editor.value.zoom_refresh();
@@ -170,7 +255,12 @@ export default {
 
     return {
       editor,
+      contextMenu,
       dropHandler,
+      contextMenuHandler: {
+        deleteBlock,
+        duplicateBlock,
+      },
     };
   },
 };

+ 2 - 1
src/components/newtab/workflow/WorkflowDataColumns.vue

@@ -12,7 +12,7 @@
   </div>
   <ul
     class="space-y-2 overflow-y-auto scroll py-1"
-    style="max-height: calc(100vh - 10rem)"
+    style="max-height: calc(100vh - 11rem)"
   >
     <li
       v-for="(column, index) in columns"
@@ -56,6 +56,7 @@ const dataTypes = [
   { id: 'string', name: 'Text' },
   { id: 'integer', name: 'Number' },
   { id: 'boolean', name: 'Boolean' },
+  { id: 'array', name: 'Array' },
 ];
 
 const state = reactive({

+ 6 - 115
src/components/newtab/workflow/WorkflowDetailsCard.vue

@@ -1,21 +1,9 @@
 <template>
-  <div class="px-4 flex items-center mb-2">
-    <ui-popover>
+  <div class="px-4 flex items-center mb-2 mt-1">
+    <ui-popover class="mr-2 h-6">
       <template #trigger>
-        <span
-          title="Workflow icon"
-          class="
-            p-2
-            inline-block
-            rounded-lg
-            cursor-pointer
-            bg-accent
-            text-white
-            mr-2
-            align-middle
-          "
-        >
-          <v-remixicon :name="workflow.icon" />
+        <span title="Workflow icon" class="cursor-pointer">
+          <v-remixicon :name="workflow.icon" size="26" />
         </span>
       </template>
       <p class="mb-2">Workflow icon</p>
@@ -30,98 +18,10 @@
         </span>
       </div>
     </ui-popover>
-    <p
-      class="
-        font-semibold
-        text-overflow
-        inline-block
-        text-lg
-        flex-1
-        mr-4
-        align-middle
-      "
-    >
+    <p class="font-semibold text-overflow inline-block text-lg flex-1 mr-4">
       {{ workflow.name }}
     </p>
   </div>
-  <div class="flex px-4 mt-2 space-x-2">
-    <ui-button variant="accent" class="flex-1 relative" @click="$emit('save')">
-      <span
-        v-if="dataChanged"
-        class="flex h-3 w-3 absolute top-0 left-0 -ml-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="riSaveLine" class="mr-2 -ml-1" />
-      Save
-    </ui-button>
-    <ui-button icon title="Execute" @click="$emit('execute')">
-      <v-remixicon name="riPlayLine" />
-    </ui-button>
-    <ui-popover>
-      <template #trigger>
-        <ui-button icon title="More">
-          <v-remixicon name="riMore2Line" />
-        </ui-button>
-      </template>
-      <ui-list>
-        <ui-list-item
-          v-close-popover
-          class="cursor-pointer"
-          @click="$emit('rename')"
-        >
-          <v-remixicon name="riPencilLine" class="mr-2 -ml-1" />
-          <span>Rename</span>
-        </ui-list-item>
-        <ui-list-item
-          v-close-popover
-          class="cursor-pointer"
-          @click="$emit('export', workflow)"
-        >
-          <v-remixicon name="riDownloadLine" class="mr-2 -ml-1" />
-          <span>Export</span>
-        </ui-list-item>
-        <ui-list-item
-          v-close-popover
-          class="cursor-pointer"
-          @click="$emit('showDataColumns')"
-        >
-          <v-remixicon name="riKey2Line" class="mr-2 -ml-1" />
-          <span>Data columns</span>
-        </ui-list-item>
-        <ui-list-item
-          v-close-popover
-          class="cursor-pointer"
-          @click="$emit('showSettings')"
-        >
-          <v-remixicon name="riSettings3Line" class="mr-2 -ml-1" />
-          <span>Settings</span>
-        </ui-list-item>
-        <ui-list-item
-          v-close-popover
-          class="cursor-pointer"
-          @click="$emit('delete')"
-        >
-          <v-remixicon name="riDeleteBin7Line" class="mr-2 -ml-1" />
-          <span>Delete</span>
-        </ui-list-item>
-      </ui-list>
-    </ui-popover>
-  </div>
   <ui-input
     v-model="query"
     prepend-icon="riSearch2Line"
@@ -190,16 +90,7 @@ defineProps({
     default: false,
   },
 });
-defineEmits([
-  'save',
-  'export',
-  'update',
-  'rename',
-  'delete',
-  'execute',
-  'showSettings',
-  'showDataColumns',
-]);
+defineEmits(['update']);
 
 const icons = [
   'riGlobalLine',

+ 52 - 0
src/components/newtab/workflow/WorkflowGlobalData.vue

@@ -0,0 +1,52 @@
+<template>
+  <div class="global-data">
+    <a
+      href="https://github.com/Kholid060/automa/wiki/Features#reference-data"
+      target="_blank"
+      rel="noopener"
+      class="inline-block text-primary"
+    >
+      Learn how to access the global data in a block
+    </a>
+    <p class="float-right clear-both" title="Characters limit">
+      {{ globalData.length }}/{{ maxLength.toLocaleString() }}
+    </p>
+    <prism-editor
+      v-model="globalData"
+      :highlight="highlighter('json')"
+      class="h-full scroll mt-2"
+      style="height: calc(100vh - 10rem)"
+    />
+  </div>
+</template>
+<script setup>
+import { ref, watch } from 'vue';
+import { PrismEditor } from 'vue-prism-editor';
+import { highlighter } from '@/lib/prism';
+import { debounce } from '@/utils/helper';
+
+const props = defineProps({
+  workflow: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['update']);
+
+const maxLength = 1e4;
+const globalData = ref(`${props.workflow.globalData}`);
+
+watch(
+  globalData,
+  debounce((value) => {
+    let newValue = value;
+
+    if (value.length > maxLength) {
+      newValue = value.slice(0, maxLength);
+      globalData.value = newValue;
+    }
+
+    emit('update', { globalData: newValue });
+  }, 250)
+);
+</script>

+ 23 - 21
src/components/newtab/workflow/WorkflowSettings.vue

@@ -1,26 +1,28 @@
 <template>
-  <div class="mb-4">
-    <p class="mb-1">On workflow error</p>
-    <div class="space-x-4">
-      <ui-radio
-        v-for="item in onError"
-        :key="item.id"
-        :model-value="workflow.settings.onError"
-        :value="item.id"
-        class="mr-4"
-        @change="updateWorkflow({ onError: $event })"
-      >
-        {{ item.name }}
-      </ui-radio>
+  <div class="workflow-settings">
+    <div class="mb-4">
+      <p class="mb-1">On workflow error</p>
+      <div class="space-x-4">
+        <ui-radio
+          v-for="item in onError"
+          :key="item.id"
+          :model-value="workflow.settings.onError"
+          :value="item.id"
+          class="mr-4"
+          @change="updateWorkflow({ onError: $event })"
+        >
+          {{ item.name }}
+        </ui-radio>
+      </div>
+    </div>
+    <div>
+      <p class="mb-1">Workflow timeout (milliseconds)</p>
+      <ui-input
+        :model-value="workflow.settings.timeout"
+        type="number"
+        @change="updateWorkflow({ timeout: +$event })"
+      />
     </div>
-  </div>
-  <div>
-    <p class="mb-1">Workflow timeout (milliseconds)</p>
-    <ui-input
-      :model-value="workflow.settings.timeout"
-      type="number"
-      @change="updateWorkflow({ timeout: +$event })"
-    />
   </div>
 </template>
 <script setup>

+ 145 - 98
src/components/newtab/workflow/edit/EditTrigger.vue

@@ -1,106 +1,135 @@
 <template>
-  <ui-textarea
-    :model-value="data.description"
-    autoresize
-    placeholder="Description"
-    class="w-full mb-2"
-    @change="updateData({ description: $event })"
-  />
-  <ui-select
-    :model-value="data.type || 'manual'"
-    placeholder="Trigger workflow"
-    class="w-full"
-    @change="handleSelectChange"
-  >
-    <option v-for="trigger in triggers" :key="trigger.id" :value="trigger.id">
-      {{ trigger.name }}
-    </option>
-  </ui-select>
-  <transition-expand mode="out-in">
-    <div v-if="data.type === 'interval'" class="flex items-center mt-1">
-      <ui-input
-        :model-value="data.interval"
-        type="number"
-        class="w-full mr-2"
-        label="Interval (minutes)"
-        placeholder="1-120"
-        min="1"
-        max="120"
-        @change="
-          updateIntervalInput($event, { key: 'interval', min: 1, max: 120 })
-        "
-      />
-      <ui-input
-        :model-value="data.delay"
-        type="number"
-        class="w-full"
-        label="Delay (minutes)"
-        min="0"
-        max="20"
-        placeholder="0-20"
-        @change="updateIntervalInput($event, { key: 'delay', min: 0, max: 20 })"
-      />
-    </div>
-    <div v-else-if="data.type === 'date'" class="mt-2">
-      <ui-input
-        :model-value="data.date"
-        :max="maxDate"
-        :min="minDate"
-        class="w-full"
-        type="date"
-        placeholder="Date"
-        @change="updateDate({ date: $event })"
-      />
-      <ui-input
-        :model-value="data.time"
-        type="time"
-        class="w-full mt-2"
-        placeholder="Time"
-        @change="updateData({ time: $event || '00:00' })"
-      />
-    </div>
-    <div v-else-if="data.type === 'visit-web'" class="mt-2">
-      <ui-input
-        :model-value="data.url"
-        placeholder="URL or Regex"
-        class="w-full"
-        @change="updateData({ url: $event })"
-      />
-      <ui-checkbox
-        :model-value="data.isUrlRegex"
-        class="mt-1"
-        @change="updateData({ isUrlRegex: $event })"
-      >
-        Use regex
-      </ui-checkbox>
-    </div>
-    <div v-else-if="data.type === 'keyboard-shortcut'" class="mt-2">
-      <div class="flex items-center mb-2">
+  <div class="trigger">
+    <ui-textarea
+      :model-value="data.description"
+      autoresize
+      placeholder="Description"
+      class="w-full mb-2"
+      @change="updateData({ description: $event })"
+    />
+    <ui-select
+      :model-value="data.type || 'manual'"
+      placeholder="Trigger workflow"
+      class="w-full"
+      @change="handleSelectChange"
+    >
+      <option v-for="trigger in triggers" :key="trigger.id" :value="trigger.id">
+        {{ trigger.name }}
+      </option>
+    </ui-select>
+    <transition-expand mode="out-in">
+      <div v-if="data.type === 'interval'" class="flex items-center mt-1">
         <ui-input
-          :model-value="recordKeys.keys"
-          readonly
-          class="flex-1 mr-2"
-          placeholder="Shortcut"
+          :model-value="data.interval"
+          type="number"
+          class="w-full mr-2"
+          label="Interval (minutes)"
+          placeholder="1-120"
+          min="1"
+          max="120"
+          @change="
+            updateIntervalInput($event, { key: 'interval', min: 1, max: 120 })
+          "
         />
-        <ui-button v-tooltip="'Record shortcut'" icon @click="toggleRecordKeys">
-          <v-remixicon
-            :name="recordKeys.isRecording ? 'riStopLine' : 'riRecordCircleLine'"
+        <ui-input
+          :model-value="data.delay"
+          type="number"
+          class="w-full"
+          label="Delay (minutes)"
+          min="0"
+          max="20"
+          placeholder="0-20"
+          @change="
+            updateIntervalInput($event, { key: 'delay', min: 0, max: 20 })
+          "
+        />
+      </div>
+      <div v-else-if="data.type === 'date'" class="mt-2">
+        <ui-input
+          :model-value="data.date"
+          :max="maxDate"
+          :min="minDate"
+          class="w-full"
+          type="date"
+          placeholder="Date"
+          @change="updateDate({ date: $event })"
+        />
+        <ui-input
+          :model-value="data.time"
+          type="time"
+          class="w-full mt-2"
+          placeholder="Time"
+          @change="updateData({ time: $event || '00:00' })"
+        />
+      </div>
+      <div v-else-if="data.type === 'specific-day'" class="mt-2">
+        <ui-input
+          :model-value="data.time"
+          type="time"
+          class="w-full my-2"
+          placeholder="Time"
+          @change="updateData({ time: $event || '00:00' })"
+        />
+        <div class="grid gap-2 grid-cols-2">
+          <ui-checkbox
+            v-for="day in days"
+            :key="day.id"
+            :model-value="data.days.includes(day.id)"
+            @change="onDayChange($event, day.id)"
+          >
+            {{ day.name }}
+          </ui-checkbox>
+        </div>
+      </div>
+      <div v-else-if="data.type === 'visit-web'" class="mt-2">
+        <ui-input
+          :model-value="data.url"
+          placeholder="URL or Regex"
+          class="w-full"
+          @change="updateData({ url: $event })"
+        />
+        <ui-checkbox
+          :model-value="data.isUrlRegex"
+          class="mt-1"
+          @change="updateData({ isUrlRegex: $event })"
+        >
+          Use regex
+        </ui-checkbox>
+      </div>
+      <div v-else-if="data.type === 'keyboard-shortcut'" class="mt-2">
+        <div class="flex items-center mb-2">
+          <ui-input
+            :model-value="recordKeys.keys"
+            readonly
+            class="flex-1 mr-2"
+            placeholder="Shortcut"
           />
-        </ui-button>
+          <ui-button
+            v-tooltip="'Record shortcut'"
+            icon
+            @click="toggleRecordKeys"
+          >
+            <v-remixicon
+              :name="
+                recordKeys.isRecording ? 'riStopLine' : 'riRecordCircleLine'
+              "
+            />
+          </ui-button>
+        </div>
+        <ui-checkbox
+          :model-value="data.activeInInput"
+          class="mb-1"
+          title="Execute shortcut even when you're in an input element"
+          @change="updateData({ activeInInput: $event })"
+        >
+          Active while in input
+        </ui-checkbox>
+        <p class="mt-4 leading-tight text-gray-600 dark:text-gray-200">
+          Note: keyboard shortcut only working when you're on a webpage
+        </p>
       </div>
-      <ui-checkbox
-        :model-value="data.activeInInput"
-        class="mb-1"
-        title="Execute shortcut even in an input element"
-        @change="updateData({ activeInInput: $event })"
-      >
-        Active while in input
-      </ui-checkbox>
-      <p class="mt-4 leading-tight text-gray-600 dark:text-gray-200">
-        Note: keyboard shortcut only working when you're on a webpage
-      </p>
-    </div>
-  </transition-expand>
+    </transition-expand>
+  </div>
 </template>
 <script setup>
 import { shallowReactive, onUnmounted } from 'vue';
@@ -118,9 +147,19 @@ const triggers = [
   { id: 'manual', name: 'Manually' },
   { id: 'interval', name: 'Interval' },
   { id: 'date', name: 'On specific date' },
+  { id: 'specific-day', name: 'On specific day' },
   { id: 'visit-web', name: 'When visit a website' },
   { id: 'keyboard-shortcut', name: 'Keyboard shortcut' },
 ];
+const days = [
+  { id: 0, name: 'Sunday' },
+  { id: 1, name: 'Monday' },
+  { id: 2, name: 'Tuesday' },
+  { id: 3, name: 'Wednesday' },
+  { id: 4, name: 'Thursday' },
+  { id: 5, name: 'Friday' },
+  { id: 6, name: 'Saturday' },
+];
 const maxDate = dayjs().add(30, 'day').format('YYYY-MM-DD');
 const minDate = dayjs().format('YYYY-MM-DD');
 const allowedKeys = {
@@ -143,6 +182,14 @@ const recordKeys = shallowReactive({
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
 }
+function onDayChange(value, id) {
+  const dataDays = [...props.data.days];
+
+  if (value) dataDays.push(id);
+  else dataDays.splice(dataDays.indexOf(id), 1);
+
+  updateData({ days: dataDays.sort() });
+}
 function handleKeydownEvent(event) {
   event.preventDefault();
   event.stopPropagation();

+ 14 - 1
src/components/ui/UiPopover.vue

@@ -41,6 +41,10 @@ export default {
       type: [String, Object, HTMLElement],
       default: '',
     },
+    options: {
+      type: Object,
+      default: () => ({}),
+    },
     disabled: {
       type: Boolean,
       default: false,
@@ -50,13 +54,20 @@ export default {
       default: false,
     },
   },
-  emits: ['show', 'trigger', 'close'],
+  emits: ['show', 'trigger', 'close', 'update:modelValue'],
   setup(props, { emit }) {
     const targetEl = ref(null);
     const content = ref(null);
     const isShow = ref(false);
     const instance = shallowRef(null);
 
+    watch(
+      () => props.options,
+      (value) => {
+        instance.value.setProps(value);
+      },
+      { deep: true }
+    );
     watch(
       () => props.disabled,
       (value) => {
@@ -102,9 +113,11 @@ export default {
         },
         onHide: () => {
           emit('close');
+          emit('update:modelValue', false);
           isShow.value = false;
         },
         onTrigger: () => emit('trigger'),
+        ...props.options,
       });
     });
     onUnmounted(() => {

+ 25 - 15
src/content/blocks-handler.js

@@ -89,18 +89,6 @@ export function getText(block) {
   });
 }
 
-function incScrollPos(element, data, vertical = true) {
-  let currentPos = vertical ? element.scrollTop : element.scrollLeft;
-
-  if (data.incY) {
-    currentPos += data.scrollY;
-  } else if (data.incX) {
-    currentPos += data.scrollX;
-  }
-
-  return currentPos;
-}
-
 const automaScript = `
 function automaNextBlock(data) {
   window.dispatchEvent(new CustomEvent('__automa-next-block__', { detail: data }));
@@ -151,6 +139,18 @@ export function javascriptCode(block) {
 }
 
 export function elementScroll(block) {
+  function incScrollPos(element, data, vertical = true) {
+    let currentPos = vertical ? element.scrollTop : element.scrollLeft;
+
+    if (data.incY) {
+      currentPos += data.scrollY;
+    } else if (data.incX) {
+      currentPos += data.scrollX;
+    }
+
+    return currentPos;
+  }
+
   return new Promise((resolve) => {
     const { data } = block;
     const behavior = data.smooth ? 'smooth' : 'auto';
@@ -175,12 +175,22 @@ export function elementScroll(block) {
 
 export function attributeValue(block) {
   return new Promise((resolve) => {
-    const result = [];
+    let result = [];
+    const { attributeName, multiple } = block.data;
+    const isCheckboxOrRadio = (element) => {
+      if (element.tagName !== 'INPUT') return false;
+
+      return ['checkbox', 'radio'].includes(element.getAttribute('type'));
+    };
 
     handleElement(block, (element) => {
-      const value = element.getAttribute(block.data.attributeName);
+      const value =
+        attributeName === 'checked' && isCheckboxOrRadio(element)
+          ? element.checked
+          : element.getAttribute(attributeName);
 
-      result.push(value);
+      if (multiple) result.push(value);
+      else result = value;
     });
 
     resolve(result);

+ 1 - 1
src/content/element-selector/ElementSelector.ce.vue

@@ -73,7 +73,7 @@ import {
   riEyeOffLine,
 } from 'v-remixicon/icons';
 
-/* to-do get list of attribute value */
+/* to-do get list of attribute value, add test for each of the web interation block */
 
 const element = reactive({
   hide: window.self !== window.top,

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

@@ -1,6 +1,7 @@
 import vRemixicon from 'v-remixicon';
 import {
   riHome5Line,
+  riFileCopyLine,
   riFolderLine,
   riInformationLine,
   riWindow2Line,
@@ -72,6 +73,7 @@ import {
 
 export const icons = {
   riHome5Line,
+  riFileCopyLine,
   riFolderLine,
   riInformationLine,
   riWindow2Line,

+ 2 - 1
src/models/collection.js

@@ -11,9 +11,10 @@ class Collection extends Model {
   static fields() {
     return {
       id: this.uid(() => nanoid()),
-      name: this.string(''),
       flow: this.attr([]),
+      name: this.string(''),
       createdAt: this.number(),
+      globalData: this.string(''),
       options: this.attr({
         atOnce: false,
       }),

+ 1 - 0
src/models/workflow.js

@@ -18,6 +18,7 @@ class Workflow extends Model {
       data: this.attr(null),
       drawflow: this.string(''),
       dataColumns: this.attr([]),
+      globalData: this.string('[{ "key": "value" }]'),
       lastRunAt: this.number(),
       createdAt: this.number(),
       settings: this.attr({

+ 0 - 1
src/newtab/App.vue

@@ -6,7 +6,6 @@
   <ui-dialog />
 </template>
 <script setup>
-/* to-do add documentation of the extension */
 import { ref } from 'vue';
 import { useStore } from 'vuex';
 import browser from 'webextension-polyfill';

+ 1 - 1
src/newtab/pages/Collections.vue

@@ -2,7 +2,7 @@
   <div class="container pt-8 pb-4">
     <h1 class="text-2xl font-semibold">Collections</h1>
     <p class="text-gray-600 dark:text-gray-200">
-      Execute your workflows continuously
+      Execute your workflows in sequence
     </p>
     <div class="flex items-center my-6 space-x-4">
       <ui-input

+ 67 - 29
src/newtab/pages/collections/[id].vue

@@ -22,7 +22,7 @@
     </div>
     <div class="flex items-start">
       <div
-        class="w-80 border-r pr-6 mr-6 p-1 scroll overflow-auto"
+        class="w-80 border-r sticky top-11 pr-6 mr-6 p-1 scroll overflow-auto"
         style="max-height: calc(100vh - 8rem)"
       >
         <ui-input
@@ -55,34 +55,44 @@
         </draggable>
       </div>
       <div class="flex-1 relative">
-        <div class="px-1 mb-4 inline-block rounded-lg bg-white">
-          <ui-tabs
-            v-model="state.activeTab"
-            class="border-none h-full space-x-1"
+        <div class="flex items-center mb-4">
+          <div class="px-1 inline-block rounded-lg bg-white">
+            <ui-tabs
+              v-model="state.activeTab"
+              class="border-none h-full space-x-1"
+            >
+              <ui-tab value="flow">Flow</ui-tab>
+              <ui-tab value="logs">Logs</ui-tab>
+              <ui-tab value="running">
+                Running
+                <span
+                  v-if="runningCollection.length > 0"
+                  class="
+                    ml-2
+                    p-1
+                    text-center
+                    inline-block
+                    text-xs
+                    rounded-full
+                    bg-black
+                    text-white
+                  "
+                  style="min-width: 25px"
+                >
+                  {{ runningCollection.length }}
+                </span>
+              </ui-tab>
+              <ui-tab value="options">Options</ui-tab>
+            </ui-tabs>
+          </div>
+          <div class="flex-grow"></div>
+          <ui-button
+            v-tooltip="'Global data'"
+            icon
+            @click="state.showGlobalData = !state.showGlobalData"
           >
-            <ui-tab value="flow">Flow</ui-tab>
-            <ui-tab value="logs">Logs</ui-tab>
-            <ui-tab value="running">
-              Running
-              <span
-                v-if="runningCollection.length > 0"
-                class="
-                  ml-2
-                  p-1
-                  text-center
-                  inline-block
-                  text-xs
-                  rounded-full
-                  bg-black
-                  text-white
-                "
-                style="min-width: 25px"
-              >
-                {{ runningCollection.length }}
-              </span>
-            </ui-tab>
-            <ui-tab value="options">Options</ui-tab>
-          </ui-tabs>
+            <v-remixicon name="riDatabase2Line" />
+          </ui-button>
         </div>
         <ui-tab-panels v-model="state.activeTab">
           <ui-tab-panel class="relative" value="flow">
@@ -129,7 +139,7 @@
                         ? 'bg-yellow-200'
                         : 'bg-green-200',
                     ]"
-                    class="absolute w-2 left-0 top-0 h-full"
+                    class="absolute w-1 left-0 top-0 h-full"
                   ></span>
                   <v-remixicon :name="element.icon" class="mr-4" />
                   <p class="flex-1 text-overflow">{{ element.name }}</p>
@@ -200,13 +210,31 @@
       </div>
     </div>
   </div>
+  <ui-modal v-model="state.showGlobalData" content-class="max-w-xl">
+    <template #header>Global data</template>
+    <p class="inline-block">
+      This will overwrite the global data of the workflow
+    </p>
+    <p class="float-right clear-both" title="Characters limit">
+      {{ collection.globalData.length }}/{{ (1e4).toLocaleString() }}
+    </p>
+    <prism-editor
+      :model-value="collection.globalData"
+      :highlight="highlighter('json')"
+      class="h-full scroll mt-2"
+      style="height: calc(100vh - 10rem)"
+      @update:modelValue="updateGlobalData"
+    />
+  </ui-modal>
 </template>
 <script setup>
 import { computed, shallowReactive, onMounted, watch } from 'vue';
 import { nanoid } from 'nanoid';
 import { useStore } from 'vuex';
 import { useRoute, useRouter } from 'vue-router';
+import { PrismEditor } from 'vue-prism-editor';
 import Draggable from 'vuedraggable';
+import { highlighter } from '@/lib/prism';
 import { useDialog } from '@/composable/dialog';
 import { sendMessage } from '@/utils/message';
 import Log from '@/models/log';
@@ -240,6 +268,7 @@ const dialog = useDialog();
 const state = shallowReactive({
   query: '',
   activeTab: 'flow',
+  showGlobalData: false,
   sidebarTab: 'workflows',
 });
 const collectionOptions = shallowReactive({
@@ -294,6 +323,15 @@ function updateCollection(data) {
     data,
   });
 }
+function updateGlobalData(str) {
+  let value = str;
+
+  if (value.length > 1e4) {
+    value = value.slice(0, 1e4);
+  }
+
+  updateCollection({ globalData: value });
+}
 function updateCollectionFlow(event) {
   const flow = event.map(({ type, id, flowId, data }) => {
     const itemFlowId = flowId || nanoid();

+ 1 - 1
src/newtab/pages/logs.vue

@@ -7,7 +7,7 @@
       @updateSorts="sortsBuilder[$event.key] = $event.value"
       @updateFilters="filtersBuilder[$event.key] = $event.value"
     />
-    <shared-logs-table :logs="logs" class="w-full">
+    <shared-logs-table :logs="logs" class="w-full" style="min-height: 320px">
       <template #item-prepend="{ log }">
         <td class="w-8">
           <ui-checkbox

+ 83 - 44
src/newtab/pages/logs/[id].vue

@@ -23,53 +23,81 @@
       <ui-button class="text-red-500" @click="deleteLog"> Delete </ui-button>
     </div>
     <div class="flex items-start">
-      <ui-list class="w-7/12 mr-6">
-        <router-link
-          v-if="collectionLog"
-          :to="activeLog.collectionLogId"
-          class="mb-4 flex block"
-        >
-          <v-remixicon name="riArrowLeftLine" class="mr-2" />
-          Go back
-          <span class="font-semibold mx-1">{{ collectionLog.name }}</span> log
-        </router-link>
-        <ui-list-item v-for="(item, index) in activeLog.history" :key="index">
-          <span
-            :class="logsType[item.type]?.color"
-            class="p-1 rounded-lg align-middle inline-block mr-2"
+      <div class="w-7/12 mr-6">
+        <ui-list>
+          <router-link
+            v-if="collectionLog"
+            :to="activeLog.collectionLogId"
+            class="mb-4 flex"
           >
-            <v-remixicon :name="logsType[item.type]?.icon" size="20" />
-          </span>
-          <div class="flex-1 text-overflow pr-2">
-            <p class="w-full text-overflow leading-tight">
-              {{ item.name }}
-            </p>
-            <p
-              v-if="item.type === 'error'"
-              :title="item.message"
-              class="
-                text-sm
-                leading-tight
-                text-overflow text-gray-600
-                dark:text-gray-200
-              "
+            <v-remixicon name="riArrowLeftLine" class="mr-2" />
+            Go back
+            <span class="font-semibold mx-1">{{ collectionLog.name }}</span> log
+          </router-link>
+          <ui-list-item v-for="(item, index) in history" :key="index">
+            <span
+              :class="logsType[item.type]?.color"
+              class="p-1 rounded-lg align-middle inline-block mr-2"
             >
-              {{ item.message }}
+              <v-remixicon :name="logsType[item.type]?.icon" size="20" />
+            </span>
+            <div class="flex-1 text-overflow pr-2">
+              <p class="w-full text-overflow leading-tight">
+                {{ item.name }}
+              </p>
+              <p
+                v-if="item.type === 'error'"
+                :title="item.message"
+                class="
+                  text-sm
+                  leading-tight
+                  text-overflow text-gray-600
+                  dark:text-gray-200
+                "
+              >
+                {{ item.message }}
+              </p>
+            </div>
+            <router-link
+              v-if="item.logId"
+              :to="'/logs/' + item.logId"
+              class="mr-4"
+              title="Open log detail"
+            >
+              <v-remixicon name="riExternalLinkLine" />
+            </router-link>
+            <p class="text-gray-600">
+              {{ countDuration(0, item.duration || 0) }}
             </p>
+          </ui-list-item>
+        </ui-list>
+        <div
+          v-if="activeLog.history.length >= 10"
+          class="flex items-center justify-between mt-4"
+        >
+          <div>
+            Showing
+            <select
+              v-model="pagination.perPage"
+              class="p-1 rounded-md bg-input"
+            >
+              <option
+                v-for="num in [10, 15, 25, 50, 100]"
+                :key="num"
+                :value="num"
+              >
+                {{ num }}
+              </option>
+            </select>
+            items out of {{ activeLog.history.length }}
           </div>
-          <router-link
-            v-if="item.logId"
-            :to="'/logs/' + item.logId"
-            class="mr-4"
-            title="Open log detail"
-          >
-            <v-remixicon name="riExternalLinkLine" />
-          </router-link>
-          <p class="text-gray-600">
-            {{ countDuration(0, item.duration || 0) }}
-          </p>
-        </ui-list-item>
-      </ui-list>
+          <ui-pagination
+            v-model="pagination.currentPage"
+            :per-page="pagination.perPage"
+            :records="activeLog.history.length"
+          />
+        </div>
+      </div>
       <div class="w-5/12 logs-details sticky top-10">
         <logs-data-viewer :log="activeLog" />
       </div>
@@ -77,7 +105,7 @@
   </div>
 </template>
 <script setup>
-import { computed, onMounted } from 'vue';
+import { computed, onMounted, shallowReactive } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import Log from '@/models/log';
 import dayjs from '@/lib/dayjs';
@@ -110,7 +138,18 @@ const logsType = {
 const route = useRoute();
 const router = useRouter();
 
+const pagination = shallowReactive({
+  perPage: 10,
+  currentPage: 1,
+});
+
 const activeLog = computed(() => Log.find(route.params.id));
+const history = computed(() =>
+  activeLog.value.history.slice(
+    (pagination.currentPage - 1) * pagination.perPage,
+    pagination.currentPage * pagination.perPage
+  )
+);
 const collectionLog = computed(() => Log.find(activeLog.value.collectionLogId));
 
 function deleteLog() {

+ 47 - 101
src/newtab/pages/workflows/[id].vue

@@ -12,20 +12,15 @@
       <workflow-details-card
         v-else
         :workflow="workflow"
-        :data-changed="state.isDataChanged"
-        @save="saveWorkflow"
-        @export="exportWorkflow"
-        @execute="executeWorkflow"
         @update="updateWorkflow"
-        @showDataColumns="state.showDataColumnsModal = true"
-        @showSettings="state.showSettings = true"
-        @rename="renameWorkflow"
-        @delete="deleteWorkflow"
       />
     </div>
     <div class="flex-1 relative overflow-auto">
-      <div class="absolute px-3 rounded-lg bg-white z-10 left-0 m-4 top-0">
-        <ui-tabs v-model="activeTab" class="border-none h-full space-x-1">
+      <div class="absolute w-full flex items-center z-10 left-0 p-4 top-0">
+        <ui-tabs
+          v-model="activeTab"
+          class="border-none px-2 rounded-lg h-full space-x-1 bg-white"
+        >
           <ui-tab value="editor">Editor</ui-tab>
           <ui-tab value="logs">Logs</ui-tab>
           <ui-tab value="running" class="flex items-center">
@@ -48,6 +43,16 @@
             </span>
           </ui-tab>
         </ui-tabs>
+        <div class="flex-grow"></div>
+        <workflow-actions
+          :is-data-changed="state.isDataChanged"
+          @showModal="(state.modalName = $event), (state.showModal = true)"
+          @save="saveWorkflow"
+          @export="exportWorkflow(workflow)"
+          @execute="executeWorkflow"
+          @rename="renameWorkflow"
+          @delete="deleteWorkflow"
+        />
       </div>
       <keep-alive>
         <workflow-builder
@@ -98,18 +103,15 @@
       </keep-alive>
     </div>
   </div>
-  <ui-modal v-model="state.showDataColumnsModal" content-class="max-w-xl">
-    <template #header>Data columns</template>
-    <workflow-data-columns
+  <ui-modal v-model="state.showModal" content-class="max-w-xl">
+    <template #header>{{ workflowModals[state.modalName].title }}</template>
+    <component
+      :is="workflowModals[state.modalName].component"
       v-bind="{ workflow }"
       @update="updateWorkflow"
-      @close="state.showDataColumnsModal = false"
+      @close="state.showModal = false"
     />
   </ui-modal>
-  <ui-modal v-model="state.showSettings">
-    <template #header>Workflow settings</template>
-    <workflow-settings v-bind="{ workflow }" @update="updateWorkflow" />
-  </ui-modal>
 </template>
 <script setup>
 /* eslint-disable consistent-return */
@@ -123,7 +125,6 @@ import {
 } from 'vue';
 import { useStore } from 'vuex';
 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
-import browser from 'webextension-polyfill';
 import emitter from 'tiny-emitter/instance';
 import { sendMessage } from '@/utils/message';
 import { debounce } from '@/utils/helper';
@@ -131,10 +132,13 @@ import { useDialog } from '@/composable/dialog';
 import { exportWorkflow } from '@/utils/workflow-data';
 import Log from '@/models/log';
 import Workflow from '@/models/workflow';
+import workflowTrigger from '@/utils/workflow-trigger';
+import WorkflowActions from '@/components/newtab/workflow/WorkflowActions.vue';
 import WorkflowBuilder from '@/components/newtab/workflow/WorkflowBuilder.vue';
 import WorkflowSettings from '@/components/newtab/workflow/WorkflowSettings.vue';
 import WorkflowEditBlock from '@/components/newtab/workflow/WorkflowEditBlock.vue';
 import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
+import WorkflowGlobalData from '@/components/newtab/workflow/WorkflowGlobalData.vue';
 import WorkflowDataColumns from '@/components/newtab/workflow/WorkflowDataColumns.vue';
 import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
 import SharedWorkflowState from '@/components/newtab/shared/SharedWorkflowState.vue';
@@ -145,15 +149,32 @@ const router = useRouter();
 const dialog = useDialog();
 
 const workflowId = route.params.id;
+const workflowModals = {
+  'data-columns': {
+    icon: 'riKey2Line',
+    title: 'Data columns',
+    component: WorkflowDataColumns,
+  },
+  'global-data': {
+    title: 'Global data',
+    icon: 'riDatabase2Line',
+    component: WorkflowGlobalData,
+  },
+  settings: {
+    icon: 'riSettings3Line',
+    title: 'Settings',
+    component: WorkflowSettings,
+  },
+};
 
 const editor = shallowRef(null);
 const activeTab = shallowRef('editor');
 const state = reactive({
   blockData: {},
+  modalName: '',
+  showModal: false,
   isEditBlock: false,
-  showSettings: false,
   isDataChanged: false,
-  showDataColumnsModal: false,
 });
 
 const workflowState = computed(() =>
@@ -197,85 +218,6 @@ function updateWorkflow(data) {
     data,
   });
 }
-function convertToTimestamp(date, hourMinutes) {
-  let timestamp = Date.now() + 60000;
-  if (date) {
-    const dateObj = new Date(date);
-    if (hourMinutes) {
-      const arr = hourMinutes.split(':');
-      dateObj.setHours(arr[0]);
-      dateObj.setMinutes(arr[1]);
-    }
-
-    timestamp = dateObj.getTime();
-  }
-
-  return timestamp;
-}
-async function handleWorkflowTrigger({ data }) {
-  try {
-    const workflowAlarm = await browser.alarms.get(workflowId);
-    const { visitWebTriggers, shortcuts } = await browser.storage.local.get([
-      'visitWebTriggers',
-      'shortcuts',
-    ]);
-    let visitWebTriggerIndex = visitWebTriggers.findIndex(
-      (item) => item.id === workflowId
-    );
-    const keyboardShortcuts = Array.isArray(shortcuts) ? {} : shortcuts || {};
-    delete keyboardShortcuts[workflowId];
-
-    if (workflowAlarm) await browser.alarms.clear(workflowId);
-    if (visitWebTriggerIndex !== -1) {
-      visitWebTriggers.splice(visitWebTriggerIndex, 1);
-
-      visitWebTriggerIndex = -1;
-    }
-
-    await browser.storage.local.set({
-      visitWebTriggers,
-      shortcuts: keyboardShortcuts,
-    });
-
-    if (['date', 'interval'].includes(data.type)) {
-      let alarmInfo;
-
-      if (data.type === 'date') {
-        alarmInfo = {
-          when: convertToTimestamp(data.date, data.time),
-        };
-      } else {
-        alarmInfo = {
-          periodInMinutes: data.interval,
-        };
-
-        if (data.delay > 0) alarmInfo.delayInMinutes = data.delay;
-      }
-
-      if (alarmInfo) await browser.alarms.create(workflowId, alarmInfo);
-    } else if (data.type === 'visit-web' && data.url.trim() !== '') {
-      const payload = {
-        id: workflowId,
-        url: data.url,
-        isRegex: data.isUrlRegex,
-      };
-
-      if (visitWebTriggerIndex === -1) {
-        visitWebTriggers.unshift(payload);
-      } else {
-        visitWebTriggers[visitWebTriggerIndex] = payload;
-      }
-
-      await browser.storage.local.set({ visitWebTriggers });
-    } else if (data.type === 'keyboard-shortcut') {
-      keyboardShortcuts[workflowId] = data.shortcut;
-
-      await browser.storage.local.set({ shortcuts: keyboardShortcuts });
-    }
-  } catch (error) {
-    console.error(error);
-  }
-}
 function saveWorkflow() {
   const data = editor.value.export();
 
@@ -283,7 +225,10 @@ function saveWorkflow() {
     const [triggerBlockId] = editor.value.getNodesFromName('trigger');
 
     if (triggerBlockId) {
-      handleWorkflowTrigger(editor.value.getNodeFromId(triggerBlockId));
+      workflowTrigger.register(
+        workflowId,
+        editor.value.getNodeFromId(triggerBlockId)
+      );
     }
 
     state.isDataChanged = false;
@@ -350,6 +295,7 @@ provide('workflow', {
 onBeforeRouteLeave(() => {
   if (!state.isDataChanged) return;
 
+  // eslint-disable-next-line no-alert
   const answer = window.confirm(
     'Do you really want to leave? you have unsaved changes!'
   );

+ 10 - 0
src/utils/helper.js

@@ -1,3 +1,13 @@
+export function parseJSON(data, def) {
+  try {
+    const result = JSON.parse(data);
+
+    return result;
+  } catch (error) {
+    return def;
+  }
+}
+
 export function replaceMustache(str, replacer) {
   /* eslint-disable-next-line */
   return str.replace(/\{\{(.*?)\}\}/g, replacer);

+ 2 - 2
src/utils/reference-data.js

@@ -6,10 +6,10 @@ const objectPath = { get, set };
 function parseKey(key) {
   const [dataKey, path] = key.split('@');
 
-  if (['prevBlockData', 'loopData'].includes(dataKey))
+  if (['prevBlockData', 'loopData', 'globalData'].includes(dataKey))
     return { dataKey, path: path || '' };
 
-  const pathArr = path.split('.');
+  const pathArr = path?.split('.') ?? '';
   let dataPath = '';
 
   if (pathArr.length === 1) {

+ 2 - 1
src/utils/shared.js

@@ -23,11 +23,12 @@ export const tasks = {
       shortcut: '',
       activeInInput: false,
       isUrlRegex: false,
+      days: [],
     },
   },
   'active-tab': {
     name: 'Active tab',
-    description: "Set current tab that you're in as a active tab",
+    description: "Set current tab that you're in as an active tab",
     icon: 'riWindowLine',
     component: 'BlockBasic',
     category: 'browser',

+ 132 - 0
src/utils/workflow-trigger.js

@@ -0,0 +1,132 @@
+import browser from 'webextension-polyfill';
+import dayjs from 'dayjs';
+
+export async function cleanWorkflowTriggers(workflowId) {
+  try {
+    await browser.alarms.clear(workflowId);
+
+    const { visitWebTriggers, shortcuts } = await browser.storage.local.get([
+      'visitWebTriggers',
+      'shortcuts',
+    ]);
+
+    const keyboardShortcuts = Array.isArray(shortcuts) ? {} : shortcuts || {};
+    delete keyboardShortcuts[workflowId];
+
+    const visitWebTriggerIndex = visitWebTriggers.findIndex(
+      (item) => item.id === workflowId
+    );
+    if (visitWebTriggerIndex !== -1) {
+      visitWebTriggers.splice(visitWebTriggerIndex, 1);
+    }
+
+    await browser.storage.local.set({
+      visitWebTriggers,
+      shortcuts: keyboardShortcuts,
+    });
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+export function registerSpecificDay(workflowId, data) {
+  if (data.days.length === 0) return null;
+
+  const [hour, minute] = data.time.split(':');
+  const dates = data.days.map((id) =>
+    dayjs().day(id).hour(hour).minute(minute)
+  );
+  const findDate =
+    dates.find((date) => date.valueOf() > Date.now()) || dates[0].add(7, 'day');
+
+  return browser.alarms.create(workflowId, {
+    when: findDate.valueOf(),
+  });
+}
+
+export function registerInterval(workflowId, data) {
+  const alarmInfo = {
+    periodInMinutes: data.interval,
+  };
+
+  if (data.delay > 0) alarmInfo.delayInMinutes = data.delay;
+
+  return browser.alarms.create(workflowId, alarmInfo);
+}
+
+export function registerSpecificDate(workflowId, data) {
+  let date = Date.now() + 60000;
+
+  if (data.date) {
+    const [hour, minute] = data.time.split(':');
+    date = dayjs(data.data).hour(hour).minute(minute).valueOf();
+  }
+
+  return browser.alarms.create(workflowId, {
+    when: date,
+  });
+}
+
+export async function registerVisitWeb(workflowId, data) {
+  try {
+    if (data.url.trim() === '') return;
+
+    const visitWebTriggers =
+      (await browser.storage.local.get('visitWebTriggers'))?.visitWebTriggers ||
+      [];
+
+    const index = visitWebTriggers.findIndex((item) => item.id === workflowId);
+    const payload = {
+      id: workflowId,
+      url: data.url,
+      isRegex: data.isUrlRegex,
+    };
+
+    if (index === -1) {
+      visitWebTriggers.unshift(payload);
+    } else {
+      visitWebTriggers[index] = payload;
+    }
+
+    await browser.storage.local.set({ visitWebTriggers });
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+export async function registerKeyboardShortcut(workflowId, data) {
+  try {
+    const { shortcuts } = await browser.storage.local.get('shortcuts');
+    const keyboardShortcuts = Array.isArray(shortcuts) ? {} : shortcuts || {};
+
+    keyboardShortcuts[workflowId] = data.shortcut;
+
+    await browser.storage.local.set({ shortcuts: keyboardShortcuts });
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+export async function registerWorkflowTrigger(workflowId, { data }) {
+  try {
+    await cleanWorkflowTriggers(workflowId);
+
+    const triggersHandler = {
+      date: registerSpecificDate,
+      interval: registerInterval,
+      'visit-web': registerVisitWeb,
+      'specific-day': registerSpecificDay,
+      'keyboard-shortcut': registerKeyboardShortcut,
+    };
+
+    if (triggersHandler[data.type])
+      await triggersHandler[data.type](workflowId, data);
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+export default {
+  cleanUp: cleanWorkflowTriggers,
+  register: registerWorkflowTrigger,
+};