소스 검색

feat: add blocks group block

Ahmad Kholid 3 년 전
부모
커밋
0a213b6852

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

@@ -0,0 +1,46 @@
+import { getBlockConnection } from '../helper';
+
+function blocksGroup({ data, outputs }, { prevBlockData }) {
+  return new Promise((resolve) => {
+    const nextBlockId = getBlockConnection({ outputs });
+
+    if (data.blocks.length === 0) {
+      resolve({
+        nextBlockId,
+        data: prevBlockData,
+      });
+
+      return;
+    }
+
+    const blocks = data.blocks.reduce((acc, block, index) => {
+      let nextBlock = data.blocks[index + 1]?.itemId;
+
+      if (index === data.blocks.length - 1) {
+        nextBlock = nextBlockId;
+      }
+
+      acc[block.itemId] = {
+        ...block,
+        id: block.itemId,
+        name: block.id,
+        outputs: {
+          output_1: {
+            connections: [{ node: nextBlock }],
+          },
+        },
+      };
+
+      return acc;
+    }, {});
+
+    Object.assign(this.blocks, blocks);
+
+    resolve({
+      data: prevBlockData,
+      nextBlockId: data.blocks[0].itemId,
+    });
+  });
+}
+
+export default blocksGroup;

+ 1 - 1
src/background/workflow-engine/engine.js

@@ -183,7 +183,7 @@ class WorkflowEngine {
   }
 
   addLog(detail) {
-    if (this.logs.length >= 1001) return;
+    if (this.logs.length >= 1001 || detail.name === 'blocks-group') return;
 
     this.logs.push(detail);
   }

+ 175 - 0
src/components/block/BlockGroup.vue

