1
0
Эх сурвалжийг харах

Merge branch 'AutomaApp:dev' into dev

Zen 3 жил өмнө
parent
commit
5ce6ff42b3
55 өөрчлөгдсөн 1551 нэмэгдсэн , 774 устгасан
  1. 1 0
      .eslintrc.js
  2. 6 3
      package.json
  3. 13 5
      src/assets/css/fonts.css
  4. BIN
      src/assets/fonts/jetbrains-mono-v6-latin-regular.woff
  5. BIN
      src/assets/fonts/jetbrains-mono-v6-latin-regular.woff2
  6. BIN
      src/assets/fonts/source-code-pro-v21-latin-600.woff
  7. BIN
      src/assets/fonts/source-code-pro-v21-latin-600.woff2
  8. BIN
      src/assets/fonts/source-code-pro-v21-latin-regular.woff
  9. BIN
      src/assets/fonts/source-code-pro-v21-latin-regular.woff2
  10. 11 7
      src/background/WorkflowLogger.js
  11. 2 0
      src/background/index.js
  12. 8 0
      src/background/workflowEngine/blocksHandler/handlerLoopBreakpoint.js
  13. 15 3
      src/background/workflowEngine/blocksHandler/handlerLoopData.js
  14. 34 25
      src/background/workflowEngine/engine.js
  15. 7 6
      src/background/workflowEngine/worker.js
  16. 8 7
      src/components/newtab/app/AppSidebar.vue
  17. 72 56
      src/components/newtab/logs/LogsDataViewer.vue
  18. 5 6
      src/components/newtab/logs/LogsFilters.vue
  19. 218 0
      src/components/newtab/logs/LogsHistory.vue
  20. 145 0
      src/components/newtab/logs/LogsTable.vue
  21. 71 0
      src/components/newtab/logs/LogsVariables.vue
  22. 1 1
      src/components/newtab/shared/SharedCodemirror.vue
  23. 128 44
      src/components/newtab/shared/SharedLogsTable.vue
  24. 1 1
      src/components/newtab/shared/SharedWysiwyg.vue
  25. 2 1
      src/components/newtab/workflow/WorkflowBuilder.vue
  26. 32 3
      src/components/newtab/workflow/edit/EditGoogleSheets.vue
  27. 2 2
      src/components/newtab/workflow/edit/EditInsertData.vue
  28. 1 1
      src/components/newtab/workflow/edit/EditNewTab.vue
  29. 1 0
      src/components/newtab/workflow/edit/EditWebhook.vue
  30. 14 1
      src/components/ui/UiExpand.vue
  31. 3 7
      src/components/ui/UiTextarea.vue
  32. 6 0
      src/composable/liveQuery.js
  33. 13 2
      src/content/blocksHandler/handlerEventClick.js
  34. 36 10
      src/content/blocksHandler/handlerLoopData.js
  35. 30 1
      src/content/index.js
  36. 22 0
      src/db/logs.js
  37. 2 0
      src/lib/vRemixicon.js
  38. 4 0
      src/locales/en/newtab.json
  39. 2 1
      src/manifest.firefox.json
  40. 29 18
      src/newtab/App.vue
  41. 39 67
      src/newtab/pages/Home.vue
  42. 49 60
      src/newtab/pages/Logs.vue
  43. 11 14
      src/newtab/pages/collections/[id].vue
  44. 116 0
      src/newtab/pages/logs/Running.vue
  45. 121 212
      src/newtab/pages/logs/[id].vue
  46. 6 15
      src/newtab/pages/workflows/Host.vue
  47. 44 43
      src/newtab/pages/workflows/[id].vue
  48. 7 2
      src/newtab/router.js
  49. 42 17
      src/store/index.js
  50. 56 0
      src/utils/dataMigration.js
  51. 7 0
      src/utils/handleFormElement.js
  52. 5 0
      src/utils/shared.js
  53. 1 1
      tailwind.config.js
  54. 3 0
      webpack.config.js
  55. 99 132
      yarn.lock

+ 1 - 0
.eslintrc.js

@@ -37,6 +37,7 @@ module.exports = {
     'no-console': ['warn', { allow: ['warn', 'error'] }],
     'no-underscore-dangle': 'off',
     'func-names': 'off',
+    'vue/v-on-event-hyphenation': 'off',
     'import/no-named-default': 'off',
     'no-restricted-syntax': 'off',
     'vue/multi-word-component-names': 'off',

+ 6 - 3
package.json

@@ -42,12 +42,14 @@
     "@tiptap/starter-kit": "^2.0.0-beta.181",
     "@tiptap/vue-3": "^2.0.0-beta.90",
     "@viselect/vanilla": "^3.0.0-beta.13",
+    "@vueuse/rxjs": "^8.6.0",
     "@vuex-orm/core": "^0.36.4",
     "compare-versions": "^4.1.2",
     "crypto-js": "^4.1.1",
     "css-selector-generator": "^3.6.0",
     "dayjs": "^1.10.7",
     "defu": "^6.0.0",
+    "dexie": "^3.2.2",
     "drawflow": "^0.0.58",
     "idb": "^7.0.0",
     "lodash.clonedeep": "^4.5.0",
@@ -56,9 +58,10 @@
     "nanoid": "^3.2.0",
     "object-path": "^0.11.8",
     "papaparse": "^5.3.1",
+    "rxjs": "^7.5.5",
     "tippy.js": "^6.3.1",
     "v-remixicon": "^0.1.1",
-    "vue": "^3.2.31",
+    "vue": "^3.2.37",
     "vue-i18n": "^9.2.0-beta.29",
     "vue-inspector-agnostic": "^1.0.0",
     "vue-router": "^4.0.11",
@@ -73,7 +76,7 @@
     "@babel/preset-env": "7.15.6",
     "@intlify/vue-i18n-loader": "^4.2.0",
     "@tailwindcss/typography": "^0.5.1",
-    "@vue/compiler-sfc": "3.2.19",
+    "@vue/compiler-sfc": "^3.2.37",
     "archiver": "^5.3.0",
     "autoprefixer": "10.3.6",
     "babel-loader": "^8.2.2",
@@ -103,7 +106,7 @@
     "source-map-loader": "3.0.0",
     "tailwindcss": "^3.0.7",
     "terser-webpack-plugin": "5.2.4",
-    "vue-loader": "16.8.1",
+    "vue-loader": "^17.0.0",
     "webpack": "5.55.1",
     "webpack-cli": "4.9.2",
     "webpack-dev-server": "3.11.2"

+ 13 - 5
src/assets/css/fonts.css

@@ -7,13 +7,21 @@
   src: url('../fonts/Inter-roman-latin.var.woff2') format("woff2");
 }
 
