Introduce ActivityBase

This commit is contained in:
Sipke Schoorstra 2022-06-29 15:36:27 +02:00
parent ae1748cee3
commit 20002e2454
19 changed files with 181 additions and 108 deletions

View file

@ -96,7 +96,8 @@ export class ToolboxActivities {
}
handleActivityDescriptorsChanged(value: Array<ActivityDescriptor>) {
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<string, string> = new Map<string, string>();
@ -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});

View file

@ -0,0 +1,8 @@
import {FunctionalComponent, h} from '@stencil/core';
import {ActivityIconSettings, getActivityIconCssClass} from "./models";
export const FlowDecisionIcon: FunctionalComponent<ActivityIconSettings> = (settings) => (
<svg class={getActivityIconCssClass(settings)} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
);

View file

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

View file

@ -82,6 +82,7 @@ export interface ActivityDescriptor {
kind: ActivityKind;
ports: Array<Port>;
isContainer: boolean;
isBrowsable: boolean;
}
export interface PropertyDescriptor {

View file

@ -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 => <ForEachIcon size={settings?.size}/>);
this.add('Elsa.Delay', settings => <DelayIcon size={settings?.size}/>);
this.add('Elsa.Timer', settings => <TimerIcon size={settings?.size}/>);
this.add('Elsa.FlowDecision', settings => <FlowDecisionIcon size={settings?.size}/>);
this.add('Elsa.Event', settings => <EventIcon size={settings?.size}/>);
this.add('Elsa.RunJavaScript', settings => <RunJavaScriptIcon size={settings?.size}/>);
}

View file

@ -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<AutoCompleteBehavior>();
}
/// <summary>
/// The condition to evaluate.
/// </summary>

View file

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

View file

@ -9,11 +9,13 @@ public class ConnectionJsonConverter : JsonConverter<Connection>
{
private readonly IDictionary<string, IActivity> _activities;
public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Connection);
public ConnectionJsonConverter(IDictionary<string, IActivity> 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<Connection>
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<Connection>
public override void Write(Utf8JsonWriter writer, Connection value, JsonSerializerOptions options)
{
var (activity, target, sourcePort, targetPort) = value;
var model = new
{
Source = activity.Id,

View file

@ -27,7 +27,9 @@ public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
var activityDictionary = activities.ToDictionary(x => x.Id);
connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary));
var connections = connectionsElement.ValueKind != JsonValueKind.Undefined ? connectionsElement.Deserialize<ICollection<Connection>>(connectionSerializerOptions) ?? new List<Connection>() : new List<Connection>();
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 flowChart = new Activities.Flowchart
{

View file

@ -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;
}
/// <summary>
/// The type name of the missing activity.
/// </summary>
public string MissingTypeName { get; set; } = default!;
}

View file

@ -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<SignalHandlerRegistration> _signalHandlers = new List<SignalHandlerRegistration>();
protected Activity()
{
TypeName = ActivityTypeNameHelper.GenerateTypeName(GetType());
Behaviors.Add<ExecutionLoggingBehavior>(this);
Behaviors.Add<ScheduledChildCallbackBehavior>(this);
Behaviors.Add<AutoCompleteBehavior>(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<string, object> ApplicationProperties { get; set; } = new Dictionary<string, object>();
public IDictionary<string, object> Metadata { get; set; } = new Dictionary<string, object>();
/// <summary>
/// A collection of reusable behaviors to add to this activity.
/// </summary>
[JsonIgnore]
public ICollection<IBehavior> Behaviors { get; } = new List<IBehavior>();
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)
{
}
/// <summary>
/// Notify the sytem that this activity completed.
/// </summary>
protected async ValueTask CompleteAsync(ActivityExecutionContext context)
{
await context.CompleteActivityAsync();
}
protected void OnSignalReceived(Type signalType, Func<object, SignalContext, ValueTask> handler) => _signalHandlers.Add(new SignalHandlerRegistration(signalType, handler));
protected void OnSignalReceived<T>(Func<T, SignalContext, ValueTask> handler) => OnSignalReceived(typeof(T), (signal, context) => handler((T)signal, context));
protected void OnSignalReceived<T>(Action<T, SignalContext> handler)
{
OnSignalReceived<T>((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<T> : ActivityWithResult
get => base.Result as Output<T>;
set => base.Result = value;
}
}
internal record SignalHandlerRegistration(Type SignalType, Func<object, SignalContext, ValueTask> Handler);
}

View file

@ -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<SignalHandlerRegistration> _signalHandlers = new List<SignalHandlerRegistration>();
protected ActivityBase()
{
TypeName = ActivityTypeNameHelper.GenerateTypeName(GetType());
Behaviors.Add<ExecutionLoggingBehavior>(this);
Behaviors.Add<ScheduledChildCallbackBehavior>(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<string, object> ApplicationProperties { get; set; } = new Dictionary<string, object>();
public IDictionary<string, object> Metadata { get; set; } = new Dictionary<string, object>();
/// <summary>
/// A collection of reusable behaviors to add to this activity.
/// </summary>
[JsonIgnore]
public ICollection<IBehavior> Behaviors { get; } = new List<IBehavior>();
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)
{
}
/// <summary>
/// Notify the sytem that this activity completed.
/// </summary>
protected async ValueTask CompleteAsync(ActivityExecutionContext context)
{
await context.CompleteActivityAsync();
}
protected void OnSignalReceived(Type signalType, Func<object, SignalContext, ValueTask> handler) => _signalHandlers.Add(new SignalHandlerRegistration(signalType, handler));
protected void OnSignalReceived<T>(Func<T, SignalContext, ValueTask> handler) => OnSignalReceived(typeof(T), (signal, context) => handler((T)signal, context));
protected void OnSignalReceived<T>(Action<T, SignalContext> handler)
{
OnSignalReceived<T>((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);
}
}

View file

@ -20,6 +20,11 @@ public class ActivityDescriptor
/// A value indicating whether this activity is a container of child activities.
/// </summary>
public bool IsContainer { get; set; }
/// <summary>
/// Whether this activity type is selectable from activity pickers.
/// </summary>
public bool IsBrowsable { get; set; }
}
public record ActivityConstructorContext(JsonElement Element, JsonSerializerOptions SerializerOptions);

View file

@ -0,0 +1,5 @@
using Elsa.Workflows.Core.Services;
namespace Elsa.Workflows.Core.Models;
internal record SignalHandlerRegistration(Type SignalType, Func<object, SignalContext, ValueTask> Handler);

View file

@ -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<WorkflowManagementFeature>? configure = default)
{
module.Configure(configure);
module.Configure<WorkflowManagementFeature>(management =>
{
management.AddActivity<NotFoundActivity>();
configure?.Invoke(management);
});
return module;
}
}

View file

@ -65,6 +65,7 @@ public class ActivityDescriber : IActivityDescriber
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();
var isTrigger = activityType.IsAssignableTo(typeof(ITrigger));
var browsableAttr = activityType.GetCustomAttribute<BrowsableAttribute>();
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);

View file

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

View file

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

View file

@ -42,6 +42,10 @@ public class DefaultWorkflowInvoker : IWorkflowInvoker
public async Task<InvokeWorkflowResult> 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);
}