WorkflowEditor.vue 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. <template>
  2. <vue-flow :id="props.id" :class="{ disabled: options.disabled }">
  3. <Background />
  4. <MiniMap v-if="minimap" :node-class-name="minimapNodeClassName" />
  5. <div
  6. v-if="editorControls"
  7. class="flex items-end absolute p-4 left-0 bottom-0 z-10"
  8. >
  9. <button
  10. v-tooltip.group="t('workflow.editor.resetZoom')"
  11. class="p-2 rounded-lg bg-white dark:bg-gray-800 mr-2"
  12. @click="editor.fitView()"
  13. >
  14. <v-remixicon name="riFullscreenLine" />
  15. </button>
  16. <div class="rounded-lg bg-white dark:bg-gray-800 inline-block">
  17. <button
  18. v-tooltip.group="t('workflow.editor.zoomOut')"
  19. class="p-2 rounded-lg relative z-10"
  20. @click="editor.zoomOut()"
  21. >
  22. <v-remixicon name="riSubtractLine" />
  23. </button>
  24. <hr class="h-6 border-r inline-block" />
  25. <button
  26. v-tooltip.group="t('workflow.editor.zoomIn')"
  27. class="p-2 rounded-lg"
  28. @click="editor.zoomIn()"
  29. >
  30. <v-remixicon name="riAddLine" />
  31. </button>
  32. </div>
  33. <editor-search-blocks :editor="editor" />
  34. </div>
  35. <template v-for="(node, name) in nodeTypes" :key="name" #[name]="nodeProps">
  36. <component
  37. :is="node"
  38. v-bind="nodeProps"
  39. @delete="deleteBlock"
  40. @edit="editBlock(nodeProps, $event)"
  41. @update="updateBlockData(nodeProps.id, $event)"
  42. />
  43. </template>
  44. </vue-flow>
  45. </template>
  46. <script setup>
  47. import { onMounted, onBeforeUnmount } from 'vue';
  48. import { useI18n } from 'vue-i18n';
  49. import {
  50. VueFlow,
  51. Background,
  52. MiniMap,
  53. useVueFlow,
  54. MarkerType,
  55. } from '@braks/vue-flow';
  56. import cloneDeep from 'lodash.clonedeep';
  57. import { useStore } from '@/stores/main';
  58. import { tasks, categories } from '@/utils/shared';
  59. import EditorSearchBlocks from './editor/EditorSearchBlocks.vue';
  60. const props = defineProps({
  61. id: {
  62. type: String,
  63. default: 'editor',
  64. },
  65. data: {
  66. type: Object,
  67. default: () => ({
  68. x: 0,
  69. y: 0,
  70. zoom: 0,
  71. nodes: [],
  72. edges: [],
  73. }),
  74. },
  75. options: {
  76. type: Object,
  77. default: () => ({}),
  78. },
  79. editorControls: {
  80. type: Boolean,
  81. default: true,
  82. },
  83. minimap: {
  84. type: Boolean,
  85. default: true,
  86. },
  87. });
  88. const emit = defineEmits(['edit', 'init', 'update:node', 'delete:node']);
  89. const fallbackBlocks = {
  90. BlockBasic: ['BlockExportData'],
  91. BlockBasicWithFallback: ['BlockWebhook'],
  92. };
  93. const isMac = navigator.appVersion.indexOf('Mac') !== -1;
  94. const blockComponents = require.context('@/components/block', false, /\.vue$/);
  95. const nodeTypes = blockComponents.keys().reduce((acc, key) => {
  96. const name = key.replace(/(.\/)|\.vue$/g, '');
  97. const component = blockComponents(key).default;
  98. if (fallbackBlocks[name]) {
  99. fallbackBlocks[name].forEach((fallbackBlock) => {
  100. acc[`node-${fallbackBlock}`] = component;
  101. });
  102. }
  103. acc[`node-${name}`] = component;
  104. return acc;
  105. }, {});
  106. const getPosition = (position) => (Array.isArray(position) ? position : [0, 0]);
  107. const { t } = useI18n();
  108. const store = useStore();
  109. const editor = useVueFlow({
  110. id: props.id,
  111. minZoom: 0.4,
  112. edgeUpdaterRadius: 20,
  113. deleteKeyCode: 'Delete',
  114. elevateEdgesOnSelect: true,
  115. defaultZoom: props.data?.zoom ?? 0.7,
  116. multiSelectionKeyCode: isMac ? 'Meta' : 'Control',
  117. defaultPosition: getPosition(props.data?.position),
  118. ...props.options,
  119. });
  120. editor.onConnect((params) => {
  121. params.class = `source-${params.sourceHandle} target-${params.targetHandle}`;
  122. /* eslint-disable-next-line */
  123. params = applyEdgeSettings(params);
  124. editor.addEdges([params]);
  125. });
  126. function applyEdgeSettings(edge) {
  127. const settings = store.settings.editor;
  128. if (settings.lineType !== 'default') {
  129. edge.type = settings.lineType;
  130. } else {
  131. delete edge.type;
  132. }
  133. if (settings.arrow) {
  134. edge.markerEnd = MarkerType.ArrowClosed;
  135. } else {
  136. delete edge.markerEnd;
  137. }
  138. return edge;
  139. }
  140. function minimapNodeClassName({ label }) {
  141. const { category } = tasks[label];
  142. const { color } = categories[category];
  143. return color;
  144. }
  145. function updateBlockData(nodeId, data = {}) {
  146. if (props.options.disabled) return;
  147. const node = editor.getNode.value(nodeId);
  148. node.data = { ...node.data, ...data };
  149. emit('update:node', node);
  150. }
  151. function editBlock({ id, label, data }, additionalData = {}) {
  152. if (props.options.disabled) return;
  153. emit('edit', {
  154. id: label,
  155. blockId: id,
  156. data: cloneDeep(data),
  157. ...additionalData,
  158. });
  159. }
  160. function deleteBlock(nodeId) {
  161. if (props.options.disabled) return;
  162. editor.removeNodes([nodeId]);
  163. emit('delete:node', nodeId);
  164. }
  165. function onMousedown(event) {
  166. if (props.options.disabled && event.shiftKey) {
  167. event.stopPropagation();
  168. event.preventDefault();
  169. }
  170. }
  171. function applyFlowData() {
  172. const settings = store.settings.editor;
  173. const edges = (props.data.edges || []).map((edge) => applyEdgeSettings(edge));
  174. if (settings.snapToGrid) {
  175. editor.snapToGrid.value = true;
  176. editor.snapGrid.value = Object.values(settings.snapGrid);
  177. }
  178. editor.setNodes(props.data.nodes || []);
  179. editor.setEdges(edges);
  180. editor.setTransform({
  181. x: props.data.x || 0,
  182. y: props.data.y || 0,
  183. zoom: props.data.zoom || 1,
  184. });
  185. }
  186. onMounted(() => {
  187. applyFlowData();
  188. window.addEventListener('mousedown', onMousedown, true);
  189. emit('init', editor);
  190. });
  191. onBeforeUnmount(() => {
  192. window.removeEventListener('mousedown', onMousedown, true);
  193. });
  194. </script>
  195. <style>
  196. @import '@braks/vue-flow/dist/style.css';
  197. @import '@braks/vue-flow/dist/theme-default.css';
  198. </style>