Ahmad Kholid 2 anos atrás
pai
commit
aafd2d02ec
52 arquivos alterados com 1177 adições e 588 exclusões
  1. 3 3
      package.json
  2. 8 2
      src/background/BackgroundEventsListeners.js
  3. 1 1
      src/components/content/shared/SharedElementSelector.vue
  4. 58 0
      src/components/newtab/app/AppLogs.vue
  5. 192 0
      src/components/newtab/app/AppLogsItem.vue
  6. 24 11
      src/components/newtab/app/AppLogsItemRunning.vue
  7. 105 6
      src/components/newtab/app/AppLogsItems.vue
  8. 17 2
      src/components/newtab/app/AppSidebar.vue
  9. 6 9
      src/components/newtab/logs/LogsFilters.vue
  10. 56 30
      src/components/newtab/logs/LogsHistory.vue
  11. 26 4
      src/components/newtab/shared/SharedLogsTable.vue
  12. 3 1
      src/components/newtab/workflow/WorkflowEditor.vue
  13. 164 73
      src/components/newtab/workflow/edit/EditCookie.vue
  14. 29 12
      src/components/newtab/workflow/edit/EditInsertData.vue
  15. 0 2
      src/components/newtab/workflow/edit/EditLoopData.vue
  16. 1 1
      src/components/newtab/workflow/edit/EditParameterPrompt.vue
  17. 17 8
      src/components/newtab/workflow/edit/Trigger/TriggerSpecificDay.vue
  18. 6 25
      src/components/newtab/workflow/editor/EditorCustomEdge.vue
  19. 14 2
      src/components/ui/UiModal.vue
  20. 5 3
      src/content/blocksHandler/handlerJavascriptCode.js
  21. 30 7
      src/content/blocksHandler/handlerTakeScreenshot.js
  22. 6 1
      src/content/elementSelector/App.vue
  23. 2 1
      src/content/handleSelector.js
  24. 17 3
      src/content/index.js
  25. 3 0
      src/content/services/webService.js
  26. 2 1
      src/locales/en/blocks.json
  27. 11 2
      src/newtab/App.vue
  28. 9 187
      src/newtab/pages/logs/[id].vue
  29. 9 9
      src/newtab/pages/workflows/Host.vue
  30. 22 13
      src/newtab/pages/workflows/[id].vue
  31. 0 12
      src/newtab/router.js
  32. 11 2
      src/newtab/utils/blocksValidation.js
  33. 1 17
      src/newtab/utils/elementSelector.js
  34. 8 9
      src/params/App.vue
  35. 21 5
      src/popup/pages/Home.vue
  36. 16 3
      src/utils/getFile.js
  37. 36 0
      src/utils/helper.js
  38. 4 0
      src/utils/shared.js
  39. 38 9
      src/workflowEngine/WorkflowEngine.js
  40. 5 5
      src/workflowEngine/WorkflowWorker.js
  41. 30 27
      src/workflowEngine/blocksHandler/handlerActiveTab.js
  42. 1 1
      src/workflowEngine/blocksHandler/handlerClipboard.js
  43. 21 13
      src/workflowEngine/blocksHandler/handlerCookie.js
  44. 43 4
      src/workflowEngine/blocksHandler/handlerHandleDialog.js
  45. 12 9
      src/workflowEngine/blocksHandler/handlerInsertData.js
  46. 1 0
      src/workflowEngine/blocksHandler/handlerLoopBreakpoint.js
  47. 9 4
      src/workflowEngine/blocksHandler/handlerLoopData.js
  48. 17 17
      src/workflowEngine/blocksHandler/handlerParameterPrompt.js
  49. 5 1
      src/workflowEngine/blocksHandler/handlerWebhook.js
  50. 2 1
      src/workflowEngine/helper.js
  51. 16 6
      src/workflowEngine/index.js
  52. 34 24
      yarn.lock

