Browse Source

feat: migrate from `vuex` to `pinia`

Ahmad Kholid 3 years ago
parent
commit
88e8871db8
60 changed files with 1054 additions and 4334 deletions
  1. 1 1
      package.json
  2. 6 0
      src/assets/css/flow.css
  3. 3 1
      src/background/index.js
  4. 8 0
      src/components/block/BlockBasic.vue
  5. 15 5
      src/components/block/BlockGroup.vue
  6. 0 6
      src/components/newtab/app/AppSidebar.vue
  7. 21 13
      src/components/newtab/settings/SettingsCloudBackup.vue
  8. 0 225
      src/components/newtab/workflow/WorkflowActions.vue
  9. 0 980
      src/components/newtab/workflow/WorkflowBuilder.vue
  10. 36 8
      src/components/newtab/workflow/WorkflowEditor.vue
  11. 4 4
      src/components/newtab/workflow/WorkflowShare.vue
  12. 5 5
      src/components/newtab/workflow/edit/EditExecuteWorkflow.vue
  13. 35 29
      src/components/newtab/workflow/editor/EditorLocalActions.vue
  14. 19 20
      src/components/newtab/workflows/WorkflowsFolder.vue
  15. 4 6
      src/components/newtab/workflows/WorkflowsHosted.vue
  16. 23 20
      src/components/newtab/workflows/WorkflowsLocal.vue
  17. 3 3
      src/components/newtab/workflows/WorkflowsShared.vue
  18. 9 6
      src/components/popup/home/HomeStartRecording.vue
  19. 2 10
      src/lib/pinia.js
  20. 0 20
      src/lib/vuexOrm.js
  21. 0 33
      src/models/collection.js
  22. 0 30
      src/models/folder.js
  23. 0 4
      src/models/index.js
  24. 0 38
      src/models/log.js
  25. 0 114
      src/models/workflow.js
  26. 43 37
      src/newtab/App.vue
  27. 0 2
      src/newtab/index.js
  28. 0 144
      src/newtab/pages/Collections.vue
  29. 0 65
      src/newtab/pages/Home.vue
  30. 3 1
      src/newtab/pages/Logs.vue
  31. 20 17
      src/newtab/pages/ScheduledWorkflow.vue
  32. 16 60
      src/newtab/pages/Workflows.vue
  33. 0 365
      src/newtab/pages/collections/[id].vue
  34. 3 3
      src/newtab/pages/logs/Running.vue
  35. 3 2
      src/newtab/pages/logs/[id].vue
  36. 3 6
      src/newtab/pages/settings/SettingsAbout.vue
  37. 24 32
      src/newtab/pages/settings/SettingsBackup.vue
  38. 4 0
      src/newtab/pages/settings/SettingsEditor.vue
  39. 3 16
      src/newtab/pages/settings/SettingsIndex.vue
  40. 44 58
      src/newtab/pages/workflows/Host.vue
  41. 0 941
      src/newtab/pages/workflows/[id].old.vue
  42. 18 16
      src/newtab/pages/workflows/[id].vue
  43. 0 12
      src/newtab/router.js
  44. 4 4
      src/popup/App.vue
  45. 2 2
      src/popup/index.js
  46. 33 38
      src/popup/pages/Home.vue
  47. 159 158
      src/popup/pages/Recording.vue
  48. 0 170
      src/store/index.js
  49. 34 1
      src/stores/folder.js
  50. 131 0
      src/stores/hostedWorkflow.js
  51. 7 0
      src/stores/main.js
  52. 53 0
      src/stores/sharedWorkflow.js
  53. 7 1
      src/stores/user.js
  54. 168 105
      src/stores/workflow.js
  55. 4 6
      src/utils/api.js
  56. 5 12
      src/utils/dataMigration.js
  57. 9 3
      src/utils/helper.js
  58. 40 35
      src/utils/workflowData.js
  59. 2 2
      src/utils/workflowTrigger.js
  60. 18 439
      yarn.lock

+ 1 - 1
package.json

@@ -30,7 +30,7 @@
     "*.{js,ts,vue}": "eslint --fix"
   },
   "dependencies": {
-    "@braks/vue-flow": "^0.4.24",
+    "@braks/vue-flow": "^0.4.25",
     "@codemirror/lang-javascript": "^6.0.0",
     "@codemirror/lang-json": "^6.0.0",
     "@codemirror/language": "^6.0.0",

+ 6 - 0
src/assets/css/flow.css

@@ -28,6 +28,12 @@
 	}
 }
 
+.vue-flow.disabled {
+	.vue-flow__handle {
+		pointer-events: none;
+	}
+}
+
 .vue-flow__edge.selected .vue-flow__edge-path {
 	stroke: theme('colors.green.300');
 }

+ 3 - 1
src/background/index.js

@@ -56,7 +56,9 @@ const workflow = {
       'workflows',
       'workflowHosts',
     ]);
-    let findWorkflow = workflows.find(({ id }) => id === workflowId);
+    let findWorkflow = Array.isArray(workflows)
+      ? workflows.find(({ id }) => id === workflowId)
+      : workflows[workflowId];
 
     if (!findWorkflow) {
       findWorkflow = Object.values(workflowHosts || {}).find(

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

@@ -94,6 +94,14 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  events: {
+    type: Object,
+    default: () => ({}),
+  },
+  dimensions: {
+    type: Object,
+    default: () => ({}),
+  },
 });
 defineEmits(['delete', 'edit', 'update']);
 

+ 15 - 5
src/components/block/BlockGroup.vue

@@ -32,7 +32,7 @@
     <draggable
       v-model="state.blocks"
       item-key="itemId"
-      class="px-4 mb-4 overflow-auto nowheel scroll text-sm space-y-1 max-h-60"
+      class="px-4 pb-4 overflow-auto nowheel scroll text-sm space-y-1 max-h-60"
       @mousedown.stop
       @dragover.prevent
       @drop="handleDrop"
@@ -112,6 +112,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  editor: {
+    type: Object,
+    default: () => ({}),
+  },
 });
 const emit = defineEmits(['update', 'delete', 'edit']);
 
@@ -122,6 +126,7 @@ const excludeBlocks = [
   'loop-breakpoint',
   'blocks-group',
   'conditions',
+  'webhook',
   'element-exists',
 ];
 