@@ -0,0 +1,175 @@
+<template>
+  <div :id="componentId" class="w-64">
+    <div class="p-4">
+      <div class="flex items-center mb-2">
+        <div
+          :class="block.category.color"
+          class="inline-flex items-center text-sm mr-4 p-2 rounded-lg"
+        >
+          <v-remixicon
+            :name="block.details.icon || 'riFolderZipLine'"
+            size="20"
+            class="inline-block mr-2"
+          />
+          <span>{{ t('workflow.blocks.blocks-group.name') }}</span>
+        </div>
+        <div class="flex-grow"></div>
+        <v-remixicon
+          name="riDeleteBin7Line"
+          class="cursor-pointer"
+          @click="editor.removeNodeId(`node-${block.id}`)"
+        />
+      </div>
+      <input
+        v-model="block.data.name"
+        :placeholder="t('workflow.blocks.blocks-group.groupName')"
+        type="text"
+        class="bg-transparent w-full focus:ring-0"
+      />
+    </div>
+    <draggable
+      v-model="block.data.blocks"
+      item-key="itemId"
+      class="px-4 mb-4 overflow-auto scroll text-sm space-y-1 max-h-60"
+      @mousedown.stop
+      @dragover.prevent
+      @drop="handleDrop"
+    >
+      <template #item="{ element, index }">
+        <div
+          class="p-2 rounded-lg bg-input space-x-2 flex items-center group"
+          style="cursor: grab"
+        >
+          <v-remixicon
+            :name="tasks[element.id].icon"
+            size="20"
+            class="flex-shrink-0"
+          />
+          <div class="leading-tight flex-1 overflow-hidden">
+            <p class="text-overflow">
+              {{ t(`workflow.blocks.${element.id}.name`) }}
+            </p>
+            <p
+              :title="element.data.description"
+              class="text-gray-600 dark:text-gray-200 text-overflow"
+            >
+              {{ element.data.description }}
+            </p>
+          </div>
+          <div class="invisible group-hover:visible">
+            <v-remixicon
+              name="riPencilLine"
+              size="20"
+              class="cursor-pointer inline-block mr-2"
+              @click="editBlock(element)"
+            />
+            <v-remixicon
+              name="riDeleteBin7Line"
+              size="20"
+              class="cursor-pointer inline-block"
+              @click="deleteItem(index, element.itemId)"
+            />
+          </div>
+        </div>
+      </template>
+      <template #footer>
+        <div
+          class="
+            p-2
+            rounded-lg
+            text-gray-600
+            dark:text-gray-200
+            border
+            text-center
+            border-dashed
+          "
+        >
+          {{ t('workflow.blocks.blocks-group.dropText') }}
+        </div>
+      </template>
+    </draggable>
+    <input class="hidden trigger" @change="handleDataChange" />
+  </div>
+</template>
+<script setup>
+import { watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { nanoid } from 'nanoid';
+import draggable from 'vuedraggable';
+import emitter from 'tiny-emitter/instance';
+import { tasks } from '@/utils/shared';
+import { useComponentId } from '@/composable/componentId';
+import { useEditorBlock } from '@/composable/editorBlock';
+
+const props = defineProps({
+  editor: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+
+const { t } = useI18n();
+const componentId = useComponentId('blocks-group');
+const block = useEditorBlock(`#${componentId}`, props.editor);
+
+const excludeBlocks = [
+  'trigger',
+  'repeat-task',
+  'export-data',
+  'loop-data',
+  'loop-breakpoint',
+  'blocks-group',
+  'conditions',
+  'element-exists',
+  'delay',
+];
+
+function handleDataChange({ detail }) {
+  if (!detail) return;
+
+  const itemIndex = block.data.blocks.findIndex(
+    ({ itemId }) => itemId === detail.itemId
+  );
+
+  if (itemIndex === -1) return;
+
+  block.data.blocks[itemIndex].data = detail.data;
+}
+function editBlock(payload) {
+  emitter.emit('editor:edit-block', {
+    ...tasks[payload.id],
+    ...payload,
+    isInGroup: true,
+    blockId: block.id,
+  });
+}
+function deleteItem(index, itemId) {
+  emitter.emit('editor:delete-block', { itemId, isInGroup: true });
+  block.data.blocks.splice(index, 1);
+}
+function handleDrop(event) {
+  event.preventDefault();
+  event.stopPropagation();
+
+  const droppedBlock = JSON.parse(event.dataTransfer.getData('block') || null);
+
+  if (!droppedBlock) return;
+
+  const { id, data } = droppedBlock;
+
+  if (excludeBlocks.includes(id)) return;
+
+  block.data.blocks.push({ id, data, itemId: nanoid(5) });
+}
+
+watch(
+  () => block.data,
+  (value, oldValue) => {
+    if (Object.keys(oldValue).length === 0) return;
+
+    props.editor.updateNodeDataFromId(block.id, value);
+    emitter.emit('editor:data-changed', block.id);
+  },
+  { deep: true }
+);
+</script>

+ 3 - 0
src/components/newtab/workflow/WorkflowBuilder.vue

@@ -100,6 +100,9 @@ export default {
 
     function dropHandler({ dataTransfer, clientX, clientY }) {
       const block = JSON.parse(dataTransfer.getData('block') || null);
+
+      if (!block) return;
+
       const isTriggerExists =
         block.id === 'trigger' &&
         editor.value.getNodesFromName('trigger').length !== 0;

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

@@ -43,6 +43,7 @@
   </div>
 </template>
 <script>
+/* eslint-disable vue/require-prop-types */
 export default {
   props: {
     modelModifiers: {

+ 3 - 0
src/composable/editorBlock.js

@@ -7,6 +7,7 @@ export function useEditorBlock(selector, editor) {
     data: {},
     details: {},
     category: {},
+    retrieved: false,
   });
 
   nextTick(() => {
@@ -30,5 +31,7 @@ export function useEditorBlock(selector, editor) {
     }, 200);
   });
 
+  block.retrieved = true;
+
   return block;
 }

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

@@ -1,6 +1,7 @@
 import vRemixicon from 'v-remixicon';
 import {
   riHome5Line,
+  riFolderZipLine,
   riHandHeartLine,
   riFileCopyLine,
   riShieldKeyholeLine,
@@ -77,6 +78,7 @@ import {
 
 export const icons = {
   riHome5Line,
+  riFolderZipLine,
   riHandHeartLine,
   riFileCopyLine,
   riShieldKeyholeLine,

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

@@ -27,6 +27,12 @@
           "text": "Multiple"
         }
       },
+      "blocks-group": {
+        "name": "Blocks group",
+        "groupName": "Group name",
+        "description": "Grouping blocks",
+        "dropText": "Drag & drop a block here"
+      },
       "trigger": {
         "name": "Trigger",
         "description": "Block where the workflow will start executing",

+ 23 - 4
src/newtab/pages/workflows/[id].vue

@@ -150,13 +150,14 @@ import {
   provide,
   onMounted,
   onUnmounted,
+  toRaw,
 } from 'vue';
 import { useStore } from 'vuex';
 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import emitter from 'tiny-emitter/instance';
 import { sendMessage } from '@/utils/message';
-import { debounce } from '@/utils/helper';
+import { debounce, isObject } from '@/utils/helper';
 import { useDialog } from '@/composable/dialog';
 import { exportWorkflow } from '@/utils/workflow-data';
 import Log from '@/models/log';
@@ -228,15 +229,25 @@ const logs = computed(() =>
 );
 
 const updateBlockData = debounce((data) => {
+  let payload = data;
+
   state.blockData.data = data;
   state.isDataChanged = true;
-  editor.value.updateNodeDataFromId(state.blockData.blockId, data);
+
+  if (state.blockData.isInGroup) {
+    payload = { itemId: state.blockData.itemId, data };
+  } else {
+    editor.value.updateNodeDataFromId(state.blockData.blockId, data);
+  }
 
   const inputEl = document.querySelector(
     `#node-${state.blockData.blockId} input.trigger`
   );
 
-  if (inputEl) inputEl.dispatchEvent(new Event('change'));
+  if (inputEl)
+    inputEl.dispatchEvent(
+      new CustomEvent('change', { detail: toRaw(payload) })
+    );
 }, 250);
 function deleteLog(logId) {
   Log.delete(logId).then(() => {
@@ -244,7 +255,13 @@ function deleteLog(logId) {
   });
 }
 function deleteBlock(id) {
-  if (state.isEditBlock && state.blockData.blockId === id) {
+  if (!state.isEditBlock) return;
+
+  const isGroupBlock =
+    isObject(id) && id.isInGroup && id.itemId === state.blockData.itemId;
+  const isEditedBlock = state.blockData.blockId === id;
+
+  if (isEditedBlock || isGroupBlock) {
     state.isEditBlock = false;
     state.blockData = {};
   }
@@ -358,11 +375,13 @@ onMounted(() => {
   };
 
   emitter.on('editor:edit-block', editBlock);
+  emitter.on('editor:delete-block', deleteBlock);
   emitter.on('editor:data-changed', handleEditorDataChanged);
 });
 onUnmounted(() => {
   window.onbeforeunload = null;
   emitter.off('editor:edit-block', editBlock);
+  emitter.off('editor:delete-block', deleteBlock);
   emitter.off('editor:data-changed', handleEditorDataChanged);
 });
 </script>

+ 0 - 1
src/utils/reference-data.js

@@ -30,7 +30,6 @@ export function parseKey(key) {
   return { dataKey: 'dataColumns', path: dataPath };
 }
 export function replaceMustacheHandler(match, data) {
-  console.log(match, data);
   const key = match.slice(2, -2).replace(/\s/g, '');
 
   if (!key) return '';

+ 17 - 1
src/utils/shared.js

@@ -1,4 +1,4 @@
-/* to-do screenshot, looping, cookies, assets, tab loaded, opened tab, and run workflow block? */
+/* to-do execute multiple blocks simultaneously, keyboard shortcut */
 import { nanoid } from 'nanoid';
 
 export const tasks = {
@@ -501,6 +501,22 @@ export const tasks = {
       loopId: '',
     },
   },
+  'blocks-group': {
+    name: 'Blocks group',
+    description: 'Grouping blocks',
+    icon: 'riFolderZipLine',
+    component: 'BlockGroup',
+    category: 'general',
+    disableEdit: true,
+    inputs: 1,
+    outputs: 1,
+    allowedInputs: true,
+    maxConnection: 1,
+    data: {
+      name: '',
+      blocks: [],
+    },
+  },
   'switch-to': {
     name: 'Switch frame',
     description: 'Switch between main window and iframe',

+ 1 - 0
utils/build-zip.js

@@ -1,3 +1,4 @@
+/* eslint-disable no-console */
 const fs = require('fs');
 const path = require('path');
 const archiver = require('archiver');