From ae1748cee304ec4409468ab09503f31519d94848 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 29 Jun 2022 12:57:01 +0200 Subject: [PATCH] Implement flow nodes --- .../Elsa.WorkflowServer.Web/Program.cs | 3 +- .../flowchart/default-node-handler.ts | 13 ++++---- .../default-activity-template.tsx | 7 ++-- .../variable-editor-dialog.tsx | 2 +- .../src/models/core.ts | 8 ++++- .../plugins/switch/switch-port-provider.ts | 6 ++-- .../Endpoints/ActivityDescriptors/List.cs | 2 +- .../Flowchart/Activities/FlowDecision.cs | 31 ++++++++++++++++++ .../Flowchart/Activities/Flowchart.cs | 32 ++++++++++++------- .../Flowchart/Attributes/FlowNodeAttribute.cs | 12 +++++++ .../Activities/Flowchart/Models/Outcome.cs | 3 ++ .../ActivityExecutionContextExtensions.cs | 4 +-- .../Elsa.Workflows.Core/Models/Port.cs | 1 + .../Elsa.Workflows.Core/Models/PortMode.cs | 7 ++++ .../Signals/ActivityCompleted.cs | 2 +- .../Implementations/ActivityDescriber.cs | 17 ++++++++-- 16 files changed, 116 insertions(+), 34 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs create mode 100644 src/modules/Elsa.Workflows.Core/Activities/Flowchart/Attributes/FlowNodeAttribute.cs create mode 100644 src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Outcome.cs create mode 100644 src/modules/Elsa.Workflows.Core/Models/PortMode.cs diff --git a/src/bundles/Elsa.WorkflowServer.Web/Program.cs b/src/bundles/Elsa.WorkflowServer.Web/Program.cs index a86854c2a..538d801ee 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Program.cs +++ b/src/bundles/Elsa.WorkflowServer.Web/Program.cs @@ -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() .AddActivity() .AddActivity() + .AddActivity() .AddActivity() .AddActivity() .AddActivity() diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/default-node-handler.ts b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/default-node-handler.ts index a262e4b2f..3351e4c20 100644 --- a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/default-node-handler.ts +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/default-node-handler.ts @@ -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; diff --git a/src/designer/elsa-workflows-designer/src/components/designer/activity-templates/default-activity-template.tsx b/src/designer/elsa-workflows-designer/src/components/designer/activity-templates/default-activity-template.tsx index ca127b093..4c1526529 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/activity-templates/default-activity-template.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/activity-templates/default-activity-template.tsx @@ -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 (
- {ports.map(port => this.renderPort(activity, port))} + {embeddedPorts.map(port => this.renderPort(activity, port))}
); }; diff --git a/src/designer/elsa-workflows-designer/src/components/designer/variables-editor/variable-editor-dialog.tsx b/src/designer/elsa-workflows-designer/src/components/designer/variables-editor/variable-editor-dialog.tsx index a625ba44e..e40ae639f 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/variables-editor/variable-editor-dialog.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/variables-editor/variable-editor-dialog.tsx @@ -30,7 +30,7 @@ export class VariableEditorDialog { const cancelAction = DefaultActions.Cancel(); const saveAction = DefaultActions.Save(); const actions = [cancelAction, saveAction]; - const availableTypes: Array = ['Object', 'String', 'Int32', 'Int64', 'Single', 'Double']; // TODO: Fetch these from backend. + const availableTypes: Array = ['Object', 'String', 'Boolean', 'Int32', 'Int64', 'Single', 'Double']; // TODO: Fetch these from backend. const storageDrivers: Array = [{ id: null, displayName: '-' }, ...descriptorsStore.storageDrivers]; return ( diff --git a/src/designer/elsa-workflows-designer/src/models/core.ts b/src/designer/elsa-workflows-designer/src/models/core.ts index 2ccd8ed4b..85563fc8f 100644 --- a/src/designer/elsa-workflows-designer/src/models/core.ts +++ b/src/designer/elsa-workflows-designer/src/models/core.ts @@ -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; } diff --git a/src/designer/elsa-workflows-designer/src/plugins/switch/switch-port-provider.ts b/src/designer/elsa-workflows-designer/src/plugins/switch/switch-port-provider.ts index 61a6a4e9e..2abf21151 100644 --- a/src/designer/elsa-workflows-designer/src/plugins/switch/switch-port-provider.ts +++ b/src/designer/elsa-workflows-designer/src/plugins/switch/switch-port-provider.ts @@ -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 { - return [{name: 'In', displayName: 'In'}]; + return []; } public getOutboundPorts(context: PortProviderContext): Array { 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})); } } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List.cs b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List.cs index 2704ca0cc..2f42e760d 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityDescriptors/List.cs @@ -47,7 +47,7 @@ public class List : Controller new TypeJsonConverter(_wellKnownTypeRegistry) }, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; return Json(model, serializerOptions); diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs new file mode 100644 index 000000000..232406b4b --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs @@ -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(); + } + + /// + /// The condition to evaluate. + /// + [Input(UIHint = "single-line")] + public Input Condition { get; set; } = new(new Literal(false)); + + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var result = context.Get(Condition); + var outcome = result ? "True" : "False"; + + await context.CompleteActivityAsync(new Outcome(outcome)); + } +} \ No newline at end of file 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 2ecffe14c..a6d1bc11d 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -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 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(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Attributes/FlowNodeAttribute.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Attributes/FlowNodeAttribute.cs new file mode 100644 index 000000000..7944c3038 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Attributes/FlowNodeAttribute.cs @@ -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 Outcomes { get; } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Outcome.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Outcome.cs new file mode 100644 index 000000000..fc97fca4b --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Outcome.cs @@ -0,0 +1,3 @@ +namespace Elsa.Workflows.Core.Activities.Flowchart.Models; + +public record Outcome(string Name); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index c8dba78dd..6ecbe6f8b 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -144,10 +144,10 @@ public static class ActivityExecutionContextExtensions /// /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. /// - 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); diff --git a/src/modules/Elsa.Workflows.Core/Models/Port.cs b/src/modules/Elsa.Workflows.Core/Models/Port.cs index ca4625290..974f15f9a 100644 --- a/src/modules/Elsa.Workflows.Core/Models/Port.cs +++ b/src/modules/Elsa.Workflows.Core/Models/Port.cs @@ -4,4 +4,5 @@ public class Port { public string Name { get; set; } = default!; public string? DisplayName { get; set; } + public PortMode Mode { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/PortMode.cs b/src/modules/Elsa.Workflows.Core/Models/PortMode.cs new file mode 100644 index 000000000..db42f90d3 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Models/PortMode.cs @@ -0,0 +1,7 @@ +namespace Elsa.Workflows.Core.Models; + +public enum PortMode +{ + Embedded, + Port +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Signals/ActivityCompleted.cs b/src/modules/Elsa.Workflows.Core/Signals/ActivityCompleted.cs index 96c1d7947..9f0417af1 100644 --- a/src/modules/Elsa.Workflows.Core/Signals/ActivityCompleted.cs +++ b/src/modules/Elsa.Workflows.Core/Signals/ActivityCompleted.cs @@ -1,3 +1,3 @@ namespace Elsa.Workflows.Core.Signals; -public record ActivityCompleted; \ No newline at end of file +public record ActivityCompleted(object? Result = default); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs b/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs index 02024d6e7..1c0d5e9f8 100644 --- a/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs @@ -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(); 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).IsAssignableFrom(prop.PropertyType) let portAttr = prop.GetCustomAttribute() @@ -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(); + var flowPorts = flowNodeAttr?.Outcomes.Select(x => new Port + { + Mode = PortMode.Port, + Name = x, + DisplayName = x + }) ?? Enumerable.Empty(); + + var allPorts = embeddedPorts.Concat(flowPorts); var properties = activityType.GetProperties(); var inputProperties = properties.Where(x => typeof(Input).IsAssignableFrom(x.PropertyType) || x.GetCustomAttribute() != 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),