@@ -167,7 +172,6 @@ function handleDrop(event) {
   event.stopPropagation();
 
   const droppedBlock = JSON.parse(event.dataTransfer.getData('block') || null);
-
   if (!droppedBlock || droppedBlock.fromGroup) return;
 
   const { id, data, blockId } = droppedBlock;
@@ -183,7 +187,7 @@ function handleDrop(event) {
   }
 
   if (blockId) {
-    emit('delete', id);
+    emit('delete', blockId);
   }
 
   state.blocks.push({ id, data, itemId: nanoid(5) });
@@ -193,13 +197,19 @@ watch(
   () => state.blocks,
   () => {
     if (!state.retrieved) return;
-
     emit('update', { blocks: state.blocks });
   },
   { deep: true }
 );
 
 onMounted(() => {
-  state.blocks = cloneDeep(props.data.blocks);
+  const copiedBlocks = cloneDeep(props.data.blocks);
+  state.blocks = Array.isArray(copiedBlocks)
+    ? copiedBlocks
+    : Object.values(copiedBlocks);
+
+  setTimeout(() => {
+    state.retrieved = true;
+  }, 500);
 });
 </script>

+ 0 - 6
src/components/newtab/app/AppSidebar.vue

@@ -117,12 +117,6 @@ const tabs = [
     path: '/schedule',
     shortcut: getShortcut('page:schedule', '/triggers'),
   },
-  {
-    id: 'collection',
-    icon: 'riFolderLine',
-    path: '/collections',
-    shortcut: getShortcut('page:collections', '/collections'),
-  },
   {
     id: 'log',
     icon: 'riHistoryLine',

+ 21 - 13
src/components/newtab/settings/SettingsCloudBackup.vue

@@ -123,21 +123,22 @@
 <script setup>
 import { computed, reactive, onMounted } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { useStore } from 'vuex';
 import { useToast } from 'vue-toastification';
 import browser from 'webextension-polyfill';
 import { fetchApi, cacheApi } from '@/utils/api';
 import { convertWorkflow } from '@/utils/workflowData';
 import { parseJSON } from '@/utils/helper';
+import { useUserStore } from '@/stores/user';
+import { useWorkflowStore } from '@/stores/workflow';
 import dayjs from '@/lib/dayjs';
-import Workflow from '@/models/workflow';
 import SettingsBackupItems from './SettingsBackupItems.vue';
 
 defineEmits(['close']);
 
 const { t } = useI18n();
-const store = useStore();
 const toast = useToast();
+const userStore = useUserStore();
+const workflowStore = useWorkflowStore();
 
 const state = reactive({
   query: '',
@@ -154,8 +155,8 @@ const backupState = reactive({
 });
 
 const workflows = computed(() =>
-  Workflow.query()
-    .where(({ name, id }) => {
+  workflowStore.getWorkflows
+    .filter(({ name, id }) => {
       const isInCloud = state.cloudWorkflows.some(
         (workflow) => workflow.id === id
       );
@@ -165,8 +166,7 @@ const workflows = computed(() =>
         !isInCloud
       );
     })
-    .orderBy('createdAt', 'desc')
-    .get()
+    .sort((a, b) => a.createdAt - b.createdAt)
 );
 const backupWorkflows = computed(() =>
   state.cloudWorkflows.filter(({ name }) =>
@@ -174,7 +174,7 @@ const backupWorkflows = computed(() =>
   )
 );
 const workflowLimit = computed(() => {
-  const maxWorkflow = store.state.user.limit.backupWorkflow;
+  const maxWorkflow = userStore.user.limit.backupWorkflow;
 
   return maxWorkflow - state.cloudWorkflows.length;
 });
@@ -277,7 +277,7 @@ async function backupWorkflowsToCloud(workflowId) {
 
     const workflowIds = workflowId ? [workflowId] : state.selectedWorkflows;
     const workflowsPayload = workflowIds.reduce((acc, id) => {
-      const findWorkflow = Workflow.find(id);
+      const findWorkflow = workflowStore.getById(id);
 
       if (!findWorkflow) return acc;
 
@@ -303,12 +303,13 @@ async function backupWorkflowsToCloud(workflowId) {
       method: 'POST',
       body: JSON.stringify({ workflows: workflowsPayload }),
     });
+    const result = await response.json();
 
     if (!response.ok) {
-      throw new Error(response.statusText);
+      throw new Error(result.message);
     }
 
-    const { lastBackup, data, ids } = await response.json();
+    const { lastBackup, data, ids } = result;
 
     backupState.uploading = false;
     backupState.workflowId = '';
@@ -319,7 +320,7 @@ async function backupWorkflowsToCloud(workflowId) {
       );
       if (isExists) return;
 
-      state.cloudWorkflows.push(Workflow.find(id));
+      state.cloudWorkflows.push(workflowStore.getById(id));
     });
 
     state.lastSync = lastBackup;
@@ -333,7 +334,14 @@ async function backupWorkflowsToCloud(workflowId) {
     userWorkflows.backup = state.cloudWorkflows;
     sessionStorage.setItem('user-workflows', JSON.stringify(userWorkflows));
 
-    await Workflow.insertOrUpdate({ data });
+    await Promise.allSettled(
+      data.map(async () => {
+        workflowStore.update({
+          data,
+          id: data.id,
+        });
+      })
+    );
     await browser.storage.local.set({
       lastBackup,
       backupIds: ids,

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

@@ -1,225 +0,0 @@
-<template>
-  <ui-card v-if="!workflow.isProtected" padding="p-1 flex items-center">
-    <ui-popover>
-      <template #trigger>
-        <button
-          v-tooltip.group="t('workflow.host.title')"
-          class="hoverable p-2 rounded-lg"
-        >
-          <v-remixicon
-            :class="{ 'text-primary': data.isHost }"
-            name="riBaseStationLine"
-          />
-        </button>
-      </template>
-      <div :class="{ 'text-center': data.loadingHost }" class="w-64">
-        <div class="flex items-center text-gray-600 dark:text-gray-200">
-          <p>
-            {{ t('workflow.host.set') }}
-          </p>
-          <a
-            :title="t('common.docs')"
-            href="https://docs.automa.site/guide/host-workflow.html"
-            target="_blank"
-            class="ml-1"
-          >
-            <v-remixicon name="riInformationLine" size="20" />
-          </a>
-          <div class="flex-grow"></div>
-          <template v-if="$store.state.user">
-            <ui-spinner v-if="data.loadingHost" color="text-accent" />
-            <ui-switch
-              v-else
-              :model-value="data.isHost"
-              @change="$emit('host', $event)"
-            />
-          </template>
-          <ui-switch v-else v-close-popover @click="$emit('host', 'auth')" />
-        </div>
-        <transition-expand>
-          <ui-input
-            v-if="data.isHost"
-            v-tooltip:bottom="t('workflow.host.id')"
-            :model-value="host.hostId"
-            prepend-icon="riLinkM"
-            readonly
-            class="mt-4 block w-full"
-            @click="$event.target.select()"
-          />
-        </transition-expand>
-      </div>
-    </ui-popover>
-    <button
-      v-tooltip.group="t('workflow.share.title')"
-      :class="{ 'text-primary': data.hasShared }"
-      class="hoverable p-2 rounded-lg"
-      @click="$emit('share')"
-    >
-      <v-remixicon name="riShareLine" />
-    </button>
-  </ui-card>
-  <ui-card padding="p-1 ml-4">
-    <button
-      v-for="item in modalActions"
-      :key="item.id"
-      v-tooltip.group="item.name"
-      class="hoverable p-2 rounded-lg"
-      @click="$emit('showModal', item.id)"
-    >
-      <v-remixicon :name="item.icon" />
-    </button>
-  </ui-card>
-  <ui-card padding="p-1 ml-4 flex items-center">
-    <button
-      v-if="!workflow.isDisabled"
-      v-tooltip.group="
-        `${t('common.execute')} (${
-          shortcuts['editor:execute-workflow'].readable
-        })`
-      "
-      class="hoverable p-2 rounded-lg"
-      @click="$emit('execute')"
-    >
-      <v-remixicon name="riPlayLine" />
-    </button>
-    <button
-      v-else
-      v-tooltip="t('workflow.clickToEnable')"
-      class="p-2"
-      @click="$emit('update', { isDisabled: false })"
-    >
-      {{ t('common.disabled') }}
-    </button>
-  </ui-card>
-  <ui-card padding="p-1 ml-4 space-x-1">
-    <ui-popover>
-      <template #trigger>
-        <button class="rounded-lg p-2 hoverable">
-          <v-remixicon name="riMore2Line" />
-        </button>
-      </template>
-      <ui-list class="w-36">
-        <ui-list-item
-          class="cursor-pointer"
-          @click="$emit('update', { isDisabled: !workflow.isDisabled })"
-        >
-          <v-remixicon name="riToggleLine" class="mr-2 -ml-1" />
-          {{ t(`common.${workflow.isDisabled ? 'enable' : 'disable'}`) }}
-        </ui-list-item>
-        <ui-list-item
-          v-for="item in moreActions"
-          :key="item.id"
-          v-close-popover
-          class="cursor-pointer"
-          @click="$emit(item.id)"
-        >
-          <v-remixicon :name="item.icon" class="mr-2 -ml-1" />
-          {{ item.name }}
-        </ui-list-item>
-      </ui-list>
-    </ui-popover>
-    <ui-button
-      :title="shortcuts['editor:save'].readable"
-      variant="accent"
-      class="relative"
-      @click="$emit('save')"
-    >
-      <span
-        v-if="isDataChanged"
-        class="flex h-3 w-3 absolute top-0 left-0 -ml-1 -mt-1"
-      >
-        <span
-          class="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"
-        ></span>
-        <span
-          class="relative inline-flex rounded-full h-3 w-3 bg-blue-600"
-        ></span>
-      </span>
-      <v-remixicon name="riSaveLine" class="mr-2 -ml-1 my-1" />
-      {{ t('common.save') }}
-    </ui-button>
-  </ui-card>
-</template>
-<script setup>
-import { useI18n } from 'vue-i18n';
-import { useGroupTooltip } from '@/composable/groupTooltip';
-import { useShortcut, getShortcut } from '@/composable/shortcut';
-
-defineProps({
-  isDataChanged: {
-    type: Boolean,
-    default: false,
-  },
-  workflow: {
-    type: Object,
-    default: () => ({}),
-  },
-  host: {
-    type: Object,
-    default: () => ({}),
-  },
-  data: {
-    type: Object,
-    default: () => ({}),
-  },
-});
-const emit = defineEmits([
-  'showModal',
-  'execute',
-  'rename',
-  'delete',
-  'save',
-  'export',
-  'update',
-  'share',
-  'host',
-]);
-
-useGroupTooltip();
-
-const { t } = useI18n();
-const shortcuts = useShortcut(
-  [
-    getShortcut('editor:save', 'save'),
-    getShortcut('editor:execute-workflow', 'execute'),
-  ],
-  ({ data }) => {
-    emit(data);
-  }
-);
-
-const modalActions = [
-  {
-    id: 'table',
-    name: t('workflow.table.title'),
-    icon: 'riTable2',
-  },
-  {
-    id: 'global-data',
-    name: t('common.globalData'),
-    icon: 'riDatabase2Line',
-  },
-  {
-    id: 'settings',
-    name: t('common.settings'),
-    icon: 'riSettings3Line',
-  },
-];
-const moreActions = [
-  {
-    id: 'export',
-    name: t('common.export'),
-    icon: 'riDownloadLine',
-  },
-  {
-    id: 'rename',
-    name: t('common.rename'),
-    icon: 'riPencilLine',
-  },
-  {
-    id: 'delete',
-    name: t('common.delete'),
-    icon: 'riDeleteBin7Line',
-  },
-];
-</script>

+ 0 - 980
src/components/newtab/workflow/WorkflowBuilder.vue

@@ -1,980 +0,0 @@
-<template>
-  <div
-    v-bind="{ arrow: $store.state.settings.editor.arrow }"
-    id="drawflow"
-    class="parent-drawflow relative"
-    @drop="dropHandler"
-    @dragover.prevent="handleDragOver"
-  >
-    <div
-      class="flex items-end absolute w-full p-4 left-0 bottom-0 justify-between z-10"
-    >
-      <div id="zoom">
-        <button
-          v-tooltip.group="t('workflow.editor.resetZoom')"
-          class="p-2 rounded-lg bg-white dark:bg-gray-800 mr-2"
-          @click="editor.zoom_reset()"
-        >
-          <v-remixicon name="riFullscreenLine" />
-        </button>
-        <div class="rounded-lg bg-white dark:bg-gray-800 inline-block">
-          <button
-            v-tooltip.group="t('workflow.editor.zoomOut')"
-            class="p-2 rounded-lg relative z-10"
-            @click="editor.zoom_out()"
-          >
-            <v-remixicon name="riSubtractLine" />
-          </button>
-          <hr class="h-6 border-r inline-block" />
-          <button
-            v-tooltip.group="t('workflow.editor.zoomIn')"
-            class="p-2 rounded-lg"
-            @click="editor.zoom_in()"
-          >
-            <v-remixicon name="riAddLine" />
-          </button>
-        </div>
-        <!-- <workflow-builder-search-blocks :editor="editor" /> -->
-      </div>
-      <slot v-bind="{ editor }"></slot>
-    </div>
-    <ui-popover
-      v-model="contextMenu.show"
-      :options="contextMenu.position"
-      padding="p-3"
-      @close="clearContextMenu"
-    >
-      <ui-list class="space-y-1 w-52">
-        <ui-list-item
-          v-for="item in contextMenu.items"
-          :key="item.id"
-          v-close-popover
-          class="cursor-pointer justify-between"
-          @click="contextMenuHandler[item.event]"
-        >
-          <span>
-            {{ item.name }}
-          </span>
-          <span
-            v-if="item.shortcut"
-            class="text-sm capitalize text-gray-600 dark:text-gray-200"
-          >
-            {{ item.shortcut }}
-          </span>
-        </ui-list-item>
-      </ui-list>
-    </ui-popover>
-  </div>
-</template>
-<script>
-/* eslint-disable camelcase */
-import {
-  onMounted,
-  shallowRef,
-  reactive,
-  getCurrentInstance,
-  watch,
-  onBeforeUnmount,
-} from 'vue';
-import { useStore } from 'vuex';
-import { useRoute } from 'vue-router';
-import { useI18n } from 'vue-i18n';
-import { compare } from 'compare-versions';
-import defu from 'defu';
-import SelectionArea from '@viselect/vanilla';
-import browser from 'webextension-polyfill';
-import emitter from '@/lib/mitt';
-import {
-  useShortcut,
-  getShortcut,
-  getReadableShortcut,
-} from '@/composable/shortcut';
-import { tasks, excludeOnError } from '@/utils/shared';
-import { parseJSON } from '@/utils/helper';
-import { useGroupTooltip } from '@/composable/groupTooltip';
-import drawflow from '@/lib/drawflow';
-// import WorkflowBuilderSearchBlocks from './WorkflowBuilderSearchBlocks.vue';
-
-export default {
-  // components: { WorkflowBuilderSearchBlocks },
-  props: {
-    data: {
-      type: [Object, String],
-      default: null,
-    },
-    isShared: {
-      type: Boolean,
-      default: false,
-    },
-    version: {
-      type: [String, Boolean],
-      default: '',
-    },
-    mode: {
-      type: String,
-      default: 'edit',
-    },
-  },
-  emits: ['load', 'loaded', 'deleteBlock', 'update', 'save'],
-  setup(props, { emit }) {
-    useGroupTooltip();
-
-    const { t } = useI18n();
-    const route = useRoute();
-    const store = useStore();
-
-    const contextMenuItems = {
-      common: [
-        {
-          id: 'paste',
-          name: t('workflow.editor.paste'),
-          icon: 'riFileCopyLine',
-          event: 'pasteBlocks',
-          shortcut: getReadableShortcut('mod+v'),
-        },
-      ],
-      block: [
-        {
-          id: 'copy',
-          name: t('workflow.editor.copy'),
-          icon: 'riFileCopyLine',
-          event: 'copyBlocks',
-          shortcut: getReadableShortcut('mod+c'),
-        },
-        {
-          id: 'duplicate',
-          name: t('workflow.editor.duplicate'),
-          icon: 'riFileCopyLine',
-          event: 'duplicateBlock',
-          shortcut: getShortcut('editor:duplicate-block').readable,
-        },
-        {
-          id: 'delete',
-          name: t('common.delete'),
-          icon: 'riDeleteBin7Line',
-          event: 'deleteBlock',
-          shortcut: 'Del',
-        },
-      ],
-    };
-
-    let activeNode = null;
-    let hasDragged = false;
-    let isDragging = false;
-    let selectedElements = [];
-
-    const selection = shallowRef(null);
-    const editor = shallowRef(null);
-    const contextMenu = reactive({
-      items: [],
-      data: null,
-      show: false,
-      position: {},
-    });
-
-    const workflowId = route.params.id;
-
-    const prevSelectedEl = {
-      output: null,
-      connection: null,
-      nodeContent: null,
-    };
-    const isOutputEl = (el) => el.classList.contains('output');
-    const isConnectionEl = (el) =>
-      el.matches('path.main-path') ||
-      el.parentElement.classList.contains('connection');
-
-    function toggleHoverClass({ target, name, active, classes }) {
-      const prev = prevSelectedEl[name];
-
-      if (active) {
-        if (prev === target) return;
-
-        target.classList.toggle(classes, true);
-      } else if (prev) {
-        prev.classList.toggle(classes, false);
-      }
-
-      prevSelectedEl[name] = target;
-    }
-    function handleDragOver({ target }) {
-      toggleHoverClass({
-        target,
-        name: 'connection',
-        classes: 'selected',
-        active: isConnectionEl(target),
-      });
-      toggleHoverClass({
-        target,
-        name: 'output',
-        classes: 'ring-4',
-        active: isOutputEl(target),
-      });
-
-      const nodeContent = target.closest(
-        '.drawflow-node:not(.blocks-group) .drawflow_content_node'
-      );
-      toggleHoverClass({
-        classes: 'ring-4',
-        target: nodeContent,
-        name: 'nodeContent',
-        active: nodeContent,
-      });
-    }
-    function getRelativePosToEditor(clientX, clientY) {
-      const { x, y } = editor.value.precanvas.getBoundingClientRect();
-      const { clientWidth, clientHeight } = editor.value.precanvas;
-      const { zoom } = editor.value;
-
-      const xPosition =
-        clientX * (clientWidth / (clientWidth * zoom)) -
-        x * (clientWidth / (clientWidth * zoom));
-      const yPosition =
-        clientY * (clientHeight / (clientHeight * zoom)) -
-        y * (clientHeight / (clientHeight * zoom));
-
-      return { xPosition, yPosition };
-    }
-    function dropHandler({ dataTransfer, clientX, clientY, target }) {
-      const block = JSON.parse(dataTransfer.getData('block') || null);
-
-      if (!block) return;
-
-      const highlightedEls = document.querySelectorAll(
-        '.drawflow_content_node.ring-4'
-      );
-      highlightedEls.forEach((el) => {
-        el.classList.remove('ring-4');
-      });
-
-      const isTriggerExists =
-        block.id === 'trigger' &&
-        editor.value.getNodesFromName('trigger').length !== 0;
-      if (isTriggerExists) return;
-
-      if (target.closest('.drawflow_content_node')) {
-        const targetNodeId = target
-          .closest('.drawflow-node')
-          .id.replace(/node-/, '');
-        const targetNode = editor.value.getNodeFromId(targetNodeId);
-        editor.value.removeNodeId(`node-${targetNodeId}`);
-
-        if (targetNode.name === 'blocks-group') return;
-
-        let targetBlock = block;
-        if (block.fromBlockBasic) {
-          targetBlock = { ...tasks[block.id], id: block.id };
-        }
-
-        const onErrorEnabled =
-          targetNode.data?.onError?.enable &&
-          !excludeOnError.includes(targetBlock.id);
-        const newNodeData = onErrorEnabled
-          ? { ...targetBlock.data, onError: targetNode.data.onError }
-          : targetBlock.data;
-
-        const newNodeId = editor.value.addNode(
-          targetBlock.id,
-          targetBlock.inputs,
-          targetBlock.outputs,
-          targetNode.pos_x,
-          targetNode.pos_y,
-          targetBlock.id,
-          newNodeData,
-          targetBlock.component,
-          'vue'
-        );
-
-        if (onErrorEnabled && targetNode.data.onError.toDo === 'fallback') {
-          editor.value.addNodeOutput(newNodeId);
-        }
-
-        const duplicateConnections = (nodeIO, type) => {
-          if (block[type] === 0) return;
-
-          Object.keys(nodeIO).forEach((name) => {
-            const { connections } = nodeIO[name];
-
-            connections.forEach(({ node, input, output }) => {
-              if (node === targetNodeId) return;
-
-              if (type === 'inputs') {
-                editor.value.addConnection(node, newNodeId, input, name);
-              } else if (type === 'outputs') {
-                editor.value.addConnection(newNodeId, node, name, output);
-              }
-            });
-          });
-        };
-
-        duplicateConnections(targetNode.inputs, 'inputs');
-        duplicateConnections(targetNode.outputs, 'outputs');
-
-        emitter.emit('editor:data-changed');
-
-        return;
-      }
-
-      if (block.fromBlockBasic) return;
-
-      const { xPosition, yPosition } = getRelativePosToEditor(clientX, clientY);
-
-      const blockId = editor.value.addNode(
-        block.id,
-        block.inputs,
-        block.outputs,
-        xPosition + 25,
-        yPosition - 25,
-        block.id,
-        block.data,
-        block.component,
-        'vue'
-      );
-
-      if (block.fromGroup) {
-        const blockEl = document.getElementById(`node-${blockId}`);
-
-        blockEl.setAttribute('group-item-id', block.itemId);
-      }
-
-      if (isConnectionEl(target)) {
-        target.classList.remove('selected');
-
-        const classes = target.parentElement.classList.toString();
-        const result = {};
-        const items = [
-          { str: 'node_in_', key: 'inputId' },
-          { str: 'input_', key: 'inputClass' },
-          { str: 'node_out_', key: 'outputId' },
-          { str: 'output_', key: 'outputClass' },
-        ];
-
-        items.forEach(({ key, str }) => {
-          result[key] = classes
-            .match(new RegExp(`${str}[^\\s]*`))[0]
-            ?.replace(/node_in_node-|node_out_node-/, '');
-        });
-
-        try {
-          editor.value.removeSingleConnection(
-            result.outputId,
-            result.inputId,
-            result.outputClass,
-            result.inputClass
-          );
-          editor.value.addConnection(
-            result.outputId,
-            blockId,
-            result.outputClass,
-            'input_1'
-          );
-          editor.value.addConnection(
-            blockId,
-            result.inputId,
-            'output_1',
-            result.inputClass
-          );
-        } catch (error) {
-          console.error(error);
-        }
-      } else if (isOutputEl(target)) {
-        prevSelectedEl.output?.classList.remove('ring-4');
-
-        const targetNodeId = target
-          .closest('.drawflow-node')
-          .id.replace(/node-/, '');
-        const outputClass = target.classList[1];
-
-        editor.value.addConnection(
-          targetNodeId,
-          blockId,
-          outputClass,
-          'input_1'
-        );
-      }
-
-      emitter.emit('editor:data-changed');
-    }
-    function isInputAllowed(allowedInputs, input) {
-      if (typeof allowedInputs === 'boolean') return allowedInputs;
-
-      return allowedInputs.some((item) => {
-        if (item.startsWith('#')) {
-          return tasks[input].category === item.substr(1);
-        }
-
-        return item === input;
-      });
-    }
-    function deleteBlock() {
-      editor.value.removeNodeId(contextMenu.data);
-    }
-    function clearSelectedElements() {
-      selection.value.clearSelection();
-      selectedElements.forEach(({ el }) => {
-        if (!el) return;
-
-        el.classList.remove('selected-list');
-      });
-      selectedElements = [];
-      activeNode = null;
-    }
-    function duplicateBlock(nodeId, isPaste = false) {
-      let initialPos = null;
-      const nodes = new Map();
-
-      const addNode = (id) => {
-        const node = editor.value.getNodeFromId(id);
-
-        if (node.name === 'trigger') return;
-
-        nodes.set(node.id, node);
-      };
-
-      if (isPaste) {
-        store.state.copiedNodes.forEach((node) => {
-          nodes.set(node.id, node);
-        });
-
-        const pos = contextMenu?.position?.getReferenceClientRect?.() ?? null;
-        if (pos) {
-          const { xPosition, yPosition } = getRelativePosToEditor(
-            pos.left,
-            pos.top
-          );
-          initialPos = { x: xPosition, y: yPosition };
-        }
-      } else {
-        if (nodeId) addNode(nodeId);
-        else if (activeNode) addNode(activeNode.id);
-
-        selectedElements.forEach((node) => {
-          if (activeNode?.id === node.id || nodeId === node.id) return;
-
-          addNode(node.id);
-        });
-      }
-
-      clearSelectedElements();
-
-      const nodesOutputs = [];
-      let firstNodePos = null;
-      let index = 0;
-
-      nodes.forEach((node) => {
-        const { outputs, inputs } = tasks[node.name];
-
-        const inputsLen = Object.keys(node.inputs).length;
-        const outputsLen = Object.keys(node.outputs).length;
-
-        const blockInputs = inputsLen || inputs;
-        const blockOutputs = outputsLen || outputs;
-
-        let nodePosX = node.pos_x;
-        let nodePosY = node.pos_y;
-
-        if (initialPos && index === 0) {
-          firstNodePos = { x: nodePosX, y: nodePosY };
-
-          nodePosX = initialPos.x;
-          nodePosY = initialPos.y;
-        } else if (firstNodePos) {
-          const xDistance = nodePosX - firstNodePos.x;
-          const yDistance = nodePosY - firstNodePos.y;
-
-          nodePosX = initialPos.x + xDistance;
-          nodePosY = initialPos.y + yDistance;
-        }
-
-        const newNodeId = editor.value.addNode(
-          node.name,
-          blockInputs,
-          blockOutputs,
-          nodePosX + 25,
-          nodePosY + 70,
-          node.name,
-          node.data,
-          node.html,
-          'vue'
-        );
-
-        nodes.set(node.id, { ...nodes.get(node.id), newId: newNodeId });
-
-        const nodeElement = document.querySelector(`#node-${newNodeId}`);
-        nodeElement.classList.add('selected-list');
-        selectedElements.push({
-          id: newNodeId,
-          el: nodeElement,
-          posY: parseInt(nodeElement.style.top, 10),
-          posX: parseInt(nodeElement.style.left, 10),
-        });
-
-        emitter.emit('editor:data-changed');
-
-        if (outputsLen > 0) {
-          nodesOutputs.push({ id: newNodeId, outputs: node.outputs });
-        }
-
-        index += 1;
-      });
-
-      if (nodesOutputs.length < 1) return;
-
-      nodesOutputs.forEach(({ id, outputs }) => {
-        Object.keys(outputs).forEach((key) => {
-          outputs[key].connections.forEach((connection) => {
-            const node = nodes.get(connection.node);
-
-            if (!node) return;
-
-            editor.value.addConnection(id, node.newId, key, 'input_1');
-          });
-        });
-      });
-    }
-    function checkWorkflowData() {
-      if (!editor.value) return;
-
-      editor.value.editor_mode = props.isShared ? 'fixed' : 'edit';
-      editor.value.container.classList.toggle('is-shared', props.isShared);
-    }
-    function saveEditorState() {
-      const editorStates =
-        parseJSON(localStorage.getItem('editor-states'), {}) || {};
-      editorStates[workflowId] = {
-        zoom: editor.value.zoom,
-        canvas_x: editor.value.canvas_x,
-        canvas_y: editor.value.canvas_y,
-      };
-
-      localStorage.setItem('editor-states', JSON.stringify(editorStates));
-    }
-    function initSelectArea() {
-      selection.value = new SelectionArea({
-        container: '#drawflow',
-        startareas: ['#drawflow'],
-        boundaries: ['#drawflow'],
-        selectables: ['.drawflow-node'],
-        features: {
-          singleTap: {
-            allow: false,
-          },
-        },
-      });
-
-      selection.value.on('beforestart', ({ event }) => {
-        if (!event.ctrlKey && !event.metaKey) return false;
-
-        editor.value.editor_mode = 'fixed';
-        editor.value.editor_selected = false;
-
-        return true;
-      });
-      selection.value.on('move', () => {
-        hasDragged = true;
-      });
-      selection.value.on('stop', (event) => {
-        event.store.selected.forEach((el) => {
-          const isExists = selectedElements.some((item) =>
-            item.el.isEqualNode(el)
-          );
-
-          if (isExists) return;
-
-          el.classList.toggle('selected-list', true);
-
-          selectedElements.push({
-            el,
-            id: el.id.slice(5),
-            posY: parseInt(el.style.top, 10),
-            posX: parseInt(el.style.left, 10),
-          });
-        });
-
-        setTimeout(() => {
-          hasDragged = false;
-        }, 500);
-      });
-    }
-    function onMouseup({ target }) {
-      editor.value.editor_mode = 'edit';
-
-      const isNodeEl = target.closest('.drawflow-node');
-      if (!isNodeEl) return;
-
-      const getPosition = (el) => {
-        return {
-          posY: parseInt(el.style.top, 10),
-          posX: parseInt(el.style.left, 10),
-        };
-      };
-
-      selectedElements.forEach(({ el }, index) => {
-        Object.assign(selectedElements[index], getPosition(el));
-      });
-
-      if (activeNode) Object.assign(activeNode, getPosition(activeNode.el));
-
-      isDragging = false;
-    }
-    function onMousedown({ target }) {
-      const nodeEl = target.closest('.drawflow-node');
-      if (!nodeEl) return;
-
-      if (nodeEl.classList.contains('selected-list')) {
-        activeNode = {
-          el: nodeEl,
-          id: nodeEl.id.slice(5),
-          posY: parseInt(nodeEl.style.top, 10),
-          posX: parseInt(nodeEl.style.left, 10),
-        };
-      }
-
-      isDragging = true;
-    }
-    function onClick({ ctrlKey, metaKey, target }) {
-      const nodeEl = target.closest('.drawflow-node');
-      if (!nodeEl) {
-        if (!hasDragged) clearSelectedElements();
-        return;
-      }
-
-      const nodeProperties = {
-        el: nodeEl,
-        id: nodeEl.id.slice(5),
-        posY: parseInt(nodeEl.style.top, 10),
-        posX: parseInt(nodeEl.style.left, 10),
-      };
-
-      if (!ctrlKey && !metaKey && !hasDragged) {
-        clearSelectedElements();
-
-        activeNode = nodeProperties;
-        nodeEl.classList.add('selected-list');
-        selectedElements = [nodeProperties];
-        hasDragged = false;
-
-        return;
-      }
-      hasDragged = false;
-
-      if (!ctrlKey && !metaKey) return;
-
-      const nodeIndex = selectedElements.findIndex(({ el }) =>
-        nodeEl.isEqualNode(el)
-      );
-      if (nodeIndex !== -1) {
-        setTimeout(() => {
-          nodeEl.classList.remove('selected-list', 'selected');
-        }, 400);
-        selectedElements.splice(nodeIndex, 1);
-      } else {
-        nodeEl.classList.add('selected-list');
-        selectedElements.push(nodeProperties);
-      }
-    }
-    function clearContextMenu() {
-      Object.assign(contextMenu, {
-        items: [],
-        data: null,
-        show: false,
-        position: {},
-      });
-    }
-    function copyBlocks() {
-      let nodes = selectedElements;
-
-      if (nodes.length === 0) {
-        const selectedEl = document.querySelector('.drawflow-node.selected');
-
-        if (selectedEl) {
-          nodes.push({ id: selectedEl.id.substr(5) });
-        }
-      }
-
-      nodes = nodes.map((node) => editor.value.getNodeFromId(node.id));
-
-      store.commit('updateState', {
-        key: 'copiedNodes',
-        value: nodes,
-      });
-    }
-    function onKeyup({ key, target, ctrlKey, metaKey }) {
-      if (ctrlKey || metaKey) {
-        if (key === 'c') {
-          copyBlocks();
-        } else if (key === 'v') {
-          duplicateBlock(null, true);
-        }
-      }
-
-      const isAnInput =
-        ['INPUT', 'TEXTAREA'].includes(target.tagName) ||
-        target.isContentEditable;
-
-      if (key !== 'Delete' || isAnInput) return;
-
-      selectedElements.forEach(({ id }) => {
-        const nodeId = `node-${id}`;
-        const isNodeExists = document.querySelector(`#${nodeId}`);
-
-        if (!isNodeExists) return;
-
-        editor.value.removeNodeId(nodeId);
-      });
-
-      selectedElements = [];
-      activeNode = null;
-    }
-
-    useShortcut('editor:duplicate-block', () => {
-      if (!activeNode && selectedElements.length <= 0) return;
-
-      duplicateBlock();
-    });
-
-    watch(() => props.isShared, checkWorkflowData);
-
-    onMounted(() => {
-      const context = getCurrentInstance().appContext.app._context;
-      const element = document.querySelector('#drawflow');
-
-      element.addEventListener('mousedown', onMousedown);
-      element.addEventListener('mouseup', onMouseup);
-      element.addEventListener('click', onClick);
-      element.addEventListener('keyup', onKeyup);
-
-      editor.value = drawflow(element, {
-        context,
-        options: {
-          reroute: true,
-          ...store.state.settings.editor,
-        },
-      });
-      editor.value.start();
-
-      emit('load', editor.value);
-
-      if (props.data) {
-        let data =
-          typeof props.data === 'string'
-            ? parseJSON(props.data, null)
-            : props.data;
-
-        if (!data || !data?.drawflow?.Home) return;
-
-        const currentExtVersion = browser.runtime.getManifest().version;
-        const isOldWorkflow = compare(
-          currentExtVersion,
-          props.version || '0.0.0',
-          '>'
-        );
-
-        if (isOldWorkflow && typeof props.version !== 'boolean') {
-          const newDrawflowData = Object.entries(
-            data.drawflow.Home.data
-          ).reduce((obj, [key, value]) => {
-            obj[key] = {
-              ...value,
-              html: tasks[value.name].component,
-              data: defu({}, value.data, tasks[value.name].data),
-            };
-
-            return obj;
-          }, {});
-
-          data = {
-            drawflow: { Home: { data: newDrawflowData } },
-          };
-
-          emit('update', { version: currentExtVersion });
-        }
-
-        editor.value.import(data);
-
-        if (isOldWorkflow) {
-          setTimeout(() => {
-            emit('save');
-          }, 200);
-        }
-      } else if (!props.isShared) {
-        editor.value.addNode(
-          'trigger',
-          0,
-          1,
-          50,
-          300,
-          'trigger',
-          tasks.trigger.data,
-          'BlockBasic',
-          'vue'
-        );
-      }
-
-      editor.value.on('mouseMove', () => {
-        if (!activeNode || !isDragging) return;
-
-        const xDistance =
-          parseInt(activeNode.el.style.left, 10) - activeNode.posX;
-        const yDistance =
-          parseInt(activeNode.el.style.top, 10) - activeNode.posY;
-
-        selectedElements.forEach(({ el, posX, posY }) => {
-          if (el.isEqualNode(activeNode.el)) return;
-
-          const nodeId = el.id.slice(5);
-          const node = editor.value.drawflow.drawflow.Home.data[nodeId];
-
-          const newPosX = posX + xDistance;
-          const newPosY = posY + yDistance;
-
-          node.pos_x = newPosX;
-          node.pos_y = newPosY;
-          el.style.top = `${newPosY}px`;
-          el.style.left = `${newPosX}px`;
-
-          editor.value.updateConnectionNodes(el.id);
-        });
-
-        hasDragged = true;
-      });
-      editor.value.on('nodeRemoved', (id) => {
-        emit('deleteBlock', id);
-      });
-      editor.value.on(
-        'connectionCreated',
-        ({ output_id, input_id, output_class, input_class }) => {
-          const { name: inputName } = editor.value.getNodeFromId(input_id);
-          const { allowedInputs } = tasks[inputName];
-          const isAllowed = isInputAllowed(allowedInputs, inputName);
-
-          if (!isAllowed) {
-            editor.value.removeSingleConnection(
-              output_id,
-              input_id,
-              output_class,
-              input_class
-            );
-          }
-
-          emitter.emit('editor:data-changed');
-        }
-      );
-      editor.value.on('connectionRemoved', () => {
-        emitter.emit('editor:data-changed');
-      });
-      editor.value.on('export', saveEditorState);
-      editor.value.on('contextmenu', ({ clientY, clientX, target }) => {
-        if (target.tagName === 'path' && target.classList.contains('main-path'))
-          return;
-
-        const isBlock = target.closest('.drawflow .drawflow-node');
-        const virtualEl = {
-          getReferenceClientRect: () => ({
-            width: 0,
-            height: 0,
-            top: clientY,
-            right: clientX,
-            bottom: clientY,
-            left: clientX,
-          }),
-        };
-
-        if (isBlock) {
-          contextMenu.data = isBlock.id;
-          contextMenu.position = virtualEl;
-          contextMenu.items = contextMenuItems.block;
-          contextMenu.show = true;
-        }
-
-        const copiedNodesLen = store.state.copiedNodes.length;
-        if (copiedNodesLen > 0) {
-          if (isBlock) {
-            contextMenuItems.common.forEach((item) => {
-              const isExists = contextMenu.items.some(
-                (menu) => menu.id === item.id
-              );
-              if (isExists) return;
-
-              contextMenu.items.unshift(item);
-            });
-          } else {
-            contextMenu.items = contextMenuItems.common;
-          }
-
-          contextMenu.position = virtualEl;
-          contextMenu.show = true;
-        }
-      });
-
-      const editorStates =
-        parseJSON(localStorage.getItem('editor-states'), {}) || {};
-      const editorState = editorStates[workflowId];
-      if (editorState) {
-        const { canvas_x, canvas_y, zoom } = editorState;
-        editor.value.translate_to(canvas_x, canvas_y, zoom);
-      }
-
-      checkWorkflowData();
-      initSelectArea();
-      emit('loaded', editor.value);
-    });
-    onBeforeUnmount(() => {
-      const element = document.querySelector('#drawflow');
-
-      if (element) {
-        element.removeEventListener('mousedown', onMousedown);
-        element.removeEventListener('mouseup', onMouseup);
-        element.removeEventListener('click', onClick);
-        element.removeEventListener('keyup', onKeyup);
-      }
-
-      saveEditorState();
-    });
-
-    return {
-      t,
-      editor,
-      contextMenu,
-      dropHandler,
-      handleDragOver,
-      clearContextMenu,
-      contextMenuHandler: {
-        copyBlocks,
-        deleteBlock,
-        pasteBlocks: () => duplicateBlock(null, true),
-        duplicateBlock: () => duplicateBlock(contextMenu.data.substr(5)),
-      },
-    };
-  },
-};
-</script>
-<style>
-#drawflow {
-  background-image: url('@/assets/images/tile.png');
-  background-size: 35px;
-  user-select: none;
-}
-.dark #drawflow {
-  background-image: url('@/assets/images/tile-white.png');
-}
-.drawflow .drawflow-node {
-  @apply dark:bg-gray-800;
-}
-#drawflow[arrow='true'] .drawflow-node .input {
-  background-color: transparent !important;
-  border-top: 10px solid transparent;
-  border-radius: 0;
-  border-left: 10px solid theme('colors.accent');
-  border-right: 10px solid transparent;
-  border-bottom: 10px solid transparent;
-}
-.selection-area {
-  background: rgba(46, 115, 252, 0.11);
-  border: 2px solid rgba(98, 155, 255, 0.81);
-  border-radius: 0.1em;
-}
-.drawflow_content_node {
-  @apply rounded-lg;
-}
-</style>

+ 36 - 8
src/components/newtab/workflow/WorkflowEditor.vue

@@ -1,5 +1,5 @@
 <template>
-  <vue-flow :id="props.id">
+  <vue-flow :id="props.id" :class="{ disabled: options.disabled }">
     <Background />
     <MiniMap :node-class-name="minimapNodeClassName" />
     <div class="flex items-end absolute p-4 left-0 bottom-0 z-10">
@@ -41,7 +41,7 @@
   </vue-flow>
 </template>
 <script setup>
-import { onMounted } from 'vue';
+import { onMounted, onBeforeUnmount } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { VueFlow, Background, MiniMap, useVueFlow } from '@braks/vue-flow';
 import cloneDeep from 'lodash.clonedeep';
@@ -63,6 +63,10 @@ const props = defineProps({
       edges: [],
     }),
   },
+  options: {
+    type: Object,
+    default: () => ({}),
+  },
 });
 const emit = defineEmits(['edit', 'init', 'update:node', 'delete:node']);
 
@@ -74,15 +78,18 @@ const nodeTypes = blockComponents.keys().reduce((acc, key) => {
 
   return acc;
 }, {});
+const getPosition = (position) => (Array.isArray(position) ? position : [0, 0]);
 
 const { t } = useI18n();
 const editor = useVueFlow({
   id: props.id,
   minZoom: 0.4,
-  defaultZoom: 0,
   deleteKeyCode: 'Delete',
+  elevateEdgesOnSelect: true,
+  defaultZoom: props.data?.zoom ?? 0.7,
   multiSelectionKeyCode: isMac ? 'Meta' : 'Control',
-  ...props.data,
+  defaultPosition: getPosition(props.data?.position),
+  ...props.options,
 });
 editor.onConnect((params) => {
   params.class = `source-${params.sourceHandle} target-${params.targetHandle}`;
@@ -95,12 +102,17 @@ function minimapNodeClassName({ label }) {
 
   return color;
 }
-function updateBlockData(nodeId, data) {
+function updateBlockData(nodeId, data = {}) {
+  if (props.options.disabled) return;
+
   const node = editor.getNode.value(nodeId);
-  Object.assign(node.data, data);
+  node.data = { ...node.data, ...data };
+
   emit('update:node', node);
 }
 function editBlock({ id, label, data }, additionalData = {}) {
+  if (props.options.disabled) return;
+
   emit('edit', {
     id: label,
     blockId: id,
@@ -109,19 +121,35 @@ function editBlock({ id, label, data }, additionalData = {}) {
   });
 }
 function deleteBlock(nodeId) {
+  if (props.options.disabled) return;
+
   editor.removeNodes([nodeId]);
   emit('delete:node', nodeId);
 }
-
-onMounted(() => {
+function onMousedown(event) {
+  if (props.options.disabled && event.shiftKey) {
+    event.stopPropagation();
+    event.preventDefault();
+  }
+}
+function applyFlowData() {
+  editor.setNodes(props.data.nodes || []);
+  editor.setEdges(props.data.edges || []);
   editor.setTransform({
     x: props.data.x || 0,
     y: props.data.y || 0,
     zoom: props.data.zoom || 1,
   });
+}
 
+onMounted(() => {
+  applyFlowData();
+  window.addEventListener('mousedown', onMousedown, true);
   emit('init', editor);
 });
+onBeforeUnmount(() => {
+  window.removeEventListener('mousedown', onMousedown, true);
+});
 </script>
 <style>
 @import '@braks/vue-flow/dist/style.css';

+ 4 - 4
src/components/newtab/workflow/WorkflowShare.vue

@@ -177,7 +177,7 @@ import { useI18n } from 'vue-i18n';
 import { useToast } from 'vue-toastification';
 import { fetchApi } from '@/utils/api';
 import { useUserStore } from '@/stores/user';
-import { useWorkflowStore } from '@/stores/workflow';
+import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import { workflowCategories } from '@/utils/shared';
 import { parseJSON, debounce } from '@/utils/helper';
 import { convertWorkflow } from '@/utils/workflowData';
@@ -195,7 +195,7 @@ const emit = defineEmits(['close', 'publish', 'change']);
 const { t } = useI18n();
 const toast = useToast();
 const userStore = useUserStore();
-const workflowStore = useWorkflowStore();
+const sharedWorkflowStore = useSharedWorkflowStore();
 
 const menuItems = [
   { id: 'bold', name: 'Bold', icon: 'riBold', action: 'toggleBold' },
@@ -241,10 +241,10 @@ async function publishWorkflow() {
 
     workflow.drawflow = props.workflow.drawflow;
 
-    workflowStore.shared[workflow.id] = workflow;
+    sharedWorkflowStore.insert(workflow);
     sessionStorage.setItem(
       'shared-workflows',
-      JSON.stringify(workflowStore.shared)
+      JSON.stringify(sharedWorkflowStore.shared)
     );
 
     state.isPublishing = false;

+ 5 - 5
src/components/newtab/workflow/edit/EditExecuteWorkflow.vue

@@ -58,7 +58,7 @@
 import { computed, shallowReactive, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useRoute } from 'vue-router';
-import Workflow from '@/models/workflow';
+import { useWorkflowStore } from '@/stores/workflow';
 
 const SharedCodemirror = defineAsyncComponent(() =>
   import('@/components/newtab/shared/SharedCodemirror.vue')
@@ -78,21 +78,21 @@ const emit = defineEmits(['update:data']);
 
 const { t } = useI18n();
 const route = useRoute();
+const workflowStore = useWorkflowStore();
 
 const state = shallowReactive({
   showGlobalData: false,
 });
 
 const workflows = computed(() =>
-  Workflow.query()
-    .where(({ id, drawflow }) => {
+  workflowStore.getWorkflows
+    .filter(({ id, drawflow }) => {
       const flow =
         typeof drawflow === 'string' ? drawflow : JSON.stringify(drawflow);
 
       return id !== route.params.id && !flow.includes(route.params.id);
     })
-    .orderBy('name', 'asc')
-    .get()
+    .sort((a, b) => (a.name > b.name ? 1 : -1))
 );
 
 function updateData(value) {

+ 35 - 29
src/components/newtab/workflow/editor/EditorLocalActions.vue

@@ -87,7 +87,7 @@
       v-else
       v-tooltip="t('workflow.clickToEnable')"
       class="p-2"
-      @click="$emit('update', { isDisabled: false })"
+      @click="updateWorkflow({ isDisabled: false })"
     >
       {{ t('common.disabled') }}
     </button>
@@ -102,7 +102,7 @@
       <ui-list class="w-36">
         <ui-list-item
           class="cursor-pointer"
-          @click="$emit('update', { isDisabled: !workflow.isDisabled })"
+          @click="updateWorkflow({ isDisabled: !workflow.isDisabled })"
         >
           <v-remixicon name="riToggleLine" class="mr-2 -ml-1" />
           {{ t(`common.${workflow.isDisabled ? 'enable' : 'disable'}`) }}
@@ -178,12 +178,13 @@ import { sendMessage } from '@/utils/message';
 import { fetchApi } from '@/utils/api';
 import { useUserStore } from '@/stores/user';
 import { useWorkflowStore } from '@/stores/workflow';
+import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import { useDialog } from '@/composable/dialog';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { useShortcut, getShortcut } from '@/composable/shortcut';
 import { parseJSON } from '@/utils/helper';
 import { exportWorkflow, convertWorkflow } from '@/utils/workflowData';
-import workflowTrigger from '@/utils/workflowTrigger';
+import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
 
 const props = defineProps({
   isDataChanged: {
@@ -199,7 +200,7 @@ const props = defineProps({
     default: () => ({}),
   },
 });
-const emit = defineEmits(['modal', 'save', 'update']);
+const emit = defineEmits(['modal', 'change', 'update']);
 
 useGroupTooltip();
 
@@ -209,6 +210,7 @@ const router = useRouter();
 const dialog = useDialog();
 const userStore = useUserStore();
 const workflowStore = useWorkflowStore();
+const sharedWorkflowStore = useSharedWorkflowStore();
 const shortcuts = useShortcut(
   [
     /* eslint-disable-next-line */
@@ -229,13 +231,21 @@ const renameState = reactive({
   showModal: false,
 });
 
-const shared = computed(() =>
-  workflowStore.getById('shared', props.workflow.id)
-);
-const hosted = computed(() =>
-  workflowStore.getById('userHosted', props.workflow.id)
-);
+const shared = computed(() => sharedWorkflowStore.getById(props.workflow.id));
+const hosted = computed(() => userStore.hostedWorkflows[props.workflow.id]);
 
+function updateWorkflow(data = {}, changedIndicator = false) {
+  return workflowStore
+    .update({
+      data,
+      id: props.workflow.id,
+    })
+    .then((result) => {
+      emit('update', { data, changedIndicator });
+
+      return result;
+    });
+}
 function executeWorkflow() {
   sendMessage(
     'workflow:execute',
@@ -291,16 +301,17 @@ async function setAsHostWorkflow(isHost) {
     }
 
     if (isHost) {
-      workflowStore.userHosted[props.workflow.id] = result;
+      userStore.hostedWorkflows[props.workflow.id] = result;
     } else {
-      delete workflowStore.userHosted[props.workflow.id];
+      delete userStore.hostedWorkflows[props.workflow.id];
     }
 
+    // Update cache
     const userWorkflows = parseJSON('user-workflows', {
       backup: [],
       hosted: {},
     });
-    userWorkflows.hosted = workflowStore.userHosted;
+    userWorkflows.hosted = userStore.hostedWorkflows;
     sessionStorage.setItem('user-workflows', JSON.stringify(userWorkflows));
 
     state.isUploadingHost = false;
@@ -340,13 +351,9 @@ function initRenameWorkflow() {
   });
 }
 function renameWorkflow() {
-  workflowStore.updateWorkflow({
-    location: 'local',
-    id: props.workflow.id,
-    data: {
-      name: renameState.name,
-      description: renameState.description,
-    },
+  updateWorkflow({
+    name: renameState.name,
+    description: renameState.description,
   });
   clearRenameModal();
 }
@@ -356,7 +363,7 @@ function deleteWorkflow() {
     okVariant: 'danger',
     body: t('message.delete', { name: props.workflow.name }),
     onConfirm: async () => {
-      await workflowStore.deleteWorkflow(props.workflow.id, 'local');
+      await workflowStore.delete(props.workflow.id);
       router.replace('/');
     },
   });
@@ -377,17 +384,16 @@ async function saveWorkflow() {
       return;
     }
 
-    workflowStore.updateWorkflow({
-      location: 'local',
-      id: props.workflow.id,
-      data: {
+    await updateWorkflow(
+      {
         drawflow: flow,
-        trigger: triggerBlock,
+        trigger: triggerBlock.data,
       },
-    });
+      false
+    );
+    await registerWorkflowTrigger(props.workflow.id, triggerBlock);
 
-    workflowTrigger.register(props.workflow.id, triggerBlock);
-    emit('save');
+    emit('change', { drawflow: flow });
   } catch (error) {
     console.error(error);
   }

+ 19 - 20
src/components/newtab/workflows/WorkflowsFolder.vue

@@ -81,8 +81,8 @@ import { computed } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useDialog } from '@/composable/dialog';
 import { parseJSON } from '@/utils/helper';
-import Folder from '@/models/folder';
-import Workflow from '@/models/workflow';
+import { useFolderStore } from '@/stores/folder';
+import { useWorkflowStore } from '@/stores/workflow';
 
 defineProps({
   modelValue: {
@@ -94,8 +94,10 @@ const emit = defineEmits(['update:modelValue']);
 
 const { t } = useI18n();
 const dialog = useDialog();
+const folderStore = useFolderStore();
+const workflowStore = useWorkflowStore();
 
-const folders = computed(() => Folder.query().orderBy('name', 'asc').get());
+const folders = computed(() => folderStore.items);
 
 function onDragover(event, toggle) {
   const parent = event.target.closest('.ui-list-item');
@@ -112,11 +114,7 @@ function newFolder() {
     onConfirm(value) {
       if (!value || !value.trim()) return false;
 
-      Folder.insert({
-        data: {
-          name: value,
-        },
-      });
+      folderStore.addFolder(value);
 
       return true;
     },
@@ -129,7 +127,7 @@ function deleteFolder({ name, id }) {
     okText: t('common.delete'),
     okVariant: 'danger',
     onConfirm() {
-      Folder.delete(id);
+      folderStore.deleteFolder(id);
 
       emit('update:modelValue', '');
     },
@@ -144,24 +142,25 @@ function renameFolder({ id, name }) {
     onConfirm(newName) {
       if (!newName || !newName.trim()) return false;
 
-      Folder.update({
-        where: id,
-        data: { name: newName },
-      });
+      folderStore.updateFolder(id, { name: newName });
 
       return true;
     },
   });
 }
-function onWorkflowsDrop({ dataTransfer }, folderId) {
+async function onWorkflowsDrop({ dataTransfer }, folderId) {
   const ids = parseJSON(dataTransfer.getData('workflows'), null);
   if (!ids || !Array.isArray(ids)) return;
 
-  ids.forEach((id) => {
-    Workflow.update({
-      where: id,
-      data: { folderId },
-    });
-  });
+  try {
+    for (const id of ids) {
+      await workflowStore.update({
+        id,
+        data: { folderId },
+      });
+    }
+  } catch (error) {
+    console.error(error);
+  }
 }
 </script>

+ 4 - 6
src/components/newtab/workflows/WorkflowsHosted.vue

@@ -12,10 +12,9 @@
 <script setup>
 import { computed } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { useWorkflowStore } from '@/stores/workflow';
 import { useDialog } from '@/composable/dialog';
 import { arraySorter } from '@/utils/helper';
-import { cleanWorkflowTriggers } from '@/utils/workflowTrigger';
+import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 
 const props = defineProps({
@@ -34,14 +33,14 @@ const props = defineProps({
 
 const { t } = useI18n();
 const dialog = useDialog();
-const workflowStore = useWorkflowStore();
+const hostedWorkflowStore = useHostedWorkflowStore();
 
 const menu = [
   { id: 'delete', name: t('common.delete'), icon: 'riDeleteBin7Line' },
 ];
 
 const workflows = computed(() => {
-  const filtered = Object.values(workflowStore.hosted).filter(({ name }) =>
+  const filtered = hostedWorkflowStore.toArray.filter(({ name }) =>
     name.toLocaleLowerCase().includes(props.search.toLocaleLowerCase())
   );
 
@@ -59,8 +58,7 @@ async function deleteWorkflow(workflow) {
     body: t('message.delete', { name: workflow.name }),
     onConfirm: async () => {
       try {
-        delete workflowStore.hosted[workflow.hostId];
-        await cleanWorkflowTriggers(workflow.hostId);
+        await hostedWorkflowStore.delete(workflow.hostId);
       } catch (error) {
         console.error(error);
       }

+ 23 - 20
src/components/newtab/workflows/WorkflowsLocal.vue

@@ -1,13 +1,13 @@
 <template>
-  <div v-if="workflowStore.local.length === 0" class="py-12 flex items-center">
+  <div
+    v-if="workflowStore.getWorkflows.length === 0"
+    class="py-12 flex items-center"
+  >
     <img src="@/assets/svg/alien.svg" class="w-96" />
     <div class="ml-4">
       <h1 class="text-2xl font-semibold max-w-md mb-6">
         {{ t('message.empty') }}
       </h1>
-      <ui-button variant="accent" @click="newWorkflow">
-        {{ t('workflow.new') }}
-      </ui-button>
     </div>
   </div>
   <template v-else>
@@ -82,10 +82,13 @@
         </template>
         <template #footer-content>
           <v-remixicon
-            v-if="workflowStore.shared[workflow.id]"
+            v-if="sharedWorkflowStore.workflows[workflow.id]"
             v-tooltip:bottom.group="
               t('workflow.share.sharedAs', {
-                name: workflowStore.shared[workflow.id]?.name.slice(0, 64),
+                name: sharedWorkflowStore.workflows[workflow.id]?.name.slice(
+                  0,
+                  64
+                ),
               })
             "
             name="riShareLine"
@@ -93,7 +96,7 @@
             class="ml-2"
           />
           <v-remixicon
-            v-if="workflowStore.hosted[workflow.id]"
+            v-if="userStore.hostedWorkflows[workflow.id]"
             v-tooltip:bottom.group="t('workflow.host.title')"
             name="riBaseStationLine"
             size="20"
@@ -159,10 +162,12 @@
 import { shallowReactive, computed, onMounted, onBeforeUnmount } from 'vue';
 import { useI18n } from 'vue-i18n';
 import SelectionArea from '@viselect/vanilla';
+import { arraySorter } from '@/utils/helper';
+import { useUserStore } from '@/stores/user';
 import { useDialog } from '@/composable/dialog';
 import { useWorkflowStore } from '@/stores/workflow';
-import { arraySorter } from '@/utils/helper';
 import { exportWorkflow } from '@/utils/workflowData';
+import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 
 const props = defineProps({
@@ -189,7 +194,9 @@ const props = defineProps({
 
 const { t } = useI18n();
 const dialog = useDialog();
+const userStore = useUserStore();
 const workflowStore = useWorkflowStore();
+const sharedWorkflowStore = useSharedWorkflowStore();
 
 const state = shallowReactive({
   selectedWorkflows: [],
@@ -237,10 +244,10 @@ selection
   });
 
 const filteredWorkflows = computed(() => {
-  const filtered = workflowStore.local.filter(
+  const filtered = workflowStore.getWorkflows.filter(
     ({ name, folderId }) =>
       name.toLocaleLowerCase().includes(props.search.toLocaleLowerCase()) &&
-      (!props.activeFolder || props.activeFolder === folderId)
+      (!props.folderId || props.folderId === folderId)
   );
 
   return arraySorter({
@@ -257,9 +264,8 @@ const workflows = computed(() =>
 );
 
 function toggleDisableWorkflow({ id, isDisabled }) {
-  workflowStore.updateWorkflow({
+  workflowStore.update({
     id,
-    location: 'local',
     data: {
       isDisabled: !isDisabled,
     },
@@ -282,8 +288,7 @@ function initRenameWorkflow({ name, description, id }) {
   });
 }
 function renameWorkflow() {
-  workflowStore.updateWorkflow({
-    location: 'local',
+  workflowStore.update({
     id: renameState.id,
     data: {
       name: renameState.name,
@@ -298,7 +303,7 @@ function deleteWorkflow({ name, id }) {
     okVariant: 'danger',
     body: t('message.delete', { name }),
     onConfirm: () => {
-      workflowStore.deleteWorkflow(id, 'local');
+      workflowStore.delete(id);
     },
   });
 }
@@ -313,7 +318,7 @@ function deleteSelectedWorkflows({ target, key }) {
 
   if (state.selectedWorkflows.length === 1) {
     const [workflowId] = state.selectedWorkflows;
-    const workflow = workflowStore.getById('local', workflowId);
+    const workflow = workflowStore.getById(workflowId);
     deleteWorkflow(workflow);
   } else {
     dialog.confirm({
@@ -323,9 +328,7 @@ function deleteSelectedWorkflows({ target, key }) {
         name: `${state.selectedWorkflows.length} workflows`,
       }),
       onConfirm: async () => {
-        for (const workflowId of state.selectedWorkflows) {
-          await workflowStore.deleteWorkflow(workflowId, 'local');
-        }
+        await workflowStore.delete(state.selectedWorkflows);
       },
     });
   }
@@ -338,7 +341,7 @@ function duplicateWorkflow(workflow) {
     delete copyWorkflow[key];
   });
 
-  workflowStore.addWorkflow(copyWorkflow);
+  workflowStore.insert(copyWorkflow);
 }
 function onDragStart({ dataTransfer, target }) {
   const payload = [...state.selectedWorkflows];

+ 3 - 3
src/components/newtab/workflows/WorkflowsShared.vue

@@ -10,7 +10,7 @@
 </template>
 <script setup>
 import { computed } from 'vue';
-import { useWorkflowStore } from '@/stores/workflow';
+import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import { arraySorter } from '@/utils/helper';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 
@@ -28,10 +28,10 @@ const props = defineProps({
   },
 });
 
-const workflowStore = useWorkflowStore();
+const sharedWorkflowStore = useSharedWorkflowStore();
 
 const workflows = computed(() => {
-  const filtered = Object.values(workflowStore.shared).filter(({ name }) =>
+  const filtered = sharedWorkflowStore.toArray.filter(({ name }) =>
     name.toLocaleLowerCase().includes(props.search.toLocaleLowerCase())
   );
 

+ 9 - 6
src/components/popup/home/HomeStartRecording.vue

@@ -78,7 +78,7 @@
 <script setup>
 import { reactive, computed } from 'vue';
 import { useI18n } from 'vue-i18n';
-import Workflow from '@/models/workflow';
+import { useWorkflowStore } from '@/stores/workflow';
 import HomeSelectBlock from './HomeSelectBlock.vue';
 
 const emit = defineEmits(['update', 'close', 'record']);
@@ -86,7 +86,9 @@ const emit = defineEmits(['update', 'close', 'record']);
 emit('update', 'new');
 
 const tabs = ['new', 'existing'];
+
 const { t } = useI18n();
+const workflowStore = useWorkflowStore();
 
 const state = reactive({
   query: '',
@@ -95,13 +97,14 @@ const state = reactive({
   activeWorkflow: '',
 });
 
-const activeWorkflow = computed(() => Workflow.find(state.activeWorkflow));
+const activeWorkflow = computed(() =>
+  workflowStore.getById(state.activeWorkflow)
+);
 const workflows = computed(() =>
-  Workflow.query()
-    .where(({ name }) =>
+  workflowStore.getWorkflows
+    .filter(({ name }) =>
       name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())
     )
-    .orderBy('createdAt', 'desc')
-    .get()
+    .sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1))
 );
 </script>

+ 2 - 10
src/lib/pinia.js

@@ -7,18 +7,10 @@ function saveToStoragePlugin({ store, options }) {
 
   store.saveToStorage = (key) => {
     const storageKey = options.storageMap[key];
-    if (!storageKey) return;
+    if (!storageKey || !store.retrieved) return null;
 
-    newBrowser.storage.local.set({ [storageKey]: toRaw(store[key]) });
+    return newBrowser.storage.local.set({ [storageKey]: toRaw(store[key]) });
   };
-  store.$subscribe(({ events }, state) => {
-    if (!state.retrieved || !options.storageMap) return;
-
-    const storageKey = options.storageMap[events.key];
-    if (!storageKey) return;
-
-    console.log(storageKey, events.newValue);
-  });
 }
 
 const pinia = createPinia();

+ 0 - 20
src/lib/vuexOrm.js

@@ -1,20 +0,0 @@
-import VuexORM, { Query } from '@vuex-orm/core';
-
-function callback(model, param, entity) {
-  if (this.baseModel.autoSave) {
-    this.store.dispatch('saveToStorage', entity);
-  }
-}
-
-Query.on('afterUpdate', callback);
-Query.on('afterDelete', callback);
-
-const database = new VuexORM.Database();
-
-export default function (models) {
-  Object.values(models).forEach((model) => {
-    database.register(model);
-  });
-
-  return VuexORM.install(database);
-}

+ 0 - 33
src/models/collection.js

@@ -1,33 +0,0 @@
-import { Model } from '@vuex-orm/core';
-import { nanoid } from 'nanoid';
-
-class Collection extends Model {
-  static entity = 'collections';
-
-  static primaryKey = 'id';
-
-  static autoSave = true;
-
-  static fields() {
-    return {
-      id: this.uid(() => nanoid()),
-      flow: this.attr([]),
-      name: this.string(''),
-      createdAt: this.number(),
-      globalData: this.string(''),
-      options: this.attr({
-        atOnce: false,
-      }),
-    };
-  }
-
-  static async insert(payload) {
-    const res = await super.insert(payload);
-
-    await this.store().dispatch('saveToStorage', 'collections');
-
-    return res;
-  }
-}
-
-export default Collection;

+ 0 - 30
src/models/folder.js

@@ -1,30 +0,0 @@
-import { Model } from '@vuex-orm/core';
-import { nanoid } from 'nanoid';
-import Workflow from './workflow';
-
-class Folder extends Model {
-  static entity = 'folders';
-
-  static primaryKey = 'id';
-
-  static autoSave = true;
-
-  static fields() {
-    return {
-      id: this.uid(() => nanoid()),
-      name: this.string(''),
-      createdAt: this.number(),
-      workflows: this.hasMany(Workflow, 'folderId'),
-    };
-  }
-
-  static async insert(payload) {
-    const res = await super.insert(payload);
-
-    await this.store().dispatch('saveToStorage', 'folders');
-
-    return res;
-  }
-}
-
-export default Folder;

+ 0 - 4
src/models/index.js

@@ -1,4 +0,0 @@
-export { default as Workflow } from './workflow';
-export { default as Collection } from './collection';
-export { default as Log } from './log';
-export { default as Folder } from './folder';

+ 0 - 38
src/models/log.js

@@ -1,38 +0,0 @@
-import { Model } from '@vuex-orm/core';
-import { nanoid } from 'nanoid';
-
-class Log extends Model {
-  static entity = 'logs';
-
-  static fields() {
-    return {
-      id: this.uid(() => nanoid()),
-      data: this.attr({}),
-      name: this.string(''),
-      history: this.attr([]),
-      endedAt: this.number(0),
-      message: this.string(''),
-      startedAt: this.number(0),
-      parentLog: this.attr(null),
-      workflowId: this.attr(null),
-      collectionId: this.attr(null),
-      status: this.string('success'),
-      isChildLog: this.boolean(false),
-      collectionLogId: this.attr(null),
-      icon: this.string('riGlobalLine'),
-      isInCollection: this.boolean(false),
-    };
-  }
-
-  static afterDelete(item) {
-    const logs = this.query().where('collectionLogId', item.id).get();
-
-    if (logs.length !== 0) {
-      Promise.allSettled(logs.map(({ id }) => this.delete(id))).then(() => {
-        this.store().dispatch('saveToStorage', 'workflows');
-      });
-    }
-  }
-}
-
-export default Log;

+ 0 - 114
src/models/workflow.js

@@ -1,114 +0,0 @@
-import { Model } from '@vuex-orm/core';
-import { nanoid } from 'nanoid';
-import browser from 'webextension-polyfill';
-import { cleanWorkflowTriggers } from '@/utils/workflowTrigger';
-import { fetchApi } from '@/utils/api';
-import decryptFlow, { getWorkflowPass } from '@/utils/decryptFlow';
-import Log from './log';
-
-class Workflow extends Model {
-  static entity = 'workflows';
-
-  static primaryKey = 'id';
-
-  static autoSave = true;
-
-  static fields() {
-    return {
-      __id: this.attr(null),
-      id: this.uid(() => nanoid()),
-      name: this.string(''),
-      icon: this.string('riGlobalLine'),
-      data: this.attr(null),
-      folderId: this.attr(null),
-      drawflow: this.attr(''),
-      table: this.attr([]),
-      dataColumns: this.attr([]),
-      description: this.string(''),
-      pass: this.string(''),
-      trigger: this.attr(null),
-      version: this.string(''),
-      createdAt: this.number(Date.now()),
-      isDisabled: this.boolean(false),
-      isProtected: this.boolean(false),
-      settings: this.attr({
-        publicId: '',
-        blockDelay: 0,
-        saveLog: true,
-        debugMode: false,
-        restartTimes: 3,
-        notification: true,
-        reuseLastState: false,
-        inputAutocomplete: true,
-        onError: 'stop-workflow',
-        executedBlockOnWeb: false,
-        insertDefaultColumn: true,
-        defaultColumnName: 'column',
-      }),
-      logs: this.hasMany(Log, 'workflowId'),
-      globalData: this.string('{\n\t"key": "value"\n}'),
-    };
-  }
-
-  static beforeCreate(model) {
-    if (model.dataColumns?.length > 0) {
-      model.table = model.dataColumns;
-      model.dataColumns = [];
-    }
-    if (model.isProtected) {
-      const pass = getWorkflowPass(model.pass);
-
-      model.drawflow = decryptFlow(model, pass);
-      model.isProtected = false;
-    }
-    if (model.table && !model.table[0]?.id) {
-      model.table = model.table.map((column) => {
-        if (!column.id) column.id = column.name;
-
-        return column;
-      });
-    }
-
-    return model;
-  }
-
-  static async insert(payload) {
-    const res = await super.insert(payload);
-
-    await this.store().dispatch('saveToStorage', 'workflows');
-
-    return res;
-  }
-
-  static async afterDelete({ id }) {
-    try {
-      await cleanWorkflowTriggers(id);
-      const hostedWorkflow = this.store().state.hostWorkflows[id];
-      const { backupIds } = await browser.storage.local.get('backupIds');
-      const isBackup = (backupIds || []).includes(id);
-
-      if (hostedWorkflow || isBackup) {
-        const response = await fetchApi(`/me/workflows?id=${id}`, {
-          method: 'DELETE',
-        });
-
-        if (!response.ok) {
-          throw new Error(response.statusText);
-        }
-
-        if (isBackup) {
-          backupIds.splice(backupIds.indexOf(id), 1);
-          await browser.storage.local.set({ backupIds });
-        }
-
-        await browser.storage.local.set({ clearCache: true });
-      }
-
-      browser.storage.local.remove(`state:${id}`);
-    } catch (error) {
-      console.error(error);
-    }
-  }
-}
-
-export default Workflow;

+ 43 - 37
src/newtab/App.vue

@@ -64,11 +64,12 @@ import { useFolderStore } from '@/stores/folder';
 import { useWorkflowStore } from '@/stores/workflow';
 import { useTheme } from '@/composable/theme';
 import { parseJSON } from '@/utils/helper';
+import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
+import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import { loadLocaleMessages, setI18nLanguage } from '@/lib/vueI18n';
-import { getSharedWorkflows, getUserWorkflows } from '@/utils/api';
+import { getUserWorkflows } from '@/utils/api';
 import dbLogs from '@/db/logs';
 import dayjs from '@/lib/dayjs';
-import Workflow from '@/models/workflow';
 import AppSurvey from '@/components/newtab/app/AppSurvey.vue';
 import AppSidebar from '@/components/newtab/app/AppSidebar.vue';
 import dataMigration from '@/utils/dataMigration';
@@ -91,6 +92,8 @@ const theme = useTheme();
 const userStore = useUserStore();
 const folderStore = useFolderStore();
 const workflowStore = useWorkflowStore();
+const sharedWorkflowStore = useSharedWorkflowStore();
+const hostedWorkflowStore = useHostedWorkflowStore();
 
 theme.init();
 
@@ -102,38 +105,22 @@ const prevVersion = localStorage.getItem('ext-version') || '0.0.0';
 
 async function fetchUserData() {
   try {
-    if (!userStore.user) return;
-
-    const [sharedWorkflows, userWorkflows] = await Promise.allSettled([
-      getSharedWorkflows(),
-      getUserWorkflows(),
-    ]);
-
-    if (sharedWorkflows.status === 'fulfilled') {
-      workflowStore.shared = sharedWorkflows.value;
-    }
-
-    if (userWorkflows.status === 'fulfilled') {
-      const { backup, hosted } = userWorkflows.value;
-
-      workflowStore.userHosted = hosted || {};
-
-      if (backup?.length > 0) {
-        const { lastBackup } = browser.storage.local.get('lastBackup');
-        if (!lastBackup) {
-          const backupIds = backup.map(({ id }) => id);
-
-          userStore.backupIds = backupIds;
-          await browser.storage.local.set({
-            backupIds,
-            lastBackup: new Date().toISOString(),
-          });
-        }
-
-        await Workflow.insertOrUpdate({
-          data: backup,
+    const { backup, hosted } = await getUserWorkflows();
+    userStore.hostedWorkflows = hosted;
+
+    if (backup && backup.length > 0) {
+      const { lastBackup } = browser.storage.local.get('lastBackup');
+      if (!lastBackup) {
+        const backupIds = backup.map(({ id }) => id);
+
+        userStore.backupIds = backupIds;
+        await browser.storage.local.set({
+          backupIds,
+          lastBackup: new Date().toISOString(),
         });
       }
+
+      await workflowStore.insertOrUpdate(backup);
     }
 
     userStore.retrieved = true;
@@ -170,6 +157,20 @@ function autoDeleteLogs() {
       localStorage.setItem('checkDeleteLogs', Date.now());
     });
 }
+async function syncHostedWorkflows() {
+  const hostIds = [];
+  const userHosted = userStore.getHostedWorkflows;
+  const hostedWorkflows = hostedWorkflowStore.workflows;
+
+  Object.keys(hostedWorkflows).forEach((hostId) => {
+    const isItsOwn = userHosted.find((item) => item.hostId === hostId);
+    if (isItsOwn) return;
+
+    hostIds.push({ hostId, updatedAt: hostedWorkflows[hostId].updatedAt });
+  });
+
+  await hostedWorkflowStore.fetchWorkflows(hostIds);
+}
 
 window.addEventListener('storage', ({ key, newValue }) => {
   if (key !== 'workflowState') return;
@@ -187,22 +188,27 @@ window.addEventListener('storage', ({ key, newValue }) => {
 
     await Promise.allSettled([
       folderStore.load(),
-      workflowStore.loadLocal(),
-      workflowStore.loadStates(),
+      store.loadSettings(),
+      workflowStore.loadData(),
+      hostedWorkflowStore.loadData(),
     ]);
 
     await loadLocaleMessages(store.settings.locale, 'newtab');
     await setI18nLanguage(store.settings.locale);
 
     await dataMigration();
-    await workflowStore.loadLocal();
 
     retrieved.value = true;
     workflowStore.retrieved = true;
 
     await userStore.loadUser();
-    await fetchUserData();
-    await workflowStore.syncHostedWorkflows();
+    if (userStore.user) {
+      await Promise.allSettled([
+        sharedWorkflowStore.fetchWorkflows(),
+        fetchUserData(),
+      ]);
+    }
+    await syncHostedWorkflows();
 
     autoDeleteLogs();
   } catch (error) {

+ 0 - 2
src/newtab/index.js

@@ -1,7 +1,6 @@
 import { createApp } from 'vue';
 import App from './App.vue';
 import router from './router';
-import store from '../store';
 import pinia from '../lib/pinia';
 import compsUi from '../lib/compsUi';
 import vueI18n from '../lib/vueI18n';
@@ -14,7 +13,6 @@ import '../assets/css/flow.css';
 
 createApp(App)
   .use(router)
-  .use(store)
   .use(compsUi)
   .use(pinia)
   .use(vueI18n)

+ 0 - 144
src/newtab/pages/Collections.vue

@@ -1,144 +0,0 @@
-<template>
-  <div class="container pt-8 pb-4">
-    <div class="flex items-center">
-      <div class="flex-grow">
-        <h1 class="text-2xl font-semibold">{{ t('common.collection', 2) }}</h1>
-        <p class="text-gray-600 dark:text-gray-200">
-          {{ t('collection.description') }}
-        </p>
-      </div>
-      <div class="flex items-center px-4 py-2 bg-red-400 text-white rounded-lg">
-        <v-remixicon name="riErrorWarningLine" class="-ml-1" />
-        <p class="ml-2">This feature will be removed in the future update</p>
-      </div>
-    </div>
-    <div class="flex items-center my-6 space-x-4">
-      <ui-input
-        id="search-input"
-        v-model="query"
-        :placeholder="`${t('common.search')}... (${
-          shortcut['action:search'].readable
-        })`"
-        prepend-icon="riSearch2Line"
-        class="flex-1"
-      />
-      <ui-button
-        :title="shortcut['action:new'].readable"
-        variant="accent"
-        @click="newCollection"
-      >
-        {{ t('collection.new') }}
-      </ui-button>
-    </div>
-    <div
-      v-if="Collection.query().count() === 0"
-      class="py-12 flex items-center"
-    >
-      <img src="@/assets/svg/alien.svg" class="w-96" />
-      <div class="ml-4">
-        <h1 class="text-2xl font-semibold max-w-md mb-6">
-          {{ t('message.empty') }}
-        </h1>
-        <ui-button variant="accent" @click="newCollection">
-          {{ t('collection.new') }}
-        </ui-button>
-      </div>
-    </div>
-    <div class="grid gap-4 grid-cols-5 2xl:grid-cols-6">
-      <shared-card
-        v-for="collection in collections"
-        :key="collection.id"
-        :data="collection"
-        :menu="collectionCardMenu"
-        icon="riFolderLine"
-        @click="$router.push(`/collections/${$event.id}`)"
-        @execute="executeCollection"
-        @menuSelected="menuHandlers[$event.id]($event.data)"
-      />
-    </div>
-  </div>
-</template>
-<script setup>
-import { ref, computed } from 'vue';
-import { useI18n } from 'vue-i18n';
-import { sendMessage } from '@/utils/message';
-import { useDialog } from '@/composable/dialog';
-import { useShortcut } from '@/composable/shortcut';
-import Collection from '@/models/collection';
-import SharedCard from '@/components/newtab/shared/SharedCard.vue';
-
-const dialog = useDialog();
-const { t } = useI18n();
-
-const collectionCardMenu = [
-  { id: 'rename', name: t('common.rename'), icon: 'riPencilLine' },
-  { id: 'delete', name: t('common.delete'), icon: 'riDeleteBin7Line' },
-];
-
-const query = ref('');
-
-const collections = computed(() =>
-  Collection.query()
-    .where(({ name }) =>
-      name.toLocaleLowerCase().includes(query.value.toLocaleLowerCase())
-    )
-    .orderBy('createdAt', 'desc')
-    .get()
-);
-
-function executeCollection(collection) {
-  sendMessage('collection:execute', collection, 'background');
-}
-function newCollection() {
-  dialog.prompt({
-    title: t('collection.new'),
-    placeholder: t('common.name'),
-    okText: t('collection.add'),
-    onConfirm: (name) => {
-      Collection.insert({
-        data: {
-          name: name || 'Unnamed',
-          createdAt: Date.now(),
-        },
-      });
-    },
-  });
-}
-function renameCollection({ id, name }) {
-  dialog.prompt({
-    title: t('collection.rename'),
-    placeholder: t('common.name'),
-    okText: t('common.rename'),
-    inputValue: name,
-    onConfirm: (newName) => {
-      Collection.update({
-        where: id,
-        data: {
-          name: newName,
-        },
-      });
-    },
-  });
-}
-function deleteCollection({ name, id }) {
-  dialog.confirm({
-    title: t('collection.delete'),
-    okVariant: 'danger',
-    body: t('message.delete', { name }),
-    onConfirm: () => {
-      Collection.delete(id);
-    },
-  });
-}
-
-const shortcut = useShortcut(['action:search', 'action:new'], ({ id }) => {
-  if (id === 'action:search') {
-    const searchInput = document.querySelector('#search-input input');
-    searchInput?.focus();
-  } else {
-    newCollection();
-  }
-});
-
-const menuHandlers = { rename: renameCollection, delete: deleteCollection };
-</script>

+ 0 - 65
src/newtab/pages/Home.vue

@@ -1,65 +0,0 @@
-<template>
-  <div class="container pt-8 pb-4">
-    <h1 class="text-2xl font-semibold mb-8">{{ t('common.dashboard') }}</h1>
-    <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>
-import { computed } from 'vue';
-import { useStore } from 'vuex';
-import { useI18n } from 'vue-i18n';
-import { sendMessage } from '@/utils/message';
-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';
-
-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 workflowState = computed(() => store.state.workflowState);
-
-function executeWorkflow(workflow) {
-  sendMessage('workflow:execute', workflow, 'background');
-}
-</script>

+ 3 - 1
src/newtab/pages/Logs.vue

@@ -11,7 +11,7 @@
     <div v-if="logs" style="min-height: 320px">
       <shared-logs-table
         :logs="logs"
-        :running="$store.state.workflowState"
+        :running="workflowStore.workflowState"
         class="w-full"
       >
         <template #item-prepend="{ log }">
@@ -83,6 +83,7 @@ import { shallowReactive, ref, computed, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useDialog } from '@/composable/dialog';
 import dbLogs from '@/db/logs';
+import { useWorkflowStore } from '@/stores/workflow';
 import { useLiveQuery } from '@/composable/liveQuery';
 import LogsFilters from '@/components/newtab/logs/LogsFilters.vue';
 import LogsDataViewer from '@/components/newtab/logs/LogsDataViewer.vue';
@@ -90,6 +91,7 @@ import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
 
 const { t } = useI18n();
 const dialog = useDialog();
+const workflowStore = useWorkflowStore();
 const storedlogs = useLiveQuery(() => dbLogs.items.toArray());
 
 const savedSorts = JSON.parse(localStorage.getItem('logs-sorts') || '{}');

+ 20 - 17
src/newtab/pages/ScheduledWorkflow.vue

@@ -53,11 +53,12 @@ import { onMounted, reactive, computed } from 'vue';
 import { useI18n } from 'vue-i18n';
 import dayjs from 'dayjs';
 import browser from 'webextension-polyfill';
+import { useWorkflowStore } from '@/stores/workflow';
 import { findTriggerBlock } from '@/utils/helper';
 import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
-import Workflow from '@/models/workflow';
 
 const { t } = useI18n();
+const workflowStore = useWorkflowStore();
 
 const triggersData = {};
 const state = reactive({
@@ -135,8 +136,10 @@ function scheduleText(data) {
         time: data.interval,
       });
       break;
-    case 'data':
-      dayjs(data.date).format('DD MMM YYYY, hh:mm:ss A');
+    case 'date':
+      text.schedule = dayjs(`${data.date}, ${data.time}`).format(
+        'DD MMM YYYY, hh:mm:ss A'
+      );
       break;
     default:
   }
@@ -202,21 +205,21 @@ async function refreshSchedule(id) {
 }
 
 onMounted(async () => {
-  const workflows = Workflow.all();
-
-  for (const workflow of workflows) {
-    let { trigger } = workflow;
-
-    if (!trigger) {
-      const drawflow =
-        typeof workflow.drawflow === 'string'
-          ? JSON.parse(workflow.drawflow)
-          : workflow.drawflow;
-      trigger = findTriggerBlock(drawflow)?.data;
+  for (const workflow of workflowStore.getWorkflows) {
+    if (!workflow.isDisabled) {
+      let { trigger } = workflow;
+
+      if (!trigger) {
+        const drawflow =
+          typeof workflow.drawflow === 'string'
+            ? JSON.parse(workflow.drawflow)
+            : workflow.drawflow;
+        trigger = findTriggerBlock(drawflow)?.data;
+      }
+
+      const obj = await getTriggerObj(trigger, workflow);
+      if (obj) state.triggers.push(obj);
     }
-
-    const obj = await getTriggerObj(trigger, workflow);
-    if (obj) state.triggers.push(obj);
   }
 });
 </script>

+ 16 - 60
src/newtab/pages/Workflows.vue

@@ -203,12 +203,11 @@ import { useToast } from 'vue-toastification';
 import { useDialog } from '@/composable/dialog';
 import { useShortcut } from '@/composable/shortcut';
 import { useGroupTooltip } from '@/composable/groupTooltip';
-import { fetchApi } from '@/utils/api';
 import { importWorkflow } from '@/utils/workflowData';
-import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
-import { findTriggerBlock, isWhitespace } from '@/utils/helper';
+import { isWhitespace } from '@/utils/helper';
 import { useUserStore } from '@/stores/user';
 import { useWorkflowStore } from '@/stores/workflow';
+import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import WorkflowsLocal from '@/components/newtab/workflows/WorkflowsLocal.vue';
 import WorkflowsShared from '@/components/newtab/workflows/WorkflowsShared.vue';
 import WorkflowsHosted from '@/components/newtab/workflows/WorkflowsHosted.vue';
@@ -220,6 +219,7 @@ const toast = useToast();
 const dialog = useDialog();
 const userStore = useUserStore();
 const workflowStore = useWorkflowStore();
+const hostedWorkflowStore = useHostedWorkflowStore();
 
 const sorts = ['name', 'createdAt'];
 
@@ -238,7 +238,7 @@ const addWorkflowModal = shallowReactive({
   description: '',
 });
 
-const hostedWorkflows = computed(() => Object.values(workflowStore.hosted));
+const hostedWorkflows = computed(() => hostedWorkflowStore.toArray);
 
 function clearAddWorkflowModal() {
   Object.assign(addWorkflowModal, {
@@ -248,7 +248,7 @@ function clearAddWorkflowModal() {
   });
 }
 function addWorkflow() {
-  workflowStore.addWorkflow({
+  workflowStore.insert({
     name: addWorkflowModal.name,
     description: addWorkflowModal.description,
   });
@@ -263,66 +263,22 @@ function addHostedWorkflow() {
     label: t('workflow.host.id'),
     placeholder: 'abcd123',
     onConfirm: async (value) => {
-      try {
-        if (isWhitespace(value)) return false;
-
-        let length = 0;
-        let isItsOwn = false;
-        let isHostExist = false;
-        const hostId = value.replace(/\s/g, '');
-
-        hostedWorkflows.value.forEach((host) => {
-          if (hostId === host.hostId) isHostExist = true;
-
-          length += 1;
-        });
-
-        if (!userStore.user && length >= 3) {
-          toast.error(t('message.rateExceeded'));
-          return false;
-        }
-
-        Object.values(workflowStore.userHosted).forEach((host) => {
-          if (hostId === host.hostId) isItsOwn = true;
-        });
-
-        if (isHostExist || isItsOwn) {
-          toast.error(t('workflow.host.messages.hostExist'));
-          return false;
-        }
+      if (isWhitespace(value)) return false;
+      const hostId = value.replace(/\s/g, '');
 
-        const response = await fetchApi('/workflows/hosted', {
-          method: 'POST',
-          body: JSON.stringify({ hostId }),
-        });
-        const result = await response.json();
-
-        if (!response.ok) {
-          const error = new Error(result.message);
-          error.data = result.data;
-
-          throw error;
-        }
-
-        if (result === null) {
-          toast.error(t('workflow.host.messages.notFound', { id: hostId }));
-          return false;
-        }
-
-        result.hostId = hostId;
-        result.createdAt = Date.now();
-
-        workflowStore.hosted[hostId] = result;
-        const triggerBlock = findTriggerBlock(result.drawflow);
-        await registerWorkflowTrigger(hostId, triggerBlock);
+      try {
+        await hostedWorkflowStore.addHostedWorkflow(hostId);
 
         return true;
       } catch (error) {
-        console.error(error);
+        const messages = {
+          exists: t('workflow.host.messages.hostExist'),
+          'rate-exceeded': t('message.rateExceeded'),
+          'not-found': t('workflow.host.messages.notFound', { id: hostId }),
+        };
+        const errorMessage = messages[error.message] || error.message;
 
-        toast.error(
-          error.data?.show ? error.message : t('message.somethingWrong')
-        );
+        toast.error(errorMessage);
 
         return false;
       }

+ 0 - 365
src/newtab/pages/collections/[id].vue

@@ -1,365 +0,0 @@
-<template>
-  <div class="container pt-8 pb-4">
-    <div class="flex items-center mb-8">
-      <input
-        :value="collection.name"
-        placeholder="Collection name"
-        class="text-2xl hover:ring-2 hover:ring-accent font-semibold bg-transparent"
-        @blur="updateCollection({ name: $event.target.value || 'Unnamed' })"
-      />
-      <div class="flex-grow"></div>
-      <ui-button variant="accent" class="mr-4" @click="executeCollection">
-        {{ t('common.execute') }}
-      </ui-button>
-      <ui-button class="text-red-500" @click="deleteCollection">
-        {{ t('common.delete') }}
-      </ui-button>
-    </div>
-    <div class="flex items-start">
-      <div
-        class="w-80 border-r sticky top-11 pr-6 mr-6 p-1 scroll overflow-auto"
-        style="max-height: calc(100vh - 8rem)"
-      >
-        <ui-input
-          v-model="state.query"
-          placeholder="Search workflows"
-          class="w-full space-x-1 mb-3"
-          prepend-icon="riSearch2Line"
-        />
-        <ui-tabs v-model="state.sidebarTab" fill class="w-full mb-4">
-          <ui-tab value="workflows">{{ t('common.workflow', 2) }}</ui-tab>
-          <ui-tab value="blocks">{{ t('common.block', 2) }}</ui-tab>
-        </ui-tabs>
-        <draggable
-          :list="state.sidebarTab === 'workflows' ? workflows : blocksArr"
-          :group="{ name: 'collection', pull: 'clone', put: false }"
-          :sort="false"
-          item-key="id"
-        >
-          <template #item="{ element }">
-            <ui-card
-              v-bind="{
-                title: element.description ? element.description : element.name,
-              }"
-              class="mb-2 cursor-move flex items-center"
-            >
-              <v-remixicon :name="element.icon" class="mr-2" />
-              <p class="flex-1 text-overflow">{{ element.name }}</p>
-            </ui-card>
-          </template>
-        </draggable>
-      </div>
-      <div class="flex-1 relative">
-        <div class="flex items-center mb-4">
-          <div class="px-1 inline-block rounded-lg bg-white dark:bg-gray-800">
-            <ui-tabs
-              v-model="state.activeTab"
-              class="border-none h-full space-x-1"
-            >
-              <ui-tab value="flow">{{ t('collection.flow') }}</ui-tab>
-              <ui-tab value="logs">{{ t('common.log', 2) }}</ui-tab>
-              <ui-tab value="running">
-                {{ t('common.running') }}
-                <span
-                  v-if="runningCollection.length > 0"
-                  class="ml-2 p-1 text-center inline-block text-xs rounded-full bg-black text-white"
-                  style="min-width: 25px"
-                >
-                  {{ runningCollection.length }}
-                </span>
-              </ui-tab>
-              <ui-tab value="options">{{ t('common.options') }}</ui-tab>
-            </ui-tabs>
-          </div>
-          <div class="flex-grow"></div>
-          <ui-button
-            v-tooltip="t('common.globalData')"
-            icon
-            @click="state.showGlobalData = !state.showGlobalData"
-          >
-            <v-remixicon name="riDatabase2Line" />
-          </ui-button>
-        </div>
-        <ui-tab-panels v-model="state.activeTab">
-          <ui-tab-panel class="relative" value="flow">
-            <div
-              v-if="collection.flow.length === 0"
-              class="border text-gray-600 absolute top-0 w-full z-0 dark:text-gray-200 rounded-lg border-dashed text-center p-4"
-            >
-              {{ t('collection.dragDropText') }}
-            </div>
-            <draggable
-              :model-value="collectionFlow"
-              item-key="id"
-              group="collection"
-              style="min-height: 200px"
-              @update:modelValue="updateCollectionFlow"
-            >
-              <template #item="{ element, index }">
-                <ui-card
-                  class="group flex cursor-move mb-2 items-center relative overflow-hidden"
-                >
-                  <span
-                    :class="[
-                      element.type === 'block'
-                        ? 'bg-yellow-200'
-                        : 'bg-green-200',
-                    ]"
-                    class="absolute w-1 left-0 top-0 h-full"
-                  ></span>
-                  <v-remixicon :name="element.icon" class="mr-4" />
-                  <p class="flex-1 text-overflow">{{ element.name }}</p>
-                  <router-link
-                    v-if="element.type !== 'block'"
-                    :to="'/workflows/' + element.id"
-                    title="Open workflow"
-                    class="mr-4 group group-hover:visible invisible"
-                  >
-                    <v-remixicon name="riExternalLinkLine" />
-                  </router-link>
-                  <v-remixicon
-                    name="riDeleteBin7Line"
-                    class="cursor-pointer"
-                    @click="deleteCollectionFlow(index)"
-                  />
-                </ui-card>
-              </template>
-            </draggable>
-          </ui-tab-panel>
-          <ui-tab-panel value="logs">
-            <div v-if="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">
-              <template #item-append="{ log }">
-                <td class="text-right">
-                  <v-remixicon
-                    name="riDeleteBin7Line"
-                    class="inline-block text-red-500 cursor-pointer"
-                    @click="deleteLog(log.id)"
-                  />
-                </td>
-              </template>
-            </shared-logs-table>
-          </ui-tab-panel>
-          <ui-tab-panel value="running">
-            <div v-if="runningCollection.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 runningCollection"
-                :key="item.id"
-                :data="item"
-              />
-            </div>
-          </ui-tab-panel>
-          <ui-tab-panel value="options">
-            <ui-checkbox v-model="collectionOptions.atOnce">
-              <p class="leading-tight">
-                {{ t('collection.options.atOnce.title') }}
-              </p>
-              <p class="text-sm text-gray-600 dark:text-gray-200 leading-tight">
-                {{ t('collection.options.atOnce.description') }}
-              </p>
-            </ui-checkbox>
-          </ui-tab-panel>
-        </ui-tab-panels>
-      </div>
-    </div>
-  </div>
-  <ui-modal v-model="state.showGlobalData" content-class="max-w-xl">
-    <template #header>{{ t('common.globalData') }}</template>
-    <p class="inline-block">
-      {{ t('collection.globalData.note') }}
-    </p>
-    <p class="float-right clear-both" title="Characters limit">
-      {{ collection.globalData.length }}/{{ (1e4).toLocaleString() }}
-    </p>
-    <shared-codemirror
-      :model-value="collection.globalData"
-      lang="json"
-      class="mt-2"
-      style="height: calc(100vh - 10rem)"
-      @change="updateGlobalData"
-    />
-  </ui-modal>
-</template>
-<script setup>
-import {
-  computed,
-  shallowReactive,
-  onMounted,
-  watch,
-  defineAsyncComponent,
-} from 'vue';
-import { nanoid } from 'nanoid';
-import { useStore } from 'vuex';
-import { useRoute, useRouter } from 'vue-router';
-import { useI18n } from 'vue-i18n';
-import Draggable from 'vuedraggable';
-import { useDialog } from '@/composable/dialog';
-import { sendMessage } from '@/utils/message';
-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';
-import SharedWorkflowState from '@/components/newtab/shared/SharedWorkflowState.vue';
-
-const SharedCodemirror = defineAsyncComponent(() =>
-  import('@/components/newtab/shared/SharedCodemirror.vue')
-);
-
-const { t } = useI18n();
-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': {
-    type: 'block',
-    id: 'export-result',
-    icon: 'riDownloadLine',
-    name: t('collection.blocks.export-result.name'),
-    description: t('collection.blocks.export-result.description'),
-    data: {
-      type: 'json',
-    },
-  },
-};
-const blocksArr = Object.entries(blocks).map(([id, value]) => ({
-  ...value,
-  id,
-}));
-
-const state = shallowReactive({
-  query: '',
-  activeTab: 'flow',
-  showGlobalData: false,
-  sidebarTab: 'workflows',
-});
-const collectionOptions = shallowReactive({
-  atOnce: false,
-});
-
-const runningCollection = computed(() =>
-  store.state.workflowState.filter(({ id }) => id === route.params.id)
-);
-const workflows = computed(() =>
-  Workflow.query()
-    .where(({ name }) =>
-      name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())
-    )
-    .orderBy('createdAt', 'desc')
-    .get()
-);
-const collection = computed(() => Collection.find(route.params.id));
-const collectionFlow = computed(() => {
-  if (!collection.value) return [];
-
-  return collection.value.flow.map(({ itemId, type }) => {
-    if (type === 'workflow') return Workflow.find(itemId) || { type };
-
-    return blocks[itemId];
-  });
-});
-
-function deleteLog(logId) {
-  dbLogs.items.where('id').equals(logId).delete();
-}
-function executeCollection() {
-  sendMessage('collection:execute', collection.value, 'background');
-}
-function updateCollection(data) {
-  Collection.update({
-    where: route.params.id,
-    data,
-  });
-}
-function updateGlobalData(str) {
-  let value = str;
-
-  if (value.length > 1e4) {
-    value = value.slice(0, 1e4);
-  }
-
-  updateCollection({ globalData: value });
-}
-function updateCollectionFlow(event) {
-  const flow = event.map(({ type, id, flowId, data }) => {
-    const itemFlowId = flowId || nanoid();
-
-    return type === 'block'
-      ? { type, itemId: id, id: itemFlowId, data }
-      : { type: 'workflow', itemId: id, id: itemFlowId };
-  });
-
-  updateCollection({ flow });
-}
-function deleteCollectionFlow(index) {
-  const flow = [...collection.value.flow];
-
-  flow.splice(index, 1);
-
-  updateCollection({ flow });
-}
-function deleteCollection() {
-  dialog.confirm({
-    title: 'Delete collection',
-    okVariant: 'danger',
-    body: 'Are you sure you want to delete this collection?',
-    onConfirm: () => {
-      Collection.delete(route.params.id).then(() => {
-        router.replace('/collections');
-      });
-    },
-  });
-}
-
-watch(
-  () => collectionOptions,
-  (value) => {
-    Collection.update({
-      where: route.params.id,
-      data: {
-        options: value,
-      },
-    });
-  },
-  { deep: true }
-);
-
-onMounted(() => {
-  Object.assign(collectionOptions, collection.value.options);
-
-  collectionFlow.value.forEach((item, index) => {
-    if (!item.itemId && item.type === 'workflow') {
-      deleteCollectionFlow(index);
-    }
-  });
-});
-</script>
-<style>
-.ghost {
-  position: relative;
-  z-index: 100;
-}
-</style>

+ 3 - 3
src/newtab/pages/logs/Running.vue

@@ -75,19 +75,19 @@
 </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 { useWorkflowStore } from '@/stores/workflow';
 import dbLogs from '@/db/logs';
 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 workflowStore = useWorkflowStore();
 
 const key = shallowRef(0);
 const interval = setInterval(() => {
@@ -95,7 +95,7 @@ const interval = setInterval(() => {
 }, 1000);
 
 const running = computed(() =>
-  store.state.workflowState.find(({ id }) => id === route.params.id)
+  workflowStore.states.find(({ id }) => id === route.params.id)
 );
 
 function stopWorkflow() {

+ 3 - 2
src/newtab/pages/logs/[id].vue

@@ -75,8 +75,8 @@ import { shallowReactive, shallowRef, watch } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import dbLogs from '@/db/logs';
-import Workflow from '@/models/workflow';
 import dayjs from '@/lib/dayjs';
+import { useWorkflowStore } from '@/stores/workflow';
 import { countDuration, convertArrObjTo2DArr } from '@/utils/helper';
 import LogsTable from '@/components/newtab/logs/LogsTable.vue';
 import LogsHistory from '@/components/newtab/logs/LogsHistory.vue';
@@ -85,6 +85,7 @@ import LogsVariables from '@/components/newtab/logs/LogsVariables.vue';
 const { t } = useI18n();
 const route = useRoute();
 const router = useRouter();
+const workflowStore = useWorkflowStore();
 
 const ctxData = shallowRef({});
 const parentLog = shallowRef(null);
@@ -182,7 +183,7 @@ async function fetchLog() {
     ...logDetail,
   };
 
-  state.workflowExists = Boolean(Workflow.find(logDetail.workflowId));
+  state.workflowExists = Boolean(workflowStore.getById(logDetail.workflowId));
 
   const parentLogId = logDetail.collectionLogId || logDetail.parentLog?.id;
   if (parentLogId) {

+ 3 - 6
src/newtab/pages/settings/SettingsAbout.vue

@@ -30,7 +30,7 @@
     </p>
     <div class="mt-4 gap-2 mb-12 grid grid-cols-7">
       <a
-        v-for="contributor in store.state.contributors"
+        v-for="contributor in store.contributors"
         :key="contributor.username"
         v-tooltip.group="contributor.username"
         :href="contributor.url"
@@ -50,8 +50,8 @@
 <script setup>
 /* eslint-disable camelcase */
 import { onMounted } from 'vue';
-import { useStore } from 'vuex';
 import browser from 'webextension-polyfill';
+import { useStore } from '@/stores/main';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { communities } from '@/utils/shared';
 
@@ -96,10 +96,7 @@ onMounted(async () => {
       []
     );
 
-    store.commit('updateState', {
-      key: 'contributors',
-      value: contributors,
-    });
+    store.contributors = contributors;
   } catch (error) {
     console.error(error);
   }

+ 24 - 32
src/newtab/pages/settings/SettingsBackup.vue

@@ -4,7 +4,7 @@
       <h2 class="font-semibold mb-2">
         {{ t('settings.backupWorkflows.cloud.title') }}
       </h2>
-      <template v-if="$store.state.user">
+      <template v-if="userStore.user">
         <div
           class="border dark:border-gray-700 p-4 rounded-lg flex items-center"
         >
@@ -111,9 +111,8 @@
   </ui-modal>
 </template>
 <script setup>
-import { reactive, watch } from 'vue';
+import { reactive, onMounted } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { useStore } from 'vuex';
 import { useToast } from 'vue-toastification';
 import dayjs from 'dayjs';
 import AES from 'crypto-js/aes';
@@ -121,15 +120,17 @@ import encUtf8 from 'crypto-js/enc-utf8';
 import browser from 'webextension-polyfill';
 import hmacSHA256 from 'crypto-js/hmac-sha256';
 import { useDialog } from '@/composable/dialog';
+import { useUserStore } from '@/stores/user';
 import { getUserWorkflows } from '@/utils/api';
+import { useWorkflowStore } from '@/stores/workflow';
 import { fileSaver, openFilePicker, parseJSON } from '@/utils/helper';
-import Workflow from '@/models/workflow';
 import SettingsCloudBackup from '@/components/newtab/settings/SettingsCloudBackup.vue';
 
 const { t } = useI18n();
-const store = useStore();
 const toast = useToast();
 const dialog = useDialog();
+const userStore = useUserStore();
+const workflowStore = useWorkflowStore();
 
 const state = reactive({
   lastSync: null,
@@ -153,17 +154,17 @@ async function syncBackupWorkflows() {
   try {
     state.loadingSync = true;
     const { backup, hosted } = await getUserWorkflows(false);
+    const backupIds = backup.map(({ id }) => id);
+
+    userStore.backupIds = backupIds;
+    userStore.hostedWorkflows = hosted;
 
-    store.commit('updateState', {
-      key: 'hostWorkflows',
-      value: hosted,
-    });
     await browser.storage.local.set({
+      backupIds,
       lastBackup: new Date().toISOString(),
     });
-    await Workflow.insertOrUpdate({
-      data: backup,
-    });
+
+    await workflowStore.insertOrUpdate(backup);
 
     state.loadingSync = false;
   } catch (error) {
@@ -173,7 +174,7 @@ async function syncBackupWorkflows() {
   }
 }
 function backupWorkflows() {
-  const workflows = Workflow.all().reduce((acc, workflow) => {
+  const workflows = workflowStore.getWorkflows.reduce((acc, workflow) => {
     if (workflow.isProtected) return acc;
 
     delete workflow.$id;
@@ -246,14 +247,10 @@ async function restoreWorkflows() {
       };
 
       if (state.updateIfExists) {
-        return Workflow.insertOrUpdate({
-          data: newWorkflows,
-        }).then(showMessage);
+        return workflowStore.insertOrUpdate(newWorkflows).then(showMessage);
       }
 
-      return Workflow.insert({
-        data: newWorkflows,
-      }).then(showMessage);
+      return workflowStore.insert(newWorkflows).then(showMessage);
     };
 
     reader.onload = ({ target }) => {
@@ -303,18 +300,13 @@ async function restoreWorkflows() {
   }
 }
 
-watch(
-  () => store.state.userDataRetrieved,
-  async () => {
-    const { lastBackup, lastSync } = await browser.storage.local.get([
-      'backupIds',
-      'lastBackup',
-      'lastSync',
-    ]);
+onMounted(async () => {
+  const { lastBackup, lastSync } = await browser.storage.local.get([
+    'lastBackup',
+    'lastSync',
+  ]);
 
-    state.lastSync = lastSync;
-    state.lastBackup = lastBackup;
-  },
-  { immediate: true }
-);
+  state.lastSync = lastSync;
+  state.lastBackup = lastBackup;
+});
 </script>

+ 4 - 0
src/newtab/pages/settings/SettingsEditor.vue

@@ -1,4 +1,7 @@
 <template>
+  <p>Hello world</p>
+</template>
+<!-- <template>
   <div class="max-w-2xl">
     <p class="font-semibold">
       {{ t('settings.editor.curvature.title') }}
@@ -101,3 +104,4 @@ function curvatureLimit(value) {
   return value;
 }
 </script>
+ -->

+ 3 - 16
src/newtab/pages/settings/SettingsIndex.vue

@@ -79,10 +79,8 @@
 </template>
 <script setup>
 import { computed, ref } from 'vue';
-import { useStore } from 'vuex';
 import { useI18n } from 'vue-i18n';
-import cloneDeep from 'lodash.clonedeep';
-import browser from 'webextension-polyfill';
+import { useStore } from '@/stores/main';
 import { useTheme } from '@/composable/theme';
 import { supportLocales } from '@/utils/shared';
 
@@ -93,21 +91,10 @@ const store = useStore();
 const theme = useTheme();
 
 const isLangChange = ref(false);
-const settings = computed(() => store.state.settings);
+const settings = computed(() => store.settings);
 
 function updateSetting(path, value) {
-  store.commit('updateStateNested', {
-    value,
-    path: `settings.${path}`,
-  });
-
-  let userSettings = settings.value;
-
-  if (BROWSER_TYPE === 'firefox') {
-    userSettings = cloneDeep(userSettings);
-  }
-
-  browser.storage.local.set({ settings: userSettings });
+  store.updateSettings({ [path]: value });
 }
 function updateLanguage(value) {
   isLangChange.value = true;

+ 44 - 58
src/newtab/pages/workflows/Host.vue

@@ -36,16 +36,6 @@
       >
         <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') }}
-          <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"
-            style="min-width: 25px"
-          >
-            {{ workflowState.length }}
-          </span>
-        </ui-tab>
       </ui-tabs>
       <div class="flex-grow"></div>
       <ui-card padding="p-1">
@@ -91,13 +81,15 @@
       class="h-full"
     >
       <ui-tab-panel class="h-full" value="editor">
-        <workflow-builder
-          v-if="workflow?.drawflow"
+        <workflow-editor
+          v-if="state.retrieved"
+          :id="route.params.id"
           :key="state.editorKey"
-          :version="false"
-          :is-shared="true"
           :data="workflow.drawflow"
+          :options="editorOptions"
+          :disabled="true"
           class="h-full w-full"
+          @init="onEditorInit"
         />
       </ui-tab-panel>
       <ui-tab-panel value="logs">
@@ -113,63 +105,59 @@
           </template>
         </shared-logs-table>
       </ui-tab-panel>
-      <ui-tab-panel value="running">
-        <div class="grid grid-cols-3 gap-4">
-          <shared-workflow-state
-            v-for="item in workflowState"
-            :key="item.id"
-            :data="item"
-          />
-        </div>
-      </ui-tab-panel>
     </ui-tab-panels>
   </div>
 </template>
 <script setup>
 import { computed, reactive, onMounted, watch } from 'vue';
-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 { sendMessage } from '@/utils/message';
 import { useDialog } from '@/composable/dialog';
 import { useShortcut } from '@/composable/shortcut';
+import { useLiveQuery } from '@/composable/liveQuery';
 import { useGroupTooltip } from '@/composable/groupTooltip';
-import { parseJSON, findTriggerBlock } from '@/utils/helper';
-import { cleanWorkflowTriggers } from '@/utils/workflowTrigger';
-import { sendMessage } from '@/utils/message';
+import { findTriggerBlock } from '@/utils/helper';
+import convertWorkflowData from '@/utils/convertWorkflowData';
+import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import dbLogs from '@/db/logs';
 import getTriggerText from '@/utils/triggerText';
-import WorkflowBuilder from '@/components/newtab/workflow/WorkflowBuilder.vue';
+import WorkflowEditor from '@/components/newtab/workflow/WorkflowEditor.vue';
 import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
-import SharedWorkflowState from '@/components/newtab/shared/SharedWorkflowState.vue';
 
 useGroupTooltip();
 
 const { t } = useI18n();
-const store = useStore();
 const route = useRoute();
 const router = useRouter();
 const dialog = useDialog();
+const hostedWorkflowStore = useHostedWorkflowStore();
+
+const workflowId = route.params.id;
+const editorOptions = {
+  disabled: true,
+  fitViewOnInit: true,
+  nodesDraggable: false,
+  edgesUpdateable: false,
+  nodesConnectable: false,
+  elementsSelectable: false,
+};
+
 /* eslint-disable-next-line */
 const shortcut = useShortcut('editor:execute-workflow', executeWorkflow);
 const logs = useLiveQuery(() =>
-  dbLogs.items.query().where('workflowId').equals(route.params.id).toArray()
+  dbLogs.items.where('workflowId').equals(workflowId).toArray()
 );
 
-const workflowId = route.params.id;
-
 const state = reactive({
   editorKey: 0,
+  retrieved: false,
   loadingSync: false,
   activeTab: 'editor',
   trigger: 'Trigger: Manually',
 });
 
-const workflow = computed(() => store.state.workflowHosts[workflowId]);
-const workflowState = computed(() =>
-  store.getters.getWorkflowState(workflowId)
-);
+const workflow = computed(() => hostedWorkflowStore.getById(workflowId));
 
 function syncWorkflow() {
   state.loadingSync = true;
@@ -178,8 +166,8 @@ function syncWorkflow() {
     updatedAt: null,
   };
 
-  store
-    .dispatch('fetchWorkflowHosts', [hostId])
+  hostedWorkflowStore
+    .fetchWorkflows([hostId])
     .then(() => {
       if (!workflow.value) {
         router.replace('/workflows');
@@ -200,12 +188,7 @@ async function deleteWorkflowHost() {
     body: t('message.delete', { name: workflow.value.name }),
     onConfirm: async () => {
       try {
-        store.commit('deleteStateNested', `workflowHosts.${workflowId}`);
-
-        await browser.storage.local.set({
-          workflowHosts: store.state.sharedWorkflows,
-        });
-        await cleanWorkflowTriggers(workflowId);
+        await hostedWorkflowStore.delete(workflowId);
 
         router.replace('/workflows');
       } catch (error) {
@@ -226,9 +209,7 @@ function deleteLog(logId) {
   dbLogs.items.where('id').equals(logId);
 }
 async function retrieveTriggerText() {
-  const flow = parseJSON(workflow.value?.drawflow, null);
-  const triggerBlock = findTriggerBlock(flow);
-
+  const triggerBlock = findTriggerBlock(workflow.value.drawflow);
   if (!triggerBlock) return;
 
   state.triggerText = await getTriggerText(
@@ -238,22 +219,27 @@ async function retrieveTriggerText() {
     true
   );
 }
+function onEditorInit(editor) {
+  editor.setInteractive(false);
+}
 
-watch(
-  () => workflow.value?.drawflow,
-  () => {
-    state.editorKey += 1;
-    retrieveTriggerText();
-  }
-);
+watch(workflow, () => {
+  state.editorKey += 1;
+});
 
 onMounted(() => {
-  if (!workflow.value) {
+  const currentWorkflow = hostedWorkflowStore.workflows[workflowId];
+  if (!currentWorkflow) {
     router.push('/workflows');
     return;
   }
 
+  const convertedData = convertWorkflowData(currentWorkflow);
+  hostedWorkflowStore.update({ id: workflowId, ...convertedData });
+
   retrieveTriggerText();
+
+  state.retrieved = true;
 });
 </script>
 <style>

+ 0 - 941
src/newtab/pages/workflows/[id].old.vue

@@ -1,941 +0,0 @@
-<template>
-  <div v-if="workflow" class="flex h-screen">
-    <div
-      v-if="state.showSidebar"
-      class="w-80 bg-white dark:bg-gray-800 py-6 relative border-l border-gray-100 dark:border-gray-700 dark:border-opacity-50 flex flex-col"
-    >
-      <workflow-edit-block
-        v-if="state.isEditBlock && workflowData.active !== 'shared'"
-        v-model:autocomplete="autocomplete.cache"
-        :data="state.blockData"
-        :data-changed="autocomplete.dataChanged"
-        :editor="editor"
-        :workflow="workflow"
-        @update="updateBlockData"
-        @close="(state.isEditBlock = false), (state.blockData = {})"
-      />
-      <workflow-details-card
-        v-else
-        :workflow="workflow"
-        :data="workflowData"
-        @update="updateWorkflow"
-      />
-    </div>
-    <div class="flex-1 relative overflow-auto">
-      <div class="absolute w-full flex items-center z-10 left-0 p-4 top-0">
-        <ui-tabs
-          v-model="activeTab"
-          class="border-none px-2 rounded-lg h-full space-x-1 bg-white dark:bg-gray-800"
-        >
-          <button
-            v-tooltip="
-              `${t('workflow.toggleSidebar')} (${
-                shortcut['editor:toggle-sidebar'].readable
-              })`
-            "
-            style="margin-right: 6px"
-            @click="toggleSidebar"
-          >
-            <v-remixicon
-              :name="state.showSidebar ? 'riSideBarFill' : 'riSideBarLine'"
-            />
-          </button>
-          <ui-tab value="editor">{{ t('common.editor') }}</ui-tab>
-          <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"
-              style="min-width: 25px"
-            >
-              {{ workflowState.length }}
-            </span>
-          </ui-tab>
-        </ui-tabs>
-        <div class="flex-grow"></div>
-        <workflow-shared-actions
-          v-if="workflowData.active === 'shared'"
-          :data="workflowData"
-          :workflow="workflow"
-          @insertLocal="insertToLocal"
-          @update="updateSharedWorkflow"
-          @fetchLocal="fetchLocalWorkflow"
-          @save="saveUpdatedSharedWorkflow"
-          @unpublish="unpublishSharedWorkflow"
-        />
-        <workflow-actions
-          v-else
-          :data="workflowData"
-          :host="hostWorkflow"
-          :workflow="workflow"
-          :is-data-changed="state.isDataChanged"
-          @save="saveWorkflow"
-          @share="shareWorkflow"
-          @rename="renameWorkflow"
-          @update="updateWorkflow"
-          @delete="deleteWorkflow"
-          @host="setAsHostWorkflow"
-          @execute="executeWorkflow"
-          @export="workflowExporter"
-          @showModal="(state.modalName = $event), (state.showModal = true)"
-        />
-      </div>
-      <keep-alive>
-        <workflow-builder
-          v-if="activeTab === 'editor' && state.drawflow !== null"
-          class="h-full w-full"
-          :is-shared="workflowData.active === 'shared'"
-          :data="state.drawflow"
-          :version="workflow.version"
-          @save="saveWorkflow"
-          @update="updateWorkflow"
-          @load="editor = $event"
-          @loaded="onEditorLoaded"
-          @deleteBlock="deleteBlock"
-        >
-          <ui-tabs
-            v-if="
-              workflowData.hasLocal &&
-              workflowData.hasShared &&
-              !state.isDataChanged
-            "
-            v-model="workflowData.active"
-            class="z-10 text-sm"
-            color="bg-white dark:bg-gray-800"
-            type="fill"
-          >
-            <ui-tab value="local">
-              {{ t('workflow.type.local') }}
-            </ui-tab>
-            <ui-tab value="shared">
-              {{ t('workflow.type.shared') }}
-            </ui-tab>
-          </ui-tabs>
-        </workflow-builder>
-        <div v-else class="container pb-4 mt-24 px-4">
-          <template v-if="activeTab === 'logs'">
-            <div
-              v-if="(!logs || logs.length === 0) && 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>
-            <shared-logs-table
-              :logs="logs"
-              :running="workflowState"
-              hide-select
-              class="w-full"
-            >
-              <template #item-append="{ log: itemLog }">
-                <td class="text-right">
-                  <v-remixicon
-                    name="riDeleteBin7Line"
-                    class="inline-block text-red-500 cursor-pointer dark:text-red-400"
-                    @click="deleteLog(itemLog.id)"
-                  />
-                </td>
-              </template>
-            </shared-logs-table>
-          </template>
-        </div>
-      </keep-alive>
-    </div>
-  </div>
-  <ui-modal
-    v-model="state.showModal"
-    :content-class="workflowModal?.width || 'max-w-xl'"
-    v-bind="workflowModal.attrs || {}"
-  >
-    <template v-if="workflowModal.title" #header>
-      {{ workflowModal.title }}
-      <a
-        v-if="workflowModal.docs"
-        :title="t('common.docs')"
-        :href="workflowModal.docs"
-        target="_blank"
-        class="inline-block align-middle"
-      >
-        <v-remixicon name="riInformationLine" size="20" />
-      </a>
-    </template>
-    <component
-      :is="workflowModal.component"
-      v-bind="{ workflow }"
-      v-on="workflowModal?.events || {}"
-      @update="updateWorkflow"
-      @close="state.showModal = false"
-    />
-  </ui-modal>
-  <ui-modal v-model="renameModal.show" title="Workflow">
-    <ui-input
-      v-model="renameModal.name"
-      :placeholder="t('common.name')"
-      class="w-full mb-4"
-      @keyup.enter="updateNameAndDesc"
-    />
-    <ui-textarea
-      v-model="renameModal.description"
-      :placeholder="t('common.description')"
-      height="165px"
-      class="w-full dark:text-gray-200"
-      max="300"
-    />
-    <p class="mb-6 text-right text-gray-600 dark:text-gray-200">
-      {{ renameModal.description.length }}/300
-    </p>
-    <div class="space-x-2 flex">
-      <ui-button class="w-full" @click="renameModal.show = false">
-        {{ t('common.cancel') }}
-      </ui-button>
-      <ui-button variant="accent" class="w-full" @click="updateNameAndDesc">
-        {{ t('common.update') }}
-      </ui-button>
-    </div>
-  </ui-modal>
-</template>
-<script setup>
-/* eslint-disable consistent-return, no-use-before-define */
-import {
-  computed,
-  reactive,
-  shallowRef,
-  provide,
-  onMounted,
-  onUnmounted,
-  toRaw,
-  watch,
-} from 'vue';
-import { useStore } from 'vuex';
-import { useToast } from 'vue-toastification';
-import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
-import { useI18n } from 'vue-i18n';
-import defu from 'defu';
-import browser from 'webextension-polyfill';
-import emitter from '@/lib/mitt';
-import { useDialog } from '@/composable/dialog';
-import { useShortcut } from '@/composable/shortcut';
-import { sendMessage } from '@/utils/message';
-import { exportWorkflow, convertWorkflow } from '@/utils/workflowData';
-import { tasks } from '@/utils/shared';
-import { fetchApi } from '@/utils/api';
-import {
-  debounce,
-  isObject,
-  objectHasKey,
-  parseJSON,
-  throttle,
-} from '@/utils/helper';
-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';
-import WorkflowActions from '@/components/newtab/workflow/WorkflowActions.vue';
-import WorkflowBuilder from '@/components/newtab/workflow/WorkflowBuilder.vue';
-import WorkflowSettings from '@/components/newtab/workflow/WorkflowSettings.vue';
-import WorkflowEditBlock from '@/components/newtab/workflow/WorkflowEditBlock.vue';
-import WorkflowDataTable from '@/components/newtab/workflow/WorkflowDataTable.vue';
-import WorkflowGlobalData from '@/components/newtab/workflow/WorkflowGlobalData.vue';
-import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
-import WorkflowSharedActions from '@/components/newtab/workflow/WorkflowSharedActions.vue';
-import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
-
-const { t } = useI18n();
-const store = useStore();
-const route = useRoute();
-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';
-
-const editor = shallowRef(null);
-const activeTab = shallowRef(activeTabQuery);
-
-const autocomplete = reactive({
-  cache: null,
-  dataChanged: false,
-});
-const workflowPayload = reactive({
-  data: {},
-  isUpdating: false,
-});
-const state = reactive({
-  blockData: {},
-  modalName: '',
-  drawflow: null,
-  showModal: false,
-  showSidebar: true,
-  isEditBlock: false,
-  isLoadingFlow: false,
-  isDataChanged: false,
-});
-const workflowData = reactive({
-  isHost: false,
-  hasLocal: true,
-  hasShared: false,
-  isChanged: false,
-  isUpdating: false,
-  loadingHost: false,
-  isUnpublishing: false,
-  changingKeys: new Set(),
-  active: route.query.shared ? 'shared' : 'local',
-});
-const renameModal = reactive({
-  show: false,
-  name: '',
-  description: '',
-});
-
-const workflowId = route.params.id;
-const workflowModals = {
-  table: {
-    icon: 'riKey2Line',
-    width: 'max-w-2xl',
-    component: WorkflowDataTable,
-    title: t('workflow.table.title'),
-    docs: 'https://docs.automa.site/api-reference/table.html',
-    events: {
-      change() {
-        autocomplete.dataChanged = true;
-      },
-    },
-  },
-  'workflow-share': {
-    icon: 'riShareLine',
-    component: WorkflowShare,
-    attrs: {
-      blur: true,
-      persist: true,
-      customContent: true,
-    },
-    events: {
-      close() {
-        state.showModal = false;
-        state.modalName = '';
-      },
-      publish() {
-        workflowData.hasShared = true;
-
-        state.showModal = false;
-        state.modalName = '';
-      },
-    },
-  },
-  'global-data': {
-    width: 'max-w-2xl',
-    icon: 'riDatabase2Line',
-    component: WorkflowGlobalData,
-    title: t('common.globalData'),
-    docs: 'https://docs.automa.site/api-reference/global-data.html',
-  },
-  settings: {
-    width: 'max-w-2xl',
-    icon: 'riSettings3Line',
-    component: WorkflowSettings,
-    title: t('common.settings'),
-    attrs: {
-      customContent: true,
-    },
-    events: {
-      close() {
-        state.showModal = false;
-        state.modalName = '';
-      },
-    },
-  },
-};
-
-const hostWorkflow = computed(() => store.state.hostWorkflows[workflowId]);
-const sharedWorkflow = computed(() => store.state.sharedWorkflows[workflowId]);
-const localWorkflow = computed(() => Workflow.find(workflowId));
-const workflow = computed(() =>
-  workflowData.active === 'local' ? localWorkflow.value : sharedWorkflow.value
-);
-const workflowModal = computed(() => workflowModals[state.modalName] || {});
-const workflowState = computed(() =>
-  store.getters.getWorkflowState(workflowId)
-);
-
-const updateBlockData = debounce((data) => {
-  let payload = data;
-
-  state.blockData.data = data;
-  state.isDataChanged = true;
-  autocomplete.dataChanged = true;
-
-  if (state.blockData.isInGroup) {
-    payload = { itemId: state.blockData.itemId, data };
-  } else {
-    editor.value.updateNodeDataFromId(state.blockData.blockId, data);
-  }
-
-  const inputEl = document.querySelector(
-    `#node-${state.blockData.blockId} input.trigger`
-  );
-
-  if (inputEl)
-    inputEl.dispatchEvent(
-      new CustomEvent('change', { detail: toRaw(payload) })
-    );
-}, 250);
-const executeWorkflow = throttle(() => {
-  if (editor.value.getNodesFromName('trigger').length === 0) {
-    /* eslint-disable-next-line */
-    toast.error(t('message.noTriggerBlock'));
-    return;
-  }
-
-  const payload = {
-    ...workflow.value,
-    isTesting: state.isDataChanged,
-    drawflow: JSON.stringify(editor.value.export()),
-  };
-
-  sendMessage('workflow:execute', payload, 'background');
-}, 300);
-
-async function updateHostedWorkflow() {
-  if (!store.state.user || workflowPayload.isUpdating) return;
-
-  const { backupIds } = await browser.storage.local.get('backupIds');
-  const isBackup = (backupIds || []).includes(workflowId);
-  const isExists = Workflow.query().where('id', workflowId).exists();
-
-  if (
-    (!isBackup && !workflowData.isHost) ||
-    !isExists ||
-    Object.keys(workflowPayload.data).length === 0
-  )
-    return;
-
-  workflowPayload.isUpdating = true;
-
-  try {
-    if (workflowPayload.data.drawflow) {
-      workflowPayload.data.drawflow = parseJSON(
-        workflowPayload.data.drawflow,
-        null
-      );
-    }
-
-    const response = await fetchApi(`/me/workflows/${workflowId}`, {
-      method: 'PUT',
-      keepalive: true,
-      body: JSON.stringify({
-        workflow: workflowPayload.data,
-      }),
-    });
-
-    if (!response.ok) throw new Error(response.statusText);
-
-    if (isBackup) {
-      const result = await response.json();
-
-      if (result.updatedAt) {
-        await browser.storage.local.set({ lastBackup: result.updatedAt });
-      }
-    }
-
-    workflowPayload.data = {};
-    workflowPayload.isUpdating = false;
-  } catch (error) {
-    console.error(error);
-    workflowPayload.isUpdating = false;
-  }
-}
-function unpublishSharedWorkflow() {
-  dialog.confirm({
-    title: t('workflow.unpublish.title'),
-    body: t('workflow.unpublish.body', { name: workflow.value.name }),
-    okVariant: 'danger',
-    okText: t('workflow.unpublish.button'),
-    async onConfirm() {
-      try {
-        workflowData.isUnpublishing = true;
-
-        const response = await fetchApi(`/me/workflows/shared/${workflowId}`, {
-          method: 'DELETE',
-        });
-
-        if (response.status !== 200) {
-          throw new Error(response.statusText);
-        }
-
-        store.commit('deleteStateNested', `sharedWorkflows.${workflowId}`);
-        sessionStorage.setItem(
-          'shared-workflows',
-          JSON.stringify(store.state.sharedWorkflows)
-        );
-
-        if (workflowData.hasLocal) {
-          workflowData.active = 'local';
-          workflowData.hasShared = false;
-        } else {
-          router.push('/');
-        }
-
-        workflowData.isUnpublishing = false;
-      } catch (error) {
-        console.error(error);
-        workflowData.isUnpublishing = false;
-        toast.error(t('message.somethingWrong'));
-      }
-    },
-  });
-}
-async function saveUpdatedSharedWorkflow() {
-  try {
-    workflowData.isUpdating = true;
-
-    const payload = {};
-    workflowData.changingKeys.forEach((key) => {
-      if (key === 'drawflow') {
-        payload.drawflow = JSON.parse(workflow.value.drawflow);
-      } else {
-        payload[key] = workflow.value[key];
-      }
-    });
-
-    const url = `/me/workflows/shared/${workflowId}`;
-    const response = await fetchApi(url, {
-      method: 'PUT',
-      body: JSON.stringify(payload),
-    });
-
-    if (response.status !== 200) {
-      toast.error(t('message.somethingWrong'));
-      throw new Error(response.statusText);
-    }
-
-    workflowData.isChanged = false;
-    workflowData.changingKeys.clear();
-    sessionStorage.setItem(
-      'shared-workflows',
-      JSON.stringify(store.state.sharedWorkflows)
-    );
-
-    workflowData.isUpdating = false;
-  } catch (error) {
-    console.error(error);
-    workflowData.isUpdating = false;
-  }
-}
-function updateSharedWorkflow(data = {}) {
-  Object.keys(data).forEach((key) => {
-    workflowData.changingKeys.add(key);
-  });
-
-  store.commit('updateStateNested', {
-    path: `sharedWorkflows.${workflowId}`,
-    value: {
-      ...workflow.value,
-      ...data,
-    },
-  });
-  workflowData.isChanged = true;
-}
-function fetchLocalWorkflow() {
-  const localData = {};
-  const keys = [
-    'drawflow',
-    'name',
-    'description',
-    'icon',
-    'globalData',
-    'dataColumns',
-    'table',
-    'settings',
-  ];
-
-  keys.forEach((key) => {
-    if (localWorkflow.value.isProtected && key === 'drawflow') return;
-
-    localData[key] = localWorkflow.value[key];
-  });
-
-  if (localData.drawflow) {
-    editor.value.import(JSON.parse(localData.drawflow), false);
-  }
-
-  updateSharedWorkflow(localData);
-}
-function insertToLocal() {
-  const copy = {
-    ...workflow.value,
-    createdAt: Date.now(),
-    version: browser.runtime.getManifest().version,
-  };
-
-  Workflow.insert({
-    data: copy,
-  }).then(() => {
-    workflowData.hasLocal = true;
-  });
-}
-async function setAsHostWorkflow(isHost) {
-  if (!store.state.user || isHost === 'auth') {
-    dialog.custom('auth', {
-      title: t('auth.title'),
-    });
-    return;
-  }
-
-  workflowData.loadingHost = true;
-
-  try {
-    let url = '/me/workflows';
-    let payload = {};
-
-    if (isHost) {
-      const workflowPaylod = convertWorkflow(workflow.value, ['id']);
-      workflowPaylod.drawflow = parseJSON(workflow.value.drawflow, null);
-      delete workflowPaylod.extVersion;
-
-      url += `/host`;
-      payload = {
-        method: 'POST',
-        body: JSON.stringify({
-          workflow: workflowPaylod,
-        }),
-      };
-    } else {
-      url += `?id=${workflowId}&type=host`;
-      payload.method = 'DELETE';
-    }
-
-    const response = await fetchApi(url, payload);
-    const result = await response.json();
-
-    if (!response.ok) {
-      const error = new Error(response.statusText);
-      error.data = result.data;
-
-      throw error;
-    }
-
-    if (isHost) {
-      store.commit('updateStateNested', {
-        path: `hostWorkflows.${workflowId}`,
-        value: result,
-      });
-    } else {
-      store.commit('deleteStateNested', `hostWorkflows.${workflowId}`);
-    }
-
-    const userWorkflows = parseJSON('user-workflows', {
-      backup: [],
-      hosted: {},
-    });
-    userWorkflows.hosted = store.state.hostWorkflows;
-    sessionStorage.setItem('user-workflows', JSON.stringify(userWorkflows));
-
-    workflowData.isHost = isHost;
-    workflowData.loadingHost = false;
-  } catch (error) {
-    console.error(error);
-    workflowData.loadingHost = false;
-    toast.error(error.message);
-  }
-}
-function shareWorkflow() {
-  if (workflowData.hasShared) {
-    workflowData.active = 'shared';
-
-    return;
-  }
-
-  if (store.state.user) {
-    state.modalName = 'workflow-share';
-    state.showModal = true;
-  } else {
-    dialog.custom('auth', {
-      title: t('auth.title'),
-    });
-  }
-}
-function deleteLog(logId) {
-  dbLogs.items.where('id').equals(logId).delete();
-}
-function workflowExporter() {
-  const currentWorkflow = { ...workflow.value };
-
-  if (currentWorkflow.isProtected) {
-    currentWorkflow.drawflow = decryptFlow(
-      workflow.value,
-      getWorkflowPass(workflow.value.pass)
-    );
-    delete currentWorkflow.isProtected;
-  }
-
-  exportWorkflow(currentWorkflow);
-}
-function toggleSidebar() {
-  state.showSidebar = !state.showSidebar;
-  localStorage.setItem('workflow:sidebar', state.showSidebar);
-}
-function deleteBlock(id) {
-  if (!state.isEditBlock) return;
-
-  const isGroupBlock =
-    isObject(id) && id.isInGroup && id.itemId === state.blockData.itemId;
-  const isEditedBlock = state.blockData.blockId === id;
-
-  if (isEditedBlock || isGroupBlock) {
-    state.isEditBlock = false;
-    state.blockData = {};
-  }
-
-  state.isDataChanged = true;
-  autocomplete.dataChanged = true;
-}
-function updateWorkflow(data) {
-  if (workflowData.active === 'shared') return;
-
-  return Workflow.update({
-    where: workflowId,
-    data,
-  }).then((event) => {
-    delete data.id;
-    delete data.pass;
-    delete data.logs;
-    delete data.trigger;
-    delete data.createdAt;
-    delete data.isDisabled;
-    delete data.isProtected;
-
-    workflowPayload.data = { ...workflowPayload.data, ...data };
-
-    return event;
-  });
-}
-function updateNameAndDesc() {
-  updateWorkflow({
-    name: renameModal.name,
-    description: renameModal.description.slice(0, 300),
-  }).then(() => {
-    Object.assign(renameModal, {
-      show: false,
-      name: '',
-      description: '',
-    });
-  });
-}
-async function saveWorkflow() {
-  if (workflowData.active === 'shared') return;
-
-  try {
-    const flow = JSON.stringify(editor.value.export());
-    const [triggerBlockId] = editor.value.getNodesFromName('trigger');
-    const triggerBlock = editor.value.getNodeFromId(triggerBlockId);
-
-    updateWorkflow({ drawflow: flow, trigger: triggerBlock?.data }).then(() => {
-      if (triggerBlock) {
-        workflowTrigger.register(workflowId, triggerBlock);
-      }
-
-      state.isDataChanged = false;
-    });
-  } catch (error) {
-    console.error(error);
-  }
-}
-function editBlock(data) {
-  if (workflowData.active === 'shared') return;
-
-  state.isEditBlock = true;
-  state.showSidebar = true;
-  state.blockData = defu(data, tasks[data.id] || {});
-
-  if (data.id === 'wait-connections') {
-    const node = editor.value.getNodeFromId(data.blockId);
-    const connections = node.inputs.input_1.connections.map((input) => {
-      const inputNode = editor.value.getNodeFromId(input.node);
-      const nodeDesc = inputNode.data.description;
-
-      let name = t(`workflow.blocks.${inputNode.name}.name`);
-
-      if (nodeDesc) name += ` (${nodeDesc})`;
-
-      return {
-        name,
-        id: input.node,
-      };
-    });
-
-    state.blockData.connections = connections;
-  }
-}
-function handleEditorDataChanged() {
-  state.isDataChanged = true;
-  autocomplete.dataChanged = true;
-}
-function deleteWorkflow() {
-  dialog.confirm({
-    title: t('workflow.delete'),
-    okVariant: 'danger',
-    body: t('message.delete', { name: workflow.value.name }),
-    onConfirm: () => {
-      Workflow.delete(route.params.id).then(() => {
-        router.replace('/workflows');
-      });
-    },
-  });
-}
-function renameWorkflow() {
-  Object.assign(renameModal, {
-    show: true,
-    name: workflow.value.name,
-    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,
-  updateWorkflow,
-  showDataColumnsModal: (show = true) => {
-    state.showModal = show;
-    state.modalName = 'table';
-  },
-});
-
-watch(activeTab, (value) => {
-  router.replace({ ...route, query: { tab: value } });
-});
-watch(() => workflowPayload.data, throttle(updateHostedWorkflow, 5000), {
-  deep: true,
-});
-watch(
-  () => workflowData.active,
-  (value) => {
-    if (value === 'shared') {
-      state.isEditBlock = false;
-      state.blockData = {};
-    }
-
-    let drawflow = parseJSON(workflow.value.drawflow, null);
-
-    if (!drawflow?.drawflow?.Home) {
-      drawflow = { drawflow: { Home: { data: {} } } };
-    }
-
-    editor.value.import(drawflow, false);
-  }
-);
-watch(
-  () => store.state.userDataRetrieved,
-  () => {
-    if (workflowData.hasShared) return;
-
-    workflowData.hasShared = objectHasKey(
-      store.state.sharedWorkflows,
-      workflowId
-    );
-    workflowData.isHost = objectHasKey(store.state.hostWorkflows, workflowId);
-  }
-);
-
-onBeforeRouteLeave(() => {
-  updateHostedWorkflow();
-
-  if (!state.isDataChanged) return;
-
-  const answer = window.confirm(t('message.notSaved'));
-
-  if (!answer) return false;
-});
-onMounted(() => {
-  const isWorkflowExists = Workflow.query().where('id', workflowId).exists();
-
-  workflowData.hasLocal = isWorkflowExists;
-  workflowData.hasShared = objectHasKey(
-    store.state.sharedWorkflows,
-    workflowId
-  );
-  workflowData.isHost = objectHasKey(store.state.hostWorkflows, workflowId);
-
-  const dontHaveLocal = !isWorkflowExists && workflowData.active === 'local';
-  const dontHaveShared =
-    !workflowData.hasShared && workflowData.active === 'shared';
-
-  if (dontHaveLocal || dontHaveShared) {
-    router.push('/workflows');
-    return;
-  }
-
-  state.drawflow = workflow.value.drawflow;
-  state.showSidebar =
-    JSON.parse(localStorage.getItem('workflow:sidebar')) ?? true;
-
-  window.onbeforeunload = () => {
-    updateHostedWorkflow();
-
-    if (state.isDataChanged) {
-      return t('message.notSaved');
-    }
-  };
-
-  emitter.on('editor:edit-block', editBlock);
-  emitter.on('editor:delete-block', deleteBlock);
-  emitter.on('editor:data-changed', handleEditorDataChanged);
-});
-onUnmounted(() => {
-  window.onbeforeunload = null;
-  emitter.off('editor:edit-block', editBlock);
-  emitter.off('editor:delete-block', deleteBlock);
-  emitter.off('editor:data-changed', handleEditorDataChanged);
-});
-</script>
-<style>
-.ghost-task {
-  height: 40px;
-  @apply bg-gray-200;
-}
-.ghost-task:not(.workflow-task) * {
-  display: none;
-}
-
-.parent-drawflow.is-shared .drawflow-node * {
-  pointer-events: none;
-}
-.parent-drawflow.is-shared .drawflow-node .move-to-group,
-.parent-drawflow.is-shared .drawflow-node .menu {
-  display: none;
-}
-</style>

+ 18 - 16
src/newtab/pages/workflows/[id].vue

@@ -58,7 +58,7 @@
           :editor="editor"
           :workflow="workflow"
           :is-data-changed="state.dataChanged"
-          @save="state.dataChanged = false"
+          @update="onActionUpdated"
           @modal="(modalState.name = $event), (modalState.show = true)"
         />
       </div>
@@ -141,6 +141,7 @@ import { useShortcut } from '@/composable/shortcut';
 import { tasks } from '@/utils/shared';
 import { debounce, parseJSON, throttle } from '@/utils/helper';
 import { fetchApi } from '@/utils/api';
+import browser from 'webextension-polyfill';
 import EditorUtils from '@/utils/EditorUtils';
 import convertWorkflowData from '@/utils/convertWorkflowData';
 import WorkflowShare from '@/components/newtab/workflow/WorkflowShare.vue';
@@ -185,11 +186,11 @@ const autocompleteState = reactive({
   cache: new Map(),
   dataChanged: false,
 });
-
 const workflowPayload = {
   data: {},
   isUpdating: false,
 };
+
 const workflowModals = {
   table: {
     icon: 'riKey2Line',
@@ -241,9 +242,7 @@ const workflowModals = {
   },
 };
 
-const workflow = computed(() =>
-  workflowStore.getById('local', route.params.id)
-);
+const workflow = computed(() => workflowStore.getById(route.params.id));
 const activeWorkflowModal = computed(
   () => workflowModals[modalState.name] || {}
 );
@@ -251,6 +250,7 @@ const activeWorkflowModal = computed(
 const updateBlockData = debounce((data) => {
   const node = editor.value.getNode.value(editState.blockData.blockId);
   node.data = data;
+  editState.blockData.data = data;
   state.dataChanged = true;
   // let payload = data;
 
@@ -276,14 +276,13 @@ const updateBlockData = debounce((data) => {
 const updateHostedWorkflow = throttle(async () => {
   if (!userStore.user || workflowPayload.isUpdating) return;
 
-  const isHosted = workflowStore.userHosted[route.param.id];
-  const isBackup = (userStore.backupIds || []).includes(route.params.id);
-  const isExists = Boolean(workflow.value);
+  const isHosted = userStore.hostedWorkflows[route.params.id];
+  const isBackup = userStore.backupIds.includes(route.params.id);
+  const workflowExist = workflowStore.getById(route.params.id);
 
   if (
     (!isBackup && !isHosted) ||
-    !isExists ||
-    Object.keys(workflowPayload.data).length === 0
+    (workflowExist && Object.keys(workflowPayload.data).length === 0)
   )
     return;
 
@@ -346,12 +345,17 @@ function initEditBlock(data) {
   editState.blockData = { ...data, editComponent };
 }
 function updateWorkflow(data) {
-  workflowStore.updateWorkflow({
+  workflowStore.update({
     data,
-    location: 'local',
     id: route.params.id,
   });
   workflowPayload.data = { ...workflowPayload.data, ...data };
+  updateHostedWorkflow();
+}
+function onActionUpdated({ data, changedIndicator }) {
+  state.dataChanged = changedIndicator;
+  workflowPayload.data = { ...workflowPayload.data, ...data };
+  updateHostedWorkflow();
 }
 function onEditorInit(instance) {
   editor.value = instance;
@@ -555,7 +559,6 @@ function onKeydown({ ctrlKey, metaKey, key }) {
 
   if (command('c')) {
     copySelectedElements();
-    console.log(store.copiedEls);
   } else if (command('v')) {
     pasteCopiedElements();
   }
@@ -565,7 +568,7 @@ function onKeydown({ ctrlKey, metaKey, key }) {
 onBeforeRouteLeave(() => {
   updateHostedWorkflow();
 
-  if (!state.dataChange) return;
+  if (!state.dataChanged) return;
 
   const confirm = window.confirm(t('message.notSaved'));
 
@@ -587,10 +590,9 @@ onMounted(() => {
   window.onbeforeunload = () => {
     updateHostedWorkflow();
 
-    if (state.dataChange) {
+    if (state.dataChanged) {
       return t('message.notSaved');
     }
-    return true;
   };
   window.addEventListener('keydown', onKeydown);
 });

+ 0 - 12
src/newtab/router.js

@@ -4,8 +4,6 @@ import Workflows from './pages/Workflows.vue';
 import WorkflowHost from './pages/workflows/Host.vue';
 import WorkflowDetails from './pages/workflows/[id].vue';
 import ScheduledWorkflow from './pages/ScheduledWorkflow.vue';
-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';
@@ -48,16 +46,6 @@ const routes = [
     path: '/workflows/:id/host',
     component: WorkflowHost,
   },
-  {
-    name: 'collections',
-    path: '/collections',
-    component: Collections,
-  },
-  {
-    name: 'collections-details',
-    path: '/collections/:id',
-    component: CollectionsDetails,
-  },
   {
     name: 'logs',
     path: '/logs',

+ 4 - 4
src/popup/App.vue

@@ -6,9 +6,9 @@
 </template>
 <script setup>
 import { ref, onMounted } from 'vue';
-import { useStore } from 'vuex';
 import { useRouter } from 'vue-router';
 import browser from 'webextension-polyfill';
+import { useStore } from '@/stores/main';
 import { loadLocaleMessages, setI18nLanguage } from '@/lib/vueI18n';
 
 const store = useStore();
@@ -22,9 +22,9 @@ browser.storage.local.get('isRecording').then(({ isRecording }) => {
 
 onMounted(async () => {
   try {
-    await store.dispatch('retrieve');
-    await loadLocaleMessages(store.state.settings.locale, 'popup');
-    await setI18nLanguage(store.state.settings.locale);
+    await store.loadSettings();
+    await loadLocaleMessages(store.settings.locale, 'popup');
+    await setI18nLanguage(store.settings.locale);
 
     retrieved.value = true;
   } catch (error) {

+ 2 - 2
src/popup/index.js

@@ -1,7 +1,7 @@
 import { createApp } from 'vue';
 import App from './App.vue';
 import router from './router';
-import store from '../store';
+import pinia from '../lib/pinia';
 import compsUi from '../lib/compsUi';
 import vueI18n from '../lib/vueI18n';
 import vRemixicon, { icons } from '../lib/vRemixicon';
@@ -10,9 +10,9 @@ import '../assets/css/fonts.css';
 
 createApp(App)
   .use(router)
-  .use(store)
   .use(compsUi)
   .use(vueI18n)
+  .use(pinia)
   .use(vRemixicon, icons)
   .mount('#app');
 

+ 33 - 38
src/popup/pages/Home.vue

@@ -1,10 +1,10 @@
 <template>
   <div
-    :class="[workflowHostKeys.length === 0 ? 'h-48' : 'h-56']"
+    :class="[hostedWorkflowStore.toArray.length === 0 ? 'h-48' : 'h-56']"
     class="bg-accent rounded-b-2xl absolute top-0 left-0 w-full"
   ></div>
   <div
-    :class="[workflowHostKeys.length === 0 ? 'mb-6' : 'mb-2']"
+    :class="[hostedWorkflowStore.toArray.length === 0 ? 'mb-6' : 'mb-2']"
     class="dark placeholder-black relative z-10 text-white px-5 pt-8"
   >
     <div class="flex items-center mb-4">
@@ -46,7 +46,7 @@
       />
     </div>
     <ui-tabs
-      v-if="workflowHostKeys.length > 0"
+      v-if="hostedWorkflowStore.toArray.length > 0"
       v-model="state.activeTab"
       fill
       class="mt-1"
@@ -57,7 +57,7 @@
     </ui-tabs>
   </div>
   <div class="px-5 pb-5 space-y-2">
-    <ui-card v-if="Workflow.all().length === 0" class="text-center">
+    <ui-card v-if="workflowStore.getWorkflows.length === 0" class="text-center">
       <img src="@/assets/svg/alien.svg" />
       <p class="font-semibold">{{ t('message.empty') }}</p>
       <ui-button
@@ -73,7 +73,7 @@
       :key="workflow.id"
       :workflow="workflow"
       :tab="state.activeTab"
-      @details="openDashboard(`/workflows/${$event.id}`)"
+      @details="openWorkflowPage"
       @update="updateWorkflow(workflow.id, $event)"
       @execute="executeWorkflow"
       @rename="renameWorkflow"
@@ -108,12 +108,12 @@
 <script setup>
 import { computed, onMounted, shallowReactive } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { useStore } from 'vuex';
 import browser from 'webextension-polyfill';
 import { useDialog } from '@/composable/dialog';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { sendMessage } from '@/utils/message';
-import Workflow from '@/models/workflow';
+import { useWorkflowStore } from '@/stores/workflow';
+import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import HomeWorkflowCard from '@/components/popup/home/HomeWorkflowCard.vue';
 import HomeStartRecording from '@/components/popup/home/HomeStartRecording.vue';
 
@@ -124,8 +124,9 @@ const recordingCardHeight = {
 };
 
 const { t } = useI18n();
-const store = useStore();
 const dialog = useDialog();
+const workflowStore = useWorkflowStore();
+const hostedWorkflowStore = useHostedWorkflowStore();
 
 useGroupTooltip();
 
@@ -137,41 +138,30 @@ const state = shallowReactive({
   newRecordingModal: false,
 });
 
-const workflowHostKeys = computed(() => Object.keys(store.state.workflowHosts));
-const workflowHosts = computed(() => {
+const hostedWorkflows = computed(() => {
   if (state.activeTab !== 'host') return [];
 
-  return workflowHostKeys.value.reduce((acc, key) => {
-    const workflow = store.state.workflowHosts[key];
-    const isMatch = workflow.name
-      .toLocaleLowerCase()
-      .includes(state.query.toLocaleLowerCase());
-
-    if (isMatch) acc.push({ ...workflow, id: key });
-
-    return acc;
-  }, []);
+  return hostedWorkflowStore.toArray.filter((workflow) =>
+    workflow.name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())
+  );
 });
 const localWorkflows = computed(() => {
   if (state.activeTab !== 'local') return [];
 
-  return Workflow.query()
-    .where(({ name }) =>
-      name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())
-    )
-    .orderBy('createdAt', 'desc')
-    .get();
+  return workflowStore.getWorkflows.filter(({ name }) =>
+    name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())
+  );
 });
 const workflows = computed(() =>
-  state.activeTab === 'local' ? localWorkflows.value : workflowHosts.value
+  state.activeTab === 'local' ? localWorkflows.value : hostedWorkflows.value
 );
 
 function executeWorkflow(workflow) {
   sendMessage('workflow:execute', workflow, 'background');
 }
 function updateWorkflow(id, data) {
-  return Workflow.update({
-    where: id,
+  return workflowStore.update({
+    id,
     data,
   });
 }
@@ -193,15 +183,9 @@ function deleteWorkflow({ id, name }) {
     body: t('message.delete', { name }),
     onConfirm: () => {
       if (state.activeTab === 'local') {
-        Workflow.delete(id);
+        workflowStore.delete(id);
       } else {
-        store.commit('deleteStateNested', `workflowHosts.${id}`);
-
-        if (workflowHostKeys.value.length === 0) {
-          state.activeTab = 'local';
-        }
-
-        browser.storage.local.set({ workflowHosts: store.state.workflowHosts });
+        hostedWorkflowStore.delete(id);
       }
     },
   });
@@ -273,11 +257,22 @@ async function recordWorkflow(options = {}) {
     console.error(error);
   }
 }
+function openWorkflowPage({ id, hostId }) {
+  let url = `/workflows/${id}`;
+
+  if (state.activeTab === 'host') {
+    url = `/workflows/${hostId}/host`;
+  }
+
+  openDashboard(url);
+}
 
 onMounted(async () => {
   const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
-
   state.haveAccess = /^(https?)/.test(tab.url);
+
+  await workflowStore.loadData();
+  await hostedWorkflowStore.loadData();
 });
 </script>
 <style>

+ 159 - 158
src/popup/pages/Recording.vue

@@ -50,17 +50,18 @@
   </div>
 </template>
 <script setup>
+/* eslint-disable */
 import { onMounted, reactive, toRaw } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { useRouter } from 'vue-router';
+// import { useRouter } from 'vue-router';
 import { nanoid } from 'nanoid';
 import defu from 'defu';
 import browser from 'webextension-polyfill';
 import { tasks } from '@/utils/shared';
-import Workflow from '@/models/workflow';
+// import Workflow from '@/models/workflow';
 
 const { t } = useI18n();
-const router = useRouter();
+// const router = useRouter();
 
 const state = reactive({
   name: '',
@@ -69,161 +70,161 @@ const state = reactive({
   isGenerating: false,
 });
 
-function generateDrawflow(startBlock, startBlockData) {
-  let nextNodeId = nanoid();
-  const triggerId = startBlock?.id || nanoid();
-  let prevNodeId = startBlock?.id || triggerId;
-
-  const nodes = {
-    [triggerId]: {
-      pos_x: 50,
-      pos_y: 300,
-      inputs: {},
-      outputs: {
-        output_1: {
-          connections: [{ node: nextNodeId, output: 'input_1' }],
-        },
-      },
-      id: triggerId,
-      typenode: 'vue',
-      name: 'trigger',
-      class: 'trigger',
-      html: 'BlockBasic',
-      data: tasks.trigger.data,
-      ...startBlockData,
-    },
-  };
-
-  if (startBlock) {
-    nodes[triggerId].outputs[startBlock.output]?.connections.push({
-      node: nextNodeId,
-      output: 'input_1',
-    });
-  }
-
-  const position = {
-    y: startBlockData ? startBlockData.pos_y + 50 : 300,
-    x: startBlockData ? startBlockData.pos_x + 120 : 260,
-  };
-  const groups = {};
-
-  state.flows.forEach((block, index) => {
-    if (block.groupId) {
-      if (!groups[block.groupId]) groups[block.groupId] = [];
-
-      groups[block.groupId].push({
-        id: block.id,
-        itemId: nanoid(),
-        data: defu(block.data, tasks[block.id].data),
-      });
-
-      const nextNodeInGroup = state.flows[index + 1]?.groupId;
-      if (nextNodeInGroup) return;
-
-      block.id = 'blocks-group';
-      block.data = { blocks: groups[block.groupId] };
-
-      delete groups[block.groupId];
-    }
-
-    const node = {
-      id: nextNodeId,
-      name: block.id,
-      class: block.id,
-      typenode: 'vue',
-      pos_x: position.x,
-      pos_y: position.y,
-      inputs: { input_1: { connections: [] } },
-      outputs: { output_1: { connections: [] } },
-      html: tasks[block.id].component,
-      data: defu(block.data, tasks[block.id].data),
-    };
-
-    node.inputs.input_1.connections.push({
-      node: prevNodeId,
-      input: index === 0 && startBlock ? startBlock.output : 'output_1',
-    });
-
-    const isLastIndex = index === state.flows.length - 1;
-
-    prevNodeId = nextNodeId;
-    nextNodeId = nanoid();
-
-    if (!isLastIndex) {
-      node.outputs.output_1.connections.push({
-        node: nextNodeId,
-        output: 'input_1',
-      });
-    }
-
-    const inNewRow = (index + 1) % 5 === 0;
-    const blockNameLen = tasks[block.id].name.length * 14 + 120;
-    position.x = inNewRow ? 50 : position.x + blockNameLen;
-    position.y = inNewRow ? position.y + 150 : position.y;
-
-    nodes[node.id] = node;
-  });
-
-  if (startBlock) return nodes;
-
-  return { drawflow: { Home: { data: nodes } } };
-}
-async function stopRecording() {
-  if (state.isGenerating) return;
-
-  try {
-    state.isGenerating = true;
-
-    if (state.flows.length !== 0) {
-      if (state.workflowId) {
-        const workflow = Workflow.find(state.workflowId);
-        const drawflow =
-          typeof workflow.drawflow === 'string'
-            ? JSON.parse(workflow.drawflow)
-            : workflow.drawflow;
-        const node = drawflow.drawflow.Home.data[state.connectFrom.id];
-        const updatedDrawflow = generateDrawflow(state.connectFrom, node);
-
-        Object.assign(drawflow.drawflow.Home.data, updatedDrawflow);
-
-        await Workflow.update({
-          where: state.workflowId,
-          data: {
-            drawflow: JSON.stringify(drawflow),
-          },
-        });
-      } else {
-        const drawflow = generateDrawflow();
-
-        await Workflow.insert({
-          data: {
-            name: state.name,
-            drawflow: JSON.stringify(drawflow),
-          },
-        });
-      }
-    }
-
-    await browser.storage.local.remove(['isRecording', 'recording']);
-    await browser.browserAction.setBadgeText({ text: '' });
-
-    const tabs = (await browser.tabs.query({})).filter((tab) =>
-      tab.url.startsWith('http')
-    );
-    Promise.allSettled(
-      tabs.map(({ id }) =>
-        browser.tabs.sendMessage(id, { type: 'recording:stop' })
-      )
-    );
-
-    state.isGenerating = false;
-
-    router.push('/');
-  } catch (error) {
-    state.isGenerating = false;
-    console.error(error);
-  }
-}
+// function generateDrawflow(startBlock, startBlockData) {
+//   let nextNodeId = nanoid();
+//   const triggerId = startBlock?.id || nanoid();
+//   let prevNodeId = startBlock?.id || triggerId;
+
+//   const nodes = {
+//     [triggerId]: {
+//       pos_x: 50,
+//       pos_y: 300,
+//       inputs: {},
+//       outputs: {
+//         output_1: {
+//           connections: [{ node: nextNodeId, output: 'input_1' }],
+//         },
+//       },
+//       id: triggerId,
+//       typenode: 'vue',
+//       name: 'trigger',
+//       class: 'trigger',
+//       html: 'BlockBasic',
+//       data: tasks.trigger.data,
+//       ...startBlockData,
+//     },
+//   };
+
+//   if (startBlock) {
+//     nodes[triggerId].outputs[startBlock.output]?.connections.push({
+//       node: nextNodeId,
+//       output: 'input_1',
+//     });
+//   }
+
+//   const position = {
+//     y: startBlockData ? startBlockData.pos_y + 50 : 300,
+//     x: startBlockData ? startBlockData.pos_x + 120 : 260,
+//   };
+//   const groups = {};
+
+//   state.flows.forEach((block, index) => {
+//     if (block.groupId) {
+//       if (!groups[block.groupId]) groups[block.groupId] = [];
+
+//       groups[block.groupId].push({
+//         id: block.id,
+//         itemId: nanoid(),
+//         data: defu(block.data, tasks[block.id].data),
+//       });
+
+//       const nextNodeInGroup = state.flows[index + 1]?.groupId;
+//       if (nextNodeInGroup) return;
+
+//       block.id = 'blocks-group';
+//       block.data = { blocks: groups[block.groupId] };
+
+//       delete groups[block.groupId];
+//     }
+
+//     const node = {
+//       id: nextNodeId,
+//       name: block.id,
+//       class: block.id,
+//       typenode: 'vue',
+//       pos_x: position.x,
+//       pos_y: position.y,
+//       inputs: { input_1: { connections: [] } },
+//       outputs: { output_1: { connections: [] } },
+//       html: tasks[block.id].component,
+//       data: defu(block.data, tasks[block.id].data),
+//     };
+
+//     node.inputs.input_1.connections.push({
+//       node: prevNodeId,
+//       input: index === 0 && startBlock ? startBlock.output : 'output_1',
+//     });
+
+//     const isLastIndex = index === state.flows.length - 1;
+
+//     prevNodeId = nextNodeId;
+//     nextNodeId = nanoid();
+
+//     if (!isLastIndex) {
+//       node.outputs.output_1.connections.push({
+//         node: nextNodeId,
+//         output: 'input_1',
+//       });
+//     }
+
+//     const inNewRow = (index + 1) % 5 === 0;
+//     const blockNameLen = tasks[block.id].name.length * 14 + 120;
+//     position.x = inNewRow ? 50 : position.x + blockNameLen;
+//     position.y = inNewRow ? position.y + 150 : position.y;
+
+//     nodes[node.id] = node;
+//   });
+
+//   if (startBlock) return nodes;
+
+//   return { drawflow: { Home: { data: nodes } } };
+// }
+// async function stopRecording() {
+//   if (state.isGenerating) return;
+
+//   try {
+//     state.isGenerating = true;
+
+//     if (state.flows.length !== 0) {
+//       if (state.workflowId) {
+//         const workflow = Workflow.find(state.workflowId);
+//         const drawflow =
+//           typeof workflow.drawflow === 'string'
+//             ? JSON.parse(workflow.drawflow)
+//             : workflow.drawflow;
+//         const node = drawflow.drawflow.Home.data[state.connectFrom.id];
+//         const updatedDrawflow = generateDrawflow(state.connectFrom, node);
+
+//         Object.assign(drawflow.drawflow.Home.data, updatedDrawflow);
+
+//         await Workflow.update({
+//           where: state.workflowId,
+//           data: {
+//             drawflow: JSON.stringify(drawflow),
+//           },
+//         });
+//       } else {
+//         const drawflow = generateDrawflow();
+
+//         await Workflow.insert({
+//           data: {
+//             name: state.name,
+//             drawflow: JSON.stringify(drawflow),
+//           },
+//         });
+//       }
+//     }
+
+//     await browser.storage.local.remove(['isRecording', 'recording']);
+//     await browser.browserAction.setBadgeText({ text: '' });
+
+//     const tabs = (await browser.tabs.query({})).filter((tab) =>
+//       tab.url.startsWith('http')
+//     );
+//     Promise.allSettled(
+//       tabs.map(({ id }) =>
+//         browser.tabs.sendMessage(id, { type: 'recording:stop' })
+//       )
+//     );
+
+//     state.isGenerating = false;
+
+//     router.push('/');
+//   } catch (error) {
+//     state.isGenerating = false;
+//     console.error(error);
+//   }
+// }
 function removeBlock(index) {
   state.flows.splice(index, 1);
 

+ 0 - 170
src/store/index.js

@@ -1,170 +0,0 @@
-import { createStore } from 'vuex';
-import objectPath from 'object-path';
-import browser from 'webextension-polyfill';
-import vuexORM from '@/lib/vuexOrm';
-import defu from 'defu';
-import * as models from '@/models';
-import { firstWorkflows } from '@/utils/shared';
-import { fetchApi } from '@/utils/api';
-import { findTriggerBlock, parseJSON } from '@/utils/helper';
-import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
-
-const store = createStore({
-  plugins: [vuexORM(models)],
-  state: () => ({
-    user: null,
-    workflowState: [],
-    backupIds: [],
-    contributors: null,
-    hostWorkflows: {},
-    sharedWorkflows: {},
-    workflowHosts: {},
-    copiedNodes: [],
-    settings: {
-      locale: 'en',
-      deleteLogAfter: 30,
-      editor: {
-        arrow: false,
-        disableCurvature: false,
-        curvature: 0.5,
-        reroute_curvature: 0.5,
-        reroute_curvature_start_end: 0.5,
-      },
-    },
-    userDataRetrieved: false,
-  }),
-  mutations: {
-    updateState(state, { key, value }) {
-      state[key] = value;
-    },
-    updateStateNested(state, { path, value }) {
-      objectPath.set(state, path, value);
-    },
-    deleteStateNested(state, path) {
-      objectPath.del(state, path);
-    },
-  },
-  getters: {
-    getWorkflowState: (state) => (id) =>
-      (state.workflowState || []).filter(
-        ({ workflowId, isInCollection }) => workflowId === id && !isInCollection
-      ),
-  },
-  actions: {
-    async retrieve({ dispatch, getters, commit, state }, keys = 'workflows') {
-      try {
-        const data = await browser.storage.local.get(keys);
-        const promises = Object.keys(data).map((entity) => {
-          const entityData = getters[`entities/${entity}/all`]();
-
-          if (entityData.length > 0) return entityData;
-
-          return dispatch('entities/create', {
-            entity,
-            data: data[entity],
-          });
-        });
-
-        const { isFirstTime, settings, workflowHosts } =
-          await browser.storage.local.get([
-            'isFirstTime',
-            'settings',
-            'workflowHosts',
-          ]);
-
-        commit('updateState', {
-          key: 'settings',
-          value: defu(settings || {}, state.settings),
-        });
-        commit('updateState', {
-          key: 'workflowHosts',
-          value: workflowHosts || {},
-        });
-
-        if (isFirstTime) {
-          await dispatch('entities/insert', {
-            entity: 'workflows',
-            data: firstWorkflows,
-          });
-          await browser.storage.local.set({
-            isFirstTime: false,
-          });
-          await dispatch('saveToStorage', 'workflows');
-        }
-
-        return await Promise.allSettled(promises);
-      } catch (error) {
-        console.error(error);
-        throw error;
-      }
-    },
-    retrieveWorkflowState({ commit }) {
-      const storedStates = localStorage.getItem('workflowState') || '{}';
-      const states = parseJSON(storedStates, {});
-
-      commit('updateState', {
-        key: 'workflowState',
-        value: Object.values(states).filter(
-          ({ isDestroyed, parentState }) =>
-            !isDestroyed && !parentState?.isCollection
-        ),
-      });
-    },
-    saveToStorage({ getters }, key) {
-      return new Promise((resolve, reject) => {
-        if (!key) {
-          reject(new Error('You need to pass the entity name'));
-          return;
-        }
-        const data = getters[`entities/${key}/all`]();
-
-        browser.storage.local
-          .set({ [key]: JSON.parse(JSON.stringify(data)) })
-          .then(() => {
-            resolve();
-          });
-      });
-    },
-    async fetchWorkflowHosts({ commit, state }, hosts) {
-      if (!hosts || hosts.length === 0) return null;
-
-      const response = await fetchApi('/workflows/hosted', {
-        method: 'POST',
-        body: JSON.stringify({ hosts }),
-      });
-
-      if (!response.ok) throw new Error(response.statusText);
-
-      const result = await response.json();
-      const newValue = JSON.parse(JSON.stringify(state.workflowHosts));
-
-      result.forEach(({ hostId, status, data }) => {
-        if (status === 'deleted') {
-          delete newValue[hostId];
-          return;
-        }
-        if (status === 'updated') {
-          const triggerBlock = findTriggerBlock(data.drawflow);
-          registerWorkflowTrigger(hostId, triggerBlock);
-
-          data.drawflow = JSON.stringify(data.drawflow);
-        }
-
-        data.hostId = hostId;
-        newValue[hostId] = data;
-      });
-
-      commit('updateState', {
-        key: 'workflowHosts',
-        value: newValue,
-      });
-      await browser.storage.local.set({
-        workflowHosts: newValue,
-      });
-
-      return newValue;
-    },
-  },
-});
-
-export default store;

+ 34 - 1
src/stores/folder.js

@@ -1,15 +1,48 @@
 import { defineStore } from 'pinia';
+import { nanoid } from 'nanoid';
 import browser from 'webextension-polyfill';
 
 export const useFolderStore = defineStore('folder', {
+  storageMap: {
+    items: 'folders',
+  },
   state: () => ({
     items: [],
+    retrieved: false,
   }),
   actions: {
+    async addFolder(name) {
+      this.items.push({
+        name,
+        id: nanoid(),
+      });
+
+      await this.saveToStorage('items');
+
+      return this.items.at(-1);
+    },
+    async deleteFolder(id) {
+      const index = this.items.findIndex((folder) => folder.id === id);
+      if (index === -1) return null;
+
+      this.items.splice(index, 1);
+      await this.saveToStorage('items');
+
+      return index;
+    },
+    async updateFolder(id, data = {}) {
+      const index = this.items.findIndex((folder) => folder.id === id);
+      if (index === -1) return null;
+
+      Object.assign(this.items[index], data);
+      await this.saveToStorage('items');
+
+      return this.items[index];
+    },
     load() {
       return browser.storage.local.get('folders').then(({ folders }) => {
         this.items = folders;
-
+        this.retrieved = true;
         return folders;
       });
     },

+ 131 - 0
src/stores/hostedWorkflow.js

@@ -0,0 +1,131 @@
+import { defineStore } from 'pinia';
+import browser from 'webextension-polyfill';
+import { fetchApi } from '@/utils/api';
+import { findTriggerBlock } from '@/utils/helper';
+import {
+  registerWorkflowTrigger,
+  cleanWorkflowTriggers,
+} from '@/utils/workflowTrigger';
+import { useUserStore } from './user';
+
+export const useHostedWorkflowStore = defineStore('hosted-workflows', {
+  storageMap: {
+    workflows: 'workflowHosts',
+  },
+  state: () => ({
+    workflows: {},
+    retrieved: false,
+  }),
+  getters: {
+    getById: (state) => (id) => state.workflows[id],
+    toArray: (state) => Object.values(state.workflows),
+  },
+  actions: {
+    async loadData() {
+      const { workflowHosts } = await browser.storage.local.get(
+        'workflowHosts'
+      );
+      this.workflows = workflowHosts || {};
+      this.retrieved = true;
+    },
+    async insert(data, idKey = 'hostId') {
+      if (Array.isArray(data)) {
+        data.forEach((item) => {
+          this.workflows[item[idKey]] = item;
+        });
+      } else {
+        this.workflows[data[idKey]] = data;
+      }
+
+      await this.saveToStorage('workflows');
+
+      return data;
+    },
+    async delete(id) {
+      delete this.workflows[id];
+
+      await this.saveToStorage('workflows');
+      await cleanWorkflowTriggers(id);
+
+      return id;
+    },
+    async update({ id, data }) {
+      if (!this.workflows[id]) return null;
+
+      Object.assign(this.workflows[id], data);
+      await this.saveToStorage('workflows');
+
+      return this.workflows[id];
+    },
+    async addHostedWorkflow(hostId) {
+      if (this.workflows[hostId]) throw new Error('exist');
+
+      const userStore = useUserStore();
+      if (!userStore.user && this.toArray.length >= 3)
+        throw new Error('rate-exceeded');
+
+      const isTheUserHost = userStore.getHostedWorkflows.some(
+        (host) => hostId === host.hostId
+      );
+      if (isTheUserHost) throw new Error('exist');
+
+      const response = await fetchApi('/workflows/hosted', {
+        method: 'POST',
+        body: JSON.stringify({ hostId }),
+      });
+      const result = await response.json();
+
+      if (!response.ok) {
+        const error = new Error(result.message);
+        error.data = result.data;
+
+        throw error;
+      }
+
+      if (result === null) throw new Error('not-found');
+
+      result.hostId = hostId;
+      result.createdAt = Date.now();
+
+      const triggerBlock = findTriggerBlock(result.drawflow);
+      await registerWorkflowTrigger(hostId, triggerBlock);
+
+      this.workflows[hostId] = result;
+      await this.saveToStorage('workflows');
+
+      return result;
+    },
+    async fetchWorkflows(ids) {
+      if (!ids || ids.length === 0) return null;
+
+      const response = await fetchApi('/workflows/hosted', {
+        method: 'POST',
+        body: JSON.stringify({ hosts: ids }),
+      });
+      const result = await response.json();
+
+      if (!response.ok) throw new Error(result.message);
+
+      const dataToReturn = [];
+
+      result.forEach(({ hostId, status, data }) => {
+        if (status === 'deleted') {
+          delete this.workflows[hostId];
+          return;
+        }
+        if (status === 'updated') {
+          const triggerBlock = findTriggerBlock(data.drawflow);
+          registerWorkflowTrigger(hostId, triggerBlock);
+        }
+
+        data.hostId = hostId;
+        dataToReturn.push(data);
+        this.workflows[hostId] = data;
+      });
+
+      await this.saveToStorage('workflows');
+
+      return dataToReturn;
+    },
+  },
+});

+ 7 - 0
src/stores/main.js

@@ -1,8 +1,12 @@
 import { defineStore } from 'pinia';
 import defu from 'defu';
 import browser from 'webextension-polyfill';
+import deepmerge from 'lodash.merge';
 
 export const useStore = defineStore('main', {
+  storageMap: {
+    settings: 'settings',
+  },
   state: () => ({
     copiedEls: {
       edges: [],
@@ -26,5 +30,8 @@ export const useStore = defineStore('main', {
         this.settings = defu(settings || {}, this.settings);
       });
     },
+    updateSettings(settings = {}) {
+      deepmerge(this.settings, settings);
+    },
   },
 });

+ 53 - 0
src/stores/sharedWorkflow.js

@@ -0,0 +1,53 @@
+import { defineStore } from 'pinia';
+import { fetchApi, cacheApi } from '@/utils/api';
+
+export const useSharedWorkflowStore = defineStore('shared-workflows', {
+  state: () => ({
+    workflows: {},
+  }),
+  getters: {
+    toArray: (state) => Object.values(state.workflows),
+    getById: (state) => (id) => state.workflows[id],
+  },
+  actions: {
+    insert(data) {
+      if (Array.isArray(data)) {
+        data.forEach((item) => {
+          this.workflows[item.id] = item;
+        });
+      } else {
+        this.workflows[data.id] = data;
+      }
+    },
+    async fetchWorkflows(useCache = true) {
+      const workflows = await cacheApi(
+        'shared-workflows',
+        async () => {
+          try {
+            const response = await fetchApi('/me/workflows/shared?data=all');
+
+            if (response.status !== 200) throw new Error(response.statusText);
+
+            const result = await response.json();
+            const sharedWorkflows = result.reduce((acc, item) => {
+              item.table = item.table || item.dataColumns || [];
+              item.createdAt = new Date(item.createdAt || Date.now()).getTime();
+
+              acc[item.id] = item;
+
+              return acc;
+            }, {});
+
+            return sharedWorkflows;
+          } catch (error) {
+            console.error(error);
+            return {};
+          }
+        },
+        useCache
+      );
+
+      this.workflows = workflows;
+    },
+  },
+});

+ 7 - 1
src/stores/user.js

@@ -7,7 +7,11 @@ export const useUserStore = defineStore('user', {
     user: null,
     backupIds: [],
     retrieved: false,
+    hostedWorkflows: {},
   }),
+  getters: {
+    getHostedWorkflows: (state) => Object.values(state.hostedWorkflows),
+  },
   actions: {
     async loadUser() {
       try {
@@ -17,7 +21,6 @@ export const useUserStore = defineStore('user', {
         if (!response.ok) throw new Error(response.message);
 
         const username = localStorage.getItem('username');
-
         if (!user || username !== user.username) {
           sessionStorage.removeItem('shared-workflows');
           sessionStorage.removeItem('user-workflows');
@@ -34,6 +37,9 @@ export const useUserStore = defineStore('user', {
 
         localStorage.setItem('username', user?.username);
 
+        const { backupIds } = await browser.storage.local.get('backupIds');
+        this.backupIds = backupIds || [];
+
         this.user = user;
       } catch (error) {
         console.error(error);

+ 168 - 105
src/stores/workflow.js

@@ -4,153 +4,216 @@ import defu from 'defu';
 import deepmerge from 'lodash.merge';
 import browser from 'webextension-polyfill';
 import { fetchApi } from '@/utils/api';
-import { firstWorkflows } from '@/utils/shared';
-import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
-import { parseJSON, objectHasKey, findTriggerBlock } from '@/utils/helper';
-
-function getWorkflowKey(state, id, location = 'local') {
-  let key = id;
-
-  if (Array.isArray(state[location])) {
-    const index = state[location].findIndex((workflow) => workflow.id === id);
-    key = index === -1 ? null : index;
-  } else {
-    key = objectHasKey(state[location]) ? key : null;
+import { firstWorkflows, tasks } from '@/utils/shared';
+import {
+  registerWorkflowTrigger,
+  cleanWorkflowTriggers,
+} from '@/utils/workflowTrigger';
+import { parseJSON, findTriggerBlock } from '@/utils/helper';
+import { useUserStore } from './user';
+
+const defaultWorkflow = (data = null) => {
+  let workflowData = {
+    id: nanoid(),
+    name: '',
+    icon: 'riGlobalLine',
+    folderId: null,
+    drawflow: {
+      edges: [],
+      position: { zoom: 1 },
+      nodes: [
+        {
+          position: {
+            x: 100,
+            y: window.innerHeight / 2,
+          },
+          id: nanoid(),
+          label: 'trigger',
+          data: tasks.trigger.data,
+          type: tasks.trigger.component,
+        },
+      ],
+    },
+    table: [],
+    dataColumns: [],
+    description: '',
+    trigger: null,
+    version: '',
+    createdAt: Date.now(),
+    isDisabled: false,
+    settings: {
+      publicId: '',
+      blockDelay: 0,
+      saveLog: true,
+      debugMode: false,
+      restartTimes: 3,
+      notification: true,
+      reuseLastState: false,
+      inputAutocomplete: true,
+      onError: 'stop-workflow',
+      executedBlockOnWeb: false,
+      insertDefaultColumn: true,
+      defaultColumnName: 'column',
+    },
+    globalData: '{\n\t"key": "value"\n}',
+  };
+
+  if (data) workflowData = defu(data, workflowData);
+
+  return workflowData;
+};
+
+function convertWorkflowsToObject(workflows) {
+  if (Array.isArray(workflows)) {
+    return workflows.reduce((acc, workflow) => {
+      acc[workflow.id] = workflow;
+
+      return acc;
+    }, {});
   }
 
-  return key;
+  return workflows;
 }
 
 export const useWorkflowStore = defineStore('workflow', {
   storageMap: {
-    local: 'workflows',
-    hosted: 'workflowHosts',
-    userHosted: 'hostWorkflow',
+    workflows: 'workflows',
   },
   state: () => ({
-    local: [],
     states: [],
-    shared: {},
-    hosted: {},
-    userHosted: {},
+    workflows: {},
     retrieved: false,
   }),
   getters: {
-    getById: (state) => (location, id) => {
-      let data = state[location];
-
-      if (!Array.isArray(data)) data = Object.values(data);
-
-      return data.find((item) => item.id === id);
-    },
+    getById: (state) => (id) => state.workflows[id],
+    getWorkflows: (state) => Object.values(state.workflows),
   },
   actions: {
-    loadLocal() {
-      return browser.storage.local
-        .get(['workflows', 'workflowHosts', 'isFirstTime'])
-        .then(({ workflows, workflowHosts, isFirstTime }) => {
-          this.hosted = workflowHosts || {};
-          this.local = isFirstTime ? firstWorkflows : workflows;
-
-          if (isFirstTime) {
-            browser.storage.local.set({
-              isFirstTime: false,
-            });
-          }
-        });
-    },
-    loadStates() {
+    async loadData() {
+      const { workflows, isFirstTime } = await browser.storage.local.get([
+        'workflows',
+        'isFirstTime',
+      ]);
+
+      const localWorkflows = isFirstTime ? firstWorkflows : workflows;
+      this.workflows = convertWorkflowsToObject(localWorkflows);
+
+      if (isFirstTime) {
+        await browser.storage.local.set({ isFirstTime: false });
+      }
+
       const storedStates = localStorage.getItem('workflowState') || '{}';
       const states = parseJSON(storedStates, {});
-
       this.states = Object.values(states).filter(
         ({ isDestroyed }) => !isDestroyed
       );
     },
-    addWorkflow(data = {}) {
-      const workflow = defu(data, {
-        id: nanoid(),
-        name: '',
-        icon: 'riGlobalLine',
-        folderId: null,
-        drawflow: { edges: [], nodes: [] },
-        table: [],
-        dataColumns: [],
-        description: '',
-        trigger: null,
-        version: '',
-        createdAt: Date.now(),
-        isDisabled: false,
-        settings: {
-          publicId: '',
-          blockDelay: 0,
-          saveLog: true,
-          debugMode: false,
-          restartTimes: 3,
-          notification: true,
-          reuseLastState: false,
-          inputAutocomplete: true,
-          onError: 'stop-workflow',
-          executedBlockOnWeb: false,
-          insertDefaultColumn: true,
-          defaultColumnName: 'column',
-        },
-        globalData: '{\n\t"key": "value"\n}',
-      });
+    async insert(data = {}) {
+      const insertedWorkflows = {};
 
-      this.local.push(workflow);
-    },
-    workflowExist(id, location = 'local') {
-      let key = id;
+      if (Array.isArray(data)) {
+        data.forEach((item) => {
+          delete item.id;
 
-      if (Array.isArray(this[location])) {
-        const index = this.local.findIndex((workflow) => workflow.id === id);
-        key = index === -1 ? null : index;
+          const workflow = defaultWorkflow(item);
+          this.workflows[workflow.id] = workflow;
+          insertedWorkflows[workflow.id] = workflow;
+        });
       } else {
-        key = objectHasKey(this[location]) ? key : null;
+        delete data.id;
+
+        const workflow = defaultWorkflow(data);
+        this.workflows[workflow.id] = workflow;
+        insertedWorkflows[workflow.id] = workflow;
       }
 
-      return Boolean(key);
+      await this.saveToStorage('workflows');
+
+      return insertedWorkflows;
     },
-    async updateWorkflow({ id, location = 'local', data = {}, deep = false }) {
-      const key = getWorkflowKey(this, id, location);
-      if (key === null) return null;
+    async update({ id, data = {}, deep = false }) {
+      if (!this.workflows[id]) return null;
 
       if (deep) {
-        deepmerge(this[location][key], data);
+        deepmerge(this.workflows[id], data);
       } else {
-        Object.assign(this[location][key], data);
+        Object.assign(this.workflows[id], data);
       }
 
-      if (this.retrieved) {
-        await this.saveToStorage(location);
-      }
+      await this.saveToStorage('workflows');
 
-      return this[location][key];
+      return this.workflows;
     },
-    deleteWorkflow(id, location = 'local') {
-      const key = getWorkflowKey(this, id, location);
-      if (key === null) return null;
+    async insertOrUpdate(data = []) {
+      const insertedData = {};
+
+      data.forEach((item) => {
+        if (this.workflows[item.id]) {
+          Object.assign(this.workflows[item.id], item);
+          insertedData[item.id] = this.workflows[item.id];
+        } else {
+          const workflow = defaultWorkflow(item);
+          this.workflows[workflow.id] = workflow;
+          insertedData[workflow.id] = workflow;
+        }
+      });
 
-      if (Array.isArray(this[location])) {
-        this[location].splice(key, 1);
+      await this.saveToStorage('workflows');
+
+      return insertedData;
+    },
+    async delete(id) {
+      if (Array.isArray(id)) {
+        id.forEach((workflowId) => {
+          delete this.workflows[workflowId];
+        });
       } else {
-        delete this[location];
+        delete this.workflows[id];
+      }
+
+      await cleanWorkflowTriggers(id);
+
+      const userStore = useUserStore();
+
+      const hostedWorkflow = userStore.hostedWorkflows[id];
+      const backupIndex = userStore.backupIds.indexOf(id);
+
+      if (hostedWorkflow || backupIndex !== -1) {
+        const response = await fetchApi(`/me/workflows?id=${id}`, {
+          method: 'DELETE',
+        });
+        const result = await response.json();
+
+        if (!response.ok) {
+          throw new Error(result.message);
+        }
+
+        if (backupIndex !== -1) {
+          userStore.backupIds.splice(backupIndex, 1);
+          await browser.storage.local.set({ backupIds: userStore.backupIds });
+        }
       }
 
+      await browser.storage.local.remove(`state:${id}`);
+
+      await this.saveToStorage('workflows');
+
       return id;
     },
-    async syncHostedWorkflows() {
-      const ids = [];
-      const userHosted = Object.values(this.userHosted);
+    async syncHostedWorkflows(hostIds = []) {
+      const ids = hostIds;
 
-      Object.keys(this.hosted).forEach((hostId) => {
-        const isItsOwn = userHosted.find((item) => item.hostId === hostId);
+      if (ids.length === 0) {
+        const userHosted = Object.values(this.userHosted);
 
-        if (isItsOwn) return;
+        Object.keys(this.hosted).forEach((hostId) => {
+          const isItsOwn = userHosted.find((item) => item.hostId === hostId);
 
-        ids.push({ hostId, updatedAt: this.hosted[hostId].updatedAt });
-      });
+          if (isItsOwn) return;
+
+          ids.push({ hostId, updatedAt: this.hosted[hostId].updatedAt });
+        });
+      }
 
       const response = await fetchApi('/workflows/hosted', {
         method: 'POST',

+ 4 - 6
src/utils/api.js

@@ -62,14 +62,14 @@ export const googleSheets = {
 };
 
 export async function cacheApi(key, callback, useCache = true) {
-  const halfAnHour = 1000 * 60 * 15;
-  const halfAnHourAgo = Date.now() - halfAnHour;
+  const tenMinutes = 1000 * 10;
+  const tenMinutesAgo = Date.now() - tenMinutes;
 
   const timerKey = `cache-time:${key}`;
   const cacheResult = parseJSON(sessionStorage.getItem(key), null);
   const cacheTime = +sessionStorage.getItem(timerKey) || Date.now();
 
-  if (useCache && cacheResult && halfAnHourAgo < cacheTime) {
+  if (useCache && cacheResult && tenMinutesAgo < cacheTime) {
     return cacheResult;
   }
 
@@ -139,9 +139,7 @@ export async function getUserWorkflows(useCache = true) {
               };
             }
 
-            if (workflow.isBackup) {
-              acc.backup.push(workflow);
-            }
+            acc.backup.push(workflow);
 
             return acc;
           },

+ 5 - 12
src/utils/dataMigration.js

@@ -1,16 +1,13 @@
 import browser from 'webextension-polyfill';
 import dbLogs from '@/db/logs';
-import convertWorkflowData from './convertWorkflowData';
 
 export default async function () {
   try {
-    const { logs, logsCtxData, migration, workflows } =
-      await browser.storage.local.get([
-        'logs',
-        'migration',
-        'workflows',
-        'logsCtxData',
-      ]);
+    const { logs, logsCtxData, migration } = await browser.storage.local.get([
+      'logs',
+      'migration',
+      'logsCtxData',
+    ]);
     const hasMigrated = migration || {};
     const backupData = {};
 
@@ -49,10 +46,6 @@ export default async function () {
       await browser.storage.local.remove('logs');
     }
 
-    if (!hasMigrated.workflows && workflows) {
-      workflows.forEach((workflow) => convertWorkflowData(workflow));
-    }
-
     await browser.storage.local.set({
       migration: hasMigrated,
       ...backupData,

+ 9 - 3
src/utils/helper.js

@@ -31,11 +31,17 @@ export function sleep(timeout = 500) {
 export function findTriggerBlock(drawflow = {}) {
   if (!drawflow) return null;
 
-  const blocks = Object.values(drawflow.drawflow?.Home?.data);
+  if (drawflow.drawflow) {
+    const blocks = Object.values(drawflow.drawflow?.Home?.data);
+    if (!blocks) return null;
 
-  if (!blocks) return null;
+    return blocks.find(({ name }) => name === 'trigger');
+  }
+  if (drawflow.nodes) {
+    return drawflow.nodes.find((node) => node.label === 'trigger');
+  }
 
-  return blocks.find(({ name }) => name === 'trigger');
+  return null;
 }
 
 export function throttle(callback, limit) {

+ 40 - 35
src/utils/workflowData.js

@@ -1,23 +1,19 @@
 import browser from 'webextension-polyfill';
-import Workflow from '@/models/workflow';
-import { parseJSON, fileSaver, openFilePicker, isObject } from './helper';
+import { useWorkflowStore } from '@/stores/workflow';
+import { parseJSON, fileSaver, openFilePicker } from './helper';
 
 export function importWorkflow(attrs = {}) {
   openFilePicker(['application/json'], attrs)
     .then((files) => {
-      const getDrawflow = ({ drawflow }) => {
-        if (isObject(drawflow)) return JSON.stringify(drawflow);
-
-        return drawflow;
-      };
       const handleOnLoadReader = ({ target }) => {
         const workflow = JSON.parse(target.result);
+        const workflowStore = useWorkflowStore();
 
         if (workflow.includedWorkflows) {
           Object.keys(workflow.includedWorkflows).forEach((workflowId) => {
-            const isWorkflowExists = Workflow.query()
-              .where('id', workflowId)
-              .exists();
+            const isWorkflowExists = Boolean(
+              workflowStore.workflows[workflowId]
+            );
 
             if (isWorkflowExists) return;
 
@@ -26,13 +22,10 @@ export function importWorkflow(attrs = {}) {
               currentWorkflow.table || currentWorkflow.dataColumns;
             delete currentWorkflow.dataColumns;
 
-            Workflow.insert({
-              data: {
-                ...currentWorkflow,
-                drawflow: getDrawflow(workflow.includedWorkflows[workflowId]),
-                id: workflowId,
-                createdAt: Date.now(),
-              },
+            workflowStore.insert({
+              ...currentWorkflow,
+              id: workflowId,
+              createdAt: Date.now(),
             });
           });
 
@@ -42,12 +35,9 @@ export function importWorkflow(attrs = {}) {
         workflow.table = workflow.table || workflow.dataColumns;
         delete workflow.dataColumns;
 
-        Workflow.insert({
-          data: {
-            ...workflow,
-            drawflow: getDrawflow(workflow),
-            createdAt: Date.now(),
-          },
+        workflowStore.insert({
+          ...workflow,
+          createdAt: Date.now(),
         });
       };
 
@@ -87,30 +77,45 @@ export function convertWorkflow(workflow, additionalKeys = []) {
 
   return content;
 }
-function findIncludedWorkflows({ drawflow }, maxDepth = 3, workflows = {}) {
+function findIncludedWorkflows(
+  { drawflow },
+  store,
+  maxDepth = 3,
+  workflows = {}
+) {
   if (maxDepth === 0) return workflows;
 
-  const blocks = parseJSON(drawflow, null)?.drawflow.Home.data;
-
+  const flow = parseJSON(drawflow, drawflow);
+  const blocks = flow?.drawflow?.Home.data ?? flow.nodes ?? null;
   if (!blocks) return workflows;
 
-  Object.values(blocks).forEach(({ data, name }) => {
-    if (name !== 'execute-workflow' || workflows[data.workflowId]) return;
+  const checkWorkflow = (type, workflowId) => {
+    if (type !== 'execute-workflow' || workflows[workflowId]) return;
 
-    const workflow = Workflow.find(data.workflowId);
-
-    if (workflow && !workflow.isProtected) {
-      workflows[data.workflowId] = convertWorkflow(workflow);
-      findIncludedWorkflows(workflow, maxDepth - 1, workflows);
+    const workflow = store.getById(workflowId);
+    if (workflow) {
+      workflows[workflowId] = convertWorkflow(workflow);
+      findIncludedWorkflows(workflow, store, maxDepth - 1, workflows);
     }
-  });
+  };
+
+  if (flow.nodes) {
+    flow.nodes.forEach((node) => {
+      checkWorkflow(node.label, node.data.workflowId);
+    });
+  } else {
+    Object.values(blocks).forEach(({ data, name }) => {
+      checkWorkflow(name, data.workflowId);
+    });
+  }
 
   return workflows;
 }
 export function exportWorkflow(workflow) {
   if (workflow.isProtected) return;
 
-  const includedWorkflows = findIncludedWorkflows(workflow);
+  const workflowStore = useWorkflowStore();
+  const includedWorkflows = findIncludedWorkflows(workflow, workflowStore);
   const content = convertWorkflow(workflow);
 
   content.includedWorkflows = includedWorkflows;

+ 2 - 2
src/utils/workflowTrigger.js

@@ -162,7 +162,6 @@ export function registerSpecificDate(workflowId, data) {
 
   if (data.date) {
     const [hour, minute] = data.time.split(':');
-
     date = dayjs(data.date).hour(hour).minute(minute).second(0).valueOf();
   }
 
@@ -229,8 +228,9 @@ export async function registerWorkflowTrigger(workflowId, { data }) {
       'keyboard-shortcut': registerKeyboardShortcut,
     };
 
-    if (triggersHandler[data.type])
+    if (triggersHandler[data.type]) {
       await triggersHandler[data.type](workflowId, data);
+    }
   } catch (error) {
     console.error(error);
   }

+ 18 - 439
yarn.lock

@@ -912,14 +912,12 @@
     "@babel/helper-validator-identifier" "^7.16.7"
     to-fast-properties "^2.0.0"
 
-"@braks/vue-flow@^0.4.24":
-  version "0.4.24"
-  resolved "https://registry.yarnpkg.com/@braks/vue-flow/-/vue-flow-0.4.24.tgz#126f6f96dc8e0fc727e81eeb8d3d1f53bfca7468"
-  integrity sha512-9yRLeQGGnTgBSdBt1euqRWeEYjnmX2gdyhfZqgizUVHLdB90cfLltScLp3C7SXBAX3cbG1HCpS22oB7+OsXTPw==
+"@braks/vue-flow@^0.4.25":
+  version "0.4.25"
+  resolved "https://registry.yarnpkg.com/@braks/vue-flow/-/vue-flow-0.4.25.tgz#3aa96d8ba102e48deceb4b082fa2b4b15c3904e5"
+  integrity sha512-kQYAOonPLBUVzbXz8nmp1PSXXQ1pq7k5L8RsiRJ6FM4hpqRWWI+KG1p6IDdcACpbwoZ07XhPq7roLFX4M8u59Q==
   dependencies:
-    "@types/d3" "^7.4.0"
     "@vueuse/core" "^8.7.5"
-    d3 "^7.4.4"
     d3-drag "^3.0.0"
     d3-selection "^3.0.0"
     d3-zoom "^3.0.0"
@@ -1497,216 +1495,6 @@
   dependencies:
     "@types/node" "*"
 
-"@types/d3-array@*":
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac"
-  integrity sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==
-
-"@types/d3-axis@*":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.1.tgz#6afc20744fa5cc0cbc3e2bd367b140a79ed3e7a8"
-  integrity sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw==
-  dependencies:
-    "@types/d3-selection" "*"
-
-"@types/d3-brush@*":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.1.tgz#ae5f17ce391935ca88b29000e60ee20452c6357c"
-  integrity sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw==
-  dependencies:
-    "@types/d3-selection" "*"
-
-"@types/d3-chord@*":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.1.tgz#54c8856c19c8e4ab36a53f73ba737de4768ad248"
-  integrity sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==
-
-"@types/d3-color@*":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.0.tgz#6594da178ded6c7c3842f3cc0ac84b156f12f2d4"
-  integrity sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==
-
-"@types/d3-contour@*":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.1.tgz#9ff4e2fd2a3910de9c5097270a7da8a6ef240017"
-  integrity sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ==
-  dependencies:
-    "@types/d3-array" "*"
-    "@types/geojson" "*"
-
-"@types/d3-delaunay@*":
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz#006b7bd838baec1511270cb900bf4fc377bbbf41"
-  integrity sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==
-
-"@types/d3-dispatch@*":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz#a1b18ae5fa055a6734cb3bd3cbc6260ef19676e3"
-  integrity sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw==
-
-"@types/d3-drag@*":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.1.tgz#fb1e3d5cceeee4d913caa59dedf55c94cb66e80f"
-  integrity sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==
-  dependencies:
-    "@types/d3-selection" "*"
-
-"@types/d3-dsv@*":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.0.tgz#f3c61fb117bd493ec0e814856feb804a14cfc311"
-  integrity sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==
-
-"@types/d3-ease@*":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.0.tgz#c29926f8b596f9dadaeca062a32a45365681eae0"
-  integrity sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==
-
-"@types/d3-fetch@*":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.1.tgz#f9fa88b81aa2eea5814f11aec82ecfddbd0b8fe0"
-  integrity sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw==
-  dependencies:
-    "@types/d3-dsv" "*"
-
-"@types/d3-force@*":
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.3.tgz#76cb20d04ae798afede1ea6e41750763ff5a9c82"
-  integrity sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA==
-
-"@types/d3-format@*":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.1.tgz#194f1317a499edd7e58766f96735bdc0216bb89d"
-  integrity sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==
-
-"@types/d3-geo@*":
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.0.2.tgz#e7ec5f484c159b2c404c42d260e6d99d99f45d9a"
-  integrity sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ==
-  dependencies:
-    "@types/geojson" "*"
-
-"@types/d3-hierarchy@*":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.0.tgz#4561bb7ace038f247e108295ef77b6a82193ac25"
-  integrity sha512-g+sey7qrCa3UbsQlMZZBOHROkFqx7KZKvUpRzI/tAp/8erZWpYq7FgNKvYwebi2LaEiVs1klhUfd3WCThxmmWQ==
-
-"@types/d3-interpolate@*":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc"
-  integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==
-  dependencies:
-    "@types/d3-color" "*"
-
-"@types/d3-path@*":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b"
-  integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==
-
-"@types/d3-polygon@*":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.0.tgz#5200a3fa793d7736fa104285fa19b0dbc2424b93"
-  integrity sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==
-
-"@types/d3-quadtree@*":
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz#433112a178eb7df123aab2ce11c67f51cafe8ff5"
-  integrity sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==
-
-"@types/d3-random@*":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.1.tgz#5c8d42b36cd4c80b92e5626a252f994ca6bfc953"
-  integrity sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==
-
-"@types/d3-scale-chromatic@*":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954"
-  integrity sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==
-
-"@types/d3-scale@*":
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.2.tgz#41be241126af4630524ead9cb1008ab2f0f26e69"
-  integrity sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==
-  dependencies:
-    "@types/d3-time" "*"
-
-"@types/d3-selection@*":
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.2.tgz#23e48a285b24063630bbe312cc0cfe2276de4a59"
-  integrity sha512-d29EDd0iUBrRoKhPndhDY6U/PYxOWqgIZwKTooy2UkBfU7TNZNpRho0yLWPxlatQrFWk2mnTu71IZQ4+LRgKlQ==
-
-"@types/d3-shape@*":
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.0.tgz#1d87a6ddcf28285ef1e5c278ca4bdbc0658f3505"
-  integrity sha512-jYIYxFFA9vrJ8Hd4Se83YI6XF+gzDL1aC5DCsldai4XYYiVNdhtpGbA/GM6iyQ8ayhSp3a148LY34hy7A4TxZA==
-  dependencies:
-    "@types/d3-path" "*"
-
-"@types/d3-time-format@*":
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz#ee7b6e798f8deb2d9640675f8811d0253aaa1946"
-  integrity sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==
-
-"@types/d3-time@*":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819"
-  integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==
-
-"@types/d3-timer@*":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.0.tgz#e2505f1c21ec08bda8915238e397fb71d2fc54ce"
-  integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==
-
-"@types/d3-transition@*":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.1.tgz#c9a96125567173d6163a6985b874f79154f4cc3d"
-  integrity sha512-Sv4qEI9uq3bnZwlOANvYK853zvpdKEm1yz9rcc8ZTsxvRklcs9Fx4YFuGA3gXoQN/c/1T6QkVNjhaRO/cWj94g==
-  dependencies:
-    "@types/d3-selection" "*"
-
-"@types/d3-zoom@*":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.1.tgz#4bfc7e29625c4f79df38e2c36de52ec3e9faf826"
-  integrity sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ==
-  dependencies:
-    "@types/d3-interpolate" "*"
-    "@types/d3-selection" "*"
-
-"@types/d3@^7.4.0":
-  version "7.4.0"
-  resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.0.tgz#fc5cac5b1756fc592a3cf1f3dc881bf08225f515"
-  integrity sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==
-  dependencies:
-    "@types/d3-array" "*"
-    "@types/d3-axis" "*"
-    "@types/d3-brush" "*"
-    "@types/d3-chord" "*"
-    "@types/d3-color" "*"
-    "@types/d3-contour" "*"
-    "@types/d3-delaunay" "*"
-    "@types/d3-dispatch" "*"
-    "@types/d3-drag" "*"
-    "@types/d3-dsv" "*"
-    "@types/d3-ease" "*"
-    "@types/d3-fetch" "*"
-    "@types/d3-force" "*"
-    "@types/d3-format" "*"
-    "@types/d3-geo" "*"
-    "@types/d3-hierarchy" "*"
-    "@types/d3-interpolate" "*"
-    "@types/d3-path" "*"
-    "@types/d3-polygon" "*"
-    "@types/d3-quadtree" "*"
-    "@types/d3-random" "*"
-    "@types/d3-scale" "*"
-    "@types/d3-scale-chromatic" "*"
-    "@types/d3-selection" "*"
-    "@types/d3-shape" "*"
-    "@types/d3-time" "*"
-    "@types/d3-time-format" "*"
-    "@types/d3-timer" "*"
-    "@types/d3-transition" "*"
-    "@types/d3-zoom" "*"
-
 "@types/eslint-scope@^3.7.3":
   version "3.7.3"
   resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224"
@@ -1747,11 +1535,6 @@
     "@types/qs" "*"
     "@types/serve-static" "*"
 
-"@types/geojson@*":
-  version "7946.0.8"
-  resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca"
-  integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==
-
 "@types/glob@^7.1.1":
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
@@ -2789,16 +2572,16 @@ command-line-args@^5.2.0:
     lodash.camelcase "^4.3.0"
     typical "^4.0.0"
 
-commander@7, commander@^7.0.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
-  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
-
 commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
+commander@^7.0.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
 commander@^8.3.0:
   version "8.3.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
@@ -3021,61 +2804,17 @@ csstype@^2.6.8:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.20.tgz#9229c65ea0b260cf4d3d997cb06288e36a8d6dda"
   integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==
 
-"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3:
-  version "3.1.6"
-  resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.1.6.tgz#0342c835925826f49b4d16eb7027aec334ffc97d"
-  integrity sha512-DCbBBNuKOeiR9h04ySRBMW52TFVc91O9wJziuyXw6Ztmy8D3oZbmCkOO3UHKC7ceNJsN2Mavo9+vwV8EAEUXzA==
-  dependencies:
-    internmap "1 - 2"
-
-d3-axis@3:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322"
-  integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
-
-d3-brush@3:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
-  integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
-  dependencies:
-    d3-dispatch "1 - 3"
-    d3-drag "2 - 3"
-    d3-interpolate "1 - 3"
-    d3-selection "3"
-    d3-transition "3"
-
-d3-chord@3:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966"
-  integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
-  dependencies:
-    d3-path "1 - 3"
-
-"d3-color@1 - 3", d3-color@3:
+"d3-color@1 - 3":
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
   integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
 
-d3-contour@3:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-3.0.1.tgz#2c64255d43059599cd0dba8fe4cc3d51ccdd9bbd"
-  integrity sha512-0Oc4D0KyhwhM7ZL0RMnfGycLN7hxHB8CMmwZ3+H26PWAG0ozNuYG5hXSDNgmP1SgJkQMrlG6cP20HoaSbvcJTQ==
-  dependencies:
-    d3-array "2 - 3"
-
-d3-delaunay@6:
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.2.tgz#7fd3717ad0eade2fc9939f4260acfb503f984e92"
-  integrity sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==
-  dependencies:
-    delaunator "5"
-
-"d3-dispatch@1 - 3", d3-dispatch@3:
+"d3-dispatch@1 - 3":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
   integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
 
-"d3-drag@2 - 3", d3-drag@3, d3-drag@^3.0.0:
+"d3-drag@2 - 3", d3-drag@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
   integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
@@ -3083,131 +2822,29 @@ d3-delaunay@6:
     d3-dispatch "1 - 3"
     d3-selection "3"
 
-"d3-dsv@1 - 3", d3-dsv@3:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
-  integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
-  dependencies:
-    commander "7"
-    iconv-lite "0.6"
-    rw "1"
-
-"d3-ease@1 - 3", d3-ease@3:
+"d3-ease@1 - 3":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
   integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
 
-d3-fetch@3:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22"
-  integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
-  dependencies:
-    d3-dsv "1 - 3"
-
-d3-force@3:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
-  integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
-  dependencies:
-    d3-dispatch "1 - 3"
-    d3-quadtree "1 - 3"
-    d3-timer "1 - 3"
-
-"d3-format@1 - 3", d3-format@3:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
-  integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
-
-d3-geo@3:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.0.1.tgz#4f92362fd8685d93e3b1fae0fd97dc8980b1ed7e"
-  integrity sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==
-  dependencies:
-    d3-array "2.5.0 - 3"
-
-d3-hierarchy@3:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
-  integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
-
-"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3:
+"d3-interpolate@1 - 3":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
   integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
   dependencies:
     d3-color "1 - 3"
 
-"d3-path@1 - 3", d3-path@3:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e"
-  integrity sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==
-
-d3-polygon@3:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398"
-  integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
-
-"d3-quadtree@1 - 3", d3-quadtree@3:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
-  integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
-
-d3-random@3:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4"
-  integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
-
-d3-scale-chromatic@3:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a"
-  integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==
-  dependencies:
-    d3-color "1 - 3"
-    d3-interpolate "1 - 3"
-
-d3-scale@4:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
-  integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
-  dependencies:
-    d3-array "2.10.0 - 3"
-    d3-format "1 - 3"
-    d3-interpolate "1.2.0 - 3"
-    d3-time "2.1.1 - 3"
-    d3-time-format "2 - 4"
-
 "d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
   integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
 
-d3-shape@3:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556"
-  integrity sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==
-  dependencies:
-    d3-path "1 - 3"
-
-"d3-time-format@2 - 4", d3-time-format@4:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
-  integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
-  dependencies:
-    d3-time "1 - 3"
-
-"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.0.0.tgz#65972cb98ae2d4954ef5c932e8704061335d4975"
-  integrity sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==
-  dependencies:
-    d3-array "2 - 3"
-
-"d3-timer@1 - 3", d3-timer@3:
+"d3-timer@1 - 3":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
   integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
 
-"d3-transition@2 - 3", d3-transition@3:
+"d3-transition@2 - 3":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
   integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
@@ -3218,7 +2855,7 @@ d3-shape@3:
     d3-interpolate "1 - 3"
     d3-timer "1 - 3"
 
-d3-zoom@3, d3-zoom@^3.0.0:
+d3-zoom@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
   integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
@@ -3229,42 +2866,6 @@ d3-zoom@3, d3-zoom@^3.0.0:
     d3-selection "2 - 3"
     d3-transition "2 - 3"
 
-d3@^7.4.4:
-  version "7.4.4"
-  resolved "https://registry.yarnpkg.com/d3/-/d3-7.4.4.tgz#bfbf87487c37d3196efebd5a63e3a0ed8299d8ff"
-  integrity sha512-97FE+MYdAlV3R9P74+R3Uar7wUKkIFu89UWMjEaDhiJ9VxKvqaMxauImy8PC2DdBkdM2BxJOIoLxPrcZUyrKoQ==
-  dependencies:
-    d3-array "3"
-    d3-axis "3"
-    d3-brush "3"
-    d3-chord "3"
-    d3-color "3"
-    d3-contour "3"
-    d3-delaunay "6"
-    d3-dispatch "3"
-    d3-drag "3"
-    d3-dsv "3"
-    d3-ease "3"
-    d3-fetch "3"
-    d3-force "3"
-    d3-format "3"
-    d3-geo "3"
-    d3-hierarchy "3"
-    d3-interpolate "3"
-    d3-path "3"
-    d3-polygon "3"
-    d3-quadtree "3"
-    d3-random "3"
-    d3-scale "4"
-    d3-scale-chromatic "3"
-    d3-selection "3"
-    d3-shape "3"
-    d3-time "3"
-    d3-time-format "4"
-    d3-timer "3"
-    d3-transition "3"
-    d3-zoom "3"
-
 dayjs@^1.10.7:
   version "1.11.3"
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.3.tgz#4754eb694a624057b9ad2224b67b15d552589258"
@@ -3339,13 +2940,6 @@ del@^4.1.1:
     pify "^4.0.1"
     rimraf "^2.6.3"
 
-delaunator@5:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b"
-  integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==
-  dependencies:
-    robust-predicates "^3.0.0"
-
 depd@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
@@ -4467,7 +4061,7 @@ iconv-lite@0.4.24:
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-iconv-lite@0.6, iconv-lite@^0.6.3:
+iconv-lite@^0.6.3:
   version "0.6.3"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
   integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
@@ -4552,11 +4146,6 @@ internal-slot@^1.0.3:
     has "^1.0.3"
     side-channel "^1.0.4"
 
-"internmap@1 - 2":
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
-  integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
-
 interpret@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
@@ -6073,11 +5662,6 @@ rimraf@^3.0.2:
   dependencies:
     glob "^7.1.3"
 
-robust-predicates@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a"
-  integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==
-
 rope-sequence@^1.3.0:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.3.tgz#3f67fc106288b84b71532b4a5fd9d4881e4457f0"
@@ -6090,11 +5674,6 @@ run-parallel@^1.1.9:
   dependencies:
     queue-microtask "^1.2.2"
 
-rw@1:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
-  integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
-
 rxjs@^7.5.5:
   version "7.5.5"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f"