diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/Activity.cs b/src/clients/Elsa.Api.Client/Shared/Models/Activity.cs similarity index 65% rename from src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/Activity.cs rename to src/clients/Elsa.Api.Client/Shared/Models/Activity.cs index 0a5ff5bdd..75549ffbc 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/Activity.cs +++ b/src/clients/Elsa.Api.Client/Shared/Models/Activity.cs @@ -1,9 +1,9 @@ -namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; +namespace Elsa.Api.Client.Shared.Models; /// /// Represents an activity in a workflow definition. /// -public class Activity : Dictionary +public class Activity { /// /// Gets or sets the ID of this activity. @@ -24,20 +24,26 @@ public class Activity : Dictionary /// Gets or sets the metadata of this activity. /// public IDictionary Metadata { get; set; } = new Dictionary(); - - /// - /// Gets or sets whether this activity can be used as a trigger to start a workflow. - /// - public bool CanStartWorkflow { get; set; } - - /// - /// Gets or sets a value whether this activity should execute asynchronously. - /// - public bool RunAsynchronously { get; set; } /// /// Gets or sets custom properties of this activity. /// public IDictionary CustomProperties { get; set; } = new Dictionary(); +} + +/// +/// Represents a flowchart activity. +/// +public class Flowchart : Container +{ + public ICollection Type1 { get; set; } +} + +/// +/// Represents a connection between two activities. +/// +public class Connection +{ + } \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/Container.cs b/src/clients/Elsa.Api.Client/Shared/Models/Container.cs similarity index 82% rename from src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/Container.cs rename to src/clients/Elsa.Api.Client/Shared/Models/Container.cs index 26c24619a..cb01359fe 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/Container.cs +++ b/src/clients/Elsa.Api.Client/Shared/Models/Container.cs @@ -1,4 +1,6 @@ -namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; +using Elsa.Api.Client.Resources.WorkflowDefinitions.Models; + +namespace Elsa.Api.Client.Shared.Models; /// /// Represents a container activity. diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/Trigger.cs b/src/clients/Elsa.Api.Client/Shared/Models/Trigger.cs similarity index 60% rename from src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/Trigger.cs rename to src/clients/Elsa.Api.Client/Shared/Models/Trigger.cs index f7958af6d..1bd9057a2 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/Trigger.cs +++ b/src/clients/Elsa.Api.Client/Shared/Models/Trigger.cs @@ -1,4 +1,4 @@ -namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; +namespace Elsa.Api.Client.Shared.Models; /// /// Represents a trigger. diff --git a/src/designer/designer_packages/elsa-workflows-designer/src/modules/flowchart/flowchart.tsx b/src/designer/designer_packages/elsa-workflows-designer/src/modules/flowchart/flowchart.tsx index 7992b665d..db1f2d6b5 100644 --- a/src/designer/designer_packages/elsa-workflows-designer/src/modules/flowchart/flowchart.tsx +++ b/src/designer/designer_packages/elsa-workflows-designer/src/modules/flowchart/flowchart.tsx @@ -416,8 +416,8 @@ export class FlowchartComponent { const activities = flowchart.activities; const connections = flowchart.connections; this.updateGraphInternal(activities, connections); - - if (this.isReadonly) { + + if (this.isReadonly) { this.graph.disableSelectionMovable(); this.graph.disableKeyboard(); } @@ -457,7 +457,7 @@ export class FlowchartComponent { const connections = graphModel.cells.filter(x => x.shape == 'elsa-edge' && !!x.data).map(x => x.data as Connection); let rootActivities = activities.filter(activity => { - const hasInboundConnections = connections.find(c => c.target == activity.id) != null; + const hasInboundConnections = connections.find(c => c.target.activity == activity.id) != null; return !hasInboundConnections; }); @@ -475,10 +475,10 @@ export class FlowchartComponent { shape: 'elsa-edge', zIndex: -1, data: connection, - source: connection.source, - target: connection.target, - sourcePort: connection.sourcePort, - targetPort: connection.targetPort + source: connection.source.activity, + target: connection.target.activity, + sourcePort: connection.source.port, + targetPort: connection.target.port }; } @@ -489,14 +489,14 @@ export class FlowchartComponent { for (const edge of edges) { const connection: Connection = edge.data; - if (connection.target != cachedActivityId && connection.source != cachedActivityId) + if (connection.target.activity != cachedActivityId && connection.source.activity != cachedActivityId) continue; - if (connection.target == cachedActivityId) - connection.target = updatedActivity.id; + if (connection.target.activity == cachedActivityId) + connection.target.activity = updatedActivity.id; - if (connection.source == cachedActivityId) - connection.source = updatedActivity.id; + if (connection.source.activity == cachedActivityId) + connection.source.activity = updatedActivity.id; edge.data = connection; } @@ -599,10 +599,14 @@ export class FlowchartComponent { const targetPort = targetNode.getPort(edge.getTargetPortId()).id; const connection: Connection = { - source: sourceActivity.id, - sourcePort: sourcePort, - target: targetActivity.id, - targetPort: targetPort + source: { + activity: sourceActivity.id, + port: sourcePort + }, + target: { + activity: targetActivity.id, + port: targetPort + } }; edge.data = connection; diff --git a/src/designer/designer_packages/elsa-workflows-designer/src/modules/flowchart/graph-factory.ts b/src/designer/designer_packages/elsa-workflows-designer/src/modules/flowchart/graph-factory.ts index 7ad8b030e..ecc78c67f 100644 --- a/src/designer/designer_packages/elsa-workflows-designer/src/modules/flowchart/graph-factory.ts +++ b/src/designer/designer_packages/elsa-workflows-designer/src/modules/flowchart/graph-factory.ts @@ -238,10 +238,17 @@ export function createGraph( for (const cell of connectionCells) { const connection = {...cell.getData()} as Connection; - connection.sourcePort = deriveNewPortId(connection.sourcePort); - connection.targetPort = deriveNewPortId(connection.targetPort); - connection.source = idMap[connection.source]; - connection.target = idMap[connection.target]; + + connection.source = { + activity: idMap[connection.source.activity], + port: deriveNewPortId(connection.source.port) + }; + + connection.target = { + activity: idMap[connection.target.activity], + port: deriveNewPortId(connection.target.port) + }; + const newEdgeProps = createEdge(connection); const edge = graph.createEdge(newEdgeProps); newCells.push(edge); diff --git a/src/designer/designer_packages/elsa-workflows-designer/src/modules/flowchart/models.ts b/src/designer/designer_packages/elsa-workflows-designer/src/modules/flowchart/models.ts index eb8fee637..b198af67d 100644 --- a/src/designer/designer_packages/elsa-workflows-designer/src/modules/flowchart/models.ts +++ b/src/designer/designer_packages/elsa-workflows-designer/src/modules/flowchart/models.ts @@ -6,10 +6,13 @@ export interface Flowchart extends Container { } export interface Connection { - source: string; - target: string; - sourcePort: string; - targetPort: string; + source: Endpoint; + target: Endpoint; +} + +export interface Endpoint { + activity: string; + port: string; } export interface FlowchartNavigationItem { diff --git a/src/designer/designer_packages/elsa-workflows-designer/src/utils/graph.ts b/src/designer/designer_packages/elsa-workflows-designer/src/utils/graph.ts index a02dcadb0..6fb3fe389 100644 --- a/src/designer/designer_packages/elsa-workflows-designer/src/utils/graph.ts +++ b/src/designer/designer_packages/elsa-workflows-designer/src/utils/graph.ts @@ -1,8 +1,8 @@ -import { Edge, Graph, Node } from "@antv/x6"; -import { PortManager } from "@antv/x6/lib/model/port"; -import { Connection } from "../modules/flowchart/models"; -import { v4 as uuid } from 'uuid'; -import { Activity } from "../models"; +import {Edge, Graph, Node} from "@antv/x6"; +import {PortManager} from "@antv/x6/lib/model/port"; +import {Connection} from "../modules/flowchart/models"; +import {v4 as uuid} from 'uuid'; +import {Activity} from "../models"; import optionsStore from '../data/designer-options-store'; export function rebuildGraph(graph: Graph) { @@ -22,7 +22,7 @@ export function autoOrientConnections(graph: Graph, selectedNode: Node) { neighbourNode: neighbourNode } }); - nodeCouplesWithPositions.forEach(couple => updatePortsWithNewPositions(graph, couple.selectedNode, couple.portPositionOfSelectedNode, couple.neighbourNode as Node, couple.portPositionOfNeighbourNode)); + nodeCouplesWithPositions.forEach(couple => updatePortsWithNewPositions(graph, couple.selectedNode, couple.portPositionOfSelectedNode, couple.neighbourNode as Node, couple.portPositionOfNeighbourNode)); } function updatePortsWithNewPositions( @@ -40,12 +40,12 @@ function updatePortsWithNewPositions( } function calculatePositionsForInflexibleNode(sourceNode: Node, targetNodes: Node[]): -{ sourceNode: Node, sourceNodePosition: "left" | "right" | "top" | "bottom"; targetNode: Node, targetNodePosition: "left" | "right" | "top" | "bottom"; }[] { + { sourceNode: Node, sourceNodePosition: "left" | "right" | "top" | "bottom"; targetNode: Node, targetNodePosition: "left" | "right" | "top" | "bottom"; }[] { const sourceNodeCenter = sourceNode.getBBox().center; const dxAverageForTargetNodes = targetNodes.map(node => node.getBBox().center.x).reduce((a, b) => a + b, 0) / targetNodes.length; const dyAverageForTargetNodes = targetNodes.map(node => node.getBBox().center.y).reduce((a, b) => a + b, 0) / targetNodes.length; - const sourcePortWithNewPosition = { node: sourceNode, position: calculatePortPositionsOfNodeCouple(sourceNodeCenter.x, sourceNodeCenter.y, dxAverageForTargetNodes, dyAverageForTargetNodes).portPositionOfSelectedNode }; + const sourcePortWithNewPosition = {node: sourceNode, position: calculatePortPositionsOfNodeCouple(sourceNodeCenter.x, sourceNodeCenter.y, dxAverageForTargetNodes, dyAverageForTargetNodes).portPositionOfSelectedNode}; return targetNodes.map((targetNode) => { return { sourceNode: sourcePortWithNewPosition.node, @@ -79,7 +79,7 @@ function calculatePortPositionsOfNodeCouple(selectedNodeX: number, selectedNodeY } } else if (dx <= 0 && dy <= 0) { if (dx > dy) { - return {portPositionOfSelectedNode: "right", portPositionOfNeighbourNode: "left"}; + return {portPositionOfSelectedNode: "right", portPositionOfNeighbourNode: "left"}; } else { return {portPositionOfSelectedNode: "bottom", portPositionOfNeighbourNode: "left"}; } @@ -87,11 +87,11 @@ function calculatePortPositionsOfNodeCouple(selectedNodeX: number, selectedNodeY } function updatePortsAndEdgeOfNodeCouple(graph: Graph, sourceNode: Node, targetNode: Node, portPositionOfSourceNode: string, portPositionOfTargetNode: string) { - const edge = graph.model.getEdges().find(({ data }) => data.source == sourceNode.id && data.target == targetNode.id); + const edge = graph.model.getEdges().find(({data}) => data.source == sourceNode.id && data.target == targetNode.id); if (edge != null) { const sourcePortOfConnection = edge.data.sourcePort; - if(!optionsStore.enableFlexiblePorts && isNewCalculationNeededForInflexiblePort(graph, sourceNode, sourcePortOfConnection)){ + if (!optionsStore.enableFlexiblePorts && isNewCalculationNeededForInflexiblePort(graph, sourceNode, sourcePortOfConnection)) { const outgoingEdges = findOutgoingEdges(graph, sourceNode, sourcePortOfConnection); const targetNodes = graph.getNodes().filter(node => outgoingEdges.map(edge => edge.data.target).includes(node.id)); const nodeCouplesWithPositions = calculatePositionsForInflexibleNode(sourceNode, targetNodes); @@ -119,7 +119,7 @@ function isNewCalculationNeededForInflexiblePort(graph: Graph, sourceNode: Node< } function updatePortsAndEdge(graph: Graph, sourceNode: Node, targetNode: Node, newSourceNodePosition: string, newTargetNodePosition: string) { - const edge = graph.model.getEdges().find(({ data }) => data.source == sourceNode.id && data.target == targetNode.id); + const edge = graph.model.getEdges().find(({data}) => data.source == sourceNode.id && data.target == targetNode.id); const sourceNodePort = sourceNode.getPort(edge.data.sourcePort) ?? sourceNode.getPorts().find(p => p.type == "out" && getPortNameByPortId(p.id) == getPortNameByPortId(edge.data.sourcePort)); const targetNodePort = targetNode.getPort(edge.data.targetPort) ?? targetNode.getPorts().find(p => p.type == "in" && getPortNameByPortId(p.id) == getPortNameByPortId(edge.data.targetPort)); @@ -131,16 +131,20 @@ function updatePortsAndEdge(graph: Graph, sourceNode: Node, tar const newTargetNodePortId = updatePort(graph, targetNode, targetNodePort, newTargetNodePosition); graph.addEdge(createEdge({ - source: sourceNode.id, - target: targetNode.id, - sourcePort: newSourceNodePortId ?? sourceNodePort.id, - targetPort: newTargetNodePortId ?? targetNodePort.id + source: { + activity: sourceNode.id, + port: newSourceNodePortId ?? sourceNodePort.id + }, + target: { + activity: targetNode.id, + port: newTargetNodePortId ?? targetNodePort.id + } })); } } function hasPortAnEdge(graph: Graph, port: PortManager.PortMetadata) { - return graph.getEdges().some(({ data }) => data.sourcePort == port.id || data.targetPort == port.id); + return graph.getEdges().some(({data}) => data.sourcePort == port.id || data.targetPort == port.id); } function findMatchingPortForEdge(node: Node, position: string, portType: string, portName: string) { @@ -152,7 +156,7 @@ export function getPortNameByPortId(portId: string) { } function findOutgoingEdges(graph: Graph, node: Node, portId: string): Edge[] { - return graph.model.getEdges().filter(({ data }) => data.source == node.id && getPortNameByPortId(data.sourcePort) == getPortNameByPortId(portId)); + return graph.model.getEdges().filter(({data}) => data.source == node.id && getPortNameByPortId(data.sourcePort) == getPortNameByPortId(portId)); } function updatePort(graph: Graph, node: Node, nodePort: PortManager.PortMetadata, newPortPosition: string) { @@ -167,8 +171,7 @@ function updatePort(graph: Graph, node: Node, nodePort: PortMan if (matchingPort == null) { newNodePortId = createNewPort(nodePort, node, newPortPosition); - } - else { + } else { newNodePortId = matchingPort.id; } } @@ -208,8 +211,7 @@ export function adjustPortMarkupByNode(node: Node) { fill: '#888', }, }); - } - else { + } else { node.setPortProp(port.id, "attrs", { circle: { r: 5, @@ -232,10 +234,10 @@ export function createEdge(connection: Connection): Edge.Metadata { shape: 'elsa-edge', zIndex: -1, data: connection, - source: connection.source, - target: connection.target, - sourcePort: connection.sourcePort, - targetPort: connection.targetPort + source: connection.source.activity, + target: connection.target.activity, + sourcePort: connection.source.port, + targetPort: connection.target.port }; } diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs index 6706bcd93..6dd6e5286 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -84,11 +84,11 @@ public class Flowchart : Container // If specific outcomes were provided by the completed activity, use them to find the connection to the next activity. Func outboundConnectionsQuery = signal.Result is Outcomes outcomes - ? connection => connection.Source == completedActivity && outcomes.Names.Contains(connection.SourcePort) - : connection => connection.Source == completedActivity; + ? connection => connection.Source.Activity == completedActivity && outcomes.Names.Contains(connection.Source.Port) + : connection => connection.Source.Activity == completedActivity; var outboundConnections = Connections.Where(outboundConnectionsQuery).ToList(); - var children = outboundConnections.Select(x => x.Target).ToList(); + var children = outboundConnections.Select(x => x.Target.Activity).ToList(); var scope = flowchartActivityExecutionContext.GetProperty(ScopeProperty, () => new FlowScope()); scope.RegisterActivityExecution(completedActivity); diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/ConnectionsExtensions.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/ConnectionsExtensions.cs index b0190a0d9..f2d70030d 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/ConnectionsExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/ConnectionsExtensions.cs @@ -3,22 +3,37 @@ using Elsa.Workflows.Core.Contracts; namespace Elsa.Workflows.Core.Activities.Flowchart.Extensions; +/// +/// Contains extension methods for . +/// public static class ConnectionsExtensions { + /// + /// Returns all connections that are descendants of the specified parent activity. + /// public static IEnumerable Descendants(this ICollection connections, IActivity parent) { var visitedActivities = new HashSet(); return connections.Descendants(parent, visitedActivities); } + /// + /// Returns all ancestor connections of the specified parent activity. + /// public static IEnumerable Ancestors(this ICollection connections, IActivity activity) { var visitedActivities = new HashSet(); return connections.Ancestors(activity, visitedActivities); } - public static IEnumerable InboundConnections(this ICollection connections, IActivity activity) => connections.Where(x => x.Target == activity).ToList(); + /// + /// Returns all inbound connections of the specified activity. + /// + public static IEnumerable InboundConnections(this ICollection connections, IActivity activity) => connections.Where(x => x.Target.Activity == activity).ToList(); + /// + /// Returns all "left" inbound connections of the specified activity. "Left" means "not a descendant of the activity". + /// public static IEnumerable LeftInboundConnections(this ICollection connections, IActivity activity) { // We only take "left" inbound connections, which means we exclude descendent connections looping back. @@ -28,6 +43,9 @@ public static class ConnectionsExtensions return filteredConnections; } + /// + /// Returns all "left" ancestor connections of the specified activity. "Left" means "not a descendant of the activity". + /// public static IEnumerable LeftAncestorConnections(this ICollection connections, IActivity activity) { // We only take "left" inbound connections, which means we exclude descendent connections looping back. @@ -37,20 +55,31 @@ public static class ConnectionsExtensions return filteredConnections; } - public static IEnumerable InboundActivities(this ICollection connections, IActivity activity) => connections.InboundConnections(activity).Select(x => x.Source); - public static IEnumerable LeftInboundActivities(this ICollection connections, IActivity activity) => connections.LeftInboundConnections(activity).Select(x => x.Source); - public static IEnumerable LeftAncestorActivities(this ICollection connections, IActivity activity) => connections.LeftAncestorConnections(activity).Select(x => x.Source); + /// + /// Returns all inbound activities of the specified activity. + /// + public static IEnumerable InboundActivities(this ICollection connections, IActivity activity) => connections.InboundConnections(activity).Select(x => x.Source.Activity); + + /// + /// Returns all "left" inbound activities of the specified activity. "Left" means "not a descendant of the activity". + /// + public static IEnumerable LeftInboundActivities(this ICollection connections, IActivity activity) => connections.LeftInboundConnections(activity).Select(x => x.Source.Activity); + + /// + /// Returns all "left" ancestor activities of the specified activity. "Left" means "not a descendant of the activity". + /// + public static IEnumerable LeftAncestorActivities(this ICollection connections, IActivity activity) => connections.LeftAncestorConnections(activity).Select(x => x.Source.Activity); private static IEnumerable Descendants(this ICollection connections, IActivity parent, ISet visitedActivities) { - var children = connections.Where(x => parent == x.Source && !visitedActivities.Contains(x.Target)).ToList(); + var children = connections.Where(x => parent == x.Source.Activity && !visitedActivities.Contains(x.Target.Activity)).ToList(); foreach (var child in children) { - visitedActivities.Add(child.Target); + visitedActivities.Add(child.Target.Activity); yield return child; - var descendants = connections.Descendants(child.Target, visitedActivities).ToList(); + var descendants = connections.Descendants(child.Target.Activity, visitedActivities).ToList(); foreach (var descendant in descendants) { @@ -61,14 +90,14 @@ public static class ConnectionsExtensions private static IEnumerable Ancestors(this ICollection connections, IActivity activity, ISet visitedActivities) { - var parents = connections.Where(x => activity == x.Target && !visitedActivities.Contains(x.Source)).ToList(); + var parents = connections.Where(x => activity == x.Target.Activity && !visitedActivities.Contains(x.Source.Activity)).ToList(); foreach (var parent in parents) { - visitedActivities.Add(parent.Source); + visitedActivities.Add(parent.Source.Activity); yield return parent; - var ancestors = connections.Ancestors(parent.Source, visitedActivities).ToList(); + var ancestors = connections.Ancestors(parent.Source.Activity, visitedActivities).ToList(); foreach (var ancestor in ancestors) { diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Connection.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Connection.cs index 1f0826fac..2a284c485 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Connection.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Connection.cs @@ -1,8 +1,50 @@ +using System.Text.Json.Serialization; using Elsa.Workflows.Core.Contracts; namespace Elsa.Workflows.Core.Activities.Flowchart.Models; /// -/// A connection between a source and target activity via the source out port to the target in port. +/// A connection between a source and a target endpoint. /// -public record Connection(IActivity Source, IActivity Target, string? SourcePort = default, string? TargetPort = default); \ No newline at end of file +public class Connection +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + public Connection() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The source endpoint. + /// The target endpoint. + public Connection(Endpoint source, Endpoint target) + { + Source = source; + Target = target; + } + + /// + /// Initializes a new instance of the class. + /// + /// The source endpoint. + /// The target endpoint. + public Connection(IActivity source, IActivity target) + { + Source = new Endpoint(source); + Target = new Endpoint(target); + } + + /// + /// The source endpoint. + /// + public Endpoint Source { get; set; } = default!; + + /// + /// The target endpoint. + /// + public Endpoint Target { get; set; } = default!; +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Endpoint.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Endpoint.cs new file mode 100644 index 000000000..bfe217659 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Endpoint.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; +using Elsa.Workflows.Core.Contracts; + +namespace Elsa.Workflows.Core.Activities.Flowchart.Models; + +/// +/// Represents an endpoint of a connection. +/// +public class Endpoint +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + public Endpoint() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The activity that the endpoint is connected to. + /// The port that the endpoint is connected to. + public Endpoint(IActivity activity, string? port = default) + { + Activity = activity; + Port = port; + } + + /// + /// The activity that the endpoint is connected to. + /// + public IActivity Activity { get; set; } = default!; + + /// + /// The port that the endpoint is connected to. + /// + public string? Port { get; set; } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/ObsoleteConnection.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/ObsoleteConnection.cs new file mode 100644 index 000000000..034cdbdc1 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/ObsoleteConnection.cs @@ -0,0 +1,61 @@ +using System.Text.Json.Serialization; +using Elsa.Workflows.Core.Contracts; + +namespace Elsa.Workflows.Core.Activities.Flowchart.Models; + +/// +/// A connection between a source and target activity via the source out port to the target in port. +/// +[Obsolete("Use Connection instead.")] +public class ObsoleteConnection +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + public ObsoleteConnection() + { + } + + /// + /// Initializes a new instance of the class. + /// + public ObsoleteConnection(IActivity source, IActivity target, string? sourcePort = default, string? targetPort = default) + { + Source = source; + Target = target; + SourcePort = sourcePort; + TargetPort = targetPort; + } + + /// + /// The source activity. + /// + public IActivity Source { get; set; } = default!; + + /// + /// The target activity. + /// + public IActivity Target { get; set; } = default!; + + /// + /// The source port. + /// + public string? SourcePort { get; set; } + + /// + /// The target port. + /// + public string? TargetPort { get; set; } + + /// + /// Deconstructs the connection into its parts. + /// + public void Deconstruct(out IActivity source, out IActivity target, out string? sourcePort, out string? targetPort) + { + source = Source; + target = Target; + sourcePort = SourcePort; + targetPort = TargetPort; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs index c15d9a796..cd0015067 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs @@ -5,43 +5,57 @@ using Elsa.Workflows.Core.Contracts; namespace Elsa.Workflows.Core.Activities.Flowchart.Serialization; +/// +/// Converts to and from JSON. +/// public class ConnectionJsonConverter : JsonConverter { private readonly IDictionary _activities; + /// public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Connection); + /// public ConnectionJsonConverter(IDictionary activities) { _activities = activities; } + /// public override Connection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (!JsonDocument.TryParseValue(ref reader, out var doc)) throw new JsonException("Failed to parse JsonDocument"); - var sourceId = doc.RootElement.GetProperty("source").GetString()!; - var targetId = doc.RootElement.GetProperty("target").GetString()!; - var sourcePort = doc.RootElement.GetProperty("sourcePort").GetString()!; - var targetPort = doc.RootElement.GetProperty("targetPort").GetString()!; + var sourceElement = doc.RootElement.GetProperty("source"); + var targetElement = doc.RootElement.GetProperty("target"); + var sourceId = sourceElement.GetProperty("activity").GetString()!; + var targetId = targetElement.GetProperty("activity").GetString()!; + var sourcePort = sourceElement.GetProperty("port").GetString()!; + var targetPort = targetElement.GetProperty("port").GetString()!; - var source = _activities.TryGetValue(sourceId, out var s) ? s : default!; - var target = _activities.TryGetValue(targetId, out var t) ? t : default!; - - return new Connection(source, target, sourcePort, targetPort); + var sourceActivity = _activities.TryGetValue(sourceId, out var s) ? s : default!; + var targetActivity = _activities.TryGetValue(targetId, out var t) ? t : default!; + var source = new Endpoint(sourceActivity, sourcePort); + var target = new Endpoint(targetActivity, targetPort); + return new Connection(source, target); } + /// public override void Write(Utf8JsonWriter writer, Connection value, JsonSerializerOptions options) { - var (activity, target, sourcePort, targetPort) = value; - var model = new { - Source = activity.Id, - Target = target.Id, - SourcePort = sourcePort, - TargetPort = targetPort + Source = new + { + Activity = value.Source.Activity.Id, + Port = value.Source.Port + }, + Target = new + { + Activity = value.Target.Activity.Id, + Port = value.Target.Port + } }; JsonSerializer.Serialize(writer, model, options); diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs index 5e3d0e618..73e795720 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs @@ -35,16 +35,10 @@ public class FlowchartJsonConverter : JsonConverter var metadataElement = doc.RootElement.TryGetProperty("metadata", out var metadataEl) ? metadataEl : default; var metadata = metadataElement.ValueKind != JsonValueKind.Undefined ? metadataElement.Deserialize>(options) ?? new Dictionary() : new Dictionary(); var start = activities.FirstOrDefault(x => x.Id == startId) ?? activities.FirstOrDefault(); - var connectionSerializerOptions = new JsonSerializerOptions(options); + //var connectionSerializerOptions = new JsonSerializerOptions(options); var activityDictionary = activities.ToDictionary(x => x.Id); - - connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary)); - - var connections = connectionsElement.ValueKind != JsonValueKind.Undefined - ? connectionsElement.Deserialize>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List() - : new List(); - - var notFoundConnections = GetNotFoundConnections(doc.RootElement, connectionSerializerOptions, activities, connections); + var connections = DeserializeConnections(connectionsElement, activityDictionary, options); + var notFoundConnections = GetNotFoundConnections(doc.RootElement, activityDictionary, connections, options); var connectionsToRestore = FindConnectionsThatCanBeRestored(notFoundConnections, activities); var connectionsWithRestoredOnes = connections.Except(notFoundConnections).Union(connectionsToRestore).ToList(); @@ -97,19 +91,15 @@ public class FlowchartJsonConverter : JsonConverter JsonSerializer.Serialize(writer, model, connectionSerializerOptions); } - private static List GetNotFoundConnections(JsonElement rootElement, JsonSerializerOptions connectionSerializerOptions, IEnumerable activities, IEnumerable connections) + private static ICollection GetNotFoundConnections(JsonElement rootElement, IDictionary activities, IEnumerable connections, JsonSerializerOptions connectionSerializerOptions) { var applicationPropertiesElement = rootElement.TryGetProperty("applicationProperties", out var applicationPropertiesEl) ? applicationPropertiesEl : default; var notFoundConnectionsElement = applicationPropertiesElement.ValueKind != JsonValueKind.Undefined ? applicationPropertiesElement.TryGetProperty(NotFoundConnectionsKey, out var notFoundConnectionsEl) ? notFoundConnectionsEl : default : default; - - var notFoundConnections = notFoundConnectionsElement.ValueKind != JsonValueKind.Undefined - ? notFoundConnectionsElement.Deserialize>(connectionSerializerOptions) - ?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List() - : new List(); + var notFoundConnections = DeserializeConnections(notFoundConnectionsElement, activities, connectionSerializerOptions); // Add connections of NotFoundActivity to the list if they aren't already in it. - var notFoundActivities = activities.Where(x => x is NotFoundActivity).Cast().ToList(); - var notFoundActivityConnections = connections.Where(x => notFoundActivities.Contains(x.Source)).ToList(); + var notFoundActivities = activities.Values.Where(x => x is NotFoundActivity).Cast().ToList(); + var notFoundActivityConnections = connections.Where(x => notFoundActivities.Contains(x.Source.Activity)).ToList(); foreach (var notFoundConnection in notFoundActivityConnections) { @@ -129,15 +119,40 @@ public class FlowchartJsonConverter : JsonConverter { var missingSource = notFoundConnection.Source; var missingTarget = notFoundConnection.Target; - var source = foundActivities.FirstOrDefault(x => x.Id == missingSource.Id); - var target = foundActivities.FirstOrDefault(x => x.Id == missingTarget.Id); + var source = foundActivities.FirstOrDefault(x => x.Id == missingSource.Activity.Id); + var target = foundActivities.FirstOrDefault(x => x.Id == missingTarget.Activity.Id); if (source == null || target == null) continue; - var connection = notFoundConnection with {Source = source, Target = target}; + var connection = new Connection(new Endpoint(source, missingSource.Port), new Endpoint(target, missingTarget.Port)); connectionsThatCanBeRestored.Add(connection); } return connectionsThatCanBeRestored; } + + private static ICollection DeserializeConnections(JsonElement connectionsElement, IDictionary activityDictionary, JsonSerializerOptions options) + { + // To not break existing workflow definitions, we need to support the old connection format. + var useOldConnectionConverter = connectionsElement.EnumerateArray().Any(x => x.TryGetProperty("sourcePort", out var sourcePort) && sourcePort.ValueKind == JsonValueKind.String); + + var connectionSerializerOptions = new JsonSerializerOptions(options); + + if(useOldConnectionConverter) + { + connectionSerializerOptions.Converters.Add(new ObsoleteConnectionJsonConverter(activityDictionary)); + + var obsoleteConnections = connectionsElement.ValueKind != JsonValueKind.Undefined + ? connectionsElement.Deserialize>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List() + : new List(); + + return obsoleteConnections.Select(x => new Connection(new Endpoint(x.Source, x.SourcePort), new Endpoint(x.Target, x.TargetPort))).ToList(); + } + + connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary)); + + return connectionsElement.ValueKind != JsonValueKind.Undefined + ? connectionsElement.Deserialize>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List() + : new List(); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ObsoleteConnectionJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ObsoleteConnectionJsonConverter.cs new file mode 100644 index 000000000..9e9140cbb --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ObsoleteConnectionJsonConverter.cs @@ -0,0 +1,57 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Workflows.Core.Activities.Flowchart.Models; +using Elsa.Workflows.Core.Contracts; + +namespace Elsa.Workflows.Core.Activities.Flowchart.Serialization; + +/// +/// Converts to and from JSON. +/// +[Obsolete("Use ConnectionJsonConverter instead.")] +public class ObsoleteConnectionJsonConverter : JsonConverter +{ + private readonly IDictionary _activities; + + /// + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(ObsoleteConnection); + + /// + public ObsoleteConnectionJsonConverter(IDictionary activities) + { + _activities = activities; + } + + /// + public override ObsoleteConnection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (!JsonDocument.TryParseValue(ref reader, out var doc)) + throw new JsonException("Failed to parse JsonDocument"); + + var sourceId = doc.RootElement.GetProperty("source").GetString()!; + var targetId = doc.RootElement.GetProperty("target").GetString()!; + var sourcePort = doc.RootElement.GetProperty("sourcePort").GetString()!; + var targetPort = doc.RootElement.GetProperty("targetPort").GetString()!; + + var source = _activities.TryGetValue(sourceId, out var s) ? s : default!; + var target = _activities.TryGetValue(targetId, out var t) ? t : default!; + + return new ObsoleteConnection(source, target, sourcePort, targetPort); + } + + /// + public override void Write(Utf8JsonWriter writer, ObsoleteConnection value, JsonSerializerOptions options) + { + var (activity, target, sourcePort, targetPort) = value; + + var model = new + { + Source = activity.Id, + Target = target.Id, + SourcePort = sourcePort, + TargetPort = targetPort + }; + + JsonSerializer.Serialize(writer, model, options); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/Workflows/ImplicitLoopWorkflow.cs b/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/Workflows/ImplicitLoopWorkflow.cs index ff3da9c6c..b85c57eb1 100644 --- a/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/Workflows/ImplicitLoopWorkflow.cs +++ b/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/Workflows/ImplicitLoopWorkflow.cs @@ -34,8 +34,8 @@ public class ImplicitLoopWorkflow : WorkflowBase { new Connection(start, incrementCounter), new Connection(incrementCounter, counterGreaterThanOne), - new Connection(counterGreaterThanOne, retry, SourcePort: "False"), - new Connection(counterGreaterThanOne, end, SourcePort: "True"), + new Connection(new Endpoint(counterGreaterThanOne, "False"), new Endpoint(retry)), + new Connection(new Endpoint(counterGreaterThanOne, "True"), new Endpoint(end)), new Connection(retry, incrementCounter), } };