Bladeren bron

feat: support define multiple workflow triggers

Ahmad Kholid 3 jaren geleden
bovenliggende
commit
20cb793932

+ 38 - 9
src/background/index.js

@@ -233,21 +233,28 @@ async function openDashboard(url) {
   }
 }
 async function checkVisitWebTriggers(tabId, tabUrl) {
+  const visitWebTriggers = await browserStorage.get('visitWebTriggers');
+  if (!visitWebTriggers || visitWebTriggers.length === 0) return;
+
   const workflowState = await workflow.states.get(({ state }) =>
     state.tabIds.includes(tabId)
   );
-  const visitWebTriggers = await browserStorage.get('visitWebTriggers');
   const triggeredWorkflow = visitWebTriggers?.find(({ url, isRegex, id }) => {
     if (url.trim() === '') return false;
 
     const matchUrl = tabUrl.match(isRegex ? new RegExp(url, 'g') : url);
 
-    return matchUrl && id !== workflowState?.workflowId;
+    return matchUrl && !id.includes(workflowState?.workflowId);
   });
 
   if (triggeredWorkflow) {
-    const workflowData = await workflow.get(triggeredWorkflow.id);
+    let workflowId = triggeredWorkflow.id;
+    if (triggeredWorkflow.id.startsWith('trigger')) {
+      const { 1: triggerWorkflowId } = triggeredWorkflow.id.split(':');
+      workflowId = triggerWorkflowId;
+    }
 
+    const workflowData = await workflow.get(workflowId);
     if (workflowData) workflow.execute(workflowData, { tabId });
   }
 }
