浏览代码

feat: rename webhook to HTTP request

Ahmad Kholid 3 年之前
父节点
当前提交
9e81bb8aa0

+ 31 - 2
src/background/workflow-engine/blocks-handler/handler-webhook.js

@@ -1,3 +1,4 @@
+import objectPath from 'object-path';
 import { getBlockConnection } from '../helper';
 import { isWhitespace } from '@/utils/helper';
 import { executeWebhook } from '@/utils/webhookUtil';
@@ -9,11 +10,39 @@ export async function webhook({ data, outputs }) {
     if (isWhitespace(data.url)) throw new Error('url-empty');
     if (!data.url.startsWith('http')) throw new Error('invalid-url');
 
-    await executeWebhook(data);
+    const response = await executeWebhook(data);
+
+    if (!response.ok) {
+      throw new Error(`(${response.status}) ${response.statusText}`);
+    }
+
+    if (!data.assignVariable && !data.saveData) {
+      return {
+        data: '',
+        nextBlockId,
+      };
+    }
+
+    let returnData = '';
+
+    if (data.responseType === 'json') {
+      const jsonRes = await response.json();
+
+      returnData = objectPath.get(jsonRes, data.dataPath);
+    } else {
+      returnData = await response.text();
+    }
+
+    if (data.assignVariable) {
+      this.referenceData.variables[data.variableName] = returnData;
+    }
+    if (data.saveData) {
+      this.addDataToColumn(data.dataColumn, returnData);
+    }
 
     return {
-      data: '',
       nextBlockId,
+      data: returnData,
     };
   } catch (error) {
     error.nextBlockId = nextBlockId;

+ 1 - 0
src/components/newtab/logs/LogsDataViewer.vue

@@ -37,6 +37,7 @@
   <shared-codemirror
     :model-value="dataStr"
     :class="editorClass"
+    class="rounded-t-none"
     lang="json"
     readonly
   />

+ 1 - 1
src/components/newtab/shared/SharedCodemirror.vue

@@ -2,7 +2,7 @@
   <div
     ref="containerEl"
     :class="{ 'hide-gutters': !lineNumbers }"
-    class="codemirror relative overflow-auto"
+    class="codemirror relative overflow-auto rounded-lg"
   ></div>
 </template>
 <script setup>

+ 14 - 15
src/components/newtab/workflow/edit/EditGetText.vue

@@ -75,22 +75,21 @@
     >
       {{ t('workflow.blocks.get-text.checkbox') }}
     </ui-checkbox>
-    <div v-if="data.saveData" class="flex items-center mt-2 mb-4">
-      <ui-select
-        :model-value="data.dataColumn"
-        placeholder="Select column"
-        class="mr-2 flex-1"
-        @change="updateData({ dataColumn: $event })"
+    <ui-select
+      v-if="data.saveData"
+      :model-value="data.dataColumn"
+      placeholder="Select column"
+      class="w-full mt-2 mb-4"
+      @change="updateData({ dataColumn: $event })"
+    >
+      <option
+        v-for="column in workflow.data.value.table"
+        :key="column.name"
+        :value="column.name"
       >
-        <option
-          v-for="column in workflow.data.value.table"
-          :key="column.name"
-          :value="column.name"
-        >
-          {{ column.name }}
-        </option>
-      </ui-select>
-    </div>
+        {{ column.name }}
+      </option>
+    </ui-select>
     <ui-checkbox
       :model-value="data.addExtraRow"
       block

+ 96 - 10
src/components/newtab/workflow/edit/EditWebhook.vue

@@ -6,18 +6,28 @@
       class="w-full mb-2"
       @change="updateData({ description: $event })"
     />
+    <ui-select
+      :model-value="data.method || 'POST'"
+      :label="t('workflow.blocks.webhook.method')"
+      class="mb-2 w-full"
+      @change="updateMethod"
+    >
+      <option v-for="method in methods" :key="method" :value="method">
+        {{ method }}
+      </option>
+    </ui-select>
     <ui-input
       :model-value="data.url"
+      :label="`${t('workflow.blocks.webhook.url')}*`"
+      placeholder="http://api.example.com"
       class="mb-2 w-full"
-      placeholder="https://example.com/postreceive"
       required
-      :title="t('workflow.blocks.webhook.url')"
       type="url"
       @change="updateData({ url: $event })"
     />
     <ui-select
       :model-value="data.contentType"
-      :placeholder="t('workflow.blocks.webhook.contentType')"
+      :label="t('workflow.blocks.webhook.contentType')"
       class="mb-2 w-full"
       @change="updateData({ contentType: $event })"
     >
@@ -31,32 +41,39 @@
     </ui-select>
     <ui-input
       :model-value="data.timeout"
-      :placeholder="t('workflow.blocks.webhook.timeout.placeholder')"
+      :label="t('workflow.blocks.webhook.timeout.placeholder')"
       :title="t('workflow.blocks.webhook.timeout.title')"
       class="mb-2 w-full"
       type="number"
       @change="updateData({ timeout: +$event })"
     />
-    <ui-tabs v-model="activeTab" fill class="mb-4">
+    <ui-tabs v-model="activeTab" fill>
       <ui-tab value="headers">
         {{ t('workflow.blocks.webhook.tabs.headers') }}
       </ui-tab>
-      <ui-tab value="body">{{ t('workflow.blocks.webhook.tabs.body') }}</ui-tab>
+      <ui-tab v-if="!notHaveBody.includes(data.method)" value="body">
+        {{ t('workflow.blocks.webhook.tabs.body') }}
+      </ui-tab>
+      <ui-tab value="response">
+        {{ t('workflow.blocks.webhook.tabs.response') }}
+      </ui-tab>
     </ui-tabs>
-    <ui-tab-panels :model-value="activeTab">
+    <ui-tab-panels v-model="activeTab">
       <ui-tab-panel
         value="headers"
-        class="grid grid-cols-7 justify-items-center gap-2"
+        class="grid grid-cols-7 justify-items-center mt-4 gap-2"
       >
         <template v-for="(items, index) in headers" :key="index">
           <ui-input
             v-model="items.name"
+            :title="items.name"
             :placeholder="`Header ${index + 1}`"
             type="text"
             class="col-span-3"
           />
           <ui-input
             v-model="items.value"
+            :title="items.value"
             placeholder="Value"
             type="text"
             class="col-span-3"
@@ -73,7 +90,7 @@
           <span> {{ t('workflow.blocks.webhook.buttons.header') }} </span>
         </ui-button>
       </ui-tab-panel>
-      <ui-tab-panel value="body">
+      <ui-tab-panel value="body" class="mt-4">
         <pre
           v-if="!showBodyModal"
           class="rounded-lg text-gray-200 p-4 max-h-80 bg-gray-900 overflow-auto"
@@ -81,6 +98,64 @@
           v-text="data.body"
         />
       </ui-tab-panel>
+      <ui-tab-panel value="response" class="mt-2">
+        <ui-select
+          :model-value="data.responseType"
+          label="Response type"
+          class="w-full"
+          @change="updateData({ responseType: $event })"
+        >
+          <option value="json">JSON</option>
+          <option value="text">Text</option>
+        </ui-select>
+        <ui-input
+          v-if="data.responseType === 'json'"
+          :model-value="data.dataPath"
+          placeholder="path.to.data"
+          label="Data path"
+          class="w-full mt-2"
+          @change="updateData({ dataPath: $event })"
+        />
+        <ui-checkbox
+          :model-value="data.assignVariable"
+          block
+          class="mt-2"
+          @change="updateData({ assignVariable: $event })"
+        >
+          {{ t('workflow.variables.assign') }}
+        </ui-checkbox>
+        <ui-input
+          v-if="data.assignVariable"
+          :model-value="data.variableName"
+          :placeholder="t('workflow.variables.name')"
+          :title="t('workflow.variables.name')"
+          class="mt-2 w-full mb-2"
+          @change="updateData({ variableName: $event })"
+        />
+        <ui-checkbox
+          :model-value="data.saveData"
+          block
+          class="mt-2"
+          @change="updateData({ saveData: $event })"
+        >
+          {{ t('workflow.blocks.get-text.checkbox') }}
+        </ui-checkbox>
+        <ui-select
+          v-if="data.saveData"
+          :model-value="data.dataColumn"
+          placeholder="Select column"
+          class="mt-2 w-full"
+          @change="updateData({ dataColumn: $event })"
+        >
+          <option
+            v-for="column in workflow.data.value.table"
+            :key="column.name"
+            :value="column.name"
+          >
+            {{ column.name }}
+          </option>
+        </ui-select>
+      </ui-tab-panel>
     </ui-tab-panels>
     <ui-modal
       v-model="showBodyModal"
@@ -107,7 +182,7 @@
   </div>
 </template>
 <script setup>
-import { ref, watch, defineAsyncComponent } from 'vue';
+import { ref, watch, defineAsyncComponent, inject } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { contentTypes } from '@/utils/shared';
 
@@ -125,6 +200,10 @@ const emit = defineEmits(['update:data']);
 
 const { t } = useI18n();
 
+const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
+const notHaveBody = ['GET', 'DELETE'];
+
+const workflow = inject('workflow');
 const activeTab = ref('headers');
 const showBodyModal = ref(false);
 const headers = ref(JSON.parse(JSON.stringify(props.data.headers)));
@@ -138,6 +217,13 @@ function removeHeader(index) {
 function addHeader() {
   headers.value.push({ name: '', value: '' });
 }
+function updateMethod(method) {
+  if (notHaveBody.includes(method) && activeTab.value === 'body') {
+    activeTab.value = 'headers';
+  }
+
+  updateData({ method });
+}
 
 watch(
   headers,

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

@@ -3,6 +3,7 @@ import {
   riH1,
   riH2,
   riLinkM,
+  riEarthLine,
   riLock2Line,
   riBaseStationLine,
   riKeyboardLine,
@@ -101,6 +102,7 @@ export const icons = {
   riH1,
   riH2,
   riLinkM,
+  riEarthLine,
   riLock2Line,
   riBaseStationLine,
   riKeyboardLine,

+ 7 - 4
src/locales/en/blocks.json

@@ -355,10 +355,12 @@
         }
       },
       "webhook": {
-        "name": "Webhook",
-        "description": "Webhook allow external service to be notified",
+        "name": "HTTP request",
+        "description": "make an HTTP request",
         "url": "The Post receive URL",
-        "contentType": "Select a content type",
+        "contentType": "Content type",
+        "method": "Request method",
+        "url": "Request URL",
         "buttons": {
           "header": "Add header"
         },
@@ -368,7 +370,8 @@
         },
         "tabs": {
           "headers": "Headers",
-          "body": "Content body"
+          "body": "Body",
+          "response": "Response"
         }
       },
       "loop-data": {

+ 21 - 0
src/models/workflow.js

@@ -2,6 +2,7 @@ import { Model } from '@vuex-orm/core';
 import { nanoid } from 'nanoid';
 import Log from './log';
 import { cleanWorkflowTriggers } from '@/utils/workflow-trigger';
+import { fetchApi } from '@/utils/api';
 
 class Workflow extends Model {
   static entity = 'workflows';
@@ -58,6 +59,26 @@ class Workflow extends Model {
   static async afterDelete({ id }) {
     try {
       await cleanWorkflowTriggers(id);
+
+      try {
+        const hostedWorkflow = this.store().state.hostWorkflows[id];
+
+        if (hostedWorkflow) {
+          const response = await fetchApi(
+            `/me/workflows/host?id=${hostedWorkflow.hostId}`,
+            {
+              method: 'DELETE',
+            }
+          );
+
+          if (response.status !== 200) {
+            throw new Error(response.statusText);
+          }
+        }
+      } catch (error) {
+        console.error(error);
+      }
+      /* delete host workflow */
     } catch (error) {
       console.error(error);
     }

+ 3 - 1
src/utils/reference-data/mustache-replacer.js

@@ -45,13 +45,15 @@ export function keyParser(key, data) {
   if (firstPath === '$last') {
     const lastIndex = data.table.length - 1;
 
-    path = `${lastIndex}.${restPath}`;
+    path = `${lastIndex}.${restPath || ''}`;
   } else if (!restPath) {
     path = `0.${firstPath}`;
   } else if (typeof +firstPath !== 'number' || Number.isNaN(+firstPath)) {
     path = `0.${firstPath}.${restPath}`;
   }
 
+  path = path.replace(/\.$/, '');
+
   return { dataKey: 'table', path };
 }
 

+ 13 - 6
src/utils/shared.js

@@ -549,9 +549,9 @@ export const tasks = {
     },
   },
   webhook: {
-    name: 'Webhook',
-    description: 'Webhook allow external service to be notified',
-    icon: 'webhookIcon',
+    name: 'HTTP Request',
+    description: 'make an HTTP request',
+    icon: 'riEarthLine',
     component: 'BlockBasic',
     editComponent: 'EditWebhook',
     category: 'general',
@@ -563,10 +563,17 @@ export const tasks = {
     data: {
       description: '',
       url: '',
-      contentType: 'json',
-      timeout: 10000,
-      headers: [],
       body: '{}',
+      headers: [],
+      method: 'POST',
+      timeout: 10000,
+      dataPath: '',
+      contentType: 'json',
+      variableName: '',
+      assignVariable: false,
+      saveData: false,
+      dataColumn: '',
+      responseType: 'json',
     },
   },
   'loop-data': {

+ 23 - 18
src/utils/webhookUtil.js

@@ -1,14 +1,11 @@
-import { isObject, parseJSON } from './helper';
+import { isObject, parseJSON, isWhitespace } from './helper';
 
 const renderContent = (content, contentType) => {
-  // 1. render the content
-  // 2. if the content type is json then parse the json
-  // 3. else parse to form data
   const renderedJson = parseJSON(content, new Error('invalid-body'));
 
   if (renderedJson instanceof Error) throw renderedJson;
 
-  if (contentType === 'form') {
+  if (contentType === 'application/x-www-form-urlencoded') {
     return Object.keys(renderedJson)
       .map((key) => {
         const value = isObject(renderedJson[key])
@@ -33,11 +30,11 @@ const filterHeaders = (headers) => {
   return filteredHeaders;
 };
 
-const convertContentType = (contentType) => {
-  return contentType === 'json'
-    ? 'application/json'
-    : 'application/x-www-form-urlencoded';
+const contentTypes = {
+  json: 'application/json',
+  form: 'application/x-www-form-urlencoded',
 };
+const notHaveBody = ['GET', 'DELETE'];
 
 export async function executeWebhook({
   url,
@@ -45,29 +42,37 @@ export async function executeWebhook({
   headers,
   timeout,
   body,
+  method,
 }) {
   const controller = new AbortController();
-  const id = setTimeout(() => {
+  const controllerId = setTimeout(() => {
     controller.abort();
   }, timeout);
 
   try {
     const finalHeaders = filterHeaders(headers);
-    const finalContent = renderContent(body, contentType);
+    const contentTypeHeader = contentTypes[contentType || 'json'];
 
-    await fetch(url, {
-      method: 'POST',
+    const payload = {
+      method: method || 'POST',
       headers: {
-        'Content-Type': convertContentType(contentType),
+        'Content-Type': contentTypeHeader,
         ...finalHeaders,
       },
-      body: finalContent,
       signal: controller.signal,
-    });
+    };
 
-    clearTimeout(id);
+    if (!notHaveBody.includes(method || 'POST') && !isWhitespace(body)) {
+      payload.body = renderContent(body, payload.headers['Content-Type']);
+    }
+
+    const response = await fetch(url, payload);
+
+    clearTimeout(controllerId);
+
+    return response;
   } catch (error) {
-    clearTimeout(id);
+    clearTimeout(controllerId);
     throw error;
   }
 }