Procházet zdrojové kódy

Merge branch 'AutomaApp:dev' into dev

Zen před 3 roky
rodič
revize
18fc106b0c

+ 20 - 0
src/assets/css/tailwind.css

@@ -63,6 +63,26 @@ select:focus,
   overflow: hidden;
   text-overflow: ellipsis;
 }
+
+
+.custom-table thead {
+  @apply bg-box-transparent;
+}
+.custom-table thead th {
+  @apply font-semibold;
+}
+.custom-table thead th:first-child {
+  @apply rounded-l-lg;
+}
+.custom-table thead th:last-child {
+  @apply rounded-r-lg;
+}
+.custom-table tbody {
+  @apply divide-y;
+}
+
+
+
 pre {
   font-size: 15px;
 }

+ 5 - 1
src/background/index.js

@@ -320,7 +320,11 @@ browser.alarms.onAlarm.addListener(async ({ name }) => {
   const currentWorkflow = await workflow.get(name);
   if (!currentWorkflow) return;
 
-  const { data } = findTriggerBlock(JSON.parse(currentWorkflow.drawflow)) || {};
+  const drawflow =
+    typeof currentWorkflow.drawflow === 'string'
+      ? parseJSON(currentWorkflow.drawflow, {})
+      : currentWorkflow.drawflow;
+  const { data } = findTriggerBlock(drawflow) || {};
   if (data && data.type === 'interval' && data.fixedDelay) {
     const workflowState = await workflow.states.get(
       ({ workflowId }) => name === workflowId

+ 2 - 2
src/background/workflowEngine/blocksHandler/handlerConditions.js

@@ -42,7 +42,7 @@ function checkConditions(data, conditionOptions) {
   });
 }
 
-async function conditions({ data, outputs }, { prevBlockData, refData }) {
+async function conditions({ data, outputs, id }, { prevBlockData, refData }) {
   if (data.conditions.length === 0) {
     throw new Error('conditions-empty');
   }
@@ -62,7 +62,7 @@ async function conditions({ data, outputs }, { prevBlockData, refData }) {
       refData,
       activeTab: this.activeTab.id,
       sendMessage: (payload) =>
-        this._sendMessageToTab({ ...payload, isBlock: false }),
+        this._sendMessageToTab({ ...payload.data, name: 'conditions', id }),
     };
 
     const conditionsResult = await checkConditions(data, conditionPayload);

+ 3 - 0
src/background/workflowEngine/blocksHandler/handlerJavascriptCode.js

@@ -24,6 +24,9 @@ export async function javascriptCode({ outputs, data, ...block }, { refData }) {
     const payload = { ...block, data, refData: { variables: {} } };
     if (data.code.includes('automaRefData')) payload.refData = refData;
 
+    if (!data.code.includes('automaNextBlock'))
+      payload.data.code += `\nautomaNextBlock()`;
+
     const result = await this._sendMessageToTab(payload);
 
     if (result) {

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

@@ -109,6 +109,12 @@ const tabs = [
     path: '/workflows',
     shortcut: getShortcut('page:workflows', '/workflows'),
   },
+  {
+    id: 'schedule',
+    icon: 'riTimeLine',
+    path: '/schedule',
+    shortcut: getShortcut('page:schedule', '/triggers'),
+  },
   {
     id: 'collection',
     icon: 'riFolderLine',

+ 1 - 20
src/components/newtab/shared/SharedCard.vue

@@ -56,21 +56,12 @@
     <div class="flex items-center text-gray-600 dark:text-gray-200">
       <p class="flex-1">{{ state.date }}</p>
       <slot name="footer-content" />
-      <v-remixicon
-        v-if="state.triggerText"
-        v-tooltip="state.triggerText"
-        :class="{ 'ml-2': $slots['footer-content'] }"
-        name="riFlashlightLine"
-        size="20"
-      />
     </div>
   </ui-card>
 </template>
 <script setup>
-import { onMounted, shallowReactive } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { shallowReactive } from 'vue';
 import dayjs from '@/lib/dayjs';
-import triggerText from '@/utils/triggerText';
 
 const props = defineProps({
   data: {
@@ -93,18 +84,8 @@ const props = defineProps({
 
 defineEmits(['execute', 'click', 'menuSelected']);
 
-const { t } = useI18n();
-
 const state = shallowReactive({
   triggerText: null,
   date: dayjs(props.data.createdAt).fromNow(),
 });
-
-onMounted(async () => {
-  const { trigger, id } = props.data;
-
-  if (!trigger) return;
-
-  state.triggerText = await triggerText(trigger, t, id);
-});
 </script>

+ 60 - 18
src/components/newtab/workflow/WorkflowBuilder.vue

@@ -42,6 +42,7 @@
       v-model="contextMenu.show"
       :options="contextMenu.position"
       padding="p-3"
+      @close="clearContextMenu"
     >
       <ui-list class="space-y-1 w-52">
         <ui-list-item
@@ -220,6 +221,20 @@ export default {
         active: nodeContent,
       });
     }
+    function getRelativePosToEditor(clientX, clientY) {
+      const { x, y } = editor.value.precanvas.getBoundingClientRect();
+      const { clientWidth, clientHeight } = editor.value.precanvas;
+      const { zoom } = editor.value;
+
+      const xPosition =
+        clientX * (clientWidth / (clientWidth * zoom)) -
+        x * (clientWidth / (clientWidth * zoom));
+      const yPosition =
+        clientY * (clientHeight / (clientHeight * zoom)) -
+        y * (clientHeight / (clientHeight * zoom));
+
+      return { xPosition, yPosition };
+    }
     function dropHandler({ dataTransfer, clientX, clientY, target }) {
       const block = JSON.parse(dataTransfer.getData('block') || null);
 
@@ -302,20 +317,7 @@ export default {
 
       if (block.fromBlockBasic) return;
 
-      const xPosition =
-        clientX *
-          (editor.value.precanvas.clientWidth /
-            (editor.value.precanvas.clientWidth * editor.value.zoom)) -
-        editor.value.precanvas.getBoundingClientRect().x *
-          (editor.value.precanvas.clientWidth /
-            (editor.value.precanvas.clientWidth * editor.value.zoom));
-      const yPosition =
-        clientY *
-          (editor.value.precanvas.clientHeight /
-            (editor.value.precanvas.clientHeight * editor.value.zoom)) -
-        editor.value.precanvas.getBoundingClientRect().y *
-          (editor.value.precanvas.clientHeight /
-            (editor.value.precanvas.clientHeight * editor.value.zoom));
+      const { xPosition, yPosition } = getRelativePosToEditor(clientX, clientY);
 
       const blockId = editor.value.addNode(
         block.id,
@@ -418,7 +420,9 @@ export default {
       activeNode = null;
     }
     function duplicateBlock(nodeId, isPaste = false) {
+      let initialPos = null;
       const nodes = new Map();
+
       const addNode = (id) => {
         const node = editor.value.getNodeFromId(id);
 
@@ -431,6 +435,15 @@ export default {
         store.state.copiedNodes.forEach((node) => {
           nodes.set(node.id, node);
         });
+
+        const pos = contextMenu?.position?.getReferenceClientRect?.() ?? null;
+        if (pos) {
+          const { xPosition, yPosition } = getRelativePosToEditor(
+            pos.left,
+            pos.top
+          );
+          initialPos = { x: xPosition, y: yPosition };
+        }
       } else {
         if (nodeId) addNode(nodeId);
         else if (activeNode) addNode(activeNode.id);
@@ -442,10 +455,12 @@ export default {
         });
       }
 
-      const nodesOutputs = [];
-
       clearSelectedElements();
 
+      const nodesOutputs = [];
+      let firstNodePos = null;
+      let index = 0;
+
       nodes.forEach((node) => {
         const { outputs, inputs } = tasks[node.name];
 
@@ -455,12 +470,28 @@ export default {
         const blockInputs = inputsLen || inputs;
         const blockOutputs = outputsLen || outputs;
 
+        let nodePosX = node.pos_x;
+        let nodePosY = node.pos_y;
+
+        if (initialPos && index === 0) {
+          firstNodePos = { x: nodePosX, y: nodePosY };
+
+          nodePosX = initialPos.x;
+          nodePosY = initialPos.y;
+        } else if (firstNodePos) {
+          const xDistance = nodePosX - firstNodePos.x;
+          const yDistance = nodePosY - firstNodePos.y;
+
+          nodePosX = initialPos.x + xDistance;
+          nodePosY = initialPos.y + yDistance;
+        }
+
         const newNodeId = editor.value.addNode(
           node.name,
           blockInputs,
           blockOutputs,
-          node.pos_x + 25,
-          node.pos_y + 70,
+          nodePosX + 25,
+          nodePosY + 70,
           node.name,
           node.data,
           node.html,
@@ -483,6 +514,8 @@ export default {
         if (outputsLen > 0) {
           nodesOutputs.push({ id: newNodeId, outputs: node.outputs });
         }
+
+        index += 1;
       });
 
       if (nodesOutputs.length < 1) return;
@@ -640,6 +673,14 @@ export default {
         selectedElements.push(nodeProperties);
       }
     }
+    function clearContextMenu() {
+      Object.assign(contextMenu, {
+        items: [],
+        data: null,
+        show: false,
+        position: {},
+      });
+    }
     function copyBlocks() {
       let nodes = selectedElements;
 
@@ -897,6 +938,7 @@ export default {
       contextMenu,
       dropHandler,
       handleDragOver,
+      clearContextMenu,
       contextMenuHandler: {
         copyBlocks,
         deleteBlock,

+ 8 - 4
src/composable/shortcut.js

@@ -10,19 +10,23 @@ const defaultShortcut = {
   },
   'page:workflows': {
     id: 'page:workflows',
-    combo: 'option+2',
+    combo: 'option+w',
+  },
+  'page:schedule': {
+    id: 'page:schedule',
+    combo: 'option+t',
   },
   'page:collections': {
     id: 'page:collections',
-    combo: 'option+3',
+    combo: 'option+c',
   },
   'page:logs': {
     id: 'page:logs',
-    combo: 'option+4',
+    combo: 'option+l',
   },
   'page:settings': {
     id: 'page:settings',
-    combo: 'option+5',
+    combo: 'option+s',
   },
   'action:search': {
     id: 'action:search',

+ 12 - 4
src/content/handleTestCondition.js → src/content/blocksHandler/handlerConditions.js

@@ -1,14 +1,22 @@
 import { customAlphabet } from 'nanoid/non-secure';
 import { visibleInViewport, isXPath } from '@/utils/helper';
-import FindElement from '@/utils/FindElement';
-import { automaRefDataStr } from './utils';
+import handleSelector from '../handleSelector';
+import { automaRefDataStr } from '../utils';
 
 const nanoid = customAlphabet('1234567890abcdef', 5);
 
-function handleConditionElement({ data, type }) {
+async function handleConditionElement({ data, type, id, frameSelector }) {
   const selectorType = isXPath(data.selector) ? 'xpath' : 'cssSelector';
 
-  const element = FindElement[selectorType](data);
+  const element = await handleSelector({
+    id,
+    data: {
+      ...data,
+      findBy: selectorType,
+    },
+    frameSelector,
+    type: selectorType,
+  });
   const { 1: actionType } = type.split('#');
 
   const elementActions = {

+ 0 - 6
src/content/index.js

@@ -3,7 +3,6 @@ import findSelector from '@/lib/findSelector';
 import { toCamelCase } from '@/utils/helper';
 import blocksHandler from './blocksHandler';
 import showExecutedBlock from './showExecutedBlock';
-import handleTestCondition from './handleTestCondition';
 import shortcutListener from './services/shortcutListener';
 // import elementObserver from './elementObserver';
 import { elementSelectorInstance } from './utils';
@@ -182,11 +181,6 @@ function messageListener({ data, source }) {
           });
       } else {
         switch (data.type) {
-          case 'condition-builder':
-            handleTestCondition(data.data)
-              .then((result) => resolve(result))
-              .catch((error) => reject(error));
-            break;
           case 'content-script-exists':
             resolve(true);
             break;

+ 2 - 0
src/lib/vRemixicon.js

@@ -19,6 +19,7 @@ import {
   riMoreLine,
   riStopLine,
   riSortDesc,
+  riTimeLine,
   riFlagLine,
   riGroupLine,
   riGuideLine,
@@ -132,6 +133,7 @@ export const icons = {
   riMoreLine,
   riStopLine,
   riSortDesc,
+  riTimeLine,
   riFlagLine,
   riGroupLine,
   riGuideLine,

+ 1 - 0
src/locales/en/common.json

@@ -5,6 +5,7 @@
     "collection": "Collection | Collections",
     "log": "Log | Logs",
     "block": "Block | Blocks",
+    "schedule": "Schedule",
     "folder": "Folder | Folders",
     "new": "New",
     "docs": "Documentation",

+ 13 - 0
src/locales/en/newtab.json

@@ -8,6 +8,19 @@
     "text": "Get started by reading the documentation or browsing workflows in the Automa Marketplace.",
     "marketplace": "Marketplace"
   },
+  "scheduledWorkflow": {
+    "title": "Scheduled workflows",
+    "nextRun": "Next run",
+    "active": "Active",
+    "refresh": "Refresh",
+    "schedule":{
+      "title": "Schedule",
+      "types": {
+        "general": "Every {time}",
+        "interval": "Every {time} minutes"
+      }
+    }
+  },
   "updateMessage": {
     "text1": "Automa has been updated to v{version},",
     "text2": "see what's new."

+ 194 - 0
src/newtab/pages/ScheduledWorkflow.vue

@@ -0,0 +1,194 @@
+<template>
+  <div class="container pt-8 pb-4">
+    <h1 class="text-2xl font-semibold mb-8 capitalize">
+      {{ t('scheduledWorkflow.title', 2) }}
+    </h1>
+    <ui-input
+      v-model="state.query"
+      prepend-icon="riSearch2Line"
+      :placeholder="t('common.search')"
+    />
+    <table class="w-full mt-4 custom-table">
+      <thead>
+        <tr class="text-left font-semibold">
+          <th class="w-3/12">{{ t('common.name') }}</th>
+          <th class="w-4/12">{{ t('scheduledWorkflow.schedule.title') }}</th>
+          <th>{{ t('scheduledWorkflow.nextRun') }}</th>
+          <th class="text-center">{{ t('scheduledWorkflow.active') }}</th>
+          <th></th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr v-for="trigger in triggers" :key="trigger.id" class="hoverable">
+          <td>
+            <router-link
+              :to="`/workflows/${trigger.workflowId}`"
+              class="block h-full w-full"
+              style="min-height: 20px"
+            >
+              {{ trigger.name }}
+            </router-link>
+          </td>
+          <td v-tooltip="{ content: trigger.scheduleDetail, allowHTML: true }">
+            {{ trigger.schedule }}
+          </td>
+          <td>
+            {{ trigger.nextRun }}
+          </td>
+          <td class="text-center">
+            <v-remixicon
+              v-if="trigger.active"
+              class="text-green-500 dark:text-green-400 inline-block"
+              name="riCheckLine"
+            />
+          </td>
+          <td class="text-right">
+            <button
+              v-tooltip="t('scheduledWorkflow.refresh')"
+              class="rounded-md text-gray-600 dark:text-gray-300"
+              @click="refreshSchedule(trigger.id)"
+            >
+              <v-remixicon name="riRefreshLine" />
+            </button>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</template>
+<script setup>
+import { onMounted, reactive, computed } from 'vue';
+import { useI18n } from 'vue-i18n';
+import dayjs from 'dayjs';
+import browser from 'webextension-polyfill';
+import { findTriggerBlock } from '@/utils/helper';
+import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
+import Workflow from '@/models/workflow';
+
+const { t } = useI18n();
+
+const triggersData = {};
+const state = reactive({
+  query: '',
+  triggers: [],
+  activeTrigger: 'scheduled',
+});
+
+let rowId = 0;
+const scheduledTypes = ['interval', 'date', 'specific-day'];
+
+const triggers = computed(() =>
+  state.triggers.filter(({ name }) =>
+    name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())
+  )
+);
+
+function scheduleText(data) {
+  const text = {
+    schedule: '',
+    scheduleDetail: '',
+  };
+
+  switch (data.type) {
+    case 'specific-day': {
+      const days = data.days.map((item) => {
+        const day = t(`workflow.blocks.trigger.days.${item.id}`);
+        text.scheduleDetail += `${day}: ${item.times.join(', ')}<br>`;
+
+        return day;
+      });
+      text.schedule = t('scheduledWorkflow.schedule.types.general', {
+        time: days.join(', '),
+      });
+      break;
+    }
+    case 'interval':
+      text.schedule = t('scheduledWorkflow.schedule.types.interval', {
+        time: data.interval,
+      });
+      break;
+    case 'data':
+      dayjs(data.date).format('DD MMM YYYY, hh:mm:ss A');
+      break;
+    default:
+  }
+
+  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,
+  };
+
+  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'
+      );
+    }
+
+    triggersData[rowId] = {
+      ...trigger,
+      workflow: { id, name },
+    };
+    Object.assign(triggerObj, scheduleText(trigger));
+
+    return triggerObj;
+  } catch (error) {
+    console.error(error);
+    return null;
+  }
+}
+async function refreshSchedule(id) {
+  try {
+    const triggerData = triggersData[id];
+    if (!triggerData) return;
+
+    await registerWorkflowTrigger(triggerData.workflow.id, {
+      data: triggerData,
+    });
+
+    const triggerObj = await getTriggerObj(triggerData, triggerData.workflow);
+    if (!triggerObj) return;
+
+    const triggerIndex = state.triggers.findIndex(
+      (trigger) => trigger.id === id
+    );
+    if (triggerIndex === -1) return;
+
+    state.triggers[triggerIndex] = triggerObj;
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+onMounted(async () => {
+  const workflows = Workflow.all();
+
+  for (const workflow of workflows) {
+    let { trigger } = workflow;
+
+    if (!trigger) {
+      const drawflow =
+        typeof workflow.drawflow === 'string'
+          ? JSON.parse(workflow.drawflow)
+          : workflow.drawflow;
+      trigger = findTriggerBlock(drawflow)?.data;
+    }
+
+    const obj = await getTriggerObj(trigger, workflow);
+    if (obj) state.triggers.push(obj);
+  }
+});
+</script>

+ 6 - 0
src/newtab/router.js

@@ -3,6 +3,7 @@ import Welcome from './pages/Welcome.vue';
 import Workflows from './pages/Workflows.vue';
 import WorkflowHost from './pages/workflows/Host.vue';
 import WorkflowDetails from './pages/workflows/[id].vue';
+import ScheduledWorkflow from './pages/ScheduledWorkflow.vue';
 import Collections from './pages/Collections.vue';
 import CollectionsDetails from './pages/collections/[id].vue';
 import Logs from './pages/Logs.vue';
@@ -31,6 +32,11 @@ const routes = [
     path: '/workflows',
     component: Workflows,
   },
+  {
+    name: 'schedule',
+    path: '/schedule',
+    component: ScheduledWorkflow,
+  },
   {
     name: 'workflows-details',
     path: '/workflows/:id',