@@ -359,7 +366,16 @@ browser.tabs.onCreated.addListener(async (tab) => {
   await browser.storage.local.set({ recording });
 });
 browser.alarms.onAlarm.addListener(async ({ name }) => {
-  const currentWorkflow = await workflow.get(name);
+  let workflowId = name;
+  let triggerId = null;
+
+  if (name.startsWith('trigger')) {
+    const { 1: triggerWorkflowId, 2: triggerItemId } = name.split(':');
+    triggerId = triggerItemId;
+    workflowId = triggerWorkflowId;
+  }
+
+  const currentWorkflow = await workflow.get(workflowId);
   if (!currentWorkflow) return;
 
   const drawflow =
@@ -369,15 +385,15 @@ browser.alarms.onAlarm.addListener(async ({ name }) => {
   const { data } = findTriggerBlock(drawflow) || {};
   if (data && data.type === 'interval' && data.fixedDelay) {
     const workflowState = await workflow.states.get(
-      ({ workflowId }) => name === workflowId
+      (item) => item.workflowId === workflowId
     );
 
     if (workflowState) {
       let { workflowQueue } = await browser.storage.local.get('workflowQueue');
       workflowQueue = workflowQueue || [];
 
-      if (!workflowQueue.includes(name)) {
-        (workflowQueue = workflowQueue || []).push(name);
+      if (!workflowQueue.includes(workflowId)) {
+        (workflowQueue = workflowQueue || []).push(workflowId);
         await browser.storage.local.set({ workflowQueue });
       }
 
@@ -397,7 +413,14 @@ browser.alarms.onAlarm.addListener(async ({ name }) => {
   workflow.execute(currentWorkflow);
 
   if (data && data.type === 'specific-day') {
-    registerSpecificDay(currentWorkflow.id, data);
+    let triggerData = data;
+    if (triggerId && data.triggers) {
+      triggerData = data.triggers.find(
+        (trigger) => trigger.id === triggerId
+      )?.data;
+    }
+
+    if (triggerData) registerSpecificDay(workflowId, triggerData);
   }
 });
 
@@ -413,7 +436,13 @@ if (contextMenu && contextMenu.onClicked) {
           frameId: 0,
           type: 'context-element',
         });
-        const workflowData = await workflow.get(menuItemId);
+        let workflowId = menuItemId;
+        if (menuItemId.startsWith('trigger')) {
+          const { 1: triggerWorkflowId } = menuItemId.split(':');
+          workflowId = triggerWorkflowId;
+        }
+
+        const workflowData = await workflow.get(workflowId);
 
         workflow.execute(workflowData, {
           data: {

+ 177 - 15
src/components/newtab/workflow/edit/EditTrigger.vue

@@ -7,7 +7,7 @@
       class="w-full mb-2"
       @change="updateData({ description: $event })"
     />
-    <ui-select
+    <!-- <ui-select
       :model-value="data.type || 'manual'"
       :placeholder="t('workflow.blocks.trigger.forms.triggerWorkflow')"
       class="w-full"
@@ -20,27 +20,101 @@
     <transition-expand mode="out-in">
       <keep-alive>
         <component
-          :is="triggers[data.type]"
+          :is="triggers[data.type]?.component"
           :data="data"
           @update="updateData"
         />
       </keep-alive>
-    </transition-expand>
-    <ui-button class="mt-4" @click="showModal = true">
+    </transition-expand> -->
+    <ui-button
+      variant="accent"
+      class="w-full mt-4"
+      @click="state.showTriggersModal = true"
+    >
+      Edit Triggers
+    </ui-button>
+    <ui-button class="mt-4" @click="state.showParamModal = true">
       <v-remixicon name="riCommandLine" class="mr-2 -ml-1" />
       <span>Parameters</span>
     </ui-button>
-    <ui-modal v-model="showModal" title="Parameters" content-class="max-w-4xl">
+    <ui-modal
+      v-model="state.showParamModal"
+      title="Parameters"
+      content-class="max-w-4xl"
+    >
       <edit-workflow-parameters
         :data="data.parameters"
         @update="updateData({ parameters: $event })"
       />
     </ui-modal>
+    <ui-modal
+      v-model="state.showTriggersModal"
+      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>
+    </ui-modal>
   </div>
 </template>
 <script setup>
-import { shallowRef } from 'vue';
+import { onMounted, reactive, watch } 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';
@@ -59,22 +133,110 @@ const props = defineProps({
 const emit = defineEmits(['update:data']);
 
 const triggers = {
-  manual: null,
-  interval: TriggerInterval,
-  'context-menu': TriggerContextMenu,
   // 'element-change': TriggerElementChange,
-  date: TriggerDate,
-  'specific-day': TriggerSpecificDay,
-  'on-startup': null,
-  'visit-web': TriggerVisitWeb,
-  'keyboard-shortcut': TriggerKeyboardShortcut,
+  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 showModal = shallowRef(false);
+const state = reactive({
+  showParamModal: false,
+  showTriggersModal: false,
+  triggers: [...(props.data?.triggers || [])],
+});
 
 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 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;
+
+  state.triggers = [
+    { type: props.data.type, data: { ...props.data }, id: nanoid(5) },
+  ];
+});
 </script>
+<style>
+.trigger-item > button {
+  @apply focus:ring-0;
+  text-align: left;
+  .delete-btn {
+    visibility: hidden;
+  }
+  &:hover .delete-btn {
+    visibility: visible;
+  }
+}
+</style>

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

@@ -1,5 +1,5 @@
 <template>
-  <div class="mt-4">
+  <div>
     <template v-if="!permission.has[permissionName]">
       <p>
         {{ t('workflow.blocks.trigger.contextMenus.noPermission') }}

+ 1 - 1
src/components/newtab/workflow/edit/Trigger/TriggerDate.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="mt-2">
+  <div>
     <ui-input
       :model-value="data.date"
       :max="maxDate"

+ 1 - 1
src/components/newtab/workflow/edit/Trigger/TriggerElementChange.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="mt-4">
+  <div>
     <ui-input
       v-model="observeDetail.matchPattern"
       :label="t('workflow.blocks.trigger.element-change.target')"

+ 1 - 1
src/components/newtab/workflow/edit/Trigger/TriggerElementOptions.vue

@@ -1,5 +1,5 @@
 <template>
-  <ul class="space-y-2 mt-1">
+  <ul class="space-y-2">
     <li v-for="option in types" :key="option" class="group">
       <ui-checkbox
         :model-value="modelValue[option]"

+ 32 - 34
src/components/newtab/workflow/edit/Trigger/TriggerInterval.vue

@@ -1,39 +1,37 @@
 <template>
-  <div>
-    <div class="flex items-center mt-1">
-      <ui-input
-        :model-value="data.interval"
-        :label="t('workflow.blocks.trigger.forms.interval')"
-        type="number"
-        class="w-full"
-        placeholder="1-120"
-        min="1"
-        max="120"
-        @change="
-          updateIntervalInput($event, { key: 'interval', min: 1, max: 120 })
-        "
-      />
-      <ui-input
-        v-if="!data.fixedDelay"
-        :model-value="data.delay"
-        type="number"
-        class="w-full ml-2"
-        :label="t('workflow.blocks.trigger.forms.delay')"
-        min="0"
-        max="20"
-        placeholder="0-20"
-        @change="updateIntervalInput($event, { key: 'delay', min: 0, max: 20 })"
-      />
-    </div>
-    <ui-checkbox
-      :model-value="data.fixedDelay"
-      block
-      class="mt-2"
-      @change="emit('update', { fixedDelay: $event })"
-    >
-      {{ t('workflow.blocks.trigger.fixedDelay') }}
-    </ui-checkbox>
+  <div class="flex items-center">
+    <ui-input
+      :model-value="data.interval"
+      :label="t('workflow.blocks.trigger.forms.interval')"
+      type="number"
+      class="w-full"
+      placeholder="1-120"
+      min="1"
+      max="120"
+      @change="
+        updateIntervalInput($event, { key: 'interval', min: 1, max: 120 })
+      "
+    />
+    <ui-input
+      v-if="!data.fixedDelay"
+      :model-value="data.delay"
+      type="number"
+      class="w-full ml-2"
+      :label="t('workflow.blocks.trigger.forms.delay')"
+      min="0"
+      max="20"
+      placeholder="0-20"
+      @change="updateIntervalInput($event, { key: 'delay', min: 0, max: 20 })"
+    />
   </div>
+  <ui-checkbox
+    :model-value="data.fixedDelay"
+    block
+    class="mt-2"
+    @change="emit('update', { fixedDelay: $event })"
+  >
+    {{ t('workflow.blocks.trigger.fixedDelay') }}
+  </ui-checkbox>
 </template>
 <script setup>
 import { useI18n } from 'vue-i18n';

+ 1 - 1
src/components/newtab/workflow/edit/Trigger/TriggerKeyboardShortcut.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="mt-2">
+  <div>
     <div class="flex items-center mb-2">
       <ui-input
         :model-value="getReadableShortcut(recordKeys.keys)"

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

@@ -1,5 +1,5 @@
 <template>
-  <div class="mt-4">
+  <div>
     <ui-popover
       :options="{ animation: null }"
       trigger-width
@@ -44,11 +44,11 @@
         {{ t('workflow.blocks.trigger.addTime') }}
       </ui-button>
     </div>
-    <div class="my-2">
+    <div class="grid grid-cols-2 gap-x-4 gap-y-2 mt-4">
       <ui-expand
         v-for="(day, index) in sortedDaysArr"
         :key="day.id"
-        header-class="focus:ring-0 flex items-center py-2 w-full group text-left"
+        header-class="focus:ring-0 flex items-center w-full group text-left"
         type="time"
         class="w-full"
       >

+ 1 - 1
src/components/newtab/workflow/edit/Trigger/TriggerVisitWeb.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="mt-2">
+  <div>
     <ui-input
       :model-value="data.url"
       :placeholder="t('workflow.blocks.trigger.forms.url')"

+ 7 - 1
src/content/services/shortcutListener.js

@@ -31,7 +31,13 @@ function workflowShortcutsListener(findWorkflow, shortcutsObj) {
   if (shortcuts.length === 0) return;
 
   const keyboardShortcuts = shortcuts.reduce((acc, [id, value]) => {
-    const workflow = findWorkflow(id);
+    let workflowId = id;
+    if (id.startsWith('trigger')) {
+      const { 1: triggerWorkflowId } = id.split(':');
+      workflowId = triggerWorkflowId;
+    }
+
+    const workflow = findWorkflow(workflowId);
     if (!workflow) return acc;
 
     (acc[value] = acc[value] || []).push({

+ 6 - 4
src/utils/convertWorkflowData.js

@@ -1,10 +1,12 @@
 import { parseJSON, findTriggerBlock } from './helper';
 
+const getFlowData = (workflow) =>
+  typeof workflow.drawflow === 'string'
+    ? parseJSON(workflow.drawflow, {})
+    : workflow.drawflow;
+
 export default function (workflow) {
-  const data =
-    typeof workflow.drawflow === 'string'
-      ? parseJSON(workflow.drawflow, {})
-      : workflow.drawflow;
+  const data = getFlowData(workflow);
   if (!data?.drawflow) return workflow;
 
   const triggerBlock = findTriggerBlock(data);

+ 27 - 16
src/utils/workflowTrigger.js

@@ -59,7 +59,9 @@ export function registerContextMenu(workflowId, data) {
 
 async function removeFromWorkflowQueue(workflowId) {
   const { workflowQueue } = await browser.storage.local.get('workflowQueue');
-  const queueIndex = (workflowQueue || []).indexOf(workflowId);
+  const queueIndex = (workflowQueue || []).findIndex((id) =>
+    id.includes(workflowId)
+  );
 
   if (!workflowQueue || queueIndex === -1) return;
 
@@ -70,7 +72,12 @@ async function removeFromWorkflowQueue(workflowId) {
 
 export async function cleanWorkflowTriggers(workflowId) {
   try {
-    await browser.alarms.clear(workflowId);
+    const alarms = await browser.alarms.getAll();
+    for (const alarm of alarms) {
+      if (alarm.name.includes(workflowId)) {
+        await browser.alarms.clear(alarm.name);
+      }
+    }
 
     const { visitWebTriggers, onStartupTriggers, shortcuts } =
       await browser.storage.local.get([
@@ -80,27 +87,25 @@ export async function cleanWorkflowTriggers(workflowId) {
       ]);
 
     const keyboardShortcuts = Array.isArray(shortcuts) ? {} : shortcuts || {};
-    delete keyboardShortcuts[workflowId];
+    Object.keys(keyboardShortcuts).forEach((shortcutId) => {
+      if (!shortcutId.includes(workflowId)) return;
 
-    const startupTriggers = onStartupTriggers || [];
-    const startupTriggerIndex = startupTriggers.indexOf(workflowId);
-    if (startupTriggerIndex !== -1) {
-      startupTriggers.splice(startupTriggerIndex, 1);
-    }
+      delete keyboardShortcuts[shortcutId];
+    });
 
-    const visitWebTriggerIndex = visitWebTriggers.findIndex(
-      (item) => item.id === workflowId
+    const startupTriggers = (onStartupTriggers || []).filter(
+      (id) => !id.includes(workflowId)
+    );
+    const filteredVisitWebTriggers = visitWebTriggers.filter(
+      (item) => !item.id.includes(workflowId)
     );
-    if (visitWebTriggerIndex !== -1) {
-      visitWebTriggers.splice(visitWebTriggerIndex, 1);
-    }
 
-    await removeFromWorkflowQueue();
+    await removeFromWorkflowQueue(workflowId);
 
     await browser.storage.local.set({
-      visitWebTriggers,
       shortcuts: keyboardShortcuts,
       onStartupTriggers: startupTriggers,
+      visitWebTriggers: filteredVisitWebTriggers,
     });
 
     const removeFromContextMenu = async () => {
@@ -241,7 +246,13 @@ export async function registerWorkflowTrigger(workflowId, { data }) {
       'keyboard-shortcut': registerKeyboardShortcut,
     };
 
-    if (triggersHandler[data.type]) {
+    if (data.triggers) {
+      for (const trigger of data.triggers) {
+        const handler = triggersHandler[trigger.type];
+        if (handler)
+          await handler(`trigger:${workflowId}:${trigger.id}`, trigger.data);
+      }
+    } else if (triggersHandler[data.type]) {
       await triggersHandler[data.type](workflowId, data);
     }
   } catch (error) {