+ 3 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "automa",
-  "version": "1.23.4",
+  "version": "1.24.0",
   "description": "An extension for automating your browser by connecting blocks",
   "repository": {
     "type": "git",
@@ -44,8 +44,8 @@
     "@tiptap/starter-kit": "^2.0.0-beta.197",
     "@tiptap/vue-3": "^2.0.0-beta.96",
     "@viselect/vanilla": "^3.1.0",
-    "@vue-flow/additional-components": "1.0.0",
-    "@vue-flow/core": "1.0.0",
+    "@vue-flow/additional-components": "^1.2.4",
+    "@vue-flow/core": "^1.4.1",
     "@vueuse/head": "^0.9.7",
     "@vueuse/rxjs": "^9.1.1",
     "@vuex-orm/core": "^0.36.4",

+ 8 - 2
src/background/BackgroundEventsListeners.js

@@ -25,10 +25,16 @@ class BackgroundEventsListeners {
     BackgroundWorkflowTriggers.contextMenu(event, tab);
   }
 
-  static onNotificationClicked(notificationId) {
+  static async onNotificationClicked(notificationId) {
     if (notificationId.startsWith('logs')) {
       const { 1: logId } = notificationId.split(':');
-      BackgroundUtils.openDashboard(`/logs/${logId}`);
+
+      const [tab] = await browser.tabs.query({
+        url: browser.runtime.getURL('/newtab.html'),
+      });
+      if (!tab) await BackgroundUtils.openDashboard('');
+
+      await BackgroundUtils.sendMessageToDashboard('open-logs', { logId });
     }
   }
 

+ 1 - 1
src/components/content/shared/SharedElementSelector.vue

@@ -117,8 +117,8 @@ function getElementRectWithOffset(
   }
   if (withElOptions && element.tagName === 'SELECT') {
     rect.options = Array.from(element.querySelectorAll('option')).map((el) => ({
+      value: el.value,
       name: el.innerText,
-      value: el.textContent,
     }));
   }
 

+ 58 - 0
src/components/newtab/app/AppLogs.vue

@@ -0,0 +1,58 @@
+<template>
+  <ui-modal
+    v-model="state.show"
+    custom-content
+    content-position="start"
+    @close="clearState"
+  >
+    <ui-card class="w-full mt-8" style="max-width: 1400px; min-height: 600px">
+      <app-logs-items
+        v-if="!state.logId"
+        :workflow-id="state.workflowId"
+        @select="onSelectLog"
+        @close="clearState"
+      />
+      <app-logs-item-running
+        v-else-if="state.runningWorkflow"
+        :log-id="state.logId"
+        @close="closeItemPage"
+      />
+      <app-logs-item v-else :log-id="state.logId" @close="closeItemPage" />
+    </ui-card>
+  </ui-modal>
+</template>
+<script setup>
+import { reactive } from 'vue';
+import emitter from '@/lib/mitt';
+import AppLogsItem from './AppLogsItem.vue';
+import AppLogsItems from './AppLogsItems.vue';
+import AppLogsItemRunning from './AppLogsItemRunning.vue';
+
+const state = reactive({
+  logId: '',
+  source: '',
+  show: false,
+  workflowId: '',
+  runningWorkflow: false,
+});
+
+emitter.on('ui:logs', (event = {}) => {
+  Object.assign(state, event);
+});
+
+function clearState() {
+  state.show = false;
+  state.logId = '';
+  state.source = '';
+  state.runningWorkflow = false;
+}
+function closeItemPage(closeModal = false) {
+  state.logId = '';
+
+  if (closeModal) clearState();
+}
+function onSelectLog({ id, type }) {
+  state.runningWorkflow = type === 'running';
+  state.logId = id;
+}
+</script>

+ 192 - 0
src/components/newtab/app/AppLogsItem.vue

@@ -0,0 +1,192 @@
+<template>
+  <div v-if="currentLog.id">
+    <div class="flex items-center">
+      <button
+        v-tooltip:bottom="t('workflow.blocks.go-back.name')"
+        role="button"
+        class="h-12 px-1 transition mr-2 bg-input rounded-lg dark:text-gray-300 text-gray-600"
+        @click="$emit('close')"
+      >
+        <v-remixicon name="riArrowLeftSLine" />
+      </button>
+      <div>
+        <h1 class="text-2xl max-w-md text-overflow font-semibold">
+          {{ currentLog.name }}
+        </h1>
+        <p class="text-gray-600 dark:text-gray-200">
+          {{
+            t(`log.description.text`, {
+              status: t(
+                `log.description.status.${currentLog.status || 'success'}`
+              ),
+              date: dayjs(currentLog.startedAt).format('DD MMM'),
+              duration: countDuration(currentLog.startedAt, currentLog.endedAt),
+            })
+          }}
+        </p>
+      </div>
+      <div class="flex-grow"></div>
+      <ui-button
+        v-if="state.workflowExists"
+        v-tooltip="t('log.goWorkflow')"
+        icon
+        class="mr-4"
+        @click="goToWorkflow"
+      >
+        <v-remixicon name="riExternalLinkLine" />
+      </ui-button>
+      <ui-button class="text-red-500 dark:text-red-400" @click="deleteLog">
+        {{ t('common.delete') }}
+      </ui-button>
+    </div>
+    <ui-tabs v-model="state.activeTab" class="mt-4" @change="onTabChange">
+      <ui-tab v-for="tab in tabs" :key="tab.id" class="mr-4" :value="tab.id">
+        {{ tab.name }}
+      </ui-tab>
+    </ui-tabs>
+    <ui-tab-panels
+      :model-value="state.activeTab"
+      class="mt-4 pb-4 overflow-auto scroll px-2"
+      style="min-height: 500px; max-height: calc(100vh - 15rem)"
+    >
+      <ui-tab-panel value="logs">
+        <logs-history
+          :current-log="currentLog"
+          :ctx-data="ctxData"
+          :parent-log="parentLog"
+        />
+      </ui-tab-panel>
+      <ui-tab-panel value="table">
+        <logs-table :current-log="currentLog" :table-data="tableData" />
+      </ui-tab-panel>
+      <ui-tab-panel value="variables">
+        <logs-variables :current-log="currentLog" />
+      </ui-tab-panel>
+    </ui-tab-panels>
+  </div>
+</template>
+<script setup>
+import { shallowReactive, shallowRef, watch } from 'vue';
+import { useRouter } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+import dbLogs from '@/db/logs';
+import dayjs from '@/lib/dayjs';
+import { useWorkflowStore } from '@/stores/workflow';
+import { countDuration, convertArrObjTo2DArr } from '@/utils/helper';
+import LogsTable from '@/components/newtab/logs/LogsTable.vue';
+import LogsHistory from '@/components/newtab/logs/LogsHistory.vue';
+import LogsVariables from '@/components/newtab/logs/LogsVariables.vue';
+
+const props = defineProps({
+  logId: {
+    type: String,
+    default: '',
+  },
+});
+const emit = defineEmits(['close']);
+
+const { t } = useI18n();
+const router = useRouter();
+const workflowStore = useWorkflowStore();
+
+const ctxData = shallowRef({});
+const parentLog = shallowRef(null);
+
+const tabs = [
+  { id: 'logs', name: t('common.log', 2) },
+  { id: 'table', name: t('workflow.table.title') },
+  { id: 'variables', name: t('workflow.variables.title', 2) },
+];
+
+const state = shallowReactive({
+  activeTab: 'logs',
+  workflowExists: false,
+});
+const tableData = shallowReactive({
+  converted: false,
+  body: [],
+  header: [],
+});
+const currentLog = shallowRef({
+  history: [],
+  data: {
+    table: [],
+    variables: {},
+  },
+});
+
+function deleteLog() {
+  dbLogs.items
+    .where('id')
+    .equals(props.logId)
+    .delete()
+    .then(() => {
+      emit('close');
+    });
+}
+function goToWorkflow() {
+  const path = `/workflows/${currentLog.value.workflowId}`;
+
+  router.push(path);
+  emit('close', true);
+}
+function convertToTableData() {
+  const data = currentLog.value.data?.table;
+  if (!data) return;
+
+  const [header] = convertArrObjTo2DArr(data);
+
+  tableData.converted = true;
+  tableData.body = data.map((item, index) => ({ ...item, id: index + 1 }));
+  tableData.header = header.map((name) => ({
+    text: name,
+    value: name,
+    filterable: true,
+  }));
+  tableData.header.unshift({ value: 'id', text: '', sortable: false });
+}
+function onTabChange(value) {
+  if (value === 'table' && !tableData.converted) {
+    convertToTableData();
+  }
+}
+async function fetchLog() {
+  if (!props.logId) return;
+
+  const logDetail = await dbLogs.items.where('id').equals(props.logId).last();
+  if (!logDetail) return;
+
+  tableData.body = [];
+  tableData.header = [];
+  parentLog.value = null;
+  tableData.converted = false;
+
+  const [logCtxData, logHistory, logsData] = await Promise.all(
+    ['ctxData', 'histories', 'logsData'].map((key) =>
+      dbLogs[key].where('logId').equals(props.logId).last()
+    )
+  );
+
+  ctxData.value = logCtxData?.data || {};
+  currentLog.value = {
+    history: logHistory?.data || [],
+    data: logsData?.data || {},
+    ...logDetail,
+  };
+
+  state.workflowExists = Boolean(workflowStore.getById(logDetail.workflowId));
+
+  const parentLogId = logDetail.collectionLogId || logDetail.parentLog?.id;
+  if (parentLogId) {
+    parentLog.value =
+      (await dbLogs.items.where('id').equals(parentLogId).last()) || null;
+  }
+}
+
+watch(() => props.logId, fetchLog, { immediate: true });
+</script>
+<style>
+.logs-details .cm-editor {
+  max-height: calc(100vh - 15rem);
+}
+</style>

+ 24 - 11
src/newtab/pages/logs/Running.vue → src/components/newtab/app/AppLogsItemRunning.vue

@@ -1,6 +1,14 @@
 <template>
-  <div v-if="running" class="container py-8">
+  <div v-if="running">
     <div class="flex items-center">
+      <button
+        v-tooltip:bottom="t('workflow.blocks.go-back.name')"
+        role="button"
+        class="h-12 px-1 transition mr-2 bg-input rounded-lg dark:text-gray-300 text-gray-600"
+        @click="$emit('close')"
+      >
+        <v-remixicon name="riArrowLeftSLine" />
+      </button>
       <div class="flex-grow overflow-hidden">
         <h1 class="text-2xl max-w-md text-overflow font-semibold text-overflow">
           {{ running.state.name }}
@@ -21,6 +29,7 @@
     </div>
     <div class="mt-8">
       <logs-history
+        :is-running="true"
         :current-log="{
           history: running.state.logs,
           workflowId: running.workflowId,
@@ -76,7 +85,7 @@
 </template>
 <script setup>
 import { computed, watch, shallowRef, onBeforeUnmount } from 'vue';
-import { useRoute, useRouter } from 'vue-router';
+import { useRouter } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import { countDuration } from '@/utils/helper';
 import { useWorkflowStore } from '@/stores/workflow';
@@ -85,8 +94,15 @@ import dbLogs from '@/db/logs';
 import dayjs from '@/lib/dayjs';
 import LogsHistory from '@/components/newtab/logs/LogsHistory.vue';
 
+const props = defineProps({
+  logId: {
+    type: String,
+    default: '',
+  },
+});
+const emit = defineEmits(['close']);
+
 const { t } = useI18n();
-const route = useRoute();
 const router = useRouter();
 const workflowStore = useWorkflowStore();
 
@@ -96,11 +112,12 @@ const interval = setInterval(() => {
 }, 1000);
 
 const running = computed(() =>
-  workflowStore.getAllStates.find(({ id }) => id === route.params.id)
+  workflowStore.getAllStates.find(({ id }) => id === props.logId)
 );
 
 function stopWorkflow() {
   stopWorkflowExec(running.value.id);
+  emit('close');
 }
 function getBlockPath(blockId) {
   const { workflowId, teamId } = running.value;
@@ -116,16 +133,12 @@ function getBlockPath(blockId) {
 watch(
   running,
   async () => {
-    if (!route.name.startsWith('logs')) return;
-    if (!running.value && route.params.id) {
-      const log = await dbLogs.items
-        .where('id')
-        .equals(route.params.id)
-        .first();
+    if (!running.value && props.logId) {
+      const log = await dbLogs.items.where('id').equals(props.logId).first();
       let path = '/logs';
 
       if (log) {
-        path = `/logs/${route.params.id}`;
+        path = `/logs/${props.logId}`;
       }
 
       router.replace(path);

+ 105 - 6
src/newtab/pages/Logs.vue → src/components/newtab/app/AppLogsItems.vue

@@ -1,18 +1,71 @@
 <template>
-  <div class="container pt-8 pb-4 logs-list">
-    <h1 class="text-2xl font-semibold mb-6">{{ t('common.log', 2) }}</h1>
+  <div class="pb-4 pt-1 overflow-auto logs-list">
+    <div class="flex items-center mb-8">
+      <h1 class="text-2xl font-semibold flex-1">
+        {{ $t('common.log', 2) }}
+      </h1>
+      <v-remixicon
+        name="riCloseLine"
+        class="cursor-pointer text-gray-600 dark:text-gray-300"
+        @click="$emit('close')"
+      />
+    </div>
     <logs-filters
       :sorts="sortsBuilder"
       :filters="filtersBuilder"
       @clear="clearLogs"
       @updateSorts="sortsBuilder[$event.key] = $event.value"
       @updateFilters="filtersBuilder[$event.key] = $event.value"
-    />
+    >
+      <ui-popover padding="" @click="filtersBuilder.workflowQuery = ''">
+        <template #trigger>
+          <ui-button>
+            <span class="text-overflow text-left" style="max-width: 160px">
+              {{ activeWorkflowName }}
+            </span>
+            <v-remixicon name="riArrowDropDownLine" class="-mr-1 ml-2" />
+          </ui-button>
+        </template>
+        <div class="w-64">
+          <div class="p-4">
+            <ui-input
+              v-model="filtersBuilder.workflowQuery"
+              autofocus
+              placeholder="Search..."
+              class="w-full"
+              prepend-icon="riSearch2Line"
+            />
+            <div class="text-right">
+              <span
+                class="underline text-sm cursor-pointer text-gray-600 dark:text-gray-300"
+                @click="filtersBuilder.workflowId = ''"
+              >
+                Clear
+              </span>
+            </div>
+          </div>
+          <ui-list class="mb-4 px-4 space-y-1 overflow-auto max-h-96 scroll">
+            <ui-list-item
+              v-for="workflow in workflows"
+              :key="workflow.id"
+              :active="filtersBuilder.workflowId === workflow.id"
+              class="cursor-pointer"
+              @click="filtersBuilder.workflowId = workflow.id"
+            >
+              <p class="text-overflow">{{ workflow.name }}</p>
+            </ui-list-item>
+          </ui-list>
+        </div>
+      </ui-popover>
+    </logs-filters>
     <div v-if="logs" style="min-height: 320px">
       <shared-logs-table
         :logs="logs"
-        :running="workflowStore.getAllStates"
+        :modal="true"
+        :running="workflowStates"
         class="w-full"
+        style="max-height: calc(100vh - 18rem)"
+        @select="$emit('select', $event)"
       >
         <template #item-prepend="{ log }">
           <td class="w-8">
@@ -85,14 +138,24 @@ import { useI18n } from 'vue-i18n';
 import { useDialog } from '@/composable/dialog';
 import dbLogs from '@/db/logs';
 import { useWorkflowStore } from '@/stores/workflow';
+import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import { useLiveQuery } from '@/composable/liveQuery';
 import LogsFilters from '@/components/newtab/logs/LogsFilters.vue';
 import LogsDataViewer from '@/components/newtab/logs/LogsDataViewer.vue';
 import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
 
+const props = defineProps({
+  workflowId: {
+    type: String,
+    default: '',
+  },
+});
+defineEmits(['select', 'close']);
+
 const { t } = useI18n();
 const dialog = useDialog();
 const workflowStore = useWorkflowStore();
+const hostedWorkflows = useHostedWorkflowStore();
 const storedlogs = useLiveQuery(() => dbLogs.items.toArray());
 
 const savedSorts = JSON.parse(localStorage.getItem('logs-sorts') || '{}');
@@ -106,6 +169,8 @@ const filtersBuilder = shallowReactive({
   query: '',
   byDate: 0,
   byStatus: 'all',
+  workflowQuery: '',
+  workflowId: props.workflowId,
 });
 const sortsBuilder = shallowReactive({
   order: savedSorts.order || 'desc',
@@ -116,13 +181,47 @@ const exportDataModal = shallowReactive({
   log: {},
 });
 
+const allWorkflows = computed(() =>
+  [...hostedWorkflows.toArray, ...workflowStore.getWorkflows].sort((a, b) =>
+    a.createdAt > b.createdAt ? -1 : 1
+  )
+);
+const workflows = computed(() =>
+  allWorkflows.value.filter((workflow) =>
+    workflow.name
+      .toLocaleLowerCase()
+      .includes(filtersBuilder.workflowQuery.toLocaleLowerCase())
+  )
+);
+const activeWorkflowName = computed(() => {
+  if (!filtersBuilder.workflowId) return 'All workflows';
+
+  const workflow = allWorkflows.value.find(
+    (item) => item.id === filtersBuilder.workflowId
+  );
+
+  return workflow?.name ?? 'All workflows';
+});
+
+const workflowStates = computed(() => {
+  const states = workflowStore.getAllStates;
+  if (!filtersBuilder.workflowId) return states;
+
+  return states.filter(
+    (state) => state.workflowId === filtersBuilder.workflowId
+  );
+});
+
 const filteredLogs = computed(() => {
   if (!storedlogs.value) return [];
 
   return storedlogs.value
-    .filter(({ name, status, endedAt }) => {
+    .filter(({ name, status, endedAt, workflowId }) => {
       let dateFilter = true;
       let statusFilter = true;
+      const workflowIdFilter = filtersBuilder.workflowId
+        ? filtersBuilder.workflowId === workflowId
+        : true;
       const searchFilter = name
         .toLocaleLowerCase()
         .includes(filtersBuilder.query.toLocaleLowerCase());
@@ -137,7 +236,7 @@ const filteredLogs = computed(() => {
         dateFilter = date <= endedAt;
       }
 
-      return searchFilter && statusFilter && dateFilter;
+      return searchFilter && workflowIdFilter && statusFilter && dateFilter;
     })
     .slice()
     .sort((a, b) => {

+ 17 - 2
src/components/newtab/app/AppSidebar.vue

@@ -31,9 +31,9 @@
             }`
           "
           :class="{ 'is-active': isActive }"
-          :href="href"
+          :href="tab.id === 'log' ? '#' : href"
           class="z-10 relative w-full flex items-center justify-center tab relative"
-          @click="navigate"
+          @click="navigateLink($event, navigate, tab)"
           @mouseenter="hoverHandler"
         >
           <div class="p-2 rounded-lg transition-colors inline-block">
@@ -124,6 +124,7 @@ import { useShortcut, getShortcut } from '@/composable/shortcut';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { communities } from '@/utils/shared';
 import { initElementSelector } from '@/newtab/utils/elementSelector';
+import emitter from '@/lib/mitt';
 
 useGroupTooltip();
 
@@ -192,10 +193,24 @@ useShortcut(
   ({ data }) => {
     if (!data) return;
 
+    if (data.includes('/logs')) {
+      emitter.emit('ui:logs', { show: true });
+      return;
+    }
+
     router.push(data);
   }
 );
 
+function navigateLink(event, navigateFn, tab) {
+  event.preventDefault();
+
+  if (tab.id === 'log') {
+    emitter.emit('ui:logs', { show: true });
+  } else {
+    navigateFn();
+  }
+}
 function hoverHandler({ target }) {
   showHoverIndicator.value = true;
   hoverIndicator.value.style.transform = `translate(-50%, ${target.offsetTop}px)`;

+ 6 - 9
src/components/newtab/logs/LogsFilters.vue

@@ -3,13 +3,12 @@
     <ui-input
       id="search-input"
       :model-value="filters.query"
-      :placeholder="`${t('common.search')}... (${
-        shortcut['action:search'].readable
-      })`"
+      :placeholder="`${t('common.search')}...`"
       prepend-icon="riSearch2Line"
       class="w-6/12 md:w-auto md:flex-1"
       @change="updateFilters('query', $event)"
     />
+    <slot />
     <div class="flex items-center workflow-sort w-5/12 ml-4 md:ml-0 md:w-auto">
       <ui-button
         icon
@@ -78,7 +77,6 @@
 </template>
 <script setup>
 import { useI18n } from 'vue-i18n';
-import { useShortcut } from '@/composable/shortcut';
 
 defineProps({
   filters: {
@@ -89,15 +87,14 @@ defineProps({
     type: Object,
     default: () => ({}),
   },
+  workflows: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['updateSorts', 'updateFilters', 'clear']);
 
 const { t } = useI18n();
-const shortcut = useShortcut('action:search', () => {
-  const searchInput = document.querySelector('#search-input input');
-
-  searchInput?.focus();
-});
 
 const filterByStatus = [
   { id: 'all', name: t('common.all') },

+ 56 - 30
src/components/newtab/logs/LogsHistory.vue

@@ -10,7 +10,7 @@
   </router-link>
   <div class="flex items-start flex-col-reverse lg:flex-row">
     <div class="lg:flex-1 w-full lg:w-auto">
-      <div class="rounded-lg bg-gray-900 dark:bg-gray-800 text-gray-100 dark">
+      <div class="rounded-lg bg-gray-900 text-gray-100 dark">
         <div
           class="border-b px-4 pt-4 flex items-center text-gray-200 pb-4 mb-4"
         >
@@ -44,7 +44,7 @@
           </div>
           <slot name="header-prepend" />
           <div class="flex-grow" />
-          <ui-popover trigger-width class="mr-4">
+          <ui-popover v-if="!isRunning" trigger-width class="mr-4">
             <template #trigger>
               <ui-button>
                 <span>
@@ -66,6 +66,7 @@
             </ui-list>
           </ui-popover>
           <ui-input
+            v-if="!isRunning"
             v-model="state.search"
             :placeholder="t('common.search')"
             prepend-icon="riSearch2Line"
@@ -73,7 +74,7 @@
         </div>
         <div
           id="log-history"
-          style="max-height: 600px"
+          style="max-height: 500px"
           class="scroll p-4 overflow-auto"
         >
           <slot name="prepend" />
@@ -209,7 +210,7 @@
     </div>
     <div
       v-if="state.itemId && activeLog"
-      class="w-full lg:w-4/12 lg:ml-8 mb-4 lg:mb-0 rounded-lg bg-gray-900 dark:bg-gray-800 text-gray-100 dark"
+      class="w-full lg:w-4/12 lg:ml-8 mb-4 lg:mb-0 rounded-lg bg-gray-900 text-gray-100 dark"
     >
       <div class="p-4 relative">
         <v-remixicon
@@ -262,27 +263,25 @@
           </tbody>
         </table>
       </div>
-      <template v-if="ctxData[state.itemId]">
-        <div class="px-4 pb-4 flex items-center">
-          <p>Log data</p>
-          <div class="flex-grow" />
-          <ui-select v-model="state.activeTab">
-            <option v-for="option in tabs" :key="option.id" :value="option.id">
-              {{ option.name }}
-            </option>
-          </ui-select>
-        </div>
-        <div class="pb-4 px-2 log-data-prev">
-          <shared-codemirror
-            :model-value="logCtxData"
-            readonly
-            hide-lang
-            lang="json"
-            style="max-height: 460px"
-            class="scroll"
-          />
-        </div>
-      </template>
+      <div class="px-4 pb-4 flex items-center">
+        <p>Log data</p>
+        <div class="flex-grow" />
+        <ui-select v-model="state.activeTab">
+          <option v-for="option in tabs" :key="option.id" :value="option.id">
+            {{ option.name }}
+          </option>
+        </ui-select>
+      </div>
+      <div class="pb-4 px-2 log-data-prev">
+        <shared-codemirror
+          :model-value="logCtxData"
+          readonly
+          hide-lang
+          lang="json"
+          style="max-height: 460px"
+          class="scroll"
+        />
+      </div>
     </div>
   </div>
 </template>
@@ -320,6 +319,7 @@ const props = defineProps({
     type: Object,
     default: null,
   },
+  isRunning: Boolean,
 });
 
 const files = {
@@ -423,15 +423,35 @@ const errorBlock = computed(() => {
   };
 });
 const logCtxData = computed(() => {
-  if (!state.itemId || !props.ctxData[state.itemId]) return '';
+  let logData = props.ctxData;
+  if (logData.ctxData) logData = logData.ctxData;
+
+  if (!state.itemId || !logData[state.itemId]) return '';
 
-  const data = props.ctxData[state.itemId];
-  const logData =
+  const data = logData[state.itemId];
+  const itemLogData =
     state.activeTab === 'all' ? data : objectPath.get(data, state.activeTab);
 
-  return JSON.stringify(logData, null, 2);
+  /* eslint-disable-next-line */
+  getDataSnapshot(itemLogData.referenceData);
+
+  return JSON.stringify(itemLogData, null, 2);
 });
 
+function getDataSnapshot(refData) {
+  if (!props.ctxData?.dataSnapshot) return;
+
+  const data = props.ctxData.dataSnapshot;
+  const getData = (key) => {
+    const currentData = refData[key];
+    if (typeof currentData !== 'string') return currentData;
+
+    return data[currentData] ?? {};
+  };
+
+  refData.loopData = getData('loopData');
+  refData.variables = getData('variables');
+}
 function exportLogs(type) {
   let data = type === 'plain-text' ? '' : [];
   const getItemData = {
@@ -475,6 +495,12 @@ function exportLogs(type) {
     },
   };
   translatedLog.value.forEach((item, index) => {
+    let logData = props.ctxData;
+    if (logData.ctxData) logData = logData.ctxData;
+
+    const itemData = logData[item.id] || null;
+    if (itemData) getDataSnapshot(itemData.referenceData);
+
     getItemData[type](
       [
         dayjs(item.timestamp || Date.now()).format('DD-MM-YYYY, hh:mm:ss'),
@@ -482,7 +508,7 @@ function exportLogs(type) {
         item.name,
         item.description || 'NULL',
         item.message || 'NULL',
-        props.ctxData[item.id] || null,
+        itemData,
       ],
       index
     );

+ 26 - 4
src/components/newtab/shared/SharedLogsTable.vue

@@ -17,10 +17,17 @@
               />
             </td>
             <td class="w-4/12">
+              <p
+                v-if="modal"
+                class="log-link text-overflow"
+                @click="$emit('select', { type: 'running', id: item.id })"
+              >
+                {{ item.state.name }}
+              </p>
               <router-link
+                v-else
                 :to="`/logs/${item.id}/running`"
-                class="inline-block text-overflow w-full align-middle min-h"
-                style="min-height: 28px"
+                class="log-link text-overflow"
               >
                 {{ item.state.name }}
               </router-link>
@@ -65,10 +72,17 @@
             class="text-overflow w-4/12"
             style="min-width: 140px; max-width: 330px"
           >
+            <p
+              v-if="modal"
+              class="log-link text-overflow"
+              @click="$emit('select', { type: 'log', id: log.id })"
+            >
+              {{ log.name }}
+            </p>
             <router-link
+              v-else
               :to="`/logs/${log.id}`"
-              class="inline-block text-overflow w-full align-middle min-h"
-              style="min-height: 28px"
+              class="log-link text-overflow"
             >
               {{ log.name }}
             </router-link>
@@ -126,8 +140,10 @@ defineProps({
     type: Array,
     default: () => [],
   },
+  modal: Boolean,
   hideSelect: Boolean,
 });
+defineEmits(['select']);
 
 const { t, te } = useI18n();
 
@@ -186,4 +202,10 @@ function stopSelectedWorkflow() {
   display: inline-block;
   vertical-align: middle;
 }
+
+.log-link {
+  @apply inline-block w-full align-middle;
+  cursor: pointer;
+  min-height: 28px;
+}
 </style>

+ 3 - 1
src/components/newtab/workflow/WorkflowEditor.vue

@@ -279,7 +279,9 @@ function applyFlowData() {
     editor.snapGrid.value = Object.values(settings.snapGrid);
   }
 
-  editor.setNodes(props.data?.nodes || []);
+  editor.setNodes(
+    props.data?.nodes.map((node) => ({ ...node, events: {} })) || []
+  );
   editor.setEdges(props.data?.edges || []);
   editor.setTransform({
     x: props.data?.x || 0,

+ 164 - 73
src/components/newtab/workflow/edit/EditCookie.vue

@@ -24,81 +24,110 @@
       >
         {{ t('workflow.blocks.cookie.types.getAll') }}
       </ui-checkbox>
-      <ui-input
-        :model-value="data.url"
-        class="mt-2 w-full"
-        type="url"
-        label="URL"
-        placeholder="https://example.com/"
-        @change="updateData({ url: $event })"
-      />
-      <ui-input
-        :model-value="data.name"
-        :label="`Name ${
-          data.type === 'get' && !data.getAll ? '' : '(optional)'
-        }`"
-        class="mt-2 w-full"
-        placeholder="site-cookie"
-        @change="updateData({ name: $event })"
-      />
-      <ui-input
-        v-if="data.type === 'set'"
-        :model-value="data.value"
-        label="Value (optional)"
-        class="mt-2 w-full"
-        placeholder="value"
-        @change="updateData({ value: $event })"
-      />
-      <ui-input
-        :model-value="data.path"
-        class="mt-2 w-full"
-        label="Path (optional)"
-        placeholder="/"
-        @change="updateData({ path: $event })"
-      />
-      <ui-input
-        v-if="isGetOrSet"
-        :model-value="data.domain"
-        class="mt-2 w-full"
-        label="Domain (optional)"
-        placeholder=".example.com"
-        @change="updateData({ domain: $event })"
-      />
-      <ui-input
-        v-if="data.type === 'set'"
-        :model-value="data.sameSite"
-        class="mt-2 w-full"
-        label="sameSite (optional)"
-        placeholder="lax"
-        @change="updateData({ sameSite: $event })"
-      />
-      <ui-input
-        v-if="data.type === 'set'"
-        :model-value="data.expirationDate"
-        class="mt-2 w-full"
-        label="expirationDate (seconds) (optional)"
-        placeholder="3600"
-        @change="updateData({ expirationDate: $event })"
-      />
-      <div
-        v-if="data.type === 'set' || (data.type === 'get' && data.getAll)"
-        class="mt-4"
+      <ui-checkbox
+        :model-value="data.useJson"
+        block
+        class="mt-1"
+        @change="updateData({ useJson: $event })"
       >
-        <ui-checkbox
-          v-if="data.type === 'set'"
-          :model-value="data.httpOnly"
-          class="mr-4"
-          @change="updateData({ httpOnly: $event })"
+        {{ t('workflow.blocks.cookie.useJson') }}
+      </ui-checkbox>
+      <template v-if="data.useJson">
+        <shared-codemirror
+          :model-value="data.jsonCode"
+          :extensions="codemirrorExts"
+          lang="json"
+          class="mt-4 cookie-editor"
+          @change="updateData({ jsonCode: $event })"
+        />
+        <a
+          :href="`https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies/${
+            data.type === 'get' && data.getAll ? 'getAll' : data.type
+          }`"
+          rel="noopener"
+          class="underline mt-2 inline-block"
+          target="_blank"
         >
-          httpOnly
-        </ui-checkbox>
-        <ui-checkbox
-          :model-value="data.secure"
-          @change="updateData({ secure: $event })"
+          See all available properties
+        </a>
+      </template>
+      <template v-else>
+        <ui-input
+          :model-value="data.url"
+          class="mt-2 w-full"
+          type="url"
+          label="URL"
+          placeholder="https://example.com/"
+          @change="updateData({ url: $event })"
+        />
+        <ui-input
+          :model-value="data.name"
+          :label="`Name ${
+            data.type === 'get' && !data.getAll ? '' : '(optional)'
+          }`"
+          class="mt-2 w-full"
+          placeholder="site-cookie"
+          @change="updateData({ name: $event })"
+        />
+        <ui-input
+          v-if="data.type === 'set'"
+          :model-value="data.value"
+          label="Value (optional)"
+          class="mt-2 w-full"
+          placeholder="value"
+          @change="updateData({ value: $event })"
+        />
+        <ui-input
+          :model-value="data.path"
+          class="mt-2 w-full"
+          label="Path (optional)"
+          placeholder="/"
+          @change="updateData({ path: $event })"
+        />
+        <ui-input
+          v-if="isGetOrSet"
+          :model-value="data.domain"
+          class="mt-2 w-full"
+          label="Domain (optional)"
+          placeholder=".example.com"
+          @change="updateData({ domain: $event })"
+        />
+        <ui-input
+          v-if="data.type === 'set'"
+          :model-value="data.sameSite"
+          class="mt-2 w-full"
+          label="sameSite (optional)"
+          placeholder="lax"
+          @change="updateData({ sameSite: $event })"
+        />
+        <ui-input
+          v-if="data.type === 'set'"
+          :model-value="data.expirationDate"
+          class="mt-2 w-full"
+          label="expirationDate (seconds) (optional)"
+          placeholder="3600"
+          @change="updateData({ expirationDate: $event })"
+        />
+        <div
+          v-if="data.type === 'set' || (data.type === 'get' && data.getAll)"
+          class="mt-4"
         >
-          secure
-        </ui-checkbox>
-      </div>
+          <ui-checkbox
+            v-if="data.type === 'set'"
+            :model-value="data.httpOnly"
+            class="mr-4"
+            @change="updateData({ httpOnly: $event })"
+          >
+            httpOnly
+          </ui-checkbox>
+          <ui-checkbox
+            :model-value="data.secure"
+            @change="updateData({ secure: $event })"
+          >
+            secure
+          </ui-checkbox>
+        </div>
+      </template>
       <div v-if="data.type === 'get'" class="pt-4 border-t mt-4 cookie-data">
         <insert-workflow-data :data="data" variables @update="updateData" />
       </div>
@@ -114,11 +143,17 @@
   </div>
 </template>
 <script setup>
-import { computed } from 'vue';
+import { computed, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { autocompletion } from '@codemirror/autocomplete';
+import { syntaxTree } from '@codemirror/language';
 import { useHasPermissions } from '@/composable/hasPermissions';
 import InsertWorkflowData from './InsertWorkflowData.vue';
 
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
+
 const props = defineProps({
   data: {
     type: Object,
@@ -131,6 +166,16 @@ const { t } = useI18n();
 const permission = useHasPermissions(['cookies']);
 
 const types = ['get', 'set', 'remove'];
+const methodProps = {
+  name: { label: 'name', type: 'text' },
+  url: { label: 'url', type: 'text' },
+  path: { label: 'path', type: 'text' },
+  session: { label: 'session', type: 'text' },
+  secure: { label: 'secure', type: 'text' },
+  domain: { label: 'domain', type: 'text' },
+  sameSite: { label: 'sameSite', type: 'text' },
+  httpOnly: { label: 'httpOnly', type: 'text' },
+};
 
 const isGetOrSet = computed(
   () =>
@@ -138,12 +183,58 @@ const isGetOrSet = computed(
     props.data.type === 'set'
 );
 
+function cookieOptionsAutocomplete(context) {
+  const word = context.matchBefore(/\w*/);
+  const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
+
+  if (
+    nodeBefore.name !== 'PropertyName' ||
+    (word.from === word.to && !context.explicit)
+  )
+    return null;
+
+  let options = [];
+
+  if (props.data.type === 'get') {
+    if (props.data.getAll) {
+      options = [
+        methodProps.domain,
+        methodProps.name,
+        methodProps.path,
+        methodProps.secure,
+        methodProps.url,
+      ];
+    } else {
+      options = [methodProps.name, methodProps.url];
+    }
+  } else if (props.data.type === 'set') {
+    options = Object.values(methodProps);
+  } else if (props.data.type === 'remove') {
+    options = [methodProps.name, methodProps.url];
+  }
+
+  return {
+    options,
+    from: word.from,
+  };
+}
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
 }
+
+const codemirrorExts = [
+  autocompletion({
+    override: [cookieOptionsAutocomplete],
+  }),
+];
 </script>
 <style>
 .cookie-data .block-variable {
   margin-top: 0;
 }
+
+.cookie-editor .cm-tooltip-autocomplete {
+  margin-left: 0px !important;
+  margin-top: -5px !important;
+}
 </style>

+ 29 - 12
src/components/newtab/workflow/edit/EditInsertData.vue

@@ -117,13 +117,21 @@
                 </ui-button>
                 <div class="flex-grow" />
                 <ui-select
-                  v-if="item.filePath.endsWith('.csv')"
-                  v-model="item.csvAction"
-                  placeholder="CSV File Action"
+                  :model-value="item.action || item.csvAction"
+                  placeholder="File Action"
+                  @change="item.action = $event"
                 >
-                  <option value="text">Read as text</option>
-                  <option value="json">Read as JSON</option>
-                  <option value="json-header">Read as JSON with headers</option>
+                  <option value="default">Default</option>
+                  <option value="base64">Read as base64</option>
+                  <optgroup
+                    v-if="item.filePath.endsWith('.csv')"
+                    label="CSV File"
+                  >
+                    <option value="json">Read as JSON</option>
+                    <option value="json-header">
+                      Read as JSON with headers
+                    </option>
+                  </optgroup>
                 </ui-select>
               </template>
             </div>
@@ -151,7 +159,7 @@ import { useI18n } from 'vue-i18n';
 import { useToast } from 'vue-toastification';
 import Papa from 'papaparse';
 import browser from 'webextension-polyfill';
-import getFile from '@/utils/getFile';
+import getFile, { readFileAsBase64 } from '@/utils/getFile';
 import EditAutocomplete from './EditAutocomplete.vue';
 
 const SharedCodemirror = defineAsyncComponent(() =>
@@ -200,7 +208,7 @@ function addItem() {
     value: '',
     filePath: '',
     isFile: false,
-    csvAction: 'text',
+    action: 'default',
   });
 }
 function changeItemType(index, type) {
@@ -223,19 +231,28 @@ async function previewData(index, item) {
     const path = item.filePath || '';
     const isJSON = path.endsWith('.json');
     const isCSV = path.endsWith('.csv');
+    let action = item.action || item.csvAction || 'default';
+
+    if (action === 'text' && !isCSV) action = 'default';
+
+    let stringify = isJSON && action !== 'base64';
+    let responseType = isJSON ? 'json' : 'text';
+
+    if (action === 'base64') responseType = 'blob';
 
-    let stringify = isJSON;
     let result = await getFile(path, {
+      responseType,
       returnValue: true,
-      responseType: isJSON ? 'json' : 'text',
     });
 
-    if (result && isCSV && item.csvAction && item.csvAction.includes('json')) {
+    if (result && isCSV && action && action.includes('json')) {
       const parsedCSV = Papa.parse(result, {
-        header: item.csvAction.includes('header'),
+        header: action.includes('header'),
       });
       result = parsedCSV.data || [];
       stringify = true;
+    } else if (action === 'base64') {
+      result = await readFileAsBase64(result);
     }
 
     previewState.itemId = index;

+ 0 - 2
src/components/newtab/workflow/edit/EditLoopData.vue

@@ -119,8 +119,6 @@
         :label="t('workflow.blocks.loop-data.startIndex')"
         placeholder="0"
         class="w-full mt-2"
-        min="0"
-        type="number"
         @change="updateData({ startIndex: +$event || 0 })"
       />
       <ui-checkbox

+ 1 - 1
src/components/newtab/workflow/edit/EditParameterPrompt.vue

@@ -9,7 +9,7 @@
     <ui-input
       :model-value="data.timeout"
       type="number"
-      label="Timeout (millisecond)"
+      label="Timeout (millisecond) (0 to disable)"
       class="w-full mt-2"
       @change="updateData({ timeout: +$event })"
     />

+ 17 - 8
src/components/newtab/workflow/edit/Trigger/TriggerSpecificDay.vue

@@ -46,7 +46,7 @@
     </div>
     <div class="grid grid-cols-2 gap-x-4 gap-y-2 mt-4">
       <ui-expand
-        v-for="(day, index) in sortedDaysArr"
+        v-for="day in sortedDaysArr"
         :key="day.id"
         header-class="focus:ring-0 flex items-center w-full group text-left"
         type="time"
@@ -60,7 +60,7 @@
             <v-remixicon
               name="riDeleteBin7Line"
               class="mr-1 group invisible group-hover:visible inline-block"
-              @click="daysArr.splice(index, 1)"
+              @click="removeDay(day.id)"
             />
             {{ day.times.length }}x
           </span>
@@ -68,7 +68,7 @@
         <div class="grid grid-cols-2 gap-1 mb-1">
           <div
             v-for="(time, timeIndex) in day.times"
-            :key="time"
+            :key="day.id + time"
             class="flex items-center p-2 border rounded-lg group"
           >
             <span class="flex-1"> {{ formatTime(time) }} </span>
@@ -76,7 +76,7 @@
               name="riDeleteBin7Line"
               class="cursor-pointer"
               size="18"
-              @click.stop="removeDayTime(index, timeIndex)"
+              @click.stop="removeDayTime(day.id, timeIndex)"
             />
           </div>
         </div>
@@ -131,11 +131,20 @@ function formatTime(time) {
     .second(seconds || 0)
     .format('hh:mm:ss A');
 }
-function removeDayTime(index, timeIndex) {
-  daysArr.value[index].times.splice(timeIndex, 1);
+function removeDay(dayId) {
+  const dayIndex = daysArr.value.findIndex((day) => day.id === dayId);
+  if (dayIndex === -1) return;
 
-  if (daysArr.value[index].times.length === 0) {
-    daysArr.value.splice(index, 1);
+  daysArr.value.splice(dayIndex, 1);
+}
+function removeDayTime(dayId, timeIndex) {
+  const dayIndex = daysArr.value.findIndex((day) => day.id === dayId);
+  if (dayIndex === -1) return;
+
+  daysArr.value[dayIndex].times.splice(timeIndex, 1);
+
+  if (daysArr.value[dayIndex].times.length === 0) {
+    daysArr.value.splice(dayIndex, 1);
   }
 }
 function addTime() {

+ 6 - 25
src/components/newtab/workflow/editor/EditorCustomEdge.vue

@@ -1,16 +1,12 @@
 <template>
-  <path
+  <base-edge
     :id="id"
     :style="style"
-    class="vue-flow__edge-path"
-    :d="edgePath"
+    :path="path[0]"
     :marker-end="markerEnd"
-  />
-  <edge-text
-    v-if="label"
-    :x="center[0]"
-    :y="center[1]"
     :label="label"
+    :label-x="path[1]"
+    :label-y="path[2]"
     :label-style="{ fill: 'white' }"
     :label-show-bg="true"
     :label-bg-style="{ fill: '#3b82f6' }"
@@ -20,12 +16,7 @@
 </template>
 <script setup>
 import { computed } from 'vue';
-import {
-  getBezierPath,
-  getSmoothStepPath,
-  getEdgeCenter,
-  EdgeText,
-} from '@vue-flow/core';
+import { BaseEdge, getBezierPath, getSmoothStepPath } from '@vue-flow/core';
 
 const props = defineProps({
   id: {
@@ -78,17 +69,7 @@ const props = defineProps({
   },
 });
 
-const center = computed(() => {
-  if (!props.label) return null;
-
-  return getEdgeCenter({
-    sourceX: props.sourceX,
-    sourceY: props.sourceY,
-    targetX: props.targetX,
-    targetY: props.targetY,
-  });
-});
-const edgePath = computed(() => {
+const path = computed(() => {
   const options = {
     sourceX: props.sourceX,
     sourceY: props.sourceY,

+ 14 - 2
src/components/ui/UiModal.vue

@@ -7,7 +7,8 @@
       <transition name="modal" mode="out-in">
         <div
           v-if="show"
-          class="overflow-y-auto modal-ui__content-container z-50 flex justify-center items-center"
+          :class="[positions[contentPosition]]"
+          class="overflow-y-auto modal-ui__content-container z-50 flex justify-center"
           :style="{ 'backdrop-filter': blur && 'blur(2px)' }"
         >
           <div
@@ -68,6 +69,10 @@ export default {
       type: String,
       default: 'p-4',
     },
+    contentPosition: {
+      type: String,
+      default: 'center',
+    },
     customContent: Boolean,
     persist: Boolean,
     blur: Boolean,
@@ -75,6 +80,11 @@ export default {
   },
   emits: ['close', 'update:modelValue'],
   setup(props, { emit }) {
+    const positions = {
+      center: 'items-center',
+      start: 'items-start',
+    };
+
     const show = ref(false);
     const modalContent = ref(null);
 
@@ -98,7 +108,6 @@ export default {
       () => props.modelValue,
       (value) => {
         show.value = value;
-        toggleBodyOverflow(value);
       },
       { immediate: true }
     );
@@ -106,10 +115,13 @@ export default {
     watch(show, (value) => {
       if (value) window.addEventListener('keyup', keyupHandler);
       else window.removeEventListener('keyup', keyupHandler);
+
+      toggleBodyOverflow(value);
     });
 
     return {
       show,
+      positions,
       closeModal,
       modalContent,
     };

+ 5 - 3
src/content/blocksHandler/handlerJavascriptCode.js

@@ -1,13 +1,15 @@
 import { jsContentHandler } from '@/workflowEngine/utils/javascriptBlockUtil';
 
 function javascriptCode({ data, isPreloadScripts, frameSelector }) {
-  if (!isPreloadScripts) return jsContentHandler(...data);
+  if (!isPreloadScripts && Array.isArray(data))
+    return jsContentHandler(...data);
+  if (!data.scripts) return Promise.resolve({ success: true });
 
   let $documentCtx = document;
 
   if (frameSelector) {
     const iframeCtx = document.querySelector(frameSelector)?.contentDocument;
-    if (!iframeCtx) return Promise.resolve(false);
+    if (!iframeCtx) return Promise.resolve({ success: false });
 
     $documentCtx = iframeCtx;
   }
@@ -29,7 +31,7 @@ function javascriptCode({ data, isPreloadScripts, frameSelector }) {
     $documentCtx.documentElement.appendChild(scriptEl);
   });
 
-  return Promise.resolve(true);
+  return Promise.resolve({ success: true });
 }
 
 export default javascriptCode;

+ 30 - 7
src/content/blocksHandler/handlerTakeScreenshot.js

@@ -62,7 +62,7 @@ async function takeScreenshot(tabId, options) {
 
   return imageUrl;
 }
-async function captureElement({ selector, tabId, options }) {
+async function captureElement({ selector, tabId, options, $frameRect }) {
   const element = document.querySelector(selector);
 
   if (!element) {
@@ -77,7 +77,6 @@ async function captureElement({ selector, tabId, options }) {
   });
 
   await sleep(500);
-
   const imageUrl = await takeScreenshot(tabId, options);
   const image = await loadAsyncImg(imageUrl);
 
@@ -85,8 +84,16 @@ async function captureElement({ selector, tabId, options }) {
   const context = canvas.getContext('2d');
   const { height, width, x, y } = element.getBoundingClientRect();
 
-  const diffHeight = image.height / window.innerHeight;
-  const diffWidth = image.width / window.innerWidth;
+  let windowWidth = window.innerWidth;
+  let windowHeight = window.innerHeight;
+
+  if ($frameRect) {
+    windowWidth = $frameRect.windowWidth;
+    windowHeight = $frameRect.windowHeight;
+  }
+
+  const diffWidth = image.width / windowWidth;
+  const diffHeight = image.height / windowHeight;
 
   const newWidth = width * diffWidth;
   const newHeight = height * diffHeight;
@@ -94,10 +101,21 @@ async function captureElement({ selector, tabId, options }) {
   canvas.width = newWidth;
   canvas.height = newHeight;
 
+  let xPos = x;
+  let yPos = y;
+
+  if ($frameRect) {
+    yPos += $frameRect.y;
+    xPos += $frameRect.x;
+  }
+
+  xPos *= diffWidth;
+  yPos *= diffHeight;
+
   context.drawImage(
     image,
-    x * diffHeight,
-    y * diffWidth,
+    xPos,
+    yPos,
     newWidth,
     newHeight,
     0,
@@ -109,12 +127,17 @@ async function captureElement({ selector, tabId, options }) {
   return canvasToBase64(canvas, options);
 }
 
-export default async function ({ tabId, options, data: { type, selector } }) {
+export default async function ({
+  tabId,
+  options,
+  data: { type, selector, $frameRect },
+}) {
   if (type === 'element') {
     const imageUrl = await captureElement({
       tabId,
       options,
       selector,
+      $frameRect,
     });
 
     return imageUrl;

+ 6 - 1
src/content/elementSelector/App.vue

@@ -80,7 +80,12 @@
           @highlight="toggleHighlightElement"
           @execute="state.isExecuting = $event"
         />
-        <div v-if="state.showSettings && !state.hide" class="mt-4">
+        <div
+          v-if="
+            state.showSettings && state.selectorType === 'css' && !state.hide
+          "
+          class="mt-4"
+        >
           <p class="font-semibold mb-4">Selector settings</p>
           <ul class="space-y-4">
             <li>

+ 2 - 1
src/content/handleSelector.js

@@ -104,7 +104,8 @@ export default async function (
 
     return elements;
   } catch (error) {
-    console.error(error);
+    if (onError) onError(error);
+
     throw error;
   }
 }

+ 17 - 3
src/content/index.js

@@ -4,9 +4,8 @@ import cloneDeep from 'lodash.clonedeep';
 import findSelector from '@/lib/findSelector';
 import { sendMessage } from '@/utils/message';
 import automa from '@business';
-import FindElement from '@/utils/FindElement';
 import { toCamelCase, isXPath } from '@/utils/helper';
-import handleSelector from './handleSelector';
+import handleSelector, { queryElements } from './handleSelector';
 import blocksHandler from './blocksHandler';
 import showExecutedBlock from './showExecutedBlock';
 import shortcutListener from './services/shortcutListener';
@@ -53,7 +52,12 @@ async function executeBlock(data) {
       findBy = isXPath(frameSelector) ? 'xpath' : 'cssSelector';
     }
 
-    const frameElement = FindElement[findBy]({ selector: frameSelector });
+    const frameElement = await queryElements({
+      findBy,
+      multiple: false,
+      waitForSelector: 5000,
+      selector: frameSelector,
+    });
     const frameError = (message) => {
       const error = new Error(message);
       error.data = { selector: frameSelector };
@@ -66,7 +70,16 @@ async function executeBlock(data) {
     const isFrameEelement = ['IFRAME', 'FRAME'].includes(frameElement.tagName);
     if (!isFrameEelement) throw frameError('not-iframe');
 
+    const { x, y } = frameElement.getBoundingClientRect();
+    const iframeDetails = { x, y };
+
+    if (isMainFrame) {
+      iframeDetails.windowWidth = window.innerWidth;
+      iframeDetails.windowHeight = window.innerHeight;
+    }
+
     data.data.selector = selector;
+    data.data.$frameRect = iframeDetails;
     data.data.$frameSelector = frameSelector;
 
     if (frameElement.contentDocument) {
@@ -207,6 +220,7 @@ function messageListener({ data, source }) {
         executeBlock(data)
           .then(resolve)
           .catch((error) => {
+            console.error(error);
             const elNotFound = error.message === 'element-not-found';
             const isLoopItem = data.data?.selector?.includes('automa-loop');
             if (elNotFound && isLoopItem) {

+ 3 - 0
src/content/services/webService.js

@@ -191,6 +191,9 @@ window.addEventListener('DOMContentLoaded', async () => {
         const isInstalled = packages.some((pkg) => pkg.id === data);
 
         sendMessageBack(type, isInstalled);
+      } else if (type === 'get-workflows') {
+        const storage = await browser.storage.local.get('workflows');
+        sendMessageBack(type, storage.workflows);
       }
     });
   } catch (error) {

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

@@ -112,7 +112,8 @@
           "set": "Set cookie",
           "remove": "Remove cookies",
           "getAll": "Get all cookies"
-        }
+        },
+        "useJson": "Use JSON format"
       },
       "note": {
         "name": "Note"

+ 11 - 2
src/newtab/App.vue

@@ -4,6 +4,7 @@
     <main :class="{ 'pl-16': $route.name !== 'recording' }">
       <router-view />
     </main>
+    <app-logs />
     <ui-dialog>
       <template #auth>
         <div class="text-center">
@@ -76,10 +77,12 @@ import { getUserWorkflows } from '@/utils/api';
 import { getWorkflowPermissions } from '@/utils/workflowData';
 import { sendMessage } from '@/utils/message';
 import { workflowState, startWorkflowExec } from '@/workflowEngine';
+import emitter from '@/lib/mitt';
 import automa from '@business';
 import dbLogs from '@/db/logs';
 import dayjs from '@/lib/dayjs';
 import AppSurvey from '@/components/newtab/app/AppSurvey.vue';
+import AppLogs from '@/components/newtab/app/AppLogs.vue';
 import AppSidebar from '@/components/newtab/app/AppSidebar.vue';
 import dataMigration from '@/utils/dataMigration';
 import iconFirefox from '@/assets/svg/logoFirefox.svg';
@@ -204,6 +207,12 @@ const messageEvents = {
   'refresh-packages': function () {
     packageStore.loadData(true);
   },
+  'open-logs': function (data) {
+    emitter.emit('ui:logs', {
+      show: true,
+      logId: data.logId,
+    });
+  },
   'workflow:added': function (data) {
     if (data.source === 'team') {
       teamWorkflowStore.loadData().then(() => {
@@ -230,8 +239,8 @@ const messageEvents = {
         });
     }
   },
-  'workflow:execute': function ({ data }) {
-    startWorkflowExec(data, data?.options ?? {});
+  'workflow:execute': function ({ data, options }) {
+    startWorkflowExec(data, options ?? data?.options ?? {});
   },
   'recording:stop': stopRecording,
   'background--recording:stop': stopRecording,

+ 9 - 187
src/newtab/pages/logs/[id].vue

@@ -1,198 +1,20 @@
 <template>
-  <div v-if="currentLog.id" class="container pt-8 pb-4">
-    <div class="flex items-center">
-      <button
-        v-tooltip:bottom="t('workflow.blocks.go-back.name')"
-        role="button"
-        class="h-12 px-1 transition mr-2 bg-input rounded-lg dark:text-gray-300 text-gray-600"
-        @click="goBack"
-      >
-        <v-remixicon name="riArrowLeftSLine" />
-      </button>
-      <div>
-        <h1 class="text-2xl max-w-md text-overflow font-semibold">
-          {{ currentLog.name }}
-        </h1>
-        <p class="text-gray-600 dark:text-gray-200">
-          {{
-            t(`log.description.text`, {
-              status: t(
-                `log.description.status.${currentLog.status || 'success'}`
-              ),
-              date: dayjs(currentLog.startedAt).format('DD MMM'),
-              duration: countDuration(currentLog.startedAt, currentLog.endedAt),
-            })
-          }}
-        </p>
-      </div>
-      <div class="flex-grow"></div>
-      <ui-button
-        v-if="state.workflowExists"
-        v-tooltip="t('log.goWorkflow')"
-        icon
-        class="mr-4"
-        @click="goToWorkflow"
-      >
-        <v-remixicon name="riExternalLinkLine" />
-      </ui-button>
-      <ui-button class="text-red-500 dark:text-red-400" @click="deleteLog">
-        {{ t('common.delete') }}
-      </ui-button>
-    </div>
-    <ui-tabs v-model="state.activeTab" class="mt-4" @change="onTabChange">
-      <ui-tab v-for="tab in tabs" :key="tab.id" class="mr-4" :value="tab.id">
-        {{ tab.name }}
-      </ui-tab>
-    </ui-tabs>
-    <ui-tab-panels
-      :model-value="state.activeTab"
-      class="mt-4 pb-4"
-      style="min-height: 500px"
-    >
-      <ui-tab-panel value="logs">
-        <logs-history
-          :current-log="currentLog"
-          :ctx-data="ctxData"
-          :parent-log="parentLog"
-        />
-      </ui-tab-panel>
-      <ui-tab-panel value="table">
-        <logs-table :current-log="currentLog" :table-data="tableData" />
-      </ui-tab-panel>
-      <ui-tab-panel value="variables">
-        <logs-variables :current-log="currentLog" />
-      </ui-tab-panel>
-    </ui-tab-panels>
-  </div>
+  <p>Hello :)</p>
 </template>
 <script setup>
-import { shallowReactive, shallowRef, watch } from 'vue';
+import { onMounted } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
-import { useI18n } from 'vue-i18n';
-import dbLogs from '@/db/logs';
-import dayjs from '@/lib/dayjs';
-import { useWorkflowStore } from '@/stores/workflow';
-import { countDuration, convertArrObjTo2DArr } from '@/utils/helper';
-import LogsTable from '@/components/newtab/logs/LogsTable.vue';
-import LogsHistory from '@/components/newtab/logs/LogsHistory.vue';
-import LogsVariables from '@/components/newtab/logs/LogsVariables.vue';
+import emitter from '@/lib/mitt';
 
-const { t } = useI18n();
 const route = useRoute();
 const router = useRouter();
-const workflowStore = useWorkflowStore();
 
-const ctxData = shallowRef({});
-const parentLog = shallowRef(null);
+onMounted(() => {
+  emitter.emit('ui:logs', {
+    show: true,
+    logId: route.params.id,
+  });
 
-const backHistory = window.history.state.back;
-const tabs = [
-  { id: 'logs', name: t('common.log', 2) },
-  { id: 'table', name: t('workflow.table.title') },
-  { id: 'variables', name: t('workflow.variables.title', 2) },
-];
-
-const state = shallowReactive({
-  activeTab: 'logs',
-  workflowExists: false,
-});
-const tableData = shallowReactive({
-  converted: false,
-  body: [],
-  header: [],
+  router.replace('/');
 });
-const currentLog = shallowRef({
-  history: [],
-  data: {
-    table: [],
-    variables: {},
-  },
-});
-
-function goBack() {
-  router.go(-1);
-}
-function deleteLog() {
-  dbLogs.items
-    .where('id')
-    .equals(route.params.id)
-    .delete()
-    .then(() => {
-      if (backHistory?.startsWith('/workflows')) {
-        router.replace(backHistory);
-        return;
-      }
-
-      router.replace('/logs');
-    });
-}
-function goToWorkflow() {
-  let path = `/workflows/${currentLog.value.workflowId}`;
-
-  if (backHistory?.startsWith(path)) {
-    path = backHistory;
-  }
-
-  router.push(path);
-}
-function convertToTableData() {
-  const data = currentLog.value.data?.table;
-  if (!data) return;
-
-  const [header] = convertArrObjTo2DArr(data);
-
-  tableData.converted = true;
-  tableData.body = data.map((item, index) => ({ ...item, id: index + 1 }));
-  tableData.header = header.map((name) => ({
-    text: name,
-    value: name,
-    filterable: true,
-  }));
-  tableData.header.unshift({ value: 'id', text: '', sortable: false });
-}
-function onTabChange(value) {
-  if (value === 'table' && !tableData.converted) {
-    convertToTableData();
-  }
-}
-async function fetchLog() {
-  const logId = route.params.id;
-  if (!logId) return;
-
-  const logDetail = await dbLogs.items.where('id').equals(logId).last();
-  if (!logDetail) return;
-
-  tableData.body = [];
-  tableData.header = [];
-  parentLog.value = null;
-  tableData.converted = false;
-
-  const [logCtxData, logHistory, logsData] = await Promise.all(
-    ['ctxData', 'histories', 'logsData'].map((key) =>
-      dbLogs[key].where('logId').equals(logId).last()
-    )
-  );
-
-  ctxData.value = logCtxData?.data || {};
-  currentLog.value = {
-    history: logHistory?.data || [],
-    data: logsData?.data || {},
-    ...logDetail,
-  };
-
-  state.workflowExists = Boolean(workflowStore.getById(logDetail.workflowId));
-
-  const parentLogId = logDetail.collectionLogId || logDetail.parentLog?.id;
-  if (parentLogId) {
-    parentLog.value =
-      (await dbLogs.items.where('id').equals(parentLogId).last()) || null;
-  }
-}
-
-watch(() => route.params, fetchLog, { immediate: true });
 </script>
-<style>
-.logs-details .cm-editor {
-  max-height: calc(100vh - 15rem);
-}
-</style>

+ 9 - 9
src/newtab/pages/workflows/Host.vue

@@ -30,12 +30,12 @@
         </div>
       </ui-card>
       <ui-tabs
-        v-model="state.activeTab"
+        model-value="'editor'"
         class="border-none px-2 rounded-lg h-full space-x-1 bg-white dark:bg-gray-800 ml-4"
         style="height: 48px"
       >
         <ui-tab value="editor">{{ t('common.editor') }}</ui-tab>
-        <ui-tab value="logs">
+        <ui-tab value="logs" @click="openLogs">
           {{ t('common.log', 2) }}
           <span
             v-if="workflowStates.length > 0"
@@ -101,12 +101,6 @@
           @init="onEditorInit"
         />
       </ui-tab-panel>
-      <ui-tab-panel value="logs">
-        <editor-logs
-          :workflow-id="workflowId"
-          :workflow-states="workflowStates"
-        />
-      </ui-tab-panel>
     </ui-tab-panels>
   </div>
 </template>
@@ -123,8 +117,8 @@ import { useWorkflowStore } from '@/stores/workflow';
 import { executeWorkflow } from '@/workflowEngine';
 import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import getTriggerText from '@/utils/triggerText';
-import EditorLogs from '@/components/newtab/workflow/editor/EditorLogs.vue';
 import WorkflowEditor from '@/components/newtab/workflow/WorkflowEditor.vue';
+import emitter from '@/lib/mitt';
 
 useGroupTooltip();
 
@@ -161,6 +155,12 @@ const workflowStates = computed(() =>
   workflowStore.getWorkflowStates(workflowId)
 );
 
+function openLogs() {
+  emitter.emit('ui:logs', {
+    workflowId,
+    show: true,
+  });
+}
 function syncWorkflow() {
   state.loadingSync = true;
   const hostId = {

+ 22 - 13
src/newtab/pages/workflows/[id].vue

@@ -57,8 +57,9 @@
           </div>
         </ui-card>
         <ui-tabs
-          v-model="state.activeTab"
+          :model-value="'editor'"
           class="border-none px-2 rounded-lg h-full space-x-1 bg-white dark:bg-gray-800 pointer-events-auto"
+          @change="onTabChange"
         >
           <button
             v-if="haveEditAccess"
@@ -234,12 +235,6 @@
             @duplicate="duplicateElements"
           />
         </ui-tab-panel>
-        <ui-tab-panel value="logs" class="mt-24 container">
-          <editor-logs
-            :workflow-id="route.params.id"
-            :workflow-states="workflowStates"
-          />
-        </ui-tab-panel>
       </ui-tab-panels>
     </div>
   </div>
@@ -320,9 +315,10 @@ import { getBlocks } from '@/utils/getSharedData';
 import { excludeGroupBlocks } from '@/utils/shared';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { useCommandManager } from '@/composable/commandManager';
-import { debounce, parseJSON, throttle } from '@/utils/helper';
+import { debounce, parseJSON, throttle, getActiveTab } from '@/utils/helper';
 import { executeWorkflow } from '@/workflowEngine';
 import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
+import emitter from '@/lib/mitt';
 import functions from '@/workflowEngine/templating/templatingFunctions';
 import browser from 'webextension-polyfill';
 import dbStorage from '@/db/storage';
@@ -340,7 +336,6 @@ import WorkflowDataTable from '@/components/newtab/workflow/WorkflowDataTable.vu
 import WorkflowGlobalData from '@/components/newtab/workflow/WorkflowGlobalData.vue';
 import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
 import SharedPermissionsModal from '@/components/newtab/shared/SharedPermissionsModal.vue';
-import EditorLogs from '@/components/newtab/workflow/editor/EditorLogs.vue';
 import EditorAddPackage from '@/components/newtab/workflow/editor/EditorAddPackage.vue';
 import EditorPkgActions from '@/components/newtab/workflow/editor/EditorPkgActions.vue';
 import EditorLocalCtxMenu from '@/components/newtab/workflow/editor/EditorLocalCtxMenu.vue';
@@ -587,7 +582,7 @@ const updateHostedWorkflow = throttle(async () => {
   if (!userStore.user || workflowPayload.isUpdating) return;
 
   const isHosted = userStore.hostedWorkflows[route.params.id];
-  const isBackup = userStore.backupIds.includes(route.params.id);
+  const isBackup = userStore.backupIds?.includes(route.params.id);
   const workflowExist = workflowStore.getById(route.params.id);
 
   if (
@@ -671,6 +666,16 @@ const onEdgesChange = debounce((changes) => {
   // if (command) commandManager.add(command);
 }, 250);
 
+function onTabChange(tabVal) {
+  if (tabVal !== 'logs') return;
+
+  state.activeTab = 'editor';
+
+  emitter.emit('ui:logs', {
+    workflowId,
+    show: true,
+  });
+}
 function onUpdateBlockSettings({ blockId, itemId, settings }) {
   state.dataChanged = true;
 
@@ -692,7 +697,10 @@ async function executeFromBlock(blockId) {
 
     const workflowOptions = { blockId };
 
-    const [tab] = await browser.tabs.query({ active: true, url: '*://*/*' });
+    let tab = await getActiveTab();
+    if (!tab) {
+      [tab] = await browser.tabs.query({ active: true, url: '*://*/*' });
+    }
     if (tab) {
       workflowOptions.tabId = tab.id;
     }
@@ -1306,7 +1314,7 @@ function onDropInEditor({ dataTransfer, clientX, clientY, target }) {
   }
 
   const block = parseJSON(dataTransfer.getData('block'), null);
-  if (!block) return;
+  if (!block || block.fromBlockBasic) return;
 
   if (block.id === 'trigger' && isPackage) return;
 
@@ -1563,7 +1571,8 @@ function checkWorkflowUpdate() {
 }
 
 useHead({
-  title: () => `${workflow.value?.name} workflow - Automa` || 'Automa',
+  title: () =>
+    `${workflow.value?.name} ${isPackage ? 'package' : 'workflow'}` || 'Automa',
 });
 const shortcut = useShortcut([
   getShortcut('editor:toggle-sidebar', toggleSidebar),

+ 0 - 12
src/newtab/router.js

@@ -8,9 +8,7 @@ import WorkflowShared from './pages/workflows/Shared.vue';
 import ScheduledWorkflow from './pages/ScheduledWorkflow.vue';
 import Storage from './pages/Storage.vue';
 import StorageTables from './pages/storage/Tables.vue';
-import Logs from './pages/Logs.vue';
 import LogsDetails from './pages/logs/[id].vue';
-import LogsRunning from './pages/logs/Running.vue';
 import Recording from './pages/Recording.vue';
 import Settings from './pages/Settings.vue';
 import SettingsIndex from './pages/settings/SettingsIndex.vue';
@@ -86,21 +84,11 @@ const routes = [
     path: '/storage/tables/:id',
     component: StorageTables,
   },
-  {
-    name: 'logs',
-    path: '/logs',
-    component: Logs,
-  },
   {
     name: 'logs-details',
     path: '/logs/:id',
     component: LogsDetails,
   },
-  {
-    name: 'logs-running',
-    path: '/logs/:id/running',
-    component: LogsRunning,
-  },
   {
     path: '/settings',
     component: Settings,

+ 11 - 2
src/newtab/utils/blocksValidation.js

@@ -255,8 +255,6 @@ export async function validateSaveAssets(data) {
 export async function validatePressKey(data) {
   const errors = [];
 
-  if (isEmptyStr(data.selector)) errors.push('The Selector is empty');
-
   const isKeyEmpty =
     !data.action || (data.action === 'press-key' && isEmptyStr(data.keys));
   const isMultipleKeysEmpty =
@@ -274,6 +272,13 @@ export async function validateNotification() {
   return [];
 }
 
+export async function validateCookie() {
+  const hasPermission = await checkPermissions(['cookies']);
+  if (!hasPermission) return ["Don't have cookies permissions"];
+
+  return [];
+}
+
 export default {
   trigger: {
     ...defaultOptions,
@@ -388,4 +393,8 @@ export default {
     ...defaultOptions,
     func: validateInteractionBasic,
   },
+  cookie: {
+    ...defaultOptions,
+    func: validateCookie,
+  },
 };

+ 1 - 17
src/newtab/utils/elementSelector.js

@@ -1,24 +1,8 @@
 import browser from 'webextension-polyfill';
-import { isXPath, sleep } from '@/utils/helper';
+import { isXPath, sleep, getActiveTab } from '@/utils/helper';
 
 const isMV2 = browser.runtime.getManifest().manifest_version === 2;
 
-async function getActiveTab() {
-  const currentWindow = await browser.windows.getCurrent();
-  if (currentWindow)
-    await browser.windows.update(currentWindow.id, { focused: false });
-
-  await sleep(200);
-
-  const [tab] = await browser.tabs.query({
-    active: true,
-    url: '*://*/*',
-    lastFocusedWindow: true,
-  });
-  if (!tab) throw new Error('No active tab');
-
-  return tab;
-}
 async function makeDashboardFocus() {
   const [currentTab] = await browser.tabs.query({
     active: true,

+ 8 - 9
src/params/App.vue

@@ -252,10 +252,9 @@ function runWorkflow(index, { data, params }) {
     });
 }
 function cancelParamBlock(index, { data }, message) {
-  const key = `params-prompt:${data.execId}__${data.blockId}`;
   browser.storage.local
     .set({
-      [key]: {
+      [data.promptId]: {
         message,
         $isError: true,
       },
@@ -265,15 +264,11 @@ function cancelParamBlock(index, { data }, message) {
     });
 }
 function continueWorkflow(index, { data, params }) {
-  if (Date.now() > data.timeout) {
-    deleteWorkflow(index);
-    return;
-  }
+  const timeout = Date.now() > data.timeout;
 
-  const key = `params-prompt:${data.execId}__${data.blockId}`;
   browser.storage.local
     .set({
-      [key]: getParamsValues(params),
+      [data.promptId]: timeout ? { $timeout: true } : getParamsValues(params),
     })
     .then(() => {
       deleteWorkflow(index);
@@ -308,7 +303,11 @@ browser.runtime.onMessage.addListener(({ name, data }) => {
     if (!checkTimeout) {
       checkTimeout = setInterval(() => {
         workflows.value.forEach((workflow, index) => {
-          if (workflow.type !== 'block' || Date.now() < workflow.data.timeout)
+          if (
+            workflow.type !== 'block' ||
+            Date.now() < workflow.data.timeout ||
+            workflow.data.timeoutMs <= 0
+          )
             return;
 
           cancelParamBlock(index, workflow, 'Timeout');

+ 21 - 5
src/popup/pages/Home.vue

@@ -10,6 +10,16 @@
     <div class="flex items-center mb-4">
       <h1 class="text-xl font-semibold text-white">Automa</h1>
       <div class="flex-grow"></div>
+      <ui-button
+        v-tooltip.group="
+          'Start recording by opening the dashboard. Click to learn more'
+        "
+        icon
+        class="mr-2"
+        @click="openDocs"
+      >
+        <v-remixicon name="riRecordCircleLine" />
+      </ui-button>
       <ui-button
         v-tooltip.group="
           t(`home.elementSelector.${state.haveAccess ? 'name' : 'noAccess'}`)
@@ -51,7 +61,7 @@
       <ui-tab v-if="hostedWorkflowStore.toArray.length > 0" value="host">
         {{ t(`home.workflow.type.host`) }}
       </ui-tab>
-      <ui-tab v-if="userStore.user?.teams" value="team"> Teams </ui-tab>
+      <ui-tab v-if="userStore.user?.teams?.length" value="team"> Teams </ui-tab>
     </ui-tabs>
   </div>
   <home-team-workflows
@@ -147,6 +157,7 @@ import { initElementSelector as initElementSelectorFunc } from '@/newtab/utils/e
 import automa from '@business';
 import HomeWorkflowCard from '@/components/popup/home/HomeWorkflowCard.vue';
 import HomeTeamWorkflows from '@/components/popup/home/HomeTeamWorkflows.vue';
+import BackgroundUtils from '@/background/BackgroundUtils';
 
 const isMV2 = browser.runtime.getManifest().manifest_version === 2;
 
@@ -211,9 +222,16 @@ const workflows = computed(() =>
   state.activeTab === 'local' ? localWorkflows.value : hostedWorkflows.value
 );
 const showTab = computed(
-  () => hostedWorkflowStore.toArray.length > 0 || userStore.user?.teams
+  () =>
+    hostedWorkflowStore.toArray.length > 0 || userStore.user?.teams?.length > 0
 );
 
+function openDocs() {
+  window.open(
+    'https://docs.automa.site/guide/quick-start.html#recording-actions',
+    '_blank'
+  );
+}
 function closeSettingsPopup() {
   state.showSettingsPopup = false;
   localStorage.setItem('settingsPopup', false);
@@ -287,9 +305,7 @@ function deleteWorkflow({ id, name }) {
   });
 }
 function openDashboard(url) {
-  sendMessage('open:dashboard', url, 'background').then(() => {
-    window.close();
-  });
+  BackgroundUtils.openDashboard(url);
 }
 async function initElementSelector() {
   const [tab] = await browser.tabs.query({

+ 16 - 3
src/utils/getFile.js

@@ -1,3 +1,13 @@
+export function readFileAsBase64(blob) {
+  return new Promise((resolve) => {
+    const reader = new FileReader();
+    reader.onload = () => {
+      resolve(reader.result);
+    };
+    reader.readAsDataURL(blob);
+  });
+}
+
 async function downloadFile(url, options) {
   const response = await fetch(url);
   if (!response.ok) throw new Error(response.statusText);
@@ -9,9 +19,12 @@ async function downloadFile(url, options) {
     return result;
   }
 
-  const objUrl = URL.createObjectURL(result);
-
-  return { objUrl, path: url, type: result.type };
+  if (URL.createObjectURL) {
+    const objUrl = URL.createObjectURL(result);
+    return { objUrl, path: url, type: result.type };
+  }
+  const base64 = await readFileAsBase64(result);
+  return { path: url, objUrl: base64, type: result.type };
 }
 function getLocalFile(path, options) {
   return new Promise((resolve, reject) => {

+ 36 - 0
src/utils/helper.js

@@ -1,5 +1,41 @@
 import browser from 'webextension-polyfill';
 
+export async function getActiveTab() {
+  try {
+    let windowId = null;
+    const tabsQuery = {
+      active: true,
+      url: '*://*/*',
+    };
+    const extURL = browser.runtime.getURL('');
+    const windows = await browser.windows.getAll({ populate: true });
+    for (const browserWindow of windows) {
+      const [tab] = browserWindow.tabs;
+      const isDashboard =
+        browserWindow.tabs.length === 1 && tab.url?.includes(extURL);
+
+      if (isDashboard) {
+        await browser.windows.update(browserWindow.id, {
+          focused: false,
+          state: 'minimized',
+        });
+      } else if (browserWindow.focused) {
+        windowId = browserWindow.id;
+      }
+    }
+
+    if (windowId) tabsQuery.windowId = windowId;
+    else if (windows.length > 2) tabsQuery.lastFocusedWindow = true;
+
+    const [tab] = await browser.tabs.query(tabsQuery);
+
+    return tab;
+  } catch (error) {
+    console.error(error);
+    return null;
+  }
+}
+
 export function isXPath(str) {
   const regex = /^([(/@]|id\()/;
 

+ 4 - 0
src/utils/shared.js

@@ -699,6 +699,7 @@ export const tasks = {
     refDataKeys: [
       'maxLoop',
       'loopData',
+      'startIndex',
       'variableName',
       'referenceKey',
       'elementSelector',
@@ -1278,11 +1279,14 @@ export const tasks = {
       'name',
       'url',
       'value',
+      'jsonCode',
     ],
     data: {
       disableBlock: false,
       description: '',
       type: 'get',
+      jsonCode: '{\n\n}',
+      useJson: false,
       getAll: false,
       domain: '',
       expirationDate: '',

+ 38 - 9
src/workflowEngine/WorkflowEngine.js

@@ -64,6 +64,17 @@ class WorkflowEngine {
     }
     this.options = options;
 
+    this.refDataSnapshots = {};
+    this.refDataSnapshotsKeys = {
+      loopData: {
+        index: 0,
+        key: '##loopData0',
+      },
+      variables: {
+        index: 0,
+        key: '##variables0',
+      },
+    };
     this.referenceData = {
       variables,
       table: [],
@@ -120,6 +131,10 @@ class WorkflowEngine {
         return;
       }
 
+      if (!this.workflow.settings) {
+        this.workflow.settings = {};
+      }
+
       blocks = getBlocks();
 
       const checkParams = this.options?.checkParams ?? true;
@@ -143,7 +158,7 @@ class WorkflowEngine {
           browser.windows.create({
             type: 'popup',
             width: 480,
-            height: 650,
+            height: 700,
             url: browser.runtime.getURL(
               `/params.html?workflowId=${this.workflow.id}`
             ),
@@ -173,7 +188,8 @@ class WorkflowEngine {
         {}
       );
 
-      const workflowTable = this.workflow.table || this.workflow.dataColumns;
+      const workflowTable =
+        this.workflow.table || this.workflow.dataColumns || [];
       let columns = Array.isArray(workflowTable)
         ? workflowTable
         : Object.values(workflowTable);
@@ -245,6 +261,8 @@ class WorkflowEngine {
         this.referenceData.variables[`$$${name}`] = value;
       });
 
+      this.addRefDataSnapshot('variables');
+
       await this.states.add(this.id, {
         id: this.id,
         state: this.state,
@@ -258,6 +276,16 @@ class WorkflowEngine {
     }
   }
 
+  addRefDataSnapshot(key) {
+    this.refDataSnapshotsKeys[key].index += 1;
+    this.refDataSnapshotsKeys[
+      key
+    ].key = `##${key}${this.refDataSnapshotsKeys[key].index}`;
+
+    const keyName = this.refDataSnapshotsKeys[key].key;
+    this.refDataSnapshots[keyName] = cloneDeep(this.referenceData[key]);
+  }
+
   addWorker(detail) {
     this.workerId += 1;
 
@@ -285,15 +313,13 @@ class WorkflowEngine {
       detail.name === 'javascript-code' ||
       (blocks[detail.name]?.refDataKeys && this.saveLog)
     ) {
-      const { activeTabUrl, variables, loopData } = JSON.parse(
-        JSON.stringify(this.referenceData)
-      );
+      const { variables, loopData } = this.refDataSnapshotsKeys;
 
       this.historyCtxData[this.logHistoryId] = {
         referenceData: {
-          loopData,
-          variables,
-          activeTabUrl,
+          loopData: loopData.key,
+          variables: variables.key,
+          activeTabUrl: detail.activeTabUrl,
           prevBlockData: detail.prevBlockData || '',
         },
         replacedValue: cloneDeep(detail.replacedValue),
@@ -481,7 +507,10 @@ class WorkflowEngine {
           },
           ctxData: {
             logId: this.id,
-            data: this.historyCtxData,
+            data: {
+              ctxData: this.historyCtxData,
+              dataSnapshot: this.refDataSnapshots,
+            },
           },
           data: {
             logId: this.id,

+ 5 - 5
src/workflowEngine/WorkflowWorker.js

@@ -87,6 +87,7 @@ class WorkflowWorker {
 
   setVariable(name, value) {
     this.engine.referenceData.variables[name] = value;
+    this.engine.addRefDataSnapshot('variables');
   }
 
   getBlockConnections(blockId, outputIndex = 1) {
@@ -196,6 +197,7 @@ class WorkflowWorker {
         blockId: block.id,
         workerId: this.id,
         timestamp: startExecuteTime,
+        activeTabUrl: this.activeTab?.url,
         replacedValue: replacedBlock.replacedValue,
         duration: Math.round(Date.now() - startExecuteTime),
         ...obj,
@@ -291,12 +293,11 @@ class WorkflowWorker {
           this.executeNextBlocks(nodeConnections, error.data || '');
         }, blockDelay);
       } else if (onError === 'restart-workflow' && !this.parentWorkflow) {
-        const restartKey = `restart-count:${this.id}`;
-        const restartCount = +localStorage.getItem(restartKey) || 0;
+        const restartCount = this.engine.restartWorkersCount[this.id] || 0;
         const maxRestart = this.settings.restartTimes ?? 3;
 
         if (restartCount >= maxRestart) {
-          localStorage.removeItem(restartKey);
+          delete this.engine.restartWorkersCount[this.id];
           this.engine.destroy('error', error.message, errorLogItem);
           return;
         }
@@ -306,7 +307,7 @@ class WorkflowWorker {
         const triggerBlock = this.engine.blocks[this.engine.triggerBlockId];
         if (triggerBlock) this.executeBlock(triggerBlock, execParam);
 
-        localStorage.setItem(restartKey, restartCount + 1);
+        this.engine.restartWorkersCount[this.id] = restartCount + 1;
       } else {
         this.engine.destroy('error', error.message, errorLogItem);
       }
@@ -374,7 +375,6 @@ class WorkflowWorker {
         frameSelector: this.frameSelector,
         ...payload,
       };
-
       const data = await browser.tabs.sendMessage(
         this.activeTab.id,
         messagePayload,

+ 30 - 27
src/workflowEngine/blocksHandler/handlerActiveTab.js

@@ -15,39 +15,42 @@ async function activeTab(block) {
       return data;
     }
 
-    const minimizeDashboard = async (currentWindow) => {
-      if (currentWindow.type !== 'popup') return;
-
-      const [tab] = currentWindow.tabs;
-      const isDashboard = tab && tab.url.includes(browser.runtime.getURL(''));
-      const isWindowFocus =
-        currentWindow.focused || currentWindow.state === 'maximized';
-
-      if (isWindowFocus && isDashboard) {
-        const windowOptions = { focused: false };
-        if (currentWindow.state === 'maximized')
-          windowOptions.state = 'minimized';
+    const tabsQuery = {
+      active: true,
+      url: '*://*/*',
+    };
 
-        await browser.windows.update(currentWindow.id, windowOptions);
+    if (BROWSER_TYPE === 'firefox') {
+      tabsQuery.currentWindow = true;
+    } else if (this.engine.isPopup) {
+      let windowId = null;
+      const extURL = browser.runtime.getURL('');
+      const windows = await browser.windows.getAll({ populate: true });
+      for (const browserWindow of windows) {
+        const [tab] = browserWindow.tabs;
+        const isDashboard =
+          browserWindow.tabs.length === 1 && tab.url?.includes(extURL);
+
+        if (isDashboard) {
+          await browser.windows.update(browserWindow.id, {
+            focused: false,
+            state: 'minimized',
+          });
+        } else if (browserWindow.focused) {
+          windowId = browserWindow.id;
+        }
       }
-    };
 
-    if (this.engine.isPopup) {
-      const currentWindow = await browser.windows.getCurrent({
-        populate: true,
-      });
-      await minimizeDashboard(currentWindow);
+      if (windowId) tabsQuery.windowId = windowId;
+      else if (windows.length > 2) tabsQuery.lastFocusedWindow = true;
     } else {
-      const allWindows = await browser.windows.getAll({ populate: true });
-      for (const currWindow of allWindows) {
-        await minimizeDashboard(currWindow);
-      }
+      tabsQuery.currentWindow = true;
     }
 
-    const [tab] = await browser.tabs.query({
-      active: true,
-      lastFocusedWindow: true,
-    });
+    const [tab] = await browser.tabs.query(tabsQuery);
+    if (!tab) {
+      throw new Error("Can't find active tab");
+    }
     if (!tab || !tab?.url.startsWith('http')) {
       const error = new Error('invalid-active-tab');
       error.data = { url: tab?.url };

+ 1 - 1
src/workflowEngine/blocksHandler/handlerClipboard.js

@@ -21,7 +21,7 @@ function doCommand(command, value) {
 }
 
 export default async function ({ data, id, label }) {
-  if (!this.engine.isPopup)
+  if (!this.engine.isPopup && !this.engins.isMV2)
     throw new Error('Clipboard block is not supported in background execution');
 
   const hasPermission = await browser.permissions.contains({

+ 21 - 13
src/workflowEngine/blocksHandler/handlerCookie.js

@@ -1,4 +1,5 @@
 import browser from 'webextension-polyfill';
+import { parseJSON } from '@/utils/helper';
 
 function getValues(data, keys) {
   const values = {};
@@ -46,23 +47,30 @@ async function cookie({ data, id }) {
   let key = data.type;
   if (key === 'get' && data.getAll) key = 'getAll';
 
-  const values = getValues(data, keys[key]);
-  if (values.expirationDate) {
-    values.expirationDate = Date.now() / 1000 + +values.expirationDate;
-  }
-
   let result = null;
 
-  if (data.type === 'remove' && !data.name) {
-    const cookies = await browser.cookies.getAll({ url: data.url });
-    const removePromise = cookies.map(({ name }) =>
-      browser.cookies.remove({ name, url: data.url })
-    );
-    await Promise.allSettled(removePromise);
+  if (data.useJson) {
+    const obj = parseJSON(data.jsonCode, null);
+    if (!obj) throw new Error('Invalid JSON format');
 
-    result = cookies;
+    result = await browser.cookies[key](obj);
   } else {
-    result = await browser.cookies[key](values);
+    const values = getValues(data, keys[key]);
+    if (values.expirationDate) {
+      values.expirationDate = Date.now() / 1000 + +values.expirationDate;
+    }
+
+    if (data.type === 'remove' && !data.name) {
+      const cookies = await browser.cookies.getAll({ url: data.url });
+      const removePromise = cookies.map(({ name }) =>
+        browser.cookies.remove({ name, url: data.url })
+      );
+      await Promise.allSettled(removePromise);
+
+      result = cookies;
+    } else {
+      result = await browser.cookies[key](values);
+    }
   }
 
   if (data.type === 'get') {

+ 43 - 4
src/workflowEngine/blocksHandler/handlerHandleDialog.js

@@ -1,4 +1,5 @@
-import { sendDebugCommand } from '../helper';
+import browser from 'webextension-polyfill';
+import { sendDebugCommand, checkCSPAndInject } from '../helper';
 
 const overwriteDialog = (accept, promptText) => `
   const realConfirm = window.confirm;
@@ -22,18 +23,56 @@ async function handleDialog({ data, id: blockId }) {
     const isScriptExist = this.preloadScripts.some(({ id }) => id === blockId);
 
     if (!isScriptExist) {
+      const jsCode = overwriteDialog(data.accept, data.promptText);
       const payload = {
         id: blockId,
         isBlock: true,
         name: 'javascript-code',
+        isPreloadScripts: true,
         data: {
           everyNewTab: true,
-          code: overwriteDialog(data.accept, data.promptText),
+          scripts: [{ data: { code: jsCode }, id: blockId }],
         },
       };
 
-      this.preloadScripts.push(payload);
-      await this._sendMessageToTab(payload, {}, true);
+      if (this.engine.isMV2) {
+        this.preloadScripts.push(payload);
+        await this._sendMessageToTab(payload, {}, true);
+      } else {
+        const target = { tabId: this.activeTab.id, allFrames: true };
+        const { debugMode } = this.engine.workflow.settings;
+        const cspResult = await checkCSPAndInject(
+          {
+            target,
+            debugMode,
+            injectOptions: {
+              injectImmediately: true,
+            },
+          },
+          () => jsCode
+        );
+        if (!cspResult.isBlocked) {
+          await browser.scripting.executeScript({
+            target,
+            args: [data],
+            world: 'MAIN',
+            injectImmediately: true,
+            func: (blockData) => {
+              window.confirm = function () {
+                return blockData.accept;
+              };
+
+              window.alert = function () {
+                return blockData.accept;
+              };
+
+              window.prompt = function () {
+                return blockData.accept ? blockData.promptText : null;
+              };
+            },
+          });
+        }
+      }
     }
   } else {
     this.dialogParams = {

+ 12 - 9
src/workflowEngine/blocksHandler/handlerInsertData.js

@@ -1,6 +1,6 @@
 import Papa from 'papaparse';
 import { parseJSON } from '@/utils/helper';
-import getFile from '@/utils/getFile';
+import getFile, { readFileAsBase64 } from '@/utils/getFile';
 import renderString from '../templating/renderString';
 
 async function insertData({ id, data }, { refData }) {
@@ -19,21 +19,24 @@ async function insertData({ id, data }, { refData }) {
       const isJSON = path.endsWith('.json');
       const isCSV = path.endsWith('.csv');
 
+      let action = item.action || item.csvAction || 'default';
+      if (action === 'text' && !isCSV) action = 'default';
+
+      let responseType = isJSON ? 'json' : 'text';
+      if (action === 'base64') responseType = 'blob';
+
       let result = await getFile(path, {
+        responseType,
         returnValue: true,
-        responseType: isJSON ? 'json' : 'text',
       });
 
-      if (
-        result &&
-        isCSV &&
-        item.csvAction &&
-        item.csvAction.includes('json')
-      ) {
+      if (result && isCSV && action && action.includes('json')) {
         const parsedCSV = Papa.parse(result, {
-          header: item.csvAction.includes('header'),
+          header: action.includes('header'),
         });
         result = parsedCSV.data || [];
+      } else if (action === 'base64') {
+        result = await readFileAsBase64(result);
       }
 
       value = result;

+ 1 - 0
src/workflowEngine/blocksHandler/handlerLoopBreakpoint.js

@@ -70,6 +70,7 @@ async function loopBreakpoint(block, { prevBlockData }) {
 
   delete this.loopList[block.data.loopId];
   delete this.engine.referenceData.loopData[block.data.loopId];
+  this.engine.addRefDataSnapshot('loopData');
 
   return {
     data: prevBlockData,

+ 9 - 4
src/workflowEngine/blocksHandler/handlerLoopData.js

@@ -76,10 +76,12 @@ async function loopData({ data, id }, { refData }) {
           throw new Error('invalid-loop-data');
         }
 
-        if (data.resumeLastWorkflow) {
+        const startIndex = +data.startIndex;
+
+        if (data.resumeLastWorkflow && this.engine.isPopup) {
           index = JSON.parse(localStorage.getItem(`index:${id}`)) || 0;
-        } else if (data.startIndex > 0) {
-          index = data.startIndex;
+        } else if (!Number.isNaN(startIndex) && startIndex > 0) {
+          index = startIndex;
         }
 
         if (data.reverseLoop && data.loopThrough !== 'elements') {
@@ -106,9 +108,12 @@ async function loopData({ data, id }, { refData }) {
             : currLoopData[index],
         $index: index,
       };
+      this.engine.addRefDataSnapshot('loopData');
     }
 
-    localStorage.setItem(`index:${id}`, this.loopList[data.loopId].index);
+    if (this.engine.isPopup) {
+      localStorage.setItem(`index:${id}`, this.loopList[data.loopId].index);
+    }
 
     return {
       data: refData.loopData[data.loopId],

+ 17 - 17
src/workflowEngine/blocksHandler/handlerParameterPrompt.js

@@ -1,18 +1,19 @@
+import { nanoid } from 'nanoid/non-secure';
 import browser from 'webextension-polyfill';
 import { sleep } from '@/utils/helper';
 
-function getInputtedParams({ execId, blockId }, ms) {
+function getInputtedParams(promptId, ms = 10000) {
   return new Promise((resolve, reject) => {
-    let timeout = null;
-    const key = `params-prompt:${execId}__${blockId}`;
+    const timeout = null;
 
     const storageListener = (event) => {
-      if (!event[key]) return;
+      if (!event[promptId]) return;
 
       clearTimeout(timeout);
       browser.storage.onChanged.removeListener(storageListener);
+      browser.storage.local.remove(promptId);
 
-      const { newValue } = event[key];
+      const { newValue } = event[promptId];
       if (newValue.$isError) {
         reject(new Error(newValue.message));
         return;
@@ -21,10 +22,12 @@ function getInputtedParams({ execId, blockId }, ms) {
       resolve(newValue);
     };
 
-    timeout = setTimeout(() => {
-      browser.storage.onChanged.removeListener(storageListener);
-      resolve({});
-    }, ms || 10000);
+    if (ms > 0) {
+      setTimeout(() => {
+        browser.storage.onChanged.removeListener(storageListener);
+        resolve({});
+      }, ms);
+    }
 
     browser.storage.onChanged.addListener(storageListener);
   });
@@ -52,12 +55,15 @@ export default async function ({ data, id }) {
     await browser.windows.update(tab.windowId, { focused: true });
   }
 
-  const timeout = data.timeout || 20000;
+  const promptId = `params-prompt:${nanoid(4)}__${id}`;
+  const { timeout } = data;
 
   await browser.tabs.sendMessage(tab.id, {
     name: 'workflow:params-block',
     data: {
+      promptId,
       blockId: id,
+      timeoutMs: timeout,
       execId: this.engine.id,
       params: data.parameters,
       timeout: Date.now() + timeout,
@@ -67,13 +73,7 @@ export default async function ({ data, id }) {
     },
   });
 
-  const result = await getInputtedParams(
-    {
-      blockId: id,
-      execId: this.engine.id,
-    },
-    timeout
-  );
+  const result = await getInputtedParams(promptId, timeout);
 
   Object.entries(result).forEach(([varName, varValue]) => {
     this.setVariable(varName, varValue);

+ 5 - 1
src/workflowEngine/blocksHandler/handlerWebhook.js

@@ -84,7 +84,11 @@ export async function webhook({ data, id }, { refData }) {
       data: returnData,
     };
   } catch (error) {
-    if (fallbackOutput && error.message === 'Failed to fetch') {
+    const fallbackErrors = ['Failed to fetch', 'user aborted'];
+    const executeFallback =
+      fallbackOutput &&
+      fallbackErrors.some((message) => error.message.includes(message));
+    if (executeFallback) {
       return {
         data: '',
         nextBlockId: fallbackOutput,

+ 2 - 1
src/workflowEngine/helper.js

@@ -228,7 +228,7 @@ export function injectPreloadScript({ target, scripts, frameSelector }) {
 }
 
 export async function checkCSPAndInject(
-  { target, debugMode, options = {} },
+  { target, debugMode, options = {}, injectOptions = {} },
   callback
 ) {
   const [isBlockedByCSP] = await browser.scripting.executeScript({
@@ -273,6 +273,7 @@ export async function checkCSPAndInject(
       });
     },
     world: 'MAIN',
+    ...(injectOptions || {}),
   });
 
   if (isBlockedByCSP.result) {

+ 16 - 6
src/workflowEngine/index.js

@@ -1,3 +1,4 @@
+import { toRaw } from 'vue';
 import browser from 'webextension-polyfill';
 import dayjs from '@/lib/dayjs';
 import decryptFlow, { getWorkflowPass } from '@/utils/decryptFlow';
@@ -45,7 +46,12 @@ export function startWorkflowExec(workflowData, options, isPopup = true) {
     }
   }
 
-  const convertedWorkflow = convertWorkflowData(workflowData);
+  const clonedWorkflowData = {};
+  Object.keys(workflowData).forEach((key) => {
+    clonedWorkflowData[key] = toRaw(workflowData[key]);
+  });
+
+  const convertedWorkflow = convertWorkflowData(clonedWorkflowData);
   const engine = new WorkflowEngine(convertedWorkflow, {
     options,
     isPopup,
@@ -65,10 +71,13 @@ export function startWorkflowExec(workflowData, options, isPopup = true) {
       endedTimestamp,
       blockDetail,
     }) => {
-      if (workflowData.id.startsWith('team') && workflowData.teamId) {
+      if (
+        clonedWorkflowData.id.startsWith('team') &&
+        clonedWorkflowData.teamId
+      ) {
         const payload = {
           status,
-          workflowId: workflowData.id,
+          workflowId: clonedWorkflowData.id,
           workflowLog: {
             status,
             endedTimestamp,
@@ -97,7 +106,7 @@ export function startWorkflowExec(workflowData, options, isPopup = true) {
           };
         }
 
-        fetchApi(`/teams/${workflowData.teamId}/workflows/logs`, {
+        fetchApi(`/teams/${clonedWorkflowData.teamId}/workflows/logs`, {
           method: 'POST',
           body: JSON.stringify(payload),
         }).catch((error) => {
@@ -109,9 +118,10 @@ export function startWorkflowExec(workflowData, options, isPopup = true) {
         browser.permissions
           .contains({ permissions: ['notifications'] })
           .then((hasPermission) => {
-            if (!hasPermission || !workflowData.settings.notification) return;
+            if (!hasPermission || !clonedWorkflowData.settings.notification)
+              return;
 
-            const name = workflowData.name.slice(0, 32);
+            const name = clonedWorkflowData.name.slice(0, 32);
 
             browser.notifications.create(`logs:${id}`, {
               type: 'basic',

+ 34 - 24
yarn.lock

@@ -1719,10 +1719,10 @@
   dependencies:
     "@types/node" "*"
 
-"@types/web-bluetooth@^0.0.15":
-  version "0.0.15"
-  resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz#d60330046a6ed8a13b4a53df3813c44942ebdf72"
-  integrity sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA==
+"@types/web-bluetooth@^0.0.16":
+  version "0.0.16"
+  resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz#1d12873a8e49567371f2a75fe3e7f7edca6662d8"
+  integrity sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==
 
 "@types/ws@^8.5.1":
   version "8.5.3"
@@ -1736,17 +1736,20 @@
   resolved "https://registry.yarnpkg.com/@viselect/vanilla/-/vanilla-3.1.1.tgz#e63ef3529f819cc83e8c1aea6b3ab9936d550f76"
   integrity sha512-aS1UF6WkV3TvO5vqg2uQk8WRI36b2SAtfozVrxtfVI1WTuaOG0uYxvdXTmIYwfki6fFF4qQpkiQcg8+NqpJA8Q==
 
-"@vue-flow/additional-components@1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@vue-flow/additional-components/-/additional-components-1.0.0.tgz#527edb54ed6ce527cd0370a46ea4ca74a6a7d8c2"
-  integrity sha512-xHDe5r60unpn2YuNEPdNAOgaS2KghdpVMOo5dVFdYJeRDaW8e0tVyXIUzGaP2iDT9t1WL1rlM/8BXizfFIdBaw==
+"@vue-flow/additional-components@^1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@vue-flow/additional-components/-/additional-components-1.2.4.tgz#251173d6eb95bf189393049aa1d906fbbe0fa380"
+  integrity sha512-WaZ8IDXO8M3WhlxwZyjCj1OSL3icrRY9M+Y9wQFYbBgFIDOj4V5E2SR7KGhrBZ4L0Amv1pezbI19p+v/DymC1g==
+  dependencies:
+    d3-selection "^3.0.0"
+    d3-zoom "^3.0.0"
 
-"@vue-flow/core@1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@vue-flow/core/-/core-1.0.0.tgz#b342863b4b96635d6e914c6c57141e37ba4ebbde"
-  integrity sha512-CU0Q8o31vXFC8BdOoPdRU7rN0EkgAFNwlG/gLR18Kk6ZnIflAc8BcaT3fShoVXNZLysXg10UF5C/L4wWPk2Gjw==
+"@vue-flow/core@^1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@vue-flow/core/-/core-1.4.1.tgz#43d289213580b27a72fa3255fe067ca829a54025"
+  integrity sha512-8Ngh756YPE6tHxXXVt+TRQZAxw/3q9skynRWVUh34cehK0w7jLspCFv3OlHl5kQ7b6DMfhN80gKKJ7ERePROGw==
   dependencies:
-    "@vueuse/core" "^9.3.0"
+    "@vueuse/core" "^9.5.0"
     d3-drag "^3.0.0"
     d3-selection "^3.0.0"
     d3-zoom "^3.0.0"
@@ -1904,14 +1907,14 @@
   resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.41.tgz#fbc95422df654ea64e8428eced96ba6ad555d2bb"
   integrity sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw==
 
-"@vueuse/core@^9.3.0":
-  version "9.3.0"
-  resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-9.3.0.tgz#74d855bd19cb5eadd2edb30c871918fac881e8b8"
-  integrity sha512-64Rna8IQDWpdrJxgitDg7yv1yTp41ZmvV8zlLEylK4QQLWAhz1OFGZDPZ8bU4lwcGgbEJ2sGi2jrdNh4LttUSQ==
+"@vueuse/core@^9.5.0":
+  version "9.5.0"
+  resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-9.5.0.tgz#6726e952e8f92b465457d3bc95deb385aacd9a41"
+  integrity sha512-6GsWBsJHEb3sYw15mbLrcbslAVY45pkzjJYTKYKCXv88z7srAF0VEW0q+oXKsl58tCbqooplInahXFg8Yo1m4w==
   dependencies:
-    "@types/web-bluetooth" "^0.0.15"
-    "@vueuse/metadata" "9.3.0"
-    "@vueuse/shared" "9.3.0"
+    "@types/web-bluetooth" "^0.0.16"
+    "@vueuse/metadata" "9.5.0"
+    "@vueuse/shared" "9.5.0"
     vue-demi "*"
 
 "@vueuse/head@^0.9.7":
@@ -1923,10 +1926,10 @@
     "@zhead/schema" "^0.8.5"
     "@zhead/schema-vue" "^0.8.5"
 
-"@vueuse/metadata@9.3.0":
-  version "9.3.0"
-  resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.3.0.tgz#c107fe77a577e1f221536cd1b291039c0c7c4bce"
-  integrity sha512-GnnfjbzIPJIh9ngL9s9oGU1+Hx/h5/KFqTfJykzh/1xjaHkedV9g0MASpdmPZIP+ynNhKAcEfA6g5i8KXwtoMA==
+"@vueuse/metadata@9.5.0":
+  version "9.5.0"
+  resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.5.0.tgz#b01c84230261ddee4d439ae5d9c21343dc5ae565"
+  integrity sha512-4M1AyPZmIv41pym+K5+4wup3bKuYebbH8w8BROY1hmT7rIwcyS4tEL+UsGz0Hiu1FCOxcoBrwtAizc0YmBJjyQ==
 
 "@vueuse/rxjs@^9.1.1":
   version "9.3.0"
@@ -1943,6 +1946,13 @@
   dependencies:
     vue-demi "*"
 
+"@vueuse/shared@9.5.0":
+  version "9.5.0"
+  resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-9.5.0.tgz#f5306548af0dc9f2b3a0d4da74e62bfdd6211241"
+  integrity sha512-HnnCWU1Vg9CVWRCcI8ohDKDRB2Sc4bTgT1XAIaoLSfVHHn+TKbrox6pd3klCSw4UDxkhDfOk8cAdcK+Z5KleCA==
+  dependencies:
+    vue-demi "*"
+
 "@vuex-orm/core@^0.36.4":
   version "0.36.4"
   resolved "https://registry.yarnpkg.com/@vuex-orm/core/-/core-0.36.4.tgz#9e2b1b8dfd74c2a508f1862ffa3e4a2c1e4cc60c"