Refactor connection model (#4146)

This commit is contained in:
Sipke Schoorstra 2023-06-18 14:32:48 +02:00 committed by GitHub
parent 02572e9ce2
commit b356cdb52b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 396 additions and 115 deletions

View file

@ -1,9 +1,9 @@
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
namespace Elsa.Api.Client.Shared.Models;
/// <summary>
/// Represents an activity in a workflow definition.
/// </summary>
public class Activity : Dictionary<string, object>
public class Activity
{
/// <summary>
/// Gets or sets the ID of this activity.
@ -24,20 +24,26 @@ public class Activity : Dictionary<string, object>
/// Gets or sets the metadata of this activity.
/// </summary>
public IDictionary<string, object> Metadata { get; set; } = new Dictionary<string, object>();
/// <summary>
/// Gets or sets whether this activity can be used as a trigger to start a workflow.
/// </summary>
public bool CanStartWorkflow { get; set; }
/// <summary>
/// Gets or sets a value whether this activity should execute asynchronously.
/// </summary>
public bool RunAsynchronously { get; set; }
/// <summary>
/// Gets or sets custom properties of this activity.
/// </summary>
public IDictionary<string, object> CustomProperties { get; set; } = new Dictionary<string, object>();
}
/// <summary>
/// Represents a flowchart activity.
/// </summary>
public class Flowchart : Container
{
public ICollection<Connection> Type1 { get; set; }
}
/// <summary>
/// Represents a connection between two activities.
/// </summary>
public class Connection
{
}

View file

@ -1,4 +1,6 @@
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
namespace Elsa.Api.Client.Shared.Models;
/// <summary>
/// Represents a container activity.

View file

@ -1,4 +1,4 @@
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
namespace Elsa.Api.Client.Shared.Models;
/// <summary>
/// Represents a trigger.

View file

@ -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;

View file

@ -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);

View file

