WorkflowBuilder.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  1. <template>
  2. <div
  3. v-bind="{ arrow: $store.state.settings.editor.arrow }"
  4. id="drawflow"
  5. class="parent-drawflow relative"
  6. @drop="dropHandler"
  7. @dragover.prevent="handleDragOver"
  8. >
  9. <div
  10. class="flex items-end absolute w-full p-4 left-0 bottom-0 justify-between z-10"
  11. >
  12. <div id="zoom">
  13. <button
  14. v-tooltip.group="t('workflow.editor.resetZoom')"
  15. class="p-2 rounded-lg bg-white dark:bg-gray-800 mr-2"
  16. @click="editor.zoom_reset()"
  17. >
  18. <v-remixicon name="riFullscreenLine" />
  19. </button>
  20. <div class="rounded-lg bg-white dark:bg-gray-800 inline-block">
  21. <button
  22. v-tooltip.group="t('workflow.editor.zoomOut')"
  23. class="p-2 rounded-lg relative z-10"
  24. @click="editor.zoom_out()"
  25. >
  26. <v-remixicon name="riSubtractLine" />
  27. </button>
  28. <hr class="h-6 border-r inline-block" />
  29. <button
  30. v-tooltip.group="t('workflow.editor.zoomIn')"
  31. class="p-2 rounded-lg"
  32. @click="editor.zoom_in()"
  33. >
  34. <v-remixicon name="riAddLine" />
  35. </button>
  36. </div>
  37. <!-- <workflow-builder-search-blocks :editor="editor" /> -->
  38. </div>
  39. <slot v-bind="{ editor }"></slot>
  40. </div>
  41. <ui-popover
  42. v-model="contextMenu.show"
  43. :options="contextMenu.position"
  44. padding="p-3"
  45. @close="clearContextMenu"
  46. >
  47. <ui-list class="space-y-1 w-52">
  48. <ui-list-item
  49. v-for="item in contextMenu.items"
  50. :key="item.id"
  51. v-close-popover
  52. class="cursor-pointer justify-between"
  53. @click="contextMenuHandler[item.event]"
  54. >
  55. <span>
  56. {{ item.name }}
  57. </span>
  58. <span
  59. v-if="item.shortcut"
  60. class="text-sm capitalize text-gray-600 dark:text-gray-200"
  61. >
  62. {{ item.shortcut }}
  63. </span>
  64. </ui-list-item>
  65. </ui-list>
  66. </ui-popover>
  67. </div>
  68. </template>
  69. <script>
  70. /* eslint-disable camelcase */
  71. import {
  72. onMounted,
  73. shallowRef,
  74. reactive,
  75. getCurrentInstance,
  76. watch,
  77. onBeforeUnmount,
  78. } from 'vue';
  79. import { useStore } from 'vuex';
  80. import { useRoute } from 'vue-router';
  81. import { useI18n } from 'vue-i18n';
  82. import { compare } from 'compare-versions';
  83. import defu from 'defu';
  84. import SelectionArea from '@viselect/vanilla';
  85. import browser from 'webextension-polyfill';
  86. import emitter from '@/lib/mitt';
  87. import {
  88. useShortcut,
  89. getShortcut,
  90. getReadableShortcut,
  91. } from '@/composable/shortcut';
  92. import { tasks, excludeOnError } from '@/utils/shared';
  93. import { parseJSON } from '@/utils/helper';
  94. import { useGroupTooltip } from '@/composable/groupTooltip';
  95. import drawflow from '@/lib/drawflow';
  96. // import WorkflowBuilderSearchBlocks from './WorkflowBuilderSearchBlocks.vue';
  97. export default {
  98. // components: { WorkflowBuilderSearchBlocks },
  99. props: {
  100. data: {
  101. type: [Object, String],
  102. default: null,
  103. },
  104. isShared: {
  105. type: Boolean,
  106. default: false,
  107. },
  108. version: {
  109. type: [String, Boolean],
  110. default: '',
  111. },
  112. mode: {
  113. type: String,
  114. default: 'edit',
  115. },
  116. },
  117. emits: ['load', 'loaded', 'deleteBlock', 'update', 'save'],
  118. setup(props, { emit }) {
  119. useGroupTooltip();
  120. const { t } = useI18n();
  121. const route = useRoute();
  122. const store = useStore();
  123. const contextMenuItems = {
  124. common: [
  125. {
  126. id: 'paste',
  127. name: t('workflow.editor.paste'),
  128. icon: 'riFileCopyLine',
  129. event: 'pasteBlocks',
  130. shortcut: getReadableShortcut('mod+v'),
  131. },
  132. ],
  133. block: [
  134. {
  135. id: 'copy',
  136. name: t('workflow.editor.copy'),
  137. icon: 'riFileCopyLine',
  138. event: 'copyBlocks',
  139. shortcut: getReadableShortcut('mod+c'),
  140. },
  141. {
  142. id: 'duplicate',
  143. name: t('workflow.editor.duplicate'),
  144. icon: 'riFileCopyLine',
  145. event: 'duplicateBlock',
  146. shortcut: getShortcut('editor:duplicate-block').readable,
  147. },
  148. {
  149. id: 'delete',
  150. name: t('common.delete'),
  151. icon: 'riDeleteBin7Line',
  152. event: 'deleteBlock',
  153. shortcut: 'Del',
  154. },
  155. ],
  156. };
  157. let activeNode = null;
  158. let hasDragged = false;
  159. let isDragging = false;
  160. let selectedElements = [];
  161. const selection = shallowRef(null);
  162. const editor = shallowRef(null);
  163. const contextMenu = reactive({
  164. items: [],
  165. data: null,
  166. show: false,
  167. position: {},
  168. });
  169. const workflowId = route.params.id;
  170. const prevSelectedEl = {
  171. output: null,
  172. connection: null,
  173. nodeContent: null,
  174. };
  175. const isOutputEl = (el) => el.classList.contains('output');
  176. const isConnectionEl = (el) =>
  177. el.matches('path.main-path') ||
  178. el.parentElement.classList.contains('connection');
  179. function toggleHoverClass({ target, name, active, classes }) {
  180. const prev = prevSelectedEl[name];
  181. if (active) {
  182. if (prev === target) return;
  183. target.classList.toggle(classes, true);
  184. } else if (prev) {
  185. prev.classList.toggle(classes, false);
  186. }
  187. prevSelectedEl[name] = target;
  188. }
  189. function handleDragOver({ target }) {
  190. toggleHoverClass({
  191. target,
  192. name: 'connection',
  193. classes: 'selected',
  194. active: isConnectionEl(target),
  195. });
  196. toggleHoverClass({
  197. target,
  198. name: 'output',
  199. classes: 'ring-4',
  200. active: isOutputEl(target),
  201. });
  202. const nodeContent = target.closest(
  203. '.drawflow-node:not(.blocks-group) .drawflow_content_node'
  204. );
  205. toggleHoverClass({
  206. classes: 'ring-4',
  207. target: nodeContent,
  208. name: 'nodeContent',
  209. active: nodeContent,
  210. });
  211. }
  212. function getRelativePosToEditor(clientX, clientY) {
  213. const { x, y } = editor.value.precanvas.getBoundingClientRect();
  214. const { clientWidth, clientHeight } = editor.value.precanvas;
  215. const { zoom } = editor.value;
  216. const xPosition =
  217. clientX * (clientWidth / (clientWidth * zoom)) -
  218. x * (clientWidth / (clientWidth * zoom));
  219. const yPosition =
  220. clientY * (clientHeight / (clientHeight * zoom)) -
  221. y * (clientHeight / (clientHeight * zoom));
  222. return { xPosition, yPosition };
  223. }
  224. function dropHandler({ dataTransfer, clientX, clientY, target }) {
  225. const block = JSON.parse(dataTransfer.getData('block') || null);
  226. if (!block) return;
  227. const highlightedEls = document.querySelectorAll(
  228. '.drawflow_content_node.ring-4'
  229. );
  230. highlightedEls.forEach((el) => {
  231. el.classList.remove('ring-4');
  232. });
  233. const isTriggerExists =
  234. block.id === 'trigger' &&
  235. editor.value.getNodesFromName('trigger').length !== 0;
  236. if (isTriggerExists) return;
  237. if (target.closest('.drawflow_content_node')) {
  238. const targetNodeId = target
  239. .closest('.drawflow-node')
  240. .id.replace(/node-/, '');
  241. const targetNode = editor.value.getNodeFromId(targetNodeId);
  242. editor.value.removeNodeId(`node-${targetNodeId}`);
  243. if (targetNode.name === 'blocks-group') return;
  244. let targetBlock = block;
  245. if (block.fromBlockBasic) {
  246. targetBlock = { ...tasks[block.id], id: block.id };
  247. }
  248. const onErrorEnabled =
  249. targetNode.data?.onError?.enable &&
  250. !excludeOnError.includes(targetBlock.id);
  251. const newNodeData = onErrorEnabled
  252. ? { ...targetBlock.data, onError: targetNode.data.onError }
  253. : targetBlock.data;
  254. const newNodeId = editor.value.addNode(
  255. targetBlock.id,
  256. targetBlock.inputs,
  257. targetBlock.outputs,
  258. targetNode.pos_x,
  259. targetNode.pos_y,
  260. targetBlock.id,
  261. newNodeData,
  262. targetBlock.component,
  263. 'vue'
  264. );
  265. if (onErrorEnabled && targetNode.data.onError.toDo === 'fallback') {
  266. editor.value.addNodeOutput(newNodeId);
  267. }
  268. const duplicateConnections = (nodeIO, type) => {
  269. if (block[type] === 0) return;
  270. Object.keys(nodeIO).forEach((name) => {
  271. const { connections } = nodeIO[name];
  272. connections.forEach(({ node, input, output }) => {
  273. if (node === targetNodeId) return;
  274. if (type === 'inputs') {
  275. editor.value.addConnection(node, newNodeId, input, name);
  276. } else if (type === 'outputs') {
  277. editor.value.addConnection(newNodeId, node, name, output);
  278. }
  279. });
  280. });
  281. };
  282. duplicateConnections(targetNode.inputs, 'inputs');
  283. duplicateConnections(targetNode.outputs, 'outputs');
  284. emitter.emit('editor:data-changed');
  285. return;
  286. }
  287. if (block.fromBlockBasic) return;
  288. const { xPosition, yPosition } = getRelativePosToEditor(clientX, clientY);
  289. const blockId = editor.value.addNode(
  290. block.id,
  291. block.inputs,
  292. block.outputs,
  293. xPosition + 25,
  294. yPosition - 25,
  295. block.id,
  296. block.data,
  297. block.component,
  298. 'vue'
  299. );
  300. if (block.fromGroup) {
  301. const blockEl = document.getElementById(`node-${blockId}`);
  302. blockEl.setAttribute('group-item-id', block.itemId);
  303. }
  304. if (isConnectionEl(target)) {
  305. target.classList.remove('selected');
  306. const classes = target.parentElement.classList.toString();
  307. const result = {};
  308. const items = [
  309. { str: 'node_in_', key: 'inputId' },
  310. { str: 'input_', key: 'inputClass' },
  311. { str: 'node_out_', key: 'outputId' },
  312. { str: 'output_', key: 'outputClass' },
  313. ];
  314. items.forEach(({ key, str }) => {
  315. result[key] = classes
  316. .match(new RegExp(`${str}[^\\s]*`))[0]
  317. ?.replace(/node_in_node-|node_out_node-/, '');
  318. });
  319. try {
  320. editor.value.removeSingleConnection(
  321. result.outputId,
  322. result.inputId,
  323. result.outputClass,
  324. result.inputClass
  325. );
  326. editor.value.addConnection(
  327. result.outputId,
  328. blockId,
  329. result.outputClass,
  330. 'input_1'
  331. );
  332. editor.value.addConnection(
  333. blockId,
  334. result.inputId,
  335. 'output_1',
  336. result.inputClass
  337. );
  338. } catch (error) {
  339. console.error(error);
  340. }
  341. } else if (isOutputEl(target)) {
  342. prevSelectedEl.output?.classList.remove('ring-4');
  343. const targetNodeId = target
  344. .closest('.drawflow-node')
  345. .id.replace(/node-/, '');
  346. const outputClass = target.classList[1];
  347. editor.value.addConnection(
  348. targetNodeId,
  349. blockId,
  350. outputClass,
  351. 'input_1'
  352. );
  353. }
  354. emitter.emit('editor:data-changed');
  355. }
  356. function isInputAllowed(allowedInputs, input) {
  357. if (typeof allowedInputs === 'boolean') return allowedInputs;
  358. return allowedInputs.some((item) => {
  359. if (item.startsWith('#')) {
  360. return tasks[input].category === item.substr(1);
  361. }
  362. return item === input;
  363. });
  364. }
  365. function deleteBlock() {
  366. editor.value.removeNodeId(contextMenu.data);
  367. }
  368. function clearSelectedElements() {
  369. selection.value.clearSelection();
  370. selectedElements.forEach(({ el }) => {
  371. if (!el) return;
  372. el.classList.remove('selected-list');
  373. });
  374. selectedElements = [];
  375. activeNode = null;
  376. }
  377. function duplicateBlock(nodeId, isPaste = false) {
  378. let initialPos = null;
  379. const nodes = new Map();
  380. const addNode = (id) => {
  381. const node = editor.value.getNodeFromId(id);
  382. if (node.name === 'trigger') return;
  383. nodes.set(node.id, node);
  384. };
  385. if (isPaste) {
  386. store.state.copiedNodes.forEach((node) => {
  387. nodes.set(node.id, node);
  388. });
  389. const pos = contextMenu?.position?.getReferenceClientRect?.() ?? null;
  390. if (pos) {
  391. const { xPosition, yPosition } = getRelativePosToEditor(
  392. pos.left,
  393. pos.top
  394. );
  395. initialPos = { x: xPosition, y: yPosition };
  396. }
  397. } else {
  398. if (nodeId) addNode(nodeId);
  399. else if (activeNode) addNode(activeNode.id);
  400. selectedElements.forEach((node) => {
  401. if (activeNode?.id === node.id || nodeId === node.id) return;
  402. addNode(node.id);
  403. });
  404. }
  405. clearSelectedElements();
  406. const nodesOutputs = [];
  407. let firstNodePos = null;
  408. let index = 0;
  409. nodes.forEach((node) => {
  410. const { outputs, inputs } = tasks[node.name];
  411. const inputsLen = Object.keys(node.inputs).length;
  412. const outputsLen = Object.keys(node.outputs).length;
  413. const blockInputs = inputsLen || inputs;
  414. const blockOutputs = outputsLen || outputs;
  415. let nodePosX = node.pos_x;
  416. let nodePosY = node.pos_y;
  417. if (initialPos && index === 0) {
  418. firstNodePos = { x: nodePosX, y: nodePosY };
  419. nodePosX = initialPos.x;
  420. nodePosY = initialPos.y;
  421. } else if (firstNodePos) {
  422. const xDistance = nodePosX - firstNodePos.x;
  423. const yDistance = nodePosY - firstNodePos.y;
  424. nodePosX = initialPos.x + xDistance;
  425. nodePosY = initialPos.y + yDistance;
  426. }
  427. const newNodeId = editor.value.addNode(
  428. node.name,
  429. blockInputs,
  430. blockOutputs,
  431. nodePosX + 25,
  432. nodePosY + 70,
  433. node.name,
  434. node.data,
  435. node.html,
  436. 'vue'
  437. );
  438. nodes.set(node.id, { ...nodes.get(node.id), newId: newNodeId });
  439. const nodeElement = document.querySelector(`#node-${newNodeId}`);
  440. nodeElement.classList.add('selected-list');
  441. selectedElements.push({
  442. id: newNodeId,
  443. el: nodeElement,
  444. posY: parseInt(nodeElement.style.top, 10),
  445. posX: parseInt(nodeElement.style.left, 10),
  446. });
  447. emitter.emit('editor:data-changed');
  448. if (outputsLen > 0) {
  449. nodesOutputs.push({ id: newNodeId, outputs: node.outputs });
  450. }
  451. index += 1;
  452. });
  453. if (nodesOutputs.length < 1) return;
  454. nodesOutputs.forEach(({ id, outputs }) => {
  455. Object.keys(outputs).forEach((key) => {
  456. outputs[key].connections.forEach((connection) => {
  457. const node = nodes.get(connection.node);
  458. if (!node) return;
  459. editor.value.addConnection(id, node.newId, key, 'input_1');
  460. });
  461. });
  462. });
  463. }
  464. function checkWorkflowData() {
  465. if (!editor.value) return;
  466. editor.value.editor_mode = props.isShared ? 'fixed' : 'edit';
  467. editor.value.container.classList.toggle('is-shared', props.isShared);
  468. }
  469. function saveEditorState() {
  470. const editorStates =
  471. parseJSON(localStorage.getItem('editor-states'), {}) || {};
  472. editorStates[workflowId] = {
  473. zoom: editor.value.zoom,
  474. canvas_x: editor.value.canvas_x,
  475. canvas_y: editor.value.canvas_y,
  476. };
  477. localStorage.setItem('editor-states', JSON.stringify(editorStates));
  478. }
  479. function initSelectArea() {
  480. selection.value = new SelectionArea({
  481. container: '#drawflow',
  482. startareas: ['#drawflow'],
  483. boundaries: ['#drawflow'],
  484. selectables: ['.drawflow-node'],
  485. features: {
  486. singleTap: {
  487. allow: false,
  488. },
  489. },
  490. });
  491. selection.value.on('beforestart', ({ event }) => {
  492. if (!event.ctrlKey && !event.metaKey) return false;
  493. editor.value.editor_mode = 'fixed';
  494. editor.value.editor_selected = false;
  495. return true;
  496. });
  497. selection.value.on('move', () => {
  498. hasDragged = true;
  499. });
  500. selection.value.on('stop', (event) => {
  501. event.store.selected.forEach((el) => {
  502. const isExists = selectedElements.some((item) =>
  503. item.el.isEqualNode(el)
  504. );
  505. if (isExists) return;
  506. el.classList.toggle('selected-list', true);
  507. selectedElements.push({
  508. el,
  509. id: el.id.slice(5),
  510. posY: parseInt(el.style.top, 10),
  511. posX: parseInt(el.style.left, 10),
  512. });
  513. });
  514. setTimeout(() => {
  515. hasDragged = false;
  516. }, 500);
  517. });
  518. }
  519. function onMouseup({ target }) {
  520. editor.value.editor_mode = 'edit';
  521. const isNodeEl = target.closest('.drawflow-node');
  522. if (!isNodeEl) return;
  523. const getPosition = (el) => {
  524. return {
  525. posY: parseInt(el.style.top, 10),
  526. posX: parseInt(el.style.left, 10),
  527. };
  528. };
  529. selectedElements.forEach(({ el }, index) => {
  530. Object.assign(selectedElements[index], getPosition(el));
  531. });
  532. if (activeNode) Object.assign(activeNode, getPosition(activeNode.el));
  533. isDragging = false;
  534. }
  535. function onMousedown({ target }) {
  536. const nodeEl = target.closest('.drawflow-node');
  537. if (!nodeEl) return;
  538. if (nodeEl.classList.contains('selected-list')) {
  539. activeNode = {
  540. el: nodeEl,
  541. id: nodeEl.id.slice(5),
  542. posY: parseInt(nodeEl.style.top, 10),
  543. posX: parseInt(nodeEl.style.left, 10),
  544. };
  545. }
  546. isDragging = true;
  547. }
  548. function onClick({ ctrlKey, metaKey, target }) {
  549. const nodeEl = target.closest('.drawflow-node');
  550. if (!nodeEl) {
  551. if (!hasDragged) clearSelectedElements();
  552. return;
  553. }
  554. const nodeProperties = {
  555. el: nodeEl,
  556. id: nodeEl.id.slice(5),
  557. posY: parseInt(nodeEl.style.top, 10),
  558. posX: parseInt(nodeEl.style.left, 10),
  559. };
  560. if (!ctrlKey && !metaKey && !hasDragged) {
  561. clearSelectedElements();
  562. activeNode = nodeProperties;
  563. nodeEl.classList.add('selected-list');
  564. selectedElements = [nodeProperties];
  565. hasDragged = false;
  566. return;
  567. }
  568. hasDragged = false;
  569. if (!ctrlKey && !metaKey) return;
  570. const nodeIndex = selectedElements.findIndex(({ el }) =>
  571. nodeEl.isEqualNode(el)
  572. );
  573. if (nodeIndex !== -1) {
  574. setTimeout(() => {
  575. nodeEl.classList.remove('selected-list', 'selected');
  576. }, 400);
  577. selectedElements.splice(nodeIndex, 1);
  578. } else {
  579. nodeEl.classList.add('selected-list');
  580. selectedElements.push(nodeProperties);
  581. }
  582. }
  583. function clearContextMenu() {
  584. Object.assign(contextMenu, {
  585. items: [],
  586. data: null,
  587. show: false,
  588. position: {},
  589. });
  590. }
  591. function copyBlocks() {
  592. let nodes = selectedElements;
  593. if (nodes.length === 0) {
  594. const selectedEl = document.querySelector('.drawflow-node.selected');
  595. if (selectedEl) {
  596. nodes.push({ id: selectedEl.id.substr(5) });
  597. }
  598. }
  599. nodes = nodes.map((node) => editor.value.getNodeFromId(node.id));
  600. store.commit('updateState', {
  601. key: 'copiedNodes',
  602. value: nodes,
  603. });
  604. }
  605. function onKeyup({ key, target, ctrlKey, metaKey }) {
  606. if (ctrlKey || metaKey) {
  607. if (key === 'c') {
  608. copyBlocks();
  609. } else if (key === 'v') {
  610. duplicateBlock(null, true);
  611. }
  612. }
  613. const isAnInput =
  614. ['INPUT', 'TEXTAREA'].includes(target.tagName) ||
  615. target.isContentEditable;
  616. if (key !== 'Delete' || isAnInput) return;
  617. selectedElements.forEach(({ id }) => {
  618. const nodeId = `node-${id}`;
  619. const isNodeExists = document.querySelector(`#${nodeId}`);
  620. if (!isNodeExists) return;
  621. editor.value.removeNodeId(nodeId);
  622. });
  623. selectedElements = [];
  624. activeNode = null;
  625. }
  626. useShortcut('editor:duplicate-block', () => {
  627. if (!activeNode && selectedElements.length <= 0) return;
  628. duplicateBlock();
  629. });
  630. watch(() => props.isShared, checkWorkflowData);
  631. onMounted(() => {
  632. const context = getCurrentInstance().appContext.app._context;
  633. const element = document.querySelector('#drawflow');
  634. element.addEventListener('mousedown', onMousedown);
  635. element.addEventListener('mouseup', onMouseup);
  636. element.addEventListener('click', onClick);
  637. element.addEventListener('keyup', onKeyup);
  638. editor.value = drawflow(element, {
  639. context,
  640. options: {
  641. reroute: true,
  642. ...store.state.settings.editor,
  643. },
  644. });
  645. editor.value.start();
  646. emit('load', editor.value);
  647. if (props.data) {
  648. let data =
  649. typeof props.data === 'string'
  650. ? parseJSON(props.data, null)
  651. : props.data;
  652. if (!data || !data?.drawflow?.Home) return;
  653. const currentExtVersion = browser.runtime.getManifest().version;
  654. const isOldWorkflow = compare(
  655. currentExtVersion,
  656. props.version || '0.0.0',
  657. '>'
  658. );
  659. if (isOldWorkflow && typeof props.version !== 'boolean') {
  660. const newDrawflowData = Object.entries(
  661. data.drawflow.Home.data
  662. ).reduce((obj, [key, value]) => {
  663. obj[key] = {
  664. ...value,
  665. html: tasks[value.name].component,
  666. data: defu({}, value.data, tasks[value.name].data),
  667. };
  668. return obj;
  669. }, {});
  670. data = {
  671. drawflow: { Home: { data: newDrawflowData } },
  672. };
  673. emit('update', { version: currentExtVersion });
  674. }
  675. editor.value.import(data);
  676. if (isOldWorkflow) {
  677. setTimeout(() => {
  678. emit('save');
  679. }, 200);
  680. }
  681. } else if (!props.isShared) {
  682. editor.value.addNode(
  683. 'trigger',
  684. 0,
  685. 1,
  686. 50,
  687. 300,
  688. 'trigger',
  689. tasks.trigger.data,
  690. 'BlockBasic',
  691. 'vue'
  692. );
  693. }
  694. editor.value.on('mouseMove', () => {
  695. if (!activeNode || !isDragging) return;
  696. const xDistance =
  697. parseInt(activeNode.el.style.left, 10) - activeNode.posX;
  698. const yDistance =
  699. parseInt(activeNode.el.style.top, 10) - activeNode.posY;
  700. selectedElements.forEach(({ el, posX, posY }) => {
  701. if (el.isEqualNode(activeNode.el)) return;
  702. const nodeId = el.id.slice(5);
  703. const node = editor.value.drawflow.drawflow.Home.data[nodeId];
  704. const newPosX = posX + xDistance;
  705. const newPosY = posY + yDistance;
  706. node.pos_x = newPosX;
  707. node.pos_y = newPosY;
  708. el.style.top = `${newPosY}px`;
  709. el.style.left = `${newPosX}px`;
  710. editor.value.updateConnectionNodes(el.id);
  711. });
  712. hasDragged = true;
  713. });
  714. editor.value.on('nodeRemoved', (id) => {
  715. emit('deleteBlock', id);
  716. });
  717. editor.value.on(
  718. 'connectionCreated',
  719. ({ output_id, input_id, output_class, input_class }) => {
  720. const { name: inputName } = editor.value.getNodeFromId(input_id);
  721. const { allowedInputs } = tasks[inputName];
  722. const isAllowed = isInputAllowed(allowedInputs, inputName);
  723. if (!isAllowed) {
  724. editor.value.removeSingleConnection(
  725. output_id,
  726. input_id,
  727. output_class,
  728. input_class
  729. );
  730. }
  731. emitter.emit('editor:data-changed');
  732. }
  733. );
  734. editor.value.on('connectionRemoved', () => {
  735. emitter.emit('editor:data-changed');
  736. });
  737. editor.value.on('export', saveEditorState);
  738. editor.value.on('contextmenu', ({ clientY, clientX, target }) => {
  739. if (target.tagName === 'path' && target.classList.contains('main-path'))
  740. return;
  741. const isBlock = target.closest('.drawflow .drawflow-node');
  742. const virtualEl = {
  743. getReferenceClientRect: () => ({
  744. width: 0,
  745. height: 0,
  746. top: clientY,
  747. right: clientX,
  748. bottom: clientY,
  749. left: clientX,
  750. }),
  751. };
  752. if (isBlock) {
  753. contextMenu.data = isBlock.id;
  754. contextMenu.position = virtualEl;
  755. contextMenu.items = contextMenuItems.block;
  756. contextMenu.show = true;
  757. }
  758. const copiedNodesLen = store.state.copiedNodes.length;
  759. if (copiedNodesLen > 0) {
  760. if (isBlock) {
  761. contextMenuItems.common.forEach((item) => {
  762. const isExists = contextMenu.items.some(
  763. (menu) => menu.id === item.id
  764. );
  765. if (isExists) return;
  766. contextMenu.items.unshift(item);
  767. });
  768. } else {
  769. contextMenu.items = contextMenuItems.common;
  770. }
  771. contextMenu.position = virtualEl;
  772. contextMenu.show = true;
  773. }
  774. });
  775. const editorStates =
  776. parseJSON(localStorage.getItem('editor-states'), {}) || {};
  777. const editorState = editorStates[workflowId];
  778. if (editorState) {
  779. const { canvas_x, canvas_y, zoom } = editorState;
  780. editor.value.translate_to(canvas_x, canvas_y, zoom);
  781. }
  782. checkWorkflowData();
  783. initSelectArea();
  784. emit('loaded', editor.value);
  785. });
  786. onBeforeUnmount(() => {
  787. const element = document.querySelector('#drawflow');
  788. if (element) {
  789. element.removeEventListener('mousedown', onMousedown);
  790. element.removeEventListener('mouseup', onMouseup);
  791. element.removeEventListener('click', onClick);
  792. element.removeEventListener('keyup', onKeyup);
  793. }
  794. saveEditorState();
  795. });
  796. return {
  797. t,
  798. editor,
  799. contextMenu,
  800. dropHandler,
  801. handleDragOver,
  802. clearContextMenu,
  803. contextMenuHandler: {
  804. copyBlocks,
  805. deleteBlock,
  806. pasteBlocks: () => duplicateBlock(null, true),
  807. duplicateBlock: () => duplicateBlock(contextMenu.data.substr(5)),
  808. },
  809. };
  810. },
  811. };
  812. </script>
  813. <style>
  814. #drawflow {
  815. background-image: url('@/assets/images/tile.png');
  816. background-size: 35px;
  817. user-select: none;
  818. }
  819. .dark #drawflow {
  820. background-image: url('@/assets/images/tile-white.png');
  821. }
  822. .drawflow .drawflow-node {
  823. @apply dark:bg-gray-800;
  824. }
  825. #drawflow[arrow='true'] .drawflow-node .input {
  826. background-color: transparent !important;
  827. border-top: 10px solid transparent;
  828. border-radius: 0;
  829. border-left: 10px solid theme('colors.accent');
  830. border-right: 10px solid transparent;
  831. border-bottom: 10px solid transparent;
  832. }
  833. .selection-area {
  834. background: rgba(46, 115, 252, 0.11);
  835. border: 2px solid rgba(98, 155, 255, 0.81);
  836. border-radius: 0.1em;
  837. }
  838. .drawflow_content_node {
  839. @apply rounded-lg;
  840. }
  841. </style>