Refactor connection model (#4146)
This commit is contained in:
parent
02572e9ce2
commit
b356cdb52b
|
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
|
||||
namespace Elsa.Api.Client.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a trigger.
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue