浏览代码

feat: add "On error" in blocks

Ahmad Kholid 3 年之前
父节点
当前提交
3b25ea2dfb
共有 31 个文件被更改,包括 364 次插入115 次删除
  1. 1 2
      package.json
  2. 39 16
      src/background/workflow-engine/engine.js
  3. 47 0
      src/components/block/BlockBasic.vue
  4. 1 0
      src/components/newtab/shared/SharedConditionBuilder/ConditionBuilderInputs.vue
  5. 26 1
      src/components/newtab/workflow/WorkflowEditBlock.vue
  6. 6 1
      src/components/newtab/workflow/edit/EditAttributeValue.vue
  7. 1 0
      src/components/newtab/workflow/edit/EditCloseTab.vue
  8. 1 0
      src/components/newtab/workflow/edit/EditElementExists.vue
  9. 1 0
      src/components/newtab/workflow/edit/EditExportData.vue
  10. 1 0
      src/components/newtab/workflow/edit/EditForms.vue
  11. 2 0
      src/components/newtab/workflow/edit/EditGetText.vue
  12. 12 2
      src/components/newtab/workflow/edit/EditGoogleSheets.vue
  13. 1 0
      src/components/newtab/workflow/edit/EditHandleDialog.vue
  14. 1 0
      src/components/newtab/workflow/edit/EditInteractionBase.vue
  15. 1 0
      src/components/newtab/workflow/edit/EditLoopData.vue
  16. 1 0
      src/components/newtab/workflow/edit/EditNewTab.vue
  17. 2 0
      src/components/newtab/workflow/edit/EditSaveAssets.vue
  18. 7 1
      src/components/newtab/workflow/edit/EditSwitchTab.vue
  19. 1 0
      src/components/newtab/workflow/edit/EditSwitchTo.vue
  20. 1 0
      src/components/newtab/workflow/edit/EditTakeScreenshot.vue
  21. 1 0
      src/components/newtab/workflow/edit/EditUploadFile.vue
  22. 1 0
      src/components/newtab/workflow/edit/EditWebhook.vue
  23. 142 0
      src/components/newtab/workflow/edit/OnBlockError.vue
  24. 9 20
      src/components/ui/UiAutocomplete.vue
  25. 3 1
      src/composable/editorBlock.js
  26. 2 0
      src/lib/v-remixicon.js
  27. 22 0
      src/locales/en/blocks.json
  28. 8 0
      src/locales/en/newtab.json
  29. 16 33
      src/newtab/pages/settings/SettingsIndex.vue
  30. 2 1
      src/utils/reference-data/index.js
  31. 5 37
      yarn.lock

+ 1 - 2
package.json

@@ -41,8 +41,8 @@
     "dayjs": "^1.10.7",
     "dayjs": "^1.10.7",
     "defu": "^5.0.1",
     "defu": "^5.0.1",
     "drawflow": "^0.0.51",
     "drawflow": "^0.0.51",
-    "floating-vue": "^2.0.0-beta.13",
     "idb": "^7.0.0",
     "idb": "^7.0.0",
+    "lodash.clonedeep": "^4.5.0",
     "mitt": "^3.0.0",
     "mitt": "^3.0.0",
     "mousetrap": "^1.6.5",
     "mousetrap": "^1.6.5",
     "nanoid": "^3.2.0",
     "nanoid": "^3.2.0",
@@ -52,7 +52,6 @@
     "v-remixicon": "^0.1.1",
     "v-remixicon": "^0.1.1",
     "vue": "^3.2.31",
     "vue": "^3.2.31",
     "vue-i18n": "^9.2.0-beta.29",
     "vue-i18n": "^9.2.0-beta.29",
-    "vue-mention": "^2.0.0-alpha.3",
     "vue-router": "^4.0.11",
     "vue-router": "^4.0.11",
     "vue-toastification": "^2.0.0-rc.5",
     "vue-toastification": "^2.0.0-rc.5",
     "vuedraggable": "^4.1.0",
     "vuedraggable": "^4.1.0",

+ 39 - 16
src/background/workflow-engine/engine.js

@@ -10,7 +10,7 @@ import {
   objectHasKey,
   objectHasKey,
 } from '@/utils/helper';
 } from '@/utils/helper';
 import referenceData from '@/utils/reference-data';
 import referenceData from '@/utils/reference-data';
-import { convertData, waitTabLoaded } from './helper';
+import { convertData, waitTabLoaded, getBlockConnection } from './helper';
 import executeContentScript from './execute-content-script';
 import executeContentScript from './execute-content-script';
 
 
 class WorkflowEngine {
 class WorkflowEngine {
@@ -371,7 +371,7 @@ class WorkflowEngine {
     }
     }
   }
   }
 
 
