diff --git a/frontend/src/components/visual-editor/hooks/useVisualEditor.tsx b/frontend/src/components/visual-editor/hooks/useVisualEditor.tsx
index 6fd146d..18dd47a 100644
--- a/frontend/src/components/visual-editor/hooks/useVisualEditor.tsx
+++ b/frontend/src/components/visual-editor/hooks/useVisualEditor.tsx
@@ -8,10 +8,7 @@
*/
import { debounce } from "@mui/material";
-import createEngine, {
- DefaultLinkModel,
- DiagramModel,
-} from "@projectstorm/react-diagrams";
+import createEngine, { DiagramModel } from "@projectstorm/react-diagrams";
import * as React from "react";
import { createContext, useContext } from "react";
@@ -26,6 +23,10 @@ import {
} from "@/types/visual-editor.types";
import { ZOOM_LEVEL } from "../constants";
+import {
+ AdvancedLinkFactory,
+ AdvancedLinkModel,
+} from "../v2/AdvancedLink/AdvancedLink";
import { CustomCanvasWidget } from "../v2/CustomCanvasWidget";
import { CustomDeleteItemsAction } from "../v2/CustomDiagramNodes/CustomDeleteAction";
import { NodeFactory } from "../v2/CustomDiagramNodes/NodeFactory";
@@ -87,6 +88,8 @@ const buildDiagram = ({
model = new DiagramModel();
engine.getNodeFactories().registerFactory(new NodeFactory());
+ engine.getLinkFactories().registerFactory(new AdvancedLinkFactory());
+
engine
.getActionEventBus()
.registerAction(new CustomDeleteItemsAction({ callback: onRemoveNode }));
@@ -139,12 +142,12 @@ const buildDiagram = ({
}
}
};
- const links: DefaultLinkModel[] = [];
+ const links: AdvancedLinkModel[] = [];
data.forEach((datum, index) => {
if ("nextBlocks" in datum && Array.isArray(datum.nextBlocks)) {
datum.nextBlocks?.forEach((nextBlock) => {
- const link = new DefaultLinkModel();
+ const link = new AdvancedLinkModel();
const sourceNode = nodes[index];
const targetNode = nodes.find(
// @ts-ignore
@@ -164,7 +167,7 @@ const buildDiagram = ({
//recursive link
if ("attachedBlock" in datum && datum.attachedBlock) {
- const link = new DefaultLinkModel({
+ const link = new AdvancedLinkModel({
color: "#019185",
selectedColor: "#019185",
type: "default",
diff --git a/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLink.tsx b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLink.tsx
new file mode 100644
index 0000000..fbf933f
--- /dev/null
+++ b/frontend/src/components/visual-editor/v2/AdvancedLink/AdvancedLink.tsx
@@ -0,0 +1,103 @@
+import { css, keyframes } from "@emotion/react";
+import styled from "@emotion/styled";
+import {
+ DefaultLinkFactory,
+ DefaultLinkModel,
+ DefaultLinkModelOptions,
+ DefaultLinkWidget,
+} from "@projectstorm/react-diagrams";
+import React from "react";
+
+interface Point {
+ x: number;
+ y: number;
+}
+const createCurvedPath = (start: Point, end: Point) => {
+ const controlPoint1X = start.x + 220;
+ const controlPoint1Y = start.y - 250;
+ const controlPoint2X = end.x - 250;
+ const controlPoint2Y = end.y - 250;
+
+ return `M ${start.x},${start.y} C ${controlPoint1X},${controlPoint1Y} ${controlPoint2X},${controlPoint2Y} ${end.x},${end.y}`;
+};
+
+namespace S {
+ export const Keyframes = keyframes`
+ from {
+ stroke-dashoffset: 24;
+ }
+ to {
+ stroke-dashoffset: 0;
+ }
+ `;
+
+ const selected = css`
+ stroke-dasharray: 10, 2;
+ animation: ${Keyframes} 1s linear infinite;
+ `;
+
+ export const Path = styled.path<{ selected: boolean }>`
+ ${(p) => p.selected && selected};
+ fill: none;
+ pointer-events: auto;
+ `;
+}
+
+export class AdvancedLinkModel extends DefaultLinkModel {
+ constructor(options?: DefaultLinkModelOptions) {
+ super({
+ ...options,
+ type: "advanced",
+ });
+ }
+}
+
+export class AdvancedLinkFactory extends DefaultLinkFactory {
+ constructor() {
+ super("advanced");
+ }
+
+ generateModel(): AdvancedLinkModel {
+ return new AdvancedLinkModel();
+ }
+
+ generateReactWidget(event): JSX.Element {
+ return ;
+ }
+
+ generateLinkSegment(
+ model: AdvancedLinkModel,
+ selected: boolean,
+ path: string,
+ ) {
+ const isSelfLoop =
+ model.getSourcePort().getNode() === model.getTargetPort().getNode();
+
+ if (isSelfLoop) {
+ // Adjust the path to create a curve
+ const sourcePortPosition = model.getSourcePort().getPosition();
+ const targetPortPosition = model.getTargetPort().getPosition();
+ const startPoint: Point = {
+ x: sourcePortPosition.x + 20,
+ y: sourcePortPosition.y + 20,
+ };
+ const endPoint: Point = {
+ x: targetPortPosition.x + 20,
+ y: targetPortPosition.y + 20,
+ };
+
+ path = createCurvedPath(startPoint, endPoint);
+ }
+
+ return (
+
+ );
+ }
+}
diff --git a/frontend/src/components/visual-editor/v2/Diagrams.tsx b/frontend/src/components/visual-editor/v2/Diagrams.tsx
index 431a408..3b5aee3 100644
--- a/frontend/src/components/visual-editor/v2/Diagrams.tsx
+++ b/frontend/src/components/visual-editor/v2/Diagrams.tsx
@@ -25,7 +25,6 @@ import {
tabsClasses,
} from "@mui/material";
import {
- DefaultLinkModel,
DefaultPortModel,
DiagramEngine,
DiagramModel,
@@ -54,6 +53,7 @@ import { IBlock } from "@/types/block.types";
import { ICategory } from "@/types/category.types";
import { BlockPorts } from "@/types/visual-editor.types";
+import { AdvancedLinkModel } from "./AdvancedLink/AdvancedLink";
import BlockDialog from "../BlockDialog";
import { ZOOM_LEVEL } from "../constants";
import { useVisualEditor } from "../hooks/useVisualEditor";
@@ -195,7 +195,7 @@ const Diagrams = () => {
entity,
port,
}: {
- entity: DefaultLinkModel;
+ entity: AdvancedLinkModel;
port: DefaultPortModel;
}) => {
const link = model.getLink(entity.getOptions().id as string);
@@ -205,7 +205,10 @@ const Diagrams = () => {
[BlockPorts.nextBlocksOutPort, BlockPorts.attachmentOutPort].includes(
// @ts-expect-error protected attr
entity.targetPort.getOptions().label,
- )
+ ) ||
+ (link.getSourcePort().getType() === "attached" &&
+ link.getSourcePort().getParent().getOptions().id ===
+ link.getTargetPort().getParent().getOptions().id)
) {
model.removeLink(link);