Browse Source

feat: storage

Ahmad Kholid 3 years ago
parent
commit
46949ce483

+ 0 - 2
src/background/index.js

@@ -523,11 +523,9 @@ message.on('workflow:execute', (workflowData, sender) => {
 });
 });
 message.on('workflow:stop', (id) => workflow.states.stop(id));
 message.on('workflow:stop', (id) => workflow.states.stop(id));
 message.on('workflow:added', (workflowId) => {
 message.on('workflow:added', (workflowId) => {
-  console.log(browser.runtime.getURL('/newtab.html'));
   browser.tabs
   browser.tabs
     .query({ url: browser.runtime.getURL('/newtab.html') })
     .query({ url: browser.runtime.getURL('/newtab.html') })
     .then((tabs) => {
     .then((tabs) => {
-      console.log(tabs, tabs.length);
       if (tabs.length >= 1) {
       if (tabs.length >= 1) {
         const lastTab = tabs.at(-1);
         const lastTab = tabs.at(-1);
 
 

+ 0 - 2
src/background/workflowEngine/blocksHandler/handlerTabUrl.js

@@ -13,8 +13,6 @@ export async function logData({ id, data }) {
     urls = tabs.map((tab) => tab.url);
     urls = tabs.map((tab) => tab.url);
   }
   }
 
 
-  console.log(urls, data);
-
   if (data.assignVariable) {
   if (data.assignVariable) {
     this.setVariable(data.variableName, urls);
     this.setVariable(data.variableName, urls);
   }
   }

+ 131 - 62
src/background/workflowEngine/engine.js

@@ -2,6 +2,7 @@ import browser from 'webextension-polyfill';
 import { nanoid } from 'nanoid';
 import { nanoid } from 'nanoid';
 import { tasks } from '@/utils/shared';
 import { tasks } from '@/utils/shared';
 import { clearCache, sleep, parseJSON, isObject } from '@/utils/helper';
 import { clearCache, sleep, parseJSON, isObject } from '@/utils/helper';
+import dbStorage from '@/db/storage';
 import Worker from './worker';
 import Worker from './worker';
 
 
 class WorkflowEngine {
 class WorkflowEngine {
@@ -85,86 +86,114 @@ class WorkflowEngine {
     };
     };
   }
   }
 
 
-  init() {
-    if (this.workflow.isDisabled) return;
-
-    if (!this.states) {
-      console.error(`"${this.workflow.name}" workflow doesn't have states`);
-      this.destroy('error');
-      return;
-    }
-
-    const { nodes, edges } = this.workflow.drawflow;
-    if (!nodes || nodes.length === 0) {
-      console.error(`${this.workflow.name} doesn't have blocks`);
-      return;
-    }
-
-    const triggerBlock = nodes.find((node) => node.label === 'trigger');
-    if (!triggerBlock) {
-      console.error(`${this.workflow.name} doesn't have a trigger block`);
-      return;
-    }
-
-    this.triggerBlockId = triggerBlock.id;
+  async init() {
+    try {
+      if (this.workflow.isDisabled) return;
 
 
-    this.blocks = nodes.reduce((acc, node) => {
-      acc[node.id] = node;
+      if (!this.states) {
+        console.error(`"${this.workflow.name}" workflow doesn't have states`);
+        this.destroy('error');
+        return;
+      }
 
 
-      return acc;
-    }, {});
-    this.connectionsMap = edges.reduce((acc, { sourceHandle, target }) => {
-      if (!acc[sourceHandle]) acc[sourceHandle] = [];
+      const { nodes, edges } = this.workflow.drawflow;
+      if (!nodes || nodes.length === 0) {
+        console.error(`${this.workflow.name} doesn't have blocks`);
+        return;
+      }
 
 
-      acc[sourceHandle].push(target);
+      const triggerBlock = nodes.find((node) => node.label === 'trigger');
+      if (!triggerBlock) {
+        console.error(`${this.workflow.name} doesn't have a trigger block`);
+        return;
+      }
 
 
-      return acc;
-    }, {});
+      this.triggerBlockId = triggerBlock.id;
+
+      this.blocks = nodes.reduce((acc, node) => {
+        acc[node.id] = node;
+
+        return acc;
+      }, {});
+      this.connectionsMap = edges.reduce((acc, { sourceHandle, target }) => {
+        if (!acc[sourceHandle]) acc[sourceHandle] = [];
+
+        acc[sourceHandle].push(target);
+
+        return acc;
+      }, {});
+
+      const workflowTable = this.workflow.table || this.workflow.dataColumns;
+      let columns = Array.isArray(workflowTable)
+        ? workflowTable
+        : Object.values(workflowTable);
+
+      if (this.workflow.connectedTable) {
+        const connectedTable = await dbStorage.tablesItems
+          .where('id')
+          .equals(this.workflow.connectedTable)
+          .first();
+        const connectedTableData = await dbStorage.tablesData
+          .where('tableId')
+          .equals(connectedTable?.id)
+          .first();
+        if (connectedTable && connectedTableData) {
+          columns = Object.values(connectedTable.columns);
+          Object.assign(this.columns, connectedTableData.columnsIndex);
+          this.referenceData.table = connectedTableData.items || [];
+        } else {
+          this.workflow.connectedTable = null;
+        }
+      }
 
 
-    const workflowTable = this.workflow.table || this.workflow.dataColumns;
-    const columns = Array.isArray(workflowTable)
-      ? workflowTable
-      : Object.values(workflowTable);
+      const variables = await dbStorage.variables.toArray();
+      variables.forEach(({ name, value }) => {
+        this.referenceData.variables[`$$${name}`] = value;
+      });
 
 
-    columns.forEach(({ name, type, id }) => {
-      const columnId = id || name;
+      columns.forEach(({ name, type, id }) => {
+        const columnId = id || name;
 
 
-      this.columnsId[name] = columnId;
-      this.columns[columnId] = { index: 0, name, type };
-    });
+        this.columnsId[name] = columnId;
+        if (!this.columns[columnId])
+          this.columns[columnId] = { index: 0, name, type };
+      });
 
 
-    if (BROWSER_TYPE !== 'chrome') {
-      this.workflow.settings.debugMode = false;
-    }
-    if (this.workflow.settings.debugMode) {
-      chrome.debugger.onEvent.addListener(this.onDebugEvent);
-    }
-    if (this.workflow.settings.reuseLastState) {
-      const lastStateKey = `state:${this.workflow.id}`;
-      browser.storage.local.get(lastStateKey).then((value) => {
+      if (BROWSER_TYPE !== 'chrome') {
+        this.workflow.settings.debugMode = false;
+      }
+      if (this.workflow.settings.debugMode) {
+        chrome.debugger.onEvent.addListener(this.onDebugEvent);
+      }
+      if (
+        this.workflow.settings.reuseLastState &&
+        !this.workflow.connectedTable
+      ) {
+        const lastStateKey = `state:${this.workflow.id}`;
+        const value = await browser.storage.local.get(lastStateKey);
         const lastState = value[lastStateKey];
         const lastState = value[lastStateKey];
-        if (!lastState) return;
 
 
-        Object.assign(this.columns, lastState.columns);
-        Object.assign(this.referenceData, lastState.referenceData);
-      });
-    }
+        if (lastState) {
+          Object.assign(this.columns, lastState.columns);
+          Object.assign(this.referenceData, lastState.referenceData);
+        }
+      }
 
 
-    this.workflow.table = columns;
-    this.startedTimestamp = Date.now();
+      this.workflow.table = columns;
+      this.startedTimestamp = Date.now();
 
 
-    this.states.on('stop', this.onWorkflowStopped);
+      this.states.on('stop', this.onWorkflowStopped);
 
 
-    this.states
-      .add(this.id, {
+      await this.states.add(this.id, {
         id: this.id,
         id: this.id,
         state: this.state,
         state: this.state,
         workflowId: this.workflow.id,
         workflowId: this.workflow.id,
         parentState: this.parentWorkflow,
         parentState: this.parentWorkflow,
-      })
-      .then(() => {
-        this.addWorker({ blockId: triggerBlock.id });
       });
       });
+      this.addWorker({ blockId: triggerBlock.id });
+    } catch (error) {
+      console.error(error);
+    }
   }
   }
 
 
   resume({ id, state }) {
   resume({ id, state }) {
@@ -344,7 +373,47 @@ class WorkflowEngine {
         clearCache(this.workflow);
         clearCache(this.workflow);
       }
       }
 
 
+      const { table, variables } = this.referenceData;
+      const tableId = this.workflow.connectedTable;
+
+      await dbStorage.transaction(
+        'rw',
+        dbStorage.tablesItems,
+        dbStorage.tablesData,
+        dbStorage.variables,
+        async () => {
+          if (tableId) {
+            await dbStorage.tablesItems.update(tableId, {
+              modifiedAt: Date.now(),
+              rowsCount: table.length,
+            });
+            await dbStorage.tablesData.where('tableId').equals(tableId).modify({
+              items: table,
+              columnsIndex: this.columns,
+            });
+          }
+
+          for (const key in variables) {
+            if (key.startsWith('$$')) {
+              const varName = key.slice(2);
+              const varValue = variables[key];
+
+              const variable =
+                (await dbStorage.variables
+                  .where('name')
+                  .equals(varName)
+                  .first()) || {};
+              variable.name = varName;
+              variable.value = varValue;
+
+              await dbStorage.variables.put(variable);
+            }
+          }
+        }
+      );
+
       this.isDestroyed = true;
       this.isDestroyed = true;
+      this.referenceData = {};
       this.eventListeners = {};
       this.eventListeners = {};
     } catch (error) {
     } catch (error) {
       console.error(error);
       console.error(error);

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

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

+ 0 - 1
src/components/newtab/shared/SharedPermissionsModal.vue

@@ -54,7 +54,6 @@ const icons = {
 };
 };
 
 
 function requestPermission() {
 function requestPermission() {
-  console.log(props.permissions);
   browser.permissions
   browser.permissions
     .request({ permissions: toRaw(props.permissions) })
     .request({ permissions: toRaw(props.permissions) })
     .then(() => {
     .then(() => {

+ 174 - 0
src/components/newtab/storage/StorageEditTable.vue

@@ -0,0 +1,174 @@
+<template>
+  <ui-modal :model-value="modelValue" persist custom-content>
+    <ui-card
+      padding="p-0"
+      class="max-w-xl w-full flex flex-col"
+      style="height: 600px"
+    >
+      <p class="p-4 font-semibold">
+        {{ t('storage.table.add') }}
+      </p>
+      <div class="overflow-auto scroll px-4 pb-4 flex-1">
+        <ui-input
+          v-model="state.name"
+          class="w-full -mt-1"
+          label="Table name"
+          placeholder="My table"
+        />
+        <div class="flex items-center mt-4">
+          <p class="flex-1">Columns</p>
+          <ui-button icon :title="t('common.add')" @click="addColumn">
+            <v-remixicon name="riAddLine" />
+          </ui-button>
+        </div>
+        <p
+          v-if="state.columns && state.columns.length === 0"
+          class="text-center my-4 text-gray-600 dark:text-gray-300"
+        >
+          {{ t('message.noData') }}
+        </p>
+        <ul class="mt-4 space-y-2">
+          <li
+            v-for="(column, index) in state.columns"
+            :key="column.id"
+            class="flex items-center space-x-2"
+          >
+            <ui-input
+              :model-value="column.name"
+              :placeholder="t('workflow.table.column.name')"
+              class="flex-1"
+              @blur="updateColumnName(index, $event.target)"
+            />
+            <ui-select
+              v-model="column.type"
+              class="flex-1"
+              :placeholder="t('workflow.table.column.type')"
+            >
+              <option v-for="type in dataTypes" :key="type.id" :value="type.id">
+                {{ type.name }}
+              </option>
+            </ui-select>
+            <button @click="deleteColumn(index)">
+              <v-remixicon name="riDeleteBin7Line" />
+            </button>
+          </li>
+        </ul>
+      </div>
+      <div class="text-right p-4">
+        <ui-button class="mr-4" @click="clearTempTables(true)">
+          {{ t('common.cancel') }}
+        </ui-button>
+        <ui-button
+          :disabled="!state.name || state.columns.length === 0"
+          variant="accent"
+          @click="saveTable"
+        >
+          {{ t('common.save') }}
+        </ui-button>
+      </div>
+    </ui-card>
+  </ui-modal>
+</template>
+<script setup>
+import { reactive, toRaw, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { nanoid } from 'nanoid';
+import cloneDeep from 'lodash.clonedeep';
+import { dataTypes } from '@/utils/constants/table';
+
+const props = defineProps({
+  modelValue: {
+    type: Boolean,
+    default: false,
+  },
+  name: {
+    type: String,
+    default: '',
+  },
+  columns: {
+    type: Array,
+    default: () => [],
+  },
+});
+const emit = defineEmits(['update:modelValue', 'save']);
+
+const { t } = useI18n();
+
+let changes = {};
+const state = reactive({
+  name: '',
+  columns: [],
+});
+
+function getColumnName(name) {
+  const columnName = name.replace(/[\s@[\]]/g, '');
+  const isColumnExists = state.columns.some(
+    (column) => column.name === columnName
+  );
+
+  if (isColumnExists || columnName.trim() === '') return '';
+
+  return columnName;
+}
+function updateColumnName(index, target) {
+  const columnName = getColumnName(target.value);
+  const { id, name } = state.columns[index];
+  if (!columnName) {
+    target.value = name;
+    return;
+  }
+
+  changes[id] = { type: 'rename', id, oldValue: name, newValue: columnName };
+  state.columns[index].name = columnName;
+}
+function saveTable() {
+  const rawState = toRaw(state);
+
+  emit('save', { ...rawState, changes });
+}
+function addColumn() {
+  const columnId = nanoid(5);
+  const columnName = `column_${columnId}`;
+
+  changes[columnId] = {
+    type: 'add',
+    id: columnId,
+    name: columnName,
+  };
+
+  state.columns.push({
+    id: columnId,
+    type: 'string',
+    name: columnName,
+  });
+}
+function clearTempTables(close = false) {
+  state.name = '';
+  state.columns = [];
+  changes = {};
+
+  if (close) {
+    emit('update:modelValue', false);
+  }
+}
+function deleteColumn(index) {
+  const column = state.columns[index];
+  changes[column.id] = { type: 'delete', id: column.id, name: column.name };
+
+  state.columns.splice(index, 1);
+}
+
+watch(
+  () => props.modelValue,
+  () => {
+    if (props.modelValue) {
+      Object.assign(state, {
+        name: `${props.name}`,
+        columns: cloneDeep(props.columns),
+      });
+    } else {
+      clearTempTables();
+    }
+  }
+);
+</script>

+ 153 - 0
src/components/newtab/storage/StorageTables.vue

@@ -0,0 +1,153 @@
+<template>
+  <div class="flex mt-6">
+    <ui-input
+      v-model="state.query"
+      :placeholder="t('common.search')"
+      prepend-icon="riSearch2Line"
+    />
+    <div class="flex-grow"></div>
+    <ui-button variant="accent" @click="state.showAddTable = true">
+      {{ t('storage.table.add') }}
+    </ui-button>
+  </div>
+  <ui-table
+    item-key="id"
+    :headers="tableHeaders"
+    :items="items"
+    :search="state.query"
+    class="w-full mt-4"
+  >
+    <template #item-name="{ item }">
+      <router-link
+        :to="`/storage/tables/${item.id}`"
+        class="w-full block"
+        style="min-height: 29px"
+      >
+        {{ item.name }}
+      </router-link>
+    </template>
+    <template #item-createdAt="{ item }">
+      {{ formatDate(item.createdAt) }}
+    </template>
+    <template #item-modifiedAt="{ item }">
+      {{ formatDate(item.modifiedAt) }}
+    </template>
+    <template #item-actions="{ item }">
+      <v-remixicon
+        name="riDeleteBin7Line"
+        class="cursor-pointer"
+        @click="deleteTable(item)"
+      />
+    </template>
+  </ui-table>
+  <storage-edit-table v-model="state.showAddTable" @save="saveTable" />
+</template>
+<script setup>
+import { reactive } from 'vue';
+import { useI18n } from 'vue-i18n';
+import dayjs from 'dayjs';
+import { useDialog } from '@/composable/dialog';
+import { useWorkflowStore } from '@/stores/workflow';
+import { useLiveQuery } from '@/composable/liveQuery';
+import dbStorage from '@/db/storage';
+import StorageEditTable from './StorageEditTable.vue';
+
+const { t } = useI18n();
+const dialog = useDialog();
+const workflowStore = useWorkflowStore();
+
+const state = reactive({
+  query: '',
+  showAddTable: false,
+});
+
+const tableHeaders = [
+  {
+    value: 'name',
+    filterable: true,
+    text: t('common.name'),
+    attrs: {
+      class: 'w-4/12',
+    },
+  },
+  {
+    align: 'center',
+    value: 'createdAt',
+    text: t('storage.table.createdAt'),
+  },
+  {
+    align: 'center',
+    value: 'modifiedAt',
+    text: t('storage.table.modifiedAt'),
+  },
+  {
+    value: 'rowsCount',
+    align: 'center',
+    text: t('storage.table.rowsCount'),
+  },
+  {
+    value: 'actions',
+    align: 'right',
+    text: '',
+    sortable: false,
+  },
+];
+const items = useLiveQuery(() => dbStorage.tablesItems.reverse().toArray());
+
+function formatDate(date) {
+  return dayjs(date).format('DD MMM YYYY, hh:mm:ss A');
+}
+async function saveTable({ columns, name }) {
+  try {
+    const columnsIndex = columns.reduce(
+      (acc, column) => {
+        acc[column.id] = {
+          index: 0,
+          type: column.type,
+          name: column.name,
+        };
+
+        return acc;
+      },
+      { column: { index: 0, type: 'any', name: 'column' } }
+    );
+
+    const tableId = await dbStorage.tablesItems.add({
+      rowsCount: 0,
+      name,
+      createdAt: Date.now(),
+      modifiedAt: Date.now(),
+      columns,
+    });
+    await dbStorage.tablesData.add({
+      tableId,
+      items: [],
+      columnsIndex,
+    });
+
+    state.showAddTable = false;
+  } catch (error) {
+    console.error(error);
+  }
+}
+function deleteTable(table) {
+  dialog.confirm({
+    title: t('storage.table.delete'),
+    okVariant: 'danger',
+    body: t('message.delete', { name: table.name }),
+    onConfirm: async () => {
+      try {
+        await dbStorage.tablesItems.where('id').equals(table.id).delete();
+        await dbStorage.tablesData.where('tableId').equals(table.id).delete();
+
+        await workflowStore.update({
+          id: (workflow) => workflow.connectedTable === table.id,
+          data: { connectedTable: null },
+        });
+      } catch (error) {
+        console.error(error);
+      }
+    },
+  });
+}
+</script>

+ 168 - 0
src/components/newtab/storage/StorageVariables.vue

@@ -0,0 +1,168 @@
+<template>
+  <div class="flex mt-6">
+    <ui-input
+      v-model="state.query"
+      :placeholder="t('common.search')"
+      prepend-icon="riSearch2Line"
+    />
+    <div class="flex-grow"></div>
+    <ui-button variant="accent" @click="editState.show = true">
+      Add variable
+    </ui-button>
+  </div>
+  <ui-table
+    item-key="id"
+    :headers="tableHeaders"
+    :items="variables"
+    :search="state.query"
+    class="w-full mt-4"
+  >
+    <template #item-actions="{ item }">
+      <v-remixicon
+        name="riPencilLine"
+        class="cursor-pointer inline-block mr-4"
+        @click="editVariable(item)"
+      />
+      <v-remixicon
+        name="riDeleteBin7Line"
+        class="cursor-pointer inline-block"
+        @click="deleteVariable(item)"
+      />
+    </template>
+  </ui-table>
+  <ui-modal
+    v-model="editState.show"
+    :title="`${editState.type === 'edit' ? 'Edit' : 'Add'} variable`"
+  >
+    <ui-input v-model="editState.name" placeholder="Name" class="w-full" />
+    <ui-textarea
+      v-model="editState.value"
+      placeholder="value"
+      class="w-full mt-4"
+    />
+    <div class="text-right mt-8">
+      <ui-button class="mr-4" @click="editState.show = false">
+        {{ t('common.cancel') }}
+      </ui-button>
+      <ui-button
+        :disabled="!editState.name || editState.disabled"
+        variant="accent"
+        @click="saveVariable"
+      >
+        {{ t('common.save') }}
+      </ui-button>
+    </div>
+  </ui-modal>
+</template>
+<script setup>
+import { shallowReactive, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { useToast } from 'vue-toastification';
+import { parseJSON } from '@/utils/helper';
+import { useLiveQuery } from '@/composable/liveQuery';
+import dbStorage from '@/db/storage';
+
+const { t } = useI18n();
+const toast = useToast();
+const variables = useLiveQuery(() => dbStorage.variables.toArray());
+
+const tableHeaders = [
+  {
+    value: 'name',
+    filterable: true,
+    text: t('common.name'),
+    attrs: {
+      class: 'w-3/12 text-overflow',
+    },
+  },
+  {
+    value: 'value',
+    filterable: false,
+    text: 'Value',
+    attrs: {
+      class: 'flex-1 line-clamp',
+    },
+  },
+  {
+    value: 'actions',
+    filterable: false,
+    sortable: false,
+    text: '',
+    attrs: {
+      class: 'w-24',
+    },
+  },
+];
+
+const state = shallowReactive({
+  id: '',
+  query: '',
+});
+const editState = shallowReactive({
+  type: '',
+  name: '',
+  value: '',
+  show: false,
+});
+
+function deleteVariable({ id }) {
+  dbStorage.variables.delete(id);
+}
+function editVariable({ id, name, value }) {
+  state.id = id;
+  editState.name = name;
+  editState.value = value;
+  editState.type = 'edit';
+  editState.show = true;
+}
+function saveVariable() {
+  if (!editState.name) return;
+
+  const trimmedName = editState.name.trim();
+  const duplicateName = variables.value.some(
+    ({ name, id }) => name.trim() === trimmedName && id !== state.id
+  );
+
+  if (duplicateName) {
+    toast.error(`You alread add "${trimmedName}" variable`);
+    return;
+  }
+
+  const varValue = parseJSON(editState.value, editState.value);
+
+  if (editState.type === 'edit') {
+    dbStorage.variables
+      .update(state.id, {
+        value: varValue,
+        name: trimmedName,
+      })
+      .then(() => {
+        editState.show = false;
+      });
+  } else {
+    dbStorage.variables
+      .add({
+        value: varValue,
+        name: trimmedName,
+      })
+      .then(() => {
+        editState.show = false;
+      });
+  }
+}
+
+watch(
+  () => editState.show,
+  (value) => {
+    if (value) return;
+
+    state.id = '';
+    Object.assign(editState, {
+      name: '',
+      type: '',
+      value: '',
+      show: false,
+    });
+  }
+);
+</script>

+ 119 - 26
src/components/newtab/workflow/WorkflowDataTable.vue

@@ -1,16 +1,58 @@
 <template>
 <template>
-  <div class="flex mb-4">
-    <ui-input
-      v-model="state.query"
-      autofocus
-      :placeholder="t('workflow.table.placeholder')"
-      class="mr-2 flex-1"
-      @keyup.enter="addColumn"
-      @keyup.esc="$emit('close')"
+  <template v-if="!workflow.connectedTable">
+    <ui-popover class="mb-4">
+      <template #trigger>
+        <ui-button> Connect to a storage table </ui-button>
+      </template>
+      <p>Select a table</p>
+      <ui-list class="mt-2 space-y-1 max-h-80 overflow-auto w-64">
+        <p v-if="state.tableList.length === 0">
+          {{ t('message.noData') }}
+        </p>
+        <ui-list-item
+          v-for="item in state.tableList"
+          :key="item.id"
+          class="text-overflow cursor-pointer"
+          @click="connectTable(item)"
+        >
+          {{ item.name }}
+        </ui-list-item>
+      </ui-list>
+    </ui-popover>
+    <div class="flex mb-4">
+      <ui-input
+        v-model="state.query"
+        autofocus
+        :placeholder="t('workflow.table.placeholder')"
+        class="mr-2 flex-1"
+        @keyup.enter="addColumn"
+        @keyup.esc="$emit('close')"
+      />
+      <ui-button variant="accent" @click="addColumn">
+        {{ t('common.add') }}
+      </ui-button>
+    </div>
+  </template>
+  <div
+    v-else-if="state.connectedTable"
+    class="py-2 px-4 rounded-md bg-green-200 dark:bg-green-300 flex items-center mb-4"
+  >
+    <p class="mr-1 text-black">
+      This workflow is connected to the
+      <router-link
+        :to="`/storage/tables/${state.connectedTable.id}`"
+        class="underline"
+      >
+        {{ state.connectedTable.name }}
+      </router-link>
+      table
+    </p>
+    <v-remixicon
+      name="riLinkUnlinkM"
+      title="Disconnect table"
+      class="cursor-pointer"
+      @click="disconnectTable"
     />
     />
-    <ui-button variant="accent" @click="addColumn">
-      {{ t('common.add') }}
-    </ui-button>
   </div>
   </div>
   <div
   <div
     class="overflow-y-auto scroll"
     class="overflow-y-auto scroll"
@@ -26,6 +68,7 @@
         class="flex items-center space-x-2"
         class="flex items-center space-x-2"
       >
       >
         <ui-input
         <ui-input
+          :disabled="Boolean(workflow.connectedTable)"
           :model-value="columns[index].name"
           :model-value="columns[index].name"
           :placeholder="t('workflow.table.column.name')"
           :placeholder="t('workflow.table.column.name')"
           class="flex-1"
           class="flex-1"
@@ -33,14 +76,18 @@
         />
         />
         <ui-select
         <ui-select
           v-model="columns[index].type"
           v-model="columns[index].type"
-          class="flex-1"
+          :disabled="Boolean(workflow.connectedTable)"
           :placeholder="t('workflow.table.column.type')"
           :placeholder="t('workflow.table.column.type')"
+          class="flex-1"
         >
         >
           <option v-for="type in dataTypes" :key="type.id" :value="type.id">
           <option v-for="type in dataTypes" :key="type.id" :value="type.id">
             {{ type.name }}
             {{ type.name }}
           </option>
           </option>
         </ui-select>
         </ui-select>
-        <button @click="state.columns.splice(index, 1)">
+        <button
+          v-if="!Boolean(workflow.connectedTable)"
+          @click="state.columns.splice(index, 1)"
+        >
           <v-remixicon name="riDeleteBin7Line" />
           <v-remixicon name="riDeleteBin7Line" />
         </button>
         </button>
       </li>
       </li>
@@ -51,7 +98,10 @@
 import { computed, onMounted, watch, reactive } from 'vue';
 import { computed, onMounted, watch, reactive } from 'vue';
 import { nanoid } from 'nanoid';
 import { nanoid } from 'nanoid';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
+import dbStorage from '@/db/storage';
 import { debounce } from '@/utils/helper';
 import { debounce } from '@/utils/helper';
+import { dataTypes } from '@/utils/constants/table';
+import { useWorkflowStore } from '@/stores/workflow';
 
 
 const props = defineProps({
 const props = defineProps({
   workflow: {
   workflow: {
@@ -59,25 +109,28 @@ const props = defineProps({
     default: () => ({}),
     default: () => ({}),
   },
   },
 });
 });
-const emit = defineEmits(['update', 'close', 'change']);
+const emit = defineEmits([
+  'update',
+  'close',
+  'change',
+  'connect',
+  'disconnect',
+]);
 
 
 const { t } = useI18n();
 const { t } = useI18n();
-
-const dataTypes = [
-  { id: 'any', name: 'Any' },
-  { id: 'string', name: 'Text' },
-  { id: 'integer', name: 'Number' },
-  { id: 'boolean', name: 'Boolean' },
-  { id: 'array', name: 'Array' },
-];
+const workflowStore = useWorkflowStore();
 
 
 const state = reactive({
 const state = reactive({
   query: '',
   query: '',
   columns: [],
   columns: [],
+  tableList: [],
+  connectedTable: null,
+});
+const columns = computed(() => {
+  if (state.connectedTable) return state.connectedTable.columns;
+
+  return state.columns.filter(({ name }) => name.includes(state.query));
 });
 });
-const columns = computed(() =>
-  state.columns.filter(({ name }) => name.includes(state.query))
-);
 
 
 function getColumnName(name) {
 function getColumnName(name) {
   const columnName = name.replace(/[\s@[\]]/g, '');
   const columnName = name.replace(/[\s@[\]]/g, '');
@@ -107,10 +160,36 @@ function addColumn() {
   state.columns.push({ id: nanoid(5), name: columnName, type: 'string' });
   state.columns.push({ id: nanoid(5), name: columnName, type: 'string' });
   state.query = '';
   state.query = '';
 }
 }
+function connectTable(table) {
+  workflowStore
+    .update({
+      id: props.workflow.id,
+      data: { connectedTable: table.id },
+    })
+    .then(() => {
+      emit('connect');
+      state.query = '';
+      state.connectedTable = table;
+    });
+}
+function disconnectTable() {
+  workflowStore
+    .update({
+      id: props.workflow.id,
+      data: { connectedTable: null },
+    })
+    .then(() => {
+      state.columns = props.workflow.table;
+      state.connectedTable = null;
+      emit('disconnect');
+    });
+}
 
 
 watch(
 watch(
   () => state.columns,
   () => state.columns,
   debounce((newValue) => {
   debounce((newValue) => {
+    if (props.workflow.connectedTable) return;
+
     const data = { table: newValue };
     const data = { table: newValue };
 
 
     emit('update', data);
     emit('update', data);
@@ -119,7 +198,21 @@ watch(
   { deep: true }
   { deep: true }
 );
 );
 
 
-onMounted(() => {
+onMounted(async () => {
+  state.tableList = await dbStorage.tablesItems.toArray();
+  if (props.workflow.connectedTable) {
+    const findTable = state.tableList.find(
+      (table) => table.id === props.workflow.connectedTable
+    );
+
+    if (findTable) {
+      state.connectedTable = findTable;
+      return;
+    }
+    emit('change', { connectedTable: null });
+    emit('update', { connectedTable: null });
+  }
+
   let isChanged = false;
   let isChanged = false;
   state.columns =
   state.columns =
     props.workflow.table?.map((column) => {
     props.workflow.table?.map((column) => {

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

@@ -45,7 +45,7 @@
           </option>
           </option>
           <option value="column">Column</option>
           <option value="column">Column</option>
           <option
           <option
-            v-for="column in workflow.data.value.table"
+            v-for="column in workflow.columns.value"
             :key="column.id"
             :key="column.id"
             :value="column.id"
             :value="column.id"
           >
           >

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

@@ -38,7 +38,7 @@
             :placeholder="t('workflow.table.select')"
             :placeholder="t('workflow.table.select')"
           >
           >
             <option
             <option
-              v-for="column in workflow.data.value.table"
+              v-for="column in workflow.columns.value"
               :key="column.id"
               :key="column.id"
               :value="column.id"
               :value="column.id"
             >
             >
@@ -78,7 +78,7 @@ const emit = defineEmits(['update:data']);
 
 
 const { t } = useI18n();
 const { t } = useI18n();
 
 
-const workflow = inject('workflow');
+const workflow = inject('workflow', {});
 const dataList = ref(JSON.parse(JSON.stringify(props.data.dataList)));
 const dataList = ref(JSON.parse(JSON.stringify(props.data.dataList)));
 
 
 function updateData(value) {
 function updateData(value) {

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

@@ -83,7 +83,7 @@
       @change="updateData({ dataColumn: $event })"
       @change="updateData({ dataColumn: $event })"
     >
     >
       <option
       <option
-        v-for="column in workflow.data.value.table"
+        v-for="column in workflow.columns.value"
         :key="column.id || column.name"
         :key="column.id || column.name"
         :value="column.id || column.name"
         :value="column.id || column.name"
       >
       >

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

@@ -33,7 +33,7 @@
     @change="updateData({ dataColumn: $event })"
     @change="updateData({ dataColumn: $event })"
   >
   >
     <option
     <option
-      v-for="column in [...columns, ...workflow.data.value.table]"
+      v-for="column in [...columns, ...workflow.columns.value]"
       :key="column.id"
       :key="column.id"
       :value="column.id"
       :value="column.id"
     >
     >
@@ -64,7 +64,7 @@
         @change="updateData({ extraRowDataColumn: $event })"
         @change="updateData({ extraRowDataColumn: $event })"
       >
       >
         <option
         <option
-          v-for="column in workflow.data.value.table"
+          v-for="column in [...columns, ...workflow.columns.value]"
           :key="column.id"
           :key="column.id"
           :value="column.id"
           :value="column.id"
         >
         >

+ 6 - 1
src/components/ui/UiSelect.vue

@@ -18,7 +18,11 @@
       />
       />
       <select
       <select
         :id="selectId"
         :id="selectId"
-        :class="{ 'pl-8': prependIcon }"
+        :disabled="disabled"
+        :class="{
+          'pl-8': prependIcon,
+          'opacity-75 pointer-events-none': disabled,
+        }"
         :value="modelValue"
         :value="modelValue"
         class="px-4 pr-10 transition rounded-lg bg-input bg-transparent py-2 z-10 appearance-none w-full h-full appearance-none"
         class="px-4 pr-10 transition rounded-lg bg-input bg-transparent py-2 z-10 appearance-none w-full h-full appearance-none"
         @change="emitValue"
         @change="emitValue"
@@ -62,6 +66,7 @@ export default {
       default: () => ({}),
       default: () => ({}),
     },
     },
     block: Boolean,
     block: Boolean,
+    disabled: Boolean,
     showDetail: Boolean,
     showDetail: Boolean,
   },
   },
   emits: ['update:modelValue', 'change'],
   emits: ['update:modelValue', 'change'],

+ 8 - 3
src/components/ui/UiTable.vue

@@ -41,6 +41,7 @@
           v-bind="header.rowAttrs"
           v-bind="header.rowAttrs"
           :key="header.value"
           :key="header.value"
           :align="header.align"
           :align="header.align"
+          v-on="header.rowEvents || {}"
         >
         >
           <slot :name="`item-${header.value}`" :item="item">
           <slot :name="`item-${header.value}`" :item="item">
             {{ item[header.value] }}
             {{ item[header.value] }}
@@ -94,9 +95,13 @@ const filteredItems = computed(() => {
   const filterFunc =
   const filterFunc =
     props.customFilter ||
     props.customFilter ||
     ((search, item) => {
     ((search, item) => {
-      return table.filterKeys.some((key) =>
-        item[key].toLocaleLowerCase().includes(search)
-      );
+      return table.filterKeys.some((key) => {
+        const value = item[key];
+        if (typeof value === 'string')
+          return value.toLocaleLowerCase().includes(search);
+
+        return value === search;
+      });
     });
     });
 
 
   const search = props.search.toLocaleLowerCase();
   const search = props.search.toLocaleLowerCase();

+ 10 - 0
src/db/storage.js

@@ -0,0 +1,10 @@
+import Dexie from 'dexie';
+
+const dbStorage = new Dexie('storage');
+dbStorage.version(1).stores({
+  tablesData: '++id, tableId',
+  tablesItems: '++id, name, createdAt, modifiedAt',
+  variables: '++id, &name',
+});
+
+export default dbStorage;

+ 10 - 3
src/locales/en/newtab.json

@@ -22,6 +22,16 @@
       }
       }
     }
     }
   },
   },
+  "storage": {
+    "title": "Storage",
+    "table": {
+      "add": "Add table",
+      "createdAt": "Created at",
+      "modifiedAt": "Modified at",
+      "rowsCount": "Rows count",
+      "delete": "Delete table"
+    }
+  },
   "workflowPermissions": {
   "workflowPermissions": {
     "title": "Workflow permissions",
     "title": "Workflow permissions",
     "description": "This workflow requires these permissions to run properly",
     "description": "This workflow requires these permissions to run properly",
@@ -65,9 +75,6 @@
     "start": "Started on {date}",
     "start": "Started on {date}",
     "message": "This only display the last 5 logs"
     "message": "This only display the last 5 logs"
   },
   },
-  "storage": {
-    "title": "Storage"
-  },
   "settings": {
   "settings": {
     "theme": "Theme",
     "theme": "Theme",
     "shortcuts": {
     "shortcuts": {

+ 35 - 0
src/newtab/pages/Storage.vue

@@ -0,0 +1,35 @@
+<template>
+  <div class="container py-8 pb-4">
+    <h1 class="text-2xl font-semibold">
+      {{ t('common.storage') }}
+    </h1>
+    <ui-tabs v-model="state.activeTab" class="mt-5">
+      <ui-tab value="tables">
+        {{ t('workflow.table.title', 2) }}
+      </ui-tab>
+      <ui-tab value="variables">
+        {{ t('workflow.variables.title', 2) }}
+      </ui-tab>
+    </ui-tabs>
+    <ui-tab-panels v-model="state.activeTab">
+      <ui-tab-panel value="tables">
+        <storage-tables />
+      </ui-tab-panel>
+      <ui-tab-panel value="variables">
+        <storage-variables />
+      </ui-tab-panel>
+    </ui-tab-panels>
+  </div>
+</template>
+<script setup>
+import { reactive } from 'vue';
+import { useI18n } from 'vue-i18n';
+import StorageTables from '@/components/newtab/storage/StorageTables.vue';
+import StorageVariables from '@/components/newtab/storage/StorageVariables.vue';
+
+const { t } = useI18n();
+
+const state = reactive({
+  activeTab: 'variables',
+});
+</script>

+ 328 - 0
src/newtab/pages/storage/Tables.vue

@@ -0,0 +1,328 @@
+<template>
+  <div v-if="tableDetail && tableData" class="container py-8 pb-4">
+    <div class="mb-12 flex items-center">
+      <h1 class="font-semibold text-3xl">
+        {{ tableDetail.name }}
+      </h1>
+      <div class="flex-grow"></div>
+      <ui-button v-tooltip="'Delete table'" icon @click="deleteTable">
+        <v-remixicon name="riDeleteBin7Line" />
+      </ui-button>
+    </div>
+    <div class="flex items-center mb-4">
+      <ui-input
+        v-model="state.query"
+        :placeholder="t('common.search')"
+        prepend-icon="riSearch2Line"
+      />
+      <ui-button class="ml-4" @click="editTable">
+        <v-remixicon name="riPencilLine" class="mr-2 -ml-1" />
+        <span>Edit table</span>
+      </ui-button>
+      <div class="flex-1"></div>
+      <ui-button class="text-red-400 dark:text-red-300" @click="clearData">
+        <v-remixicon name="riDeleteBin7Line" class="mr-2 -ml-1" />
+        <span>Clear data</span>
+      </ui-button>
+    </div>
+    <ui-table
+      :headers="table.header"
+      :items="rows"
+      :search="state.query"
+      item-key="id"
+      class="w-full"
+    >
+      <template #item-action="{ item }">
+        <v-remixicon
+          title="Delete row"
+          class="cursor-pointer"
+          name="riDeleteBin7Line"
+          @click="deleteRow(item)"
+        />
+      </template>
+    </ui-table>
+    <div
+      v-if="table.body && table.body.length >= 10"
+      class="flex items-center justify-between mt-4"
+    >
+      <div>
+        {{ t('components.pagination.text1') }}
+        <select v-model="pagination.perPage" class="p-1 rounded-md bg-input">
+          <option
+            v-for="num in [10, 15, 25, 50, 100, 150]"
+            :key="num"
+            :value="num"
+          >
+            {{ num }}
+          </option>
+        </select>
+        {{
+          t('components.pagination.text2', {
+            count: table.body.length,
+          })
+        }}
+      </div>
+      <ui-pagination
+        v-model="pagination.currentPage"
+        :per-page="pagination.perPage"
+        :records="table.body.length"
+      />
+    </div>
+    <storage-edit-table
+      v-model="editState.show"
+      :name="editState.name"
+      :columns="editState.columns"
+      @save="saveEditedTable"
+    />
+  </div>
+</template>
+<script setup>
+import {
+  watch,
+  shallowRef,
+  shallowReactive,
+  computed,
+  toRaw,
+  triggerRef,
+} from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+import { useWorkflowStore } from '@/stores/workflow';
+import { useLiveQuery } from '@/composable/liveQuery';
+import { useDialog } from '@/composable/dialog';
+import { objectHasKey } from '@/utils/helper';
+import StorageEditTable from '@/components/newtab/storage/StorageEditTable.vue';
+import dbStorage from '@/db/storage';
+
+const { t } = useI18n();
+const route = useRoute();
+const dialog = useDialog();
+const router = useRouter();
+const workflowStore = useWorkflowStore();
+
+const tableId = +route.params.id;
+
+const tableDetail = useLiveQuery(() =>
+  dbStorage.tablesItems.where('id').equals(tableId).first()
+);
+const tableData = useLiveQuery(() =>
+  dbStorage.tablesData.where('tableId').equals(tableId).first()
+);
+
+const table = shallowRef({
+  body: [],
+  header: [],
+});
+const state = shallowReactive({
+  query: '',
+});
+const editState = shallowReactive({
+  name: '',
+  columns: [],
+  show: false,
+});
+const pagination = shallowReactive({
+  perPage: 10,
+  currentPage: 1,
+});
+
+const rows = computed(() =>
+  table.value.body.slice(
+    (pagination.currentPage - 1) * pagination.perPage,
+    pagination.currentPage * pagination.perPage
+  )
+);
+
+function editTable() {
+  editState.name = tableDetail.value.name;
+  editState.columns = tableDetail.value.columns;
+  editState.show = true;
+}
+function additionalHeaders(headers) {
+  headers.unshift({ value: '$$id', text: '', sortable: false });
+  headers.push({
+    value: 'action',
+    text: '',
+    sortable: false,
+    align: 'right',
+    attrs: {
+      width: '100px',
+    },
+  });
+
+  return headers;
+}
+async function saveEditedTable({ columns, name, changes }) {
+  const columnsChanges = Object.values(changes);
+
+  try {
+    await dbStorage.tablesItems.update(tableId, {
+      name,
+      columns,
+    });
+
+    const headers = [];
+    const newTableData = [];
+    const newColumnsIndex = {};
+    const { columnsIndex } = tableData.value;
+
+    columns.forEach(({ name: columnName, id, type }) => {
+      const index = columnsIndex[id]?.index || 0;
+
+      newColumnsIndex[id] = {
+        type,
+        index,
+        name: columnName,
+      };
+      headers.push({
+        text: columnName,
+        value: columnName,
+        filterable: ['string', 'any'].includes(type),
+      });
+    });
+
+    if (columnsIndex.column) {
+      newColumnsIndex.column = toRaw(columnsIndex.column);
+    }
+
+    table.value.header = additionalHeaders(headers);
+    table.value.body = table.value.body.map((item, index) => {
+      columnsChanges.forEach(
+        ({ type, oldValue, newValue, name: columnName }) => {
+          if (type === 'rename' && objectHasKey(item, oldValue)) {
+            item[newValue] = item[oldValue];
+
+            delete item[oldValue];
+          } else if (type === 'delete') {
+            delete item[columnName];
+          }
+        }
+      );
+
+      delete item.$$id;
+      newTableData.push({ ...item });
+      item.$$id = index + 1;
+
+      return item;
+    });
+
+    await dbStorage.tablesData.where('tableId').equals(tableId).modify({
+      items: newTableData,
+      columnsIndex: newColumnsIndex,
+    });
+
+    editState.show = false;
+  } catch (error) {
+    console.error(error);
+  }
+}
+function deleteRow(item) {
+  const rowIndex = table.value.body.findIndex(({ $$id }) => $$id === item.$$id);
+  if (rowIndex === -1) return;
+
+  const cache = {};
+  const { columnsIndex } = tableData.value;
+  const columns = Object.values(tableDetail.value.columns);
+
+  Object.keys(item).forEach((key) => {
+    if (key === '$$id') return;
+
+    const column =
+      cache[key] || columns.find((currColumn) => currColumn.name === key);
+    if (!column) return;
+
+    const columnIndex = columnsIndex[column.id];
+    if (columnIndex && columnIndex.index >= item.$$id - 1) {
+      columnIndex.index -= 1;
+    }
+
+    cache[key] = column;
+  });
+
+  table.value.body.splice(rowIndex, 1);
+  tableData.value.items.splice(rowIndex, 1);
+
+  dbStorage.tablesItems.update(tableId, {
+    modifiedAt: Date.now(),
+    rowsCount: tableDetail.value.rowsCount - 1,
+  });
+  dbStorage.tablesData
+    .where('tableId')
+    .equals(tableId)
+    .modify({ items: toRaw(tableData.value.items) })
+    .then(() => {
+      triggerRef(table);
+    });
+}
+function clearData() {
+  dialog.confirm({
+    title: 'Clear data',
+    okVariant: 'danger',
+    body: 'Are you sure want to clear the table data?',
+    onConfirm: async () => {
+      await dbStorage.tablesItems.update(tableId, {
+        rowsCount: 0,
+        modifiedAt: Date.now(),
+      });
+
+      const columnsIndex = tableDetail.value.columns.reduce(
+        (acc, column) => {
+          acc[column.id] = {
+            index: 0,
+            type: column.type,
+            name: column.name,
+          };
+
+          return acc;
+        },
+        { column: { index: 0, type: 'any', name: 'column' } }
+      );
+      await dbStorage.tablesData.where('tableId').equals(tableId).modify({
+        items: [],
+        columnsIndex,
+      });
+    },
+  });
+}
+function deleteTable() {
+  dialog.confirm({
+    title: t('storage.table.delete'),
+    okVariant: 'danger',
+    body: t('message.delete', { name: tableDetail.value.name }),
+    onConfirm: async () => {
+      try {
+        await dbStorage.tablesItems.where('id').equals(tableId).delete();
+        await dbStorage.tablesData.where('tableId').equals(tableId).delete();
+
+        await workflowStore.update({
+          id: (workflow) => workflow.connectedTable === tableId,
+          data: { connectedTable: null },
+        });
+
+        router.replace('/storage');
+      } catch (error) {
+        console.error(error);
+      }
+    },
+  });
+}
+
+watch(tableData, () => {
+  if (!tableDetail.value || !tableData.value) return;
+
+  const dataTable = { header: [], body: [] };
+  const headers = tableDetail.value.columns.map(({ name, type }) => ({
+    text: name,
+    value: name,
+    filterable: ['string', 'any'].includes(type),
+  }));
+
+  dataTable.body = tableData.value.items.map((item, index) => ({
+    ...item,
+    $$id: index + 1,
+  }));
+  dataTable.header = additionalHeaders(headers);
+
+  table.value = dataTable;
+});
+</script>

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

@@ -162,6 +162,7 @@ import { tasks } from '@/utils/shared';
 import { fetchApi } from '@/utils/api';
 import { fetchApi } from '@/utils/api';
 import { debounce, parseJSON, throttle } from '@/utils/helper';
 import { debounce, parseJSON, throttle } from '@/utils/helper';
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
+import dbStorage from '@/db/storage';
 import DroppedNode from '@/utils/editor/DroppedNode';
 import DroppedNode from '@/utils/editor/DroppedNode';
 import convertWorkflowData from '@/utils/convertWorkflowData';
 import convertWorkflowData from '@/utils/convertWorkflowData';
 import WorkflowShare from '@/components/newtab/workflow/WorkflowShare.vue';
 import WorkflowShare from '@/components/newtab/workflow/WorkflowShare.vue';
@@ -186,6 +187,7 @@ const userStore = useUserStore();
 const workflowStore = useWorkflowStore();
 const workflowStore = useWorkflowStore();
 
 
 const editor = shallowRef(null);
 const editor = shallowRef(null);
+const connectedTable = shallowRef(null);
 
 
 const state = reactive({
 const state = reactive({
   showSidebar: true,
   showSidebar: true,
@@ -222,6 +224,13 @@ const workflowModals = {
     component: WorkflowDataTable,
     component: WorkflowDataTable,
     title: t('workflow.table.title'),
     title: t('workflow.table.title'),
     docs: 'https://docs.automa.site/api-reference/table.html',
     docs: 'https://docs.automa.site/api-reference/table.html',
+    events: {
+      /* eslint-disable-next-line */
+      connect: fetchConnectedTable,
+      disconnect() {
+        connectedTable.value = null;
+      },
+    },
   },
   },
   'workflow-share': {
   'workflow-share': {
     icon: 'riShareLine',
     icon: 'riShareLine',
@@ -273,10 +282,18 @@ const workflowStates = computed(() =>
 const activeWorkflowModal = computed(
 const activeWorkflowModal = computed(
   () => workflowModals[modalState.name] || {}
   () => workflowModals[modalState.name] || {}
 );
 );
+const workflowColumns = computed(() => {
+  if (connectedTable.value) {
+    return connectedTable.value.columns;
+  }
+
+  return workflow.value.table;
+});
 
 
 provide('workflow', {
 provide('workflow', {
   editState,
   editState,
   data: workflow,
   data: workflow,
+  columns: workflowColumns,
 });
 });
 provide('workflow-editor', editor);
 provide('workflow-editor', editor);
 
 
@@ -704,6 +721,15 @@ function onKeydown({ ctrlKey, metaKey, key }) {
     pasteCopiedElements();
     pasteCopiedElements();
   }
   }
 }
 }
+async function fetchConnectedTable() {
+  const table = await dbStorage.tablesItems
+    .where('id')
+    .equals(workflow.value.connectedTable)
+    .first();
+  if (!table) return;
+
+  connectedTable.value = table;
+}
 
 
 const shortcut = useShortcut([
 const shortcut = useShortcut([
   getShortcut('editor:toggle-sidebar', toggleSidebar),
   getShortcut('editor:toggle-sidebar', toggleSidebar),
@@ -750,6 +776,10 @@ onMounted(() => {
     });
     });
   }
   }
 
 
+  if (workflow.value.connectedTable) {
+    fetchConnectedTable();
+  }
+
   window.onbeforeunload = () => {
   window.onbeforeunload = () => {
     updateHostedWorkflow();
     updateHostedWorkflow();
 
 

+ 12 - 0
src/newtab/router.js

@@ -5,6 +5,8 @@ import WorkflowHost from './pages/workflows/Host.vue';
 import WorkflowDetails from './pages/workflows/[id].vue';
 import WorkflowDetails from './pages/workflows/[id].vue';
 import WorkflowShared from './pages/workflows/Shared.vue';
 import WorkflowShared from './pages/workflows/Shared.vue';
 import ScheduledWorkflow from './pages/ScheduledWorkflow.vue';
 import ScheduledWorkflow from './pages/ScheduledWorkflow.vue';
+import Storage from './pages/Storage.vue';
+import StorageTables from './pages/storage/Tables.vue';
 import Logs from './pages/Logs.vue';
 import Logs from './pages/Logs.vue';
 import LogsDetails from './pages/logs/[id].vue';
 import LogsDetails from './pages/logs/[id].vue';
 import LogsRunning from './pages/logs/Running.vue';
 import LogsRunning from './pages/logs/Running.vue';
@@ -52,6 +54,16 @@ const routes = [
     path: '/workflows/:id/shared',
     path: '/workflows/:id/shared',
     component: WorkflowShared,
     component: WorkflowShared,
   },
   },
+  {
+    name: 'storage',
+    path: '/storage',
+    component: Storage,
+  },
+  {
+    name: 'storage-tables',
+    path: '/storage/tables/:id',
+    component: StorageTables,
+  },
   {
   {
     name: 'logs',
     name: 'logs',
     path: '/logs',
     path: '/logs',

+ 29 - 6
src/stores/workflow.js

@@ -19,6 +19,7 @@ const defaultWorkflow = (data = null) => {
     name: '',
     name: '',
     icon: 'riGlobalLine',
     icon: 'riGlobalLine',
     folderId: null,
     folderId: null,
+    connectedTable: null,
     drawflow: {
     drawflow: {
       edges: [],
       edges: [],
       position: { zoom: 1 },
       position: { zoom: 1 },
@@ -40,6 +41,7 @@ const defaultWorkflow = (data = null) => {
     description: '',
     description: '',
     trigger: null,
     trigger: null,
     createdAt: Date.now(),
     createdAt: Date.now(),
+    updatedAt: Date.now(),
     isDisabled: false,
     isDisabled: false,
     settings: {
     settings: {
       publicId: '',
       publicId: '',
@@ -146,18 +148,39 @@ export const useWorkflowStore = defineStore('workflow', {
 
 
       return insertedWorkflows;
       return insertedWorkflows;
     },
     },
-    async update({ id, data = {}, deep = false }) {
-      if (!this.workflows[id]) return null;
+    async update({ id, data = {}, deep = false, checkLastUpdate = false }) {
+      const isFunction = typeof id === 'function';
+      if (!isFunction && !this.workflows[id]) return null;
+
+      const updatedWorkflows = {};
+      const workflowUpdater = (workflowId) => {
+        console.log(checkLastUpdate);
+
+        if (deep) {
+          this.workflows[workflowId] = deepmerge(
+            this.workflows[workflowId],
+            data
+          );
+        } else {
+          Object.assign(this.workflows[workflowId], data);
+        }
 
 
-      if (deep) {
-        this.workflows[id] = deepmerge(this.workflows[id], data);
+        this.workflows[workflowId].updatedAt = Date.now();
+        updatedWorkflows[workflowId] = this.workflows[workflowId];
+      };
+
+      if (isFunction) {
+        this.getWorkflows.forEach((workflow) => {
+          const isMatch = id(workflow) ?? false;
+          if (isMatch) workflowUpdater(workflow.id);
+        });
       } else {
       } else {
-        Object.assign(this.workflows[id], data);
+        workflowUpdater(id);
       }
       }
 
 
       await this.saveToStorage('workflows');
       await this.saveToStorage('workflows');
 
 
-      return this.workflows;
+      return updatedWorkflows;
     },
     },
     async insertOrUpdate(data = []) {
     async insertOrUpdate(data = []) {
       const insertedData = {};
       const insertedData = {};

+ 7 - 0
src/utils/constants/table.js

@@ -0,0 +1,7 @@
+export const dataTypes = [
+  { id: 'any', name: 'Any' },
+  { id: 'string', name: 'Text' },
+  { id: 'integer', name: 'Number' },
+  { id: 'boolean', name: 'Boolean' },
+  { id: 'array', name: 'Array' },
+];