-/* fira-code-regular - latin */
-/* jetbrains-mono-regular - latin */
+/* source-code-pro-regular - latin */
 @font-face {
-  font-family: 'JetBrains Mono';
+  font-family: 'Source Code Pro';
   font-style: normal;
   font-weight: 400;
   src: local(''),
-       url('../fonts/jetbrains-mono-v6-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
-       url('../fonts/jetbrains-mono-v6-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+       url('../fonts/source-code-pro-v21-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+       url('../fonts/source-code-pro-v21-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
 }
+/* source-code-pro-600 - latin */
+@font-face {
+  font-family: 'Source Code Pro';
+  font-style: normal;
+  font-weight: 600;
+  src: local(''),
+       url('../fonts/source-code-pro-v21-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+       url('../fonts/source-code-pro-v21-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}

BIN
src/assets/fonts/jetbrains-mono-v6-latin-regular.woff


BIN
src/assets/fonts/jetbrains-mono-v6-latin-regular.woff2


BIN
src/assets/fonts/source-code-pro-v21-latin-600.woff


BIN
src/assets/fonts/source-code-pro-v21-latin-600.woff2


BIN
src/assets/fonts/source-code-pro-v21-latin-regular.woff


BIN
src/assets/fonts/source-code-pro-v21-latin-regular.woff2


+ 11 - 7
src/background/WorkflowLogger.js

@@ -1,15 +1,19 @@
+import dbLogs, { defaultLogItem } from '@/db/logs';
+/* eslint-disable class-methods-use-this */
 class WorkflowLogger {
-  constructor({ storage, key = 'logs' }) {
+  constructor({ key = 'logs' }) {
     this.key = key;
-    this.storage = storage;
   }
 
-  async add(data) {
-    const logs = (await this.storage.get(this.key)) || [];
+  async add({ detail, history, ctxData, data }) {
+    const logDetail = { ...defaultLogItem, ...detail };
 
-    logs.unshift(data);
-
-    await this.storage.set(this.key, logs);
+    await Promise.all([
+      dbLogs.logsData.add(data),
+      dbLogs.ctxData.add(ctxData),
+      dbLogs.items.add(logDetail),
+      dbLogs.histories.add(history),
+    ]);
   }
 }
 

+ 2 - 0
src/background/index.js

@@ -93,6 +93,8 @@ const workflow = {
     } else {
       engine.init();
       engine.on('destroyed', ({ id, status }) => {
+        if (status === 'stopped') return;
+
         browser.permissions
           .contains({ permissions: ['notifications'] })
           .then((hasPermission) => {

+ 8 - 0
src/background/workflowEngine/blocksHandler/handlerLoopBreakpoint.js

@@ -23,6 +23,14 @@ function loopBreakpoint(block, { prevBlockData }) {
         nextBlockId: currentLoop.blockId,
       });
     } else {
+      if (currentLoop.type === 'elements') {
+        const loopElsIndex = this.loopEls.findIndex(
+          ({ blockId }) => blockId === currentLoop.blockId
+        );
+
+        if (loopElsIndex !== -1) this.loopEls.splice(loopElsIndex, 1);
+      }
+
       delete this.loopList[block.data.loopId];
       delete this.engine.referenceData.loopData[block.data.loopId];
 

+ 15 - 3
src/background/workflowEngine/blocksHandler/handlerLoopData.js

@@ -35,16 +35,28 @@ async function loopData({ data, id, outputs }, { refData }) {
           return parseJSON(variableVal, variableVal);
         },
         elements: async () => {
-          const elements = await this._sendMessageToTab({
+          const max = +data.maxLoop || 0;
+          const findBy = isXPath(data.elementSelector)
+            ? 'xpath'
+            : 'cssSelector';
+          const { elements, url, loopId } = await this._sendMessageToTab({
             id,
             name: 'loop-data',
             data: {
+              max,
+              findBy,
               multiple: true,
-              max: +data.maxLoop || 0,
               selector: data.elementSelector,
-              findBy: isXPath(data.elementSelector) ? 'xpath' : 'cssSelector',
             },
           });
+          this.loopEls.push({
+            url,
+            max,
+            loopId,
+            findBy,
+            blockId: id,
+            selector: data.elementSelector,
+          });
 
           return elements;
         },

+ 34 - 25
src/background/workflowEngine/engine.js

@@ -5,18 +5,16 @@ import { clearCache, sleep, parseJSON, isObject } from '@/utils/helper';
 import Worker from './worker';
 
 class WorkflowEngine {
-  constructor(
-    workflow,
-    { states, logger, blocksHandler, parentWorkflow, options }
-  ) {
+  constructor(workflow, { states, logger, blocksHandler, options }) {
     this.id = nanoid();
     this.states = states;
     this.logger = logger;
     this.workflow = workflow;
     this.blocksHandler = blocksHandler;
-    this.parentWorkflow = parentWorkflow;
+    this.parentWorkflow = options?.parentWorkflow;
     this.saveLog = workflow.settings?.saveLog ?? true;
 
+    this.workerId = 0;
     this.workers = new Map();
     this.waitConnections = {};
 
@@ -166,7 +164,10 @@ class WorkflowEngine {
   }
 
   addWorker(detail) {
-    const worker = new Worker(this);
+    this.workerId += 1;
+
+    const workerId = `worker-${this.workerId}`;
+    const worker = new Worker(workerId, this);
     worker.init(detail);
 
     this.workers.set(worker.id, worker);
@@ -178,7 +179,7 @@ class WorkflowEngine {
     const isLimit = this.history.length >= 1001;
     const notErrorLog = detail.type !== 'error';
 
-    if ((!this.saveLog || isLimit) && notErrorLog) return;
+    if (!this.saveLog && isLimit && notErrorLog) return;
 
     this.logHistoryId += 1;
     detail.id = this.logHistoryId;
@@ -273,24 +274,31 @@ class WorkflowEngine {
       if (!this.workflow.isTesting) {
         const { name, id } = this.workflow;
 
-        let { logsCtxData } = await browser.storage.local.get('logsCtxData');
-        if (!logsCtxData) logsCtxData = {};
-        logsCtxData[this.id] = this.historyCtxData;
-        await browser.storage.local.set({ logsCtxData });
-
         await this.logger.add({
-          name,
-          status,
-          message,
-          id: this.id,
-          workflowId: id,
-          endedAt: endedTimestamp,
-          parentLog: this.parentWorkflow,
-          startedAt: this.startedTimestamp,
-          history: this.saveLog ? this.history : [],
+          detail: {
+            name,
+            status,
+            message,
+            id: this.id,
+            workflowId: id,
+            endedAt: endedTimestamp,
+            parentLog: this.parentWorkflow,
+            startedAt: this.startedTimestamp,
+          },
+          history: {
+            logId: this.id,
+            data: this.saveLog ? this.history : [],
+          },
+          ctxData: {
+            logId: this.id,
+            data: this.historyCtxData,
+          },
           data: {
-            table: this.referenceData.table,
-            variables: this.referenceData.variables,
+            logId: this.id,
+            data: {
+              table: this.referenceData.table,
+              variables: this.referenceData.variables,
+            },
           },
         });
       }
@@ -334,13 +342,14 @@ class WorkflowEngine {
       tabIds: [],
       currentBlock: [],
       name: this.workflow.name,
+      logs: this.history.slice(-5),
       startedTimestamp: this.startedTimestamp,
     };
 
     this.workers.forEach((worker) => {
-      const { id, name } = worker.currentBlock;
+      const { id, name, startedAt } = worker.currentBlock;
 
-      state.currentBlock.push({ id, name });
+      state.currentBlock.push({ id, name, startedAt });
       state.tabIds.push(worker.activeTab.id);
     });
 

+ 7 - 6
src/background/workflowEngine/worker.js

@@ -1,4 +1,3 @@
-import { nanoid } from 'nanoid';
 import browser from 'webextension-polyfill';
 import { toCamelCase, sleep, objectHasKey, isObject } from '@/utils/helper';
 import { tasks } from '@/utils/shared';
@@ -6,11 +5,12 @@ import referenceData from '@/utils/referenceData';
 import { convertData, waitTabLoaded, getBlockConnection } from './helper';
 
 class Worker {
-  constructor(engine) {
-    this.id = nanoid(5);
+  constructor(id, engine) {
+    this.id = id;
     this.engine = engine;
     this.settings = engine.workflow.settings;
 
+    this.loopEls = [];
     this.loopList = {};
     this.repeatedTasks = {};
     this.preloadScripts = [];
@@ -111,8 +111,9 @@ class Worker {
       return;
     }
 
+    const startExecuteTime = Date.now();
     const prevBlock = this.currentBlock;
-    this.currentBlock = block;
+    this.currentBlock = { ...block, startedAt: startExecuteTime };
 
     if (!isRetry) {
       await this.engine.updateState({
@@ -121,8 +122,6 @@ class Worker {
       });
     }
 
-    const startExecuteTime = Date.now();
-
     const blockHandler = this.engine.blocksHandler[toCamelCase(block.name)];
     const handler =
       !blockHandler && tasks[block.name].category === 'interaction'
@@ -153,6 +152,7 @@ class Worker {
         prevBlockData,
         type: status,
         name: block.name,
+        blockId: block.id,
         workerId: this.id,
         description: block.data.description,
         replacedValue: replacedBlock.replacedValue,
@@ -319,6 +319,7 @@ class Worker {
         isBlock: true,
         debugMode,
         executedBlockOnWeb,
+        loopEls: this.loopEls,
         activeTabId: this.activeTab.id,
         frameSelector: this.frameSelector,
         ...payload,

+ 8 - 7
src/components/newtab/app/AppSidebar.vue

@@ -37,6 +37,12 @@
           <div class="p-2 rounded-lg transition-colors inline-block">
             <v-remixicon :name="tab.icon" />
           </div>
+          <span
+            v-if="tab.id === 'log' && runningWorkflowsLen > 0"
+            class="absolute h-4 w-4 text-xs dark:text-black text-white rounded-full bg-accent -top-1 right-2"
+          >
+            {{ runningWorkflowsLen }}
+          </span>
         </a>
       </router-link>
     </div>
@@ -80,7 +86,7 @@
   </aside>
 </template>
 <script setup>
-import { ref } from 'vue';
+import { ref, computed } from 'vue';
 import { useStore } from 'vuex';
 import { useI18n } from 'vue-i18n';
 import { useRouter } from 'vue-router';
@@ -97,12 +103,6 @@ const router = useRouter();
 
 const extensionVersion = browser.runtime.getManifest().version;
 const tabs = [
-  {
-    id: 'dashboard',
-    icon: 'riHome5Line',
-    path: '/',
-    shortcut: getShortcut('page:dashboard', '/'),
-  },
   {
     id: 'workflow',
     icon: 'riFlowChart',
@@ -130,6 +130,7 @@ const tabs = [
 ];
 const hoverIndicator = ref(null);
 const showHoverIndicator = ref(false);
+const runningWorkflowsLen = computed(() => store.state.workflowState.length);
 
 useShortcut(
   tabs.map(({ shortcut }) => shortcut),

+ 72 - 56
src/components/newtab/logs/LogsDataViewer.vue

@@ -1,50 +1,61 @@
 <template>
-  <div class="flex items-center mb-2">
-    <ui-input
-      v-model="state.fileName"
-      :placeholder="t('common.fileName')"
-      :title="t('common.fileName')"
-    />
-    <div class="flex-grow"></div>
-    <ui-popover trigger-width>
-      <template #trigger>
-        <ui-button variant="accent">
-          <span>{{ t('log.exportData.title') }}</span>
-          <v-remixicon name="riArrowDropDownLine" class="ml-2 -mr-1" />
-        </ui-button>
-      </template>
-      <ui-list class="space-y-1">
-        <ui-list-item
-          v-for="type in dataExportTypes"
-          :key="type.id"
-          v-close-popover
-          class="cursor-pointer"
-          @click="exportData(type.id)"
-        >
-          {{ t(`log.exportData.types.${type.id}`) }}
-        </ui-list-item>
-      </ui-list>
-    </ui-popover>
+  <div v-if="state.status === 'loading'" class="text-center py-8">
+    <ui-spinner color="text-primary" />
   </div>
-  <ui-tabs v-if="objectHasKey(log.data, 'table')" v-model="state.activeTab">
-    <ui-tab value="table">
-      {{ t('workflow.table.title') }}
-    </ui-tab>
-    <ui-tab value="variables">
-      {{ t('workflow.variables.title', 2) }}
-    </ui-tab>
-  </ui-tabs>
-  <shared-codemirror
-    :model-value="dataStr"
-    :class="editorClass"
-    class="rounded-t-none"
-    lang="json"
-    readonly
-  />
+  <template v-else-if="state.status === 'idle'">
+    <div class="flex items-center mb-2">
+      <ui-input
+        v-model="state.fileName"
+        :placeholder="t('common.fileName')"
+        :title="t('common.fileName')"
+      />
+      <div class="flex-grow"></div>
+      <ui-popover trigger-width>
+        <template #trigger>
+          <ui-button variant="accent">
+            <span>{{ t('log.exportData.title') }}</span>
+            <v-remixicon name="riArrowDropDownLine" class="ml-2 -mr-1" />
+          </ui-button>
+        </template>
+        <ui-list class="space-y-1">
+          <ui-list-item
+            v-for="type in dataExportTypes"
+            :key="type.id"
+            v-close-popover
+            class="cursor-pointer"
+            @click="exportData(type.id)"
+          >
+            {{ t(`log.exportData.types.${type.id}`) }}
+          </ui-list-item>
+        </ui-list>
+      </ui-popover>
+    </div>
+    <ui-tabs v-if="objectHasKey(logsData, 'table')" v-model="state.activeTab">
+      <ui-tab value="table">
+        {{ t('workflow.table.title') }}
+      </ui-tab>
+      <ui-tab value="variables">
+        {{ t('workflow.variables.title', 2) }}
+      </ui-tab>
+    </ui-tabs>
+    <shared-codemirror
+      :model-value="dataStr"
+      :class="editorClass"
+      class="rounded-t-none"
+      lang="json"
+      readonly
+    />
+  </template>
 </template>
 <script setup>
-import { shallowReactive, computed, defineAsyncComponent } from 'vue';
+import {
+  shallowReactive,
+  computed,
+  defineAsyncComponent,
+  onMounted,
+} from 'vue';
 import { useI18n } from 'vue-i18n';
+import dbLogs from '@/db/logs';
 import { dataExportTypes } from '@/utils/shared';
 import { objectHasKey } from '@/utils/helper';
 import dataExporter from '@/utils/dataExporter';
@@ -67,35 +78,40 @@ const props = defineProps({
 const { t } = useI18n();
 
 const state = shallowReactive({
+  status: 'loading',
   activeTab: 'table',
   fileName: props.log.name,
 });
-const cache = {
+const logsData = {
   table: '',
   variables: '',
 };
 
 const dataStr = computed(() => {
-  if (cache[state.activeTab]) return cache[state.activeTab];
-
-  let { data } = props.log;
-
-  if (objectHasKey(props.log.data, 'table')) {
-    data = props.log.data[state.activeTab];
-  }
-
-  data = JSON.stringify(data, null, 2);
-  /* eslint-disable-next-line */
-  cache[state.activeTab] = data;
+  if (state.status !== 'idle') return '';
 
-  return data;
+  return logsData[state.activeTab] ? logsData[state.activeTab] : '';
 });
 
 function exportData(type) {
   dataExporter(
-    props.log.data?.table || props.log.data,
+    logsData?.table || logsData,
     { name: state.fileName, type },
     true
   );
 }
+
+onMounted(async () => {
+  const data = await dbLogs.logsData.where('logId').equals(props.log.id).last();
+
+  if (!data) {
+    state.status = 'error';
+    return;
+  }
+
+  Object.keys(data.data).forEach((key) => {
+    logsData[key] = JSON.stringify(data.data[key], null, 2);
+  });
+  state.status = 'idle';
+});
 </script>

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

@@ -68,12 +68,11 @@
         </ui-select>
       </div>
     </ui-popover>
-    <ui-button
-      v-tooltip:bottom="t('log.clearLogs.title')"
-      icon
-      @click="$emit('clear')"
-    >
-      <v-remixicon name="riDeleteBin7Line" />
+    <ui-button @click="$emit('clear')">
+      <v-remixicon name="riDeleteBin7Line" class="mr-2 -ml-1" />
+      <span>
+        {{ t('log.clearLogs.title') }}
+      </span>
     </ui-button>
   </div>
 </template>

+ 218 - 0
src/components/newtab/logs/LogsHistory.vue

@@ -0,0 +1,218 @@
+<template>
+  <router-link
+    v-if="parentLog"
+    replace
+    :to="'/logs/' + currentLog.parentLog?.id || currentLog.collectionLogId"
+    class="mb-4 flex"
+  >
+    <v-remixicon name="riArrowLeftLine" class="mr-2" />
+    {{ t('log.goBack', { name: parentLog.name }) }}
+  </router-link>
+  <div
+    class="p-4 rounded-lg bg-gray-900 dark:bg-gray-800 text-gray-100 dark scroll overflow-auto"
+    style="max-height: 600px"
+  >
+    <slot name="prepend" />
+    <div class="text-sm font-mono space-y-1 w-full overflow-auto">
+      <ui-expand
+        v-for="(item, index) in history"
+        :key="item.id || index"
+        :disabled="!ctxData[item.id]"
+        hide-header-icon
+        class="hoverable rounded-md"
+        active-class="bg-box-transparent"
+        header-class="px-2 w-full text-left focus:ring-0 py-1 rounded-md group cursor-default flex items-start"
+        @click="state.itemId = item.id"
+      >
+        <template #header="{ show }">
+          <span class="w-6">
+            <v-remixicon
+              v-show="ctxData[item.id]"
+              :rotate="show ? 270 : 180"
+              size="20"
+              name="riArrowLeftSLine"
+              class="transition-transform text-gray-400 -ml-1 mr-2"
+            />
+          </span>
+          <span
+            :title="`${t('log.duration')}: ${Math.round(
+              item.duration / 1000
+            )}s`"
+            class="w-14 flex-shrink-0 text-overflow text-gray-400"
+          >
+            {{ countDuration(0, item.duration || 0) }}
+          </span>
+          <span
+            :class="logsType[item.type]?.color"
+            :title="item.type"
+            class="w-2/12 flex-shrink-0 text-overflow"
+          >
+            <v-remixicon
+              :name="logsType[item.type]?.icon"
+              size="18"
+              class="inline-block -mr-1 align-text-top"
+            />
+            {{ item.name }}
+          </span>
+          <span
+            :title="`${t('common.description')} (${item.description})`"
+            class="ml-2 w-2/12 text-overflow flex-shrink-0"
+          >
+            {{ item.description }}
+          </span>
+          <p
+            :title="item.message"
+            class="text-sm line-clamp ml-2 flex-1 leading-tight text-gray-600 dark:text-gray-200"
+          >
+            {{ item.message }}
+          </p>
+          <router-link
+            v-if="item.logId"
+            v-slot="{ navigate }"
+            :to="{ name: 'logs-details', params: { id: item.logId } }"
+            custom
+          >
+            <v-remixicon
+              title="Open log detail"
+              class="ml-2 text-gray-300 cursor-pointer"
+              size="20"
+              name="riFileTextLine"
+              @click.stop="navigate"
+            />
+          </router-link>
+          <router-link
+            v-show="currentLog.workflowId && item.blockId"
+            :to="`/workflows/${currentLog.workflowId}?blockId=${item.blockId}`"
+          >
+            <v-remixicon
+              name="riExternalLinkLine"
+              size="20"
+              title="Go to block"
+              class="text-gray-300 cursor-pointer ml-2 invisible group-hover:visible"
+            />
+          </router-link>
+        </template>
+        <pre
+          class="px-2 pb-2 text-gray-300 overflow-auto text-sm ml-6 scroll max-h-96"
+          >{{ ctxData[state.itemId] || 'EMPTY' }}</pre
+        >
+      </ui-expand>
+      <slot name="append-items" />
+    </div>
+  </div>
+  <div
+    v-if="currentLog.history.length >= 25"
+    class="flex items-center justify-between mt-4"
+  >
+    <div>
+      {{ t('components.pagination.text1') }}
+      <select v-model="pagination.perPage" class="p-1 rounded-md bg-input">
+        <option
+          v-for="num in [25, 50, 75, 100, 150, 200]"
+          :key="num"
+          :value="num"
+        >
+          {{ num }}
+        </option>
+      </select>
+      {{
+        t('components.pagination.text2', {
+          count: currentLog.history.length,
+        })
+      }}
+    </div>
+    <ui-pagination
+      v-model="pagination.currentPage"
+      :per-page="pagination.perPage"
+      :records="currentLog.history.length"
+    />
+  </div>
+</template>
+<script setup>
+/* eslint-disable no-use-before-define */
+import { computed, shallowReactive } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { countDuration } from '@/utils/helper';
+
+const props = defineProps({
+  currentLog: {
+    type: Object,
+    default: () => ({}),
+  },
+  ctxData: {
+    type: Object,
+    default: () => ({}),
+  },
+  parentLog: {
+    type: Object,
+    default: null,
+  },
+});
+
+const logsType = {
+  success: {
+    color: 'text-green-400',
+    icon: 'riCheckLine',
+  },
+  stop: {
+    color: 'text-yellow-400',
+    icon: 'riStopLine',
+  },
+  stopped: {
+    color: 'text-yellow-400',
+    icon: 'riStopLine',
+  },
+  error: {
+    color: 'text-red-400',
+    icon: 'riErrorWarningLine',
+  },
+  finish: {
+    color: 'text-blue-300',
+    icon: 'riFlagLine',
+  },
+};
+
+const { t, te } = useI18n();
+
+const state = shallowReactive({
+  itemId: '',
+});
+const pagination = shallowReactive({
+  perPage: 25,
+  currentPage: 1,
+});
+
+const history = computed(() =>
+  props.currentLog.history
+    .slice(
+      (pagination.currentPage - 1) * pagination.perPage,
+      pagination.currentPage * pagination.perPage
+    )
+    .map(translateLog)
+);
+
+function translateLog(log) {
+  const copyLog = { ...log };
+  const getTranslatation = (path, def) => {
+    const params = typeof path === 'string' ? { path } : path;
+
+    return te(params.path) ? t(params.path, params.params) : def;
+  };
+
+  if (['finish', 'stop'].includes(log.type)) {
+    copyLog.name = t(`log.types.${log.type}`);
+  } else {
+    copyLog.name = getTranslatation(
+      `workflow.blocks.${log.name}.name`,
+      log.name
+    );
+  }
+
+  copyLog.message = getTranslatation(
+    { path: `log.messages.${log.message}`, params: log },
+    log.message
+  );
+
+  return copyLog;
+}
+</script>

+ 145 - 0
src/components/newtab/logs/LogsTable.vue

@@ -0,0 +1,145 @@
+<template>
+  <div v-if="tableData.body.length === 0" class="text-center">
+    <img src="@/assets/svg/files-and-folder.svg" class="mx-auto max-w-sm" />
+    <p class="text-xl font-semibold">{{ t('message.noData') }}</p>
+  </div>
+  <template v-else>
+    <div class="flex items-center">
+      <ui-tabs
+        v-model="state.activeTab"
+        type="fill"
+        class="mb-4"
+        color=""
+        style="padding: 0"
+      >
+        <ui-tab value="table"> Table </ui-tab>
+        <ui-tab value="raw"> Raw </ui-tab>
+      </ui-tabs>
+      <div class="flex-grow"></div>
+      <ui-popover trigger-width>
+        <template #trigger>
+          <ui-button variant="accent">
+            <span>{{ t('log.exportData.title') }}</span>
+            <v-remixicon name="riArrowDropDownLine" class="ml-2 -mr-1" />
+          </ui-button>
+        </template>
+        <ui-list class="space-y-1">
+          <ui-list-item
+            v-for="type in dataExportTypes"
+            :key="type.id"
+            v-close-popover
+            class="cursor-pointer"
+            @click="exportData(type.id)"
+          >
+            {{ t(`log.exportData.types.${type.id}`) }}
+          </ui-list-item>
+        </ui-list>
+      </ui-popover>
+    </div>
+    <shared-codemirror
+      v-show="state.activeTab === 'raw'"
+      :model-value="JSON.stringify(currentLog.data.table, null, 2)"
+      readonly
+      lang="json"
+      style="max-height: 600px"
+    />
+    <table v-show="state.activeTab === 'table'" class="w-full">
+      <thead>
+        <tr>
+          <th
+            v-for="header in tableData.header"
+            :key="header"
+            class="last:rounded-r-lg first:rounded-l-lg text-left bg-box-transparent"
+          >
+            {{ header }}
+          </th>
+        </tr>
+      </thead>
+      <tbody class="divide-y">
+        <tr v-for="(row, index) in rows" :key="index">
+          <td v-for="(column, colIndex) in row" :key="index + colIndex">
+            <p class="line-clamp">
+              {{ column }}
+            </p>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+    <div
+      v-if="
+        state.activeTab === 'table' &&
+        tableData.body &&
+        tableData.body.length >= 10
+      "
+      class="flex items-center justify-between mt-4"
+    >
+      <div>
+        {{ t('components.pagination.text1') }}
+        <select v-model="pagination.perPage" class="p-1 rounded-md bg-input">
+          <option
+            v-for="num in [10, 15, 25, 50, 100, 150]"
+            :key="num"
+            :value="num"
+          >
+            {{ num }}
+          </option>
+        </select>
+        {{
+          t('components.pagination.text2', {
+            count: tableData.body.length,
+          })
+        }}
+      </div>
+      <ui-pagination
+        v-model="pagination.currentPage"
+        :per-page="pagination.perPage"
+        :records="tableData.body.length"
+      />
+    </div>
+  </template>
+</template>
+<script setup>
+import { computed, shallowReactive, defineAsyncComponent } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { dataExportTypes } from '@/utils/shared';
+import dataExporter from '@/utils/dataExporter';
+
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
+
+const props = defineProps({
+  tableData: {
+    type: Object,
+    default: () => ({}),
+  },
+  currentLog: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+
+const { t } = useI18n();
+
+const state = shallowReactive({
+  activeTab: 'table',
+});
+const pagination = shallowReactive({
+  perPage: 10,
+  currentPage: 1,
+});
+const rows = computed(() =>
+  props.tableData.body.slice(
+    (pagination.currentPage - 1) * pagination.perPage,
+    pagination.currentPage * pagination.perPage
+  )
+);
+
+function exportData(type) {
+  dataExporter(
+    props.currentLog.data.table,
+    { name: props.currentLog.name, type },
+    true
+  );
+}
+</script>

+ 71 - 0
src/components/newtab/logs/LogsVariables.vue

@@ -0,0 +1,71 @@
+<template>
+  <div v-if="Object.keys(variables).length === 0" class="text-center">
+    <img src="@/assets/svg/files-and-folder.svg" class="mx-auto max-w-sm" />
+    <p class="text-xl font-semibold">{{ t('message.noData') }}</p>
+  </div>
+  <template v-else>
+    <ui-tabs
+      v-model="state.activeTab"
+      type="fill"
+      class="mb-4"
+      color=""
+      style="padding: 0"
+    >
+      <ui-tab value="gui"> GUI </ui-tab>
+      <ui-tab value="raw"> Raw </ui-tab>
+    </ui-tabs>
+    <div v-if="state.activeTab === 'gui'" class="mt-4">
+      <ul class="space-y-2 grid grid-cols-1 md:grid-cols-2 gap-4">
+        <li
+          v-for="(varValue, varName) in variables"
+          :key="varName"
+          class="px-2 pb-2 pt-1 rounded-lg flex items-center border-2 space-x-2"
+        >
+          <ui-input
+            :model-value="varName"
+            :label="t('common.name')"
+            class="w-full"
+            placeholder="EMPTY"
+            readonly
+          />
+          <ui-input
+            :model-value="varValue"
+            label="Value"
+            class="w-full"
+            placeholder="EMPTY"
+            readonly
+          />
+        </li>
+      </ul>
+    </div>
+    <shared-codemirror
+      v-else
+      :model-value="JSON.stringify(variables, null, 2)"
+      class="mt-4"
+      lang="json"
+      readonly
+    />
+  </template>
+</template>
+<script setup>
+import { defineAsyncComponent, shallowReactive, computed } from 'vue';
+import { useI18n } from 'vue-i18n';
+
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
+
+const props = defineProps({
+  currentLog: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+
+const { t } = useI18n();
+const state = shallowReactive({
+  activeTab: 'gui',
+});
+
+const variables = computed(() => props.currentLog.data?.variables || {});
+</script>

+ 1 - 1
src/components/newtab/shared/SharedCodemirror.vue

@@ -109,7 +109,7 @@ onBeforeUnmount(() => {
 .cm-editor .cm-gutters,
 .cm-editor .cm-content,
 .cm-tooltip.cm-tooltip-autocomplete > ul {
-  font-family: JetBrains Mono, Fira code, Fira Mono, Consolas, Menlo, Courier,
+  font-family: 'Source Code Pro', Fira code, Fira Mono, Consolas, Menlo, Courier,
     monospace !important;
 }
 </style>

+ 128 - 44
src/components/newtab/shared/SharedLogsTable.vue

@@ -1,47 +1,108 @@
 <template>
-  <table>
-    <tbody class="divide-y dark:divide-gray-800">
-      <tr v-for="log in logs" :key="log.id" class="hoverable">
-        <slot name="item-prepend" :log="log" />
-        <td class="text-overflow" style="min-width: 140px; max-width: 330px">
-          <router-link
-            :to="`/logs/${log.id}`"
-            class="inline-block text-overflow w-full align-middle min-h"
-            style="min-height: 28px"
+  <div class="logs-table">
+    <transition-expand>
+      <div v-if="state.selected.length > 0" class="border-x border-t px-4 py-2">
+        <ui-button @click="stopSelectedWorkflow"> Stop selected </ui-button>
+      </div>
+    </transition-expand>
+    <table class="w-full">
+      <tbody class="divide-y dark:divide-gray-800">
+        <tr v-for="item in running" :key="item.id" class="p-2 border">
+          <td v-if="!hideSelect" class="w-8">
+            <ui-checkbox
+              :model-value="state.selected.includes(item.id)"
+              class="align-text-bottom"
+              @change="toggleSelectedLog($event, item.id)"
+            />
+          </td>
+          <td class="w-4/12">
+            <router-link
+              :to="`/logs/${item.id}/running`"
+              class="inline-block text-overflow w-full align-middle min-h"
+              style="min-height: 28px"
+            >
+              {{ item.state.name }}
+            </router-link>
+          </td>
+          <td
+            class="log-time w-2/12 dark:text-gray-200"
+            :title="t('log.duration')"
           >
-            {{ log.name }}
-          </router-link>
-        </td>
-        <td class="log-time dark:text-gray-200">
-          <v-remixicon
-            :title="t('log.startedDate')"
-            name="riCalendarLine"
-            class="mr-2 inline-block align-middle"
-          />
-          <span :title="formatDate(log.startedAt, 'DD MMM YYYY, hh:mm A')">
-            {{ formatDate(log.startedAt, 'relative') }}
-          </span>
-        </td>
-        <td class="log-time dark:text-gray-200" :title="t('log.duration')">
-          <v-remixicon name="riTimerLine"></v-remixicon>
-          <span>{{ countDuration(log.startedAt, log.endedAt) }}</span>
-        </td>
-        <td class="text-right">
-          <span
-            :class="statusColors[log.status]"
-            :title="log.status === 'error' ? getErrorMessage(log) : null"
-            class="inline-block py-1 w-16 text-center text-sm rounded-md dark:text-black"
+            <v-remixicon name="riTimerLine"></v-remixicon>
+            <span>{{
+              countDuration(item.state.startedTimestamp, Date.now())
+            }}</span>
+          </td>
+          <td title="Executing block" class="text-overflow">
+            <ui-spinner color="text-accent" size="20" />
+            <span class="align-middle inline-block ml-3 text-overflow">
+              {{ t(`workflow.blocks.${item.state.currentBlock[0].name}.name`) }}
+            </span>
+          </td>
+          <td class="text-right">
+            <span
+              class="inline-block py-1 w-16 text-center text-sm rounded-md dark:text-black bg-blue-300"
+            >
+              {{ t('common.running') }}
+            </span>
+          </td>
+          <td class="text-right">
+            <ui-button small class="text-sm" @click="stopWorkflow(item.id)">
+              {{ t('common.stop') }}
+            </ui-button>
+          </td>
+        </tr>
+        <tr v-for="log in logs" :key="log.id" class="hoverable">
+          <slot name="item-prepend" :log="log" />
+          <td
+            class="text-overflow w-4/12"
+            style="min-width: 140px; max-width: 330px"
           >
-            {{ t(`logStatus.${log.status}`) }}
-          </span>
-        </td>
-        <slot name="item-append" :log="log" />
-      </tr>
-    </tbody>
-  </table>
+            <router-link
+              :to="`/logs/${log.id}`"
+              class="inline-block text-overflow w-full align-middle min-h"
+              style="min-height: 28px"
+            >
+              {{ log.name }}
+            </router-link>
+          </td>
+          <td class="log-time w-3/12 dark:text-gray-200">
+            <v-remixicon
+              :title="t('log.startedDate')"
+              name="riCalendarLine"
+              class="mr-2 inline-block align-middle"
+            />
+            <span :title="formatDate(log.startedAt, 'DD MMM YYYY, hh:mm A')">
+              {{ formatDate(log.startedAt, 'relative') }}
+            </span>
+          </td>
+          <td
+            class="log-time w-2/12 dark:text-gray-200"
+            :title="t('log.duration')"
+          >
+            <v-remixicon name="riTimerLine"></v-remixicon>
+            <span>{{ countDuration(log.startedAt, log.endedAt) }}</span>
+          </td>
+          <td class="text-right">
+            <span
+              :class="statusColors[log.status]"
+              :title="log.status === 'error' ? getErrorMessage(log) : null"
+              class="inline-block py-1 w-16 text-center text-sm rounded-md dark:text-black"
+            >
+              {{ t(`logStatus.${log.status}`) }}
+            </span>
+          </td>
+          <slot name="item-append" :log="log" />
+        </tr>
+        <slot name="table:append" />
+      </tbody>
+    </table>
+  </div>
 </template>
 <script setup>
+import { reactive } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { sendMessage } from '@/utils/message';
 import { countDuration } from '@/utils/helper';
 import dayjs from '@/lib/dayjs';
 
@@ -50,6 +111,11 @@ defineProps({
     type: Array,
     default: () => [],
   },
+  running: {
+    type: Array,
+    default: () => [],
+  },
+  hideSelect: Boolean,
 });
 
 const { t, te } = useI18n();
@@ -59,24 +125,42 @@ const statusColors = {
   success: 'bg-green-200 dark:bg-green-300',
   stopped: 'bg-yellow-200 dark:bg-yellow-300',
 };
+const state = reactive({
+  selected: [],
+});
+
+function stopWorkflow(stateId) {
+  sendMessage('workflow:stop', stateId, 'background');
+}
+function toggleSelectedLog(selected, id) {
+  if (selected) {
+    state.selected.push(id);
+    return;
+  }
+
+  const index = state.selected.indexOf(id);
 
+  if (index !== -1) state.selected.splice(index, 1);
+}
 function formatDate(date, format) {
   if (format === 'relative') return dayjs(date).fromNow();
 
   return dayjs(date).format(format);
 }
-function getErrorMessage({ history, message }) {
+function getErrorMessage({ message }) {
   const messagePath = `log.messages.${message}`;
 
   if (message && te(messagePath)) {
     return t(messagePath);
   }
 
-  const lastHistory = history[history.length - 1];
-
-  return lastHistory && lastHistory.type === 'error'
-    ? lastHistory.message
-    : null;
+  return '';
+}
+function stopSelectedWorkflow() {
+  state.selected.forEach((id) => {
+    stopWorkflow(id);
+  });
+  state.selected = [];
 }
 </script>
 <style scoped>

+ 1 - 1
src/components/newtab/shared/SharedWysiwyg.vue

@@ -81,7 +81,7 @@ onBeforeUnmount(() => {
 <style>
 .ProseMirror pre,
 .ProseMirror code {
-  font-family: 'JetBrains Mono', monospace;
+  font-family: 'Source Code Pro', monospace;
 }
 .ProseMirror:focus {
   outline: none;

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

@@ -114,7 +114,7 @@ export default {
       default: 'edit',
     },
   },
-  emits: ['load', 'deleteBlock', 'update', 'save'],
+  emits: ['load', 'loaded', 'deleteBlock', 'update', 'save'],
   setup(props, { emit }) {
     useGroupTooltip();
 
@@ -876,6 +876,7 @@ export default {
 
       checkWorkflowData();
       initSelectArea();
+      emit('loaded', editor.value);
     });
     onBeforeUnmount(() => {
       const element = document.querySelector('#drawflow');

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

@@ -20,7 +20,7 @@
         :model-value="data.spreadsheetId"
         class="w-full"
         placeholder="abcd123"
-        @change="updateData({ spreadsheetId: $event })"
+        @change="updateData({ spreadsheetId: $event }), checkPermission($event)"
       >
         <template #label>
           {{ t('workflow.blocks.google-sheets.spreadsheetId.label') }}*
@@ -35,6 +35,16 @@
         </template>
       </ui-input>
     </edit-autocomplete>
+    <a
+      v-if="!state.havePermission"
+      href="https://docs.automa.site/blocks/google-sheets.html#access-to-spreadsheet"
+      target="_blank"
+      rel="noopener"
+      class="text-sm leading-tight inline-block ml-1"
+    >
+      Automa doesn't have access to the spreadsheet
+      <v-remixicon name="riInformationLine" size="18" class="inline" />
+    </a>
     <edit-autocomplete>
       <ui-input
         :model-value="data.range"
@@ -195,8 +205,8 @@
 <script setup>
 import { shallowReactive, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { googleSheets } from '@/utils/api';
-import { convert2DArrayToArrayObj } from '@/utils/helper';
+import { googleSheets, fetchApi } from '@/utils/api';
+import { convert2DArrayToArrayObj, debounce } from '@/utils/helper';
 import EditAutocomplete from './EditAutocomplete.vue';
 import InsertWorkflowData from './InsertWorkflowData.vue';
 
@@ -228,6 +238,25 @@ const customDataState = shallowReactive({
   showModal: false,
   data: props.data.customData,
 });
+const state = shallowReactive({
+  lastSheetId: null,
+  havePermission: true,
+});
+
+const checkPermission = debounce(async (value) => {
+  try {
+    if (state.lastSheetId === value) return;
+
+    const response = await fetchApi(
+      `/services/google-sheets/meta?spreadsheetId=${value}`
+    );
+
+    state.havePermission = response.status !== 403;
+    state.lastSheetId = value;
+  } catch (error) {
+    console.error(error);
+  }
+}, 1000);
 
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });

+ 2 - 2
src/components/newtab/workflow/edit/EditInsertData.vue

@@ -46,8 +46,8 @@
             </option>
           </ui-select>
         </div>
-        <div class="flex items-center">
-          <ui-input
+        <div class="flex items-start">
+          <ui-textarea
             v-model="item.value"
             placeholder="value"
             title="value"

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

@@ -12,8 +12,8 @@
       </label>
       <ui-textarea
         id="new-tab-url"
-        key="anu"
         :model-value="data.url"
+        rows="1"
         class="w-full"
         autocomplete="off"
         placeholder="http://example.com/"

+ 1 - 0
src/components/newtab/workflow/edit/EditWebhook.vue

@@ -22,6 +22,7 @@
         :label="`${t('workflow.blocks.webhook.url')}*`"
         placeholder="http://api.example.com"
         class="w-full"
+        rows="1"
         autocomplete="off"
         required
         type="url"

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

@@ -1,5 +1,5 @@
 <template>
-  <div :aria-expanded="show" class="ui-expand">
+  <div :aria-expanded="show" :class="{ [activeClass]: show }" class="ui-expand">
     <button
       :class="[headerClass, { [headerActiveClass]: show }]"
       @click="toggleExpand"
@@ -45,10 +45,15 @@ const props = defineProps({
     type: String,
     default: '',
   },
+  activeClass: {
+    type: String,
+    default: '',
+  },
   hideHeaderIcon: {
     type: Boolean,
     default: false,
   },
+  disabled: Boolean,
   appendIcon: Boolean,
 });
 const emit = defineEmits(['update:modelValue']);
@@ -56,6 +61,8 @@ const emit = defineEmits(['update:modelValue']);
 const show = ref(false);
 
 function toggleExpand() {
+  if (props.disabled) return;
+
   show.value = !show.value;
 
   emit('update:modelValue', show.value);
@@ -70,4 +77,10 @@ watch(
   },
   { immediate: true }
 );
+watch(
+  () => props.disabled,
+  () => {
+    show.value = false;
+  }
+);
 </script>

+ 3 - 7
src/components/ui/UiTextarea.vue

@@ -1,11 +1,11 @@
 <template>
   <textarea
-    v-bind="{ value: modelValue, placeholder, maxlength: max }"
+    v-bind="{ placeholder, maxlength: max }"
     :id="textareaId"
     ref="textarea"
+    :value="modelValue"
     class="ui-textarea w-full ui-input rounded-lg px-4 py-2 transition bg-input"
     :class="{ 'overflow-hidden resize-none': autoresize }"
-    :style="{ height }"
     @input="emitValue"
     @keyup="$emit('keyup', $event)"
     @keydown="$emit('keydown', $event)"
@@ -35,10 +35,6 @@ export default {
       type: Boolean,
       default: false,
     },
-    height: {
-      type: [Number, String],
-      default: '',
-    },
     max: {
       type: [Number, String],
       default: null,
@@ -66,7 +62,7 @@ export default {
 
       emit('update:modelValue', value);
       emit('change', value);
-      calcHeight();
+      // calcHeight();
     }
 
     onMounted(calcHeight);

+ 6 - 0
src/composable/liveQuery.js

@@ -0,0 +1,6 @@
+import { liveQuery } from 'dexie';
+import { useObservable } from '@vueuse/rxjs';
+
+export function useLiveQuery(querier) {
+  return useObservable(liveQuery(querier));
+}

+ 13 - 2
src/content/blocksHandler/handlerEventClick.js

@@ -4,6 +4,14 @@ import handleSelector from '../handleSelector';
 
 function eventClick(block) {
   return new Promise((resolve, reject) => {
+    const dispatchClickEvents = (element, eventFn) => {
+      const eventOpts = { bubbles: true };
+
+      element.dispatchEvent(new MouseEvent('mousedown', eventOpts));
+      element.dispatchEvent(new MouseEvent('mouseup', eventOpts));
+      eventFn();
+    };
+
     handleSelector(block, {
       async onSelected(element) {
         if (block.debugMode) {
@@ -34,9 +42,12 @@ function eventClick(block) {
         }
 
         if (element.click) {
-          element.click();
+          dispatchClickEvents(element, () => element.click());
         } else {
-          element.dispatchEvent(new PointerEvent('click', { bubbles: true }));
+          dispatchClickEvents(
+            () => element,
+            element.dispatchEvent(new PointerEvent('click', { bubbles: true }))
+          );
         }
       },
       onError(error) {

+ 36 - 10
src/content/blocksHandler/handlerLoopData.js

@@ -1,19 +1,11 @@
 import { nanoid } from 'nanoid';
 import handleSelector from '../handleSelector';
 
-export default async function loopElements(block) {
-  const elements = await handleSelector(block);
+function generateLoopSelectors(elements, { max, attrId, frameSelector }) {
   const selectors = [];
-  const attrId = nanoid(5);
-
-  let frameSelector = '';
-
-  if (block.data.$frameSelector) {
-    frameSelector = `${block.data.$frameSelector} |> `;
-  }
 
   elements.forEach((el, index) => {
-    if (block.data.max > 0 && selectors.length - 1 > block.data.max) return;
+    if (max > 0 && selectors.length - 1 > max) return;
 
     const attrName = 'automa-loop';
     const attrValue = `${attrId}--${index}`;
@@ -24,3 +16,37 @@ export default async function loopElements(block) {
 
   return selectors;
 }
+
+export default async function loopElements(block) {
+  const elements = await handleSelector(block);
+  if (!elements) throw new Error('element-not-found');
+
+  let frameSelector = '';
+  if (block.data.$frameSelector) {
+    frameSelector = `${block.data.$frameSelector} |> `;
+  }
+
+  if (block.onlyGenerate) {
+    generateLoopSelectors(elements, {
+      ...block.data,
+      frameSelector,
+      attrId: block.data.loopId,
+    });
+
+    return {};
+  }
+
+  const attrId = nanoid(5);
+  const selectors = generateLoopSelectors(elements, {
+    ...block.data,
+    frameSelector,
+    attrId,
+  });
+  const { origin, pathname } = window.location;
+
+  return {
+    loopId: attrId,
+    elements: selectors,
+    url: origin + pathname,
+  };
+}

+ 30 - 1
src/content/index.js

@@ -150,7 +150,36 @@ function messageListener({ data, source }) {
   browser.runtime.onMessage.addListener((data) => {
     return new Promise((resolve, reject) => {
       if (data.isBlock) {
-        executeBlock(data).then(resolve).catch(reject);
+        executeBlock(data)
+          .then(resolve)
+          .catch((error) => {
+            const elNotFound = error.message === 'element-not-found';
+            const selectLoopItem = data.data?.selector?.includes('automa-loop');
+            if (elNotFound && selectLoopItem) {
+              const findLoopEl = data.loopEls.find(({ url }) =>
+                window.location.href.includes(url)
+              );
+
+              const blockData = { ...data.data, ...findLoopEl, multiple: true };
+              const loopBlock = {
+                ...data,
+                onlyGenerate: true,
+                data: blockData,
+              };
+
+              blocksHandler
+                .loopData(loopBlock)
+                .then(() => {
+                  executeBlock(data).then(resolve).catch(reject);
+                })
+                .catch((blockError) => {
+                  reject(blockError);
+                });
+              return;
+            }
+
+            reject(error);
+          });
       } else {
         switch (data.type) {
           case 'condition-builder':

+ 22 - 0
src/db/logs.js

@@ -0,0 +1,22 @@
+import Dexie from 'dexie';
+
+const dbLogs = new Dexie('logs');
+dbLogs.version(1).stores({
+  ctxData: '++id, logId',
+  logsData: '++id, logId',
+  histories: '++id, logId',
+  items: '++id, name, endedAt, workflowId, status, collectionId',
+});
+
+export const defaultLogItem = {
+  name: '',
+  endedAt: 0,
+  message: '',
+  startedAt: 0,
+  parentLog: null,
+  workflowId: null,
+  status: 'success',
+  collectionId: null,
+};
+
+export default dbLogs;

+ 2 - 0
src/lib/vRemixicon.js

@@ -52,6 +52,7 @@ import {
   riTwitterLine,
   riDiscordLine,
   riLinkUnlinkM,
+  riYoutubeLine,
   riSideBarLine,
   riSideBarFill,
   riWindow2Line,
@@ -164,6 +165,7 @@ export const icons = {
   riTwitterLine,
   riDiscordLine,
   riLinkUnlinkM,
+  riYoutubeLine,
   riSideBarLine,
   riSideBarFill,
   riWindow2Line,

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

@@ -27,6 +27,10 @@
     "clickHere": "Click here",
     "text": "You need to be signed in before you can do that"
   },
+  "running": {
+    "start": "Started on {date}",
+    "message": "This only display the last 5 logs"
+  },
   "settings": {
     "theme": "Theme",
     "shortcuts": {

+ 2 - 1
src/manifest.firefox.json

@@ -69,5 +69,6 @@
     "/Inter-roman-latin.var.woff2",
     "/locales/*",
     "elementSelector.bundle.js"
-  ]
+  ],
+  "content_security_policy": "script-src 'self' https:; object-src 'self'"
 }

+ 29 - 18
src/newtab/App.vue

@@ -83,22 +83,21 @@
 import { ref, shallowReactive, computed } from 'vue';
 import { useStore } from 'vuex';
 import { useI18n } from 'vue-i18n';
-import { useRoute } from 'vue-router';
 import { compare } from 'compare-versions';
 import browser from 'webextension-polyfill';
+import dbLogs from '@/db/logs';
 import { useTheme } from '@/composable/theme';
 import { loadLocaleMessages, setI18nLanguage } from '@/lib/vueI18n';
 import { parseJSON } from '@/utils/helper';
 import { fetchApi, getSharedWorkflows, getUserWorkflows } from '@/utils/api';
 import dayjs from '@/lib/dayjs';
-import Log from '@/models/log';
 import Workflow from '@/models/workflow';
 import AppSidebar from '@/components/newtab/app/AppSidebar.vue';
+import dataMigration from '@/utils/dataMigration';
 
 const { t } = useI18n();
 const store = useStore();
 const theme = useTheme();
-const route = useRoute();
 
 theme.init();
 
@@ -238,14 +237,31 @@ async function fetchUserData() {
 /* eslint-disable-next-line */
 function autoDeleteLogs() {
   const deleteAfter = store.state.settings.deleteLogAfter;
-
   if (deleteAfter === 'never') return;
 
-  Log.delete(({ endedAt }) => {
-    const diff = dayjs().diff(dayjs(endedAt), 'day');
+  const lastCheck =
+    +localStorage.getItem('checkDeleteLogs') || Date.now() - 8.64e7;
+  const dayDiff = dayjs().diff(dayjs(lastCheck), 'day');
 
-    return diff >= deleteAfter;
-  });
+  if (dayDiff < 1) return;
+
+  const aDayInMs = 8.64e7;
+  const maxLogAge = Date.now() - aDayInMs * deleteAfter;
+
+  dbLogs.items
+    .where('endedAt')
+    .below(maxLogAge)
+    .toArray()
+    .then((values) => {
+      const ids = values.map(({ id }) => id);
+
+      dbLogs.items.bulkDelete(ids);
+      dbLogs.ctxData.where('logId').anyOf(ids).delete();
+      dbLogs.logsData.where('logId').anyOf(ids).delete();
+      dbLogs.histories.where('logId').anyOf(ids).delete();
+
+      localStorage.setItem('checkDeleteLogs', Date.now());
+    });
 }
 function handleStorageChanged(change) {
   if (change.logs) {
@@ -288,9 +304,8 @@ window.addEventListener('beforeunload', () => {
   browser.storage.onChanged.removeListener(handleStorageChanged);
 });
 
-const includeRoutes = ['home', 'workflows-details'];
 window.addEventListener('storage', ({ key, newValue }) => {
-  if (key !== 'workflowState' || !includeRoutes.includes(route.name)) return;
+  if (key !== 'workflowState') return;
 
   const states = parseJSON(newValue, {});
   store.commit('updateState', {
@@ -316,24 +331,20 @@ window.addEventListener('storage', ({ key, newValue }) => {
     }
 
     await Promise.allSettled([
-      store.dispatch('retrieve', [
-        'workflows',
-        'logs',
-        'collections',
-        'folders',
-      ]),
+      store.dispatch('retrieve', ['workflows', 'collections', 'folders']),
       store.dispatch('retrieveWorkflowState'),
     ]);
 
     await loadLocaleMessages(store.state.settings.locale, 'newtab');
     await setI18nLanguage(store.state.settings.locale);
 
+    await dataMigration();
+
     retrieved.value = true;
 
     await fetchUserData();
     await syncHostWorkflow();
-
-    // autoDeleteLogs();
+    autoDeleteLogs();
   } catch (error) {
     retrieved.value = true;
     console.error(error);

+ 39 - 67
src/newtab/pages/Home.vue

@@ -1,59 +1,40 @@
 <template>
   <div class="container pt-8 pb-4">
     <h1 class="text-2xl font-semibold mb-8">{{ t('common.dashboard') }}</h1>
-    <div class="flex items-start">
-      <div class="w-8/12 mr-8">
-        <div class="grid gap-4 mb-8 2xl:grid-cols-4 grid-cols-3">
-          <p
-            v-if="workflows.length === 0"
-            class="text-center text-gray-600 dark:text-gray-200"
-          >
-            {{ t('message.noData') }}
-          </p>
-          <shared-card
-            v-for="workflow in workflows"
-            :key="workflow.id"
-            :data="workflow"
-            :show-details="false"
-            style="max-width: 250px"
-            @execute="executeWorkflow"
-            @click="$router.push(`/workflows/${$event.id}`)"
-          />
-        </div>
-        <div>
-          <div class="mb-2 flex items-center justify-between">
-            <p class="font-semibold inline-block">Logs</p>
-            <router-link
-              to="/logs"
-              class="text-gray-600 dark:text-gray-200 text-sm"
-            >
-              {{ t('home.viewAll') }}
-            </router-link>
-          </div>
-          <p
-            v-if="logs.length === 0"
-            class="text-center text-gray-600 dark:text-gray-200"
-          >
-            {{ t('message.noData') }}
-          </p>
-          <shared-logs-table :logs="logs" class="w-full" />
-        </div>
-      </div>
-      <div class="w-4/12 space-y-4">
-        <p
-          v-if="workflowState.length === 0"
-          class="text-center text-gray-600 dark:text-gray-200"
-        >
-          {{ t('message.noData') }}
-        </p>
-        <shared-workflow-state
-          v-for="item in workflowState"
-          v-bind="{ data: item }"
-          :key="item.id"
-          class="w-full"
-        />
-      </div>
+    <div class="grid gap-4 mb-8 2xl:grid-cols-5 grid-cols-4">
+      <p
+        v-if="workflows.length === 0"
+        class="text-center text-gray-600 dark:text-gray-200"
+      >
+        {{ t('message.noData') }}
+      </p>
+      <shared-card
+        v-for="workflow in workflows"
+        :key="workflow.id"
+        :data="workflow"
+        :show-details="false"
+        style="max-width: 250px"
+        @execute="executeWorkflow"
+        @click="$router.push(`/workflows/${$event.id}`)"
+      />
     </div>
+    <div class="mb-2 flex items-center justify-between">
+      <p class="font-semibold inline-block">{{ t('common.log', 2) }}</p>
+      <router-link to="/logs" class="text-gray-600 dark:text-gray-200 text-sm">
+        {{ t('home.viewAll') }}
+      </router-link>
+    </div>
+    <p
+      v-if="logs?.length === 0"
+      class="text-center text-gray-600 dark:text-gray-200"
+    >
+      {{ t('message.noData') }}
+    </p>
+    <shared-logs-table
+      :logs="logs || []"
+      :running="workflowState"
+      class="w-full"
+    />
   </div>
 </template>
 <script setup>
@@ -61,31 +42,22 @@ import { computed } from 'vue';
 import { useStore } from 'vuex';
 import { useI18n } from 'vue-i18n';
 import { sendMessage } from '@/utils/message';
-import Log from '@/models/log';
+import { useLiveQuery } from '@/composable/liveQuery';
+import dbLogs from '@/db/logs';
 import Workflow from '@/models/workflow';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
-import SharedWorkflowState from '@/components/newtab/shared/SharedWorkflowState.vue';
 
 const { t } = useI18n();
 const store = useStore();
 
+const logs = useLiveQuery(() =>
+  dbLogs.items.orderBy('endedAt').reverse().limit(10).toArray()
+);
 const workflows = computed(() =>
   Workflow.query().orderBy('createdAt', 'desc').limit(3).get()
 );
-const logs = computed(() =>
-  Log.query()
-    .where(
-      ({ isInCollection, isChildLog, parentLog }) =>
-        !isInCollection && !isChildLog && !parentLog
-    )
-    .orderBy('startedAt', 'desc')
-    .limit(10)
-    .get()
-);
-const workflowState = computed(() =>
-  store.state.workflowState.filter(({ isInCollection }) => !isInCollection)
-);
+const workflowState = computed(() => store.state.workflowState);
 
 function executeWorkflow(workflow) {
   sendMessage('workflow:execute', workflow, 'background');

+ 49 - 60
src/newtab/pages/Logs.vue

@@ -8,8 +8,12 @@
       @updateSorts="sortsBuilder[$event.key] = $event.value"
       @updateFilters="filtersBuilder[$event.key] = $event.value"
     />
-    <div style="min-height: 320px">
-      <shared-logs-table :logs="logs" class="w-full">
+    <div v-if="logs" style="min-height: 320px">
+      <shared-logs-table
+        :logs="logs"
+        :running="$store.state.workflowState"
+        class="w-full"
+      >
         <template #item-prepend="{ log }">
           <td class="w-8">
             <ui-checkbox
@@ -20,23 +24,12 @@
           </td>
         </template>
         <template #item-append="{ log }">
-          <td class="ml-4">
-            <div class="flex items-center justify-end space-x-4">
-              <v-remixicon
-                v-if="Object.keys(log.data).length !== 0"
-                name="riFileTextLine"
-                class="cursor-pointer"
-                @click="
-                  exportDataModal.show = true;
-                  exportDataModal.log = log;
-                "
-              />
-              <v-remixicon
-                name="riDeleteBin7Line"
-                class="text-red-500 dark:text-red-400 cursor-pointer"
-                @click="deleteLog(log.id)"
-              />
-            </div>
+          <td class="ml-4 text-right">
+            <v-remixicon
+              name="riDeleteBin7Line"
+              class="text-red-500 inline-block dark:text-red-400 cursor-pointer"
+              @click="deleteLog(log.id)"
+            />
           </td>
         </template>
       </shared-logs-table>
@@ -65,7 +58,7 @@
         {{
           t(
             `log.${
-              selectedLogs.length >= logs.length ? 'deselectAll' : 'selectAll'
+              selectedLogs.length >= logs?.length ? 'deselectAll' : 'selectAll'
             }`
           )
         }}
@@ -87,17 +80,17 @@
 </template>
 <script setup>
 import { shallowReactive, ref, computed, watch } from 'vue';
-import { useStore } from 'vuex';
 import { useI18n } from 'vue-i18n';
 import { useDialog } from '@/composable/dialog';
-import Log from '@/models/log';
+import dbLogs from '@/db/logs';
+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 { t } = useI18n();
-const store = useStore();
 const dialog = useDialog();
+const storedlogs = useLiveQuery(() => dbLogs.items.toArray());
 
 const savedSorts = JSON.parse(localStorage.getItem('logs-sorts') || '{}');
 
@@ -113,41 +106,45 @@ const filtersBuilder = shallowReactive({
 });
 const sortsBuilder = shallowReactive({
   order: savedSorts.order || 'desc',
-  by: savedSorts.by || 'startedAt',
+  by: savedSorts.by || 'endedAt',
 });
 const exportDataModal = shallowReactive({
   show: false,
   log: {},
 });
 
-const filteredLogs = computed(() =>
-  Log.query()
-    .where(
-      ({ name, status, startedAt, isInCollection, isChildLog, parentLog }) => {
-        if (isInCollection || isChildLog || parentLog) return false;
-
-        let statusFilter = true;
-        let dateFilter = true;
-        const searchFilter = name
-          .toLocaleLowerCase()
-          .includes(filtersBuilder.query.toLocaleLowerCase());
+const filteredLogs = computed(() => {
+  if (!storedlogs.value) return [];
 
-        if (filtersBuilder.byStatus !== 'all') {
-          statusFilter = status === filtersBuilder.byStatus;
-        }
+  return storedlogs.value
+    .filter(({ name, status, endedAt }) => {
+      let dateFilter = true;
+      let statusFilter = true;
+      const searchFilter = name
+        .toLocaleLowerCase()
+        .includes(filtersBuilder.query.toLocaleLowerCase());
 
-        if (filtersBuilder.byDate > 0) {
-          const date = Date.now() - filtersBuilder.byDate * 24 * 60 * 60 * 1000;
+      if (filtersBuilder.byStatus !== 'all') {
+        statusFilter = status === filtersBuilder.byStatus;
+      }
 
-          dateFilter = date <= startedAt;
-        }
+      if (filtersBuilder.byDate > 0) {
+        const date = Date.now() - filtersBuilder.byDate * 24 * 60 * 60 * 1000;
 
-        return searchFilter && statusFilter && dateFilter;
+        dateFilter = date <= endedAt;
       }
-    )
-    .orderBy(sortsBuilder.by, sortsBuilder.order)
-    .get()
-);
+
+      return searchFilter && statusFilter && dateFilter;
+    })
+    .sort((a, b) => {
+      const valueA = a[sortsBuilder.by];
+      const valueB = b[sortsBuilder.by];
+
+      if (sortsBuilder.order === 'asc') return valueA > valueB ? 1 : -1;
+
+      return valueB > valueA ? 1 : -1;
+    });
+});
 const logs = computed(() =>
   filteredLogs.value.slice(
     (pagination.currentPage - 1) * pagination.perPage,
@@ -156,9 +153,7 @@ const logs = computed(() =>
 );
 
 function deleteLog(id) {
-  Log.delete(id).then(() => {
-    store.dispatch('saveToStorage', 'logs');
-  });
+  dbLogs.items.where('id').equals(id).delete();
 }
 function toggleSelectedLog(selected, logId) {
   if (selected) {
@@ -176,11 +171,8 @@ function deleteSelectedLogs() {
     okVariant: 'danger',
     body: t('log.delete.description'),
     onConfirm: () => {
-      const promises = selectedLogs.value.map((logId) => Log.delete(logId));
-
-      Promise.allSettled(promises).then(() => {
+      dbLogs.items.bulkDelete(selectedLogs.value).then(() => {
         selectedLogs.value = [];
-        store.dispatch('saveToStorage', 'logs');
       });
     },
   });
@@ -191,20 +183,17 @@ function clearLogs() {
     okVariant: 'danger',
     body: t('log.clearLogs.description'),
     onConfirm: () => {
-      Log.deleteAll().then(() => {
-        selectedLogs.value = [];
-        store.dispatch('saveToStorage', 'logs');
-      });
+      dbLogs.delete();
     },
   });
 }
 function selectAllLogs() {
-  if (selectedLogs.value.length >= logs.value.length) {
+  if (selectedLogs.value.length >= logs.value?.length) {
     selectedLogs.value = [];
     return;
   }
 
-  const logIds = logs.value.map(({ id }) => id);
+  const logIds = logs?.value.map(({ id }) => id);
 
   selectedLogs.value = logIds;
 }

+ 11 - 14
src/newtab/pages/collections/[id].vue

@@ -208,7 +208,8 @@ import { useI18n } from 'vue-i18n';
 import Draggable from 'vuedraggable';
 import { useDialog } from '@/composable/dialog';
 import { sendMessage } from '@/utils/message';
-import Log from '@/models/log';
+import { useLiveQuery } from '@/composable/liveQuery';
+import dbLogs from '@/db/logs';
 import Workflow from '@/models/workflow';
 import Collection from '@/models/collection';
 import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
@@ -223,6 +224,14 @@ const store = useStore();
 const route = useRoute();
 const router = useRouter();
 const dialog = useDialog();
+const logs = useLiveQuery(() =>
+  dbLogs.items
+    .where('collectionId')
+    .equals(route.params.id)
+    .reverse()
+    .limit(15)
+    .sortBy('endedAt')
+);
 
 const blocks = {
   'export-result': {
@@ -254,16 +263,6 @@ const collectionOptions = shallowReactive({
 const runningCollection = computed(() =>
   store.state.workflowState.filter(({ id }) => id === route.params.id)
 );
-const logs = computed(() =>
-  Log.query()
-    .where(
-      ({ collectionId, isInCollection, isChildLog }) =>
-        collectionId === route.params.id && (!isInCollection || !isChildLog)
-    )
-    .orderBy('startedAt', 'desc')
-    .limit(10)
-    .get()
-);
 const workflows = computed(() =>
   Workflow.query()
     .where(({ name }) =>
@@ -284,9 +283,7 @@ const collectionFlow = computed(() => {
 });
 
 function deleteLog(logId) {
-  Log.delete(logId).then(() => {
-    store.dispatch('saveToStorage', 'logs');
-  });
+  dbLogs.items.where('id').equals(logId).delete();
 }
 function executeCollection() {
   sendMessage('collection:execute', collection.value, 'background');

+ 116 - 0
src/newtab/pages/logs/Running.vue

@@ -0,0 +1,116 @@
+<template>
+  <div v-if="running" class="container py-8">
+    <div class="flex items-center">
+      <div class="flex-grow overflow-hidden">
+        <h1 class="text-2xl max-w-md text-overflow font-semibold text-overflow">
+          {{ running.state.name }}
+        </h1>
+        <p>
+          {{
+            t('running.start', {
+              date: dayjs(running.state.startedTimestamp).format(
+                'DD MMM, hh:mm A'
+              ),
+            })
+          }}
+        </p>
+      </div>
+      <ui-button @click="stopWorkflow">
+        {{ t('common.stop') }}
+      </ui-button>
+    </div>
+    <div class="mt-8">
+      <logs-history
+        :current-log="{
+          history: running.state.logs,
+          workflowId: running.workflowId,
+        }"
+      >
+        <template #prepend>
+          <div class="mb-4 text-sm">
+            <h3 class="leading-tight">
+              {{ t('common.log', 2) }}
+            </h3>
+            <p class="leading-tight text-gray-600 dark:text-gray-300">
+              {{ t('running.message') }}
+            </p>
+          </div>
+        </template>
+        <template #append-items>
+          <div
+            v-for="block in running.state.currentBlock"
+            :key="block.id"
+            class="px-2 py-1 rounded-md w-full group hoverable flex items-center"
+          >
+            <span
+              :key="key"
+              :title="`Duration: ${Math.round(
+                (Date.now() - block.startedAt) / 1000
+              )}s`"
+              class="w-14 flex-shrink-0 text-overflow text-gray-400 ml-6"
+            >
+              {{ countDuration(block.startedAt, Date.now()) }}
+            </span>
+            <ui-spinner size="16" class="mr-2" color="text-accent" />
+            <p class="flex-1">
+              {{ t(`workflow.blocks.${block.name}.name`) }}
+            </p>
+            <router-link
+              :to="`/workflows/${running.workflowId}?block=${block.id}`"
+              title="Go to block"
+              class="invisible group-hover:visible"
+            >
+              <v-remixicon
+                name="riExternalLinkLine"
+                size="20"
+                title="Go to block"
+                class="text-gray-300 cursor-pointer ml-2 invisible group-hover:visible"
+              />
+            </router-link>
+          </div>
+        </template>
+      </logs-history>
+    </div>
+  </div>
+</template>
+<script setup>
+import { computed, watch, shallowRef, onBeforeUnmount } from 'vue';
+import { useStore } from 'vuex';
+import { useRoute, useRouter } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+import { countDuration } from '@/utils/helper';
+import { sendMessage } from '@/utils/message';
+import dayjs from '@/lib/dayjs';
+import LogsHistory from '@/components/newtab/logs/LogsHistory.vue';
+
+const { t } = useI18n();
+const store = useStore();
+const route = useRoute();
+const router = useRouter();
+
+const key = shallowRef(0);
+const interval = setInterval(() => {
+  key.value += 1;
+}, 1000);
+
+const running = computed(() =>
+  store.state.workflowState.find(({ id }) => id === route.params.id)
+);
+
+function stopWorkflow() {
+  sendMessage('workflow:stop', running.value.id, 'background');
+}
+
+watch(
+  running,
+  () => {
+    if (!running.value && route.params.id) {
+      router.replace('/logs');
+    }
+  },
+  { immediate: true }
+);
+onBeforeUnmount(() => {
+  clearInterval(interval);
+});
+</script>

+ 121 - 212
src/newtab/pages/logs/[id].vue

@@ -1,23 +1,23 @@
 <template>
-  <div v-if="activeLog" class="container pt-8 pb-4">
-    <div class="flex items-center mb-8">
+  <div v-if="currentLog.id" class="container pt-8 pb-4">
+    <div class="flex items-center">
       <div>
         <h1 class="text-2xl max-w-md text-overflow font-semibold">
-          {{ activeLog.name }}
+          {{ currentLog.name }}
         </h1>
         <p class="text-gray-600 dark:text-gray-200">
           {{
             t(`log.description.text`, {
-              status: t(`log.description.status.${activeLog.status}`),
-              date: dayjs(activeLog.startedAt).format('DD MMM'),
-              duration: countDuration(activeLog.startedAt, activeLog.endedAt),
+              status: t(`log.description.status.${currentLog.status}`),
+              date: dayjs(currentLog.startedAt).format('DD MMM'),
+              duration: countDuration(currentLog.startedAt, currentLog.endedAt),
             })
           }}
         </p>
       </div>
       <div class="flex-grow"></div>
       <ui-button
-        v-if="workflowExists"
+        v-if="state.workflowExists"
         v-tooltip="t('log.goWorkflow')"
         icon
         class="mr-4"
@@ -29,224 +29,93 @@
         {{ t('common.delete') }}
       </ui-button>
     </div>
-    <div class="flex items-start">
-      <div class="w-7/12 mr-6">
-        <ui-list>
-          <router-link
-            v-if="collectionLog"
-            :to="activeLog.parentLog?.id || activeLog.collectionLogId"
-            replace
-            class="mb-4 flex"
-          >
-            <v-remixicon name="riArrowLeftLine" class="mr-2" />
-            {{ t('log.goBack', { name: collectionLog.name }) }}
-          </router-link>
-          <ui-expand
-            v-for="(item, index) in history"
-            :key="item.id || index"
-            hide-header-icon
-            class="mb-1"
-            header-active-class="bg-box-transparent rounded-b-none"
-            header-class="flex items-center px-4 py-2 hoverable rounded-lg w-full text-left history-item focus:ring-0"
-          >
-            <template #header="{ show }">
-              <v-remixicon
-                :rotate="show ? 270 : 180"
-                size="20"
-                name="riArrowLeftSLine"
-                class="transition-transform dark:text-gray-200 text-gray-600 -ml-1 mr-2"
-              />
-              <span
-                :class="logsType[item.type]?.color"
-                class="p-1 rounded-lg align-middle inline-block mr-2 dark:text-black"
-              >
-                <v-remixicon :name="logsType[item.type]?.icon" size="20" />
-              </span>
-              <div class="flex-1 line-clamp pr-2">
-                <p class="w-full text-overflow leading-tight">
-                  {{ item.name }}
-                  <span
-                    v-show="item.description"
-                    :title="item.description"
-                    class="text-overflow text-gray-600 dark:text-gray-200 text-sm"
-                  >
-                    ({{ item.description }})
-                  </span>
-                </p>
-                <p
-                  v-if="item.message"
-                  :title="item.message"
-                  class="text-sm line-clamp text-gray-600 dark:text-gray-200"
-                >
-                  {{ item.message }}
-                </p>
-              </div>
-              <router-link
-                v-if="item.logId"
-                :to="'/logs/' + item.logId"
-                class="mr-4"
-                title="Open log detail"
-              >
-                <v-remixicon name="riExternalLinkLine" />
-              </router-link>
-              <code
-                v-show="item.workerId"
-                :title="t('log.flowId')"
-                class="text-xs mr-4 bg-box-transparent rounded-lg p-1 rounded-md"
-              >
-                {{ item.workerId }}
-              </code>
-              <p class="text-gray-600 dark:text-gray-200">
-                {{ countDuration(0, item.duration || 0) }}
-              </p>
-            </template>
-            <pre
-              class="text-sm px-4 max-h-52 overflow-auto scroll bg-box-transparent pb-2 rounded-b-lg"
-              >{{ ctxData[item.id] }}</pre
-            >
-          </ui-expand>
-        </ui-list>
-        <div
-          v-if="activeLog.history.length >= 10"
-          class="flex items-center justify-between mt-4"
-        >
-          <div>
-            {{ t('components.pagination.text1') }}
-            <select
-              v-model="pagination.perPage"
-              class="p-1 rounded-md bg-input"
-            >
-              <option
-                v-for="num in [10, 15, 25, 50, 100]"
-                :key="num"
-                :value="num"
-              >
-                {{ num }}
-              </option>
-            </select>
-            {{
-              t('components.pagination.text2', {
-                count: activeLog.history.length,
-              })
-            }}
-          </div>
-          <ui-pagination
-            v-model="pagination.currentPage"
-            :per-page="pagination.perPage"
-            :records="activeLog.history.length"
-          />
-        </div>
-      </div>
-      <div class="w-5/12 logs-details sticky top-10">
-        <logs-data-viewer :log="activeLog" />
-      </div>
-    </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>
 </template>
 <script setup>
-import { computed, onMounted, shallowReactive, shallowRef } from 'vue';
+import { shallowReactive, shallowRef, watch } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { useI18n } from 'vue-i18n';
-import browser from 'webextension-polyfill';
-import Log from '@/models/log';
+import dbLogs from '@/db/logs';
 import Workflow from '@/models/workflow';
 import dayjs from '@/lib/dayjs';
-import { countDuration } from '@/utils/helper';
-import LogsDataViewer from '@/components/newtab/logs/LogsDataViewer.vue';
+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 logsType = {
-  success: {
-    color: 'bg-green-200 dark:bg-green-300',
-    icon: 'riCheckLine',
-  },
-  stop: {
-    color: 'bg-yellow-200 dark:bg-yellow-300',
-    icon: 'riStopLine',
-  },
-  stopped: {
-    color: 'bg-yellow-200 dark:bg-yellow-300',
-    icon: 'riStopLine',
-  },
-  error: {
-    color: 'bg-red-200 dark:bg-red-300',
-    icon: 'riErrorWarningLine',
-  },
-  finish: {
-    color: 'bg-blue-200 dark:bg-blue-300',
-    icon: 'riFlagLine',
-  },
-};
-
-const { t, te } = useI18n();
+const { t } = useI18n();
 const route = useRoute();
 const router = useRouter();
 
 const ctxData = shallowRef({});
-const pagination = shallowReactive({
-  perPage: 10,
-  currentPage: 1,
-});
-
-function translateLog(log) {
-  const copyLog = { ...log };
-  const getTranslatation = (path, def) => {
-    const params = typeof path === 'string' ? { path } : path;
+const parentLog = shallowRef(null);
 
-    return te(params.path) ? t(params.path, params.params) : def;
-  };
-
-  if (['finish', 'stop'].includes(log.type)) {
-    copyLog.name = t(`log.types.${log.type}`);
-  } else {
-    copyLog.name = getTranslatation(
-      `workflow.blocks.${log.name}.name`,
-      log.name
-    );
-  }
-
-  copyLog.message = getTranslatation(
-    { path: `log.messages.${log.message}`, params: log },
-    log.message
-  );
-
-  return copyLog;
-}
-
-const activeLog = computed(() => Log.find(route.params.id));
-const history = computed(() =>
-  activeLog.value.history
-    .slice(
-      (pagination.currentPage - 1) * pagination.perPage,
-      pagination.currentPage * pagination.perPage
-    )
-    .map(translateLog)
-);
-const collectionLog = computed(() => {
-  if (activeLog.value.parentLog) {
-    return Log.find(activeLog.value.parentLog.id);
-  }
+const tabs = [
+  { id: 'logs', name: t('common.log', 2) },
+  { id: 'table', name: t('workflow.table.title') },
+  { id: 'variables', name: t('workflow.variables.title', 2) },
+];
 
-  return Log.find(activeLog.value.collectionLogId);
+const state = shallowReactive({
+  activeTab: 'logs',
+  workflowExists: false,
+});
+const tableData = shallowReactive({
+  converted: false,
+  body: [],
+  header: [],
+});
+const currentLog = shallowRef({
+  history: [],
+  data: {
+    table: [],
+    variables: {},
+  },
 });
-const workflowExists = computed(() =>
-  Workflow.find(activeLog.value.workflowId)
-);
 
 function deleteLog() {
-  Log.delete(route.params.id).then(() => {
-    const backHistory = window.history.state.back;
-
-    if (backHistory.startsWith('/workflows')) {
-      router.replace(backHistory);
-      return;
-    }
-
-    router.replace('/logs');
-  });
+  dbLogs.items
+    .where('id')
+    .equals(route.params.id)
+    .delete()
+    .then(() => {
+      const backHistory = window.history.state.back;
+
+      if (backHistory.startsWith('/workflows')) {
+        router.replace(backHistory);
+        return;
+      }
+
+      router.replace('/logs');
+    });
 }
 function goToWorkflow() {
   const backHistory = window.history.state.back;
-  let path = `/workflows/${activeLog.value.workflowId}`;
+  let path = `/workflows/${currentLog.value.workflowId}`;
 
   if (backHistory.startsWith(path)) {
     path = backHistory;
@@ -254,16 +123,56 @@ function goToWorkflow() {
 
   router.push(path);
 }
+function convertToTableData() {
+  const data = currentLog.value.data?.table;
+  if (!data) return;
 
-onMounted(async () => {
-  if (!activeLog.value) router.replace('/logs');
+  const [header, ...body] = convertArrObjTo2DArr(data);
 
-  const { logsCtxData } = await browser.storage.local.get('logsCtxData');
-  const logCtxData = logsCtxData && logsCtxData[route.params.id];
-  if (logCtxData) {
-    ctxData.value = logCtxData;
+  tableData.body = body;
+  tableData.header = header;
+  tableData.converted = true;
+}
+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(Workflow.find(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 {

+ 6 - 15
src/newtab/pages/workflows/Host.vue

@@ -131,13 +131,14 @@ import { useStore } from 'vuex';
 import { useI18n } from 'vue-i18n';
 import { useRoute, useRouter } from 'vue-router';
 import browser from 'webextension-polyfill';
+import { useLiveQuery } from '@/composable/liveQuery';
 import { useDialog } from '@/composable/dialog';
 import { useShortcut } from '@/composable/shortcut';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { parseJSON, findTriggerBlock } from '@/utils/helper';
 import { cleanWorkflowTriggers } from '@/utils/workflowTrigger';
 import { sendMessage } from '@/utils/message';
-import Log from '@/models/log';
+import dbLogs from '@/db/logs';
 import getTriggerText from '@/utils/triggerText';
 import WorkflowBuilder from '@/components/newtab/workflow/WorkflowBuilder.vue';
 import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
@@ -152,6 +153,9 @@ const router = useRouter();
 const dialog = useDialog();
 /* eslint-disable-next-line */
 const shortcut = useShortcut('editor:execute-workflow', executeWorkflow);
+const logs = useLiveQuery(() =>
+  dbLogs.items.query().where('workflowId').equals(route.params.id).toArray()
+);
 
 const workflowId = route.params.id;
 
@@ -166,17 +170,6 @@ const workflow = computed(() => store.state.workflowHosts[workflowId]);
 const workflowState = computed(() =>
   store.getters.getWorkflowState(workflowId)
 );
-const logs = computed(() =>
-  Log.query()
-    .where(
-      (item) =>
-        item.workflowId === workflowId &&
-        (!item.isInCollection || !item.isChildLog || !item.parentLog)
-    )
-    .limit(15)
-    .orderBy('startedAt', 'desc')
-    .get()
-);
 
 function syncWorkflow() {
   state.loadingSync = true;
@@ -230,9 +223,7 @@ function executeWorkflow() {
   sendMessage('workflow:execute', payload, 'background');
 }
 function deleteLog(logId) {
-  Log.delete(logId).then(() => {
-    store.dispatch('saveToStorage', 'logs');
-  });
+  dbLogs.items.where('id').equals(logId);
 }
 async function retrieveTriggerText() {
   const flow = parseJSON(workflow.value?.drawflow, null);

+ 44 - 43
src/newtab/pages/workflows/[id].vue

@@ -41,9 +41,8 @@
             />
           </button>
           <ui-tab value="editor">{{ t('common.editor') }}</ui-tab>
-          <ui-tab value="logs">{{ t('common.log', 2) }}</ui-tab>
-          <ui-tab value="running" class="flex items-center">
-            {{ t('common.running') }}
+          <ui-tab value="logs" class="flex items-center">
+            {{ t('common.log', 2) }}
             <span
               v-if="workflowState.length > 0"
               class="ml-2 p-1 text-center inline-block text-xs rounded-full bg-accent text-white dark:text-black"
@@ -91,6 +90,7 @@
           @save="saveWorkflow"
           @update="updateWorkflow"
           @load="editor = $event"
+          @loaded="onEditorLoaded"
           @deleteBlock="deleteBlock"
         >
           <ui-tabs
@@ -114,14 +114,19 @@
         </workflow-builder>
         <div v-else class="container pb-4 mt-24 px-4">
           <template v-if="activeTab === 'logs'">
-            <div v-if="logs.length === 0" class="text-center">
+            <div v-if="!logs || logs.length === 0" class="text-center">
               <img
                 src="@/assets/svg/files-and-folder.svg"
                 class="mx-auto max-w-sm"
               />
               <p class="text-xl font-semibold">{{ t('message.noData') }}</p>
             </div>
-            <shared-logs-table :logs="logs" class="w-full">
+            <shared-logs-table
+              :logs="logs"
+              :running="workflowState"
+              hide-select
+              class="w-full"
+            >
               <template #item-append="{ log: itemLog }">
                 <td class="text-right">
                   <v-remixicon
@@ -133,22 +138,6 @@
               </template>
             </shared-logs-table>
           </template>
-          <template v-else-if="activeTab === 'running'">
-            <div v-if="workflowState.length === 0" class="text-center">
-              <img
-                src="@/assets/svg/files-and-folder.svg"
-                class="mx-auto max-w-sm"
-              />
-              <p class="text-xl font-semibold">{{ t('message.noData') }}</p>
-            </div>
-            <div class="grid grid-cols-2 gap-4">
-              <shared-workflow-state
-                v-for="item in workflowState"
-                :key="item.id"
-                :data="item"
-              />
-            </div>
-          </template>
         </div>
       </keep-alive>
     </div>
@@ -222,7 +211,6 @@ import { useToast } from 'vue-toastification';
 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import defu from 'defu';
-import AES from 'crypto-js/aes';
 import browser from 'webextension-polyfill';
 import emitter from '@/lib/mitt';
 import { useDialog } from '@/composable/dialog';
@@ -238,8 +226,9 @@ import {
   parseJSON,
   throttle,
 } from '@/utils/helper';
-import Log from '@/models/log';
+import { useLiveQuery } from '@/composable/liveQuery';
 import decryptFlow, { getWorkflowPass } from '@/utils/decryptFlow';
+import dbLogs from '@/db/logs';
 import Workflow from '@/models/workflow';
 import workflowTrigger from '@/utils/workflowTrigger';
 import WorkflowShare from '@/components/newtab/workflow/WorkflowShare.vue';
@@ -252,7 +241,6 @@ import WorkflowGlobalData from '@/components/newtab/workflow/WorkflowGlobalData.
 import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
 import WorkflowSharedActions from '@/components/newtab/workflow/WorkflowSharedActions.vue';
 import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
-import SharedWorkflowState from '@/components/newtab/shared/SharedWorkflowState.vue';
 
 const { t } = useI18n();
 const store = useStore();
@@ -261,6 +249,14 @@ const toast = useToast();
 const router = useRouter();
 const dialog = useDialog();
 const shortcut = useShortcut('editor:toggle-sidebar', toggleSidebar);
+const logs = useLiveQuery(() =>
+  dbLogs.items
+    .where('workflowId')
+    .equals(route.params.id)
+    .reverse()
+    .limit(15)
+    .sortBy('endedAt')
+);
 
 const activeTabQuery = route.query.tab || 'editor';
 
@@ -371,17 +367,6 @@ const workflowModal = computed(() => workflowModals[state.modalName] || {});
 const workflowState = computed(() =>
   store.getters.getWorkflowState(workflowId)
 );
-const logs = computed(() =>
-  Log.query()
-    .where(
-      (item) =>
-        item.workflowId === workflowId &&
-        (!item.isInCollection || !item.isChildLog || !item.parentLog)
-    )
-    .limit(15)
-    .orderBy('startedAt', 'desc')
-    .get()
-);
 
 const updateBlockData = debounce((data) => {
   let payload = data;
@@ -681,9 +666,7 @@ function shareWorkflow() {
   }
 }
 function deleteLog(logId) {
-  Log.delete(logId).then(() => {
-    store.dispatch('saveToStorage', 'logs');
-  });
+  dbLogs.items.where('id').equals(logId).delete();
 }
 function workflowExporter() {
   const currentWorkflow = { ...workflow.value };
@@ -753,14 +736,10 @@ async function saveWorkflow() {
   if (workflowData.active === 'shared') return;
 
   try {
-    let flow = JSON.stringify(editor.value.export());
+    const flow = JSON.stringify(editor.value.export());
     const [triggerBlockId] = editor.value.getNodesFromName('trigger');
     const triggerBlock = editor.value.getNodeFromId(triggerBlockId);
 
-    if (workflow.value.isProtected) {
-      flow = AES.encrypt(flow, getWorkflowPass(workflow.value.pass)).toString();
-    }
-
     updateWorkflow({ drawflow: flow, trigger: triggerBlock?.data }).then(() => {
       if (triggerBlock) {
         workflowTrigger.register(workflowId, triggerBlock);
@@ -821,6 +800,28 @@ function renameWorkflow() {
     description: workflow.value.description,
   });
 }
+function onEditorLoaded(editorInstance) {
+  const { blockId } = route.query;
+  if (!blockId) return;
+
+  const node = editorInstance.getNodeFromId(blockId);
+  if (!node) return;
+
+  if (editorInstance.zoom !== 1) {
+    editorInstance.zoom = 1;
+    editorInstance.zoom_refresh();
+  }
+
+  const { width, height } = editorInstance.container.getBoundingClientRect();
+  const rectX = width / 2;
+  const rectY = height / 2;
+
+  editorInstance.translate_to(
+    -(node.pos_x - rectX),
+    -(node.pos_y - rectY),
+    editorInstance.zoom
+  );
+}
 
 provide('workflow', {
   data: workflow,

+ 7 - 2
src/newtab/router.js

@@ -1,5 +1,4 @@
 import { createRouter, createWebHashHistory } from 'vue-router';
-import Home from './pages/Home.vue';
 import Welcome from './pages/Welcome.vue';
 import Workflows from './pages/Workflows.vue';
 import WorkflowHost from './pages/workflows/Host.vue';
@@ -8,6 +7,7 @@ import Collections from './pages/Collections.vue';
 import CollectionsDetails from './pages/collections/[id].vue';
 import Logs from './pages/Logs.vue';
 import LogsDetails from './pages/logs/[id].vue';
+import LogsRunning from './pages/logs/Running.vue';
 import Settings from './pages/Settings.vue';
 import SettingsIndex from './pages/settings/SettingsIndex.vue';
 import SettingsAbout from './pages/settings/SettingsAbout.vue';
@@ -19,7 +19,7 @@ const routes = [
   {
     name: 'home',
     path: '/',
-    component: Home,
+    component: Workflows,
   },
   {
     name: 'welcome',
@@ -61,6 +61,11 @@ const routes = [
     path: '/logs/:id',
     component: LogsDetails,
   },
+  {
+    name: 'logs-running',
+    path: '/logs/:id/running',
+    component: LogsRunning,
+  },
   {
     path: '/settings',
     component: Settings,

+ 42 - 17
src/store/index.js

@@ -6,14 +6,44 @@ import defu from 'defu';
 import * as models from '@/models';
 import { firstWorkflows } from '@/utils/shared';
 import { fetchApi } from '@/utils/api';
-import { findTriggerBlock } from '@/utils/helper';
+import { findTriggerBlock, parseJSON } from '@/utils/helper';
 import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
 
 const store = createStore({
   plugins: [vuexORM(models)],
   state: () => ({
     user: null,
-    workflowState: [],
+    workflowState: [
+      {
+        id: '7F9HCTQXKMSDGlm_q_dVW',
+        state: {
+          activeTabUrl: '',
+          childWorkflowId: null,
+          tabIds: [null],
+          currentBlock: [
+            {
+              id: '1fb2464c-b94d-48f0-b40a-2903f2592428',
+              name: 'delay',
+              startedAt: 1655001148198,
+            },
+          ],
+          name: 'Child',
+          logs: [
+            {
+              type: 'success',
+              name: 'trigger',
+              blockId: '1991a5a0-a499-4c70-9040-03b37123b5df',
+              workerId: 'worker-1',
+              description: '',
+              duration: 1,
+              id: 1,
+            },
+          ],
+          startedTimestamp: 1655001148195,
+        },
+        workflowId: 'lPKjzF5cUfzckN3KgCdmX',
+      },
+    ],
     backupIds: [],
     contributors: null,
     hostWorkflows: {},
@@ -98,22 +128,17 @@ const store = createStore({
         throw error;
       }
     },
-    async retrieveWorkflowState({ commit }) {
-      try {
-        const { workflowState } = await browser.storage.local.get(
-          'workflowState'
-        );
+    retrieveWorkflowState({ commit }) {
+      const storedStates = localStorage.getItem('workflowState') || '{}';
+      const states = parseJSON(storedStates, {});
 
-        commit('updateState', {
-          key: 'workflowState',
-          value: Object.values(workflowState || {}).filter(
-            ({ isDestroyed, parentState }) =>
-              !isDestroyed && !parentState?.isCollection
-          ),
-        });
-      } catch (error) {
-        console.error(error);
-      }
+      commit('updateState', {
+        key: 'workflowState',
+        value: Object.values(states).filter(
+          ({ isDestroyed, parentState }) =>
+            !isDestroyed && !parentState?.isCollection
+        ),
+      });
     },
     saveToStorage({ getters }, key) {
       return new Promise((resolve, reject) => {

+ 56 - 0
src/utils/dataMigration.js

@@ -0,0 +1,56 @@
+import browser from 'webextension-polyfill';
+import dbLogs from '@/db/logs';
+
+export default async function () {
+  try {
+    const { logs, logsCtxData, migration } = await browser.storage.local.get([
+      'logs',
+      'migration',
+      'logsCtxData',
+    ]);
+    const hasMigrated = migration || {};
+    const backupData = {};
+
+    if (!hasMigrated.logs && logs) {
+      const ids = new Set();
+
+      const items = [];
+      const ctxData = [];
+      const logsData = [];
+      const histories = [];
+
+      for (let index = logs.length - 1; index > 0; index -= 1) {
+        const { data, history, ...item } = logs[index];
+        const logId = item.id;
+
+        if (!ids.has(logId) && ids.size < 500) {
+          items.push(item);
+          logsData.push({ logId, data });
+          histories.push({ logId, data: history });
+          ctxData.push({ logId, data: logsCtxData[logId] });
+
+          ids.add(logId);
+        }
+      }
+
+      await Promise.all([
+        dbLogs.items.bulkAdd(items),
+        dbLogs.ctxData.bulkAdd(ctxData),
+        dbLogs.logsData.bulkAdd(logsData),
+        dbLogs.histories.bulkAdd(histories),
+      ]);
+
+      backupData.logs = logs;
+      hasMigrated.logs = true;
+
+      await browser.storage.local.remove('logs');
+    }
+
+    await browser.storage.local.set({
+      migration: hasMigrated,
+      ...backupData,
+    });
+  } catch (error) {
+    console.error(error);
+  }
+}

+ 7 - 0
src/utils/handleFormElement.js

@@ -50,6 +50,7 @@ function formEvent(element, data) {
   }
 }
 async function inputText({ data, element, isEditable }) {
+  element?.focus();
   const elementKey = isEditable ? 'textContent' : 'value';
 
   if (data.delay > 0 && !document.hidden) {
@@ -82,6 +83,8 @@ async function inputText({ data, element, isEditable }) {
   element.dispatchEvent(
     new Event('change', { bubbles: true, cancelable: true })
   );
+
+  element?.blur();
 }
 
 export default async function (element, data) {
@@ -104,13 +107,17 @@ export default async function (element, data) {
   }
 
   if (data.type === 'checkbox' || data.type === 'radio') {
+    element?.focus();
     element.checked = data.selected;
     formEvent(element, { type: data.type, value: data.selected });
+    element?.blur();
     return;
   }
 
   if (data.type === 'select') {
+    element?.focus();
     element.value = data.value;
+    element?.blur();
     formEvent(element, data);
   }
 }

+ 5 - 0
src/utils/shared.js

@@ -1118,6 +1118,11 @@ export const communities = [
     icon: 'riDiscordLine',
     url: 'https://discord.gg/C6khwwTE84',
   },
+  {
+    name: 'YouTube',
+    icon: 'riYoutubeLine',
+    url: 'https://www.youtube.com/channel/UCL3qU64hW0fsIj2vOayOQUQ',
+  },
 ];
 
 export const elementsHighlightData = {

+ 1 - 1
tailwind.config.js

@@ -25,7 +25,7 @@ module.exports = {
       },
       fontFamily: {
         sans: ['Poppins', 'sans-serif'],
-        mono: ['JetBrains Mono', 'monospace'],
+        mono: ['Source Code Pro', 'monospace'],
       },
       container: {
         center: true,

+ 3 - 0
webpack.config.js

@@ -84,6 +84,9 @@ const options = {
       {
         test: /\.vue$/,
         loader: 'vue-loader',
+        options: {
+          reactivityTransform: true,
+        },
       },
       {
         test: /\.css$/,

+ 99 - 132
yarn.lock

@@ -276,7 +276,7 @@
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.15.0", "@babel/parser@^7.15.5", "@babel/parser@^7.16.4", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3":
+"@babel/parser@^7.15.5", "@babel/parser@^7.16.4", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3":
   version "7.17.8"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240"
   integrity sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==
@@ -1629,158 +1629,115 @@
   resolved "https://registry.yarnpkg.com/@viselect/vanilla/-/vanilla-3.0.0-beta.13.tgz#cb2ac109701ba25923a885e2ba691fb82302e243"
   integrity sha512-ML6uLrIpAgtFMRDXc5NfC2K7LiD+IzcsmUfYxrUVvvJgKfxm/eZjhHBvNVbJfto9SHsD9o+KjxJR/iexFyRLVg==
 
-"@vue/compiler-core@3.2.19":
-  version "3.2.19"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.19.tgz#b537dd377ce51fdb64e9b30ebfbff7cd70a64cb9"
-  integrity sha512-8dOPX0YOtaXol0Zf2cfLQ4NU/yHYl2H7DCKsLEZ7gdvPK6ZSEwGLJ7IdghhY2YEshEpC5RB9QKdC5I07z8Dtjg==
-  dependencies:
-    "@babel/parser" "^7.15.0"
-    "@vue/shared" "3.2.19"
-    estree-walker "^2.0.2"
-    source-map "^0.6.1"
-
-"@vue/compiler-core@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.31.tgz#d38f06c2cf845742403b523ab4596a3fda152e89"
-  integrity sha512-aKno00qoA4o+V/kR6i/pE+aP+esng5siNAVQ422TkBNM6qA4veXiZbSe8OTXHXquEi/f6Akc+nLfB4JGfe4/WQ==
+"@vue/compiler-core@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz#b3c42e04c0e0f2c496ff1784e543fbefe91e215a"
+  integrity sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==
   dependencies:
     "@babel/parser" "^7.16.4"
-    "@vue/shared" "3.2.31"
+    "@vue/shared" "3.2.37"
     estree-walker "^2.0.2"
     source-map "^0.6.1"
 
-"@vue/compiler-dom@3.2.19":
-  version "3.2.19"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.19.tgz#0607bc90de6af55fde73b09b3c4d0bf8cb597ed8"
-  integrity sha512-WzQoE8rfkFjPtIioc7SSgTsnz9g2oG61DU8KHnzPrRS7fW/lji6H2uCYJfp4Z6kZE8GjnHc1Ljwl3/gxDes0cw==
-  dependencies:
-    "@vue/compiler-core" "3.2.19"
-    "@vue/shared" "3.2.19"
-
-"@vue/compiler-dom@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.31.tgz#b1b7dfad55c96c8cc2b919cd7eb5fd7e4ddbf00e"
-  integrity sha512-60zIlFfzIDf3u91cqfqy9KhCKIJgPeqxgveH2L+87RcGU/alT6BRrk5JtUso0OibH3O7NXuNOQ0cDc9beT0wrg==
-  dependencies:
-    "@vue/compiler-core" "3.2.31"
-    "@vue/shared" "3.2.31"
-
-"@vue/compiler-sfc@3.2.19":
-  version "3.2.19"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.19.tgz#d412195a98ebd49b84602f171719294a1d9549be"
-  integrity sha512-pLlbgkO1UHTO02MSpa/sFOXUwIDxSMiKZ1ozE5n71CY4DM+YmI+G3gT/ZHZ46WBId7f3VTF/D8pGwMygcQbrQA==
-  dependencies:
-    "@babel/parser" "^7.15.0"
-    "@vue/compiler-core" "3.2.19"
-    "@vue/compiler-dom" "3.2.19"
-    "@vue/compiler-ssr" "3.2.19"
-    "@vue/ref-transform" "3.2.19"
-    "@vue/shared" "3.2.19"
-    estree-walker "^2.0.2"
-    magic-string "^0.25.7"
-    postcss "^8.1.10"
-    source-map "^0.6.1"
+"@vue/compiler-dom@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz#10d2427a789e7c707c872da9d678c82a0c6582b5"
+  integrity sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==
+  dependencies:
+    "@vue/compiler-core" "3.2.37"
+    "@vue/shared" "3.2.37"
 
-"@vue/compiler-sfc@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.31.tgz#d02b29c3fe34d599a52c5ae1c6937b4d69f11c2f"
-  integrity sha512-748adc9msSPGzXgibHiO6T7RWgfnDcVQD+VVwYgSsyyY8Ans64tALHZANrKtOzvkwznV/F4H7OAod/jIlp/dkQ==
+"@vue/compiler-sfc@3.2.37", "@vue/compiler-sfc@^3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz#3103af3da2f40286edcd85ea495dcb35bc7f5ff4"
+  integrity sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==
   dependencies:
     "@babel/parser" "^7.16.4"
-    "@vue/compiler-core" "3.2.31"
-    "@vue/compiler-dom" "3.2.31"
-    "@vue/compiler-ssr" "3.2.31"
-    "@vue/reactivity-transform" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/compiler-core" "3.2.37"
+    "@vue/compiler-dom" "3.2.37"
+    "@vue/compiler-ssr" "3.2.37"
+    "@vue/reactivity-transform" "3.2.37"
+    "@vue/shared" "3.2.37"
     estree-walker "^2.0.2"
     magic-string "^0.25.7"
     postcss "^8.1.10"
     source-map "^0.6.1"
 
-"@vue/compiler-ssr@3.2.19":
-  version "3.2.19"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.19.tgz#3e91ecf70f8f961c5f63eacd2139bcdab9a7a07c"
-  integrity sha512-oLon0Cn3O7WEYzzmzZavGoqXH+199LT+smdjBT3Uf3UX4HwDNuBFCmvL0TsqV9SQnIgKvBRbQ7lhbpnd4lqM3w==
-  dependencies:
-    "@vue/compiler-dom" "3.2.19"
-    "@vue/shared" "3.2.19"
-
-"@vue/compiler-ssr@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.31.tgz#4fa00f486c9c4580b40a4177871ebbd650ecb99c"
-  integrity sha512-mjN0rqig+A8TVDnsGPYJM5dpbjlXeHUm2oZHZwGyMYiGT/F4fhJf/cXy8QpjnLQK4Y9Et4GWzHn9PS8AHUnSkw==
+"@vue/compiler-ssr@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz#4899d19f3a5fafd61524a9d1aee8eb0505313cff"
+  integrity sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==
   dependencies:
-    "@vue/compiler-dom" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/compiler-dom" "3.2.37"
+    "@vue/shared" "3.2.37"
 
 "@vue/devtools-api@^6.0.0", "@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.0.0-beta.13":
   version "6.1.3"
   resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.1.3.tgz#a44c52e8fa6d22f84db3abdcdd0be5135b7dd7cf"
   integrity sha512-79InfO2xHv+WHIrH1bHXQUiQD/wMls9qBk6WVwGCbdwP7/3zINtvqPNMtmSHXsIKjvUAHc8L0ouOj6ZQQRmcXg==
 
-"@vue/reactivity-transform@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.31.tgz#0f5b25c24e70edab2b613d5305c465b50fc00911"
-  integrity sha512-uS4l4z/W7wXdI+Va5pgVxBJ345wyGFKvpPYtdSgvfJfX/x2Ymm6ophQlXXB6acqGHtXuBqNyyO3zVp9b1r0MOA==
+"@vue/reactivity-transform@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz#0caa47c4344df4ae59f5a05dde2a8758829f8eca"
+  integrity sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==
   dependencies:
     "@babel/parser" "^7.16.4"
-    "@vue/compiler-core" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/compiler-core" "3.2.37"
+    "@vue/shared" "3.2.37"
     estree-walker "^2.0.2"
     magic-string "^0.25.7"
 
-"@vue/reactivity@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.31.tgz#fc90aa2cdf695418b79e534783aca90d63a46bbd"
-  integrity sha512-HVr0l211gbhpEKYr2hYe7hRsV91uIVGFYNHj73njbARVGHQvIojkImKMaZNDdoDZOIkMsBc9a1sMqR+WZwfSCw==
+"@vue/reactivity@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.37.tgz#5bc3847ac58828e2b78526e08219e0a1089f8848"
+  integrity sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==
   dependencies:
-    "@vue/shared" "3.2.31"
+    "@vue/shared" "3.2.37"
 
-"@vue/ref-transform@3.2.19":
-  version "3.2.19"
-  resolved "https://registry.yarnpkg.com/@vue/ref-transform/-/ref-transform-3.2.19.tgz#cf0f986486bb26838fbd09749e927bab19745600"
-  integrity sha512-03wwUnoIAeKti5IGGx6Vk/HEBJ+zUcm5wrUM3+PQsGf7IYnXTbeIfHHpx4HeSeWhnLAjqZjADQwW8uA4rBmVbg==
+"@vue/runtime-core@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.37.tgz#7ba7c54bb56e5d70edfc2f05766e1ca8519966e3"
+  integrity sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==
   dependencies:
-    "@babel/parser" "^7.15.0"
-    "@vue/compiler-core" "3.2.19"
-    "@vue/shared" "3.2.19"
-    estree-walker "^2.0.2"
-    magic-string "^0.25.7"
+    "@vue/reactivity" "3.2.37"
+    "@vue/shared" "3.2.37"
 
-"@vue/runtime-core@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.31.tgz#9d284c382f5f981b7a7b5971052a1dc4ef39ac7a"
-  integrity sha512-Kcog5XmSY7VHFEMuk4+Gap8gUssYMZ2+w+cmGI6OpZWYOEIcbE0TPzzPHi+8XTzAgx1w/ZxDFcXhZeXN5eKWsA==
+"@vue/runtime-dom@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz#002bdc8228fa63949317756fb1e92cdd3f9f4bbd"
+  integrity sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==
   dependencies:
-    "@vue/reactivity" "3.2.31"
-    "@vue/shared" "3.2.31"
-
-"@vue/runtime-dom@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.31.tgz#79ce01817cb3caf2c9d923f669b738d2d7953eff"
-  integrity sha512-N+o0sICVLScUjfLG7u9u5XCjvmsexAiPt17GNnaWHJUfsKed5e85/A3SWgKxzlxx2SW/Hw7RQxzxbXez9PtY3g==
-  dependencies:
-    "@vue/runtime-core" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/runtime-core" "3.2.37"
+    "@vue/shared" "3.2.37"
     csstype "^2.6.8"
 
-"@vue/server-renderer@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.31.tgz#201e9d6ce735847d5989403af81ef80960da7141"
-  integrity sha512-8CN3Zj2HyR2LQQBHZ61HexF5NReqngLT3oahyiVRfSSvak+oAvVmu8iNLSu6XR77Ili2AOpnAt1y8ywjjqtmkg==
+"@vue/server-renderer@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.37.tgz#840a29c8dcc29bddd9b5f5ffa22b95c0e72afdfc"
+  integrity sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==
   dependencies:
-    "@vue/compiler-ssr" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/compiler-ssr" "3.2.37"
+    "@vue/shared" "3.2.37"
+
+"@vue/shared@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.37.tgz#8e6adc3f2759af52f0e85863dfb0b711ecc5c702"
+  integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==
 
-"@vue/shared@3.2.19":
-  version "3.2.19"
-  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.19.tgz#111ec3da18337d86274446984c49925b1b2b2dd7"
-  integrity sha512-Knqhx7WieLdVgwCAZgTVrDCXZ50uItuecLh9JdLC8O+a5ayaSyIQYveUK3hCRNC7ws5zalHmZwfdLMGaS8r4Ew==
+"@vueuse/rxjs@^8.6.0":
+  version "8.6.0"
+  resolved "https://registry.yarnpkg.com/@vueuse/rxjs/-/rxjs-8.6.0.tgz#0b6a41cfff1436cec8af068cb8fab4abe9d62c17"
+  integrity sha512-mgww+P7lWlHQY8t08JOsWtP8mBmSf0m04laKMLPqQIf6R2awX5lzlpAasIDYlF/+ETeCbkzPR6RF5ifF22y+Cg==
+  dependencies:
+    "@vueuse/shared" "8.6.0"
+    vue-demi "*"
 
-"@vue/shared@3.2.31":
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.31.tgz#c90de7126d833dcd3a4c7534d534be2fb41faa4e"
-  integrity sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ==
+"@vueuse/shared@8.6.0":
+  version "8.6.0"
+  resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-8.6.0.tgz#63dad9fc4b73a7fccbe5d6b97adeacf73d4fec41"
+  integrity sha512-Y/IVywZo7IfEoSSEtCYpkVEmPV7pU35mEIxV7PbD/D3ly18B3mEsBaPbtDkNM/QP3zAZ5mn4nEkOfddX4uwuIA==
+  dependencies:
+    vue-demi "*"
 
 "@vuex-orm/core@^0.36.4":
   version "0.36.4"
@@ -3107,6 +3064,11 @@ detective@^5.2.0:
     defined "^1.0.0"
     minimist "^1.1.1"
 
+dexie@^3.2.2:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.2.tgz#fa6f2a3c0d6ed0766f8d97a03720056f88fe0e01"
+  integrity sha512-q5dC3HPmir2DERlX+toCBbHQXW5MsyrFqPFcovkH9N2S/UW/H3H5AWAB6iEOExeraAu+j+zRDG+zg/D7YhH0qg==
+
 didyoumean@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@@ -6297,7 +6259,7 @@ run-parallel@^1.1.9:
   dependencies:
     queue-microtask "^1.2.2"
 
-rxjs@^7.5.1:
+rxjs@^7.5.1, rxjs@^7.5.5:
   version "7.5.5"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f"
   integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==
@@ -7214,6 +7176,11 @@ vary@~1.1.2:
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
 
+vue-demi@*:
+  version "0.13.1"
+  resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.1.tgz#7604904c88be338418a10abbc94d5b8caa14cb8c"
+  integrity sha512-xmkJ56koG3ptpLnpgmIzk9/4nFf4CqduSJbUM0OdPoU87NwRuZ6x49OLhjSa/fC15fV+5CbEnrxU4oyE022svg==
+
 vue-eslint-parser@^9.0.1:
   version "9.0.2"
   resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.0.2.tgz#d2535516f3f55adb387939427fe741065eb7948a"
@@ -7242,10 +7209,10 @@ vue-inspector-agnostic@^1.0.0:
   resolved "https://registry.yarnpkg.com/vue-inspector-agnostic/-/vue-inspector-agnostic-1.0.0.tgz#e98d317a1c37619e84c69b1f216eab94970fa253"
   integrity sha512-8awtLkMS1ohA6xplv/f/L6cJoAJNFO6vhHz61xjCJZMwPveMI45k1yMUkxmVJJOkZJoR8SCWSUN7RLbZeOUsRQ==
 
-vue-loader@16.8.1:
-  version "16.8.1"
-  resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.8.1.tgz#354f12bc0897954158b71590f800295713a7792d"
-  integrity sha512-V53TJbHmzjBhCG5OYI2JWy/aYDspz4oVHKxS43Iy212GjGIG1T3EsB3+GWXFm/1z5VwjdjLmdZUFYM70y77vtQ==
+vue-loader@^17.0.0:
+  version "17.0.0"
+  resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-17.0.0.tgz#2eaa80aab125b19f00faa794b5bd867b17f85acb"
+  integrity sha512-OWSXjrzIvbF2LtOUmxT3HYgwwubbfFelN8PAP9R9dwpIkj48TVioHhWWSx7W7fk+iF5cgg3CBJRxwTdtLU4Ecg==
   dependencies:
     chalk "^4.1.0"
     hash-sum "^2.0.0"
@@ -7263,16 +7230,16 @@ vue-toastification@^2.0.0-rc.5:
   resolved "https://registry.yarnpkg.com/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz#92798604d806ae473cfb76ed776fae294280f8f8"
   integrity sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==
 
-vue@^3.2.31:
-  version "3.2.31"
-  resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.31.tgz#e0c49924335e9f188352816788a4cca10f817ce6"
-  integrity sha512-odT3W2tcffTiQCy57nOT93INw1auq5lYLLYtWpPYQQYQOOdHiqFct9Xhna6GJ+pJQaF67yZABraH47oywkJgFw==
+vue@^3.2.37:
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.37.tgz#da220ccb618d78579d25b06c7c21498ca4e5452e"
+  integrity sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==
   dependencies:
-    "@vue/compiler-dom" "3.2.31"
-    "@vue/compiler-sfc" "3.2.31"
-    "@vue/runtime-dom" "3.2.31"
-    "@vue/server-renderer" "3.2.31"
-    "@vue/shared" "3.2.31"
+    "@vue/compiler-dom" "3.2.37"
+    "@vue/compiler-sfc" "3.2.37"
+    "@vue/runtime-dom" "3.2.37"
+    "@vue/server-renderer" "3.2.37"
+    "@vue/shared" "3.2.37"
 
 vuedraggable@^4.1.0:
   version "4.1.0"