diff --git a/src/designer/elsa-workflows-designer/src/components/designer/workflow-definition-editor/toolbox-activities.tsx b/src/designer/elsa-workflows-designer/src/components/designer/workflow-definition-editor/toolbox-activities.tsx index 5e91271cd..7b4c076af 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/workflow-definition-editor/toolbox-activities.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/workflow-definition-editor/toolbox-activities.tsx @@ -96,7 +96,8 @@ export class ToolboxActivities { } handleActivityDescriptorsChanged(value: Array) { - const categorizedActivitiesLookup = groupBy(value, x => x.category); + const browsableDescriptors = value.filter(x => x.isBrowsable); + const categorizedActivitiesLookup = groupBy(browsableDescriptors, x => x.category); const categories = Object.keys(categorizedActivitiesLookup); const renderedActivities: Map = new Map(); @@ -114,7 +115,7 @@ export class ToolboxActivities { // Render activities. const activityDriverRegistry = Container.get(ActivityDriverRegistry); - for (const activityDescriptor of value) { + for (const activityDescriptor of browsableDescriptors) { const activityType = activityDescriptor.activityType; const driver = activityDriverRegistry.createDriver(activityType); const html = driver.display({displayType: 'picker', activityDescriptor: activityDescriptor}); diff --git a/src/designer/elsa-workflows-designer/src/components/icons/activities/flow-decision.tsx b/src/designer/elsa-workflows-designer/src/components/icons/activities/flow-decision.tsx new file mode 100644 index 000000000..0e8c16155 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/icons/activities/flow-decision.tsx @@ -0,0 +1,8 @@ +import {FunctionalComponent, h} from '@stencil/core'; +import {ActivityIconSettings, getActivityIconCssClass} from "./models"; + +export const FlowDecisionIcon: FunctionalComponent = (settings) => ( + + + +); diff --git a/src/designer/elsa-workflows-designer/src/components/icons/activities/index.tsx b/src/designer/elsa-workflows-designer/src/components/icons/activities/index.tsx index e356c67d1..5304f0bb2 100644 --- a/src/designer/elsa-workflows-designer/src/components/icons/activities/index.tsx +++ b/src/designer/elsa-workflows-designer/src/components/icons/activities/index.tsx @@ -3,6 +3,7 @@ export * from "./http-endpoint"; export * from "./write-line"; export * from "./read-line"; export * from "./if"; +export * from "./flow-decision"; export * from "./flowchart"; export * from "./foreach"; export * from "./delay"; diff --git a/src/designer/elsa-workflows-designer/src/models/core.ts b/src/designer/elsa-workflows-designer/src/models/core.ts index 85563fc8f..e4a0b1dac 100644 --- a/src/designer/elsa-workflows-designer/src/models/core.ts +++ b/src/designer/elsa-workflows-designer/src/models/core.ts @@ -82,6 +82,7 @@ export interface ActivityDescriptor { kind: ActivityKind; ports: Array; isContainer: boolean; + isBrowsable: boolean; } export interface PropertyDescriptor { diff --git a/src/designer/elsa-workflows-designer/src/services/activity-icon-registry.tsx b/src/designer/elsa-workflows-designer/src/services/activity-icon-registry.tsx index 5176cd407..ac6896495 100644 --- a/src/designer/elsa-workflows-designer/src/services/activity-icon-registry.tsx +++ b/src/designer/elsa-workflows-designer/src/services/activity-icon-registry.tsx @@ -5,7 +5,7 @@ import {Service} from "typedi"; import { DelayIcon, EventIcon, - FlowchartIcon, + FlowchartIcon, FlowDecisionIcon, ForEachIcon, HttpEndpointIcon, IfIcon, @@ -32,6 +32,7 @@ export class ActivityIconRegistry { this.add('Elsa.ForEach', settings => ); this.add('Elsa.Delay', settings => ); this.add('Elsa.Timer', settings => ); + this.add('Elsa.FlowDecision', settings => ); this.add('Elsa.Event', settings => ); this.add('Elsa.RunJavaScript', settings => ); } diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs index 232406b4b..b38ef9f9a 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs @@ -2,19 +2,14 @@ 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 +[Activity("Elsa", "Flow", "Continues on either the True or False branch, depending on the result of a specified condition")] +public class FlowDecision : ActivityBase { - public FlowDecision() - { - Behaviors.Remove(); - } - /// /// The condition to evaluate. /// 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 a6d1bc11d..72681545d 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -6,7 +6,7 @@ using Elsa.Workflows.Core.Signals; namespace Elsa.Workflows.Core.Activities.Flowchart.Activities; -[Activity("Elsa", "Workflows", "A flowchart is a collection of activities and connections between them.")] +[Activity("Elsa", "Flow", "A flowchart is a collection of activities and connections between them.")] public class Flowchart : Container { public Flowchart() diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs index d51dc12d3..ec1259527 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/ConnectionJsonConverter.cs @@ -9,11 +9,13 @@ public class ConnectionJsonConverter : JsonConverter { private readonly IDictionary _activities; + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Connection); + public ConnectionJsonConverter(IDictionary activities) { _activities = activities; } - + public override Connection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (!JsonDocument.TryParseValue(ref reader, out var doc)) @@ -24,8 +26,8 @@ public class ConnectionJsonConverter : JsonConverter var sourcePort = doc.RootElement.GetProperty("sourcePort").GetString()!; var targetPort = doc.RootElement.GetProperty("targetPort").GetString()!; - var source = _activities[sourceId]; - var target = _activities[targetId]; + 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); } @@ -33,7 +35,7 @@ public class ConnectionJsonConverter : JsonConverter public override void Write(Utf8JsonWriter writer, Connection value, JsonSerializerOptions options) { var (activity, target, sourcePort, targetPort) = value; - + var model = new { Source = activity.Id, diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs index 4935d7599..7f79834f7 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs @@ -27,7 +27,9 @@ public class FlowchartJsonConverter : JsonConverter var activityDictionary = activities.ToDictionary(x => x.Id); connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary)); - var connections = connectionsElement.ValueKind != JsonValueKind.Undefined ? connectionsElement.Deserialize>(connectionSerializerOptions) ?? new List() : new List(); + var connections = connectionsElement.ValueKind != JsonValueKind.Undefined + ? connectionsElement.Deserialize>(connectionSerializerOptions)?.Where(x => x.Source != null! && x.Target != null!).ToList() ?? new List() + : new List(); var flowChart = new Activities.Flowchart { diff --git a/src/modules/Elsa.Workflows.Core/Activities/NotFoundActivity.cs b/src/modules/Elsa.Workflows.Core/Activities/NotFoundActivity.cs new file mode 100644 index 000000000..473cf0f2e --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/NotFoundActivity.cs @@ -0,0 +1,22 @@ +using System.ComponentModel; +using Elsa.Workflows.Core.Models; + +namespace Elsa.Workflows.Core.Activities; + +[Browsable(false)] +public class NotFoundActivity : Activity +{ + public NotFoundActivity() + { + } + + public NotFoundActivity(string missingTypeName) + { + MissingTypeName = missingTypeName; + } + + /// + /// The type name of the missing activity. + /// + public string MissingTypeName { get; set; } = default!; +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/Activity.cs b/src/modules/Elsa.Workflows.Core/Models/Activity.cs index 3c5fde162..bf21f1eaf 100644 --- a/src/modules/Elsa.Workflows.Core/Models/Activity.cs +++ b/src/modules/Elsa.Workflows.Core/Models/Activity.cs @@ -1,21 +1,12 @@ -using System.Linq.Expressions; -using System.Text.Json.Serialization; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Behaviors; -using Elsa.Workflows.Core.Helpers; -using Elsa.Workflows.Core.Services; namespace Elsa.Workflows.Core.Models; -public abstract class Activity : IActivity, ISignalHandler +public abstract class Activity : ActivityBase { - private readonly ICollection _signalHandlers = new List(); - protected Activity() { - TypeName = ActivityTypeNameHelper.GenerateTypeName(GetType()); - Behaviors.Add(this); - Behaviors.Add(this); Behaviors.Add(this); } @@ -23,82 +14,6 @@ public abstract class Activity : IActivity, ISignalHandler { TypeName = activityType; } - - public string Id { get; set; } = default!; - public string TypeName { get; set; } - public bool CanStartWorkflow { get; set; } - public IDictionary ApplicationProperties { get; set; } = new Dictionary(); - public IDictionary Metadata { get; set; } = new Dictionary(); - - /// - /// A collection of reusable behaviors to add to this activity. - /// - [JsonIgnore] - public ICollection Behaviors { get; } = new List(); - - protected virtual ValueTask ExecuteAsync(ActivityExecutionContext context) - { - Execute(context); - return ValueTask.CompletedTask; - } - - protected virtual ValueTask OnSignalReceivedAsync(object signal, SignalContext context) - { - OnSignalReceived(signal, context); - return ValueTask.CompletedTask; - } - - protected virtual void OnSignalReceived(object signal, SignalContext context) - { - } - - protected virtual void Execute(ActivityExecutionContext context) - { - } - - /// - /// Notify the sytem that this activity completed. - /// - protected async ValueTask CompleteAsync(ActivityExecutionContext context) - { - await context.CompleteActivityAsync(); - } - - protected void OnSignalReceived(Type signalType, Func handler) => _signalHandlers.Add(new SignalHandlerRegistration(signalType, handler)); - protected void OnSignalReceived(Func handler) => OnSignalReceived(typeof(T), (signal, context) => handler((T)signal, context)); - - protected void OnSignalReceived(Action handler) - { - OnSignalReceived((signal, context) => - { - handler(signal, context); - return ValueTask.CompletedTask; - }); - } - - async ValueTask IActivity.ExecuteAsync(ActivityExecutionContext context) - { - await ExecuteAsync(context); - - // Invoke behaviors. - foreach (var behavior in Behaviors) await behavior.ExecuteAsync(context); - } - - async ValueTask ISignalHandler.HandleSignalAsync(object signal, SignalContext context) - { - // Give derived activity a chance to do something with the signal. - await OnSignalReceivedAsync(signal, context); - - // Invoke registered signal delegates for this particular type of signal. - var signalType = signal.GetType(); - var handlers = _signalHandlers.Where(x => x.SignalType == signalType); - - foreach (var registration in handlers) - await registration.Handler(signal, context); - - // Invoke behaviors. - foreach (var behavior in Behaviors) await behavior.HandleSignalAsync(signal, context); - } } public abstract class ActivityWithResult : Activity @@ -153,6 +68,4 @@ public abstract class Activity : ActivityWithResult get => base.Result as Output; set => base.Result = value; } -} - -internal record SignalHandlerRegistration(Type SignalType, Func Handler); \ No newline at end of file +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityBase.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityBase.cs new file mode 100644 index 000000000..c7c6b18bb --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityBase.cs @@ -0,0 +1,99 @@ +using System.Text.Json.Serialization; +using Elsa.Workflows.Core.Behaviors; +using Elsa.Workflows.Core.Helpers; +using Elsa.Workflows.Core.Services; + +namespace Elsa.Workflows.Core.Models; + +public abstract class ActivityBase : IActivity, ISignalHandler +{ + private readonly ICollection _signalHandlers = new List(); + + protected ActivityBase() + { + TypeName = ActivityTypeNameHelper.GenerateTypeName(GetType()); + Behaviors.Add(this); + Behaviors.Add(this); + } + + protected ActivityBase(string activityType) : this() + { + TypeName = activityType; + } + + public string Id { get; set; } = default!; + public string TypeName { get; set; } + public bool CanStartWorkflow { get; set; } + public IDictionary ApplicationProperties { get; set; } = new Dictionary(); + public IDictionary Metadata { get; set; } = new Dictionary(); + + /// + /// A collection of reusable behaviors to add to this activity. + /// + [JsonIgnore] + public ICollection Behaviors { get; } = new List(); + + protected virtual ValueTask ExecuteAsync(ActivityExecutionContext context) + { + Execute(context); + return ValueTask.CompletedTask; + } + + protected virtual ValueTask OnSignalReceivedAsync(object signal, SignalContext context) + { + OnSignalReceived(signal, context); + return ValueTask.CompletedTask; + } + + protected virtual void OnSignalReceived(object signal, SignalContext context) + { + } + + protected virtual void Execute(ActivityExecutionContext context) + { + } + + /// + /// Notify the sytem that this activity completed. + /// + protected async ValueTask CompleteAsync(ActivityExecutionContext context) + { + await context.CompleteActivityAsync(); + } + + protected void OnSignalReceived(Type signalType, Func handler) => _signalHandlers.Add(new SignalHandlerRegistration(signalType, handler)); + protected void OnSignalReceived(Func handler) => OnSignalReceived(typeof(T), (signal, context) => handler((T)signal, context)); + + protected void OnSignalReceived(Action handler) + { + OnSignalReceived((signal, context) => + { + handler(signal, context); + return ValueTask.CompletedTask; + }); + } + + async ValueTask IActivity.ExecuteAsync(ActivityExecutionContext context) + { + await ExecuteAsync(context); + + // Invoke behaviors. + foreach (var behavior in Behaviors) await behavior.ExecuteAsync(context); + } + + async ValueTask ISignalHandler.HandleSignalAsync(object signal, SignalContext context) + { + // Give derived activity a chance to do something with the signal. + await OnSignalReceivedAsync(signal, context); + + // Invoke registered signal delegates for this particular type of signal. + var signalType = signal.GetType(); + var handlers = _signalHandlers.Where(x => x.SignalType == signalType); + + foreach (var registration in handlers) + await registration.Handler(signal, context); + + // Invoke behaviors. + foreach (var behavior in Behaviors) await behavior.HandleSignalAsync(signal, context); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs index e5fdcc4df..3f47b8f13 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs @@ -20,6 +20,11 @@ public class ActivityDescriptor /// A value indicating whether this activity is a container of child activities. /// public bool IsContainer { get; set; } + + /// + /// Whether this activity type is selectable from activity pickers. + /// + public bool IsBrowsable { get; set; } } public record ActivityConstructorContext(JsonElement Element, JsonSerializerOptions SerializerOptions); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/SignalHandlerRegistration.cs b/src/modules/Elsa.Workflows.Core/Models/SignalHandlerRegistration.cs new file mode 100644 index 000000000..a23baebee --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Models/SignalHandlerRegistration.cs @@ -0,0 +1,5 @@ +using Elsa.Workflows.Core.Services; + +namespace Elsa.Workflows.Core.Models; + +internal record SignalHandlerRegistration(Type SignalType, Func Handler); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Extensions/DependencyInjectionExtensions.cs b/src/modules/Elsa.Workflows.Management/Extensions/DependencyInjectionExtensions.cs index 1592afb8b..735e62f2d 100644 --- a/src/modules/Elsa.Workflows.Management/Extensions/DependencyInjectionExtensions.cs +++ b/src/modules/Elsa.Workflows.Management/Extensions/DependencyInjectionExtensions.cs @@ -1,4 +1,5 @@ using Elsa.Features.Services; +using Elsa.Workflows.Core.Activities; using Elsa.Workflows.Management.Features; namespace Elsa.Workflows.Management.Extensions; @@ -7,7 +8,11 @@ public static class DependencyInjectionExtensions { public static IModule UseManagement(this IModule module, Action? configure = default) { - module.Configure(configure); + module.Configure(management => + { + management.AddActivity(); + configure?.Invoke(management); + }); return module; } } \ 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 1c0d5e9f8..b05a47530 100644 --- a/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs @@ -65,6 +65,7 @@ public class ActivityDescriber : IActivityDescriber 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(); var isTrigger = activityType.IsAssignableTo(typeof(ITrigger)); + var browsableAttr = activityType.GetCustomAttribute(); var descriptor = new ActivityDescriptor { @@ -77,6 +78,7 @@ public class ActivityDescriber : IActivityDescriber Inputs = DescribeInputProperties(inputProperties).ToList(), Outputs = DescribeOutputProperties(outputProperties).ToList(), IsContainer = typeof(IContainer).IsAssignableFrom(activityType), + IsBrowsable = browsableAttr == null || browsableAttr.Browsable, Constructor = context => { var activity = _activityFactory.Create(activityType, context); diff --git a/src/modules/Elsa.Workflows.Management/Implementations/ActivityFactory.cs b/src/modules/Elsa.Workflows.Management/Implementations/ActivityFactory.cs index 303c70df9..e0cf51018 100644 --- a/src/modules/Elsa.Workflows.Management/Implementations/ActivityFactory.cs +++ b/src/modules/Elsa.Workflows.Management/Implementations/ActivityFactory.cs @@ -1,7 +1,6 @@ using System.Text.Json; using Elsa.Workflows.Core.Models; using Elsa.Workflows.Core.Services; -using Elsa.Workflows.Management.Models; using Elsa.Workflows.Management.Services; namespace Elsa.Workflows.Management.Implementations; diff --git a/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverter.cs b/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverter.cs index d577cd271..ee7929b39 100644 --- a/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverter.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Elsa.Workflows.Core.Activities; using Elsa.Workflows.Core.Models; using Elsa.Workflows.Core.Services; using Elsa.Workflows.Management.Models; @@ -33,7 +34,14 @@ public class ActivityJsonConverter : JsonConverter var activityDescriptor = _activityRegistry.Find(activityTypeName); if (activityDescriptor == null) - throw new Exception($"Activity of type {activityTypeName} not found in registry"); + { + var activityId = doc.RootElement.TryGetProperty("Id", out var activityIdElement) ? activityIdElement.GetString() : default; + + return new NotFoundActivity(activityTypeName) + { + Id = activityId ?? Guid.NewGuid().ToString("N"), + }; + } var newOptions = new JsonSerializerOptions(options); newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider)); diff --git a/src/modules/Elsa.Workflows.Runtime/Implementations/DefaultWorkflowInvoker.cs b/src/modules/Elsa.Workflows.Runtime/Implementations/DefaultWorkflowInvoker.cs index 8aab3a875..32992a180 100644 --- a/src/modules/Elsa.Workflows.Runtime/Implementations/DefaultWorkflowInvoker.cs +++ b/src/modules/Elsa.Workflows.Runtime/Implementations/DefaultWorkflowInvoker.cs @@ -42,6 +42,10 @@ public class DefaultWorkflowInvoker : IWorkflowInvoker public async Task InvokeAsync(InvokeWorkflowInstanceRequest request, CancellationToken cancellationToken = default) { var workflowInstance = await _workflowInstanceStore.FindByIdAsync(request.InstanceId, cancellationToken); + + if (workflowInstance == null) + throw new Exception($"No workflow instance found with ID {request.InstanceId}"); + return await InvokeAsync(workflowInstance!, request.Bookmark, request.Input, cancellationToken); }