Răsfoiți Sursa

Merge branch 'dev' of git://github.com/bxb100/automa into bxb100-dev

Ahmad Kholid 3 ani în urmă
părinte
comite
1fbd271a2a

+ 2 - 0
.gitignore

@@ -22,3 +22,5 @@
 
 # secrets
 secrets.*.js
+
+.idea

+ 1 - 0
package.json

@@ -27,6 +27,7 @@
     "dayjs": "^1.10.7",
     "drawflow": "^0.0.51",
     "mousetrap": "^1.6.5",
+    "mustache": "^4.2.0",
     "nanoid": "3.1.28",
     "object-path": "^0.11.8",
     "papaparse": "^5.3.1",

+ 25 - 0
src/background/workflow-engine/blocks-handler.js

@@ -5,6 +5,7 @@ import { tasks } from '@/utils/shared';
 import dataExporter from '@/utils/data-exporter';
 import compareBlockValue from '@/utils/compare-block-value';
 import errorMessage from './error-message';
+import { executeWebhook } from '@/utils/webhookUtil';
 
 function getBlockConnection(block, index = 1) {
   const blockId = block.outputs[`output_${index}`]?.connections[0]?.node;
@@ -452,3 +453,27 @@ export function repeatTask({ data, id, outputs }) {
     }
   });
 }
+
+export function webhook({ data, outputs }) {
+  return new Promise((resolve, reject) => {
+    if (!data.url) {
+      reject(new Error('URL is empty'));
+      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);
+    }
+  });
+}

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

@@ -0,0 +1,171 @@
+<template>
+  <div class="mb-2 mt-4">
+    <ui-input
+      :model-value="data.url"
+      class="mb-3 w-full"
+      placeholder="https://example.com/postreceive"
+      required
+      title="The Post receive URL"
+      type="url"
+      @change="updateData({ url: $event })"
+    />
+    <ui-select
+      :model-value="data.contentType"
+      placeholder="Select a content type"
+      class="mb-3 w-full"
+      @change="updateData({ contentType: $event })"
+    >
+      <option
+        v-for="type in contentTypes"
+        :key="type.value"
+        :value="type.value"
+      >
+        {{ type.name }}
+      </option>
+    </ui-select>
+    <ui-input
+      :model-value="data.timeout"
+      class="mb-3 w-full"
+      placeholder="Timeout"
+      title="Http request execution timeout(ms)"
+      type="number"
+      @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>
+          </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"
+    />
+    <ui-modal
+      v-model="showContentModalRef"
+      content-class="max-w-3xl"
+      title="Content Body"
+    >
+      <prism-editor
+        v-model="contentRef"
+        :highlight="highlighter('json')"
+        class="py-4"
+        line-numbers
+        style="height: calc(100vh - 18rem)"
+      />
+      <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>
+      </div>
+    </ui-modal>
+  </div>
+</template>
+<script setup>
+import { inject, ref, watch } from 'vue';
+import { PrismEditor } from 'vue-prism-editor';
+import { highlighter } from '@/lib/prism';
+import { contentTypes } from '@/utils/shared';
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const workflow = inject('workflow');
+const emit = defineEmits(['update:data']);
+
+const showOptionsRef = ref(false);
+const contentRef = ref(props.data.content);
+const headerRef = ref(props.data.headers);
+const showContentModalRef = ref(false);
+
+function updateData(value) {
+  emit('update:data', { ...props.data, ...value });
+}
+
+watch(contentRef, (value) => {
+  updateData({ content: value });
+});
+
+function removeHeader(index) {
+  headerRef.value.splice(index, 1);
+}
+
+function addHeader() {
+  headerRef.value.push({ name: '', value: '' });
+}
+
+watch(
+  headerRef,
+  (value) => {
+    updateData({ headers: value });
+  },
+  { deep: true }
+);
+</script>
+<style scoped>
+code {
+  @apply bg-gray-900 text-sm text-white p-1 rounded-md;
+}
+</style>

Fișier diff suprimat deoarece este prea mare
+ 1 - 0
src/lib/v-remixicon.js


Fișier diff suprimat deoarece este prea mare
+ 19 - 0
src/utils/shared.js


+ 79 - 0
src/utils/webhookUtil.js

@@ -0,0 +1,79 @@
+/* eslint-disable no-console */
+import Mustache from 'mustache';
+
+const customTags = ['{%', '%}'];
+const renderContent = (content, workflowData, contentType) => {
+  console.log('renderContent', content, workflowData);
+  // 1. render the content
+  // 2. if the content type is json then parse the json
+  // 3. else parse to form data
+  const renderedJson = JSON.parse(
+    Mustache.render(content, workflowData, {}, customTags)
+  );
+  if (contentType === 'form') {
+    return Object.keys(renderedJson)
+      .map((key) => {
+        return `${key}=${renderedJson[key]}`;
+      })
+      .join('&');
+  }
+  return JSON.stringify(renderedJson);
+};
+
+const filterHeaders = (headers) => {
+  const filteredHeaders = {};
+  headers.forEach((item) => {
+    if (item.name && item.value) {
+      filteredHeaders[item.name] = item.value;
+    }
+  });
+  return filteredHeaders;
+};
+
+const convertContentType = (contentType) => {
+  return contentType === 'json'
+    ? 'application/json'
+    : 'application/x-www-form-urlencoded';
+};
+
+export function executeWebhook({
+  url,
+  contentType,
+  headers,
+  timeout,
+  content,
+  workflowData,
+}) {
+  const finalContent = renderContent(content, workflowData, contentType);
+  const finalHeaders = filterHeaders(headers);
+  console.log(
+    'executeWebhook',
+    url,
+    contentType,
+    finalHeaders,
+    timeout,
+    finalContent,
+    workflowData
+  );
+  const controller = new AbortController();
+  const id = setTimeout(() => {
+    controller.abort();
+  }, 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;
+    });
+}

+ 5 - 0
yarn.lock

@@ -4624,6 +4624,11 @@ multicast-dns@^6.0.1:
     dns-packet "^1.3.1"
     thunky "^1.0.2"
 
+mustache@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
+  integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
+
 nan@^2.12.1:
   version "2.15.0"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff