Implement flow nodes

This commit is contained in:
Sipke Schoorstra 2022-06-29 12:57:01 +02:00
parent 14026e7342
commit ae1748cee3
16 changed files with 116 additions and 34 deletions

View file

@ -1,5 +1,3 @@
using Elsa.AspNetCore;
using Elsa.AspNetCore.Conventions;
using Elsa.AspNetCore.Extensions;
using Elsa.Extensions;
using Elsa.Hangfire.Implementations;
@ -41,6 +39,7 @@ services
.AddActivity<If>()
.AddActivity<HttpEndpoint>()
.AddActivity<Flowchart>()
.AddActivity<FlowDecision>()
.AddActivity<Elsa.Scheduling.Activities.Delay>()
.AddActivity<Elsa.Scheduling.Activities.Timer>()
.AddActivity<ForEach>()

View file

@ -3,6 +3,7 @@ import {Node} from "@antv/x6";
import {Container, Service} from "typedi"
import {ActivityNodeHandler, CreateUINodeContext} from "./activity-node-handler";
import {PortProviderContext, PortProviderRegistry} from "../../../services";
import {PortMode} from "../../../models";
@Service()
export class DefaultNodeHandler implements ActivityNodeHandler {
@ -16,18 +17,18 @@ export class DefaultNodeHandler implements ActivityNodeHandler {
const {activityDescriptor, activity, x, y} = context;
const provider = this.portProviderRegistry.get(activityDescriptor.activityType);
const providerContext: PortProviderContext = {activityDescriptor, activity};
let inPorts = provider.getInboundPorts(providerContext);
let outPorts = provider.getOutboundPorts(providerContext);
let inPorts = provider.getInboundPorts(providerContext).filter(x => x.mode == PortMode.Port);
let outPorts = provider.getOutboundPorts(providerContext).filter(x => x.mode == PortMode.Port);
//if (inPorts.length == 0)
inPorts = [{name: 'In', displayName: 'In'}];
if (inPorts.length == 0)
inPorts = [{name: 'In', displayName: 'In', mode: PortMode.Port}];
if (inPorts.length == 1)
inPorts[0].displayName = null;
// In a flowchart, always add a Done port to connect the next node.
//outPorts = [...outPorts, {name: 'Done', displayName: 'Done'}];
outPorts = [{name: 'Done', displayName: 'Done'}];
if(outPorts.length == 0)
outPorts = [{name: 'Done', displayName: 'Done', mode: PortMode.Port}];
if (outPorts.length == 1)
outPorts[0].displayName = null;

View file

@ -2,7 +2,7 @@ import {Component, h, Prop, State, Event, EventEmitter, Listen, Element} from "@
import {camelCase} from 'lodash';
import {ActivityIcon, ActivityIconRegistry} from "../../../services";
import {Container} from "typedi";
import {Activity, ActivityDescriptor, ActivityKind, ActivitySelectedArgs, ChildActivitySelectedArgs, EditChildActivityArgs, Port} from "../../../models";
import {Activity, ActivityDescriptor, ActivityKind, ActivitySelectedArgs, ChildActivitySelectedArgs, EditChildActivityArgs, Port, PortMode} from "../../../models";
import descriptorsStore from "../../../data/descriptors-store";
import {isNullOrWhitespace} from "../../../utils";
import WorkflowEditorTunnel from "../state";
@ -107,13 +107,14 @@ export class DefaultActivityTemplate {
const activityDescriptor = this.activityDescriptor;
const ports = activityDescriptor?.ports ?? [];
const embeddedPorts = ports.filter(x => x.mode == PortMode.Embedded);
if (ports.length == 0)
if (embeddedPorts.length == 0)
return undefined;
return (
<div class="activity-ports mt-2 flex space-x-2">
{ports.map(port => this.renderPort(activity, port))}
{embeddedPorts.map(port => this.renderPort(activity, port))}
</div>
);
};

View file

@ -30,7 +30,7 @@ export class VariableEditorDialog {
const cancelAction = DefaultActions.Cancel();
const saveAction = DefaultActions.Save();
const actions = [cancelAction, saveAction];
const availableTypes: Array<string> = ['Object', 'String', 'Int32', 'Int64', 'Single', 'Double']; // TODO: Fetch these from backend.
const availableTypes: Array<string> = ['Object', 'String', 'Boolean', 'Int32', 'Int64', 'Single', 'Double']; // TODO: Fetch these from backend.
const storageDrivers: Array<StorageDriverDescriptor> = [{ id: null, displayName: '-' }, ...descriptorsStore.storageDrivers];
return (

View file

@ -109,6 +109,12 @@ export interface OutputDescriptor extends PropertyDescriptor {
export interface Port {
name: string;
displayName: string;
mode: PortMode;
}
export enum PortMode {
Embedded = 'Embedded',
Port = 'Port'
}
export interface CompletionCallbackState {
@ -142,7 +148,7 @@ export enum SyntaxNames {
Json = 'Json'
}
export interface ActivityMetadata {
export interface ActivityMetadata {
displayText: string;
designer: ActivityDesignerMetadata;
}

View file

@ -1,6 +1,6 @@
import 'reflect-metadata';
import {Service} from "typedi";
import {Port} from "../../models";
import {Port, PortMode} from "../../models";
import {SwitchActivity} from "./models";
import {PortProvider, PortProviderContext} from "../../services";
@ -8,13 +8,13 @@ import {PortProvider, PortProviderContext} from "../../services";
export class SwitchPortProvider implements PortProvider {
public getInboundPorts(context: PortProviderContext): Array<Port> {
return [{name: 'In', displayName: 'In'}];
return [];
}
public getOutboundPorts(context: PortProviderContext): Array<Port> {
const activity = context.activity as SwitchActivity;
const cases = activity.cases;
return cases.map(x => ({name: x.label, displayName: x.label}));
return cases.map(x => ({name: x.label, displayName: x.label, mode: PortMode.Embedded}));
}
}

View file

@ -47,7 +47,7 @@ public class List : Controller
new TypeJsonConverter(_wellKnownTypeRegistry)
},
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
return Json(model, serializerOptions);

View file

@ -0,0 +1,31 @@
using Elsa.Expressions.Models;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Activities.Flowchart.Models;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Behaviors;
using Elsa.Workflows.Core.Models;
namespace Elsa.Workflows.Core.Activities.Flowchart.Activities;
[FlowNode("True", "False")]
public class FlowDecision : Activity
{
public FlowDecision()
{
Behaviors.Remove<AutoCompleteBehavior>();
}
/// <summary>
/// The condition to evaluate.
/// </summary>
[Input(UIHint = "single-line")]
public Input<bool> Condition { get; set; } = new(new Literal<bool>(false));
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var result = context.Get(Condition);
var outcome = result ? "True" : "False";
await context.CompleteActivityAsync(new Outcome(outcome));
}
}

View file

@ -27,29 +27,39 @@ public class Flowchart : Container
context.ScheduleActivity(Start);
}
private async ValueTask OnDescendantCompletedAsync(ActivityCompleted signal, SignalContext context)
{
await ScheduleChildrenAsync(context.ReceiverActivityExecutionContext, context.SenderActivityExecutionContext.Activity);
await ScheduleChildrenAsync(signal, context);
}
private async Task ScheduleChildrenAsync(ActivityExecutionContext context, IActivity parent)
private async Task ScheduleChildrenAsync(ActivityCompleted signal, SignalContext context)
{
var activityExecutionContext = context.ReceiverActivityExecutionContext;
var parent = context.SenderActivityExecutionContext.Activity;
if (parent == null!)
return;
// Is the activity a direct child?
var isDirectChild = Activities.Contains(parent);
// Ignore completed activities that are not immediate children.
var isDirectChild = Activities.Contains(parent);
if (!isDirectChild)
return;
var outboundConnections = Connections.Where(x => x.Source == parent).ToList();
// If a specific outcome was provided by the completed activity, use it to find the connection to the next activity.
Func<Connection, bool> outboundConnectionsQuery = signal.Result is Outcome outcome
? connection => connection.Source == parent && connection.SourcePort == outcome.Name
: connection => connection.Source == parent;
var outboundConnections = Connections.Where(outboundConnectionsQuery).ToList();
var children = outboundConnections.Select(x => x.Target).ToList();
if(children.Any())
context.ScheduleActivities(children);
if (children.Any())
activityExecutionContext.ScheduleActivities(children);
else
await context.CompleteActivityAsync();
await activityExecutionContext.CompleteActivityAsync();
context.StopPropagation();
}
}

View file

@ -0,0 +1,12 @@
namespace Elsa.Workflows.Core.Activities.Flowchart.Attributes;
[AttributeUsage(AttributeTargets.Class)]
public class FlowNodeAttribute : Attribute
{
public FlowNodeAttribute(params string[] outcomes)
{
Outcomes = outcomes;
}
public ICollection<string> Outcomes { get; }
}

View file

@ -0,0 +1,3 @@
namespace Elsa.Workflows.Core.Activities.Flowchart.Models;
public record Outcome(string Name);

View file

@ -144,10 +144,10 @@ public static class ActivityExecutionContextExtensions
/// <summary>
/// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion.
/// </summary>
public static async ValueTask CompleteActivityAsync(this ActivityExecutionContext context)
public static async ValueTask CompleteActivityAsync(this ActivityExecutionContext context, object? result = default)
{
// Send a signal.
await context.SignalAsync(new ActivityCompleted());
await context.SignalAsync(new ActivityCompleted(result));
// Remove the context.
context.WorkflowExecutionContext.ActivityExecutionContexts.Remove(context);

View file

@ -4,4 +4,5 @@ public class Port
{
public string Name { get; set; } = default!;
public string? DisplayName { get; set; }
public PortMode Mode { get; set; }
}

View file

@ -0,0 +1,7 @@
namespace Elsa.Workflows.Core.Models;
public enum PortMode
{
Embedded,
Port
}

View file

@ -1,3 +1,3 @@
namespace Elsa.Workflows.Core.Signals;
public record ActivityCompleted;
public record ActivityCompleted(object? Result = default);

View file

@ -1,6 +1,7 @@
using System.Collections;
using System.ComponentModel;
using System.Reflection;
using Elsa.Workflows.Core.Activities.Flowchart.Attributes;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Helpers;
using Elsa.Workflows.Core.Models;
@ -39,7 +40,7 @@ public class ActivityDescriber : IActivityDescriber
var descriptionAttr = activityType.GetCustomAttribute<DescriptionAttribute>();
var description = descriptionAttr?.Description ?? activityAttr?.Description;
var outboundPorts =
var embeddedPorts =
from prop in activityType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
where typeof(IActivity).IsAssignableFrom(prop.PropertyType) || typeof(IEnumerable<IActivity>).IsAssignableFrom(prop.PropertyType)
let portAttr = prop.GetCustomAttribute<PortAttribute>()
@ -47,9 +48,19 @@ public class ActivityDescriber : IActivityDescriber
select new Port
{
Name = portAttr.Name ?? prop.Name,
DisplayName = portAttr.DisplayName ?? portAttr.Name ?? prop.Name
DisplayName = portAttr.DisplayName ?? portAttr.Name ?? prop.Name,
Mode = PortMode.Embedded
};
var flowNodeAttr = activityType.GetCustomAttribute<FlowNodeAttribute>();
var flowPorts = flowNodeAttr?.Outcomes.Select(x => new Port
{
Mode = PortMode.Port,
Name = x,
DisplayName = x
}) ?? Enumerable.Empty<Port>();
var allPorts = embeddedPorts.Concat(flowPorts);
var properties = activityType.GetProperties();
var inputProperties = properties.Where(x => typeof(Input).IsAssignableFrom(x.PropertyType) || x.GetCustomAttribute<InputAttribute>() != null).ToList();
var outputProperties = properties.Where(x => typeof(Output).IsAssignableFrom(x.PropertyType)).DistinctBy(x => x.Name).ToList();
@ -62,7 +73,7 @@ public class ActivityDescriber : IActivityDescriber
ActivityType = fullTypeName,
DisplayName = displayName,
Kind = isTrigger ? ActivityKind.Trigger : ActivityKind.Action,
Ports = outboundPorts.ToList(),
Ports = allPorts.ToList(),
Inputs = DescribeInputProperties(inputProperties).ToList(),
Outputs = DescribeOutputProperties(outputProperties).ToList(),
IsContainer = typeof(IContainer).IsAssignableFrom(activityType),