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),
}
};