Browse Source

feat: add upload file block

Ahmad Kholid 3 years ago
parent
commit
241aed16e7

+ 31 - 0
src/background/index.js

@@ -168,6 +168,37 @@ message.on('open:dashboard', async (url) => {
 message.on('get:sender', (_, sender) => {
   return sender;
 });
+message.on('get:file', (path) => {
+  return new Promise((resolve, reject) => {
+    const isFile = /\.(.*)/.test(path);
+
+    if (!isFile) {
+      reject(new Error(`"${path}" is invalid file path.`));
+      return;
+    }
+
+    const fileUrl = path.startsWith('file://') ? path : `file://${path}`;
+
+    const xhr = new XMLHttpRequest();
+    xhr.responseType = 'blob';
+    xhr.onreadystatechange = () => {
+      if (xhr.readyState === XMLHttpRequest.DONE) {
+        if (xhr.status === 0 || xhr.status === 200) {
+          const objUrl = URL.createObjectURL(xhr.response);
+
+          resolve({ path, objUrl, type: xhr.response.type });
+        } else {
+          reject(new Error(xhr.statusText));
+        }
+      }
+    };
+    xhr.onerror = function () {
+      reject(new Error(xhr.statusText));
+    };
+    xhr.open('GET', fileUrl);
+    xhr.send();
+  });
+});
 
 message.on('collection:execute', (collection) => {
   const engine = new CollectionEngine(collection, {

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

@@ -0,0 +1,58 @@
+<template>
+  <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
+    <div class="mt-4 space-y-2">
+      <div
+        v-for="(path, index) in filePaths"
+        :key="index"
+        class="flex items-center group"
+      >
+        <ui-input
+          v-model="filePaths[index]"
+          :placeholder="t('workflow.blocks.upload-file.filePath')"
+          class="mr-2"
+        />
+        <v-remixicon
+          name="riDeleteBin7Line"
+          class="invisible cursor-pointer group-hover:visible"
+          @click="filePaths.splice(index, 1)"
+        />
+      </div>
+    </div>
+    <ui-button variant="accent" class="mt-2" @click="filePaths.push('')">
+      {{ t('workflow.blocks.upload-file.addFile') }}
+    </ui-button>
+  </edit-interaction-base>
+</template>
+<script setup>
+import { useI18n } from 'vue-i18n';
+import { ref, watch } from 'vue';
+import EditInteractionBase from './EditInteractionBase.vue';
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+  hideBase: {
+    type: Boolean,
+    default: false,
+  },
+});
+const emit = defineEmits(['update:data']);
+
+const { t } = useI18n();
+
+const filePaths = ref([...props.data.filePaths]);
+
+function updateData(value) {
+  emit('update:data', { ...props.data, ...value });
+}
+
+watch(
+  filePaths,
+  (paths) => {
+    updateData({ filePaths: paths });
+  },
+  { deep: true }
+);
+</script>

+ 42 - 0
src/content/blocks-handler/handler-upload-file.js

@@ -0,0 +1,42 @@
+import { sendMessage } from '@/utils/message';
+import { handleElement } from '../helper';
+
+function injectFiles(element, files) {
+  const notFileTypeAttr = element.getAttribute('type') !== 'file';
+
+  if (element.tagName !== 'INPUT' || notFileTypeAttr) return;
+
+  element.files = files;
+
+  element.dispatchEvent(new Event('change', { bubbles: true }));
+}
+
+export default async function (block) {
+  const elements = handleElement(block, { returnElement: true });
+
+  if (!elements) throw new Error('element-not-found');
+
+  const getFile = async (path) => {
+    const file = await sendMessage('get:file', path, 'background');
+    const name = file.path.replace(/^.*[\\/]/, '');
+    const blob = await fetch(file.objUrl).then((response) => response.blob());
+
+    URL.revokeObjectURL(file.objUrl);
+
+    return new File([blob], name, { type: file.type });
+  };
+  const filesPromises = await Promise.all(block.data.filePaths.map(getFile));
+  const dataTransfer = filesPromises.reduce((acc, file) => {
+    acc.items.add(file);
+
+    return acc;
+  }, new DataTransfer());
+
+  if (block.data.multiple) {
+    elements.forEach((element) => {
+      injectFiles(element, dataTransfer.files);
+    });
+  } else {
+    injectFiles(elements, dataTransfer.files);
+  }
+}

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

@@ -1,6 +1,7 @@
 import vRemixicon from 'v-remixicon';
 import {
   riHome5Line,
+  riFileUploadLine,
   riLightbulbLine,
   riSideBarLine,
   riSideBarFill,
@@ -82,6 +83,7 @@ import {
 
 export const icons = {
   riHome5Line,
+  riFileUploadLine,
   riLightbulbLine,
   riSideBarLine,
   riSideBarFill,

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

@@ -27,6 +27,12 @@
           "text": "Multiple"
         }
       },
+      "upload-file": {
+        "name": "Upload file",
+        "description": "Upload file into <input type=\"file\"> element",
+        "filePath": "File path",
+        "addFile": "Add file"
+      },
       "browser-event": {
         "name": "Browser event",
         "description": "Execute the next block when the event is triggered",

+ 18 - 0
src/utils/shared.js

@@ -608,6 +608,24 @@ export const tasks = {
       windowType: 'main-window',
     },
   },
+  'upload-file': {
+    name: 'Upload file',
+    description: 'Upload file into <input type="file"> element',
+    icon: 'riFileUploadLine',
+    component: 'BlockBasic',
+    editComponent: 'EditUploadFile',
+    category: 'interaction',
+    inputs: 1,
+    outputs: 1,
+    allowedInputs: true,
+    maxConnection: 1,
+    refDataKeys: ['selector'],
+    data: {
+      findBy: 'cssSelector',
+      selector: '',
+      filePaths: [],
+    },
+  },
 };
 
 export const categories = {