浏览代码

feat: improve webhook block

Ahmad Kholid 3 年之前
父节点
当前提交
7fda3459cb

+ 14 - 13
src/background/workflow-engine/blocks-handler.js

@@ -514,20 +514,21 @@ export function webhook({ data, outputs }) {
       reject(new Error('URL is empty'));
       reject(new Error('URL is empty'));
       return;
       return;
     }
     }
-    try {
-      const url = new URL(data.url);
 
 
-      if (!url.protocol.startsWith('http')) {
-        reject(new Error('URL is not valid'));
-        return;
-      }
-      executeWebhook({ ...data, workflowData: this.data });
-      resolve({
-        data: '',
-        nextBlockId: getBlockConnection({ outputs }),
-      });
-    } catch (error) {
-      reject(error);
+    if (!data.url.startsWith('http')) {
+      reject(new Error('URL is not valid'));
+      return;
     }
     }
+
+    executeWebhook(data)
+      .then(() => {
+        resolve({
+          data: '',
+          nextBlockId: getBlockConnection({ outputs }),
+        });
+      })
+      .catch((error) => {
+        reject(error);
+      });
   });
   });
 }
 }

+ 3 - 3
src/background/workflow-engine/error-message.js

@@ -1,4 +1,4 @@
-import { objectHasKey } from '@/utils/helper';
+import { objectHasKey, replaceMustache } from '@/utils/helper';
 
 
 const messages = {
 const messages = {
   'no-trigger-block': '"{{name}}"" workflow doesn\'t have a trigger block.',
   'no-trigger-block': '"{{name}}"" workflow doesn\'t have a trigger block.',
@@ -13,8 +13,8 @@ export default function (errorId, data) {
   if (!message) return `Can't find message for this error (${errorId})`;
   if (!message) return `Can't find message for this error (${errorId})`;
 
 
   /* eslint-disable-next-line */
   /* eslint-disable-next-line */
-  const resultMessage = message.replace(/{{\s*[\w\.]+\s*}}/g, (match) => {
-    const key = match.replace(/{|}/g, '');
+  const resultMessage = replaceMustache(message, (match) => {
+    const key = match.slice(2, -2);
 
 
     return objectHasKey(data, key) ? data[key] : key;
     return objectHasKey(data, key) ? data[key] : key;
   });
   });

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

@@ -64,6 +64,15 @@
       </router-link>
       </router-link>
     </div>
     </div>
     <div class="flex-grow"></div>
     <div class="flex-grow"></div>
+    <a
+      v-tooltip:right="'Documentation'"
+      href="https://github.com/kholid060/automa/wiki"
+      rel="noopener"
+      class="mb-8"
+      target="_blank"
+    >
+      <v-remixicon name="riBookOpenLine" />
+    </a>
     <a
     <a
       v-tooltip:right="'Github'"
       v-tooltip:right="'Github'"
       href="https://github.com/kholid060/automa"
       href="https://github.com/kholid060/automa"

+ 20 - 17
src/components/newtab/workflow/WorkflowEditBlock.vue

@@ -16,8 +16,7 @@
     />
     />
   </div>
   </div>
 </template>
 </template>
-<script></script>
-<script setup>
+<script>
 import { computed } from 'vue';
 import { computed } from 'vue';
 
 
 const editComponents = require.context(
 const editComponents = require.context(
@@ -38,22 +37,26 @@ const components = editComponents.keys().reduce((acc, key) => {
 
 
 export default {
 export default {
   components,
   components,
-};
-
-const props = defineProps({
-  data: {
-    type: Object,
-    default: () => ({}),
+  props: {
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
   },
   },
-});
-const emit = defineEmits(['close', 'update']);
+  emits: ['close', 'update'],
+  setup(props, { emit }) {
+    const blockData = computed({
+      get() {
+        return props.data.data || {};
+      },
+      set(value) {
+        emit('update', value);
+      },
+    });
 
 
-const blockData = computed({
-  get() {
-    return props.data.data || {};
+    return {
+      blockData,
+    };
   },
   },
-  set(value) {
-    emit('update', value);
-  },
-});
+};
 </script>
 </script>

+ 4 - 3
src/components/newtab/workflow/edit/EditLoopData.vue

@@ -66,10 +66,11 @@
         <p class="flex-1 text-overflow mx-4">{{ file.name }}</p>
         <p class="flex-1 text-overflow mx-4">{{ file.name }}</p>
         <template v-if="data.loopData.length > maxStrLength">
         <template v-if="data.loopData.length > maxStrLength">
           <p class="mr-2">File too large to edit</p>
           <p class="mr-2">File too large to edit</p>
-          <ui-button @click="updateData({ loopData: '[]' })"
-            >Clear data</ui-button
-          >
+          <ui-button @click="updateData({ loopData: '[]' })">
+            Clear data
+          </ui-button>
         </template>
         </template>
+        <p v-else>Max file size is 1MB</p>
       </div>
       </div>
       <div style="height: calc(100vh - 11rem)">
       <div style="height: calc(100vh - 11rem)">
         <prism-editor
         <prism-editor

+ 65 - 79
src/components/newtab/workflow/edit/EditWebhook.vue

@@ -1,8 +1,14 @@
 <template>
 <template>
   <div class="mb-2 mt-4">
   <div class="mb-2 mt-4">
+    <ui-textarea
+      :model-value="data.description"
+      placeholder="Description"
+      class="w-full mb-2"
+      @change="updateData({ description: $event })"
+    />
     <ui-input
     <ui-input
       :model-value="data.url"
       :model-value="data.url"
-      class="mb-3 w-full"
+      class="mb-2 w-full"
       placeholder="https://example.com/postreceive"
       placeholder="https://example.com/postreceive"
       required
       required
       title="The Post receive URL"
       title="The Post receive URL"
@@ -12,7 +18,7 @@
     <ui-select
     <ui-select
       :model-value="data.contentType"
       :model-value="data.contentType"
       placeholder="Select a content type"
       placeholder="Select a content type"
-      class="mb-3 w-full"
+      class="mb-2 w-full"
       @change="updateData({ contentType: $event })"
       @change="updateData({ contentType: $event })"
     >
     >
       <option
       <option
@@ -25,60 +31,57 @@
     </ui-select>
     </ui-select>
     <ui-input
     <ui-input
       :model-value="data.timeout"
       :model-value="data.timeout"
-      class="mb-3 w-full"
+      class="mb-2 w-full"
       placeholder="Timeout"
       placeholder="Timeout"
       title="Http request execution timeout(ms)"
       title="Http request execution timeout(ms)"
       type="number"
       type="number"
       @change="updateData({ timeout: +$event })"
       @change="updateData({ timeout: +$event })"
     />
     />
-    <button
-      class="mb-2 block w-full text-left focus:ring-0"
-      @click="showOptionsRef = !showOptionsRef"
-    >
-      <v-remixicon
-        name="riArrowLeftSLine"
-        class="mr-1 transition-transform align-middle inline-block -ml-1"
-        :rotate="showOptionsRef ? 270 : 180"
-      />
-      <span class="align-middle">Options Headers</span>
-    </button>
-    <transition-expand>
-      <div v-if="showOptionsRef" class="my-2 border-2 border-dashed p-2">
-        <div class="grid grid-cols-7 justify-items-center gap-2">
-          <span class="col-span-3 font-bold">KEY</span>
-          <span class="col-span-3 font-bold">VALUE</span>
-          <div class=""></div>
-          <template v-for="(items, index) in headerRef" :key="index">
-            <ui-input v-model="items.name" type="text" class="col-span-3" />
-            <ui-input v-model="items.value" type="text" class="col-span-3" />
-            <button class="focus:ring-0" @click="removeHeader(index)">
-              <v-remixicon name="riCloseCircleLine" size="20" />
-            </button>
-          </template>
-          <button
-            class="col-span-7 mt-2 block w-full text-center focus:ring-0"
-            @click="addHeader"
-          >
-            <span
-              ><v-remixicon
-                class="align-middle inline-block"
-                name="riAddLine"
-                size="20"
-            /></span>
-            <span class="align-middle">Add Header</span>
+    <ui-tabs v-model="activeTab" fill class="mb-4">
+      <ui-tab value="headers">Headers</ui-tab>
+      <ui-tab value="body">Content body</ui-tab>
+    </ui-tabs>
+    <ui-tab-panels :model-value="activeTab">
+      <ui-tab-panel
+        value="headers"
+        class="grid grid-cols-7 justify-items-center gap-2"
+      >
+        <template v-for="(items, index) in headerRef" :key="index">
+          <ui-input
+            v-model="items.name"
+            :placeholder="`Header ${index + 1}`"
+            type="text"
+            class="col-span-3"
+          />
+          <ui-input
+            v-model="items.value"
+            placeholder="Value"
+            type="text"
+            class="col-span-3"
+          />
+          <button @click="removeHeader(index)">
+            <v-remixicon name="riCloseCircleLine" size="20" />
           </button>
           </button>
-        </div>
-      </div>
-    </transition-expand>
-
-    <prism-editor
-      v-if="!showContentModalRef"
-      :highlight="highlighter('json')"
-      :model-value="data.content"
-      class="p-4 max-h-80 mb-3"
-      readonly
-      @click="showContentModalRef = true"
-    />
+        </template>
+        <ui-button
+          class="col-span-4 mt-4 block w-full"
+          variant="accent"
+          @click="addHeader"
+        >
+          <span> Add Header </span>
+        </ui-button>
+      </ui-tab-panel>
+      <ui-tab-panel value="body">
+        <prism-editor
+          v-if="!showContentModalRef"
+          :highlight="highlighter('json')"
+          :model-value="data.body"
+          class="p-4 max-h-80 mb-2"
+          readonly
+          @click="showContentModalRef = true"
+        />
+      </ui-tab-panel>
+    </ui-tab-panels>
     <ui-modal
     <ui-modal
       v-model="showContentModalRef"
       v-model="showContentModalRef"
       content-class="max-w-3xl"
       content-class="max-w-3xl"
@@ -92,36 +95,20 @@
         style="height: calc(100vh - 18rem)"
         style="height: calc(100vh - 18rem)"
       />
       />
       <div class="mt-3">
       <div class="mt-3">
-        <ul class="list-disc pl-5">
-          <li>
-            You can using <code>{%var[.index]%}</code> to dynamically calcute,
-            driven by
-            <a
-              href="https://github.com/janl/mustache.js"
-              class="border-b-2 border-red-300"
-              target="_blank"
-              >mustaches</a
-            >
-          </li>
-          <li>
-            Supported variables:
-            <ul class="list-disc space-y-2 mt-2 text-sm pl-5">
-              <li>column: Array[string]</li>
-              <template
-                v-for="column in workflow.data.value.dataColumns"
-                :key="column.name"
-              >
-                <li>{{ column.name }}: Array[{{ column.type }}]</li>
-              </template>
-            </ul>
-          </li>
-        </ul>
+        <a
+          href="https://github.com/Kholid060/automa/wiki/Features#reference-data"
+          rel="noopener"
+          class="border-b text-primary"
+          target="_blank"
+        >
+          Click here to learn how to add dynamic data
+        </a>
       </div>
       </div>
     </ui-modal>
     </ui-modal>
   </div>
   </div>
 </template>
 </template>
 <script setup>
 <script setup>
-import { inject, ref, watch } from 'vue';
+import { ref, watch } from 'vue';
 import { PrismEditor } from 'vue-prism-editor';
 import { PrismEditor } from 'vue-prism-editor';
 import { highlighter } from '@/lib/prism';
 import { highlighter } from '@/lib/prism';
 import { contentTypes } from '@/utils/shared';
 import { contentTypes } from '@/utils/shared';
@@ -132,11 +119,10 @@ const props = defineProps({
     default: () => ({}),
     default: () => ({}),
   },
   },
 });
 });
-const workflow = inject('workflow');
 const emit = defineEmits(['update:data']);
 const emit = defineEmits(['update:data']);
 
 
-const showOptionsRef = ref(false);
-const contentRef = ref(props.data.content);
+const activeTab = ref('headers');
+const contentRef = ref(props.data.body);
 const headerRef = ref(props.data.headers);
 const headerRef = ref(props.data.headers);
 const showContentModalRef = ref(false);
 const showContentModalRef = ref(false);
 
 
@@ -145,7 +131,7 @@ function updateData(value) {
 }
 }
 
 
 watch(contentRef, (value) => {
 watch(contentRef, (value) => {
-  updateData({ content: value });
+  updateData({ body: value });
 });
 });
 
 
 function removeHeader(index) {
 function removeHeader(index) {

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

@@ -7,6 +7,7 @@
       dark:text-gray-200
       dark:text-gray-200
       border-b
       border-b
       flex
       flex
+      space-x-1
       items-center
       items-center
       relative
       relative
     "
     "

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

@@ -3,6 +3,7 @@ import {
   riHome5Line,
   riHome5Line,
   riFolderLine,
   riFolderLine,
   riRefreshLine,
   riRefreshLine,
+  riBookOpenLine,
   riGithubFill,
   riGithubFill,
   riCodeSSlashLine,
   riCodeSSlashLine,
   riRecordCircleLine,
   riRecordCircleLine,
@@ -70,6 +71,7 @@ export const icons = {
   riHome5Line,
   riHome5Line,
   riFolderLine,
   riFolderLine,
   riRefreshLine,
   riRefreshLine,
+  riBookOpenLine,
   riGithubFill,
   riGithubFill,
   riCodeSSlashLine,
   riCodeSSlashLine,
   riRecordCircleLine,
   riRecordCircleLine,

+ 5 - 0
src/utils/helper.js

@@ -1,3 +1,8 @@
+export function replaceMustache(str, replacer) {
+  /* eslint-disable-next-line */
+  return str.replace(/{{\s*[\w\.@]+\s*}}/g, replacer);
+}
+
 export function openFilePicker(acceptedFileTypes = [], attrs = {}) {
 export function openFilePicker(acceptedFileTypes = [], attrs = {}) {
   return new Promise((resolve, reject) => {
   return new Promise((resolve, reject) => {
     const input = document.createElement('input');
     const input = document.createElement('input');

+ 7 - 7
src/utils/reference-data.js

@@ -1,5 +1,5 @@
 import { get, set } from 'object-path-immutable';
 import { get, set } from 'object-path-immutable';
-import { isObject, objectHasKey } from '@/utils/helper';
+import { isObject, objectHasKey, replaceMustache } from '@/utils/helper';
 
 
 const objectPath = { get, set };
 const objectPath = { get, set };
 
 
@@ -7,7 +7,7 @@ function parseKey(key) {
   const [dataKey, path] = key.split('@');
   const [dataKey, path] = key.split('@');
 
 
   if (['prevBlockData', 'loopData'].includes(dataKey))
   if (['prevBlockData', 'loopData'].includes(dataKey))
-    return { dataKey, path: path || '0' };
+    return { dataKey, path: path || '' };
 
 
   const pathArr = path.split('.');
   const pathArr = path.split('.');
   let dataPath = '';
   let dataPath = '';
@@ -31,16 +31,16 @@ function parseKey(key) {
 }
 }
 
 
 export default function (block, data) {
 export default function (block, data) {
-  const replaceKeys = ['url', 'fileName', 'name', 'value'];
+  const replaceKeys = ['url', 'fileName', 'name', 'value', 'body'];
   let replacedBlock = block;
   let replacedBlock = block;
 
 
   replaceKeys.forEach((blockDataKey) => {
   replaceKeys.forEach((blockDataKey) => {
     if (!objectHasKey(block.data, blockDataKey)) return;
     if (!objectHasKey(block.data, blockDataKey)) return;
 
 
-    const newDataValue = replacedBlock.data[blockDataKey].replace(
-      /\[(.+?)]/g,
+    const newDataValue = replaceMustache(
+      replacedBlock.data[blockDataKey],
       (match) => {
       (match) => {
-        const key = match.replace(/\[|]/g, '');
+        const key = match.slice(2, -2).replace(/\s/g, '');
         const { dataKey, path } = parseKey(key);
         const { dataKey, path } = parseKey(key);
 
 
         if (
         if (
@@ -50,7 +50,7 @@ export default function (block, data) {
           return data.prevBlockData;
           return data.prevBlockData;
         }
         }
 
 
-        return objectPath.get(data[dataKey], path) ?? match;
+        return JSON.stringify(objectPath.get(data[dataKey], path) ?? match);
       }
       }
     );
     );
 
 

+ 3 - 2
src/utils/shared.js

@@ -371,15 +371,16 @@ export const tasks = {
     editComponent: 'EditWebhook',
     editComponent: 'EditWebhook',
     category: 'general',
     category: 'general',
     inputs: 1,
     inputs: 1,
-    outputs: 0,
+    outputs: 1,
     allowedInputs: true,
     allowedInputs: true,
     maxConnection: 1,
     maxConnection: 1,
     data: {
     data: {
+      description: '',
       url: '',
       url: '',
       contentType: 'json',
       contentType: 'json',
       timeout: 10000,
       timeout: 10000,
       headers: [{ name: '', value: '' }],
       headers: [{ name: '', value: '' }],
-      content: '{\n "key": "{%data[0].key%}" \n}',
+      body: '{\n "key": {{ dataColumns@0.key }} \n}',
     },
     },
   },
   },
   'loop-data': {
   'loop-data': {

+ 33 - 40
src/utils/webhookUtil.js

@@ -1,22 +1,24 @@
-/* eslint-disable no-console */
-import Mustache from 'mustache';
+import { isObject } from './helper';
 
 
-const customTags = ['{%', '%}'];
-const renderContent = (content, workflowData, contentType) => {
-  console.log('renderContent', content, workflowData);
+const renderContent = (content, contentType) => {
   // 1. render the content
   // 1. render the content
   // 2. if the content type is json then parse the json
   // 2. if the content type is json then parse the json
   // 3. else parse to form data
   // 3. else parse to form data
-  const renderedJson = JSON.parse(
-    Mustache.render(content, workflowData, {}, customTags)
-  );
+  const renderedJson = JSON.parse(content);
+
   if (contentType === 'form') {
   if (contentType === 'form') {
     return Object.keys(renderedJson)
     return Object.keys(renderedJson)
-      .map((key) => {
-        return `${key}=${renderedJson[key]}`;
-      })
+      .map(
+        (key) =>
+          `${key}=${
+            isObject(renderedJson[key])
+              ? JSON.stringify(renderedJson[key])
+              : renderedJson[key]
+          }`
+      )
       .join('&');
       .join('&');
   }
   }
+
   return JSON.stringify(renderedJson);
   return JSON.stringify(renderedJson);
 };
 };
 
 
@@ -36,44 +38,35 @@ const convertContentType = (contentType) => {
     : 'application/x-www-form-urlencoded';
     : 'application/x-www-form-urlencoded';
 };
 };
 
 
-export function executeWebhook({
+export async function executeWebhook({
   url,
   url,
   contentType,
   contentType,
   headers,
   headers,
   timeout,
   timeout,
-  content,
-  workflowData,
+  body,
 }) {
 }) {
-  const finalContent = renderContent(content, workflowData, contentType);
-  const finalHeaders = filterHeaders(headers);
-  console.log(
-    'executeWebhook',
-    url,
-    contentType,
-    finalHeaders,
-    timeout,
-    finalContent,
-    workflowData
-  );
   const controller = new AbortController();
   const controller = new AbortController();
   const id = setTimeout(() => {
   const id = setTimeout(() => {
     controller.abort();
     controller.abort();
   }, timeout);
   }, timeout);
 
 
-  return fetch(url, {
-    method: 'POST',
-    headers: {
-      'Content-Type': convertContentType(contentType),
-      ...finalHeaders,
-    },
-    body: finalContent,
-    signal: controller.signal,
-  })
-    .then(() => {
-      clearTimeout(id);
-    })
-    .catch((err) => {
-      clearTimeout(id);
-      throw err;
+  try {
+    const finalHeaders = filterHeaders(headers);
+    const finalContent = renderContent(body, contentType);
+
+    await fetch(url, {
+      method: 'POST',
+      headers: {
+        'Content-Type': convertContentType(contentType),
+        ...finalHeaders,
+      },
+      body: finalContent,
+      signal: controller.signal,
     });
     });
+
+    clearTimeout(id);
+  } catch (error) {
+    clearTimeout(id);
+    throw error;
+  }
 }
 }