diff --git a/Elsa.sln b/Elsa.sln index cf1afb58e..463676714 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -185,6 +185,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.DistributedLocking.Sql EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.Timers", "src\activities\Elsa.Activities.Timers\Elsa.Activities.Timers.csproj", "{74E83393-50AF-41E3-A1C8-97202673CD85}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElsaDashboard.Abstractions", "src\dashboards\blazor\ElsaDashboard.Abstractions\ElsaDashboard.Abstractions.csproj", "{8FC11EB7-38FE-445C-BE58-4D96BB14AAB4}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.Rebus", "src\activities\Elsa.Activities.Rebus\Elsa.Activities.Rebus.csproj", "{A9843C3F-74F7-4282-A7B7-841F6B8EB12D}" EndProject Global @@ -445,6 +446,10 @@ Global {74E83393-50AF-41E3-A1C8-97202673CD85}.Debug|Any CPU.Build.0 = Debug|Any CPU {74E83393-50AF-41E3-A1C8-97202673CD85}.Release|Any CPU.ActiveCfg = Release|Any CPU {74E83393-50AF-41E3-A1C8-97202673CD85}.Release|Any CPU.Build.0 = Release|Any CPU + {8FC11EB7-38FE-445C-BE58-4D96BB14AAB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FC11EB7-38FE-445C-BE58-4D96BB14AAB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FC11EB7-38FE-445C-BE58-4D96BB14AAB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FC11EB7-38FE-445C-BE58-4D96BB14AAB4}.Release|Any CPU.Build.0 = Release|Any CPU {A9843C3F-74F7-4282-A7B7-841F6B8EB12D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A9843C3F-74F7-4282-A7B7-841F6B8EB12D}.Debug|Any CPU.Build.0 = Debug|Any CPU {A9843C3F-74F7-4282-A7B7-841F6B8EB12D}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -535,6 +540,7 @@ Global {C90CD39C-8CDD-4BCB-AB6A-CF981D468DD2} = {7CD5C8D5-EC78-4A99-A514-F01CA7197AC8} {78F2A9DC-3092-4371-ABFE-87C5DA4DE351} = {7CD5C8D5-EC78-4A99-A514-F01CA7197AC8} {74E83393-50AF-41E3-A1C8-97202673CD85} = {37D2E628-5CC1-4C8E-8C02-3B0A29E2ACAA} + {8FC11EB7-38FE-445C-BE58-4D96BB14AAB4} = {E02BD53A-5B08-47A0-920E-DD537A6AFAAA} {A9843C3F-74F7-4282-A7B7-841F6B8EB12D} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/clients/Elsa.Client/Models/ActivityBlueprint.cs b/src/clients/Elsa.Client/Models/ActivityBlueprint.cs index 6da699f1c..1cdf7bc8a 100644 --- a/src/clients/Elsa.Client/Models/ActivityBlueprint.cs +++ b/src/clients/Elsa.Client/Models/ActivityBlueprint.cs @@ -11,5 +11,6 @@ namespace Elsa.Client.Models [DataMember(Order = 4)] public bool PersistWorkflow { get; set; } [DataMember(Order = 5)] public bool LoadWorkflowContext { get; set; } [DataMember(Order = 6)] public bool SaveWorkflowContext { get; set; } + [DataMember(Order = 7)] public Variables Properties { get; set; } = new(); } } \ No newline at end of file diff --git a/src/clients/Elsa.Client/Models/Variables.cs b/src/clients/Elsa.Client/Models/Variables.cs index 703d4ed69..cace5604c 100644 --- a/src/clients/Elsa.Client/Models/Variables.cs +++ b/src/clients/Elsa.Client/Models/Variables.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.ComponentModel; using System.Runtime.Serialization; namespace Elsa.Client.Models @@ -20,11 +22,11 @@ namespace Elsa.Client.Models Data = data; } - [DataMember(Order = 1)]public IDictionary Data { get; set; } + [DataMember(Order = 1)] public IDictionary Data { get; set; } public object? Get(string name) => Has(name) ? Data[name] : default; - public T Get(string name) + public T? Get(string name) { if (!Has(name)) return default!; @@ -34,7 +36,14 @@ namespace Elsa.Client.Models if (value == null) return default!; - return (T)value!; + if (value is T convertedValue) + return convertedValue; + + if (value == default!) + return default!; + + var converter = TypeDescriptor.GetConverter(typeof(T)); + return converter.CanConvertFrom(value.GetType()) ? (T?) converter.ConvertFrom(value) : default; } public Variables Set(string name, object? value) diff --git a/src/core/Elsa.Abstractions/Extensions/WorkflowBlueprintExtensions.cs b/src/core/Elsa.Abstractions/Extensions/WorkflowBlueprintExtensions.cs index c70541f1d..fa2ee670a 100644 --- a/src/core/Elsa.Abstractions/Extensions/WorkflowBlueprintExtensions.cs +++ b/src/core/Elsa.Abstractions/Extensions/WorkflowBlueprintExtensions.cs @@ -31,6 +31,14 @@ namespace Elsa string propertyName, ActivityExecutionContext activityExecutionContext, CancellationToken cancellationToken) + => (T) await workflowBlueprint.GetActivityPropertyValue(activityId, propertyName, activityExecutionContext, cancellationToken); + + public static async ValueTask GetActivityPropertyValue( + this IWorkflowBlueprint workflowBlueprint, + string activityId, + string propertyName, + ActivityExecutionContext activityExecutionContext, + CancellationToken cancellationToken) { var provider = workflowBlueprint.ActivityPropertyProviders.GetProvider(activityId, propertyName); @@ -38,7 +46,7 @@ namespace Elsa return default!; var value = await provider.GetValueAsync(activityExecutionContext, cancellationToken); - return (T) value!; + return value!; } public static T? GetActivityState( diff --git a/src/core/Elsa.Abstractions/Extensions/WorkflowBlueprintWrapperExtensions.cs b/src/core/Elsa.Abstractions/Extensions/WorkflowBlueprintWrapperExtensions.cs index 0be7f63f9..cb7a7266d 100644 --- a/src/core/Elsa.Abstractions/Extensions/WorkflowBlueprintWrapperExtensions.cs +++ b/src/core/Elsa.Abstractions/Extensions/WorkflowBlueprintWrapperExtensions.cs @@ -19,12 +19,8 @@ namespace Elsa return query; } - public static IActivityBlueprintWrapper? FindActivity(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, Func, bool>? predicate = default) - where TActivity : IActivity => - workflowBlueprintWrapper.Filter(predicate).FirstOrDefault(); - - public static IActivityBlueprintWrapper GetActivity(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, string id) - where TActivity : IActivity => - workflowBlueprintWrapper.FindActivity(x => x.ActivityBlueprint.Id == id)!; + public static IActivityBlueprintWrapper? FindActivity(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, Func, bool>? predicate = default) where TActivity : IActivity => workflowBlueprintWrapper.Filter(predicate).FirstOrDefault(); + public static IActivityBlueprintWrapper? GetActivity(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, string id) where TActivity : IActivity => workflowBlueprintWrapper.FindActivity(x => x.ActivityBlueprint.Id == id)!; + public static IActivityBlueprintWrapper? GetActivity(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, string id) => workflowBlueprintWrapper.Activities.FirstOrDefault(x => x.ActivityBlueprint.Id == id); } } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Services/ActivityPropertyProviders.cs b/src/core/Elsa.Abstractions/Services/ActivityPropertyProviders.cs index 5292c382f..fe805f4a5 100644 --- a/src/core/Elsa.Abstractions/Services/ActivityPropertyProviders.cs +++ b/src/core/Elsa.Abstractions/Services/ActivityPropertyProviders.cs @@ -34,8 +34,7 @@ namespace Elsa.Services properties[propertyName] = provider; } - public IDictionary? GetProviders(string activityId) => - _providers.TryGetValue(activityId, out var properties) ? properties : null; + public IDictionary? GetProviders(string activityId) => _providers.TryGetValue(activityId, out var properties) ? properties : null; public IActivityPropertyValueProvider? GetProvider(string activityId, string propertyName) => _providers.TryGetValue(activityId, out var properties) diff --git a/src/core/Elsa.Abstractions/Services/IActivityPropertyProviders.cs b/src/core/Elsa.Abstractions/Services/IActivityPropertyProviders.cs index 4c0d1e3f8..783bca6c0 100644 --- a/src/core/Elsa.Abstractions/Services/IActivityPropertyProviders.cs +++ b/src/core/Elsa.Abstractions/Services/IActivityPropertyProviders.cs @@ -9,6 +9,7 @@ namespace Elsa.Services { void AddProvider(string activityId, string propertyName, IActivityPropertyValueProvider provider); IActivityPropertyValueProvider? GetProvider(string activityId, string propertyName); + IDictionary? GetProviders(string activityId); ValueTask SetActivityPropertiesAsync( IActivity activity, diff --git a/src/core/Elsa.Abstractions/Services/Models/ActivityBlueprintWrapper.cs b/src/core/Elsa.Abstractions/Services/Models/ActivityBlueprintWrapper.cs index 62ce289d6..055fddb20 100644 --- a/src/core/Elsa.Abstractions/Services/Models/ActivityBlueprintWrapper.cs +++ b/src/core/Elsa.Abstractions/Services/Models/ActivityBlueprintWrapper.cs @@ -17,6 +17,22 @@ namespace Elsa.Services.Models public IActivityBlueprint ActivityBlueprint => ActivityExecutionContext.ActivityBlueprint; public IActivityBlueprintWrapper As() where TActivity : IActivity => new ActivityBlueprintWrapper(ActivityExecutionContext); + + public async ValueTask GetPropertyValueAsync(string propertyName, CancellationToken cancellationToken = default) + { + var workflowBlueprint = ActivityExecutionContext.WorkflowExecutionContext.WorkflowBlueprint; + var activityId = ActivityExecutionContext.ActivityBlueprint.Id; + + // Computed property setters that depend on actual workflow state might fault, since we might be using a fake activity execution context. + try + { + return await workflowBlueprint.GetActivityPropertyValue(activityId, propertyName, ActivityExecutionContext, cancellationToken); + } + catch (Exception) + { + return default; + } + } } public class ActivityBlueprintWrapper : ActivityBlueprintWrapper, IActivityBlueprintWrapper where TActivity : IActivity diff --git a/src/core/Elsa.Abstractions/Services/Models/IActivityBlueprintWrapper.cs b/src/core/Elsa.Abstractions/Services/Models/IActivityBlueprintWrapper.cs index 271f9f518..d46faf9e9 100644 --- a/src/core/Elsa.Abstractions/Services/Models/IActivityBlueprintWrapper.cs +++ b/src/core/Elsa.Abstractions/Services/Models/IActivityBlueprintWrapper.cs @@ -9,6 +9,7 @@ namespace Elsa.Services.Models { IActivityBlueprint ActivityBlueprint { get; } IActivityBlueprintWrapper As() where TActivity : IActivity; + ValueTask GetPropertyValueAsync(string propertyName, CancellationToken cancellationToken = default); } public interface IActivityBlueprintWrapper : IActivityBlueprintWrapper where TActivity:IActivity diff --git a/src/dashboards/blazor/ElsaDashboard.Abstractions/Components/ActivityDisplayComponent.cs b/src/dashboards/blazor/ElsaDashboard.Abstractions/Components/ActivityDisplayComponent.cs new file mode 100644 index 000000000..cb61cc6ce --- /dev/null +++ b/src/dashboards/blazor/ElsaDashboard.Abstractions/Components/ActivityDisplayComponent.cs @@ -0,0 +1,10 @@ +using ElsaDashboard.Models; +using Microsoft.AspNetCore.Components; + +namespace ElsaDashboard.Components +{ + public abstract class ActivityDisplayComponent : ComponentBase + { + [Parameter] public ActivityDisplayContext DisplayContext { protected get; set; } = default!; + } +} \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Abstractions/ElsaDashboard.Abstractions.csproj b/src/dashboards/blazor/ElsaDashboard.Abstractions/ElsaDashboard.Abstractions.csproj new file mode 100644 index 000000000..bc6f8b0d2 --- /dev/null +++ b/src/dashboards/blazor/ElsaDashboard.Abstractions/ElsaDashboard.Abstractions.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + enable + ElsaDashboard + + + + + + + + + + + diff --git a/src/dashboards/blazor/ElsaDashboard.Abstractions/Extensions/RenderTreeBuilderExtensions.cs b/src/dashboards/blazor/ElsaDashboard.Abstractions/Extensions/RenderTreeBuilderExtensions.cs new file mode 100644 index 000000000..37804b6e0 --- /dev/null +++ b/src/dashboards/blazor/ElsaDashboard.Abstractions/Extensions/RenderTreeBuilderExtensions.cs @@ -0,0 +1,20 @@ +using System; +using ElsaDashboard.Components; +using ElsaDashboard.Models; +using Microsoft.AspNetCore.Components.Rendering; + +namespace ElsaDashboard.Extensions +{ + public static class RenderTreeBuilderExtensions + { + public static RenderTreeBuilder AddActivityDisplayComponent(this RenderTreeBuilder builder, ActivityDisplayContext context) where T : ActivityDisplayComponent => AddActivityDisplayComponent(builder, typeof(T), context); + + public static RenderTreeBuilder AddActivityDisplayComponent(this RenderTreeBuilder builder, Type type, ActivityDisplayContext context) + { + builder.OpenComponent(0, type); + builder.AddAttribute(1, "DisplayContext", context); + builder.CloseComponent(); + return builder; + } + } +} \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Abstractions/Extensions/ServiceCollectionExtensions.cs b/src/dashboards/blazor/ElsaDashboard.Abstractions/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..28152288d --- /dev/null +++ b/src/dashboards/blazor/ElsaDashboard.Abstractions/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ +using ElsaDashboard.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace ElsaDashboard.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddActivityDisplayProvider(this IServiceCollection services) where T : class, IActivityDisplayProvider + { + return services.AddTransient(); + } + } +} \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Abstractions/Models/ActivityDisplayContext.cs b/src/dashboards/blazor/ElsaDashboard.Abstractions/Models/ActivityDisplayContext.cs new file mode 100644 index 000000000..eba1c32ce --- /dev/null +++ b/src/dashboards/blazor/ElsaDashboard.Abstractions/Models/ActivityDisplayContext.cs @@ -0,0 +1,6 @@ +using Elsa.Client.Models; + +namespace ElsaDashboard.Models +{ + public record ActivityDisplayContext(string ActivityType, Variables Properties); +} \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Abstractions/Models/ActivityDisplayDescriptor.cs b/src/dashboards/blazor/ElsaDashboard.Abstractions/Models/ActivityDisplayDescriptor.cs new file mode 100644 index 000000000..39e1bfe63 --- /dev/null +++ b/src/dashboards/blazor/ElsaDashboard.Abstractions/Models/ActivityDisplayDescriptor.cs @@ -0,0 +1,12 @@ +using System; +using Microsoft.AspNetCore.Components; + +namespace ElsaDashboard.Models +{ + public class ActivityDisplayDescriptor + { + public string ActivityType { get; init; } = default!; + public Func? RenderBody { get; init; } + public Func? RenderIcon { get; init; } + } +} \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Abstractions/Services/ActivityDisplayProvider.cs b/src/dashboards/blazor/ElsaDashboard.Abstractions/Services/ActivityDisplayProvider.cs new file mode 100644 index 000000000..862a62f50 --- /dev/null +++ b/src/dashboards/blazor/ElsaDashboard.Abstractions/Services/ActivityDisplayProvider.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using ElsaDashboard.Models; + +namespace ElsaDashboard.Services +{ + public abstract class ActivityDisplayProvider : IActivityDisplayProvider + { + public virtual ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) + { + var descriptors = GetDescriptors(); + return ValueTask.FromResult(descriptors); + } + + protected virtual IEnumerable GetDescriptors() + { + yield break; + } + } +} \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Abstractions/Services/IActivityDisplayProvider.cs b/src/dashboards/blazor/ElsaDashboard.Abstractions/Services/IActivityDisplayProvider.cs new file mode 100644 index 000000000..6d31b3f60 --- /dev/null +++ b/src/dashboards/blazor/ElsaDashboard.Abstractions/Services/IActivityDisplayProvider.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using ElsaDashboard.Models; + +namespace ElsaDashboard.Services +{ + public interface IActivityDisplayProvider + { + ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Abstractions/Services/IActivityDisplayService.cs b/src/dashboards/blazor/ElsaDashboard.Abstractions/Services/IActivityDisplayService.cs new file mode 100644 index 000000000..cb64cd529 --- /dev/null +++ b/src/dashboards/blazor/ElsaDashboard.Abstractions/Services/IActivityDisplayService.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; +using ElsaDashboard.Models; + +namespace ElsaDashboard.Services +{ + public interface IActivityDisplayService + { + ValueTask GetDisplayDescriptorAsync(string activityType, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Activities/TimerActivity.razor b/src/dashboards/blazor/ElsaDashboard.Application/Activities/TimerActivity.razor new file mode 100644 index 000000000..b9c2ed4f4 --- /dev/null +++ b/src/dashboards/blazor/ElsaDashboard.Application/Activities/TimerActivity.razor @@ -0,0 +1,4 @@ +@using NodaTime +@inherits ActivityDisplayComponent + +

