Browse Source

feat: group and ungroup blocks

Ahmad Kholid 2 years ago
parent
commit
1f90e6177d

+ 2 - 13
src/components/block/BlockGroup.vue

@@ -100,7 +100,7 @@ import { nanoid } from 'nanoid';
 import { useToast } from 'vue-toastification';
 import { Handle, Position } from '@braks/vue-flow';
 import draggable from 'vuedraggable';
-import { tasks } from '@/utils/shared';
+import { tasks, excludeGroupBlocks } from '@/utils/shared';
 import { useComponentId } from '@/composable/componentId';
 import { useEditorBlock } from '@/composable/editorBlock';
 
@@ -128,17 +128,6 @@ const props = defineProps({
 });
 const emit = defineEmits(['update', 'delete', 'edit']);
 
-const excludeBlocks = [
-  'trigger',
-  'repeat-task',
-  'loop-data',
-  'loop-breakpoint',
-  'blocks-group',
-  'conditions',
-  'webhook',
-  'element-exists',
-];
-
 const { t, te } = useI18n();
 const toast = useToast();
 const componentId = useComponentId('blocks-group');
@@ -201,7 +190,7 @@ function handleDrop(event) {
 
   const { id, data, blockId } = droppedBlock;
 
-  if (excludeBlocks.includes(id)) {
+  if (excludeGroupBlocks.includes(id)) {
     toast.error(
       t('workflow.blocks.blocks-group.cantAdd', {
         blockName: t(`workflow.blocks.${id}.name`),

+ 29 - 4
src/components/newtab/workflow/editor/EditorLocalCtxMenu.vue

@@ -29,6 +29,7 @@
 <script setup>
 import { onMounted, reactive, markRaw } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { excludeGroupBlocks } from '@/utils/shared';
 import { getReadableShortcut, getShortcut } from '@/composable/shortcut';
 
 const props = defineProps({
@@ -37,7 +38,7 @@ const props = defineProps({
     default: () => ({}),
   },
 });
-const emit = defineEmits(['copy', 'paste', 'duplicate']);
+const emit = defineEmits(['copy', 'paste', 'duplicate', 'group', 'ungroup']);
 
 const { t } = useI18n();
 const state = reactive({
@@ -72,6 +73,18 @@ const menuItems = {
     event: () => emit('copy', ctxData),
     shortcut: getReadableShortcut('mod+c'),
   },
+  group: {
+    id: 'group',
+    name: t('workflow.editor.group'),
+    icon: 'riFolderZipLine',
+    event: () => emit('group', ctxData),
+  },
+  ungroup: {
+    id: 'ungroup',
+    name: t('workflow.editor.ungroup'),
+    icon: 'riFolderOpenLine',
+    event: () => emit('ungroup', ctxData),
+  },
   duplicate: {
     id: 'duplicate',
     name: t('workflow.editor.duplicate'),
@@ -110,8 +123,19 @@ function clearContextMenu() {
 
 onMounted(() => {
   props.editor.onNodeContextMenu(({ event, node }) => {
-    showCtxMenu(['copy', 'duplicate', 'delete'], event);
-    ctxData = { nodes: [node], edges: [] };
+    const items = ['copy', 'duplicate', 'delete'];
+    if (node.label === 'blocks-group') {
+      items.splice(2, 0, 'ungroup');
+    } else if (!excludeGroupBlocks.includes(node.label)) {
+      items.splice(2, 0, 'group');
+    }
+
+    showCtxMenu(items, event);
+    ctxData = {
+      edges: [],
+      nodes: [node],
+      position: { clientX: event.clientX, clientY: event.clientY },
+    };
   });
   props.editor.onEdgeContextMenu(({ event, edge }) => {
     showCtxMenu(['delete'], event);
@@ -126,10 +150,11 @@ onMounted(() => {
     };
   });
   props.editor.onSelectionContextMenu(({ event }) => {
-    showCtxMenu(['copy', 'duplicate', 'delete'], event);
+    showCtxMenu(['copy', 'duplicate', 'group', 'delete'], event);
     ctxData = {
       nodes: props.editor.getSelectedNodes.value,
       edges: props.editor.getSelectedEdges.value,
+      position: { clientX: event.clientX, clientY: event.clientY },
     };
   });
 });

+ 2 - 0
src/lib/vRemixicon.js

@@ -75,6 +75,7 @@ import {
   riKeyboardLine,
   riFileEditLine,
   riCompass3Line,
+  riFolderOpenLine,
   riComputerLine,
   riFileCopyLine,
   riCalendarLine,
@@ -204,6 +205,7 @@ export const icons = {
   riKeyboardLine,
   riFileEditLine,
   riCompass3Line,
+  riFolderOpenLine,
   riComputerLine,
   riFileCopyLine,
   riCalendarLine,

+ 3 - 1
src/locales/en/newtab.json

@@ -268,7 +268,9 @@
       "resetZoom": "Reset zoom",
       "duplicate": "Duplicate",
       "copy": "Copy",
-      "paste": "Paste"
+      "paste": "Paste",
+      "group": "Group blocks",
+      "ungroup": "Ungroup blocks"
     },
     "settings": {
       "saveLog": "Save workflow log",

+ 70 - 1
src/newtab/pages/workflows/[id].vue

@@ -161,6 +161,8 @@
           <editor-local-ctx-menu
             v-if="editor"
             :editor="editor"
+            @group="groupBlocks"
+            @ungroup="ungroupBlocks"
             @copy="copySelectedElements"
             @paste="pasteCopiedElements"
             @duplicate="duplicateElements"
@@ -233,8 +235,8 @@ import {
   getReadableShortcut,
 } from '@/composable/shortcut';
 import { getWorkflowPermissions } from '@/utils/workflowData';
-import { tasks } from '@/utils/shared';
 import { fetchApi } from '@/utils/api';
+import { tasks, excludeGroupBlocks } from '@/utils/shared';
 import { functions } from '@/utils/referenceData/mustacheReplacer';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { useCommandManager } from '@/composable/commandManager';
@@ -569,6 +571,73 @@ const onEdgesChange = debounce((changes) => {
   // if (command) commandManager.add(command);
 }, 250);
 
+function groupBlocks({ position }) {
+  const nodesToDelete = [];
+  const nodes = editor.value.getSelectedNodes.value;
+  const groupBlocksList = nodes.reduce((acc, node) => {
+    if (excludeGroupBlocks.includes(node.label)) return acc;
+
+    acc.push({
+      id: node.label,
+      itemId: node.id,
+      data: node.data,
+    });
+    nodesToDelete.push(node);
+
+    return acc;
+  }, []);
+
+  editor.value.removeNodes(nodesToDelete);
+
+  const { component, data } = blocks['blocks-group'];
+  editor.value.addNodes([
+    {
+      id: nanoid(),
+      type: component,
+      label: 'blocks-group',
+      data: { ...data, blocks: groupBlocksList },
+      position: editor.value.project({
+        x: position.clientX - 360,
+        y: position.clientY,
+      }),
+    },
+  ]);
+}
+function ungroupBlocks({ nodes }) {
+  const [node] = nodes;
+  if (!node || node.label !== 'blocks-group') return;
+
+  const edges = [];
+  const position = { ...node.position };
+  const copyBlocks = cloneDeep(node.data?.blocks || []);
+  const groupBlocksList = copyBlocks.map((item, index) => {
+    const nextNode = copyBlocks[index + 1];
+    if (nextNode) {
+      edges.push({
+        source: item.itemId,
+        target: nextNode.itemId,
+        sourceHandle: `${item.itemId}-output-1`,
+        targetHandle: `${nextNode.itemId}-input-1`,
+      });
+    }
+
+    item.label = item.id;
+    item.id = item.itemId;
+    item.position = { ...position };
+    item.type = blocks[item.label].component;
+
+    delete item.itemId;
+
+    position.x += 250;
+
+    return item;
+  });
+
+  editor.value.removeNodes(nodes);
+  editor.value.addNodes(groupBlocksList);
+  editor.value.addSelectedNodes(groupBlocksList);
+  editor.value.addEdges(edges);
+}
 function extractAutocopmleteData(label, { data, id }) {
   const autocompleteData = { [id]: {} };
   const getData = (blockName, blockData) => {

+ 12 - 0
src/utils/shared.js

@@ -1399,6 +1399,18 @@ export const elementsHighlightData = {
   },
 };
 
+export const excludeGroupBlocks = [
+  'trigger',
+  'repeat-task',
+  'loop-data',
+  'loop-breakpoint',
+  'blocks-group',
+  'conditions',
+  'webhook',
+  'element-exists',
+  'while-loop',
+];
+
 export const conditionBuilder = {
   valueTypes: [
     {