From 4ca300dbcef82cfa13f8ce01e5d6154df0570d8f Mon Sep 17 00:00:00 2001 From: Joshua Carter Date: Wed, 10 Apr 2024 10:20:44 -0700 Subject: [PATCH 1/5] Disable context menu on canvas to make connection deletion easier --- .../src/app/components/flow-canvas/flow-canvas-container.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/flow-client/src/app/components/flow-canvas/flow-canvas-container.tsx b/packages/flow-client/src/app/components/flow-canvas/flow-canvas-container.tsx index 42d5d62..af3da88 100644 --- a/packages/flow-client/src/app/components/flow-canvas/flow-canvas-container.tsx +++ b/packages/flow-client/src/app/components/flow-canvas/flow-canvas-container.tsx @@ -78,11 +78,14 @@ export const FlowCanvasContainer: React.FC = ({ const canvas = document.querySelector('.flow-canvas'); const handleZoom = (event: Event) => engine.increaseZoomLevel(event as WheelEvent); + const disableContextMenu = (event: Event) => event.preventDefault(); canvas?.addEventListener('wheel', handleZoom); + canvas?.addEventListener('contextmenu', disableContextMenu); return () => { canvas?.removeEventListener('wheel', handleZoom); + canvas?.removeEventListener('contextmenu', disableContextMenu); }; }, []); From 0803ca351d9b8252b9cddf90f954fd32affa9b9e Mon Sep 17 00:00:00 2001 From: Joshua Carter Date: Wed, 10 Apr 2024 17:36:44 -0700 Subject: [PATCH 2/5] Rewrite technical requirements for FB-04 --- README.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b7aa7e9..caa538b 100644 --- a/README.md +++ b/README.md @@ -166,18 +166,13 @@ The backlog is organized by epic, with each task having a unique ID, description - **Utilize `react-dnd` for Drag-and-Drop Functionality**: `react-dnd` will be used to handle the drag-and-drop operations, providing a flexible and intuitive user experience for adding nodes to the canvas. - **Visual Feedback and User Experience**: Implement visual cues during the drag-and-drop operation, such as changing the cursor, highlighting potential drop zones, and showing a "ghost" image of the node being dragged to provide clear feedback to the user. - **Responsive Design Considerations**: Ensure that the drag-and-drop functionality is fully responsive and provides a consistent experience across different devices and screen sizes. -- **FB-04** (Priority: 4): Develop node connection functionality. - - **Objective**: Allow users to create connections between nodes on the canvas, forming logical flows. +- **FB-04** (Priority: 4): Verify and Enhance Node Connection Functionality. + - **Objective**: Ensure the existing node connection functionality is working as expected and introduce enhancements for a more intuitive user experience. - **Technical Requirements**: - - Implement a method for users to draw connections between nodes, possibly by dragging from one node's output port to another node's input port. - - Utilize `@projectstorm/react-diagrams` for managing the rendering and logic of connections, ensuring compatibility with the library's way of handling links. - - Connections should be visually distinct and should support different styles (straight lines, curves) to enhance readability. - - Include validation to ensure that connections between incompatible node types or ports are not allowed. - - Provide visual feedback during the connection process, such as highlighting compatible ports when drawing a connection. -- **FB-05** (Priority: 5): Implement editor UI for nodes. - - **Objective**: Provide a user-friendly interface for configuring and editing node properties. - - **Technical Requirements**: - - Implement UI components for editing node properties, including individual node attributes and dialog boxes for configuration. + - Verify that users can draw connections between nodes by dragging from one node's output port to another node's input port, utilizing `@projectstorm/react-diagrams` for rendering and logic. Ensure connections are visually distinct, support different styles for enhanced readability, include validation for incompatible node types or ports, and provide visual feedback during the connection process. + - Implement state management for the flow canvas that streams changes into our application state, ensuring the state matches the spec for a Node-RED `flows.yaml` file. This will involve capturing the state of nodes, their connections, and any other relevant flow information in a format that is compatible with Node-RED, facilitating seamless integration and future features such as exporting flows. + - Explore the feasibility of enhancing the connection drawing process to allow for auto-attachment of connections to the nearest appropriate port (input or output) when a user draws a connection over a node. This feature aims to simplify the process of creating connections by reducing the precision required to attach a wire to a specific port, thereby improving the user experience. + - Implement visual indicators for compatible and incompatible connections during the drag-and-drop operation. This could involve changing the color of the connection line or displaying icons to indicate whether the connection can be successfully made, enhancing the feedback provided to the user in real-time. Limit this requirement to what is easily accomplished via react-diagrams. #### Epic: Node Management Interface From f46a69816627ec4fbb743e5f111940db5d9f4a02 Mon Sep 17 00:00:00 2001 From: Joshua Carter Date: Wed, 10 Apr 2024 17:37:10 -0700 Subject: [PATCH 3/5] Start FB-04 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index caa538b..9bb0cc9 100644 --- a/README.md +++ b/README.md @@ -265,8 +265,8 @@ The backlog is organized by epic, with each task having a unique ID, description | To Do | In Progress | In Review | Done | | ----- | ----------- | --------- | ----- | -| FB-04 | | | FB-01 | -| FB-05 | | | FB-02 | +| FB-05 | FB-04 | | FB-01 | +| | | | FB-02 | | | | | FB-03 | ### Progress Tracking From d94fb2ecc047601a13e7ce939ca0260745db05ac Mon Sep 17 00:00:00 2001 From: Joshua Carter Date: Wed, 10 Apr 2024 17:47:49 -0700 Subject: [PATCH 4/5] Write implementation details for FB-04 and move one technical requirement to later epic --- README.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9bb0cc9..1c31f23 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,18 @@ The backlog is organized by epic, with each task having a unique ID, description - Verify that users can draw connections between nodes by dragging from one node's output port to another node's input port, utilizing `@projectstorm/react-diagrams` for rendering and logic. Ensure connections are visually distinct, support different styles for enhanced readability, include validation for incompatible node types or ports, and provide visual feedback during the connection process. - Implement state management for the flow canvas that streams changes into our application state, ensuring the state matches the spec for a Node-RED `flows.yaml` file. This will involve capturing the state of nodes, their connections, and any other relevant flow information in a format that is compatible with Node-RED, facilitating seamless integration and future features such as exporting flows. - Explore the feasibility of enhancing the connection drawing process to allow for auto-attachment of connections to the nearest appropriate port (input or output) when a user draws a connection over a node. This feature aims to simplify the process of creating connections by reducing the precision required to attach a wire to a specific port, thereby improving the user experience. - - Implement visual indicators for compatible and incompatible connections during the drag-and-drop operation. This could involve changing the color of the connection line or displaying icons to indicate whether the connection can be successfully made, enhancing the feedback provided to the user in real-time. Limit this requirement to what is easily accomplished via react-diagrams. + - **Implementation Details**: + - **State Management for Flow Canvas**: + - To manage the state of the flow canvas effectively, including nodes, their connections, and other relevant flow information, new files dedicated to flow management will be introduced: + 1. **Flow Slice (`flow/flow.slice.ts`)**: Manages the state of the flow canvas, including nodes, connections, and flow configurations. + 2. **Flow Logic (`flow/flow.logic.ts`)**: Encapsulates the business logic for managing flows, including the creation, update, and deletion of nodes and connections. + 3. **Flow Slice Tests (`flow/flow.slice.spec.ts`)**: Ensures the flow slice correctly manages the state of the flow canvas. + 4. **Flow Logic Tests (`flow/flow.logic.spec.ts`)**: Validates the business logic for flow management. + - These files will work together to ensure a robust state management system for the flow canvas, enhancing node connection functionality and ensuring a seamless user experience. + - **Enhancements to Connection Drawing Process**: + - **User Experience (UX) Improvements**: Simplify the process of creating connections with intuitive auto-attachment to the nearest valid port and provide visual feedback during the process. + - **Technical Feasibility**: Implement port proximity detection and valid port identification to support auto-attachment features. + - **Implementation Strategies**: Explore extending or customizing `@projectstorm/react-diagrams` for auto-attachment functionality and develop custom drag-and-drop logic as needed. #### Epic: Node Management Interface @@ -215,6 +226,19 @@ The backlog is organized by epic, with each task having a unique ID, description - **UX-03**: Implement responsive design. - **Objective**: Ensure the frontend client is accessible and usable across various devices. - **Technical Requirements**: Adopt a responsive design approach that allows the frontend client to adapt to different screen sizes and resolutions, ensuring a consistent user experience. +- **UX-04**: Implement Visual Indicators for Node Connection Compatibility. + - **Objective**: Enhance the user experience by introducing visual indicators that provide immediate feedback on the compatibility of connections between nodes during the drag-and-drop operation. + - **Technical Requirements**: + - Develop a system to visually indicate when a connection being dragged is compatible or incompatible with a potential target port. + - Customize port and link models to include compatibility information, allowing for dynamic styling based on the context of the drag-and-drop operation. + - Implement custom widgets for ports and links that change appearance (e.g., color, icons) to reflect compatibility status. + - Utilize the event system in `@projectstorm/react-diagrams` to update the appearance of ports and links in real-time during drag-and-drop actions. + - **Justification**: This feature aims to simplify the process of creating connections by reducing the need for trial and error, thereby improving the overall user experience. By providing clear visual cues, users can easily identify valid connection paths, leading to more efficient flow construction. + - **Implementation Notes**: + - Consider the development effort and complexity involved in customizing the underlying library. This task may require extensive testing to ensure a seamless integration with existing functionalities. + - Prioritize user feedback on the current version of the flow builder to determine the necessity and priority of this enhancement. + - **Future Considerations**: + - Gather user feedback on the implementation to assess its effectiveness and explore further enhancements based on real-world usage. #### Epic: Debugging and Testing Tools From 28fa3854486c0480932596cf91e9338296e09dd5 Mon Sep 17 00:00:00 2001 From: Joshua Carter Date: Wed, 10 Apr 2024 19:31:21 -0700 Subject: [PATCH 5/5] Attempt to implement port proximity attachment logic --- .../components/flow-canvas/custom-engine.ts | 4 +- .../app/components/flow-canvas/diagram.tsx | 83 ++++++++++++++++ .../flow-canvas/flow-canvas-container.tsx | 97 ++++++++++++++----- .../src/app/components/flow-canvas/link.tsx | 47 +++++++++ .../src/app/components/flow-canvas/node.tsx | 8 ++ 5 files changed, 212 insertions(+), 27 deletions(-) create mode 100644 packages/flow-client/src/app/components/flow-canvas/diagram.tsx create mode 100644 packages/flow-client/src/app/components/flow-canvas/link.tsx diff --git a/packages/flow-client/src/app/components/flow-canvas/custom-engine.ts b/packages/flow-client/src/app/components/flow-canvas/custom-engine.ts index af8d9eb..4d5d1f3 100644 --- a/packages/flow-client/src/app/components/flow-canvas/custom-engine.ts +++ b/packages/flow-client/src/app/components/flow-canvas/custom-engine.ts @@ -2,7 +2,6 @@ import { AbstractReactFactory, DefaultDiagramState, DefaultLabelFactory, - DefaultLinkFactory, DefaultPortFactory, DiagramEngine, LayerModel, @@ -13,6 +12,7 @@ import { SelectionBoxLayerFactory, } from '@projectstorm/react-diagrams'; +import { CustomLinkFactory } from './link'; import { CustomNodeFactory } from './node'; export class CustomEngine extends DiagramEngine { @@ -67,7 +67,7 @@ export const createEngine = (options = {}) => { engine.getLayerFactories().registerFactory(new SelectionBoxLayerFactory()); engine.getLabelFactories().registerFactory(new DefaultLabelFactory()); engine.getNodeFactories().registerFactory(new CustomNodeFactory()); // i cant figure out why - engine.getLinkFactories().registerFactory(new DefaultLinkFactory()); + engine.getLinkFactories().registerFactory(new CustomLinkFactory()); engine.getLinkFactories().registerFactory(new PathFindingLinkFactory()); engine.getPortFactories().registerFactory(new DefaultPortFactory()); // register the default interaction behaviours diff --git a/packages/flow-client/src/app/components/flow-canvas/diagram.tsx b/packages/flow-client/src/app/components/flow-canvas/diagram.tsx new file mode 100644 index 0000000..b141c4a --- /dev/null +++ b/packages/flow-client/src/app/components/flow-canvas/diagram.tsx @@ -0,0 +1,83 @@ +import { + DiagramModel, + LinkModel, + LinkModelGenerics, +} from '@projectstorm/react-diagrams'; + +type Point = { + x: number; + y: number; +}; + +export class CustomDiagramModel extends DiagramModel { + addLink(link: LinkModel): LinkModel { + const addedLink = super.addLink(link); + // After adding the link, attach event listeners + this.attachLinkListeners(addedLink); + return addedLink; + } + + private attachLinkListeners(link: LinkModel) { + // Logic to attach event listeners goes here + // Since the actual DOM element might not yet be available immediately after adding the link, + // you might need to defer this operation or use a MutationObserver as discussed in Option 3. + setTimeout(() => this.setupLinkDragEvents(link), 50); // Example delay + } + + private setupLinkDragEvents(link: LinkModel) { + // Assuming you have a way to identify the DOM element for the link + const linkElement = document.querySelector( + `[data-linkid="${link.getID()}"]` + ); + if (linkElement) { + linkElement.addEventListener('dragstart', () => { + // Your drag start logic here + }); + linkElement.addEventListener('dragend', e => { + const dropPosition = this.getDropPosition(e); + // Now you have the drop position, you can proceed to find the nearest node or port + this.attachLinkToNearestNode(link, dropPosition); + }); + } + } + + private getDropPosition(event: DragEvent): Point { + const engine = this.engine; + // Assuming `event` is the native drop event + const { clientX, clientY } = event; + + // Translate screen coordinates to diagram coordinates + // This step is crucial because the diagram might be zoomed or scrolled + const relativePoint = engine.getRelativeMousePoint({ + clientX, + clientY, + }); + + return relativePoint; + } + + private attachLinkToNearestNode( + link: LinkModel, + dropPosition: Point + ) { + // Example pseudocode + const dropPosition = this.getDropPosition(); // Implement this based on your app's logic + let nearestNode = null; + let minDistance = Infinity; + + this.getNodes().forEach(node => { + const nodePosition = this.getNodePosition(node); // Implement this + const distance = this.calculateDistance(dropPosition, nodePosition); // Implement this + + if (distance < minDistance) { + nearestNode = node; + minDistance = distance; + } + }); + + if (nearestNode) { + // Logic to attach the link to the nearestNode's port + // This might involve finding a specific port on the node and setting the link's target port + } + } +} diff --git a/packages/flow-client/src/app/components/flow-canvas/flow-canvas-container.tsx b/packages/flow-client/src/app/components/flow-canvas/flow-canvas-container.tsx index af3da88..5d8b761 100644 --- a/packages/flow-client/src/app/components/flow-canvas/flow-canvas-container.tsx +++ b/packages/flow-client/src/app/components/flow-canvas/flow-canvas-container.tsx @@ -5,7 +5,7 @@ import { DiagramModel, PortModelAlignment, } from '@projectstorm/react-diagrams'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useDrop } from 'react-dnd'; import styled from 'styled-components'; @@ -15,6 +15,7 @@ import { NodeEntity } from '../../redux/modules/node/node.slice'; import { createEngine } from './custom-engine'; import { CustomNodeModel } from './node'; import { useAppLogic } from '../../redux/hooks'; +import { CustomDiagramModel } from './diagram'; const StyledCanvasWidget = styled(CanvasWidget)` background-color: #f0f0f0; /* Light grey background */ @@ -67,34 +68,21 @@ export const FlowCanvasContainer: React.FC = ({ }) => { const nodeLogic = useAppLogic().node; - const model = new DiagramModel(); - model.setGridSize(20); - - // Your existing setup code for adding nodes and links to the model - - engine.setModel(model); + const [model, setModel] = useState(new CustomDiagramModel()); useEffect(() => { - const canvas = document.querySelector('.flow-canvas'); - const handleZoom = (event: Event) => - engine.increaseZoomLevel(event as WheelEvent); - const disableContextMenu = (event: Event) => event.preventDefault(); + engine.setModel(model); - canvas?.addEventListener('wheel', handleZoom); - canvas?.addEventListener('contextmenu', disableContextMenu); + model.setGridSize(20); + // Add initial nodes and links to the model if any + initialDiagram.nodes?.forEach(node => model.addNode(node)); + initialDiagram.links?.forEach(link => model.addLink(link)); - return () => { - canvas?.removeEventListener('wheel', handleZoom); - canvas?.removeEventListener('contextmenu', disableContextMenu); - }; - }, []); - - // Add initial nodes and links to the model if any - initialDiagram.nodes?.forEach(node => model.addNode(node)); - initialDiagram.links?.forEach(link => model.addLink(link)); - - // Configure engine and model as needed - engine.setModel(model); + const links = model.getLinks(); + links.forEach(link => { + setupLinkDragEvents(link); + }); + }, [initialDiagram.links, initialDiagram.nodes, model]); const [, drop] = useDrop(() => ({ accept: ItemTypes.NODE, @@ -179,6 +167,65 @@ export const FlowCanvasContainer: React.FC = ({ }, })); + useEffect(() => { + const canvas = document.querySelector('.flow-canvas'); + const handleZoom = (event: Event) => + engine.increaseZoomLevel(event as WheelEvent); + const disableContextMenu = (event: Event) => event.preventDefault(); + + canvas?.addEventListener('wheel', handleZoom); + canvas?.addEventListener('contextmenu', disableContextMenu); + + return () => { + canvas?.removeEventListener('wheel', handleZoom); + canvas?.removeEventListener('contextmenu', disableContextMenu); + }; + }, []); + + useEffect(() => { + const handleDrop = (event: DragEvent) => { + event.preventDefault(); + // Assuming you have a way to get the currently dragged link + const draggedLink = getCurrentDraggedLink(); + if (!draggedLink) { + return; + } + + const mousePoint = engine.getRelativeMousePoint(event); + const nodes = engine.getModel().getNodes(); + let closestPort = null; + let minDistance = Infinity; + + nodes.forEach(node => { + node.getPorts().forEach(port => { + const portPosition = engine.getPortCoords(port); + const distance = Math.hypot( + portPosition.x - mousePoint.x, + portPosition.y - mousePoint.y + ); + + if (distance < minDistance) { + closestPort = port; + minDistance = distance; + } + }); + }); + + if (closestPort && minDistance < YOUR_DEFINED_THRESHOLD) { + // Check if the port is compatible with the link + if (isPortCompatibleWithLink(closestPort, draggedLink)) { + // Attach the link to the port + draggedLink.setTargetPort(closestPort); + engine.repaintCanvas(); + } + } + }; + + const canvas = document.querySelector('.flow-canvas'); + canvas?.addEventListener('drop', handleDrop); + return () => canvas?.removeEventListener('drop', handleDrop); + }, []); // Add dependencies as needed + // The CanvasWidget component is used to render the flow canvas within the UI. // The "canvas-widget" className can be targeted for custom styling. return ( diff --git a/packages/flow-client/src/app/components/flow-canvas/link.tsx b/packages/flow-client/src/app/components/flow-canvas/link.tsx new file mode 100644 index 0000000..04e392a --- /dev/null +++ b/packages/flow-client/src/app/components/flow-canvas/link.tsx @@ -0,0 +1,47 @@ +import { + DefaultLinkFactory, + DefaultLinkModel, + DefaultLinkProps, + DefaultLinkWidget, + GenerateWidgetEvent, +} from '@projectstorm/react-diagrams'; + +export class CustomLinkModel extends DefaultLinkModel { + constructor() { + super({ + type: 'custom', + // Additional custom properties + }); + } + + // Custom methods for handling proximity or compatibility checks +} + +export interface CustomLinkProps extends DefaultLinkProps { + link: CustomLinkModel; + // Other props +} + +export const CustomLinkWidget: React.FC = props => { + // Custom rendering logic here + return ; +}; + +export class CustomLinkFactory extends DefaultLinkFactory { + constructor() { + super('custom'); // This type should match the type in your CustomLinkModel + } + + generateModel(): CustomLinkModel { + return new CustomLinkModel(); + } + + // If you have a custom widget, override generateReactWidget method + generateReactWidget( + event: GenerateWidgetEvent + ): JSX.Element { + return ( + + ); + } +} diff --git a/packages/flow-client/src/app/components/flow-canvas/node.tsx b/packages/flow-client/src/app/components/flow-canvas/node.tsx index 45f68ec..7212d86 100644 --- a/packages/flow-client/src/app/components/flow-canvas/node.tsx +++ b/packages/flow-client/src/app/components/flow-canvas/node.tsx @@ -165,6 +165,14 @@ export class CustomNodeModel extends DefaultNodeModel { type: 'custom-node', }); } + + // Method to calculate distance from the port to a given point + calculateDistanceToPoint(x: number, y: number): number { + const portPosition = this.getPosition(); + return Math.sqrt( + Math.pow(portPosition.x - x, 2) + Math.pow(portPosition.y - y, 2) + ); + } } // Factory for the custom node, if you're using TypeScript, you might need to extend the appropriate factory class