소스 검색

feat: add cron job trigger

Ahmad Kholid 2 년 전
부모
커밋
246d811fef

+ 1 - 0
package.json

@@ -49,6 +49,7 @@
     "codemirror": "^6.0.1",
     "compare-versions": "^4.1.2",
     "cron-parser": "^4.6.0",
+    "cronstrue": "^2.11.0",
     "crypto-js": "^4.1.1",
     "css-selector-generator": "^3.6.4",
     "dagre": "^0.8.5",

+ 26 - 32
src/background/index.js

@@ -9,6 +9,7 @@ import convertWorkflowData from '@/utils/convertWorkflowData';
 import getBlockMessage from '@/utils/getBlockMessage';
 import automa from '@business';
 import {
+  registerCronJob,
   registerSpecificDay,
   registerContextMenu,
   registerWorkflowTrigger,
@@ -378,11 +379,21 @@ browser.alarms.onAlarm.addListener(async ({ name }) => {
   const currentWorkflow = await workflow.get(workflowId);
   if (!currentWorkflow) return;
 
-  const drawflow =
-    typeof currentWorkflow.drawflow === 'string'
-      ? parseJSON(currentWorkflow.drawflow, {})
-      : currentWorkflow.drawflow;
-  const { data } = findTriggerBlock(drawflow) || {};
+  let data = currentWorkflow.trigger;
+  if (!data) {
+    const drawflow =
+      typeof currentWorkflow.drawflow === 'string'
+        ? parseJSON(currentWorkflow.drawflow, {})
+        : currentWorkflow.drawflow;
+    const { data: triggerBlockData } = findTriggerBlock(drawflow) || {};
+    data = triggerBlockData;
+  }
+
+  if (triggerId) {
+    data = data.triggers.find((trigger) => trigger.id === triggerId);
+    if (data) data = { ...data, ...data.data };
+  }
+
   if (data && data.type === 'interval' && data.fixedDelay) {
     const workflowState = await workflow.states.get(
       (item) => item.workflowId === workflowId
@@ -411,16 +422,15 @@ browser.alarms.onAlarm.addListener(async ({ name }) => {
   }
 
   workflow.execute(currentWorkflow);
+  console.log(data);
+  if (!data) return;
 
-  if (data && data.type === 'specific-day') {
-    let triggerData = data;
-    if (triggerId && data.triggers) {
-      triggerData = data.triggers.find(
-        (trigger) => trigger.id === triggerId
-      )?.data;
+  if (['specific-day', 'cron-job'].includes(data.type)) {
+    if (data.type === 'specific-day') {
+      registerSpecificDay(name, data);
+    } else {
+      registerCronJob(name, data);
     }
-
-    if (triggerData) registerSpecificDay(workflowId, triggerData);
   }
 });
 
@@ -545,26 +555,10 @@ browser.runtime.onStartup.addListener(async () => {
     }
 
     if (triggerBlock) {
-      if (triggerBlock.type === 'specific-day') {
-        const alarm = await browser.alarms.get(currWorkflow.id);
-
-        if (!alarm) await registerSpecificDay(currWorkflow.id, triggerBlock);
-      } else if (triggerBlock.type === 'date' && triggerBlock.date) {
-        const [hour, minute] = triggerBlock.time.split(':');
-        const date = dayjs(triggerBlock.date)
-          .hour(hour)
-          .minute(minute)
-          .second(0);
-
-        const isBefore = dayjs().isBefore(date);
-
-        if (isBefore) {
-          await browser.alarms.create(currWorkflow.id, {
-            when: date.valueOf(),
-          });
-        }
-      } else if (triggerBlock.type === 'on-startup') {
+      if (triggerBlock.type === 'on-startup') {
         workflow.execute(currWorkflow);
+      } else {
+        await registerWorkflowTrigger(currWorkflow.id, triggerBlock);
       }
     }
   }

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

@@ -60,6 +60,7 @@ 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 TriggerCronJob from '../workflow/edit/Trigger/TriggerCronJob.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';
@@ -89,6 +90,12 @@ const triggersData = {
       fixedDelay: false,
     },
   },
+  'cron-job': {
+    component: TriggerCronJob,
+    data: {
+      expression: '',
+    },
+  },
   'context-menu': {
     onlyOne: true,
     component: TriggerContextMenu,

+ 59 - 0
src/components/newtab/workflow/edit/Trigger/TriggerCronJob.vue

@@ -0,0 +1,59 @@
+<template>
+  <ui-input
+    :model-value="data.expression"
+    :label="t('workflow.blocks.trigger.forms.cron-expression')"
+    class="w-full -mt-2"
+    placeholder="0 15 10 ? * *"
+    @change="updateCronExpression($event, true)"
+  />
+  <p
+    class="ml-1 leading-tight mt-1"
+    :class="{ 'text-red-400 dark:text-red-500': state.isError }"
+  >
+    {{ state.nextSchedule }}
+  </p>
+</template>
+<script setup>
+import { shallowReactive, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+import cronParser from 'cron-parser';
+import { debounce } from '@/utils/helper';
+import { readableCron } from '@/lib/cronstrue';
+import dayjs from '@/lib/dayjs';
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['update']);
+
+const { t } = useI18n();
+
+const state = shallowReactive({
+  isError: false,
+  readableCron: '',
+  nextSchedule: '',
+});
+
+const updateCronExpression = debounce((expression, update = false) => {
+  try {
+    const cronExpression = cronParser.parseExpression(expression);
+
+    state.isError = false;
+    state.nextSchedule = `${readableCron(expression)} - ${t(
+      'scheduledWorkflow.nextRun'
+    )}: ${dayjs(cronExpression.next()).format('DD MMM YYYY, HH:mm:ss')}`;
+
+    if (update) emit('update', { expression });
+  } catch (error) {
+    state.isError = true;
+    state.nextSchedule = error.message;
+  }
+}, 100);
+
+onMounted(() => {
+  updateCronExpression(props.data.expression);
+});
+</script>

+ 21 - 0
src/lib/cronstrue.js

@@ -0,0 +1,21 @@
+import cronstrue from 'cronstrue';
+import 'cronstrue/locales/fr';
+import 'cronstrue/locales/zh_TW';
+import 'cronstrue/locales/zh_CN';
+
+const supportedLocales = ['en', 'zh', 'zh-tw', 'fr'];
+const altLocaleId = {
+  zh: 'zh_CN',
+  'zh-tw': 'zh_TW',
+};
+
+export function readableCron(expression) {
+  const currentLang = document.documentElement.lang;
+  const locale = supportedLocales.includes(currentLang)
+    ? altLocaleId[currentLang] || currentLang
+    : 'en';
+
+  return cronstrue.toString(expression, { locale });
+}
+
+export default cronstrue;

+ 3 - 1
src/locales/en/blocks.json

@@ -322,7 +322,8 @@
           "date": "Date",
           "time": "Time",
           "url": "URL or Regex",
-          "shortcut": "Shortcut"
+          "shortcut": "Shortcut",
+          "cron-expression": "Cron expression"
         },
         "element-change": {
           "target": "Target element to observe",
@@ -357,6 +358,7 @@
         "items": {
           "manual": "Manually",
           "interval": "Interval",
+          "cron-job": "Cron job",
           "date": "On a specific date",
           "context-menu": "Context menu",
           "element-change": "When element change",

+ 6 - 2
src/newtab/pages/ScheduledWorkflow.vue

@@ -124,6 +124,7 @@ import { useUserStore } from '@/stores/user';
 import { useWorkflowStore } from '@/stores/workflow';
 import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
 import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
+import { readableCron } from '@/lib/cronstrue';
 import { findTriggerBlock, objectHasKey } from '@/utils/helper';
 import {
   registerWorkflowTrigger,
@@ -154,7 +155,7 @@ const scheduleState = reactive({
 });
 
 let rowId = 0;
-const scheduledTypes = ['interval', 'date', 'specific-day'];
+const scheduledTypes = ['interval', 'date', 'specific-day', 'cron-job'];
 const tableHeaders = [
   {
     value: 'name',
@@ -234,6 +235,9 @@ function scheduleText(data) {
         'DD MMM YYYY, hh:mm:ss A'
       );
       break;
+    case 'cron-job':
+      text.schedule = readableCron(data.expression);
+      break;
     default:
   }
 
@@ -282,7 +286,7 @@ async function getTriggersData(triggerData, { id, name }) {
       }
     };
 
-    if (triggerData.triggers) {
+    if (triggerData?.triggers) {
       const result = await Promise.all(
         triggerData.triggers.map((trigger) => {
           const triggerItemData = { ...trigger };

+ 18 - 3
src/utils/workflowTrigger.js

@@ -1,5 +1,6 @@
 import browser from 'webextension-polyfill';
 import dayjs from 'dayjs';
+import cronParser from 'cron-parser';
 import { isObject } from './helper';
 
 export function registerContextMenu(workflowId, data) {
@@ -14,7 +15,7 @@ export function registerContextMenu(workflowId, data) {
     const browserContext = isFirefox ? browser.menus : browser.contextMenus;
 
     if (!browserContext) {
-      reject(new Error("Don't have context menu permission"));
+      resolve();
       return;
     }
 
@@ -171,7 +172,7 @@ export function registerInterval(workflowId, data) {
   return browser.alarms.create(workflowId, alarmInfo);
 }
 
-export function registerSpecificDate(workflowId, data) {
+export async function registerSpecificDate(workflowId, data) {
   let date = Date.now() + 60000;
 
   if (data.date) {
@@ -183,7 +184,9 @@ export function registerSpecificDate(workflowId, data) {
       .valueOf();
   }
 
-  return browser.alarms.create(workflowId, {
+  if (Date.now() > date) return;
+
+  await browser.alarms.create(workflowId, {
     when: date,
   });
 }
@@ -232,9 +235,21 @@ export async function registerOnStartup() {
   // Do nothing
 }
 
+export async function registerCronJob(workflowId, data) {
+  try {
+    const cronExpression = cronParser.parseExpression(data.expression);
+    const nextSchedule = cronExpression.next();
+
+    await browser.alarms.create(workflowId, { when: nextSchedule.getTime() });
+  } catch (error) {
+    console.error(error);
+  }
+}
+
 export const workflowTriggersMap = {
   interval: registerInterval,
   date: registerSpecificDate,
+  'cron-job': registerCronJob,
   'visit-web': registerVisitWeb,
   'on-startup': registerOnStartup,
   'specific-day': registerSpecificDay,

+ 5 - 0
yarn.lock

@@ -2753,6 +2753,11 @@ cron-parser@^4.6.0:
   dependencies:
     luxon "^3.0.1"
 
+cronstrue@^2.11.0:
+  version "2.11.0"
+  resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.11.0.tgz#18ff1b95a836b9b4e06854f796db2dc8fa98ce41"
+  integrity sha512-iIBCSis5yqtFYWtJAmNOiwDveFWWIn+8uV5UYuPHYu/Aeu5CSSJepSbaHMyfc+pPFgnsCcGzfPQEo7LSGmWbTg==
+
 cross-env@^7.0.3:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"