-  async executeBlock(block, prevBlockData) {
+  async executeBlock(block, prevBlockData, isRetry) {
     const currentState = await this.states.get(this.id);
     const currentState = await this.states.get(this.id);
 
 
     if (!currentState || currentState.isDestroyed) {
     if (!currentState || currentState.isDestroyed) {
@@ -385,8 +385,10 @@ class WorkflowEngine {
     this.referenceData.prevBlockData = prevBlockData;
     this.referenceData.prevBlockData = prevBlockData;
     this.referenceData.activeTabUrl = this.activeTab.url || '';
     this.referenceData.activeTabUrl = this.activeTab.url || '';
 
 
-    await this.states.update(this.id, { state: this.state });
-    this.dispatchEvent('update', { state: this.state });
+    if (!isRetry) {
+      await this.states.update(this.id, { state: this.state });
+      this.dispatchEvent('update', { state: this.state });
+    }
 
 
     const startExecuteTime = Date.now();
     const startExecuteTime = Date.now();
 
 
@@ -405,9 +407,19 @@ class WorkflowEngine {
     const replacedBlock = referenceData({
     const replacedBlock = referenceData({
       block,
       block,
       data: this.referenceData,
       data: this.referenceData,
-      refKeys: tasks[block.name].refDataKeys,
+      refKeys: isRetry ? null : tasks[block.name].refDataKeys,
     });
     });
     const blockDelay = this.workflow.settings?.blockDelay || 0;
     const blockDelay = this.workflow.settings?.blockDelay || 0;
+    const addBlockLog = (status, obj = {}) => {
+      this.addLogHistory({
+        type: status,
+        name: block.name,
+        description: block.data.description,
+        replacedValue: replacedBlock.replacedValue,
+        duration: Math.round(Date.now() - startExecuteTime),
+        ...obj,
+      });
+    };
 
 
     try {
     try {
       const result = await handler.call(this, replacedBlock, {
       const result = await handler.call(this, replacedBlock, {
@@ -418,13 +430,8 @@ class WorkflowEngine {
       if (result.replacedValue)
       if (result.replacedValue)
         replacedBlock.replacedValue = result.replacedValue;
         replacedBlock.replacedValue = result.replacedValue;
 
 
-      this.addLogHistory({
-        name: block.name,
+      addBlockLog(result.status || 'success', {
         logId: result.logId,
         logId: result.logId,
-        type: result.status || 'success',
-        description: block.data.description,
-        replacedValue: replacedBlock.replacedValue,
-        duration: Math.round(Date.now() - startExecuteTime),
       });
       });
 
 
       if (result.nextBlockId) {
       if (result.nextBlockId) {
@@ -440,12 +447,28 @@ class WorkflowEngine {
         this.destroy('success');
         this.destroy('success');
       }
       }
     } catch (error) {
     } catch (error) {
-      this.addLogHistory({
-        type: 'error',
+      const { onError: blockOnError } = replacedBlock.data;
+      if (blockOnError && blockOnError.enable) {
+        if (blockOnError.retry && blockOnError.retryTimes) {
+          await sleep(blockOnError.retryInterval * 1000);
+          blockOnError.retryTimes -= 1;
+          await this.executeBlock(replacedBlock, prevBlockData, true);
+
+          return;
+        }
+
+        const nextBlockId = getBlockConnection(
+          block,
+          blockOnError.toDo === 'continue' ? 1 : 2
+        );
+        if (blockOnError.toDo !== 'error' && nextBlockId) {
+          this.executeBlock(this.blocks[nextBlockId], '');
+          return;
+        }
+      }
+
+      addBlockLog('error', {
         message: error.message,
         message: error.message,
-        name: block.name,
-        description: block.data.description,
-        replacedValue: replacedBlock.replacedValue,
         ...(error.data || {}),
         ...(error.data || {}),
       });
       });
 
 

+ 47 - 0
src/components/block/BlockBasic.vue

@@ -32,6 +32,22 @@
         />
         />
       </div>
       </div>
     </div>
     </div>
+    <div
+      v-if="
+        block.data.onError?.enable && block.data.onError?.toDo === 'fallback'
+      "
+      class="fallback flex items-center justify-end"
+    >
+      <v-remixicon
+        v-if="block"
+        :title="t('workflow.blocks.base.onError.fallbackTitle')"
+        name="riInformationLine"
+        size="18"
+      />
+      <span class="ml-1">
+        {{ t('common.fallback') }}
+      </span>
+    </div>
     <slot :block="block"></slot>
     <slot :block="block"></slot>
     <template #prepend>
     <template #prepend>
       <div
       <div
@@ -48,6 +64,7 @@
   </block-base>
   </block-base>
 </template>
 </template>
 <script setup>
 <script setup>
+import { watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import emitter from '@/lib/mitt';
 import emitter from '@/lib/mitt';
 import { useEditorBlock } from '@/composable/editorBlock';
 import { useEditorBlock } from '@/composable/editorBlock';
@@ -87,10 +104,40 @@ function handleStartDrag(event) {
 
 
   event.dataTransfer.setData('block', JSON.stringify(payload));
   event.dataTransfer.setData('block', JSON.stringify(payload));
 }
 }
+
+watch(
+  () => block.data.onError,
+  (onError) => {
+    if (!onError) return;
+
+    const blockDetail = props.editor.getNodeFromId(block.id);
+    const outputLen = Object.keys(blockDetail.outputs).length;
+
+    if (!onError.enable || onError.toDo !== 'fallback') {
+      block.containerEl.classList.toggle('block-basic-fallback', false);
+
+      if (outputLen > 1) props.editor.removeNodeOutput(block.id, 'output_2');
+
+      return;
+    }
+
+    block.containerEl.classList.toggle('block-basic-fallback', true);
+
+    if (outputLen < 2) {
+      props.editor.addNodeOutput(block.id);
+    }
+
+    props.editor.updateConnectionNodes(`node-${block.id}`);
+  },
+  { deep: true }
+);
 </script>
 </script>
 <style>
 <style>
 .drawflow-node.selected .move-to-group,
 .drawflow-node.selected .move-to-group,
 .block-basic:hover .move-to-group {
 .block-basic:hover .move-to-group {
   visibility: visible;
   visibility: visible;
 }
 }
+.block-basic-fallback .output_2 {
+  top: 11px;
+}
 </style>
 </style>

+ 1 - 0
src/components/newtab/shared/SharedConditionBuilder/ConditionBuilderInputs.vue

@@ -26,6 +26,7 @@
         :items="autocomplete"
         :items="autocomplete"
         :trigger-char="['{{', '}}']"
         :trigger-char="['{{', '}}']"
         block
         block
+        hide-empty
         class="flex-1"
         class="flex-1"
       >
       >
         <ui-input
         <ui-input

+ 26 - 1
src/components/newtab/workflow/WorkflowEditBlock.vue

@@ -26,12 +26,20 @@
       :block-id="data.blockId"
       :block-id="data.blockId"
       :autocomplete="autocompleteList"
       :autocomplete="autocompleteList"
     />
     />
+    <on-block-error
+      v-if="!excludeOnError.includes(data.id)"
+      :key="data.blockId"
+      :data="data"
+      class="mt-4"
+      @change="$emit('update', { ...blockData, onError: $event })"
+    />
   </div>
   </div>
 </template>
 </template>
 <script>
 <script>
 import { computed, ref, watch } from 'vue';
 import { computed, ref, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import { tasks } from '@/utils/shared';
 import { tasks } from '@/utils/shared';
+import OnBlockError from './edit/OnBlockError.vue';
 
 
 const editComponents = require.context(
 const editComponents = require.context(
   './edit',
   './edit',
@@ -50,7 +58,7 @@ const components = editComponents.keys().reduce((acc, key) => {
 }, {});
 }, {});
 
 
 export default {
 export default {
-  components,
+  components: { ...components, OnBlockError },
   props: {
   props: {
     data: {
     data: {
       type: Object,
       type: Object,
@@ -72,6 +80,21 @@ export default {
   },
   },
   emits: ['close', 'update', 'update:autocomplete'],
   emits: ['close', 'update', 'update:autocomplete'],
   setup(props, { emit }) {
   setup(props, { emit }) {
+    const defaultAutocomplete = [
+      'activeTabUrl',
+      '$date',
+      '$randint',
+      '$getLength',
+      'globalData',
+    ];
+    const excludeOnError = [
+      'webhook',
+      'while-loop',
+      'element-exists',
+      'conditions',
+      'trigger',
+    ];
+
     const { t } = useI18n();
     const { t } = useI18n();
     const autocompleteData = ref({});
     const autocompleteData = ref({});
 
 
@@ -86,6 +109,7 @@ export default {
     const autocompleteList = computed(() => {
     const autocompleteList = computed(() => {
       const blockId = props.data.itemId || props.data.blockId;
       const blockId = props.data.itemId || props.data.blockId;
       const arr = [
       const arr = [
+        defaultAutocomplete,
         autocompleteData.value.table,
         autocompleteData.value.table,
         autocompleteData.value[blockId],
         autocompleteData.value[blockId],
       ];
       ];
@@ -179,6 +203,7 @@ export default {
     return {
     return {
       t,
       t,
       blockData,
       blockData,
+      excludeOnError,
       autocompleteList,
       autocompleteList,
     };
     };
   },
   },

+ 6 - 1
src/components/newtab/workflow/edit/EditAttributeValue.vue

@@ -1,7 +1,12 @@
 <template>
 <template>
   <edit-interaction-base v-bind="{ data, autocomplete }" @change="updateData">
   <edit-interaction-base v-bind="{ data, autocomplete }" @change="updateData">
     <hr />
     <hr />
-    <ui-autocomplete :items="autocomplete" :trigger-char="['{{', '}}']" block>
+    <ui-autocomplete
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+      hide-empty
+    >
       <ui-input
       <ui-input
         :model-value="data.attributeName"
         :model-value="data.attributeName"
         :label="t('workflow.blocks.attribute-value.forms.name')"
         :label="t('workflow.blocks.attribute-value.forms.name')"

+ 1 - 0
src/components/newtab/workflow/edit/EditCloseTab.vue

@@ -35,6 +35,7 @@
         :items="autocomplete"
         :items="autocomplete"
         :trigger-char="['{{', '}}']"
         :trigger-char="['{{', '}}']"
         block
         block
+        hide-empty
       >
       >
         <ui-input
         <ui-input
           :model-value="data.url"
           :model-value="data.url"

+ 1 - 0
src/components/newtab/workflow/edit/EditElementExists.vue

@@ -14,6 +14,7 @@
       :items="autocomplete"
       :items="autocomplete"
       :trigger-char="['{{', '}}']"
       :trigger-char="['{{', '}}']"
       block
       block
+      hide-empty
       class="mb-1"
       class="mb-1"
     >
     >
       <ui-input
       <ui-input

+ 1 - 0
src/components/newtab/workflow/edit/EditExportData.vue

@@ -20,6 +20,7 @@
       :items="autocomplete"
       :items="autocomplete"
       :trigger-char="['{{', '}}']"
       :trigger-char="['{{', '}}']"
       block
       block
+      hide-empty
       class="mt-2"
       class="mt-2"
     >
     >
       <ui-input
       <ui-input

+ 1 - 0
src/components/newtab/workflow/edit/EditForms.vue

@@ -39,6 +39,7 @@
           :items="autocomplete"
           :items="autocomplete"
           :trigger-char="['{{', '}}']"
           :trigger-char="['{{', '}}']"
           block
           block
+          hide-empty
           class="w-full mb-1"
           class="w-full mb-1"
         >
         >
           <ui-textarea
           <ui-textarea

+ 2 - 0
src/components/newtab/workflow/edit/EditGetText.vue

@@ -31,6 +31,7 @@
         :items="autocomplete"
         :items="autocomplete"
         :trigger-char="['{{', '}}']"
         :trigger-char="['{{', '}}']"
         block
         block
+        hide-empty
         class="w-full"
         class="w-full"
       >
       >
         <ui-input
         <ui-input
@@ -47,6 +48,7 @@
         :items="autocomplete"
         :items="autocomplete"
         :trigger-char="['{{', '}}']"
         :trigger-char="['{{', '}}']"
         block
         block
+        hide-empty
         class="w-full"
         class="w-full"
       >
       >
         <ui-input
         <ui-input

+ 12 - 2
src/components/newtab/workflow/edit/EditGoogleSheets.vue

@@ -18,7 +18,12 @@
         {{ t('workflow.blocks.google-sheets.select.update') }}
         {{ t('workflow.blocks.google-sheets.select.update') }}
       </option>
       </option>
     </ui-select>
     </ui-select>
-    <ui-autocomplete :items="autocomplete" :trigger-char="['{{', '}}']" block>
+    <ui-autocomplete
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+      hide-empty
+    >
       <ui-input
       <ui-input
         :model-value="data.spreadsheetId"
         :model-value="data.spreadsheetId"
         class="w-full"
         class="w-full"
@@ -38,7 +43,12 @@
         </template>
         </template>
       </ui-input>
       </ui-input>
     </ui-autocomplete>
     </ui-autocomplete>
-    <ui-autocomplete :items="autocomplete" :trigger-char="['{{', '}}']" block>
+    <ui-autocomplete
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+      hide-empty
+    >
       <ui-input
       <ui-input
         :model-value="data.range"
         :model-value="data.range"
         class="w-full mt-1"
         class="w-full mt-1"

+ 1 - 0
src/components/newtab/workflow/edit/EditHandleDialog.vue

@@ -19,6 +19,7 @@
       :items="autocomplete"
       :items="autocomplete"
       :trigger-char="['{{', '}}']"
       :trigger-char="['{{', '}}']"
       block
       block
+      hide-empty
       class="mt-1"
       class="mt-1"
     >
     >
       <ui-input
       <ui-input

+ 1 - 0
src/components/newtab/workflow/edit/EditInteractionBase.vue

@@ -25,6 +25,7 @@
         :items="autocomplete"
         :items="autocomplete"
         :trigger-char="['{{', '}}']"
         :trigger-char="['{{', '}}']"
         block
         block
+        hide-empty
         class="mb-1"
         class="mb-1"
       >
       >
         <ui-input
         <ui-input

+ 1 - 0
src/components/newtab/workflow/edit/EditLoopData.vue

@@ -49,6 +49,7 @@
       :items="autocomplete"
       :items="autocomplete"
       :trigger-char="['{{', '}}']"
       :trigger-char="['{{', '}}']"
       block
       block
+      hide-empty
       class="mt-2"
       class="mt-2"
     >
     >
       <ui-input
       <ui-input

+ 1 - 0
src/components/newtab/workflow/edit/EditNewTab.vue

@@ -11,6 +11,7 @@
       :items="autocomplete"
       :items="autocomplete"
       :trigger-char="['{{', '}}']"
       :trigger-char="['{{', '}}']"
       block
       block
+      hide-empty
       class="mt-2"
       class="mt-2"
     >
     >
       <ui-input
       <ui-input

+ 2 - 0
src/components/newtab/workflow/edit/EditSaveAssets.vue

@@ -33,6 +33,7 @@
       :items="autocomplete"
       :items="autocomplete"
       :trigger-char="['{{', '}}']"
       :trigger-char="['{{', '}}']"
       block
       block
+      hide-empty
     >
     >
       <ui-input
       <ui-input
         :model-value="data.url"
         :model-value="data.url"
@@ -48,6 +49,7 @@
         :items="autocomplete"
         :items="autocomplete"
         :trigger-char="['{{', '}}']"
         :trigger-char="['{{', '}}']"
         block
         block
+        hide-empty
         class="mt-4"
         class="mt-4"
       >
       >
         <ui-input
         <ui-input

+ 7 - 1
src/components/newtab/workflow/edit/EditSwitchTab.vue

@@ -1,6 +1,11 @@
 <template>
 <template>
   <div>
   <div>
-    <ui-autocomplete :items="autocomplete" :trigger-char="['{{', '}}']" block>
+    <ui-autocomplete
+      :items="autocomplete"
+      :trigger-char="['{{', '}}']"
+      block
+      hide-empty
+    >
       <ui-input
       <ui-input
         :model-value="data.matchPattern"
         :model-value="data.matchPattern"
         placeholder="https://example.com/*"
         placeholder="https://example.com/*"
@@ -37,6 +42,7 @@
       :items="autocomplete"
       :items="autocomplete"
       :trigger-char="['{{', '}}']"
       :trigger-char="['{{', '}}']"
       block
       block
+      hide-empty
       class="mt-2"
       class="mt-2"
       @change="updateData({ url: $event })"
       @change="updateData({ url: $event })"
     >
     >

+ 1 - 0
src/components/newtab/workflow/edit/EditSwitchTo.vue

@@ -24,6 +24,7 @@
       :items="autocomplete"
       :items="autocomplete"
       :trigger-char="['{{', '}}']"
       :trigger-char="['{{', '}}']"
       block
       block
+      hide-empty
       class="mt-2"
       class="mt-2"
     >
     >
       <ui-input
       <ui-input

+ 1 - 0
src/components/newtab/workflow/edit/EditTakeScreenshot.vue

@@ -33,6 +33,7 @@
         :items="autocomplete"
         :items="autocomplete"
         :trigger-char="['{{', '}}']"
         :trigger-char="['{{', '}}']"
         block
         block
+        hide-empty
         class="flex-1 mr-2"
         class="flex-1 mr-2"
       >
       >
         <ui-input
         <ui-input

+ 1 - 0
src/components/newtab/workflow/edit/EditUploadFile.vue

@@ -14,6 +14,7 @@
             :items="autocomplete"
             :items="autocomplete"
             :trigger-char="['{{', '}}']"
             :trigger-char="['{{', '}}']"
             block
             block
+            hide-empty
             class="mr-2"
             class="mr-2"
           >
           >
             <ui-input
             <ui-input

+ 1 - 0
src/components/newtab/workflow/edit/EditWebhook.vue

@@ -20,6 +20,7 @@
       :items="autocomplete"
       :items="autocomplete"
       :trigger-char="['{{', '}}']"
       :trigger-char="['{{', '}}']"
       block
       block
+      hide-empty
       class="mb-2"
       class="mb-2"
     >
     >
       <ui-input
       <ui-input

+ 142 - 0
src/components/newtab/workflow/edit/OnBlockError.vue

@@ -0,0 +1,142 @@
+<template>
+  <div class="on-block-error">
+    <ui-button @click="state.showModal = true">
+      <v-remixicon name="riShieldLine" class="-ml-1 mr-2" />
+      <span>
+        {{ t('workflow.blocks.base.onError.button') }}
+      </span>
+    </ui-button>
+    <ui-modal
+      v-model="state.showModal"
+      :title="t('workflow.blocks.base.onError.title')"
+      content-class="max-w-xl"
+    >
+      <div
+        class="p-4 rounded-lg bg-green-200 dark:bg-green-300 flex items-start text-black"
+      >
+        <v-remixicon name="riInformationLine" />
+        <p class="flex-1 ml-4 text-gray-100 dark:text-black">
+          {{ t('workflow.blocks.base.onError.info') }}
+        </p>
+      </div>
+      <div class="mt-8">
+        <label class="inline-flex">
+          <ui-switch v-model="state.data.enable" />
+          <span class="ml-2">
+            {{ t('common.enable') }}
+          </span>
+        </label>
+        <template v-if="state.data.enable">
+          <div class="mt-4">
+            <label class="inline-flex">
+              <ui-switch v-model="state.data.retry" />
+              <span class="ml-2">
+                {{ t('workflow.blocks.base.onError.retry') }}
+              </span>
+            </label>
+          </div>
+          <transition-expand>
+            <div v-if="state.data.retry" class="mt-2">
+              <div class="inline-flex items-center">
+                <span>
+                  {{ t('workflow.blocks.base.onError.times.name') }}
+                </span>
+                <v-remixicon
+                  :title="t('workflow.blocks.base.onError.times.description')"
+                  name="riInformationLine"
+                  size="20"
+                  class="mr-2"
+                />
+                <ui-input
+                  v-model.number="state.data.retryTimes"
+                  type="number"
+                  min="0"
+                  class="w-20"
+                />
+              </div>
+              <div class="inline-flex items-center ml-12">
+                <span>
+                  {{ t('workflow.blocks.base.onError.interval.name') }}
+                </span>
+                <v-remixicon
+                  :title="
+                    t('workflow.blocks.base.onError.interval.description')
+                  "
+                  name="riInformationLine"
+                  size="20"
+                  class="mr-2"
+                />
+                <ui-input
+                  v-model.number="state.data.retryInterval"
+                  type="number"
+                  min="0"
+                  class="w-20"
+                />
+                <span class="ml-1">
+                  {{ t('workflow.blocks.base.onError.interval.second') }}
+                </span>
+              </div>
+            </div>
+          </transition-expand>
+          <ui-select v-model="state.data.toDo" class="mt-4 w-56">
+            <option
+              v-for="type in toDoTypes"
+              :key="type"
+              :value="type"
+              :disabled="type === 'fallback' && data.isInGroup ? true : null"
+              class="to-do-type"
+            >
+              {{ t(`workflow.blocks.base.onError.toDo.${type}`) }}
+            </option>
+          </ui-select>
+        </template>
+      </div>
+    </ui-modal>
+  </div>
+</template>
+<script setup>
+import { reactive, watch, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['change']);
+
+const { t } = useI18n();
+
+const toDoTypes = ['error', 'continue', 'fallback'];
+
+const state = reactive({
+  showModal: false,
+  data: {
+    retry: false,
+    enable: false,
+    retryTimes: 1,
+    retryInterval: 2,
+    toDo: 'error',
+  },
+});
+
+watch(
+  () => state.data,
+  (onError) => {
+    if (!state.showModal) return;
+
+    emit('change', onError);
+  },
+  { deep: true }
+);
+
+onMounted(() => {
+  state.data = Object.assign(state.data, props.data.data.onError || {});
+});
+</script>
+<style scoped>
+.to-do-type.is-active {
+  @apply bg-accent dark:text-black text-gray-100 !important;
+}
+</style>

+ 9 - 20
src/components/ui/UiAutocomplete.vue

@@ -53,33 +53,17 @@ const props = defineProps({
     type: String,
     type: String,
     default: '',
     default: '',
   },
   },
-  label: {
-    type: String,
-    default: '',
-  },
-  placeholder: {
-    type: String,
-    default: '',
-  },
-  prependIcon: {
-    type: String,
-    default: null,
-  },
   triggerChar: {
   triggerChar: {
     type: Array,
     type: Array,
     default: () => [],
     default: () => [],
   },
   },
-  inputClass: {
-    type: String,
-    default: '',
-  },
   block: {
   block: {
     type: Boolean,
     type: Boolean,
     default: false,
     default: false,
   },
   },
-  component: {
-    type: String,
-    default: 'UiInput',
+  hideEmpty: {
+    type: Boolean,
+    default: false,
   },
   },
 });
 });
 const emit = defineEmits(['update:modelValue', 'change']);
 const emit = defineEmits(['update:modelValue', 'change']);
@@ -213,8 +197,9 @@ function selectItem(itemIndex) {
   if (isTriggerChar) {
   if (isTriggerChar) {
     setTimeout(() => {
     setTimeout(() => {
       input.selectionEnd = caretPosition;
       input.selectionEnd = caretPosition;
+      const isNotTextarea = input.tagName !== 'TEXTAREA';
 
 
-      if (!/textarea/i.test(props.component)) {
+      if (isNotTextarea) {
         input.blur();
         input.blur();
         input.focus();
         input.focus();
       }
       }
@@ -285,6 +270,10 @@ watch(
   () => state.showPopover,
   () => state.showPopover,
   (value) => {
   (value) => {
     if (!value) state.inputChanged = false;
     if (!value) state.inputChanged = false;
+
+    if (props.hideEmpty && filteredItems.value.length === 0) {
+      state.showPopover = false;
+    }
   }
   }
 );
 );
 
 

+ 3 - 1
src/composable/editorBlock.js

@@ -8,6 +8,7 @@ export function useEditorBlock(selector, editor) {
     details: {},
     details: {},
     category: {},
     category: {},
     retrieved: false,
     retrieved: false,
+    containerEl: null,
   });
   });
 
 
   nextTick(() => {
   nextTick(() => {
@@ -15,7 +16,8 @@ export function useEditorBlock(selector, editor) {
 
 
     if (block.id || !element) return;
     if (block.id || !element) return;
 
 
-    block.id = element.parentElement.parentElement.id.replace('node-', '');
+    block.containerEl = element.parentElement.parentElement;
+    block.id = block.containerEl.id.replace('node-', '');
 
 
     if (block.id) {
     if (block.id) {
       const { name, data } = editor.getNodeFromId(block.id);
       const { name, data } = editor.getNodeFromId(block.id);

+ 2 - 0
src/lib/v-remixicon.js

@@ -45,6 +45,7 @@ import {
   riWindowLine,
   riWindowLine,
   riPencilLine,
   riPencilLine,
   riGlobalLine,
   riGlobalLine,
+  riShieldLine,
   riCursorLine,
   riCursorLine,
   riUploadLine,
   riUploadLine,
   riFocus3Line,
   riFocus3Line,
@@ -153,6 +154,7 @@ export const icons = {
   riWindowLine,
   riWindowLine,
   riPencilLine,
   riPencilLine,
   riGlobalLine,
   riGlobalLine,
+  riShieldLine,
   riCursorLine,
   riCursorLine,
   riUploadLine,
   riUploadLine,
   riFocus3Line,
   riFocus3Line,

+ 22 - 0
src/locales/en/blocks.json

@@ -12,6 +12,28 @@
       "base": {
       "base": {
         "moveToGroup": "Move block to blocks group",
         "moveToGroup": "Move block to blocks group",
         "selector": "Element selector",
         "selector": "Element selector",
+        "onError": {
+          "info": "These rules will apply when an error occurs on the block",
+          "button": "On error",
+          "title": "On error occurs",
+          "retry": "Retry action",
+          "fallbackTitle": "Will execute when an error occurs in the block",
+          "times": {
+            "name": "Times",
+            "description": "The number of times to retry the action",
+          },
+          "interval": {
+            "name": "Interval",
+            "description": "The time interval to wait between each try",
+            "second": "second"
+          },
+          "toDo": {
+            "error": "Throw error",
+            "continue": "Continue flow",
+            "fallback": "Execute fallback",
+            "restart": "Restart flow"
+          }
+        },
         "table": {
         "table": {
           "checkbox": "Insert to table",
           "checkbox": "Insert to table",
           "select": "Select column",
           "select": "Select column",

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

@@ -24,6 +24,14 @@
     "shortcuts": {
     "shortcuts": {
       "duplicate": "Shortcut already use by \"{name}\""
       "duplicate": "Shortcut already use by \"{name}\""
     },
     },
+    "editor": {
+      "curvature": {
+        "title": "Editor Line Curvature",
+        "line": "Line",
+        "reroute": "Reroute",
+        "rerouteFirstLast": "Reroute first & last point"
+      },
+    },
     "language": {
     "language": {
       "label": "Language",
       "label": "Language",
       "helpTranslate": "Can't find your language? Help translate.",
       "helpTranslate": "Can't find your language? Help translate.",

+ 16 - 33
src/newtab/pages/settings/SettingsIndex.vue

@@ -55,44 +55,21 @@
     </p>
     </p>
   </div>
   </div>
   <div class="mt-12 max-w-2xl">
   <div class="mt-12 max-w-2xl">
-    <p class="font-semibold">Editor Curvature</p>
+    <p class="font-semibold">
+      {{ t('settings.editor.curvature.title') }}
+    </p>
     <div class="flex space-x-2 items-end">
     <div class="flex space-x-2 items-end">
       <ui-input
       <ui-input
-        :model-value="settings.editor.curvature"
-        label="Line"
+        v-for="item in curvatureSettings"
+        :key="item.id"
+        :model-value="settings.editor[item.key]"
+        :label="t(`settings.editor.curvature.${item.id}`)"
         type="number"
         type="number"
         min="0"
         min="0"
-        max="2"
-        class="w-full"
-        placeholder="0.5"
-        @change="updateSetting('editor.curvature', curvatureLimit($event))"
-      />
-      <ui-input
-        :model-value="settings.editor.reroute_curvature"
-        label="Reroute"
-        type="number"
+        max="1"
         class="w-full"
         class="w-full"
-        min="0"
-        max="2"
         placeholder="0.5"
         placeholder="0.5"
-        @change="
-          updateSetting('editor.reroute_curvature', curvatureLimit($event))
-        "
-      />
-      <ui-input
-        :model-value="settings.editor.reroute_curvature_start_end"
-        label="Reroute first & last point"
-        type="number"
-        class="w-full"
-        min="0"
-        max="2"
-        placeholder="0.5"
-        @change="
-          updateSetting(
-            'editor.reroute_curvature_start_end',
-            curvatureLimit($event)
-          )
-        "
+        @change="updateSetting(`editor.${item.key}`, curvatureLimit($event))"
       />
       />
     </div>
     </div>
   </div>
   </div>
@@ -105,6 +82,12 @@ import browser from 'webextension-polyfill';
 import { useTheme } from '@/composable/theme';
 import { useTheme } from '@/composable/theme';
 import { supportLocales } from '@/utils/shared';
 import { supportLocales } from '@/utils/shared';
 
 
+const curvatureSettings = [
+  { id: 'line', key: 'curvature' },
+  { id: 'reroute', key: 'reroute_curvature' },
+  { id: 'rerouteFirstLast', key: 'reroute_curvature_start_end' },
+];
+
 const { t } = useI18n();
 const { t } = useI18n();
 const store = useStore();
 const store = useStore();
 const theme = useTheme();
 const theme = useTheme();
@@ -113,7 +96,7 @@ const isLangChange = ref(false);
 const settings = computed(() => store.state.settings);
 const settings = computed(() => store.state.settings);
 
 
 function curvatureLimit(value) {
 function curvatureLimit(value) {
-  if (value > 2) return 2;
+  if (value > 1) return 1;
   if (value < 0) return 0;
   if (value < 0) return 0;
 
 
   return value;
   return value;

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

@@ -1,10 +1,11 @@
 import objectPath from 'object-path';
 import objectPath from 'object-path';
+import cloneDeep from 'lodash.clonedeep';
 import mustacheReplacer from './mustache-replacer';
 import mustacheReplacer from './mustache-replacer';
 
 
 export default function ({ block, refKeys, data }) {
 export default function ({ block, refKeys, data }) {
   if (!refKeys || refKeys.length === 0) return block;
   if (!refKeys || refKeys.length === 0) return block;
 
 
-  const copyBlock = JSON.parse(JSON.stringify(block));
+  const copyBlock = cloneDeep(block);
   const addReplacedValue = (value) => {
   const addReplacedValue = (value) => {
     if (!copyBlock.replacedValue) copyBlock.replacedValue = {};
     if (!copyBlock.replacedValue) copyBlock.replacedValue = {};
 
 

+ 5 - 37
yarn.lock

@@ -1153,18 +1153,6 @@
     minimatch "^3.0.4"
     minimatch "^3.0.4"
     strip-json-comments "^3.1.1"
     strip-json-comments "^3.1.1"
 
 
-"@floating-ui/core@^0.3.0":
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.3.1.tgz#3dde0ad0724d4b730567c92f49f0950910e18871"
-  integrity sha512-ensKY7Ub59u16qsVIFEo2hwTCqZ/r9oZZFh51ivcLGHfUwTn8l1Xzng8RJUe91H/UP8PeqeBronAGx0qmzwk2g==
-
-"@floating-ui/dom@^0.1.10":
-  version "0.1.10"
-  resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.1.10.tgz#ce304136a52c71ef157826d2ebf52d68fa2deed5"
-  integrity sha512-4kAVoogvQm2N0XE0G6APQJuCNuErjOfPW8Ux7DFxh8+AfugWflwVJ5LDlHOwrwut7z/30NUvdtHzQ3zSip4EzQ==
-  dependencies:
-    "@floating-ui/core" "^0.3.0"
-
 "@humanwhocodes/config-array@^0.5.0":
 "@humanwhocodes/config-array@^0.5.0":
   version "0.5.0"
   version "0.5.0"
   resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
   resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
@@ -3886,14 +3874,6 @@ flatted@^3.1.0:
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
   integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
   integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
 
 
-floating-vue@^2.0.0-beta.13:
-  version "2.0.0-beta.13"
-  resolved "https://registry.yarnpkg.com/floating-vue/-/floating-vue-2.0.0-beta.13.tgz#471c8dbee8c5eb19b6410b14a2865b199b099bb1"
-  integrity sha512-C2bGEtdbOXm+2rmkn8W6dTQeh3xJT7YbdHnrbanYDS3vK/1lumdXhYA6j2Qs+9shViNjoVUUND1EhLxLDP2OZA==
-  dependencies:
-    "@floating-ui/dom" "^0.1.10"
-    vue-resize "^2.0.0-alpha.1"
-
 follow-redirects@^1.0.0:
 follow-redirects@^1.0.0:
   version "1.14.9"
   version "1.14.9"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
@@ -4956,6 +4936,11 @@ lodash.castarray@^4.4.0:
   resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115"
   resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115"
   integrity sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU=
   integrity sha1-wCUTUV4wna3dTCTGDP3c9ZdtkRU=
 
 
+lodash.clonedeep@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+  integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
+
 lodash.debounce@^4.0.8:
 lodash.debounce@^4.0.8:
   version "4.0.8"
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
   resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@@ -6963,11 +6948,6 @@ text-table@^0.2.0:
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
 
 
-textarea-caret@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/textarea-caret/-/textarea-caret-3.1.0.tgz#5d5a35bb035fd06b2ff0e25d5359e97f2655087f"
-  integrity sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==
-
 through@^2.3.8:
 through@^2.3.8:
   version "2.3.8"
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -7238,18 +7218,6 @@ vue-loader@16.8.1:
     hash-sum "^2.0.0"
     hash-sum "^2.0.0"
     loader-utils "^2.0.0"
     loader-utils "^2.0.0"
 
 
-vue-mention@^2.0.0-alpha.3:
-  version "2.0.0-alpha.3"
-  resolved "https://registry.yarnpkg.com/vue-mention/-/vue-mention-2.0.0-alpha.3.tgz#8b82df71ec8daf47d5d3dc31be3b377275f5bb73"
-  integrity sha512-NtM6Z6UpqHByKJPyiy2SrBy3K7wyi/6bvXltaRfWcSQdNwW3YrWzrr1M7lYB4NoWRhDFuk+4X1GpY8HH06g+XQ==
-  dependencies:
-    textarea-caret "^3.1.0"
-
-vue-resize@^2.0.0-alpha.1:
-  version "2.0.0-alpha.1"
-  resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz#43eeb79e74febe932b9b20c5c57e0ebc14e2df3a"
-  integrity sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==
-
 vue-router@^4.0.11:
 vue-router@^4.0.11:
   version "4.0.14"
   version "4.0.14"
   resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.14.tgz#ce2028c1c5c33e30c7287950c973f397fce1bd65"
   resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.14.tgz#ce2028c1c5c33e30c7287950c973f397fce1bd65"