Browse Source

feat: migrate logs data from `browser.storage` to `indexeddb`

Ahmad Kholid 3 years ago
parent
commit
ea2ad54985

+ 7 - 3
package.json

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

+ 11 - 7
src/background/WorkflowLogger.js

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

+ 29 - 18
src/background/workflowEngine/engine.js

@@ -17,6 +17,7 @@ class WorkflowEngine {
     this.parentWorkflow = parentWorkflow;
     this.saveLog = workflow.settings?.saveLog ?? true;
 
+    this.workerId = 0;
     this.workers = new Map();
     this.waitConnections = {};
 
@@ -166,7 +167,10 @@ class WorkflowEngine {
   }
 
   addWorker(detail) {
-    const worker = new Worker(this);
+    this.workerId += 1;
+
+    const workerId = `worker-${this.workerId}`;
+    const worker = new Worker(workerId, this);
     worker.init(detail);
 
     this.workers.set(worker.id, worker);
@@ -178,7 +182,7 @@ class WorkflowEngine {
     const isLimit = this.history.length >= 1001;
     const notErrorLog = detail.type !== 'error';
 
-    if ((!this.saveLog || isLimit) && notErrorLog) return;
+    if (!this.saveLog && isLimit && notErrorLog) return;
 
     this.logHistoryId += 1;
     detail.id = this.logHistoryId;
@@ -273,24 +277,31 @@ class WorkflowEngine {
       if (!this.workflow.isTesting) {
         const { name, id } = this.workflow;
 
-        let { logsCtxData } = await browser.storage.local.get('logsCtxData');
-        if (!logsCtxData) logsCtxData = {};
-        logsCtxData[this.id] = this.historyCtxData;
-        await browser.storage.local.set({ logsCtxData });
-
         await this.logger.add({
-          name,
-          status,
-          message,
-          id: this.id,
-          workflowId: id,
-          endedAt: endedTimestamp,
-          parentLog: this.parentWorkflow,
-          startedAt: this.startedTimestamp,
-          history: this.saveLog ? this.history : [],
+          detail: {
+            name,
+            status,
+            message,
+            id: this.id,
+            workflowId: id,
+            endedAt: endedTimestamp,
+            parentLog: this.parentWorkflow,
+            startedAt: this.startedTimestamp,
+          },
+          history: {
+            logId: this.id,
+            data: this.saveLog ? this.history : [],
+          },
+          ctxData: {
+            logId: this.id,
+            data: this.historyCtxData,
+          },
           data: {
-            table: this.referenceData.table,
-            variables: this.referenceData.variables,
+            logId: this.id,
+            data: {
+              table: this.referenceData.table,
+              variables: this.referenceData.variables,
+            },
           },
         });
       }

+ 2 - 3
src/background/workflowEngine/worker.js

@@ -1,4 +1,3 @@
-import { nanoid } from 'nanoid';
 import browser from 'webextension-polyfill';
 import { toCamelCase, sleep, objectHasKey, isObject } from '@/utils/helper';
 import { tasks } from '@/utils/shared';
@@ -6,8 +5,8 @@ import referenceData from '@/utils/referenceData';
 import { convertData, waitTabLoaded, getBlockConnection } from './helper';
 
 class Worker {
-  constructor(engine) {
-    this.id = nanoid(5);
+  constructor(id, engine) {
+    this.id = id;
     this.engine = engine;
     this.settings = engine.workflow.settings;
 

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

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

+ 2 - 6
src/components/newtab/shared/SharedLogsTable.vue

@@ -65,18 +65,14 @@ function formatDate(date, format) {
 
   return dayjs(date).format(format);
 }
-function getErrorMessage({ history, message }) {
+function getErrorMessage({ message }) {
   const messagePath = `log.messages.${message}`;
 
   if (message && te(messagePath)) {
     return t(messagePath);
   }
 
-  const lastHistory = history[history.length - 1];
-
-  return lastHistory && lastHistory.type === 'error'
-    ? lastHistory.message
-    : null;
+  return '';
 }
 </script>
 <style scoped>

+ 6 - 0
src/composable/liveQuery.js

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

+ 22 - 0
src/db/logs.js

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

+ 21 - 14
src/newtab/App.vue

@@ -86,14 +86,15 @@ import { useI18n } from 'vue-i18n';
 import { useRoute } from 'vue-router';
 import { compare } from 'compare-versions';
 import browser from 'webextension-polyfill';
+import dbLogs from '@/db/logs';
 import { useTheme } from '@/composable/theme';
 import { loadLocaleMessages, setI18nLanguage } from '@/lib/vueI18n';
 import { parseJSON } from '@/utils/helper';
 import { fetchApi, getSharedWorkflows, getUserWorkflows } from '@/utils/api';
 import dayjs from '@/lib/dayjs';
-import Log from '@/models/log';
 import Workflow from '@/models/workflow';
 import AppSidebar from '@/components/newtab/app/AppSidebar.vue';
+import dataMigration from '@/utils/dataMigration';
 
 const { t } = useI18n();
 const store = useStore();
@@ -238,14 +239,24 @@ async function fetchUserData() {
 /* eslint-disable-next-line */
 function autoDeleteLogs() {
   const deleteAfter = store.state.settings.deleteLogAfter;
-
   if (deleteAfter === 'never') return;
 
-  Log.delete(({ endedAt }) => {
-    const diff = dayjs().diff(dayjs(endedAt), 'day');
+  const lastCheck =
+    +localStorage.getItem('checkDeleteLogs') || Date.now() - 8.64e7;
+  const dayDiff = dayjs().diff(dayjs(lastCheck), 'day');
 
-    return diff >= deleteAfter;
-  });
+  if (dayDiff < 1) return;
+
+  const aDayInMs = 8.64e7;
+  const maxLogAge = Date.now() - aDayInMs * deleteAfter;
+
+  dbLogs.items
+    .where('endedAt')
+    .below(maxLogAge)
+    .delete()
+    .then(() => {
+      localStorage.setItem('checkDeleteLogs', Date.now());
+    });
 }
 function handleStorageChanged(change) {
   if (change.logs) {
@@ -316,24 +327,20 @@ window.addEventListener('storage', ({ key, newValue }) => {
     }
 
     await Promise.allSettled([
-      store.dispatch('retrieve', [
-        'workflows',
-        'logs',
-        'collections',
-        'folders',
-      ]),
+      store.dispatch('retrieve', ['workflows', 'collections', 'folders']),
       store.dispatch('retrieveWorkflowState'),
     ]);
 
     await loadLocaleMessages(store.state.settings.locale, 'newtab');
     await setI18nLanguage(store.state.settings.locale);
 
+    await dataMigration();
+
     retrieved.value = true;
 
     await fetchUserData();
     await syncHostWorkflow();
-
-    // autoDeleteLogs();
+    autoDeleteLogs();
   } catch (error) {
     retrieved.value = true;
     console.error(error);

+ 2 - 0
src/newtab/index.js

@@ -1,4 +1,5 @@
 import { createApp } from 'vue';
+import { createPinia } from 'pinia';
 import App from './App.vue';
 import router from './router';
 import store from '../store';
@@ -15,6 +16,7 @@ createApp(App)
   .use(store)
   .use(compsUi)
   .use(vueI18n)
+  .use(createPinia())
   .use(vueToastification)
   .use(vRemixicon, icons)
   .mount('#app');

+ 8 - 14
src/newtab/pages/Home.vue

@@ -22,7 +22,7 @@
         </div>
         <div>
           <div class="mb-2 flex items-center justify-between">
-            <p class="font-semibold inline-block">Logs</p>
+            <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"
@@ -31,12 +31,12 @@
             </router-link>
           </div>
           <p
-            v-if="logs.length === 0"
+            v-if="logs?.length === 0"
             class="text-center text-gray-600 dark:text-gray-200"
           >
             {{ t('message.noData') }}
           </p>
-          <shared-logs-table :logs="logs" class="w-full" />
+          <shared-logs-table :logs="logs || []" class="w-full" />
         </div>
       </div>
       <div class="w-4/12 space-y-4">
@@ -61,7 +61,8 @@ import { computed } from 'vue';
 import { useStore } from 'vuex';
 import { useI18n } from 'vue-i18n';
 import { sendMessage } from '@/utils/message';
-import Log from '@/models/log';
+import { useLiveQuery } from '@/composable/liveQuery';
+import dbLogs from '@/db/logs';
 import Workflow from '@/models/workflow';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
@@ -70,19 +71,12 @@ import SharedWorkflowState from '@/components/newtab/shared/SharedWorkflowState.
 const { t } = useI18n();
 const store = useStore();
 
+const logs = useLiveQuery(() =>
+  dbLogs.items.orderBy('endedAt').reverse().limit(10).toArray()
+);
 const workflows = computed(() =>
   Workflow.query().orderBy('createdAt', 'desc').limit(3).get()
 );
-const logs = computed(() =>
-  Log.query()
-    .where(
-      ({ isInCollection, isChildLog, parentLog }) =>
-        !isInCollection && !isChildLog && !parentLog
-    )
-    .orderBy('startedAt', 'desc')
-    .limit(10)
-    .get()
-);
 const workflowState = computed(() =>
   store.state.workflowState.filter(({ isInCollection }) => !isInCollection)
 );

+ 38 - 43
src/newtab/pages/Logs.vue

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

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

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

+ 59 - 41
src/newtab/pages/logs/[id].vue

@@ -1,16 +1,16 @@
 <template>
-  <div v-if="activeLog" class="container pt-8 pb-4">
+  <div v-if="currentLog.id" class="container pt-8 pb-4">
     <div class="flex items-center mb-8">
       <div>
         <h1 class="text-2xl max-w-md text-overflow font-semibold">
-          {{ activeLog.name }}
+          {{ currentLog.name }}
         </h1>
         <p class="text-gray-600 dark:text-gray-200">
           {{
             t(`log.description.text`, {
-              status: t(`log.description.status.${activeLog.status}`),
-              date: dayjs(activeLog.startedAt).format('DD MMM'),
-              duration: countDuration(activeLog.startedAt, activeLog.endedAt),
+              status: t(`log.description.status.${currentLog.status}`),
+              date: dayjs(currentLog.startedAt).format('DD MMM'),
+              duration: countDuration(currentLog.startedAt, currentLog.endedAt),
             })
           }}
         </p>
@@ -33,13 +33,13 @@
       <div class="w-7/12 mr-6">
         <ui-list>
           <router-link
-            v-if="collectionLog"
-            :to="activeLog.parentLog?.id || activeLog.collectionLogId"
+            v-if="parentLog"
+            :to="currentLog.parentLog?.id || currentLog.collectionLogId"
             replace
             class="mb-4 flex"
           >
             <v-remixicon name="riArrowLeftLine" class="mr-2" />
-            {{ t('log.goBack', { name: collectionLog.name }) }}
+            {{ t('log.goBack', { name: parentLog.name }) }}
           </router-link>
           <ui-expand
             v-for="(item, index) in history"
@@ -107,7 +107,7 @@
           </ui-expand>
         </ui-list>
         <div
-          v-if="activeLog.history.length >= 10"
+          v-if="currentLog.history.length >= 10"
           class="flex items-center justify-between mt-4"
         >
           <div>
@@ -126,29 +126,28 @@
             </select>
             {{
               t('components.pagination.text2', {
-                count: activeLog.history.length,
+                count: currentLog.history.length,
               })
             }}
           </div>
           <ui-pagination
             v-model="pagination.currentPage"
             :per-page="pagination.perPage"
-            :records="activeLog.history.length"
+            :records="currentLog.history.length"
           />
         </div>
       </div>
       <div class="w-5/12 logs-details sticky top-10">
-        <logs-data-viewer :log="activeLog" />
+        <logs-data-viewer :log="currentLog" />
       </div>
     </div>
   </div>
 </template>
 <script setup>
-import { computed, onMounted, shallowReactive, shallowRef } from 'vue';
+import { computed, shallowReactive, shallowRef } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { useI18n } from 'vue-i18n';
-import browser from 'webextension-polyfill';
-import Log from '@/models/log';
+import dbLogs from '@/db/logs';
 import Workflow from '@/models/workflow';
 import dayjs from '@/lib/dayjs';
 import { countDuration } from '@/utils/helper';
@@ -182,10 +181,15 @@ const route = useRoute();
 const router = useRouter();
 
 const ctxData = shallowRef({});
+const parentLog = shallowRef(null);
+
 const pagination = shallowReactive({
   perPage: 10,
   currentPage: 1,
 });
+const currentLog = shallowRef({
+  history: [],
+});
 
 function translateLog(log) {
   const copyLog = { ...log };
@@ -212,41 +216,37 @@ function translateLog(log) {
   return copyLog;
 }
 
-const activeLog = computed(() => Log.find(route.params.id));
 const history = computed(() =>
-  activeLog.value.history
+  currentLog.value.history
     .slice(
       (pagination.currentPage - 1) * pagination.perPage,
       pagination.currentPage * pagination.perPage
     )
     .map(translateLog)
 );
-const collectionLog = computed(() => {
-  if (activeLog.value.parentLog) {
-    return Log.find(activeLog.value.parentLog.id);
-  }
-
-  return Log.find(activeLog.value.collectionLogId);
-});
 const workflowExists = computed(() =>
-  Workflow.find(activeLog.value.workflowId)
+  Workflow.find(currentLog.value.workflowId)
 );
 
 function deleteLog() {
-  Log.delete(route.params.id).then(() => {
-    const backHistory = window.history.state.back;
+  dbLogs.items
+    .where('id')
+    .equals(route.params.id)
+    .delete()
+    .then(() => {
+      const backHistory = window.history.state.back;
 
-    if (backHistory.startsWith('/workflows')) {
-      router.replace(backHistory);
-      return;
-    }
+      if (backHistory.startsWith('/workflows')) {
+        router.replace(backHistory);
+        return;
+      }
 
-    router.replace('/logs');
-  });
+      router.replace('/logs');
+    });
 }
 function goToWorkflow() {
   const backHistory = window.history.state.back;
-  let path = `/workflows/${activeLog.value.workflowId}`;
+  let path = `/workflows/${currentLog.value.workflowId}`;
 
   if (backHistory.startsWith(path)) {
     path = backHistory;
@@ -255,15 +255,33 @@ function goToWorkflow() {
   router.push(path);
 }
 
-onMounted(async () => {
-  if (!activeLog.value) router.replace('/logs');
+(async () => {
+  const logId = route.params.id;
+  const logDetail = await dbLogs.items.where('id').equals(logId).last();
 
-  const { logsCtxData } = await browser.storage.local.get('logsCtxData');
-  const logCtxData = logsCtxData && logsCtxData[route.params.id];
-  if (logCtxData) {
-    ctxData.value = logCtxData;
+  if (!logDetail) {
+    router.replace('/logs');
+    return;
   }
-});
+
+  const [logCtxData, logHistory] = await Promise.all(
+    ['ctxData', 'histories'].map((key) =>
+      dbLogs[key].where('logId').equals(logId).last()
+    )
+  );
+
+  ctxData.value = logCtxData?.data || {};
+  currentLog.value = {
+    history: logHistory?.data || [],
+    ...logDetail,
+  };
+
+  const parentLogId = logDetail.collectionLogId || logDetail.parentLog?.id;
+  if (parentLogId) {
+    parentLog.value =
+      (await dbLogs.items.where('id').equals(parentLogId).last()) || null;
+  }
+})();
 </script>
 <style>
 .logs-details .cm-editor {

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

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

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

@@ -114,7 +114,7 @@
         </workflow-builder>
         <div v-else class="container pb-4 mt-24 px-4">
           <template v-if="activeTab === 'logs'">
-            <div v-if="logs.length === 0" class="text-center">
+            <div v-if="!logs || logs.length === 0" class="text-center">
               <img
                 src="@/assets/svg/files-and-folder.svg"
                 class="mx-auto max-w-sm"
@@ -222,7 +222,6 @@ import { useToast } from 'vue-toastification';
 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import defu from 'defu';
-import AES from 'crypto-js/aes';
 import browser from 'webextension-polyfill';
 import emitter from '@/lib/mitt';
 import { useDialog } from '@/composable/dialog';
@@ -238,8 +237,9 @@ import {
   parseJSON,
   throttle,
 } from '@/utils/helper';
-import Log from '@/models/log';
+import { useLiveQuery } from '@/composable/liveQuery';
 import decryptFlow, { getWorkflowPass } from '@/utils/decryptFlow';
+import dbLogs from '@/db/logs';
 import Workflow from '@/models/workflow';
 import workflowTrigger from '@/utils/workflowTrigger';
 import WorkflowShare from '@/components/newtab/workflow/WorkflowShare.vue';
@@ -261,6 +261,14 @@ const toast = useToast();
 const router = useRouter();
 const dialog = useDialog();
 const shortcut = useShortcut('editor:toggle-sidebar', toggleSidebar);
+const logs = useLiveQuery(() =>
+  dbLogs.items
+    .where('workflowId')
+    .equals(route.params.id)
+    .reverse()
+    .limit(15)
+    .sortBy('endedAt')
+);
 
 const activeTabQuery = route.query.tab || 'editor';
 
@@ -371,17 +379,6 @@ const workflowModal = computed(() => workflowModals[state.modalName] || {});
 const workflowState = computed(() =>
   store.getters.getWorkflowState(workflowId)
 );
-const logs = computed(() =>
-  Log.query()
-    .where(
-      (item) =>
-        item.workflowId === workflowId &&
-        (!item.isInCollection || !item.isChildLog || !item.parentLog)
-    )
-    .limit(15)
-    .orderBy('startedAt', 'desc')
-    .get()
-);
 
 const updateBlockData = debounce((data) => {
   let payload = data;
@@ -681,9 +678,7 @@ function shareWorkflow() {
   }
 }
 function deleteLog(logId) {
-  Log.delete(logId).then(() => {
-    store.dispatch('saveToStorage', 'logs');
-  });
+  dbLogs.items.where('id').equals(logId).delete();
 }
 function workflowExporter() {
   const currentWorkflow = { ...workflow.value };
@@ -753,14 +748,10 @@ async function saveWorkflow() {
   if (workflowData.active === 'shared') return;
 
   try {
-    let flow = JSON.stringify(editor.value.export());
+    const flow = JSON.stringify(editor.value.export());
     const [triggerBlockId] = editor.value.getNodesFromName('trigger');
     const triggerBlock = editor.value.getNodeFromId(triggerBlockId);
 
-    if (workflow.value.isProtected) {
-      flow = AES.encrypt(flow, getWorkflowPass(workflow.value.pass)).toString();
-    }
-
     updateWorkflow({ drawflow: flow, trigger: triggerBlock?.data }).then(() => {
       if (triggerBlock) {
         workflowTrigger.register(workflowId, triggerBlock);

+ 50 - 0
src/utils/dataMigration.js

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

+ 3 - 0
webpack.config.js

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

+ 112 - 132
yarn.lock

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