@ -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 {

View file

@ -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<Node.Properties>, targetNodes: Node<Node.Properties>[]):
{ sourceNode: Node<Node.Properties>, sourceNodePosition: "left" | "right" | "top" | "bottom"; targetNode: Node<Node.Properties>, targetNodePosition: "left" | "right" | "top" | "bottom"; }[] {
{ sourceNode: Node<Node.Properties>, sourceNodePosition: "left" | "right" | "top" | "bottom"; targetNode: Node<Node.Properties>, 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<Node.Properties>, targetNode: Node<Node.Properties>, 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<Node.Properties>, targetNode: Node<Node.Properties>, 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<Node.Properties>, 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<Node.Properties>, position: string, portType: string, portName: string) {
@ -152,7 +156,7 @@ export function getPortNameByPortId(portId: string) {
}
function findOutgoingEdges(graph: Graph, node: Node<Node.Properties>, portId: string): Edge<Edge.Properties>[] {
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<Node.Properties>, nodePort: PortManager.PortMetadata, newPortPosition: string) {
@ -167,8 +171,7 @@ function updatePort(graph: Graph, node: Node<Node.Properties>, 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
};
}

View file

@ -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<Connection, bool> 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);

View file

@ -3,22 +3,37 @@ using Elsa.Workflows.Core.Contracts;
namespace Elsa.Workflows.Core.Activities.Flowchart.Extensions;
/// <summary>
/// Contains extension methods for <see cref="ICollection{Connection}"/>.
/// </summary>
public static class ConnectionsExtensions
{
/// <summary>
/// Returns all connections that are descendants of the specified parent activity.
/// </summary>
public static IEnumerable<Connection> Descendants(this ICollection<Connection> connections, IActivity parent)
{
var visitedActivities = new HashSet<IActivity>();
return connections.Descendants(parent, visitedActivities);
}
/// <summary>
/// Returns all ancestor connections of the specified parent activity.
/// </summary>
public static IEnumerable<Connection> Ancestors(this ICollection<Connection> connections, IActivity activity)
{
var visitedActivities = new HashSet<IActivity>();
return connections.Ancestors(activity, visitedActivities);
}
public static IEnumerable<Connection> InboundConnections(this ICollection<Connection> connections, IActivity activity) => connections.Where(x => x.Target == activity).ToList();
/// <summary>
/// Returns all inbound connections of the specified activity.
/// </summary>
public static IEnumerable<Connection> InboundConnections(this ICollection<Connection> connections, IActivity activity) => connections.Where(x => x.Target.Activity == activity).ToList();
/// <summary>
/// Returns all "left" inbound connections of the specified activity. "Left" means "not a descendant of the activity".
/// </summary>
public static IEnumerable<Connection> LeftInboundConnections(this ICollection<Connection> 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;
}
/// <summary>
/// Returns all "left" ancestor connections of the specified activity. "Left" means "not a descendant of the activity".
/// </summary>
public static IEnumerable<Connection> LeftAncestorConnections(this ICollection<Connection> 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<IActivity> InboundActivities(this ICollection<Connection> connections, IActivity activity) => connections.InboundConnections(activity).Select(x => x.Source);
public static IEnumerable<IActivity> LeftInboundActivities(this ICollection<Connection> connections, IActivity activity) => connections.LeftInboundConnections(activity).Select(x => x.Source);
public static IEnumerable<IActivity> LeftAncestorActivities(this ICollection<Connection> connections, IActivity activity) => connections.LeftAncestorConnections(activity).Select(x => x.Source);
/// <summary>
/// Returns all inbound activities of the specified activity.
/// </summary>
public static IEnumerable<IActivity> InboundActivities(this ICollection<Connection> connections, IActivity activity) => connections.InboundConnections(activity).Select(x => x.Source.Activity);
/// <summary>
/// Returns all "left" inbound activities of the specified activity. "Left" means "not a descendant of the activity".
/// </summary>
public static IEnumerable<IActivity> LeftInboundActivities(this ICollection<Connection> connections, IActivity activity) => connections.LeftInboundConnections(activity).Select(x => x.Source.Activity);
/// <summary>
/// Returns all "left" ancestor activities of the specified activity. "Left" means "not a descendant of the activity".
/// </summary>
public static IEnumerable<IActivity> LeftAncestorActivities(this ICollection<Connection> connections, IActivity activity) => connections.LeftAncestorConnections(activity).Select(x => x.Source.Activity);
private static IEnumerable<Connection> Descendants(this ICollection<Connection> connections, IActivity parent, ISet<IActivity> 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<Connection> Ancestors(this ICollection<Connection> connections, IActivity activity, ISet<IActivity> 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)
{

View file

@ -1,8 +1,50 @@
using System.Text.Json.Serialization;
using Elsa.Workflows.Core.Contracts;
namespace Elsa.Workflows.Core.Activities.Flowchart.Models;
/// <summary>
/// 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.
/// </summary>
public record Connection(IActivity Source, IActivity Target, string? SourcePort = default, string? TargetPort = default);
public class Connection
{
/// <summary>
/// Initializes a new instance of the <see cref="Connection"/> class.
/// </summary>
[JsonConstructor]
public Connection()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Connection"/> class.
/// </summary>
/// <param name="source">The source endpoint.</param>
/// <param name="target">The target endpoint.</param>
public Connection(Endpoint source, Endpoint target)
{
Source = source;
Target = target;
}
/// <summary>
/// Initializes a new instance of the <see cref="Connection"/> class.
/// </summary>
/// <param name="source">The source endpoint.</param>
/// <param name="target">The target endpoint.</param>
public Connection(IActivity source, IActivity target)
{
Source = new Endpoint(source);
Target = new Endpoint(target);
}
/// <summary>
/// The source endpoint.
/// </summary>
public Endpoint Source { get; set; } = default!;
/// <summary>
/// The target endpoint.
/// </summary>
public Endpoint Target { get; set; } = default!;
}

View file

@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
using Elsa.Workflows.Core.Contracts;
namespace Elsa.Workflows.Core.Activities.Flowchart.Models;
/// <summary>
/// Represents an endpoint of a connection.
/// </summary>
public class Endpoint
{
/// <summary>
/// Initializes a new instance of the <see cref="Endpoint"/> class.
/// </summary>
[JsonConstructor]
public Endpoint()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Endpoint"/> class.
/// </summary>
/// <param name="activity">The activity that the endpoint is connected to.</param>
/// <param name="port">The port that the endpoint is connected to.</param>
public Endpoint(IActivity activity, string? port = default)
{
Activity = activity;
Port = port;
}
/// <summary>
/// The activity that the endpoint is connected to.
/// </summary>
public IActivity Activity { get; set; } = default!;
/// <summary>
/// The port that the endpoint is connected to.
/// </summary>
public string? Port { get; set; }
}

View file

@ -0,0 +1,61 @@
using System.Text.Json.Serialization;
using Elsa.Workflows.Core.Contracts;
namespace Elsa.Workflows.Core.Activities.Flowchart.Models;
/// <summary>
/// A connection between a source and target activity via the source out port to the target in port.
/// </summary>
[Obsolete("Use Connection instead.")]
public class ObsoleteConnection
{
/// <summary>
/// Initializes a new instance of the <see cref="ObsoleteConnection"/> class.
/// </summary>
[JsonConstructor]
public ObsoleteConnection()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ObsoleteConnection"/> class.
/// </summary>
public ObsoleteConnection(IActivity source, IActivity target, string? sourcePort = default, string? targetPort = default)
{
Source = source;
Target = target;
SourcePort = sourcePort;
TargetPort = targetPort;
}
/// <summary>
/// The source activity.
/// </summary>
public IActivity Source { get; set; } = default!;
/// <summary>
/// The target activity.
/// </summary>
public IActivity Target { get; set; } = default!;
/// <summary>
/// The source port.
/// </summary>
public string? SourcePort { get; set; }
/// <summary>
/// The target port.
/// </summary>
public string? TargetPort { get; set; }
/// <summary>
/// Deconstructs the connection into its parts.
/// </summary>
public void Deconstruct(out IActivity source, out IActivity target, out string? sourcePort, out string? targetPort)
{
source = Source;
target = Target;
sourcePort = SourcePort;
targetPort = TargetPort;
}
}

View file

@ -5,43 +5,57 @@ using Elsa.Workflows.Core.Contracts;
namespace Elsa.Workflows.Core.Activities.Flowchart.Serialization;
/// <summary>
/// Converts <see cref="Connection"/> to and from JSON.
/// </summary>
public class ConnectionJsonConverter : JsonConverter<Connection>
{
private readonly IDictionary<string, IActivity> _activities;
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Connection);
/// <inheritdoc />
public ConnectionJsonConverter(IDictionary<string, IActivity> activities)
{
_activities = activities;
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);

View file

@ -35,16 +35,10 @@ public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
var metadataElement = doc.RootElement.TryGetProperty("metadata", out var metadataEl) ? metadataEl : default;
var metadata = metadataElement.ValueKind != JsonValueKind.Undefined ? metadataElement.Deserialize<IDictionary<string, object>>(options) ?? new Dictionary<string, object>() : new Dictionary<string, object>();
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<ICollection<Connection>>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List<Connection>()
: new List<Connection>();
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<Activities.Flowchart>
JsonSerializer.Serialize(writer, model, connectionSerializerOptions);
}
private static List<Connection> GetNotFoundConnections(JsonElement rootElement, JsonSerializerOptions connectionSerializerOptions, IEnumerable<IActivity> activities, IEnumerable<Connection> connections)
private static ICollection<Connection> GetNotFoundConnections(JsonElement rootElement, IDictionary<string, IActivity> activities, IEnumerable<Connection> 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<ICollection<Connection>>(connectionSerializerOptions)
?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List<Connection>()
: new List<Connection>();
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<NotFoundActivity>().ToList();
var notFoundActivityConnections = connections.Where(x => notFoundActivities.Contains(x.Source)).ToList();
var notFoundActivities = activities.Values.Where(x => x is NotFoundActivity).Cast<NotFoundActivity>().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<Activities.Flowchart>
{
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<Connection> DeserializeConnections(JsonElement connectionsElement, IDictionary<string, IActivity> 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<ICollection<ObsoleteConnection>>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List<ObsoleteConnection>()
: new List<ObsoleteConnection>();
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<ICollection<Connection>>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List<Connection>()
: new List<Connection>();
}
}

View file

@ -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;
/// <summary>
/// Converts <see cref="ObsoleteConnection"/> to and from JSON.
/// </summary>
[Obsolete("Use ConnectionJsonConverter instead.")]
public class ObsoleteConnectionJsonConverter : JsonConverter<ObsoleteConnection>
{
private readonly IDictionary<string, IActivity> _activities;
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(ObsoleteConnection);
/// <inheritdoc />
public ObsoleteConnectionJsonConverter(IDictionary<string, IActivity> activities)
{
_activities = activities;
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
}

View file

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