Every @(DisplayContext.Properties.Get("Timeout"))

\ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Display/TimerDisplayProvider.cs b/src/dashboards/blazor/ElsaDashboard.Application/Display/TimerDisplayProvider.cs new file mode 100644 index 000000000..9a7b2ce58 --- /dev/null +++ b/src/dashboards/blazor/ElsaDashboard.Application/Display/TimerDisplayProvider.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using ElsaDashboard.Application.Activities; +using ElsaDashboard.Extensions; +using ElsaDashboard.Models; +using ElsaDashboard.Services; + +namespace ElsaDashboard.Application.Display +{ + public class TimerDisplayProvider : ActivityDisplayProvider + { + protected override IEnumerable GetDescriptors() + { + yield return new ActivityDisplayDescriptor + { + ActivityType = "Timer", + RenderBody = context => builder => builder.AddActivityDisplayComponent(context) + }; + } + } +} \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Application/ElsaDashboard.Application.csproj b/src/dashboards/blazor/ElsaDashboard.Application/ElsaDashboard.Application.csproj index db8b73dea..48420e13a 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/ElsaDashboard.Application.csproj +++ b/src/dashboards/blazor/ElsaDashboard.Application/ElsaDashboard.Application.csproj @@ -28,6 +28,7 @@ + diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Extensions/ServiceCollectionExtensions.cs b/src/dashboards/blazor/ElsaDashboard.Application/Extensions/ServiceCollectionExtensions.cs index 98187d1fb..54a6f9b94 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/Extensions/ServiceCollectionExtensions.cs +++ b/src/dashboards/blazor/ElsaDashboard.Application/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,8 @@ using Blazored.Modal; +using ElsaDashboard.Application.Display; using ElsaDashboard.Application.Services; +using ElsaDashboard.Extensions; +using ElsaDashboard.Services; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection @@ -11,7 +14,9 @@ namespace Microsoft.Extensions.DependencyInjection return services .AddBlazoredModal() .AddScoped() - .AddScoped(); + .AddScoped() + .AddSingleton() + .AddActivityDisplayProvider(); } } } \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Icons/Activity.razor b/src/dashboards/blazor/ElsaDashboard.Application/Icons/ActivityIcon.razor similarity index 100% rename from src/dashboards/blazor/ElsaDashboard.Application/Icons/Activity.razor rename to src/dashboards/blazor/ElsaDashboard.Application/Icons/ActivityIcon.razor diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Icons/Database.razor b/src/dashboards/blazor/ElsaDashboard.Application/Icons/DatabaseIcon.razor similarity index 100% rename from src/dashboards/blazor/ElsaDashboard.Application/Icons/Database.razor rename to src/dashboards/blazor/ElsaDashboard.Application/Icons/DatabaseIcon.razor diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Icons/Document.razor b/src/dashboards/blazor/ElsaDashboard.Application/Icons/DocumentIcon.razor similarity index 100% rename from src/dashboards/blazor/ElsaDashboard.Application/Icons/Document.razor rename to src/dashboards/blazor/ElsaDashboard.Application/Icons/DocumentIcon.razor diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Icons/Home.razor b/src/dashboards/blazor/ElsaDashboard.Application/Icons/HomeIcon.razor similarity index 100% rename from src/dashboards/blazor/ElsaDashboard.Application/Icons/Home.razor rename to src/dashboards/blazor/ElsaDashboard.Application/Icons/HomeIcon.razor diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Icons/PlayCircle.razor b/src/dashboards/blazor/ElsaDashboard.Application/Icons/PlayCircleIcon.razor similarity index 100% rename from src/dashboards/blazor/ElsaDashboard.Application/Icons/PlayCircle.razor rename to src/dashboards/blazor/ElsaDashboard.Application/Icons/PlayCircleIcon.razor diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Icons/SortAscending.razor b/src/dashboards/blazor/ElsaDashboard.Application/Icons/SortAscendingIcon.razor similarity index 100% rename from src/dashboards/blazor/ElsaDashboard.Application/Icons/SortAscending.razor rename to src/dashboards/blazor/ElsaDashboard.Application/Icons/SortAscendingIcon.razor diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Icons/SortDescending.razor b/src/dashboards/blazor/ElsaDashboard.Application/Icons/SortDescendingIcon.razor similarity index 100% rename from src/dashboards/blazor/ElsaDashboard.Application/Icons/SortDescending.razor rename to src/dashboards/blazor/ElsaDashboard.Application/Icons/SortDescendingIcon.razor diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Icons/Squares.razor b/src/dashboards/blazor/ElsaDashboard.Application/Icons/SquaresIcon.razor similarity index 100% rename from src/dashboards/blazor/ElsaDashboard.Application/Icons/Squares.razor rename to src/dashboards/blazor/ElsaDashboard.Application/Icons/SquaresIcon.razor diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Models/ActivityModel.cs b/src/dashboards/blazor/ElsaDashboard.Application/Models/ActivityModel.cs index 158fc7aab..18d3ed5be 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/Models/ActivityModel.cs +++ b/src/dashboards/blazor/ElsaDashboard.Application/Models/ActivityModel.cs @@ -1,14 +1,19 @@ using System.Collections.Generic; -using System.Linq; +using Elsa.Client.Models; +using ElsaDashboard.Models; namespace ElsaDashboard.Application.Models { public record ActivityModel { - public ActivityModel(string activityId, string type) => (ActivityId, Type) = (activityId, type); - public ActivityModel(string activityId, string type, IEnumerable outcomes) => (ActivityId, Type, Outcomes) = (activityId, type, outcomes.ToList()); public string ActivityId { get; init; } public string Type { get; init; } + public string? Name { get; init; } + public string? DisplayName { get; init; } + public string? Description { get; set; } + public ICollection Outcomes { get; init; } = new List(); + public Variables Properties { get; set; } = new(); + public ActivityDisplayDescriptor? DisplayDescriptor { get; set; } } } \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Models/WorkflowModel.cs b/src/dashboards/blazor/ElsaDashboard.Application/Models/WorkflowModel.cs index cacb2a868..0fcc91366 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/Models/WorkflowModel.cs +++ b/src/dashboards/blazor/ElsaDashboard.Application/Models/WorkflowModel.cs @@ -18,26 +18,6 @@ namespace ElsaDashboard.Application.Models public IImmutableList Connections { get; init; } = Array.Empty().ToImmutableList(); public static WorkflowModel Blank() => new("New Workflow"); - - public static WorkflowModel Demo() => - new WorkflowModel - { - Activities = new[] - { - new ActivityModel("activity-1", "ReadLine", new[] { "Done" }), - new ActivityModel("activity-2", "IfElse", new[] { "True", "False" }), - new ActivityModel("activity-3", "WriteLine", new[] { "Done" }), - new ActivityModel("activity-4", "WriteLine", new[] { "Done" }) - }.ToImmutableList(), - - Connections = new[] - { - new ConnectionModel("activity-1", "activity-2", "Done"), - new ConnectionModel("activity-2", "activity-3", "True"), - new ConnectionModel("activity-2", "activity-4", "False") - }.ToImmutableList() - }; - public WorkflowModel AddActivity(ActivityModel activity) => this with { Activities = Activities.Add(activity)}; public WorkflowModel RemoveActivity(ActivityModel activity) => this with { Activities = Activities.Where(x => x != activity).ToImmutableList()}; public WorkflowModel AddConnection(ConnectionModel connection) => this with { Connections = Connections.Add(connection)}; diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Models/WorkflowModelChangedEventArgs.cs b/src/dashboards/blazor/ElsaDashboard.Application/Models/WorkflowModelChangedEventArgs.cs index ea652c0ae..82d21ddda 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/Models/WorkflowModelChangedEventArgs.cs +++ b/src/dashboards/blazor/ElsaDashboard.Application/Models/WorkflowModelChangedEventArgs.cs @@ -1,4 +1,5 @@ using System; +using Elsa.Client.Models; namespace ElsaDashboard.Application.Models { diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Pages/WorkflowDefinitions/Designer.razor.cs b/src/dashboards/blazor/ElsaDashboard.Application/Pages/WorkflowDefinitions/Designer.razor.cs index a5dcfde5a..d4eee8b8e 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/Pages/WorkflowDefinitions/Designer.razor.cs +++ b/src/dashboards/blazor/ElsaDashboard.Application/Pages/WorkflowDefinitions/Designer.razor.cs @@ -69,7 +69,7 @@ namespace ElsaDashboard.Application.Pages.WorkflowDefinitions private ActivityDefinition CreateActivityDefinition(ActivityModel activityModel) { - return new ActivityDefinition + return new() { ActivityId = activityModel.ActivityId, Type = activityModel.Type @@ -78,7 +78,7 @@ namespace ElsaDashboard.Application.Pages.WorkflowDefinitions private WorkflowModel CreateWorkflowModel(WorkflowDefinition workflowDefinition) { - return new WorkflowModel + return new() { Name = workflowDefinition.Name, Activities = workflowDefinition.Activities.Select(CreateActivityModel).ToImmutableList(), @@ -94,7 +94,14 @@ namespace ElsaDashboard.Application.Pages.WorkflowDefinitions private ActivityModel CreateActivityModel(ActivityDefinition activityDefinition) { var descriptor = ActivityDescriptors[activityDefinition.Type]; - return new ActivityModel(activityDefinition.ActivityId, activityDefinition.Type, descriptor.Outcomes); + return new ActivityModel + { + Name = activityDefinition.Name, + ActivityId = activityDefinition.ActivityId, + Type = activityDefinition.Type, + DisplayName = descriptor.DisplayName, + Outcomes = descriptor.Outcomes + }; } private void StartBackgroundWorker() diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Pages/WorkflowInstances/List.razor b/src/dashboards/blazor/ElsaDashboard.Application/Pages/WorkflowInstances/List.razor index 3f3a6e4bc..2dea8d7d7 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/Pages/WorkflowInstances/List.razor +++ b/src/dashboards/blazor/ElsaDashboard.Application/Pages/WorkflowInstances/List.razor @@ -32,9 +32,9 @@

Filters

- - - + + + diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Pages/WorkflowRegistry/List.razor b/src/dashboards/blazor/ElsaDashboard.Application/Pages/WorkflowRegistry/List.razor index e2d5500b1..f30cb3235 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/Pages/WorkflowRegistry/List.razor +++ b/src/dashboards/blazor/ElsaDashboard.Application/Pages/WorkflowRegistry/List.razor @@ -101,14 +101,18 @@ - @foreach (var workflowBlueprint in WorkflowBlueprints.Items) + @foreach (var grouping in VersionedWorkflowBlueprints) { - var displayName = workflowBlueprint.DisplayName ?? workflowBlueprint.Name ?? "Unnamed"; + var workflowBlueprintId = grouping.Key; + var workflowBlueprints = grouping.OrderByDescending(x => x.Version).ToList(); + var latestVersion = workflowBlueprints.First(); + var publishedVersion = workflowBlueprints.FirstOrDefault(x => x.IsPublished); + var displayName = latestVersion.DisplayName ?? latestVersion.Name ?? "Unnamed"; @@ -125,10 +129,10 @@ - 1 + @latestVersion.Version - 1 + @(publishedVersion?.Version.ToString() ?? "-")
@@ -147,7 +151,7 @@ class="z-10 mx-3 origin-top-right absolute right-7 top-0 w-48 mt-1 rounded-md shadow-lg"> \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Shared/Activity.razor.cs b/src/dashboards/blazor/ElsaDashboard.Application/Shared/Activity.razor.cs index 597cfff6c..abb5e04a4 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/Shared/Activity.razor.cs +++ b/src/dashboards/blazor/ElsaDashboard.Application/Shared/Activity.razor.cs @@ -1,4 +1,6 @@ -using ElsaDashboard.Application.Models; +using Elsa.Client.Models; +using ElsaDashboard.Application.Icons; +using ElsaDashboard.Application.Models; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; @@ -6,7 +8,37 @@ namespace ElsaDashboard.Application.Shared { partial class Activity { - [Parameter] public ActivityModel Model { get; set; } = new ActivityModel("", ""); + public const string DefaultIconClass = "h-10 w-10 text-blue-500"; + [Parameter] public ActivityModel Model { get; set; } = new(); [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public RenderFragment Body { get; set; } = default!; + [Parameter] public RenderFragment Icon { get; set; } = default!; + + protected override void OnInitialized() + { + if (Body == null!) + Body = GetDefaultBody(); + + if (Icon == null!) + Icon = GetDefaultIcon(); + } + + private RenderFragment GetDefaultBody() => + builder => + { + builder.OpenElement(0, "p"); + builder.AddContent(1, Model.Description); + builder.CloseElement(); + }; + + private RenderFragment GetDefaultIcon() => + builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, "CssClass", DefaultIconClass); + builder.CloseComponent(); + }; + + private string DisplayName => string.IsNullOrWhiteSpace(Model.DisplayName) ? Model.Type : Model.DisplayName; } } \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Shared/NavMenu.razor.cs b/src/dashboards/blazor/ElsaDashboard.Application/Shared/NavMenu.razor.cs index 905d255d2..f01d4c252 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/Shared/NavMenu.razor.cs +++ b/src/dashboards/blazor/ElsaDashboard.Application/Shared/NavMenu.razor.cs @@ -12,11 +12,11 @@ namespace ElsaDashboard.Application.Shared { MenuItems = new[] { - new MenuItem("Home", "", GetIcon(), NavLinkMatch.All), - new MenuItem("Workflow Registry", "workflow-registry", GetIcon(), NavLinkMatch.Prefix), - new MenuItem("Workflow Instances", "workflow-instances", GetIcon(), NavLinkMatch.Prefix), - new MenuItem("Workflow Definitions", "workflow-definitions", GetIcon(), NavLinkMatch.Prefix), - new MenuItem("Composite Activities", "composite-activities", GetIcon(), NavLinkMatch.Prefix), + new MenuItem("Home", "", GetIcon(), NavLinkMatch.All), + new MenuItem("Workflow Registry", "workflow-registry", GetIcon(), NavLinkMatch.Prefix), + new MenuItem("Workflow Instances", "workflow-instances", GetIcon(), NavLinkMatch.Prefix), + new MenuItem("Workflow Definitions", "workflow-definitions", GetIcon(), NavLinkMatch.Prefix), + new MenuItem("Composite Activities", "composite-activities", GetIcon(), NavLinkMatch.Prefix), }; } diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Shared/WorkflowDesigner.razor b/src/dashboards/blazor/ElsaDashboard.Application/Shared/WorkflowDesigner.razor index e7a465116..3a3cde5e0 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/Shared/WorkflowDesigner.razor +++ b/src/dashboards/blazor/ElsaDashboard.Application/Shared/WorkflowDesigner.razor @@ -1,4 +1,5 @@ @using ElsaDashboard.Application.Extensions +@using ElsaDashboard.Models @{ var renderedActivities = new HashSet(); @@ -86,7 +87,7 @@ @RenderOutcomeButton($"start-button-plus-{activityId}", "", () => onAddActivityClickHandler(null, activityId, null)) } - + @RenderActivity(activity) @RenderOutcomeButtons(activity, onAddActivityClickHandler)
@if (children.Any()) diff --git a/src/dashboards/blazor/ElsaDashboard.Application/Shared/WorkflowDesigner.razor.cs b/src/dashboards/blazor/ElsaDashboard.Application/Shared/WorkflowDesigner.razor.cs index 4b2b56f69..b8d18394f 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/Shared/WorkflowDesigner.razor.cs +++ b/src/dashboards/blazor/ElsaDashboard.Application/Shared/WorkflowDesigner.razor.cs @@ -9,6 +9,8 @@ using Elsa.Client.Models; using ElsaDashboard.Application.Extensions; using ElsaDashboard.Application.Models; using ElsaDashboard.Application.Services; +using ElsaDashboard.Models; +using ElsaDashboard.Services; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web; @@ -19,7 +21,7 @@ namespace ElsaDashboard.Application.Shared partial class WorkflowDesigner : IAsyncDisposable { private static Func _connectionCreatedAction = default!; - + [Parameter] public WorkflowModel Model { private get; set; } = WorkflowModel.Blank(); [Parameter] public EventCallback WorkflowChanged { get; set; } [Inject] private IJSRuntime JS { get; set; } = default!; @@ -28,6 +30,15 @@ namespace ElsaDashboard.Application.Shared private bool _connectionsChanged = true; private EventCallbackFactory EventCallbackFactory { get; } = new(); + private readonly ActivityInfo _unknownActivityDescriptor = + new() + { + Type = "Unknown", + Category = "Unknown", + Description = "Activity type not found", + DisplayName = "Unknown" + }; + [JSInvokableAttribute("InvokeConnectionCreated")] public static Task InvokeConnectionCreated(ConnectionModel connection) => _connectionCreatedAction(connection); @@ -59,7 +70,7 @@ namespace ElsaDashboard.Application.Shared { Model = model; ConnectionsHasChanged(); - + await WorkflowChanged.InvokeAsync(new WorkflowModelChangedEventArgs(Model)); } @@ -164,7 +175,7 @@ namespace ElsaDashboard.Application.Shared { await FlyoutPanelService.ShowAsync( "Activities", - new { ActivitySelected = EventCallbackFactory.Create(this, e => OnActivityPickedAsync(e, parentActivityId, targetActivityId, outcome)) }, + new {ActivitySelected = EventCallbackFactory.Create(this, e => OnActivityPickedAsync(e, parentActivityId, targetActivityId, outcome))}, ButtonDescriptor.Create("Cancel", _ => ClosePanelAsync())); } @@ -172,7 +183,14 @@ namespace ElsaDashboard.Application.Shared { outcome ??= "Done"; - var activity = new ActivityModel(Guid.NewGuid().ToString("N"), activityInfo.Type, activityInfo.Outcomes); + var activity = new ActivityModel + { + ActivityId = Guid.NewGuid().ToString("N"), + Type = activityInfo.Type, + Outcomes = activityInfo.Outcomes, + DisplayName = activityInfo.DisplayName + }; + var model = Model.AddActivity(activity); if (targetActivityId != null) @@ -210,10 +228,35 @@ namespace ElsaDashboard.Application.Shared model = model.AddConnection(connection); } } - + await UpdateModelAsync(model); await FlyoutPanelService.HideAsync(); } + + private RenderFragment RenderActivity(ActivityModel activityModel) + { + return builder => + { + var index = 0; + builder.OpenComponent(index++); + builder.AddAttribute(index++, "Model", activityModel); + + var displayDescriptor = activityModel.DisplayDescriptor; + + if (displayDescriptor != null) + { + var context = new ActivityDisplayContext(activityModel.Type, activityModel.Properties); + + if(displayDescriptor.RenderBody != null) + builder.AddAttribute(index++, "Body", displayDescriptor.RenderBody(context)); + + if(displayDescriptor.RenderIcon != null) + builder.AddAttribute(index, "Icon", displayDescriptor.RenderIcon(context)); + } + + builder.CloseComponent(); + }; + } private void ConnectionsHasChanged() { @@ -229,7 +272,7 @@ namespace ElsaDashboard.Application.Shared await FlyoutPanelService.ShowAsync( activityInfo.DisplayName, - new { ActivityInfo = activityInfo }, + new {ActivityInfo = activityInfo}, ButtonDescriptor.Create("Cancel", _ => ShowActivityPickerAsync(sourceActivityId, targetActivityId, outcome)), ButtonDescriptor.Create("OK", _ => AddActivityAsync(activityInfo, sourceActivityId, targetActivityId, outcome), true)); } diff --git a/src/dashboards/blazor/ElsaDashboard.Application/_Imports.razor b/src/dashboards/blazor/ElsaDashboard.Application/_Imports.razor index 26d8a7fd5..5ca97ab58 100644 --- a/src/dashboards/blazor/ElsaDashboard.Application/_Imports.razor +++ b/src/dashboards/blazor/ElsaDashboard.Application/_Imports.razor @@ -3,10 +3,12 @@ @using Blazored.Modal @using Blazored.Modal.Services @using ElsaDashboard.Application +@using ElsaDashboard.Application.Icons @using ElsaDashboard.Application.Models @using ElsaDashboard.Application.Shared +@using ElsaDashboard.Components @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization -@using Microsoft.JSInterop +@using Microsoft.JSInterop \ No newline at end of file diff --git a/src/dashboards/blazor/ElsaDashboard.Shared/Surrogates/WorkflowBlueprintSurrogate.cs b/src/dashboards/blazor/ElsaDashboard.Shared/Surrogates/WorkflowBlueprintSurrogate.cs index 2bf2de8a6..c0009d4b4 100644 --- a/src/dashboards/blazor/ElsaDashboard.Shared/Surrogates/WorkflowBlueprintSurrogate.cs +++ b/src/dashboards/blazor/ElsaDashboard.Shared/Surrogates/WorkflowBlueprintSurrogate.cs @@ -1,5 +1,7 @@ using Elsa.Client.Models; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; using NodaTime; using NodaTime.Serialization.JsonNet; using ProtoBuf; @@ -16,7 +18,15 @@ namespace ElsaDashboard.Shared.Surrogates SerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto - }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + }; + + SerializerSettings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; + SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.All; + SerializerSettings.TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple; + SerializerSettings.TypeNameHandling = TypeNameHandling.Auto; + SerializerSettings.Converters.Add(new StringEnumConverter(new DefaultNamingStrategy())); } public WorkflowBlueprintSurrogate(WorkflowBlueprint value) diff --git a/src/persistence/Elsa.Persistence.MongoDb/Services/DatabaseInitializer.cs b/src/persistence/Elsa.Persistence.MongoDb/Services/DatabaseInitializer.cs index c9e5eb541..9c6d24b8f 100644 --- a/src/persistence/Elsa.Persistence.MongoDb/Services/DatabaseInitializer.cs +++ b/src/persistence/Elsa.Persistence.MongoDb/Services/DatabaseInitializer.cs @@ -28,8 +28,9 @@ namespace Elsa.Persistence.MongoDb.Services var tenantKeysDefinition = Builders.IndexKeys.Ascending(x => x.TenantId); var versionKeysDefinition = Builders.IndexKeys.Ascending(x => x.Version); var workflowStatusKeysDefinition = Builders.IndexKeys.Ascending(x => x.WorkflowStatus); + var workflowNameKeysDefinition = Builders.IndexKeys.Ascending(x => x.Name); - await CreateIndexesAsync(_mongoClient.WorkflowInstances, cancellationToken, tenantKeysDefinition, versionKeysDefinition, workflowStatusKeysDefinition); + await CreateIndexesAsync(_mongoClient.WorkflowInstances, cancellationToken, tenantKeysDefinition, versionKeysDefinition, workflowStatusKeysDefinition, workflowNameKeysDefinition); } private async Task CreateWorkflowDefinitionsIndexes(CancellationToken cancellationToken) @@ -41,7 +42,7 @@ namespace Elsa.Persistence.MongoDb.Services await CreateIndexesAsync(_mongoClient.WorkflowDefinitions, cancellationToken, tenantKeysDefinition, definitionVersionIdKeysDefinition, versionKeysDefinition, nameKeysDefinition); } - + private async Task CreateWorkflowExecutionLogIndexes(CancellationToken cancellationToken) { var tenantKeysDefinition = Builders.IndexKeys.Ascending(x => x.TenantId); diff --git a/src/samples/worker/Elsa.Samples.Timers/Program.cs b/src/samples/worker/Elsa.Samples.Timers/Program.cs index c799ecba6..e8094ea1c 100644 --- a/src/samples/worker/Elsa.Samples.Timers/Program.cs +++ b/src/samples/worker/Elsa.Samples.Timers/Program.cs @@ -22,8 +22,8 @@ namespace Elsa.Samples.Timers .AddTimerActivities(options => options.UseQuartzProvider()) .AddWorkflow() .AddWorkflow() - //.AddWorkflow() - //.AddWorkflow(new OneOffWorkflow(SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromSeconds(5)))) + .AddWorkflow() + .AddWorkflow(new OneOffWorkflow(SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromSeconds(5)))) ; }); } diff --git a/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/Get.cs b/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/Get.cs index e95b2c981..855637f83 100644 --- a/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/Get.cs +++ b/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/Get.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using AutoMapper; using Elsa.Models; using Elsa.Server.Api.Models; +using Elsa.Server.Api.Services; using Elsa.Server.Api.Swagger.Examples; using Elsa.Services; using Microsoft.AspNetCore.Http; @@ -19,12 +20,12 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry public class Get : Controller { private readonly IWorkflowRegistry _workflowRegistry; - private readonly IMapper _mapper; + private readonly IWorkflowBlueprintMapper _workflowBlueprintMapper; - public Get(IWorkflowRegistry workflowRegistry, IMapper mapper) + public Get(IWorkflowRegistry workflowRegistry, IWorkflowBlueprintMapper workflowBlueprintMapper) { _workflowRegistry = workflowRegistry; - _mapper = mapper; + _workflowBlueprintMapper = workflowBlueprintMapper; } [HttpGet] @@ -44,7 +45,7 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry if (workflowBlueprint == null) return NotFound(); - return _mapper.Map(workflowBlueprint); + return await _workflowBlueprintMapper.MapAsync(workflowBlueprint, cancellationToken); } } } \ No newline at end of file diff --git a/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/List.cs b/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/List.cs index 2547570b5..e46b5e783 100644 --- a/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/List.cs +++ b/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/List.cs @@ -1,4 +1,7 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Threading; using System.Threading.Tasks; using AutoMapper; @@ -6,8 +9,10 @@ using Elsa.Models; using Elsa.Server.Api.Models; using Elsa.Server.Api.Swagger.Examples; using Elsa.Services; +using Elsa.Services.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.Filters; @@ -21,23 +26,24 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry { private readonly IWorkflowRegistry _workflowRegistry; private readonly IMapper _mapper; + private readonly IServiceProvider _serviceProvider; - public List(IWorkflowRegistry workflowRegistry, IMapper mapper) + public List(IWorkflowRegistry workflowRegistry, IWorkflowBlueprintReflector workflowBlueprintReflector, IMapper mapper, IServiceProvider serviceProvider) { _workflowRegistry = workflowRegistry; _mapper = mapper; + _serviceProvider = serviceProvider; } [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PagedList))] - [SwaggerResponseExample(StatusCodes.Status200OK, typeof(WorkflowBlueprintModelPagedListExample))] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PagedList))] [SwaggerOperation( Summary = "Returns a list of workflow blueprints.", Description = "Returns paginated a list of workflow blueprints. When no version options are specified, the latest version is returned.", OperationId = "WorkflowBlueprints.List", - Tags = new[] { "WorkflowBlueprints" }) + Tags = new[] {"WorkflowBlueprints"}) ] - public async Task>> Handle(int? page = default, int? pageSize = default, VersionOptions? version = default, CancellationToken cancellationToken = default) + public async Task>> Handle(int? page = default, int? pageSize = default, VersionOptions? version = default, CancellationToken cancellationToken = default) { version ??= VersionOptions.Latest; var workflowBlueprints = await _workflowRegistry.GetWorkflowsAsync(cancellationToken).Where(x => x.WithVersion(version.Value)).ToListAsync(cancellationToken); @@ -45,16 +51,16 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry var skip = page * pageSize; var items = workflowBlueprints.AsEnumerable(); - if (skip != null) + if (skip != null) items = items.Skip(skip.Value); if (pageSize != null) items = items.Take(pageSize.Value); - var mappedItems = items.Select(x => _mapper.Map(x)).ToList(); - var pagedList = new PagedList(mappedItems, page, pageSize, totalCount); - - return pagedList; + using var scope = _serviceProvider.CreateScope(); + var mappedItems = _mapper.Map>(items).ToList(); + + return new PagedList(mappedItems, page, pageSize, totalCount); } } } \ No newline at end of file diff --git a/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/Models.cs b/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/Models.cs index 21feccfe8..5dc5e0b76 100644 --- a/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/Models.cs +++ b/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/Models.cs @@ -28,8 +28,8 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry public class ConnectionModel { - public string SourceId { get; set; } = default!; - public string TargetId { get; set; } = default!; + public string SourceActivityId { get; set; } = default!; + public string TargetActivityId { get; set; } = default!; public string Outcome { get; set; } = default!; } @@ -41,5 +41,19 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry public bool PersistWorkflow { get; set; } public bool LoadWorkflowContext { get; set; } public bool SaveWorkflowContext { get; set; } + public Variables Properties { get; set; } = new(); + } + + public class WorkflowBlueprintSummaryModel + { + public string Id { get; set; } = default!; + public int Version { get; set; } + public string? TenantId { get; set; } + public bool IsSingleton { get; set; } + public bool IsEnabled { get; set; } + public string? Description { get; set; } + public bool IsPublished { get; set; } + public bool IsLatest { get; set; } + public string? DisplayName { get; set; } } } \ No newline at end of file diff --git a/src/server/Elsa.Server.Api/Extensions/ServiceCollectionExtensions.cs b/src/server/Elsa.Server.Api/Extensions/ServiceCollectionExtensions.cs index c31802e2b..3c3a2c48e 100644 --- a/src/server/Elsa.Server.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/server/Elsa.Server.Api/Extensions/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Elsa; using Elsa.Models; using Elsa.Server.Api.Extensions; using Elsa.Server.Api.Mapping; +using Elsa.Server.Api.Services; using Elsa.Server.Api.Swagger.Examples; using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Any; @@ -35,6 +36,8 @@ namespace Microsoft.Extensions.DependencyInjection }); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddAutoMapperProfile(); return services; } diff --git a/src/server/Elsa.Server.Api/Mapping/ActivityBlueprintConverter.cs b/src/server/Elsa.Server.Api/Mapping/ActivityBlueprintConverter.cs new file mode 100644 index 000000000..78ab467e7 --- /dev/null +++ b/src/server/Elsa.Server.Api/Mapping/ActivityBlueprintConverter.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using AutoMapper; +using Elsa.Models; +using Elsa.Server.Api.Endpoints.WorkflowRegistry; +using Elsa.Services.Models; + +namespace Elsa.Server.Api.Mapping +{ + public class ActivityBlueprintConverter : ITypeConverter + { + public const string ActivityPropertiesKey = "ActivityProperties"; + + public ActivityBlueprintModel Convert(IActivityBlueprint source, ActivityBlueprintModel destination, ResolutionContext context) + { + return new() + { + Id = source.Id, + Name = source.Name, + Type = source.Type, + PersistWorkflow = source.PersistWorkflow, + LoadWorkflowContext = source.LoadWorkflowContext, + SaveWorkflowContext = source.SaveWorkflowContext, + Properties = GetProperties(source, context) + }; + } + + private Variables GetProperties(IActivityBlueprint activityBlueprint, ResolutionContext context) + { + if (!context.Items.ContainsKey(ActivityPropertiesKey)) + return new Variables(); + + var dictionary = (IDictionary) context.Items[ActivityPropertiesKey]; + return dictionary[activityBlueprint.Id]; + } + } +} \ No newline at end of file diff --git a/src/server/Elsa.Server.Api/Mapping/AutoMapperProfile.cs b/src/server/Elsa.Server.Api/Mapping/AutoMapperProfile.cs index 0133e32a2..7b1a9f0dc 100644 --- a/src/server/Elsa.Server.Api/Mapping/AutoMapperProfile.cs +++ b/src/server/Elsa.Server.Api/Mapping/AutoMapperProfile.cs @@ -9,7 +9,8 @@ namespace Elsa.Server.Api.Mapping public AutoMapperProfile() { CreateMap(); - CreateMap(); + CreateMap(); + CreateMap().ConvertUsing(); CreateMap(); CreateMap().ConvertUsing(); } diff --git a/src/server/Elsa.Server.Api/Mapping/ConnectionConverter.cs b/src/server/Elsa.Server.Api/Mapping/ConnectionConverter.cs index bf318bea2..5f5aec9d6 100644 --- a/src/server/Elsa.Server.Api/Mapping/ConnectionConverter.cs +++ b/src/server/Elsa.Server.Api/Mapping/ConnectionConverter.cs @@ -9,8 +9,8 @@ namespace Elsa.Server.Api.Mapping public ConnectionModel Convert(IConnection source, ConnectionModel destination, ResolutionContext context) => new() { - SourceId = source.Source.Activity.Id, - TargetId = source.Target.Activity.Id, + SourceActivityId = source.Source.Activity.Id, + TargetActivityId = source.Target.Activity.Id, Outcome = source.Source.Outcome }; } diff --git a/src/server/Elsa.Server.Api/Services/IWorkflowBlueprintMapper.cs b/src/server/Elsa.Server.Api/Services/IWorkflowBlueprintMapper.cs new file mode 100644 index 000000000..712eaf116 --- /dev/null +++ b/src/server/Elsa.Server.Api/Services/IWorkflowBlueprintMapper.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; +using Elsa.Server.Api.Endpoints.WorkflowRegistry; +using Elsa.Services.Models; + +namespace Elsa.Server.Api.Services +{ + public interface IWorkflowBlueprintMapper + { + ValueTask MapAsync(IWorkflowBlueprint workflowBlueprint, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/server/Elsa.Server.Api/Services/WorkflowBlueprintMapper.cs b/src/server/Elsa.Server.Api/Services/WorkflowBlueprintMapper.cs new file mode 100644 index 000000000..83abc204e --- /dev/null +++ b/src/server/Elsa.Server.Api/Services/WorkflowBlueprintMapper.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using Elsa.Models; +using Elsa.Server.Api.Endpoints.WorkflowRegistry; +using Elsa.Server.Api.Mapping; +using Elsa.Services; +using Elsa.Services.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Server.Api.Services +{ + public class WorkflowBlueprintMapper : IWorkflowBlueprintMapper + { + private readonly IWorkflowBlueprintReflector _workflowBlueprintReflector; + private readonly IMapper _mapper; + private readonly IServiceProvider _serviceProvider; + + public WorkflowBlueprintMapper(IWorkflowBlueprintReflector workflowBlueprintReflector, IMapper mapper, IServiceProvider serviceProvider) + { + _workflowBlueprintReflector = workflowBlueprintReflector; + _mapper = mapper; + _serviceProvider = serviceProvider; + } + + public async ValueTask MapAsync(IWorkflowBlueprint workflowBlueprint, CancellationToken cancellationToken = default) + { + using var scope = _serviceProvider.CreateScope(); + + var wrapper = await _workflowBlueprintReflector.ReflectAsync(scope, workflowBlueprint, cancellationToken); + var activityProperties = await Task.WhenAll(wrapper.Activities.Select(async x => (x.ActivityBlueprint.Id, await GetActivityPropertiesAsync(wrapper, x, cancellationToken)))); + var activityPropertyDictionary = activityProperties.ToDictionary(x => x.Id, x => x.Item2); + return _mapper.Map(workflowBlueprint, options => options.Items[ActivityBlueprintConverter.ActivityPropertiesKey] = activityPropertyDictionary); + } + + private async ValueTask GetActivityPropertiesAsync(IWorkflowBlueprintWrapper workflowBlueprintWrapper, IActivityBlueprintWrapper activityBlueprintWrapper, CancellationToken cancellationToken) + { + var workflowBlueprint = workflowBlueprintWrapper.WorkflowBlueprint; + var activityBlueprint = activityBlueprintWrapper.ActivityBlueprint; + var activityId = activityBlueprint.Id; + var activityPropertyValueProviders = workflowBlueprint.ActivityPropertyProviders.GetProviders(activityId); + var activityWrapper = workflowBlueprintWrapper.GetActivity(activityId)!; + var properties = new Variables(); + + if (activityPropertyValueProviders == null) + return properties; + + foreach (var valueProvider in activityPropertyValueProviders) + { + var value = await activityWrapper.GetPropertyValueAsync(valueProvider.Key, cancellationToken); + properties.Set(valueProvider.Key, value); + } + + return properties; + } + } +} \ No newline at end of file diff --git a/src/server/Elsa.Server.Api/Swagger/Examples/WorkflowBlueprintModelExample.cs b/src/server/Elsa.Server.Api/Swagger/Examples/WorkflowBlueprintModelExample.cs index 1ebb8d1b2..fda8350e2 100644 --- a/src/server/Elsa.Server.Api/Swagger/Examples/WorkflowBlueprintModelExample.cs +++ b/src/server/Elsa.Server.Api/Swagger/Examples/WorkflowBlueprintModelExample.cs @@ -36,7 +36,7 @@ namespace Elsa.Server.Api.Swagger.Examples Name = "Activity2", } }, - Connections = new[] { new ConnectionModel { SourceId = "activity-1", TargetId = "activity-2", Outcome = OutcomeNames.Done } } + Connections = new[] { new ConnectionModel { SourceActivityId = "activity-1", TargetActivityId = "activity-2", Outcome = OutcomeNames.Done } } }; } } diff --git a/src/server/Elsa.Server.Host/Workflows/ConditionWorkflow.cs b/src/server/Elsa.Server.Host/Workflows/ConditionWorkflow.cs index 5a32fb026..08c9c950e 100644 --- a/src/server/Elsa.Server.Host/Workflows/ConditionWorkflow.cs +++ b/src/server/Elsa.Server.Host/Workflows/ConditionWorkflow.cs @@ -1,7 +1,9 @@ using Elsa.Activities.Console; using Elsa.Activities.ControlFlow; using Elsa.Activities.Primitives; +using Elsa.Activities.Timers; using Elsa.Builders; +using NodaTime; namespace Elsa.Server.Host.Workflows { @@ -13,6 +15,7 @@ namespace Elsa.Server.Host.Workflows .WithDisplayName("Conditions") .WriteLine("What is your age?") .ReadLine() + .Timer(Duration.FromMinutes(5)) .SetVariable("Age", context => int.Parse(context.GetInput())) .IfElse(context => context.GetVariable("Age") < 18, ifElse => {