Jelajahi Sumber

feat: add trigger workflow schedule

Ahmad Kholid 2 tahun lalu
induk
melakukan
004b48ba23

+ 166 - 0
src/components/newtab/shared/SharedWorkflowTriggers.vue

@@ -0,0 +1,166 @@
+<template>
+  <div
+    class="overflow-auto scroll"
+    style="min-height: 350px; max-height: calc(100vh - 14rem)"
+  >
+    <ui-expand
+      v-for="(trigger, index) in triggersList"
+      :key="index"
+      class="border rounded-lg mb-2 trigger-item"
+    >
+      <template #header>
+        <p class="flex-1">
+          {{ t(`workflow.blocks.trigger.items.${trigger.type}`) }}
+        </p>
+        <v-remixicon
+          name="riDeleteBin7Line"
+          size="20"
+          class="delete-btn cursor-pointer"
+          @click.stop="triggersList.splice(index, 1)"
+        />
+      </template>
+      <div class="px-4 py-2">
+        <component
+          :is="triggersData[trigger.type]?.component"
+          :data="trigger.data"
+          @update="updateTriggerData(index, $event)"
+        />
+      </div>
+    </ui-expand>
+    <ui-popover class="mt-4">
+      <template #trigger>
+        <ui-button>
+          Add trigger
+          <hr class="h-4 border-r" />
+          <v-remixicon
+            name="riArrowLeftSLine"
+            class="ml-2 -mr-1"
+            rotate="-90"
+          />
+        </ui-button>
+      </template>
+      <ui-list class="space-y-1">
+        <ui-list-item
+          v-for="triggerType in triggersTypes"
+          :key="triggerType"
+          v-close-popover
+          class="cursor-pointer"
+          small
+          @click="addTrigger(triggerType)"
+        >
+          {{ t(`workflow.blocks.trigger.items.${triggerType}`) }}
+        </ui-list-item>
+      </ui-list>
+    </ui-popover>
+  </div>
+</template>
+<script setup>
+import { ref, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { nanoid } from 'nanoid/non-secure';
+import cloneDeep from 'lodash.clonedeep';
+import TriggerDate from '../workflow/edit/Trigger/TriggerDate.vue';
+import TriggerInterval from '../workflow/edit/Trigger/TriggerInterval.vue';
+import TriggerVisitWeb from '../workflow/edit/Trigger/TriggerVisitWeb.vue';
+import TriggerContextMenu from '../workflow/edit/Trigger/TriggerContextMenu.vue';
+import TriggerSpecificDay from '../workflow/edit/Trigger/TriggerSpecificDay.vue';
+// import TriggerElementChange from '../workflow/edit/Trigger/TriggerElementChange.vue';
+import TriggerKeyboardShortcut from '../workflow/edit/Trigger/TriggerKeyboardShortcut.vue';
+
+const props = defineProps({
+  triggers: {
+    type: Array,
+    default: () => [],
+  },
+  exclude: {
+    type: Array,
+    default: null,
+  },
+});
+const emit = defineEmits(['update:triggers', 'update']);
+
+const triggersData = {
+  // 'element-change': TriggerElementChange,
+  interval: {
+    component: TriggerInterval,
+    data: {
+      interval: 60,
+      delay: 5,
+      fixedDelay: false,
+    },
+  },
+  'context-menu': {
+    onlyOne: true,
+    component: TriggerContextMenu,
+    data: {
+      contextMenuName: '',
+      contextTypes: [],
+    },
+  },
+  date: {
+    component: TriggerDate,
+    data: {
+      date: '',
+    },
+  },
+  'specific-day': {
+    component: TriggerSpecificDay,
+    data: {
+      days: [],
+      time: '00:00',
+    },
+  },
+  'on-startup': {
+    onlyOne: true,
+    component: null,
+    data: null,
+  },
+  'visit-web': {
+    component: TriggerVisitWeb,
+    data: {
+      url: '',
+      isUrlRegex: false,
+    },
+  },
+  'keyboard-shortcut': {
+    component: TriggerKeyboardShortcut,
+    data: {
+      shortcut: '',
+    },
+  },
+};
+
+const triggersTypes = props.exclude
+  ? Object.keys(triggersData).filter((type) => !props.exclude.includes(type))
+  : Object.keys(triggersData);
+
+const { t } = useI18n();
+const triggersList = ref([...(props.triggers || [])]);
+
+function addTrigger(type) {
+  if (triggersData[type].onlyOne) {
+    const trigerExists = triggersList.value.some(
+      (trigger) => trigger.type === type
+    );
+    if (trigerExists) return;
+  }
+
+  triggersList.value.push({
+    id: nanoid(5),
+    type,
+    data: cloneDeep(triggersData[type].data),
+  });
+}
+function updateTriggerData(index, data) {
+  Object.assign(triggersList.value[index].data, data);
+}
+
+watch(
+  triggersList,
+  (newData) => {
+    emit('update', newData);
+    emit('update:triggers', newData);
+  },
+  { deep: true }
+);
+</script>

+ 9 - 139
src/components/newtab/workflow/edit/EditTrigger.vue

@@ -33,76 +33,18 @@
       title="Workflow Triggers"
       content-class="max-w-2xl"
     >
-      <div
-        class="overflow-auto scroll"
-        style="min-height: 350px; max-height: calc(100vh - 14rem)"
-      >
-        <ui-expand
-          v-for="(trigger, index) in state.triggers"
-          :key="index"
-          class="border rounded-lg mb-2 trigger-item"
-        >
-          <template #header>
-            <p class="flex-1">
-              {{ t(`workflow.blocks.trigger.items.${trigger.type}`) }}
-            </p>
-            <v-remixicon
-              name="riDeleteBin7Line"
-              size="20"
-              class="delete-btn cursor-pointer"
-              @click.stop="state.triggers.splice(index, 1)"
-            />
-          </template>
-          <div class="px-4 py-2">
-            <component
-              :is="triggers[trigger.type]?.component"
-              :data="trigger.data"
-              @update="updateTriggerData(index, $event)"
-            />
-          </div>
-        </ui-expand>
-        <ui-popover class="mt-4">
-          <template #trigger>
-            <ui-button>
-              Add trigger
-              <hr class="h-4 border-r" />
-              <v-remixicon
-                name="riArrowLeftSLine"
-                class="ml-2 -mr-1"
-                rotate="-90"
-              />
-            </ui-button>
-          </template>
-          <ui-list class="space-y-1">
-            <ui-list-item
-              v-for="(_, trigger) in triggers"
-              :key="trigger"
-              v-close-popover
-              :value="trigger"
-              class="cursor-pointer"
-              small
-              @click="addTrigger(trigger)"
-            >
-              {{ t(`workflow.blocks.trigger.items.${trigger}`) }}
-            </ui-list-item>
-          </ui-list>
-        </ui-popover>
-      </div>
+      <shared-workflow-triggers
+        :triggers="state.triggers"
+        @update="updateWorkflow"
+      />
     </ui-modal>
   </div>
 </template>
 <script setup>
-import { onMounted, reactive, watch } from 'vue';
+import { onMounted, reactive } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { nanoid } from 'nanoid/non-secure';
-import cloneDeep from 'lodash.clonedeep';
-import TriggerDate from './Trigger/TriggerDate.vue';
-import TriggerInterval from './Trigger/TriggerInterval.vue';
-import TriggerVisitWeb from './Trigger/TriggerVisitWeb.vue';
-import TriggerContextMenu from './Trigger/TriggerContextMenu.vue';
-import TriggerSpecificDay from './Trigger/TriggerSpecificDay.vue';
-// import TriggerElementChange from './Trigger/TriggerElementChange.vue';
-import TriggerKeyboardShortcut from './Trigger/TriggerKeyboardShortcut.vue';
+import SharedWorkflowTriggers from '@/components/newtab/shared/SharedWorkflowTriggers.vue';
 import EditWorkflowParameters from './EditWorkflowParameters.vue';
 
 const props = defineProps({
@@ -113,57 +55,6 @@ const props = defineProps({
 });
 const emit = defineEmits(['update:data']);
 
-const triggers = {
-  // 'element-change': TriggerElementChange,
-  interval: {
-    component: TriggerInterval,
-    data: {
-      interval: 60,
-      delay: 5,
-      fixedDelay: false,
-    },
-  },
-  'context-menu': {
-    onlyOne: true,
-    component: TriggerContextMenu,
-    data: {
-      contextMenuName: '',
-      contextTypes: [],
-    },
-  },
-  date: {
-    component: TriggerDate,
-    data: {
-      date: '',
-    },
-  },
-  'specific-day': {
-    component: TriggerSpecificDay,
-    data: {
-      days: [],
-      time: '00:00',
-    },
-  },
-  'on-startup': {
-    onlyOne: true,
-    component: null,
-    data: null,
-  },
-  'visit-web': {
-    component: TriggerVisitWeb,
-    data: {
-      url: '',
-      isUrlRegex: false,
-    },
-  },
-  'keyboard-shortcut': {
-    component: TriggerKeyboardShortcut,
-    data: {
-      shortcut: '',
-    },
-  },
-};
-
 const { t } = useI18n();
 
 const state = reactive({
@@ -175,31 +66,10 @@ const state = reactive({
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
 }
-function addTrigger(type) {
-  if (triggers[type].onlyOne) {
-    const trigerExists = state.triggers.some(
-      (trigger) => trigger.type === type
-    );
-    if (trigerExists) return;
-  }
-
-  state.triggers.push({
-    id: nanoid(5),
-    type,
-    data: cloneDeep(triggers[type].data),
-  });
+function updateWorkflow(triggers) {
+  state.triggers = triggers;
+  updateData({ triggers });
 }
-function updateTriggerData(index, data) {
-  Object.assign(state.triggers[index].data, data);
-}
-
-watch(
-  () => state.triggers,
-  (newData) => {
-    updateData({ triggers: newData });
-  },
-  { deep: true }
-);
 
 onMounted(() => {
   if (props.data.triggers) return;

+ 3 - 3
src/components/newtab/workflow/edit/Trigger/TriggerContextMenu.vue

@@ -8,7 +8,7 @@
         {{ t('workflow.blocks.trigger.contextMenus.grantPermission') }}
       </ui-button>
     </template>
-    <template v-else>
+    <template v-else-if="workflow.data">
       <ui-input
         :label="t('workflow.blocks.trigger.contextMenus.contextName')"
         :placeholder="workflow.data.value.name"
@@ -79,7 +79,7 @@ const permissionName = BROWSER_TYPE === 'firefox' ? 'menus' : 'contextMenus';
 const { t } = useI18n();
 const permission = useHasPermissions([permissionName]);
 
-const workflow = inject('workflow');
+const workflow = inject('workflow', {});
 
 function onSelectContextType(selected, type) {
   const contextTypes = [...props.data.contextTypes];
@@ -95,7 +95,7 @@ function onSelectContextType(selected, type) {
 }
 
 onMounted(() => {
-  if (props.data.contextMenuName.trim()) return;
+  if (props.data.contextMenuName.trim() || !workflow?.data) return;
 
   emit('update', { contextMenuName: workflow.data.value.name });
 });

+ 1 - 0
src/components/ui/UiModal.vue

@@ -23,6 +23,7 @@
                 <span class="content-header">
                   <slot name="header">{{ title }}</slot>
                 </span>
+                <slot name="header-append" />
                 <v-remixicon
                   v-show="!persist"
                   class="text-gray-600 dark:text-gray-300 cursor-pointer"

+ 258 - 43
src/newtab/pages/ScheduledWorkflow.vue

@@ -1,18 +1,25 @@
 <template>
   <div class="container pt-8 pb-4">
-    <h1 class="text-2xl font-semibold mb-8 capitalize">
+    <h1 class="text-2xl font-semibold mb-12 capitalize">
       {{ t('scheduledWorkflow.title', 2) }}
     </h1>
-    <ui-input
-      v-model="state.query"
-      prepend-icon="riSearch2Line"
-      :placeholder="t('common.search')"
-    />
+    <div class="flex items-center">
+      <ui-input
+        v-model="state.query"
+        prepend-icon="riSearch2Line"
+        :placeholder="t('common.search')"
+      />
+      <div class="flex-grow" />
+      <ui-button @click="scheduleState.showModal = true">
+        <v-remixicon name="riAddLine" class="-ml mr-2" />
+        Schedule workflow
+      </ui-button>
+    </div>
     <ui-table
       :headers="tableHeaders"
       :items="triggers"
       item-key="id"
-      class="w-full mt-4"
+      class="w-full mt-8"
     >
       <template #item-name="{ item }">
         <router-link
@@ -50,19 +57,79 @@
         </button>
       </template>
     </ui-table>
+    <ui-modal
+      v-model="scheduleState.showModal"
+      title="Workflow Triggers"
+      persist
+      content-class="max-w-2xl"
+    >
+      <template #header-append>
+        <div>
+          <ui-button @click="clearAddWorkflowSchedule">
+            {{ t('common.cancel') }}
+          </ui-button>
+          <ui-button
+            class="ml-4"
+            variant="accent"
+            @click="updateWorkflowTrigger"
+          >
+            {{ t('common.save') }}
+          </ui-button>
+        </div>
+      </template>
+      <ui-autocomplete
+        v-if="!scheduleState.selectedWorkflow.id"
+        :model-value="scheduleState.selectedWorkflow.query"
+        :items="workflowStore.getWorkflows"
+        block
+        class="mt-2"
+        item-key="id"
+        item-label="name"
+        @selected="onSelectedWorkflow"
+      >
+        <ui-input
+          v-model="scheduleState.selectedWorkflow.query"
+          class="w-full"
+          autocomplete="off"
+          placeholder="Search workflow"
+        />
+      </ui-autocomplete>
+      <template v-else>
+        <p class="font-semibold">
+          {{ scheduleState.selectedWorkflow.name }}
+        </p>
+        <shared-workflow-triggers
+          :key="scheduleState.selectedWorkflow.id"
+          v-model:triggers="scheduleState.selectedWorkflow.triggers"
+          :exclude="[
+            'context-menu',
+            'on-startup',
+            'visit-web',
+            'keyboard-shortcut',
+          ]"
+          class="mt-4"
+        />
+      </template>
+    </ui-modal>
   </div>
 </template>
 <script setup>
 import { onMounted, reactive, computed } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { nanoid } from 'nanoid';
 import dayjs from 'dayjs';
+import cloneDeep from 'lodash.clonedeep';
 import browser from 'webextension-polyfill';
 import { useUserStore } from '@/stores/user';
 import { useWorkflowStore } from '@/stores/workflow';
 import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
 import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import { findTriggerBlock, objectHasKey } from '@/utils/helper';
-import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
+import {
+  registerWorkflowTrigger,
+  workflowTriggersMap,
+} from '@/utils/workflowTrigger';
+import SharedWorkflowTriggers from '@/components/newtab/shared/SharedWorkflowTriggers.vue';
 
 const { t } = useI18n();
 const userStore = useUserStore();
@@ -76,6 +143,15 @@ const state = reactive({
   triggers: [],
   activeTrigger: 'scheduled',
 });
+const scheduleState = reactive({
+  query: '',
+  showModal: false,
+  selectedWorkflow: {
+    id: '',
+    name: '',
+    triggers: [],
+  },
+});
 
 let rowId = 0;
 const scheduledTypes = ['interval', 'date', 'specific-day'];
@@ -163,51 +239,98 @@ function scheduleText(data) {
 
   return text;
 }
-async function getTriggerObj(trigger, { id, name }) {
-  if (!trigger || !scheduledTypes.includes(trigger.type)) return null;
-
-  rowId += 1;
-  const triggerObj = {
-    name,
-    id: rowId,
-    nextRun: '-',
-    schedule: '',
-    active: false,
-    type: trigger.type,
-    workflowId: id,
-  };
-
+async function getTriggersData(triggerData, { id, name }) {
   try {
-    const alarm = await browser.alarms.get(id);
-    if (alarm) {
-      triggerObj.active = true;
-      triggerObj.nextRun = dayjs(alarm.scheduledTime).format(
-        'DD MMM YYYY, hh:mm:ss A'
-      );
-    }
+    const alarms = await browser.alarms.getAll();
+    const getTrigger = async (trigger) => {
+      try {
+        if (!trigger || !scheduledTypes.includes(trigger.type)) return null;
 
-    triggersData[rowId] = {
-      ...trigger,
-      workflow: { id, name },
+        rowId += 1;
+        const triggerObj = {
+          name,
+          id: rowId,
+          nextRun: '-',
+          schedule: '',
+          active: false,
+          type: trigger.type,
+          workflowId: id,
+          triggerId: trigger.id || null,
+        };
+
+        const alarm = alarms.find((alarmItem) => {
+          if (trigger.id) return alarmItem.name.includes(trigger.id);
+
+          return alarmItem.name.includes(id);
+        });
+        if (alarm) {
+          triggerObj.active = true;
+          triggerObj.nextRun = dayjs(alarm.scheduledTime).format(
+            'DD MMM YYYY, hh:mm:ss A'
+          );
+        }
+
+        triggersData[rowId] = {
+          ...trigger,
+          workflow: { id, name },
+        };
+        Object.assign(triggerObj, scheduleText(trigger));
+
+        return triggerObj;
+      } catch (error) {
+        return null;
+      }
     };
-    Object.assign(triggerObj, scheduleText(trigger));
 
-    return triggerObj;
+    if (triggerData.triggers) {
+      const result = await Promise.all(
+        triggerData.triggers.map((trigger) => {
+          const triggerItemData = { ...trigger };
+          Object.assign(triggerItemData, triggerItemData.data);
+
+          delete triggerItemData.data;
+
+          return getTrigger(triggerItemData);
+        })
+      );
+
+      return result.reduce((acc, item) => {
+        if (item) {
+          acc.push(item);
+        }
+
+        return acc;
+      }, []);
+    }
+    const result = await getTrigger(triggerData);
+    if (!result) return [];
+
+    return [result];
   } catch (error) {
     console.error(error);
-    return null;
+    return [];
   }
 }
 async function refreshSchedule(id) {
   try {
-    const triggerData = triggersData[id];
+    const triggerData = triggersData[id] ? cloneDeep(triggersData[id]) : null;
     if (!triggerData) return;
 
+    const handler = workflowTriggersMap[triggerData.type];
+    if (!handler) return;
+
+    if (triggerData.id) {
+      triggerData.workflow.id = `trigger:${triggerData.workflow.id}:${triggerData.id}`;
+    }
+
     await registerWorkflowTrigger(triggerData.workflow.id, {
       data: triggerData,
     });
 
-    const triggerObj = await getTriggerObj(triggerData, triggerData.workflow);
+    const [triggerObj] = await getTriggersData(
+      triggerData,
+      triggerData.workflow
+    );
     if (!triggerObj) return;
 
     const triggerIndex = state.triggers.findIndex(
@@ -233,12 +356,13 @@ async function getWorkflowTrigger(workflow, { location, path }) {
     trigger = findTriggerBlock(drawflow)?.data;
   }
 
-  const obj = await getTriggerObj(trigger, workflow);
-
-  if (obj) {
-    obj.path = path;
-    obj.location = location;
-    state.triggers.push(obj);
+  const triggersList = await getTriggersData(trigger, workflow);
+  if (triggersList.length !== 0) {
+    triggersList.forEach((triggerData) => {
+      triggerData.path = path;
+      triggerData.location = location;
+      state.triggers.push(triggerData);
+    });
   }
 }
 function iterateWorkflows({ workflows, path, location }) {
@@ -250,6 +374,97 @@ function iterateWorkflows({ workflows, path, location }) {
 
   return Promise.allSettled(promises);
 }
+function onSelectedWorkflow({ item }) {
+  if (!item.drawflow?.nodes) return;
+
+  const triggerBlock = findTriggerBlock(item.drawflow);
+  if (!triggerBlock) return;
+
+  let { triggersList } = triggerBlock.data;
+  if (!triggersList) {
+    triggersList = [
+      {
+        data: { ...triggerBlock.data },
+        type: triggerBlock.data.type,
+        id: nanoid(5),
+      },
+    ];
+  }
+
+  scheduleState.selectedWorkflow.id = item.id;
+  scheduleState.selectedWorkflow.name = item.name;
+  scheduleState.selectedWorkflow.triggers = [...triggersList];
+}
+function clearAddWorkflowSchedule() {
+  Object.assign(scheduleState, {
+    query: '',
+    showModal: false,
+    selectedWorkflow: {
+      id: '',
+      name: '',
+      triggers: [],
+    },
+  });
+}
+async function updateWorkflowTrigger() {
+  try {
+    const {
+      triggers: workflowTriggers,
+      id,
+      name,
+    } = scheduleState.selectedWorkflow;
+    const workflowData = workflowStore.getById(id);
+    if (!workflowData || !workflowData?.drawflow?.nodes) return;
+
+    const triggerBlockIndex = workflowData.drawflow.nodes.findIndex(
+      (node) => node.label === 'trigger'
+    );
+    if (triggerBlockIndex === -1) return;
+
+    const copyNodes = [...workflowData.drawflow.nodes];
+    copyNodes[triggerBlockIndex].data.triggers = cloneDeep(workflowTriggers);
+    await workflowStore.update({
+      id,
+      data: {
+        trigger: { triggers: workflowTriggers },
+        drawflow: {
+          ...workflowData.drawflow,
+          nodes: copyNodes,
+        },
+      },
+    });
+
+    state.triggers = state.triggers.filter((trigger) => {
+      const isNotMatch =
+        scheduleState.selectedWorkflow.id !== trigger.workflowId;
+      if (!isNotMatch) {
+        delete triggersData[trigger.id];
+      }
+
+      return isNotMatch;
+    });
+
+    await registerWorkflowTrigger(id, {
+      data: { triggers: workflowTriggers },
+    });
+
+    const triggersList = await getTriggersData(
+      { triggers: workflowTriggers },
+      { id, name }
+    );
+    if (triggersList.length !== 0) {
+      triggersList.forEach((triggerData) => {
+        triggerData.location = 'Local';
+        triggerData.path = `/workflows/${id}`;
+        state.triggers.push(triggerData);
+      });
+    }
+
+    clearAddWorkflowSchedule();
+  } catch (error) {
+    console.error(error);
+  }
+}
 
 onMounted(async () => {
   try {

+ 13 - 13
src/utils/workflowTrigger.js

@@ -232,28 +232,28 @@ export async function registerOnStartup() {
   // Do nothing
 }
 
+export const workflowTriggersMap = {
+  interval: registerInterval,
+  date: registerSpecificDate,
+  'visit-web': registerVisitWeb,
+  'on-startup': registerOnStartup,
+  'specific-day': registerSpecificDay,
+  'context-menu': registerContextMenu,
+  'keyboard-shortcut': registerKeyboardShortcut,
+};
+
 export async function registerWorkflowTrigger(workflowId, { data }) {
   try {
     await cleanWorkflowTriggers(workflowId);
 
-    const triggersHandler = {
-      interval: registerInterval,
-      date: registerSpecificDate,
-      'visit-web': registerVisitWeb,
-      'on-startup': registerOnStartup,
-      'specific-day': registerSpecificDay,
-      'context-menu': registerContextMenu,
-      'keyboard-shortcut': registerKeyboardShortcut,
-    };
-
     if (data.triggers) {
       for (const trigger of data.triggers) {
-        const handler = triggersHandler[trigger.type];
+        const handler = workflowTriggersMap[trigger.type];
         if (handler)
           await handler(`trigger:${workflowId}:${trigger.id}`, trigger.data);
       }
-    } else if (triggersHandler[data.type]) {
-      await triggersHandler[data.type](workflowId, data);
+    } else if (workflowTriggersMap[data.type]) {
+      await workflowTriggersMap[data.type](workflowId, data);
     }
   } catch (error) {
     console.error(error);