Browse Source

feat: add global data to the workflow

Ahmad Kholid 3 years ago
parent
commit
2ba67b6af5

+ 13 - 3
src/background/workflow-engine/index.js

@@ -2,7 +2,7 @@
 import browser from 'webextension-polyfill';
 import { nanoid } from 'nanoid';
 import { tasks } from '@/utils/shared';
-import { toCamelCase } from '@/utils/helper';
+import { toCamelCase, parseJSON } from '@/utils/helper';
 import { generateJSON } from '@/utils/data-exporter';
 import errorMessage from './error-message';
 import referenceData from '@/utils/reference-data';
@@ -17,7 +17,10 @@ function tabRemovedHandler(tabId) {
 
   delete this.tabId;
 
-  if (tasks[this.currentBlock.name].category === 'interaction') {
+  if (
+    this.currentBlock.name === 'new-tab' ||
+    tasks[this.currentBlock.name].category === 'interaction'
+  ) {
     this.destroy('error', 'Current active tab is removed');
   }
 
@@ -58,12 +61,18 @@ function tabUpdatedHandler(tabId, changeInfo) {
 }
 
 class WorkflowEngine {
-  constructor(workflow, { tabId = null, isInCollection, collectionLogId }) {
+  constructor(
+    workflow,
+    { globalData, tabId = null, isInCollection, collectionLogId }
+  ) {
+    const globalDataVal = globalData ?? workflow.globalData;
+
     this.id = nanoid();
     this.tabId = tabId;
     this.workflow = workflow;
     this.isInCollection = isInCollection;
     this.collectionLogId = collectionLogId;
+    this.globalData = parseJSON(globalDataVal, globalDataVal);
     this.data = {};
     this.logs = [];
     this.blocks = {};
@@ -264,6 +273,7 @@ class WorkflowEngine {
         prevBlockData,
         data: this.data,
         loopData: this.loopData,
+        globalData: this.globalData,
       });
 
       handler

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

@@ -0,0 +1,116 @@
+<template>
+  <ui-card padding="p-1">
+    <button
+      v-for="item in modalActions"
+      :key="item.id"
+      v-tooltip.group="item.name"
+      class="hoverable p-2 rounded-lg"
+      @click="$emit('showModal', item.id)"
+    >
+      <v-remixicon :name="item.icon" />
+    </button>
+  </ui-card>
+  <ui-card padding="p-1 ml-4">
+    <button
+      v-tooltip.group="'Execute'"
+      icon
+      class="hoverable p-2 rounded-lg"
+      @click="$emit('execute')"
+    >
+      <v-remixicon name="riPlayLine" />
+    </button>
+  </ui-card>
+  <ui-card padding="p-1 ml-4 space-x-1">
+    <ui-popover>
+      <template #trigger>
+        <button class="rounded-lg p-2 hoverable">
+          <v-remixicon name="riMore2Line" />
+        </button>
+      </template>
+      <ui-list class="w-36">
+        <ui-list-item
+          v-for="item in moreActions"
+          :key="item.id"
+          v-close-popover
+          class="cursor-pointer"
+          @click="$emit(item.id)"
+        >
+          <v-remixicon :name="item.icon" class="mr-2 -ml-1" />
+          {{ item.name }}
+        </ui-list-item>
+      </ui-list>
+    </ui-popover>
+    <ui-button variant="accent" class="relative" @click="$emit('save')">
+      <span
+        v-if="isDataChanged"
+        class="flex h-3 w-3 absolute top-0 left-0 -ml-1 -mt-1"
+      >
+        <span
+          class="
+            animate-ping
+            absolute
+            inline-flex
+            h-full
+            w-full
+            rounded-full
+            bg-primary
+            opacity-75
+          "
+        ></span>
+        <span
+          class="relative inline-flex rounded-full h-3 w-3 bg-blue-600"
+        ></span>
+      </span>
+      <v-remixicon name="riSaveLine" class="mr-2 -ml-1 my-1" />
+      Save
+    </ui-button>
+  </ui-card>
+</template>
+<script setup>
+import { useGroupTooltip } from '@/composable/groupTooltip';
+
+defineProps({
+  isDataChanged: {
+    type: Boolean,
+    default: false,
+  },
+});
+defineEmits(['showModal', 'execute', 'rename', 'delete', 'save', 'export']);
+
+useGroupTooltip();
+
+const modalActions = [
+  {
+    id: 'data-columns',
+    name: 'Data columns',
+    icon: 'riKey2Line',
+  },
+  {
+    id: 'global-data',
+    name: 'Global data',
+    icon: 'riDatabase2Line',
+  },
+  {
+    id: 'settings',
+    name: 'Settings',
+    icon: 'riSettings3Line',
+  },
+];
+const moreActions = [
+  {
+    id: 'export',
+    name: 'Export',
+    icon: 'riDownloadLine',
+  },
+  {
+    id: 'rename',
+    name: 'Rename',
+    icon: 'riPencilLine',
+  },
+  {
+    id: 'delete',
+    name: 'Delete',
+    icon: 'riDeleteBin7Line',
+  },
+];
+</script>

+ 1 - 1
src/components/newtab/workflow/WorkflowDataColumns.vue

@@ -12,7 +12,7 @@
   </div>
   <ul
     class="space-y-2 overflow-y-auto scroll py-1"
-    style="max-height: calc(100vh - 10rem)"
+    style="max-height: calc(100vh - 11rem)"
   >
     <li
       v-for="(column, index) in columns"

+ 6 - 115
src/components/newtab/workflow/WorkflowDetailsCard.vue

@@ -1,21 +1,9 @@
 <template>
-  <div class="px-4 flex items-center mb-2">
-    <ui-popover>
+  <div class="px-4 flex items-center mb-2 mt-1">
+    <ui-popover class="mr-2 h-6">
       <template #trigger>
-        <span
-          title="Workflow icon"
-          class="
-            p-2
-            inline-block
-            rounded-lg
-            cursor-pointer
-            bg-accent
-            text-white
-            mr-2
-            align-middle
-          "
-        >
-          <v-remixicon :name="workflow.icon" />
+        <span title="Workflow icon" class="cursor-pointer">
+          <v-remixicon :name="workflow.icon" size="26" />
         </span>
       </template>
       <p class="mb-2">Workflow icon</p>
@@ -30,98 +18,10 @@
         </span>
       </div>
     </ui-popover>
-    <p
-      class="
-        font-semibold
-        text-overflow
-        inline-block
-        text-lg
-        flex-1
-        mr-4
-        align-middle
-      "
-    >
+    <p class="font-semibold text-overflow inline-block text-lg flex-1 mr-4">
       {{ workflow.name }}
     </p>
   </div>
-  <div class="flex px-4 mt-2 space-x-2">
-    <ui-button variant="accent" class="flex-1 relative" @click="$emit('save')">
-      <span
-        v-if="dataChanged"
-        class="flex h-3 w-3 absolute top-0 left-0 -ml-1 -mt-1"
-      >
-        <span
-          class="
-            animate-ping
-            absolute
-            inline-flex
-            h-full
-            w-full
-            rounded-full
-            bg-primary
-            opacity-75
-          "
-        ></span>
-        <span
-          class="relative inline-flex rounded-full h-3 w-3 bg-blue-600"
-        ></span>
-      </span>
-      <v-remixicon name="riSaveLine" class="mr-2 -ml-1" />
-      Save
-    </ui-button>
-    <ui-button icon title="Execute" @click="$emit('execute')">
-      <v-remixicon name="riPlayLine" />
-    </ui-button>
-    <ui-popover>
-      <template #trigger>
-        <ui-button icon title="More">
-          <v-remixicon name="riMore2Line" />
-        </ui-button>
-      </template>
-      <ui-list>
-        <ui-list-item
-          v-close-popover
-          class="cursor-pointer"
-          @click="$emit('rename')"
-        >
-          <v-remixicon name="riPencilLine" class="mr-2 -ml-1" />
-          <span>Rename</span>
-        </ui-list-item>
-        <ui-list-item
-          v-close-popover
-          class="cursor-pointer"
-          @click="$emit('export', workflow)"
-        >
-          <v-remixicon name="riDownloadLine" class="mr-2 -ml-1" />
-          <span>Export</span>
-        </ui-list-item>
-        <ui-list-item
-          v-close-popover
-          class="cursor-pointer"
-          @click="$emit('showDataColumns')"
-        >
-          <v-remixicon name="riKey2Line" class="mr-2 -ml-1" />
-          <span>Data columns</span>
-        </ui-list-item>
-        <ui-list-item
-          v-close-popover
-          class="cursor-pointer"
-          @click="$emit('showSettings')"
-        >
-          <v-remixicon name="riSettings3Line" class="mr-2 -ml-1" />
-          <span>Settings</span>
-        </ui-list-item>
-        <ui-list-item
-          v-close-popover
-          class="cursor-pointer"
-          @click="$emit('delete')"
-        >
-          <v-remixicon name="riDeleteBin7Line" class="mr-2 -ml-1" />
-          <span>Delete</span>
-        </ui-list-item>
-      </ui-list>
-    </ui-popover>
-  </div>
   <ui-input
     v-model="query"
     prepend-icon="riSearch2Line"
@@ -190,16 +90,7 @@ defineProps({
     default: false,
   },
 });
-defineEmits([
-  'save',
-  'export',
-  'update',
-  'rename',
-  'delete',
-  'execute',
-  'showSettings',
-  'showDataColumns',
-]);
+defineEmits(['update']);
 
 const icons = [
   'riGlobalLine',

+ 52 - 0
src/components/newtab/workflow/WorkflowGlobalData.vue

@@ -0,0 +1,52 @@
+<template>
+  <div class="global-data">
+    <a
+      href="https://github.com/Kholid060/automa/wiki/Features#reference-data"
+      target="_blank"
+      rel="noopener"
+      class="inline-block text-primary"
+    >
+      Learn how to access the global data in a block
+    </a>
+    <p class="float-right clear-both" title="Characters limit">
+      {{ globalData.length }}/{{ maxLength.toLocaleString() }}
+    </p>
+    <prism-editor
+      v-model="globalData"
+      :highlight="highlighter('json')"
+      class="h-full scroll mt-2"
+      style="height: calc(100vh - 10rem)"
+    />
+  </div>
+</template>
+<script setup>
+import { ref, watch } from 'vue';
+import { PrismEditor } from 'vue-prism-editor';
+import { highlighter } from '@/lib/prism';
+import { debounce } from '@/utils/helper';
+
+const props = defineProps({
+  workflow: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['update']);
+
+const maxLength = 1e4;
+const globalData = ref(`${props.workflow.globalData}`);
+
+watch(
+  globalData,
+  debounce((value) => {
+    let newValue = value;
+
+    if (value.length > maxLength) {
+      newValue = value.slice(0, maxLength);
+      globalData.value = newValue;
+    }
+
+    emit('update', { globalData: newValue });
+  }, 250)
+);
+</script>

+ 23 - 21
src/components/newtab/workflow/WorkflowSettings.vue

@@ -1,26 +1,28 @@
 <template>
-  <div class="mb-4">
-    <p class="mb-1">On workflow error</p>
-    <div class="space-x-4">
-      <ui-radio
-        v-for="item in onError"
-        :key="item.id"
-        :model-value="workflow.settings.onError"
-        :value="item.id"
-        class="mr-4"
-        @change="updateWorkflow({ onError: $event })"
-      >
-        {{ item.name }}
-      </ui-radio>
+  <div class="workflow-settings">
+    <div class="mb-4">
+      <p class="mb-1">On workflow error</p>
+      <div class="space-x-4">
+        <ui-radio
+          v-for="item in onError"
+          :key="item.id"
+          :model-value="workflow.settings.onError"
+          :value="item.id"
+          class="mr-4"
+          @change="updateWorkflow({ onError: $event })"
+        >
+          {{ item.name }}
+        </ui-radio>
+      </div>
+    </div>
+    <div>
+      <p class="mb-1">Workflow timeout (milliseconds)</p>
+      <ui-input
+        :model-value="workflow.settings.timeout"
+        type="number"
+        @change="updateWorkflow({ timeout: +$event })"
+      />
     </div>
-  </div>
-  <div>
-    <p class="mb-1">Workflow timeout (milliseconds)</p>
-    <ui-input
-      :model-value="workflow.settings.timeout"
-      type="number"
-      @change="updateWorkflow({ timeout: +$event })"
-    />
   </div>
 </template>
 <script setup>

+ 1 - 0
src/models/workflow.js

@@ -18,6 +18,7 @@ class Workflow extends Model {
       data: this.attr(null),
       drawflow: this.string(''),
       dataColumns: this.attr([]),
+      globalData: this.string('[{ "key": "value" }]'),
       lastRunAt: this.number(),
       createdAt: this.number(),
       settings: this.attr({

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

@@ -22,7 +22,7 @@
     </div>
     <div class="flex items-start">
       <div
-        class="w-80 border-r pr-6 mr-6 p-1 scroll overflow-auto"
+        class="w-80 border-r sticky top-11 pr-6 mr-6 p-1 scroll overflow-auto"
         style="max-height: calc(100vh - 8rem)"
       >
         <ui-input

+ 42 - 20
src/newtab/pages/workflows/[id].vue

@@ -12,20 +12,15 @@
       <workflow-details-card
         v-else
         :workflow="workflow"
-        :data-changed="state.isDataChanged"
-        @save="saveWorkflow"
-        @export="exportWorkflow"
-        @execute="executeWorkflow"
         @update="updateWorkflow"
-        @showDataColumns="state.showDataColumnsModal = true"
-        @showSettings="state.showSettings = true"
-        @rename="renameWorkflow"
-        @delete="deleteWorkflow"
       />
     </div>
     <div class="flex-1 relative overflow-auto">
-      <div class="absolute px-3 rounded-lg bg-white z-10 left-0 m-4 top-0">
-        <ui-tabs v-model="activeTab" class="border-none h-full space-x-1">
+      <div class="absolute w-full flex items-center z-10 left-0 p-4 top-0">
+        <ui-tabs
+          v-model="activeTab"
+          class="border-none px-2 rounded-lg h-full space-x-1 bg-white"
+        >
           <ui-tab value="editor">Editor</ui-tab>
           <ui-tab value="logs">Logs</ui-tab>
           <ui-tab value="running" class="flex items-center">
@@ -48,6 +43,16 @@
             </span>
           </ui-tab>
         </ui-tabs>
+        <div class="flex-grow"></div>
+        <workflow-actions
+          :is-data-changed="state.isDataChanged"
+          @showModal="(state.modalName = $event), (state.showModal = true)"
+          @save="saveWorkflow"
+          @export="exportWorkflow(workflow)"
+          @execute="executeWorkflow"
+          @rename="renameWorkflow"
+          @delete="deleteWorkflow"
+        />
       </div>
       <keep-alive>
         <workflow-builder
@@ -98,18 +103,15 @@
       </keep-alive>
     </div>
   </div>
-  <ui-modal v-model="state.showDataColumnsModal" content-class="max-w-xl">
-    <template #header>Data columns</template>
-    <workflow-data-columns
+  <ui-modal v-model="state.showModal" content-class="max-w-xl">
+    <template #header>{{ workflowModals[state.modalName].title }}</template>
+    <component
+      :is="workflowModals[state.modalName].component"
       v-bind="{ workflow }"
       @update="updateWorkflow"
-      @close="state.showDataColumnsModal = false"
+      @close="state.showModal = false"
     />
   </ui-modal>
-  <ui-modal v-model="state.showSettings">
-    <template #header>Workflow settings</template>
-    <workflow-settings v-bind="{ workflow }" @update="updateWorkflow" />
-  </ui-modal>
 </template>
 <script setup>
 /* eslint-disable consistent-return */
@@ -131,10 +133,12 @@ import { exportWorkflow } from '@/utils/workflow-data';
 import Log from '@/models/log';
 import Workflow from '@/models/workflow';
 import workflowTrigger from '@/utils/workflow-trigger';
+import WorkflowActions from '@/components/newtab/workflow/WorkflowActions.vue';
 import WorkflowBuilder from '@/components/newtab/workflow/WorkflowBuilder.vue';
 import WorkflowSettings from '@/components/newtab/workflow/WorkflowSettings.vue';
 import WorkflowEditBlock from '@/components/newtab/workflow/WorkflowEditBlock.vue';
 import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
+import WorkflowGlobalData from '@/components/newtab/workflow/WorkflowGlobalData.vue';
 import WorkflowDataColumns from '@/components/newtab/workflow/WorkflowDataColumns.vue';
 import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
 import SharedWorkflowState from '@/components/newtab/shared/SharedWorkflowState.vue';
@@ -145,15 +149,32 @@ const router = useRouter();
 const dialog = useDialog();
 
 const workflowId = route.params.id;
+const workflowModals = {
+  'data-columns': {
+    icon: 'riKey2Line',
+    title: 'Data columns',
+    component: WorkflowDataColumns,
+  },
+  'global-data': {
+    title: 'Global data',
+    icon: 'riDatabase2Line',
+    component: WorkflowGlobalData,
+  },
+  settings: {
+    icon: 'riSettings3Line',
+    title: 'Settings',
+    component: WorkflowSettings,
+  },
+};
 
 const editor = shallowRef(null);
 const activeTab = shallowRef('editor');
 const state = reactive({
   blockData: {},
+  modalName: '',
+  showModal: false,
   isEditBlock: false,
-  showSettings: false,
   isDataChanged: false,
-  showDataColumnsModal: false,
 });
 
 const workflowState = computed(() =>
@@ -274,6 +295,7 @@ provide('workflow', {
 onBeforeRouteLeave(() => {
   if (!state.isDataChanged) return;
 
+  // eslint-disable-next-line no-alert
   const answer = window.confirm(
     'Do you really want to leave? you have unsaved changes!'
   );

+ 10 - 0
src/utils/helper.js

@@ -1,3 +1,13 @@
+export function parseJSON(data, def) {
+  try {
+    const result = JSON.parse(data);
+
+    return result;
+  } catch (error) {
+    return def;
+  }
+}
+
 export function replaceMustache(str, replacer) {
   /* eslint-disable-next-line */
   return str.replace(/\{\{(.*?)\}\}/g, replacer);

+ 2 - 2
src/utils/reference-data.js

@@ -6,10 +6,10 @@ const objectPath = { get, set };
 function parseKey(key) {
   const [dataKey, path] = key.split('@');
 
-  if (['prevBlockData', 'loopData'].includes(dataKey))
+  if (['prevBlockData', 'loopData', 'globalData'].includes(dataKey))
     return { dataKey, path: path || '' };
 
-  const pathArr = path.split('.');
+  const pathArr = path?.split('.') ?? '';
   let dataPath = '';
 
   if (pathArr.length === 1) {

+ 1 - 1
src/utils/workflow-trigger.js

@@ -39,7 +39,7 @@ export function registerSpecificDay(workflowId, data) {
   const findDate =
     dates.find((date) => date.valueOf() > Date.now()) || dates[0].add(7, 'day');
 
-  console.log(finDate.valueOf(), new Date(finDate.valueOf()));
+  console.log(findDate.valueOf(), new Date(findDate.valueOf()));
 
   return browser.alarms.create(workflowId, {
     when: findDate.valueOf(),