瀏覽代碼

feat: insert file content in insert data block

Ahmad Kholid 2 年之前
父節點
當前提交
863e64dab2

+ 48 - 19
src/background/workflowEngine/blocksHandler/handlerInsertData.js

@@ -1,28 +1,57 @@
+import Papa from 'papaparse';
 import { parseJSON } from '@/utils/helper';
+import getFile from '@/utils/getFile';
 import mustacheReplacer from '@/utils/referenceData/mustacheReplacer';
 
-function insertData({ id, data }, { refData }) {
-  return new Promise((resolve) => {
-    const replacedValueList = {};
-    data.dataList.forEach(({ name, value, type }) => {
-      const replacedValue = mustacheReplacer(value, refData);
-      const realValue = parseJSON(replacedValue.value, replacedValue.value);
+async function insertData({ id, data }, { refData }) {
+  const replacedValueList = {};
 
-      Object.assign(replacedValueList, replacedValue.list);
+  for (const item of data.dataList) {
+    let value = '';
+
+    if (item.isFile) {
+      const replacedPath = mustacheReplacer(item.filePath || '', refData);
+      const path = replacedPath.value;
+      const isJSON = path.endsWith('.json');
+      const isCSV = path.endsWith('.csv');
 
-      if (type === 'table') {
-        this.addDataToColumn(name, realValue);
-      } else {
-        this.setVariable(name, realValue);
+      let result = await getFile(path, {
+        returnValue: true,
+        responseType: isJSON ? 'json' : 'text',
+      });
+
+      if (
+        result &&
+        isCSV &&
+        item.csvAction &&
+        item.csvAction.includes('json')
+      ) {
+        const parsedCSV = Papa.parse(result, {
+          header: item.csvAction.includes('header'),
+        });
+        result = parsedCSV.data || [];
       }
-    });
-
-    resolve({
-      data: '',
-      replacedValue: replacedValueList,
-      nextBlockId: this.getBlockConnections(id),
-    });
-  });
+
+      value = result;
+      Object.assign(replacedValueList, replacedPath.list);
+    } else {
+      const replacedValue = mustacheReplacer(item.value, refData);
+      value = parseJSON(replacedValue.value, replacedValue.value);
+      Object.assign(replacedValueList, replacedValue.list);
+    }
+
+    if (item.type === 'table') {
+      this.addDataToColumn(item.name, value);
+    } else {
+      this.setVariable(item.name, value);
+    }
+  }
+
+  return {
+    data: '',
+    replacedValue: replacedValueList,
+    nextBlockId: this.getBlockConnections(id),
+  };
 }
 
 export default insertData;

+ 202 - 60
src/components/newtab/workflow/edit/EditInsertData.vue

@@ -6,67 +6,151 @@
       class="w-full"
       @change="updateData({ description: $event })"
     />
-    <ul v-show="dataList.length > 0" class="mt-4 data-list">
-      <li
-        v-for="(item, index) in dataList"
-        :key="index"
-        class="mb-4 pb-4 border-b"
+    <ui-button
+      class="w-full mt-4 mb-2"
+      variant="accent"
+      @click="showModal = !showModal"
+    >
+      Insert data
+    </ui-button>
+    <ui-modal
+      v-model="showModal"
+      title="Insert data"
+      padding="p-0"
+      content-class="max-w-2xl insert-data-modal"
+    >
+      <ul
+        v-show="dataList.length > 0"
+        class="mt-4 data-list px-4 pb-4 overflow-auto scroll"
+        style="max-height: calc(100vh - 13rem)"
       >
-        <div class="flex mb-2">
-          <ui-select
-            :model-value="item.type"
-            class="mr-2 flex-shrink-0"
-            @change="changeItemType(index, $event)"
-          >
-            <option value="table">
-              {{ t('workflow.table.title') }}
-            </option>
-            <option value="variable">
-              {{ t('workflow.variables.title') }}
-            </option>
-          </ui-select>
-          <ui-input
-            v-if="item.type === 'variable'"
-            v-model="item.name"
-            :placeholder="t('workflow.variables.name')"
-            :title="t('workflow.variables.name')"
-            class="flex-1"
-          />
-          <ui-select
-            v-else
-            v-model="item.name"
-            :placeholder="t('workflow.table.select')"
-          >
-            <option
-              v-for="column in workflow.columns.value"
-              :key="column.id"
-              :value="column.id"
+        <li
+          v-for="(item, index) in dataList"
+          :key="index"
+          class="mb-4 rounded-lg border"
+        >
+          <div class="p-2 border-b flex items-center">
+            <ui-select
+              :model-value="item.type"
+              class="mr-2 flex-shrink-0"
+              @change="changeItemType(index, $event)"
             >
-              {{ column.name }}
-            </option>
-          </ui-select>
-        </div>
-        <div class="flex items-start">
-          <ui-textarea
-            v-model="item.value"
-            placeholder="value"
-            title="value"
-            class="flex-1 mr-2"
-          />
-          <ui-button icon @click="dataList.splice(index, 1)">
-            <v-remixicon name="riDeleteBin7Line" />
-          </ui-button>
-        </div>
-      </li>
-    </ul>
-    <ui-button class="mt-4" variant="accent" @click="addItem">
-      {{ t('common.add') }}
-    </ui-button>
+              <option value="table">
+                {{ t('workflow.table.title') }}
+              </option>
+              <option value="variable">
+                {{ t('workflow.variables.title') }}
+              </option>
+            </ui-select>
+            <ui-input
+              v-if="item.type === 'variable'"
+              v-model="item.name"
+              :placeholder="t('workflow.variables.name')"
+              :title="t('workflow.variables.name')"
+              class="flex-1"
+            />
+            <ui-select
+              v-else
+              v-model="item.name"
+              :placeholder="t('workflow.table.select')"
+            >
+              <option
+                v-for="column in workflow.columns.value"
+                :key="column.id"
+                :value="column.id"
+              >
+                {{ column.name }}
+              </option>
+            </ui-select>
+            <div class="flex-grow" />
+            <v-remixicon
+              name="riDeleteBin7Line"
+              class="cursor-pointer"
+              @click="removeItem(index)"
+            />
+          </div>
+          <div class="p-2">
+            <div v-if="hasFileAccess && item.isFile" class="flex items-start">
+              <ui-input
+                v-model="item.filePath"
+                placeholder="File absolute path"
+                class="flex-1"
+              />
+            </div>
+            <ui-textarea
+              v-else
+              v-model="item.value"
+              placeholder="value"
+              title="value"
+              class="w-full"
+            />
+            <div class="flex mt-2 items-center">
+              <ui-button
+                v-tooltip="
+                  hasFileAccess
+                    ? 'Import file'
+                    : 'Don\'t have access, click to learn more'
+                "
+                :class="{ 'text-primary': item.isFile }"
+                icon
+                @click="setAsFile(item)"
+              >
+                <v-remixicon name="riFileLine" />
+              </ui-button>
+              <template v-if="hasFileAccess && item.isFile">
+                <ui-button class="ml-2" @click="previewData(index, item)">
+                  Preview data
+                </ui-button>
+                <ui-button
+                  v-if="previewState.itemId === index"
+                  v-tooltip="'Clear preview'"
+                  class="ml-2"
+                  icon
+                  @click="clearPreview"
+                >
+                  <v-remixicon name="riBrush2Line" />
+                </ui-button>
+                <div class="flex-grow" />
+                <ui-select
+                  v-if="item.filePath.endsWith('.csv')"
+                  v-model="item.csvAction"
+                  placeholder="CSV File Action"
+                >
+                  <option value="text">Read as text</option>
+                  <option value="json">Read as JSON</option>
+                  <option value="json-header">Read as JSON with headers</option>
+                </ui-select>
+              </template>
+            </div>
+            <shared-codemirror
+              v-if="previewState.itemId === index"
+              :model-value="previewState.data"
+              readonly
+              hide-lang
+              class="w-full mt-4"
+              lang="json"
+              style="max-height: 500px"
+            />
+          </div>
+        </li>
+        <ui-button class="mt-4 w-24" variant="accent" @click="addItem">
+          {{ t('common.add') }}
+        </ui-button>
+      </ul>
+    </ui-modal>
   </div>
 </template>
 <script setup>
-import { ref, watch, inject } from 'vue';
+import { ref, watch, inject, shallowReactive, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { useToast } from 'vue-toastification';
+import Papa from 'papaparse';
+import browser from 'webextension-polyfill';
+import getFile from '@/utils/getFile';
+
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
 
 const props = defineProps({
   data: {
@@ -77,10 +161,26 @@ const props = defineProps({
 const emit = defineEmits(['update:data']);
 
 const { t } = useI18n();
+const toast = useToast();
 
 const workflow = inject('workflow', {});
+const showModal = ref(true);
+const hasFileAccess = ref(false);
 const dataList = ref(JSON.parse(JSON.stringify(props.data.dataList)));
 
+const previewState = shallowReactive({
+  data: '',
+  itemId: '',
+});
+
+function clearPreview() {
+  previewState.itemId = '';
+  previewState.data = '';
+}
+function removeItem(index) {
+  dataList.value.splice(index, 1);
+  clearPreview();
+}
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
 }
@@ -89,6 +189,9 @@ function addItem() {
     type: 'table',
     name: '',
     value: '',
+    filePath: '',
+    isFile: false,
+    csvAction: 'text',
   });
 }
 function changeItemType(index, type) {
@@ -98,6 +201,47 @@ function changeItemType(index, type) {
     name: '',
   };
 }
+function setAsFile(item) {
+  if (!hasFileAccess.value) {
+    window.open(
+      'https://docs.automa.site/blocks/upload-file.html#requirements'
+    );
+    return;
+  }
+
+  item.isFile = !item.isFile;
+}
+async function previewData(index, item) {
+  try {
+    const path = item.filePath || '';
+    const isJSON = path.endsWith('.json');
+    const isCSV = path.endsWith('.csv');
+
+    let stringify = isJSON;
+    let result = await getFile(path, {
+      returnValue: true,
+      responseType: isJSON ? 'json' : 'text',
+    });
+
+    if (result && isCSV && item.csvAction && item.csvAction.includes('json')) {
+      const parsedCSV = Papa.parse(result, {
+        header: item.csvAction.includes('header'),
+      });
+      result = parsedCSV.data || [];
+      stringify = true;
+    }
+
+    previewState.itemId = index;
+    previewState.data = stringify ? JSON.stringify(result, null, 2) : result;
+  } catch (error) {
+    console.error(error);
+    toast.error(error.message);
+  }
+}
+
+browser.extension.isAllowedFileSchemeAccess().then((value) => {
+  hasFileAccess.value = value;
+});
 
 watch(
   dataList,
@@ -107,10 +251,8 @@ watch(
   { deep: true }
 );
 </script>
-<style scoped>
-.data-list li:last-child {
-  padding-bottom: 0;
-  margin-bottom: 0;
-  border-bottom: 0;
+<style>
+.insert-data-modal .modal-ui__content-header {
+  @apply p-4;
 }
 </style>

+ 4 - 0
src/lib/vRemixicon.js

@@ -21,6 +21,7 @@ import {
   riSortDesc,
   riTimeLine,
   riFlagLine,
+  riFileLine,
   riTeamLine,
   riLinksLine,
   riGroupLine,
@@ -51,6 +52,7 @@ import {
   riEyeOffLine,
   riWindowLine,
   riPencilLine,
+  riBrush2Line,
   riGlobalLine,
   riShieldLine,
   riCursorLine,
@@ -152,6 +154,7 @@ export const icons = {
   riSortDesc,
   riTimeLine,
   riFlagLine,
+  riFileLine,
   riTeamLine,
   riLinksLine,
   riGroupLine,
@@ -182,6 +185,7 @@ export const icons = {
   riEyeOffLine,
   riWindowLine,
   riPencilLine,
+  riBrush2Line,
   riGlobalLine,
   riShieldLine,
   riCursorLine,

+ 22 - 10
src/utils/getFile.js

@@ -1,11 +1,19 @@
-async function downloadFile(url) {
+async function downloadFile(url, options) {
   const response = await fetch(url);
-  const blob = await response.blob();
-  const objUrl = URL.createObjectURL(blob);
+  if (!response.ok) throw new Error(response.statusText);
 
-  return { objUrl, path: url, type: blob.type };
+  const type = options.responseType || 'blob';
+  const result = await response[type]();
+
+  if (options.returnValue) {
+    return result;
+  }
+
+  const objUrl = URL.createObjectURL(result);
+
+  return { objUrl, path: url, type: result.type };
 }
-function getLocalFile(path) {
+function getLocalFile(path, options) {
   return new Promise((resolve, reject) => {
     const isFile = /\.(.*)/.test(path);
 
@@ -17,12 +25,16 @@ function getLocalFile(path) {
     const fileUrl = path?.startsWith('file://') ? path : `file://${path}`;
 
     const xhr = new XMLHttpRequest();
-    xhr.responseType = 'blob';
+    xhr.responseType = options.responseType || 'blob';
     xhr.onreadystatechange = () => {
       if (xhr.readyState === XMLHttpRequest.DONE) {
         if (xhr.status === 0 || xhr.status === 200) {
-          const objUrl = URL.createObjectURL(xhr.response);
+          if (options.returnValue) {
+            resolve(xhr.response);
+            return;
+          }
 
+          const objUrl = URL.createObjectURL(xhr.response);
           resolve({ path, objUrl, type: xhr.response.type });
         } else {
           reject(new Error(xhr.statusText));
@@ -39,8 +51,8 @@ function getLocalFile(path) {
   });
 }
 
-export default function (path) {
-  if (path.startsWith('http')) return downloadFile(path);
+export default function (path, options = {}) {
+  if (path.startsWith('http')) return downloadFile(path, options);
 
-  return getLocalFile(path);
+  return getLocalFile(path, options);
 }