diff --git a/Elsa.sln b/Elsa.sln index 5b4a22f54..ad6dab898 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -275,6 +275,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Activities.File", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.ReadLineToFile", "src\samples\console\Elsa.Samples.ReadLineToFile\Elsa.Samples.ReadLineToFile.csproj", "{3EB488DF-3C28-4DAD-B43E-6062E39629AB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.SendHttp", "src\samples\aspnet\Elsa.Samples.SendHttp\Elsa.Samples.SendHttp.csproj", "{775EFD59-F99C-4D38-B54E-9734C9D9649A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -657,6 +659,10 @@ Global {3EB488DF-3C28-4DAD-B43E-6062E39629AB}.Debug|Any CPU.Build.0 = Debug|Any CPU {3EB488DF-3C28-4DAD-B43E-6062E39629AB}.Release|Any CPU.ActiveCfg = Release|Any CPU {3EB488DF-3C28-4DAD-B43E-6062E39629AB}.Release|Any CPU.Build.0 = Release|Any CPU + {775EFD59-F99C-4D38-B54E-9734C9D9649A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {775EFD59-F99C-4D38-B54E-9734C9D9649A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {775EFD59-F99C-4D38-B54E-9734C9D9649A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {775EFD59-F99C-4D38-B54E-9734C9D9649A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -785,6 +791,7 @@ Global {D55BC08B-CA82-478A-99F7-EDE90AC33160} = {C865B0FD-E505-48F0-BFAF-0D4D7C1B5CA1} {BE284508-9582-4FCB-B0DD-0761AB940AD7} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180} {3EB488DF-3C28-4DAD-B43E-6062E39629AB} = {FC9F520F-BA51-4AD2-BFEE-EF787798E734} + {775EFD59-F99C-4D38-B54E-9734C9D9649A} = {22E75696-6FE9-436A-9097-EE21C603F818} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158} diff --git a/src/activities/Elsa.Activities.AzureServiceBus/Services/ServiceBusQueuesStarter.cs b/src/activities/Elsa.Activities.AzureServiceBus/Services/ServiceBusQueuesStarter.cs index 5b6db2caf..e16e04a27 100644 --- a/src/activities/Elsa.Activities.AzureServiceBus/Services/ServiceBusQueuesStarter.cs +++ b/src/activities/Elsa.Activities.AzureServiceBus/Services/ServiceBusQueuesStarter.cs @@ -41,12 +41,22 @@ namespace Elsa.Activities.AzureServiceBus.Services await DisposeExistingWorkersAsync(); var queueNames = (await GetQueueNamesAsync(cancellationToken).ToListAsync(cancellationToken)).Distinct(); - foreach (var queueName in queueNames) + foreach (var queueName in queueNames) + await CreateAndAddWorkerAsync(queueName, cancellationToken); + } + + private async Task CreateAndAddWorkerAsync(string queueName, CancellationToken cancellationToken) + { + try { var receiver = await _messageReceiverClientFactory.GetReceiverAsync(queueName, cancellationToken); var worker = ActivatorUtilities.CreateInstance(_serviceProvider, receiver, (Func) DisposeReceiverAsync); _workers.Add(worker); } + catch (Exception e) + { + _logger.LogWarning(e, "Failed to create a receiver for queue {QueueName}", queueName); + } } private async Task DisposeExistingWorkersAsync() @@ -65,7 +75,7 @@ namespace Elsa.Activities.AzureServiceBus.Services using var scope = _scopeFactory.CreateScope(); var workflowRegistry = scope.ServiceProvider.GetRequiredService(); var workflowBlueprintReflector = scope.ServiceProvider.GetRequiredService(); - var workflows = await workflowRegistry.ListAsync(cancellationToken); + var workflows = await workflowRegistry.ListActiveAsync(cancellationToken); var query = from workflow in workflows diff --git a/src/activities/Elsa.Activities.AzureServiceBus/Services/ServiceBusTopicsStarter.cs b/src/activities/Elsa.Activities.AzureServiceBus/Services/ServiceBusTopicsStarter.cs index 078e07635..012860a1f 100644 --- a/src/activities/Elsa.Activities.AzureServiceBus/Services/ServiceBusTopicsStarter.cs +++ b/src/activities/Elsa.Activities.AzureServiceBus/Services/ServiceBusTopicsStarter.cs @@ -10,6 +10,7 @@ using Elsa.Services; using Elsa.Services.Models; using Microsoft.Azure.ServiceBus.Core; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Elsa.Activities.AzureServiceBus.Services { @@ -19,16 +20,19 @@ namespace Elsa.Activities.AzureServiceBus.Services private readonly ITopicMessageReceiverFactory _receiverFactory; private readonly IServiceScopeFactory _scopeFactory; private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; private readonly ICollection _workers; public ServiceBusTopicsStarter( ITopicMessageReceiverFactory receiverFactory, IServiceScopeFactory scopeFactory, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + ILogger logger) { _receiverFactory = receiverFactory; _scopeFactory = scopeFactory; _serviceProvider = serviceProvider; + _logger = logger; _workers = new List(); } @@ -38,12 +42,22 @@ namespace Elsa.Activities.AzureServiceBus.Services await DisposeExistingWorkersAsync(); var entities = (await GetTopicSubscriptionNamesAsync(cancellationToken).ToListAsync(cancellationToken)).Distinct(); - foreach (var entity in entities) + foreach (var entity in entities) + await CreateAndAddWorkerAsync(entity.topicName, entity.subscriptionName, cancellationToken); + } + + private async Task CreateAndAddWorkerAsync(string topicName, string subscriptionName, CancellationToken cancellationToken) + { + try { - var receiver = await _receiverFactory.GetTopicReceiverAsync(entity.topicName, entity.subscriptionName, cancellationToken); + var receiver = await _receiverFactory.GetTopicReceiverAsync(topicName, subscriptionName, cancellationToken); var worker = ActivatorUtilities.CreateInstance(_serviceProvider, receiver, (Func) DisposeReceiverAsync); _workers.Add(worker); } + catch (Exception e) + { + _logger.LogWarning(e, "Failed to create a receiver for topic {TopicName} and subscription {SubscriptionName}", topicName, subscriptionName); + } } private async Task DisposeExistingWorkersAsync() @@ -63,7 +77,7 @@ namespace Elsa.Activities.AzureServiceBus.Services var workflowRegistry = scope.ServiceProvider.GetRequiredService(); var workflowBlueprintReflector = scope.ServiceProvider.GetRequiredService(); var workflowInstanceStore = scope.ServiceProvider.GetRequiredService(); - var workflows = await workflowRegistry.ListAsync(cancellationToken); + var workflows = await workflowRegistry.ListActiveAsync(cancellationToken); var query = from workflow in workflows diff --git a/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequestBuilderExtensions.cs b/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequestBuilderExtensions.cs new file mode 100644 index 000000000..3b0f026c7 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequestBuilderExtensions.cs @@ -0,0 +1,90 @@ +using Elsa.Activities.Http.Models; +using Elsa.Builders; +using Elsa.Services.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Elsa.Activities.Http +{ + public static class SendHttpRequestBuilderExtensions + { + public static IActivityBuilder SendHttpRequest(this IActivityBuilder builder, Action> setup, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => + builder.Then(setup, null, lineNumber, sourceFile); + + public static IActivityBuilder SendHttpRequest(this IActivityBuilder builder, + Func url, + Func method, + Func content, + Func contentType, + Func authorization, + Func requestHeaders, + Func readContent, + Func> supportedStatusCodes, + [CallerLineNumber] int lineNumber = default, + [CallerFilePath] string? sourceFile = default) => + builder.SendHttpRequest( + setup => setup + .Set(x => x.Url, url) + .Set(x => x.Method, method) + .Set(x => x.Content, content) + .Set(x => x.ContentType, contentType) + .Set(x => x.Authorization, authorization) + .Set(x => x.RequestHeaders, requestHeaders) + .Set(x => x.ReadContent, readContent) + .Set(x => x.SupportedStatusCodes, supportedStatusCodes), + lineNumber, + sourceFile); + + public static IActivityBuilder SendHttpRequest(this IActivityBuilder builder, + Func url, + Func method, + Func content, + Func contentType, + Func authorization, + Func requestHeaders, + Func readContent, + Func> supportedStatusCodes, + [CallerLineNumber] int lineNumber = default, + [CallerFilePath] string? sourceFile = default) => + builder.SendHttpRequest( + setup => setup + .Set(x => x.Url, url) + .Set(x => x.Method, method) + .Set(x => x.Content, content) + .Set(x => x.ContentType, contentType) + .Set(x => x.Authorization, authorization) + .Set(x => x.RequestHeaders, requestHeaders) + .Set(x => x.ReadContent, readContent) + .Set(x => x.SupportedStatusCodes, supportedStatusCodes), + lineNumber, + sourceFile); + + public static IActivityBuilder SendHttpRequest(this IActivityBuilder builder, + Uri? url, + string? method, + string? content, + string? contentType, + string? authorization, + HttpRequestHeaders requestHeaders, + bool readContent, + ICollection supportedStatusCodes, + [CallerLineNumber] int lineNumber = default, + [CallerFilePath] string? sourceFile = default) => + builder.SendHttpRequest( + setup => setup + .Set(x => x.Url, url) + .Set(x => x.Method, method) + .Set(x => x.Content, content) + .Set(x => x.ContentType, contentType) + .Set(x => x.Authorization, authorization) + .Set(x => x.RequestHeaders, requestHeaders) + .Set(x => x.ReadContent, readContent) + .Set(x => x.SupportedStatusCodes, supportedStatusCodes), + lineNumber, + sourceFile); + } +} diff --git a/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequestExtensions.cs b/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequestExtensions.cs new file mode 100644 index 000000000..a82713aa3 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequestExtensions.cs @@ -0,0 +1,108 @@ +using Elsa.Activities.Http.Models; +using Elsa.Builders; +using Elsa.Services.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Elsa.Activities.Http +{ + public static class SendHttpRequestExtensions + { + public static ISetupActivity WithUrl(this ISetupActivity activity, Func> url) => activity.Set(x => x.Url, url); + + public static ISetupActivity WithUrl(this ISetupActivity activity, Func> url) => activity.Set(x => x.Url, url); + + public static ISetupActivity WithUrl(this ISetupActivity activity, Func url) => activity.Set(x => x.Url, url); + + public static ISetupActivity WithUrl(this ISetupActivity activity, Func url) => activity.Set(x => x.Url, url); + + public static ISetupActivity WithUrl(this ISetupActivity activity, Uri? url) => activity.Set(x => x.Url, url); + + + public static ISetupActivity WithMethod(this ISetupActivity activity, Func> method) => activity.Set(x => x.Method, method); + + public static ISetupActivity WithMethod(this ISetupActivity activity, Func> method) => activity.Set(x => x.Method, method); + + public static ISetupActivity WithMethod(this ISetupActivity activity, Func method) => activity.Set(x => x.Method, method); + + public static ISetupActivity WithMethod(this ISetupActivity activity, Func method) => activity.Set(x => x.Method, method); + + public static ISetupActivity WithMethod(this ISetupActivity activity, string? method) => activity.Set(x => x.Method, method); + + + public static ISetupActivity WithContent(this ISetupActivity activity, Func> content + ) => activity.Set(x => x.Content, content); + + public static ISetupActivity WithContent(this ISetupActivity activity, Func> content) => activity.Set(x => x.Content, content); + + public static ISetupActivity WithContent(this ISetupActivity activity, Func content) => activity.Set(x => x.Content, content); + + public static ISetupActivity WithContent(this ISetupActivity activity, Func content) => activity.Set(x => x.Content, content); + + public static ISetupActivity WithContent(this ISetupActivity activity, string? content) => activity.Set(x => x.Content, content); + + + public static ISetupActivity WithContentType(this ISetupActivity activity, Func> contentType + ) => activity.Set(x => x.ContentType, contentType); + + public static ISetupActivity WithContentType(this ISetupActivity activity, Func> contentType) => activity.Set(x => x.ContentType, contentType); + + public static ISetupActivity WithContentType(this ISetupActivity activity, Func contentType) => activity.Set(x => x.ContentType, contentType); + + public static ISetupActivity WithContentType(this ISetupActivity activity, Func contentType) => activity.Set(x => x.ContentType, contentType); + + public static ISetupActivity WithContentType(this ISetupActivity activity, string? contentType) => activity.Set(x => x.ContentType, contentType); + + + public static ISetupActivity WithAuthorization(this ISetupActivity activity, Func> authorization + ) => activity.Set(x => x.Authorization, authorization); + + public static ISetupActivity WithAuthorization(this ISetupActivity activity, Func> authorization) => activity.Set(x => x.Authorization, authorization); + + public static ISetupActivity WithAuthorization(this ISetupActivity activity, Func authorization) => activity.Set(x => x.Authorization, authorization); + + public static ISetupActivity WithAuthorization(this ISetupActivity activity, Func authorization) => activity.Set(x => x.Authorization, authorization); + + public static ISetupActivity WithAuthorization(this ISetupActivity activity, string? authorization) => activity.Set(x => x.Authorization, authorization); + + + public static ISetupActivity WithRequestHeaders(this ISetupActivity activity, Func> requestHeaders + ) => activity.Set(x => x.RequestHeaders, requestHeaders); + + public static ISetupActivity WithRequestHeaders(this ISetupActivity activity, Func> requestHeaders) => activity.Set(x => x.RequestHeaders, requestHeaders); + + public static ISetupActivity WithRequestHeaders(this ISetupActivity activity, Func requestHeaders) => activity.Set(x => x.RequestHeaders, requestHeaders); + + public static ISetupActivity WithRequestHeaders(this ISetupActivity activity, Func requestHeaders) => activity.Set(x => x.RequestHeaders, requestHeaders); + + public static ISetupActivity WithRequestHeaders(this ISetupActivity activity, HttpRequestHeaders? requestHeaders) => activity.Set(x => x.RequestHeaders, requestHeaders); + + + public static ISetupActivity WithReadContent(this ISetupActivity activity, Func> readContent + ) => activity.Set(x => x.ReadContent, readContent); + + public static ISetupActivity WithReadContent(this ISetupActivity activity, Func> readContent) => activity.Set(x => x.ReadContent, readContent); + + public static ISetupActivity WithReadContent(this ISetupActivity activity, Func readContent) => activity.Set(x => x.ReadContent, readContent); + + public static ISetupActivity WithReadContent(this ISetupActivity activity, Func readContent) => activity.Set(x => x.ReadContent, readContent); + + public static ISetupActivity WithReadContent(this ISetupActivity activity, bool? readContent) => activity.Set(x => x.ReadContent, readContent); + + + public static ISetupActivity WithSupportedHttpCodes(this ISetupActivity activity, Func?>> supportedStatusCodes + ) => activity.Set(x => x.SupportedStatusCodes, supportedStatusCodes); + + public static ISetupActivity WithSupportedHttpCodes(this ISetupActivity activity, Func?>> supportedStatusCodes) => activity.Set(x => x.SupportedStatusCodes, supportedStatusCodes); + + public static ISetupActivity WithSupportedHttpCodes(this ISetupActivity activity, Func> supportedStatusCodes) => activity.Set(x => x.SupportedStatusCodes, supportedStatusCodes); + + public static ISetupActivity WithSupportedHttpCodes(this ISetupActivity activity, Func> supportedStatusCodes) => activity.Set(x => x.SupportedStatusCodes, supportedStatusCodes); + + public static ISetupActivity WithSupportedHttpCodes(this ISetupActivity activity, ICollection supportedStatusCodes) => activity.Set(x => x.SupportedStatusCodes, supportedStatusCodes); + } +} diff --git a/src/core/Elsa.Abstractions/Design/IRuntimeSelectListItemsProvider.cs b/src/core/Elsa.Abstractions/Design/IRuntimeSelectListItemsProvider.cs new file mode 100644 index 000000000..20722e405 --- /dev/null +++ b/src/core/Elsa.Abstractions/Design/IRuntimeSelectListItemsProvider.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Elsa.Design +{ + public interface IRuntimeSelectListItemsProvider + { + ValueTask> GetItemsAsync(object? context = default, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Design/RuntimeSelectListItemsProviderSettings.cs b/src/core/Elsa.Abstractions/Design/RuntimeSelectListItemsProviderSettings.cs new file mode 100644 index 000000000..883b54639 --- /dev/null +++ b/src/core/Elsa.Abstractions/Design/RuntimeSelectListItemsProviderSettings.cs @@ -0,0 +1,30 @@ +using System; +using Elsa.Serialization.Converters; +using Newtonsoft.Json; + +namespace Elsa.Design +{ + /// + /// Represents settings to be used by the designer to invoke an Elsa's API endpoint to retrieve list options at runtime. + /// + public class RuntimeSelectListItemsProviderSettings + { + public RuntimeSelectListItemsProviderSettings(Type providerType, object? context = default) + { + RuntimeSelectListItemsProviderType = providerType; + Context = context; + } + + /// + /// The type of the list items provider. + /// + [JsonConverter(typeof(FullTypeJsonConverter))] + public Type RuntimeSelectListItemsProviderType { get; } + + /// + /// Optionally provide an object containing useful information for the list select items provider to determine what items to provide. + /// + [JsonProperty(TypeNameHandling = TypeNameHandling.All)] + public object? Context { get; } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Converters/FullTypeJsonConverter.cs b/src/core/Elsa.Abstractions/Serialization/Converters/FullTypeJsonConverter.cs new file mode 100644 index 000000000..8247a71eb --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Converters/FullTypeJsonConverter.cs @@ -0,0 +1,23 @@ +using System; +using Newtonsoft.Json; + +namespace Elsa.Serialization.Converters +{ + public class FullTypeJsonConverter : JsonConverter + { + public override bool CanRead => true; + public override bool CanWrite => true; + + public override void WriteJson(JsonWriter writer, Type? value, JsonSerializer serializer) + { + var typeName = value!.AssemblyQualifiedName; + serializer.Serialize(writer, typeName); + } + + public override Type ReadJson(JsonReader reader, Type objectType, Type? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var typeName = serializer.Deserialize(reader)!; + return typeName is not null and not "" ? Type.GetType(typeName)! : default!; + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Services/IWorkflowRegistry.cs b/src/core/Elsa.Abstractions/Services/IWorkflowRegistry.cs index 0d442febb..cc846b578 100644 --- a/src/core/Elsa.Abstractions/Services/IWorkflowRegistry.cs +++ b/src/core/Elsa.Abstractions/Services/IWorkflowRegistry.cs @@ -7,11 +7,34 @@ using Elsa.Services.Models; namespace Elsa.Services { + /// + /// Represents a complete store of all versions of all workflow blueprints. + /// public interface IWorkflowRegistry { + /// + /// Lists all versions of all workflow blueprints. + /// Task> ListAsync(CancellationToken cancellationToken = default); + + /// + /// Lists only those workflow blueprints that are published or have at least one non-finished workflow. + /// + Task> ListActiveAsync(CancellationToken cancellationToken = default); + + /// + /// Gets a single workflow blueprint with the specified ID for the specified tenant and version. + /// Task GetAsync(string id, string? tenantId, VersionOptions version, CancellationToken cancellationToken = default); + + /// + /// Returns all workflow blueprints that fit the specified predicate. + /// Task> FindManyAsync(Func predicate, CancellationToken cancellationToken = default); + + /// + /// Returns a single workflow blueprint that fits the specified predicate. + /// Task FindAsync(Func predicate, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Bookmarks/BookmarkIndexer.cs b/src/core/Elsa.Core/Bookmarks/BookmarkIndexer.cs index 2085c07bf..ef3f095a8 100644 --- a/src/core/Elsa.Core/Bookmarks/BookmarkIndexer.cs +++ b/src/core/Elsa.Core/Bookmarks/BookmarkIndexer.cs @@ -63,7 +63,7 @@ namespace Elsa.Bookmarks var workflowInstanceIds = workflowInstanceList.Select(x => x.Id).ToList(); await DeleteBookmarksAsync(workflowInstanceIds, cancellationToken); - var workflowBlueprints = await _workflowRegistry.ListAsync(cancellationToken); + var workflowBlueprints = await _workflowRegistry.ListActiveAsync(cancellationToken); var workflowBlueprintsDictionary = workflowBlueprints.ToDictionary(x => (x.Id, x.Version)); var entities = new List(); diff --git a/src/core/Elsa.Core/Decorators/CachingWorkflowRegistry.cs b/src/core/Elsa.Core/Decorators/CachingWorkflowRegistry.cs index ae39a11ea..32bb57a3d 100644 --- a/src/core/Elsa.Core/Decorators/CachingWorkflowRegistry.cs +++ b/src/core/Elsa.Core/Decorators/CachingWorkflowRegistry.cs @@ -29,6 +29,7 @@ namespace Elsa.Decorators } public async Task> ListAsync(CancellationToken cancellationToken) => await GetWorkflowBlueprints(cancellationToken); + public Task> ListActiveAsync(CancellationToken cancellationToken = default) => _workflowRegistry.ListActiveAsync(cancellationToken); public async Task GetAsync(string id, string? tenantId, VersionOptions version, CancellationToken cancellationToken) => await FindAsync(x => x.Id == id && x.TenantId == tenantId && x.WithVersion(version), cancellationToken); diff --git a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs index d22362682..9294698b5 100644 --- a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs +++ b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using Elsa.Builders; using Elsa.Consumers; using Elsa.Converters; using Elsa.Decorators; +using Elsa.Design; using Elsa.Dispatch; using Elsa.Dispatch.Consumers; using Elsa.Events; @@ -102,6 +103,7 @@ namespace Microsoft.Extensions.DependencyInjection } public static IServiceCollection AddActivityPropertyOptionsProvider(this IServiceCollection services) where T : class, IActivityPropertyOptionsProvider => services.AddSingleton(); + public static IServiceCollection AddRuntimeSelectItemsProvider(this IServiceCollection services) where T : class, IRuntimeSelectListItemsProvider => services.AddScoped(); public static IServiceCollection AddActivityTypeProvider(this IServiceCollection services) where T : class, IActivityTypeProvider => services.AddSingleton(); private static ElsaOptionsBuilder AddWorkflowsCore(this ElsaOptionsBuilder options) diff --git a/src/core/Elsa.Core/Services/WorkflowRegistry.cs b/src/core/Elsa.Core/Services/WorkflowRegistry.cs index 34fb18a65..207abd265 100644 --- a/src/core/Elsa.Core/Services/WorkflowRegistry.cs +++ b/src/core/Elsa.Core/Services/WorkflowRegistry.cs @@ -24,6 +24,7 @@ namespace Elsa.Services } public async Task> ListAsync(CancellationToken cancellationToken) => await GetWorkflowsInternalAsync(cancellationToken).ToListAsync(cancellationToken); + public async Task> ListActiveAsync(CancellationToken cancellationToken) => await ListActiveWorkflowsAsync(cancellationToken).ToListAsync(cancellationToken); public async Task GetAsync(string id, string? tenantId, VersionOptions version, CancellationToken cancellationToken) => await FindAsync(x => x.Id == id && x.TenantId == tenantId && x.WithVersion(version), cancellationToken); @@ -33,6 +34,26 @@ namespace Elsa.Services public async Task FindAsync(Func predicate, CancellationToken cancellationToken) => (await ListAsync(cancellationToken).Where(predicate).OrderByDescending(x => x.Version)).FirstOrDefault(); + + private async IAsyncEnumerable ListActiveWorkflowsAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var workflows = await ListAsync(cancellationToken); + + foreach (var workflow in workflows) + { + // If a workflow is not published, only consider it for processing if it has at least one non-ended workflow instance. + if (!workflow.IsPublished && !await WorkflowHasNonFinishedWorkflowsAsync(workflow, cancellationToken)) + continue; + + yield return workflow; + } + } + + private async Task WorkflowHasNonFinishedWorkflowsAsync(IWorkflowBlueprint workflowBlueprint, CancellationToken cancellationToken) + { + var count = await _workflowInstanceStore.CountAsync(new NonFinalizedWorkflowSpecification().WithWorkflowDefinition(workflowBlueprint.Id), cancellationToken); + return count > 0; + } private async IAsyncEnumerable GetWorkflowsInternalAsync([EnumeratorCancellation] CancellationToken cancellationToken) { diff --git a/src/core/Elsa.Core/Triggers/TriggerIndexer.cs b/src/core/Elsa.Core/Triggers/TriggerIndexer.cs index 71c35d375..cd5cf8046 100644 --- a/src/core/Elsa.Core/Triggers/TriggerIndexer.cs +++ b/src/core/Elsa.Core/Triggers/TriggerIndexer.cs @@ -36,7 +36,7 @@ namespace Elsa.Triggers public async Task IndexTriggersAsync(CancellationToken cancellationToken = default) { - var allWorkflowBlueprints = await _workflowRegistry.ListAsync(cancellationToken); + var allWorkflowBlueprints = await _workflowRegistry.ListActiveAsync(cancellationToken); var publishedWorkflowBlueprints = allWorkflowBlueprints.Where(x => x.IsPublished).ToList(); await IndexTriggersAsync(publishedWorkflowBlueprints, cancellationToken); await _mediator.Publish(new TriggerIndexingFinished(), cancellationToken); diff --git a/src/designer/elsa-workflows-studio/src/components.d.ts b/src/designer/elsa-workflows-studio/src/components.d.ts index 86b3c462e..0308d05b0 100644 --- a/src/designer/elsa-workflows-studio/src/components.d.ts +++ b/src/designer/elsa-workflows-studio/src/components.d.ts @@ -5,7 +5,7 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { ActivityDefinitionProperty, ActivityDescriptor, ActivityModel, ActivityPropertyDescriptor, VersionOptions, WorkflowBlueprint, WorkflowDefinition, WorkflowExecutionLogRecord, WorkflowModel } from "./models"; +import { ActivityDefinitionProperty, ActivityDescriptor, ActivityModel, ActivityPropertyDescriptor, SelectListItem, VersionOptions, WorkflowBlueprint, WorkflowDefinition, WorkflowExecutionLogRecord, WorkflowModel } from "./models"; import { LocationSegments, MatchResults, RouterHistory } from "@stencil/router"; import { MenuItem } from "./components/controls/elsa-context-menu/models"; import { DropdownButtonItem, DropdownButtonOrigin } from "./components/controls/elsa-dropdown-button/models"; @@ -20,6 +20,7 @@ export namespace Components { interface ElsaCheckListProperty { "propertyDescriptor": ActivityPropertyDescriptor; "propertyModel": ActivityDefinitionProperty; + "serverUrl": string; } interface ElsaCheckboxProperty { "propertyDescriptor": ActivityPropertyDescriptor; @@ -47,6 +48,7 @@ export namespace Components { interface ElsaDropdownProperty { "propertyDescriptor": ActivityPropertyDescriptor; "propertyModel": ActivityDefinitionProperty; + "serverUrl": string; } interface ElsaExpressionEditor { "context"?: string; @@ -65,6 +67,13 @@ export namespace Components { "placeHolder"?: string; "values"?: Array; } + interface ElsaInputTagsDropdown { + "dropdownValues"?: Array; + "fieldId"?: string; + "fieldName"?: string; + "placeHolder"?: string; + "values"?: Array; + } interface ElsaModalDialog { "hide": (animate: boolean) => Promise; "show": (animate: boolean) => Promise; @@ -96,6 +105,7 @@ export namespace Components { interface ElsaMultiTextProperty { "propertyDescriptor": ActivityPropertyDescriptor; "propertyModel": ActivityDefinitionProperty; + "serverUrl": string; } interface ElsaPager { "history"?: RouterHistory; @@ -130,6 +140,10 @@ export namespace Components { "monacoLibPath": string; "serverUrl": string; } + interface ElsaStudioWorkflowBlueprintView { + "match": MatchResults; + "serverUrl": string; + } interface ElsaStudioWorkflowDefinitionsEdit { "match": MatchResults; "monacoLibPath": string; @@ -159,6 +173,11 @@ export namespace Components { "hide": () => Promise; "show": (options: ToastNotificationOptions) => Promise; } + interface ElsaWorkflowBlueprintViewerScreen { + "getServerUrl": () => Promise; + "serverUrl": string; + "workflowDefinitionId": string; + } interface ElsaWorkflowDefinitionEditorNotifications { } interface ElsaWorkflowDefinitionEditorScreen { @@ -272,6 +291,12 @@ declare global { prototype: HTMLElsaInputTagsElement; new (): HTMLElsaInputTagsElement; }; + interface HTMLElsaInputTagsDropdownElement extends Components.ElsaInputTagsDropdown, HTMLStencilElement { + } + var HTMLElsaInputTagsDropdownElement: { + prototype: HTMLElsaInputTagsDropdownElement; + new (): HTMLElsaInputTagsDropdownElement; + }; interface HTMLElsaModalDialogElement extends Components.ElsaModalDialog, HTMLStencilElement { } var HTMLElsaModalDialogElement: { @@ -332,6 +357,12 @@ declare global { prototype: HTMLElsaStudioRootElement; new (): HTMLElsaStudioRootElement; }; + interface HTMLElsaStudioWorkflowBlueprintViewElement extends Components.ElsaStudioWorkflowBlueprintView, HTMLStencilElement { + } + var HTMLElsaStudioWorkflowBlueprintViewElement: { + prototype: HTMLElsaStudioWorkflowBlueprintViewElement; + new (): HTMLElsaStudioWorkflowBlueprintViewElement; + }; interface HTMLElsaStudioWorkflowDefinitionsEditElement extends Components.ElsaStudioWorkflowDefinitionsEdit, HTMLStencilElement { } var HTMLElsaStudioWorkflowDefinitionsEditElement: { @@ -374,6 +405,12 @@ declare global { prototype: HTMLElsaToastNotificationElement; new (): HTMLElsaToastNotificationElement; }; + interface HTMLElsaWorkflowBlueprintViewerScreenElement extends Components.ElsaWorkflowBlueprintViewerScreen, HTMLStencilElement { + } + var HTMLElsaWorkflowBlueprintViewerScreenElement: { + prototype: HTMLElsaWorkflowBlueprintViewerScreenElement; + new (): HTMLElsaWorkflowBlueprintViewerScreenElement; + }; interface HTMLElsaWorkflowDefinitionEditorNotificationsElement extends Components.ElsaWorkflowDefinitionEditorNotifications, HTMLStencilElement { } var HTMLElsaWorkflowDefinitionEditorNotificationsElement: { @@ -440,6 +477,7 @@ declare global { "elsa-dropdown-property": HTMLElsaDropdownPropertyElement; "elsa-expression-editor": HTMLElsaExpressionEditorElement; "elsa-input-tags": HTMLElsaInputTagsElement; + "elsa-input-tags-dropdown": HTMLElsaInputTagsDropdownElement; "elsa-modal-dialog": HTMLElsaModalDialogElement; "elsa-monaco": HTMLElsaMonacoElement; "elsa-multi-expression-editor": HTMLElsaMultiExpressionEditorElement; @@ -450,6 +488,7 @@ declare global { "elsa-script-property": HTMLElsaScriptPropertyElement; "elsa-single-line-property": HTMLElsaSingleLinePropertyElement; "elsa-studio-root": HTMLElsaStudioRootElement; + "elsa-studio-workflow-blueprint-view": HTMLElsaStudioWorkflowBlueprintViewElement; "elsa-studio-workflow-definitions-edit": HTMLElsaStudioWorkflowDefinitionsEditElement; "elsa-studio-workflow-definitions-list": HTMLElsaStudioWorkflowDefinitionsListElement; "elsa-studio-workflow-instances-list": HTMLElsaStudioWorkflowInstancesListElement; @@ -457,6 +496,7 @@ declare global { "elsa-studio-workflow-registry": HTMLElsaStudioWorkflowRegistryElement; "elsa-switch-cases-property": HTMLElsaSwitchCasesPropertyElement; "elsa-toast-notification": HTMLElsaToastNotificationElement; + "elsa-workflow-blueprint-viewer-screen": HTMLElsaWorkflowBlueprintViewerScreenElement; "elsa-workflow-definition-editor-notifications": HTMLElsaWorkflowDefinitionEditorNotificationsElement; "elsa-workflow-definition-editor-screen": HTMLElsaWorkflowDefinitionEditorScreenElement; "elsa-workflow-definitions-list-screen": HTMLElsaWorkflowDefinitionsListScreenElement; @@ -476,6 +516,7 @@ declare namespace LocalJSX { interface ElsaCheckListProperty { "propertyDescriptor"?: ActivityPropertyDescriptor; "propertyModel"?: ActivityDefinitionProperty; + "serverUrl"?: string; } interface ElsaCheckboxProperty { "propertyDescriptor"?: ActivityPropertyDescriptor; @@ -505,6 +546,7 @@ declare namespace LocalJSX { interface ElsaDropdownProperty { "propertyDescriptor"?: ActivityPropertyDescriptor; "propertyModel"?: ActivityDefinitionProperty; + "serverUrl"?: string; } interface ElsaExpressionEditor { "context"?: string; @@ -524,6 +566,14 @@ declare namespace LocalJSX { "placeHolder"?: string; "values"?: Array; } + interface ElsaInputTagsDropdown { + "dropdownValues"?: Array; + "fieldId"?: string; + "fieldName"?: string; + "onValueChanged"?: (event: CustomEvent>) => void; + "placeHolder"?: string; + "values"?: Array; + } interface ElsaModalDialog { } interface ElsaMonaco { @@ -554,6 +604,7 @@ declare namespace LocalJSX { interface ElsaMultiTextProperty { "propertyDescriptor"?: ActivityPropertyDescriptor; "propertyModel"?: ActivityDefinitionProperty; + "serverUrl"?: string; } interface ElsaPager { "history"?: RouterHistory; @@ -589,6 +640,10 @@ declare namespace LocalJSX { "monacoLibPath"?: string; "serverUrl"?: string; } + interface ElsaStudioWorkflowBlueprintView { + "match"?: MatchResults; + "serverUrl"?: string; + } interface ElsaStudioWorkflowDefinitionsEdit { "match"?: MatchResults; "monacoLibPath"?: string; @@ -616,6 +671,10 @@ declare namespace LocalJSX { } interface ElsaToastNotification { } + interface ElsaWorkflowBlueprintViewerScreen { + "serverUrl"?: string; + "workflowDefinitionId"?: string; + } interface ElsaWorkflowDefinitionEditorNotifications { } interface ElsaWorkflowDefinitionEditorScreen { @@ -671,6 +730,7 @@ declare namespace LocalJSX { "elsa-dropdown-property": ElsaDropdownProperty; "elsa-expression-editor": ElsaExpressionEditor; "elsa-input-tags": ElsaInputTags; + "elsa-input-tags-dropdown": ElsaInputTagsDropdown; "elsa-modal-dialog": ElsaModalDialog; "elsa-monaco": ElsaMonaco; "elsa-multi-expression-editor": ElsaMultiExpressionEditor; @@ -681,6 +741,7 @@ declare namespace LocalJSX { "elsa-script-property": ElsaScriptProperty; "elsa-single-line-property": ElsaSingleLineProperty; "elsa-studio-root": ElsaStudioRoot; + "elsa-studio-workflow-blueprint-view": ElsaStudioWorkflowBlueprintView; "elsa-studio-workflow-definitions-edit": ElsaStudioWorkflowDefinitionsEdit; "elsa-studio-workflow-definitions-list": ElsaStudioWorkflowDefinitionsList; "elsa-studio-workflow-instances-list": ElsaStudioWorkflowInstancesList; @@ -688,6 +749,7 @@ declare namespace LocalJSX { "elsa-studio-workflow-registry": ElsaStudioWorkflowRegistry; "elsa-switch-cases-property": ElsaSwitchCasesProperty; "elsa-toast-notification": ElsaToastNotification; + "elsa-workflow-blueprint-viewer-screen": ElsaWorkflowBlueprintViewerScreen; "elsa-workflow-definition-editor-notifications": ElsaWorkflowDefinitionEditorNotifications; "elsa-workflow-definition-editor-screen": ElsaWorkflowDefinitionEditorScreen; "elsa-workflow-definitions-list-screen": ElsaWorkflowDefinitionsListScreen; @@ -714,6 +776,7 @@ declare module "@stencil/core" { "elsa-dropdown-property": LocalJSX.ElsaDropdownProperty & JSXBase.HTMLAttributes; "elsa-expression-editor": LocalJSX.ElsaExpressionEditor & JSXBase.HTMLAttributes; "elsa-input-tags": LocalJSX.ElsaInputTags & JSXBase.HTMLAttributes; + "elsa-input-tags-dropdown": LocalJSX.ElsaInputTagsDropdown & JSXBase.HTMLAttributes; "elsa-modal-dialog": LocalJSX.ElsaModalDialog & JSXBase.HTMLAttributes; "elsa-monaco": LocalJSX.ElsaMonaco & JSXBase.HTMLAttributes; "elsa-multi-expression-editor": LocalJSX.ElsaMultiExpressionEditor & JSXBase.HTMLAttributes; @@ -724,6 +787,7 @@ declare module "@stencil/core" { "elsa-script-property": LocalJSX.ElsaScriptProperty & JSXBase.HTMLAttributes; "elsa-single-line-property": LocalJSX.ElsaSingleLineProperty & JSXBase.HTMLAttributes; "elsa-studio-root": LocalJSX.ElsaStudioRoot & JSXBase.HTMLAttributes; + "elsa-studio-workflow-blueprint-view": LocalJSX.ElsaStudioWorkflowBlueprintView & JSXBase.HTMLAttributes; "elsa-studio-workflow-definitions-edit": LocalJSX.ElsaStudioWorkflowDefinitionsEdit & JSXBase.HTMLAttributes; "elsa-studio-workflow-definitions-list": LocalJSX.ElsaStudioWorkflowDefinitionsList & JSXBase.HTMLAttributes; "elsa-studio-workflow-instances-list": LocalJSX.ElsaStudioWorkflowInstancesList & JSXBase.HTMLAttributes; @@ -731,6 +795,7 @@ declare module "@stencil/core" { "elsa-studio-workflow-registry": LocalJSX.ElsaStudioWorkflowRegistry & JSXBase.HTMLAttributes; "elsa-switch-cases-property": LocalJSX.ElsaSwitchCasesProperty & JSXBase.HTMLAttributes; "elsa-toast-notification": LocalJSX.ElsaToastNotification & JSXBase.HTMLAttributes; + "elsa-workflow-blueprint-viewer-screen": LocalJSX.ElsaWorkflowBlueprintViewerScreen & JSXBase.HTMLAttributes; "elsa-workflow-definition-editor-notifications": LocalJSX.ElsaWorkflowDefinitionEditorNotifications & JSXBase.HTMLAttributes; "elsa-workflow-definition-editor-screen": LocalJSX.ElsaWorkflowDefinitionEditorScreen & JSXBase.HTMLAttributes; "elsa-workflow-definitions-list-screen": LocalJSX.ElsaWorkflowDefinitionsListScreen & JSXBase.HTMLAttributes; diff --git a/src/designer/elsa-workflows-studio/src/components/controls/elsa-input-tags/elsa-input-tags-dropdown.css b/src/designer/elsa-workflows-studio/src/components/controls/elsa-input-tags/elsa-input-tags-dropdown.css new file mode 100644 index 000000000..cce319279 --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/controls/elsa-input-tags/elsa-input-tags-dropdown.css @@ -0,0 +1,4 @@ +:host { + display: block; +} + diff --git a/src/designer/elsa-workflows-studio/src/components/controls/elsa-input-tags/elsa-input-tags-dropdown.tsx b/src/designer/elsa-workflows-studio/src/components/controls/elsa-input-tags/elsa-input-tags-dropdown.tsx new file mode 100644 index 000000000..5334de298 --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/controls/elsa-input-tags/elsa-input-tags-dropdown.tsx @@ -0,0 +1,112 @@ +import { Component, h, Prop, Event, EventEmitter, State, Watch } from '@stencil/core'; +import { SelectListItem } from '../../../models'; + +@Component({ + tag: 'elsa-input-tags-dropdown', + styleUrl: 'elsa-input-tags-dropdown.css', + shadow: false, +}) +export class ElsaInputTagsDropdown { + + @Prop() fieldName?: string; + @Prop() fieldId?: string; + @Prop() placeHolder?: string = 'Add tag'; + @Prop() values?: Array = []; + @Prop() dropdownValues?: Array = []; + @Event({ bubbles: true }) valueChanged: EventEmitter>; + @State() currentValues?: Array = []; + @State() dropdownTags?: Array = []; + + @Watch('values') + valuesChangedHandler(newValue: Array) { + let values: Array = []; + + newValue.forEach(value => { + this.dropdownValues.forEach(tag => { + if (value === tag.value) { + values.push(tag); + } + }) + }) + this.currentValues = values || []; + } + + componentWillLoad() { + this.dropdownTags = this.dropdownValues; + let values: Array = []; + + this.values.forEach(value => { + this.dropdownValues.forEach(tag => { + if (value === tag.value) { + values.push(tag); + } + }) + }) + this.currentValues = values; + + const tagsToRemove: Array = this.currentValues.map(tag => tag.value); + this.dropdownTags = this.dropdownTags.filter(tag => !tagsToRemove.includes(tag.value)); + } + + async onTagSelected(e: any) { + e.preventDefault(); + + const input = e.target as HTMLSelectElement; + const currentTag: SelectListItem = { + text: input.options[input.selectedIndex].text.trim(), + value: input.value + } + + if (currentTag.value.length == 0) + return; + + const values: Array = [...this.currentValues]; + values.push(currentTag); + this.currentValues = values.distinct(); + input.value = "Add"; + this.dropdownTags = this.dropdownTags.filter(tag => tag.value !== currentTag.value); + await this.valueChanged.emit(values); + } + + async onDeleteTagClick(e: any, currentTag: SelectListItem) { + e.preventDefault(); + + this.currentValues = this.currentValues.filter(tag => tag.value !== currentTag.value); + this.dropdownTags = [...this.dropdownTags, currentTag].sort((tag1, tag2) => tag1.value.localeCompare(tag2.value)); + await this.valueChanged.emit(this.currentValues); + } + + render() { + let values: Array = this.currentValues || []; + + if (!Array.isArray(values)) + values = []; + + const valuesJson = JSON.stringify(values.map(tag => tag.value)); + + return ( +
+ {values.map(tag => + ( + this.onDeleteTagClick(e, tag)} class="inline-block text-xs bg-blue-400 text-white py-2 px-3 mr-1 mb-1 rounded"> + + {tag.text} + × + + ))} + + + +
+ ) + } +} \ No newline at end of file diff --git a/src/designer/elsa-workflows-studio/src/components/dashboard/pages/elsa-studio-root/elsa-studio-root.tsx b/src/designer/elsa-workflows-studio/src/components/dashboard/pages/elsa-studio-root/elsa-studio-root.tsx index aea910a81..32709cfe3 100644 --- a/src/designer/elsa-workflows-studio/src/components/dashboard/pages/elsa-studio-root/elsa-studio-root.tsx +++ b/src/designer/elsa-workflows-studio/src/components/dashboard/pages/elsa-studio-root/elsa-studio-root.tsx @@ -52,6 +52,7 @@ export class ElsaStudioRoot { } exact={true}/> + diff --git a/src/designer/elsa-workflows-studio/src/components/dashboard/pages/elsa-studio-workflow-blueprint-view/elsa-studio-workflow-blueprint-view.tsx b/src/designer/elsa-workflows-studio/src/components/dashboard/pages/elsa-studio-workflow-blueprint-view/elsa-studio-workflow-blueprint-view.tsx new file mode 100644 index 000000000..b26148455 --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/dashboard/pages/elsa-studio-workflow-blueprint-view/elsa-studio-workflow-blueprint-view.tsx @@ -0,0 +1,25 @@ +import {Component, Prop, h} from '@stencil/core'; +import {MatchResults} from '@stencil/router'; + +@Component({ + tag: 'elsa-studio-workflow-blueprint-view', + shadow: false, +}) +export class ElsaStudioWorkflowBlueprintView { + @Prop() match: MatchResults; + @Prop() serverUrl: string; + + id?: string; + + componentWillLoad() { + this.id = this.match.params.id; + } + + render() { + const id = this.id; + + return
+ +
; + } +} diff --git a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx index 565c92474..7d6e3d406 100644 --- a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx +++ b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx @@ -1,5 +1,5 @@ import {Component, Host, h, Prop, State, Event, EventEmitter, Watch} from '@stencil/core'; -import { v4 as uuid } from 'uuid'; +import {v4 as uuid} from 'uuid'; import {Map, addConnection, findActivity, getChildActivities, getInboundConnections, getOutboundConnections, removeActivity, removeConnection} from '../../../../utils/utils'; import { ActivityDescriptor, @@ -94,7 +94,9 @@ export class ElsaWorkflowDesigner { updateWorkflowModel(model: WorkflowModel) { this.workflowModel = model; this.workflowChanged.emit(model); - this.rerenderTree(); + setTimeout(() => { + this.rerenderTree(); + }, 50); } connectedCallback() { @@ -339,13 +341,12 @@ export class ElsaWorkflowDesigner { this.workflowModel.connections.forEach(({sourceId, targetId, outcome}) => { const sourceName = `${sourceId}/${outcome}`; - - if(!this.graph.hasNode(sourceName)) - { + + if (!this.graph.hasNode(sourceName)) { console.warn(`No source node with ID '${sourceName}' exists.`); return; } - + this.graph.setEdge(sourceName, targetId, {arrowhead: 'undirected'}); }); } @@ -373,7 +374,9 @@ export class ElsaWorkflowDesigner { if (this.editMode) { d3.selectAll('.node.add').each((n: any) => { const node = this.graph.node(n) as any; - d3.select(node.elem).on('click', e => { + + d3.select(node.elem) + .on('click', e => { e.preventDefault(); d3.selectAll('.node.add svg').classed('text-green-400', false).classed('text-gray-400', true).classed('hover:text-blue-500', true); this.parentActivityId = node.activity.activityId; @@ -385,6 +388,14 @@ export class ElsaWorkflowDesigner { } this.showActivityPicker(); + }) + .on("mouseover", e => { + if (this.editMode && e.shiftKey) + d3.select(node.elem).select('svg').classed('text-green-400', true).classed('hover:text-blue-500', false); + }) + .on("mouseout", e => { + if (this.editMode) + d3.select(node.elem).select('svg').classed('text-green-400', false).classed('hover:text-blue-500', true); }); }); @@ -515,27 +526,19 @@ export class ElsaWorkflowDesigner { ${contextButton} - ${this.renderActivityBody(activity.description)} + ${this.renderActivityBody(displayContext)} `; } - renderActivityBody(description: string | null) { - if (!description) return ''; + renderActivityBody(displayContext: ActivityDesignDisplayContext) { return ( `
- ${description} +
${displayContext.activityModel.activityId}
+
${displayContext.bodyDisplay}
` ); } - // private onActivitySelected(e: CustomEvent) { - // this.activitySelected.emit(e.detail); - // } - - // private onActivityDeselected(e: CustomEvent) { - // this.activityDeselected.emit(e.detail); - // } - render() { return ( (this.el = el)}> diff --git a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-check-list-property/elsa-check-list-property.tsx b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-check-list-property/elsa-check-list-property.tsx index b726f882d..7eb0196ef 100644 --- a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-check-list-property/elsa-check-list-property.tsx +++ b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-check-list-property/elsa-check-list-property.tsx @@ -1,6 +1,9 @@ import {Component, h, Prop, State} from '@stencil/core'; import {ActivityDefinitionProperty, ActivityPropertyDescriptor, SyntaxNames} from "../../../../models"; import {parseJson} from "../../../../utils/utils"; +import {getSelectListItems} from "../../../../utils/select-list-items"; +import Tunnel from "../../../../data/workflow-editor"; +import {ElsaDropdownProperty} from "../elsa-dropdown-property/elsa-dropdown-property"; @Component({ tag: 'elsa-check-list-property', @@ -11,8 +14,11 @@ export class ElsaCheckListProperty { @Prop() propertyDescriptor: ActivityPropertyDescriptor; @Prop() propertyModel: ActivityDefinitionProperty; + @Prop({mutable: true}) serverUrl: string; @State() currentValue?: string; + monacoEditor: HTMLElsaMonacoElement; + items: any[]; async componentWillLoad() { this.currentValue = this.propertyModel.expressions[SyntaxNames.Json] || '[]'; @@ -37,11 +43,15 @@ export class ElsaCheckListProperty { this.currentValue = e.detail; } + async componentWillRender(){ + this.items = await getSelectListItems(this.serverUrl, this.propertyDescriptor); + } + render() { const propertyDescriptor = this.propertyDescriptor; const propertyModel = this.propertyModel; const fieldId = propertyDescriptor.name; - const options = propertyDescriptor.options as Array; + const items = this.items; const values = parseJson(this.currentValue) || []; return ( @@ -51,11 +61,11 @@ export class ElsaCheckListProperty { editor-height="2.75em" single-line={true}>
- {options.map((option, index) => { + {items.map((item, index) => { const inputId = `${fieldId}_${index}`; - const optionIsString = typeof(option) == 'string'; - const value = optionIsString ? option : option.value; - const text = optionIsString ? option : option.text; + const optionIsString = typeof(item) == 'string'; + const value = optionIsString ? item : item.value; + const text = optionIsString ? item : item.text; const isSelected = values.findIndex(x => x == value) >= 0; return ( @@ -74,3 +84,5 @@ export class ElsaCheckListProperty { ); } } + +Tunnel.injectProps(ElsaCheckListProperty, ['serverUrl']); \ No newline at end of file diff --git a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-dropdown-property/elsa-dropdown-property.tsx b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-dropdown-property/elsa-dropdown-property.tsx index 7c1a25615..75b472e06 100644 --- a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-dropdown-property/elsa-dropdown-property.tsx +++ b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-dropdown-property/elsa-dropdown-property.tsx @@ -1,5 +1,8 @@ import {Component, h, Prop, State} from '@stencil/core'; -import {ActivityDefinitionProperty, ActivityPropertyDescriptor, SyntaxNames} from "../../../../models"; +import {ActivityDefinitionProperty, ActivityPropertyDescriptor, RuntimeSelectListItemsProviderSettings, SelectListItem, SyntaxNames} from "../../../../models"; +import Tunnel from "../../../../data/workflow-editor"; +import {createElsaClient} from "../../../../services/elsa-client"; +import {getSelectListItems} from "../../../../utils/select-list-items"; @Component({ tag: 'elsa-dropdown-property', @@ -10,11 +13,15 @@ export class ElsaDropdownProperty { @Prop() propertyDescriptor: ActivityPropertyDescriptor; @Prop() propertyModel: ActivityDefinitionProperty; + @Prop({mutable: true}) serverUrl: string; @State() currentValue?: string; + + items: any[]; async componentWillLoad() { const defaultSyntax = this.propertyDescriptor.defaultSyntax || SyntaxNames.Literal; this.currentValue = this.propertyModel.expressions[defaultSyntax] || ''; + this.items = await getSelectListItems(this.serverUrl, this.propertyDescriptor); } onChange(e: Event) { @@ -26,15 +33,15 @@ export class ElsaDropdownProperty { onDefaultSyntaxValueChanged(e: CustomEvent) { this.currentValue = e.detail; } - + render() { const propertyDescriptor = this.propertyDescriptor; const propertyModel = this.propertyModel; const propertyName = propertyDescriptor.name; const fieldId = propertyName; const fieldName = propertyName; - const options = propertyDescriptor.options as Array || []; const currentValue = this.currentValue; + const items = this.items; return ( @@ -54,3 +61,5 @@ export class ElsaDropdownProperty { ); } } + +Tunnel.injectProps(ElsaDropdownProperty, ['serverUrl']); \ No newline at end of file diff --git a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-multi-text-property/elsa-multi-text-property.tsx b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-multi-text-property/elsa-multi-text-property.tsx index 087c81494..564f11c54 100644 --- a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-multi-text-property/elsa-multi-text-property.tsx +++ b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-multi-text-property/elsa-multi-text-property.tsx @@ -1,6 +1,8 @@ import {Component, h, Prop, State} from '@stencil/core'; -import {ActivityDefinitionProperty, ActivityPropertyDescriptor, SyntaxNames} from "../../../../models"; +import {ActivityDefinitionProperty, ActivityPropertyDescriptor, SyntaxNames, SelectListItem} from "../../../../models"; import {parseJson} from "../../../../utils/utils"; +import Tunnel from "../../../../data/workflow-editor"; +import {getSelectListItems} from "../../../../utils/select-list-items"; @Component({ tag: 'elsa-multi-text-property', @@ -11,14 +13,25 @@ export class ElsaMultiTextProperty { @Prop() propertyDescriptor: ActivityPropertyDescriptor; @Prop() propertyModel: ActivityDefinitionProperty; - @State() currentValue?: string + @Prop({mutable: true}) serverUrl: string; + @State() currentValue?: string; + + items: any[]; async componentWillLoad() { this.currentValue = this.propertyModel.expressions[SyntaxNames.Json] || '[]'; } - onValueChanged(newValue: Array){ - this.currentValue = JSON.stringify(newValue); + onValueChanged(newValue: Array) { + const newValues = newValue.map(dropdown => { + if (typeof dropdown === 'string') return dropdown; + if (typeof dropdown === 'number') return dropdown.toString(); + if (typeof dropdown === 'boolean') return dropdown.toString(); + + return dropdown.value; + }) + + this.currentValue = JSON.stringify(newValues); this.propertyModel.expressions[SyntaxNames.Json] = this.currentValue; } @@ -26,6 +39,17 @@ export class ElsaMultiTextProperty { this.currentValue = e.detail; } + createKeyValueOptions(options: Array) { + if (options === null) + return options; + + return options.map(option => typeof option === 'string' ? { text: option, value: option } : option); + } + + async componentWillRender(){ + this.items = await getSelectListItems(this.serverUrl, this.propertyDescriptor); + } + render() { const propertyDescriptor = this.propertyDescriptor; const propertyModel = this.propertyModel; @@ -33,15 +57,24 @@ export class ElsaMultiTextProperty { const fieldId = propertyName; const fieldName = propertyName; const values = parseJson(this.currentValue); - + const items = this.items; + const valueType = propertyDescriptor.options !== null ? 'dropdown' : 'multi-text'; + const propertyOptions = this.createKeyValueOptions(items); + + const elsaInputTags = valueType === 'multi-text' ? + this.onValueChanged(e.detail)} /> : + this.onValueChanged(e.detail)} />; + return ( this.onDefaultSyntaxValueChanged(e)} editor-height="2em" single-line={true}> - this.onValueChanged(e.detail)}/> + {elsaInputTags} ) } } + +Tunnel.injectProps(ElsaMultiTextProperty, ['serverUrl']); diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/elsa-workflow-blueprint-viewer-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/elsa-workflow-blueprint-viewer-screen.tsx new file mode 100644 index 000000000..45f99359c --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/elsa-workflow-blueprint-viewer-screen.tsx @@ -0,0 +1,153 @@ +import {Component, h, Host, Method, Prop, State, Watch} from '@stencil/core'; +import * as collection from 'lodash/collection'; +import { + ActivityBlueprint, ActivityDefinitionProperty, + ActivityDescriptor, + ActivityModel, Connection, + ConnectionModel, + SyntaxNames, + WorkflowBlueprint, WorkflowModel, + WorkflowPersistenceBehavior +} from "../../../../models"; +import {createElsaClient} from "../../../../services/elsa-client"; +import {pluginManager} from '../../../../services/plugin-manager'; +import state from '../../../../utils/store'; + +@Component({ + tag: 'elsa-workflow-blueprint-viewer-screen', + shadow: false, +}) +export class ElsaWorkflowBlueprintViewerScreen { + + constructor() { + pluginManager.initialize(); + } + + @Prop() workflowDefinitionId: string; + @Prop({attribute: 'server-url', reflect: true}) serverUrl: string; + @State() workflowBlueprint: WorkflowBlueprint; + @State() workflowModel: WorkflowModel; + el: HTMLElement; + designer: HTMLElsaDesignerTreeElement; + + @Method() + async getServerUrl(): Promise { + return this.serverUrl; + } + + @Watch('workflowDefinitionId') + async workflowDefinitionIdChangedHandler(newValue: string) { + const workflowDefinitionId = newValue; + let workflowBlueprint: WorkflowBlueprint = { + id: null, + version: 1, + activities: [], + connections: [], + persistenceBehavior: WorkflowPersistenceBehavior.WorkflowBurst, + customAttributes: {data: {}}, + persistWorkflow: false, + isLatest: false, + isPublished: false, + loadWorkflowContext: false, + isSingleton: false, + persistOutput: false, + saveWorkflowContext: false, + variables: {data: {}}, + type: null, + properties: {data: {}} + }; + + const client = createElsaClient(this.serverUrl); + + if (workflowDefinitionId && workflowDefinitionId.length > 0) { + try { + workflowBlueprint = await client.workflowRegistryApi.get(workflowDefinitionId, {isLatest: true}); + } catch { + console.warn(`The specified workflow blueprint does not exist. Creating a new one.`); + } + } + + this.updateModels(workflowBlueprint); + } + + @Watch("serverUrl") + async serverUrlChangedHandler(newValue: string) { + if (newValue && newValue.length > 0) + await this.loadActivityDescriptors(); + } + + async componentWillLoad() { + await this.serverUrlChangedHandler(this.serverUrl); + await this.workflowDefinitionIdChangedHandler(this.workflowDefinitionId); + } + + componentDidLoad() { + if (!this.designer) { + this.designer = this.el.querySelector("elsa-designer-tree") as HTMLElsaDesignerTreeElement; + this.designer.model = this.workflowModel; + } + } + + async loadActivityDescriptors() { + const client = createElsaClient(this.serverUrl); + state.activityDescriptors = await client.activitiesApi.list(); + } + + updateModels(workflowBlueprint: WorkflowBlueprint) { + this.workflowBlueprint = workflowBlueprint; + this.workflowModel = this.mapWorkflowModel(workflowBlueprint); + debugger; + } + + mapWorkflowModel(workflowBlueprint: WorkflowBlueprint): WorkflowModel { + return { + activities: workflowBlueprint.activities.filter(x => x.parentId == null).map(this.mapActivityModel), + connections: workflowBlueprint.connections.map(this.mapConnectionModel), + persistenceBehavior: workflowBlueprint.persistenceBehavior, + }; + } + + mapActivityModel(source: ActivityBlueprint): ActivityModel { + const descriptors: Array = state.activityDescriptors; + const descriptor = descriptors.find(x => x.type == source.type); + const properties: Array = collection.map(source.properties.data, (v, k) => ({name: k, expressions: {'Literal': v}, syntax: SyntaxNames.Literal})); + + return { + activityId: source.id, + description: source.description, + displayName: source.displayName || source.name || source.type, + name: source.name, + type: source.type, + properties: properties, + outcomes: [...descriptor.outcomes], + persistOutput: source.persistOutput, + persistWorkflow: source.persistWorkflow, + saveWorkflowContext: source.saveWorkflowContext, + loadWorkflowContext: source.loadWorkflowContext + } + } + + mapConnectionModel(connection: Connection): ConnectionModel { + return { + sourceId: connection.sourceActivityId, + targetId: connection.targetActivityId, + outcome: connection.outcome + } + } + + render() { + return ( + this.el = el}> + {this.renderCanvas()} + + ); + } + + renderCanvas() { + return ( +
+ this.designer = el} editMode={false}/> +
+ ); + } +} diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/readme.md b/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/readme.md new file mode 100644 index 000000000..3d723dc3d --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/readme.md @@ -0,0 +1,51 @@ +# elsa-workflow-instance-viewer-screen + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| -------------------- | ---------------------- | ----------- | -------- | ----------- | +| `serverUrl` | `server-url` | | `string` | `undefined` | +| `workflowInstanceId` | `workflow-instance-id` | | `string` | `undefined` | + + +## Methods + +### `getServerUrl() => Promise` + + + +#### Returns + +Type: `Promise` + + + + +## Dependencies + +### Used by + + - [elsa-studio-workflow-instances-view](../../../dashboard/pages/elsa-studio-workflow-instances-view) + +### Depends on + +- [elsa-workflow-instance-journal](.) +- [elsa-designer-tree](../../../designers/tree/elsa-designer-tree) + +### Graph +```mermaid +graph TD; + elsa-workflow-instance-viewer-screen --> elsa-workflow-instance-journal + elsa-workflow-instance-viewer-screen --> elsa-designer-tree + elsa-studio-workflow-instances-view --> elsa-workflow-instance-viewer-screen + style elsa-workflow-instance-viewer-screen fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal.tsx index 112edcd71..0a998080f 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal.tsx @@ -19,13 +19,18 @@ export class ElsaActivityEditorModal { form: HTMLFormElement; formContext: FormContext; renderProps: any; + + // Force a new key every time we show the editor to make sure Stencil creates new components. + // This prevents the issue where the designer has e.g. one activity where the user edits the properties, cancels out, then opens the editor again, seeing the entered value still there. + timestamp: Date = new Date(); componentDidLoad() { eventBus.on(EventTypes.ShowActivityEditor, async (activity: ActivityModel, animate: boolean) => { this.activityModel = JSON.parse(JSON.stringify(activity)); this.activityDescriptor = state.activityDescriptors.find(x => x.type == activity.type); this.formContext = new FormContext(this.activityModel, newValue => this.activityModel = newValue); - this.selectedTab = 'Properties'; + this.selectedTab = 'Properties'; + this.timestamp = new Date(); await this.dialog.show(animate); }); } @@ -99,9 +104,9 @@ export class ElsaActivityEditorModal { return ( - this.dialog = el}> + this.dialog = el} >
-
this.onSubmit(e)} ref={el => this.form = el}> + this.onSubmit(e)} ref={el => this.form = el} key={this.timestamp.getTime().toString()}>
diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx index 0569cbd23..a622d1cf6 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx @@ -62,9 +62,6 @@ export class ElsaWorkflowDefinitionEditorScreen { this.imported = false; this.networkError = null; - // A small hack to make sure JS Plumb is cleaned up before HTML elements get removed. - // await this.designer.destroyJsPlumb(); - try { const workflowDefinition = await client.workflowDefinitionsApi.import(this.workflowDefinition.definitionId, file); this.workflowDefinition = workflowDefinition; diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-journal.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-journal/elsa-workflow-instance-journal.tsx similarity index 97% rename from src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-journal.tsx rename to src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-journal/elsa-workflow-instance-journal.tsx index 163fa4483..447aa1218 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-journal.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-journal/elsa-workflow-instance-journal.tsx @@ -2,7 +2,6 @@ import {Component, EventEmitter, h, Host, Method, Prop, State, Watch, Event} fro import * as collection from 'lodash/collection'; import moment from 'moment'; import {enter, leave, toggle} from 'el-transition' -import {registerClickOutside} from "stencil-click-outside"; import { ActivityBlueprint, ActivityDescriptor, PagedList, WorkflowBlueprint, WorkflowExecutionLogRecord, WorkflowModel, @@ -71,8 +70,7 @@ export class ElsaWorkflowInstanceJournal { @Method() async selectActivityRecord(activityId?: string) { const record = !!activityId ? this.records.items.find(x => x.activityId == activityId) : null; - this.selectedRecordId = !!record ? record.id : null; - this.selectedActivityId = activityId; + this.selectActivityRecordInternal(record); await this.show(); } @@ -94,6 +92,12 @@ export class ElsaWorkflowInstanceJournal { await this.workflowInstanceIdChangedHandler(this.workflowInstanceId); } + selectActivityRecordInternal(record?: WorkflowExecutionLogRecord) { + const activity = !!record ? this.workflowBlueprint.activities.find(x => x.id === record.activityId) : null; + this.selectedRecordId = !!record ? record.id : null; + this.selectedActivityId = activity != null ? activity.parentId != null ? activity.parentId : activity.id : null; + } + getEventColor(eventName: string) { const map = { 'Executing': 'blue', @@ -115,8 +119,7 @@ export class ElsaWorkflowInstanceJournal { } onRecordClick(record: WorkflowExecutionLogRecord) { - this.selectedRecordId = record.id; - this.selectedActivityId = record.activityId; + this.selectActivityRecordInternal(record); this.recordSelected.emit(record); } @@ -246,7 +249,7 @@ export class ElsaWorkflowInstanceJournal { if (!value) continue; - + let valueText = null; if (typeof value == 'string') diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-viewer-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-viewer-screen.tsx index bc3c9eb00..575422f8f 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-viewer-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-viewer-screen.tsx @@ -121,7 +121,7 @@ export class ElsaWorkflowInstanceViewerScreen { mapWorkflowModel(workflowBlueprint: WorkflowBlueprint): WorkflowModel { return { - activities: workflowBlueprint.activities.map(this.mapActivityModel), + activities: workflowBlueprint.activities.filter(x => x.parentId == null).map(this.mapActivityModel), connections: workflowBlueprint.connections.map(this.mapConnectionModel), persistenceBehavior: workflowBlueprint.persistenceBehavior, }; @@ -160,7 +160,9 @@ export class ElsaWorkflowInstanceViewerScreen { } onRecordSelected(e: CustomEvent) { - this.selectedActivityId = e.detail.activityId; + const record = e.detail; + const activity = !!record ? this.workflowBlueprint.activities.find(x => x.id === record.activityId) : null; + this.selectedActivityId = activity != null ? activity.parentId != null ? activity.parentId : activity.id : null; } onActivitySelected(e: CustomEvent) { diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-registry-list/elsa-workflow-registry-list-screen/elsa-workflow-registry-list-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-registry-list/elsa-workflow-registry-list-screen/elsa-workflow-registry-list-screen.tsx index b6af33ff0..6d4ef756a 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-registry-list/elsa-workflow-registry-list-screen/elsa-workflow-registry-list-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-registry-list/elsa-workflow-registry-list-screen/elsa-workflow-registry-list-screen.tsx @@ -67,7 +67,7 @@ export class ElsaWorkflowRegistryListScreen { if (!workflowDisplayName || workflowDisplayName.trim().length == 0) workflowDisplayName = 'Untitled'; - const editUrl = `/workflow-definitions/${workflowBlueprint.id}`; + const editUrl = `/workflow-registry/${workflowBlueprint.id}`; const instancesUrl = `/workflow-instances?workflow=${workflowBlueprint.id}`; const editIcon = ( diff --git a/src/designer/elsa-workflows-studio/src/index.html b/src/designer/elsa-workflows-studio/src/index.html index 258f94cb1..b6402d31f 100644 --- a/src/designer/elsa-workflows-studio/src/index.html +++ b/src/designer/elsa-workflows-studio/src/index.html @@ -15,6 +15,8 @@ + + diff --git a/src/designer/elsa-workflows-studio/src/models/domain.ts b/src/designer/elsa-workflows-studio/src/models/domain.ts index 75993deec..8dec2b078 100644 --- a/src/designer/elsa-workflows-studio/src/models/domain.ts +++ b/src/designer/elsa-workflows-studio/src/models/domain.ts @@ -42,6 +42,7 @@ export interface ActivityBlueprint { displayName?: string; description?: string; type: string; + parentId?: string; persistWorkflow: boolean; loadWorkflowContext: boolean; saveWorkflowContext: boolean; @@ -286,6 +287,16 @@ export enum ActivityTraits { Job = 4 } +export interface SelectListItem { + text: string; + value: string; +} + +export interface RuntimeSelectListItemsProviderSettings { + runtimeSelectListItemsProviderType: string; + context?: any; +} + export class SyntaxNames { static readonly Literal = 'Literal'; static readonly JavaScript = 'JavaScript'; diff --git a/src/designer/elsa-workflows-studio/src/models/events.ts b/src/designer/elsa-workflows-studio/src/models/events.ts index 434b874c4..472db377a 100644 --- a/src/designer/elsa-workflows-studio/src/models/events.ts +++ b/src/designer/elsa-workflows-studio/src/models/events.ts @@ -27,7 +27,7 @@ export interface ActivityPickedEventArgs { export interface ActivityDesignDisplayContext { activityModel: ActivityModel; activityIcon: any; - bodyDisplay: any; + bodyDisplay: string; outcomes: Array; } diff --git a/src/designer/elsa-workflows-studio/src/plugins/cron-plugin.tsx b/src/designer/elsa-workflows-studio/src/plugins/cron-plugin.tsx index 2bfe34f84..10bb8d3cb 100644 --- a/src/designer/elsa-workflows-studio/src/plugins/cron-plugin.tsx +++ b/src/designer/elsa-workflows-studio/src/plugins/cron-plugin.tsx @@ -17,6 +17,6 @@ export class CronPlugin implements ElsaPlugin { const props = activityModel.properties || []; const condition = props.find(x => x.name == 'CronExpression') || { name: 'CronExpression', expressions: {'Literal': ''}, syntax: 'Literal'}; const expression = condition.expressions[condition.syntax] || ''; - context.bodyDisplay =

{expression}

; + context.bodyDisplay = `

${expression}

`; } } diff --git a/src/designer/elsa-workflows-studio/src/plugins/http-endpoint-plugin.tsx b/src/designer/elsa-workflows-studio/src/plugins/http-endpoint-plugin.tsx index a3379e3e6..4ba7bd28f 100644 --- a/src/designer/elsa-workflows-studio/src/plugins/http-endpoint-plugin.tsx +++ b/src/designer/elsa-workflows-studio/src/plugins/http-endpoint-plugin.tsx @@ -1,6 +1,6 @@ import {ElsaPlugin} from "../services/elsa-plugin"; import {eventBus} from '../services/event-bus'; -import {ActivityDesignDisplayContext, EventTypes} from "../models"; +import {ActivityDesignDisplayContext, EventTypes, SyntaxNames} from "../models"; import {h} from "@stencil/core"; export class HttpEndpointPlugin implements ElsaPlugin { @@ -15,7 +15,8 @@ export class HttpEndpointPlugin implements ElsaPlugin { return; const props = activityModel.properties || []; - const path = props.find(x => x.name == 'Path') || { name: 'Path', expressions: { 'Literal': '', syntax: 'Literal' } }; - context.bodyDisplay =

{path.expressions[path.syntax]}

; + const path = props.find(x => x.name == 'Path') || { name: 'Path', expressions: { 'Literal': '', syntax: SyntaxNames.Literal } }; + const syntax = path.syntax || SyntaxNames.Literal; + context.bodyDisplay = `

${path.expressions[syntax]}

`; } } diff --git a/src/designer/elsa-workflows-studio/src/plugins/if-plugin.tsx b/src/designer/elsa-workflows-studio/src/plugins/if-plugin.tsx index 47481fcca..3e169ddcb 100644 --- a/src/designer/elsa-workflows-studio/src/plugins/if-plugin.tsx +++ b/src/designer/elsa-workflows-studio/src/plugins/if-plugin.tsx @@ -19,6 +19,6 @@ export class IfPlugin implements ElsaPlugin { const expression = condition.expressions[condition.syntax] || ''; const description = activityModel.description; const bodyText = description && description.length > 0 ? description : expression; - context.bodyDisplay =

{bodyText}

; + context.bodyDisplay = `

${bodyText}

`; } } diff --git a/src/designer/elsa-workflows-studio/src/plugins/send-email-plugin.tsx b/src/designer/elsa-workflows-studio/src/plugins/send-email-plugin.tsx index 00d26e8db..3263e978c 100644 --- a/src/designer/elsa-workflows-studio/src/plugins/send-email-plugin.tsx +++ b/src/designer/elsa-workflows-studio/src/plugins/send-email-plugin.tsx @@ -19,6 +19,6 @@ export class SendEmailPlugin implements ElsaPlugin { const expression = to.expressions[to.syntax || SyntaxNames.Json] || ''; const description = activityModel.description; const bodyText = description && description.length > 0 ? description : expression; - context.bodyDisplay =

To: {bodyText}

; + context.bodyDisplay = `

To: ${bodyText}

`; } } diff --git a/src/designer/elsa-workflows-studio/src/plugins/start-at-plugin.tsx b/src/designer/elsa-workflows-studio/src/plugins/start-at-plugin.tsx index cbb63e7da..457d1e6e5 100644 --- a/src/designer/elsa-workflows-studio/src/plugins/start-at-plugin.tsx +++ b/src/designer/elsa-workflows-studio/src/plugins/start-at-plugin.tsx @@ -17,6 +17,6 @@ export class StartAtPlugin implements ElsaPlugin { const props = activityModel.properties || []; const condition = props.find(x => x.name == 'Instant') || { name: 'Instant', expressions: {'Literal': ''}, syntax: 'Literal'}; const expression = condition.expressions[condition.syntax] || ''; - context.bodyDisplay =

{expression}

; + context.bodyDisplay = `

${expression}

`; } } diff --git a/src/designer/elsa-workflows-studio/src/plugins/timer-plugin.tsx b/src/designer/elsa-workflows-studio/src/plugins/timer-plugin.tsx index 2af15e4ec..7707af4c7 100644 --- a/src/designer/elsa-workflows-studio/src/plugins/timer-plugin.tsx +++ b/src/designer/elsa-workflows-studio/src/plugins/timer-plugin.tsx @@ -17,6 +17,6 @@ export class TimerPlugin implements ElsaPlugin { const props = activityModel.properties || []; const condition = props.find(x => x.name == 'Timeout') || { name: 'Timeout', expressions: {'Literal': ''}, syntax: 'Literal'}; const expression = condition.expressions[condition.syntax] || ''; - context.bodyDisplay =

{expression}

; + context.bodyDisplay = `

${expression}

`; } } diff --git a/src/designer/elsa-workflows-studio/src/plugins/while-plugin.tsx b/src/designer/elsa-workflows-studio/src/plugins/while-plugin.tsx index 43f24c876..39b0ed7cb 100644 --- a/src/designer/elsa-workflows-studio/src/plugins/while-plugin.tsx +++ b/src/designer/elsa-workflows-studio/src/plugins/while-plugin.tsx @@ -19,6 +19,6 @@ export class WhilePlugin implements ElsaPlugin { const expression = condition.expressions[condition.syntax] || ''; const description = activityModel.description; const bodyText = description && description.length > 0 ? description : expression; - context.bodyDisplay =

{bodyText}

; + context.bodyDisplay = `

${bodyText}

`; } } diff --git a/src/designer/elsa-workflows-studio/src/plugins/write-line-plugin.tsx b/src/designer/elsa-workflows-studio/src/plugins/write-line-plugin.tsx index 1971f2529..c02f77343 100644 --- a/src/designer/elsa-workflows-studio/src/plugins/write-line-plugin.tsx +++ b/src/designer/elsa-workflows-studio/src/plugins/write-line-plugin.tsx @@ -17,6 +17,6 @@ export class WriteLinePlugin implements ElsaPlugin { const props = activityModel.properties || []; const condition = props.find(x => x.name == 'Text') || { name: 'Text', expressions: {'Literal': ''}, syntax: SyntaxNames.Literal }; const expression = condition.expressions[condition.syntax || 'Literal'] || ''; - context.bodyDisplay =

{expression}

; + context.bodyDisplay = `

${expression}

`; } } diff --git a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts index 1c13ac2cc..44141ae9d 100644 --- a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts +++ b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts @@ -5,7 +5,7 @@ import { ActivityDescriptor, ConnectionDefinition, getVersionOptionsString, OrderBy, - PagedList, + PagedList, SelectListItem, Variables, VersionOptions, WorkflowBlueprint, WorkflowBlueprintSummary, WorkflowContextOptions, @@ -152,6 +152,14 @@ export const createElsaClient = function (serverUrl: string): ElsaClient { const response = await httpClient.get(`v1/scripting/javascript/type-definitions/${workflowDefinitionId}?t=${new Date().getTime()}&context=${context}`); return response.data; } + }, + designerApi:{ + runtimeSelectItemsApi:{ + get: async (providerTypeName: string, context?: any): Promise> => { + const response = await httpClient.post('v1/designer/runtime-select-list-items', { providerTypeName: providerTypeName, context: context }); + return response.data; + } + } } } } @@ -163,6 +171,7 @@ export interface ElsaClient { workflowInstancesApi: WorkflowInstancesApi; workflowExecutionLogApi: WorkflowExecutionLogApi; scriptingApi: ScriptingApi; + designerApi: DesignerApi; } export interface ActivitiesApi { @@ -220,6 +229,14 @@ export interface ScriptingApi { getJavaScriptTypeDefinitions(workflowDefinitionId: string, context?: string): Promise } +export interface DesignerApi { + runtimeSelectItemsApi: RuntimeSelectItemsApi; +} + +export interface RuntimeSelectItemsApi { + get(providerTypeName: string, context?: any): Promise> +} + export interface SaveWorkflowDefinitionRequest { workflowDefinitionId?: string; name?: string; diff --git a/src/designer/elsa-workflows-studio/src/utils/select-list-items.ts b/src/designer/elsa-workflows-studio/src/utils/select-list-items.ts new file mode 100644 index 000000000..3d87cd470 --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/utils/select-list-items.ts @@ -0,0 +1,20 @@ +import {ActivityPropertyDescriptor, RuntimeSelectListItemsProviderSettings, SelectListItem} from "../models"; +import {createElsaClient, ElsaClient} from "../services/elsa-client"; + +async function fetchRuntimeItems(serverUrl: string, options: RuntimeSelectListItemsProviderSettings): Promise> { + const elsaClient = createElsaClient(serverUrl); + return await elsaClient.designerApi.runtimeSelectItemsApi.get(options.runtimeSelectListItemsProviderType, options.context || {}); +} + +export async function getSelectListItems(serverUrl: string, propertyDescriptor: ActivityPropertyDescriptor): Promise> { + const options = propertyDescriptor.options; + let items = []; + + if (!!options.runtimeSelectListItemsProviderType) { + items = await fetchRuntimeItems(serverUrl, options); + } else { + items = options as Array || []; + } + + return items || []; +} \ No newline at end of file diff --git a/src/persistence/Elsa.Persistence.EntityFramework/Elsa.Persistence.EntityFramework.SqlServer/DbContextOptionsBuilderExtensions.cs b/src/persistence/Elsa.Persistence.EntityFramework/Elsa.Persistence.EntityFramework.SqlServer/DbContextOptionsBuilderExtensions.cs new file mode 100644 index 000000000..f16e322b7 --- /dev/null +++ b/src/persistence/Elsa.Persistence.EntityFramework/Elsa.Persistence.EntityFramework.SqlServer/DbContextOptionsBuilderExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Persistence.EntityFramework.SqlServer +{ + public static class DbContextOptionsBuilderExtensions + { + /// + /// Configures the context to use SqlServer. + /// + public static DbContextOptionsBuilder UseSqlServer(this DbContextOptionsBuilder builder, string connectionString) => + builder.UseSqlServer(connectionString, db => db.MigrationsAssembly(typeof(SqlServerElsaContextFactory).Assembly.GetName().Name)); + } +} \ No newline at end of file diff --git a/src/persistence/Elsa.Persistence.YesSql/Documents/WorkflowInstanceDocument.cs b/src/persistence/Elsa.Persistence.YesSql/Documents/WorkflowInstanceDocument.cs index 61815684a..2320f89e0 100644 --- a/src/persistence/Elsa.Persistence.YesSql/Documents/WorkflowInstanceDocument.cs +++ b/src/persistence/Elsa.Persistence.YesSql/Documents/WorkflowInstanceDocument.cs @@ -37,7 +37,7 @@ namespace Elsa.Persistence.YesSql.Documents public WorkflowFault? Fault { get; set; } public SimpleStack ScheduledActivities { get; set; } = new(); - public SimpleStack Scopes { get; set; } = new(); + public SimpleStack Scopes { get; set; } = new(); public ScheduledActivity? CurrentActivity { get; set; } } } \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.SendHttp/Elsa.Samples.SendHttp.csproj b/src/samples/aspnet/Elsa.Samples.SendHttp/Elsa.Samples.SendHttp.csproj new file mode 100644 index 000000000..f8ceca7a9 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.SendHttp/Elsa.Samples.SendHttp.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + + + + + + + + diff --git a/src/samples/aspnet/Elsa.Samples.SendHttp/Program.cs b/src/samples/aspnet/Elsa.Samples.SendHttp/Program.cs new file mode 100644 index 000000000..33f467d85 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.SendHttp/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Elsa.Samples.SendHttp +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/samples/aspnet/Elsa.Samples.SendHttp/Properties/launchSettings.json b/src/samples/aspnet/Elsa.Samples.SendHttp/Properties/launchSettings.json new file mode 100644 index 000000000..142f1a8f1 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.SendHttp/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:8929", + "sslPort": 44312 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Elsa.Samples.SendHttp": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/samples/aspnet/Elsa.Samples.SendHttp/SendHttpWorkflow.cs b/src/samples/aspnet/Elsa.Samples.SendHttp/SendHttpWorkflow.cs new file mode 100644 index 000000000..f6b9daf0b --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.SendHttp/SendHttpWorkflow.cs @@ -0,0 +1,29 @@ +using Elsa.Activities.Http; +using Elsa.Activities.Http.Models; +using Elsa.Builders; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Elsa.Samples.SendHttp +{ + public class SendHttpWorkflow : IWorkflow + { + public void Build(IWorkflowBuilder builder) + { + builder.WithWorkflowDefinitionId("SendHttp") + .WithVersion(1) + .WithDescription("Send test http request and return response") + .HttpEndpoint(setup => setup.WithPath("/helloworld") + .WithMethod("GET")) + .SendHttpRequest(setup => setup.WithUrl(new Uri("https://jsonplaceholder.typicode.com/todos/3")) + .WithMethod("GET") + .WithReadContent(true)) + .WithName("TestHttpRequest") + .WriteHttpResponse(setup => setup.WithStatusCode(System.Net.HttpStatusCode.OK) + .WithContent(x => JsonSerializer.Serialize(x.GetOutputFrom("TestHttpRequest").Content))); + } + } +} diff --git a/src/samples/aspnet/Elsa.Samples.SendHttp/Startup.cs b/src/samples/aspnet/Elsa.Samples.SendHttp/Startup.cs new file mode 100644 index 000000000..cf3a4a6a7 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.SendHttp/Startup.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Elsa.Samples.SendHttp +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddElsa(options => options.AddHttpActivities() + .AddWorkflowsFrom()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseHttpActivities(); + } + } +} diff --git a/src/samples/aspnet/Elsa.Samples.SendHttp/appsettings.Development.json b/src/samples/aspnet/Elsa.Samples.SendHttp/appsettings.Development.json new file mode 100644 index 000000000..8983e0fc1 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.SendHttp/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/samples/aspnet/Elsa.Samples.SendHttp/appsettings.json b/src/samples/aspnet/Elsa.Samples.SendHttp/appsettings.json new file mode 100644 index 000000000..d9d9a9bff --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.SendHttp/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/samples/persistence/Elsa.Samples.Persistence.EntityFramework/Program.cs b/src/samples/persistence/Elsa.Samples.Persistence.EntityFramework/Program.cs index ee08ba59c..9e3eaee24 100644 --- a/src/samples/persistence/Elsa.Samples.Persistence.EntityFramework/Program.cs +++ b/src/samples/persistence/Elsa.Samples.Persistence.EntityFramework/Program.cs @@ -6,6 +6,7 @@ using Elsa.Persistence.Specifications.WorkflowInstances; using Elsa.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Elsa.Persistence.EntityFramework.SqlServer; namespace Elsa.Samples.Persistence.EntityFramework { diff --git a/src/samples/server/Elsa.Samples.Server.Host/Activities/SampleCompositeActivity.cs b/src/samples/server/Elsa.Samples.Server.Host/Activities/SampleCompositeActivity.cs new file mode 100644 index 000000000..628c08246 --- /dev/null +++ b/src/samples/server/Elsa.Samples.Server.Host/Activities/SampleCompositeActivity.cs @@ -0,0 +1,15 @@ +using Elsa.Activities.Console; +using Elsa.Builders; +using Elsa.Services; + +namespace Elsa.Samples.Server.Host.Activities +{ + public class SampleCompositeActivity : CompositeActivity + { + public override void Build(ICompositeActivityBuilder builder) + { + builder.WriteLine("Line 1"); + builder.WriteLine("Line 2"); + } + } +} \ No newline at end of file diff --git a/src/samples/server/Elsa.Samples.Server.Host/Activities/VehicleActivity.cs b/src/samples/server/Elsa.Samples.Server.Host/Activities/VehicleActivity.cs new file mode 100644 index 000000000..f401a0e42 --- /dev/null +++ b/src/samples/server/Elsa.Samples.Server.Host/Activities/VehicleActivity.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Elsa.ActivityResults; +using Elsa.Attributes; +using Elsa.Design; +using Elsa.Expressions; +using Elsa.Metadata; +using Elsa.Services; + +namespace Elsa.Samples.Server.Host.Activities +{ + [Action] + public class VehicleActivity : Activity, IActivityPropertyOptionsProvider, IRuntimeSelectListItemsProvider + { + private Random _random; + + public VehicleActivity() + { + _random = new Random(); + } + + [ActivityProperty( + UIHint = ActivityPropertyUIHints.Dropdown, + OptionsProvider = typeof(VehicleActivity), + DefaultSyntax = SyntaxNames.Literal, + SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid } + )] + public string? Brand { get; set; } + + public object GetOptions(PropertyInfo property) => new RuntimeSelectListItemsProviderSettings(GetType(), new VehicleContext(_random.Next(100))); + + public ValueTask> GetItemsAsync(object? context, CancellationToken cancellationToken = default) + { + var vehicleContext = (VehicleContext) context!; + var brands = new[] { "BMW", "Peugot", "Tesla", vehicleContext.RandomNumber.ToString() }; + var items = brands.Select(x => new SelectListItem(x)).ToList(); + return new ValueTask>(items); + } + + protected override IActivityExecutionResult OnExecute() => Done(Brand); + } + + public record VehicleContext(int RandomNumber); +} \ No newline at end of file diff --git a/src/samples/server/Elsa.Samples.Server.Host/Startup.cs b/src/samples/server/Elsa.Samples.Server.Host/Startup.cs index b2ae8d5fb..4d6e3043a 100644 --- a/src/samples/server/Elsa.Samples.Server.Host/Startup.cs +++ b/src/samples/server/Elsa.Samples.Server.Host/Startup.cs @@ -3,6 +3,7 @@ using Elsa.Activities.UserTask.Extensions; using Elsa.Persistence.EntityFramework.Core.Extensions; using Elsa.Persistence.EntityFramework.Sqlite; using Elsa.Persistence.YesSql; +using Elsa.Samples.Server.Host.Activities; using Elsa.Server.Hangfire.Extensions; using Hangfire; using Microsoft.AspNetCore.Builder; @@ -48,6 +49,8 @@ namespace Elsa.Samples.Server.Host }); services + .AddActivityPropertyOptionsProvider() + .AddRuntimeSelectItemsProvider() .AddElsa(elsa => elsa //.UseEntityFrameworkPersistence(ef => ef.UseSqlite()) .UseYesSqlPersistence() @@ -62,9 +65,11 @@ namespace Elsa.Samples.Server.Host .AddJavaScriptActivities() .AddUserTaskActivities() .AddTelnyx() + .AddActivitiesFrom() .AddWorkflowsFrom() ); + // Elsa API endpoints. services .AddElsaApiEndpoints() .AddElsaSwagger(); diff --git a/src/server/Elsa.Server.Api/Endpoints/Designer/RuntimeSelectListItems/Get.cs b/src/server/Elsa.Server.Api/Endpoints/Designer/RuntimeSelectListItems/Get.cs new file mode 100644 index 000000000..452d3891b --- /dev/null +++ b/src/server/Elsa.Server.Api/Endpoints/Designer/RuntimeSelectListItems/Get.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Design; +using Elsa.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Swashbuckle.AspNetCore.Annotations; + +namespace Elsa.Server.Api.Endpoints.Designer.RuntimeSelectListItems +{ + [ApiController] + [ApiVersion("1")] + [Route("v{apiVersion:apiVersion}/designer/runtime-select-list-items")] + [Produces("application/json")] + public class Get : Controller + { + private readonly IServiceProvider _serviceProvider; + private readonly IContentSerializer _contentSerializer; + + public Get(IServiceProvider serviceProvider, IContentSerializer contentSerializer) + { + _serviceProvider = serviceProvider; + _contentSerializer = contentSerializer; + } + + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(OkObjectResult))] + [SwaggerOperation( + Summary = "Returns a list of items to be used by the list control requesting the items.", + Description = "Returns a list of items to be used by the list control requesting the items.", + OperationId = "Designer.RuntimeSelectListItems.Get", + Tags = new[] { "Designer.RuntimeSelectListItems" }) + ] + public async Task Handle([FromBody]RuntimeSelectListItemsContextHolder model, CancellationToken cancellationToken = default) + { + var type = Type.GetType(model.ProviderTypeName)!; + var provider = (IRuntimeSelectListItemsProvider) ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, type); + var context = model?.Context; + var items = (await provider.GetItemsAsync(context, cancellationToken)).ToList(); + var serializerSettings = _contentSerializer.GetSettings(); + + return Json(items, serializerSettings); + } + } + + [JsonObject(ItemTypeNameHandling = TypeNameHandling.All)] + public record RuntimeSelectListItemsContextHolder + { + public string ProviderTypeName { get; set; } = default!; + + public object? Context { get; set; } + }; +} \ 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 c46a6f075..68e8e8d20 100644 --- a/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/Models.cs +++ b/src/server/Elsa.Server.Api/Endpoints/WorkflowRegistry/Models.cs @@ -38,6 +38,7 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry public string? DisplayName { get; set; } public string? Description { get; set; } public string Type { get; set; } = default!; + public string? ParentId { get; set; } public bool PersistWorkflow { get; set; } public bool LoadWorkflowContext { get; set; } public bool SaveWorkflowContext { get; set; } diff --git a/src/server/Elsa.Server.Api/Mapping/ActivityBlueprintConverter.cs b/src/server/Elsa.Server.Api/Mapping/ActivityBlueprintConverter.cs index 63a759080..a6b52bd98 100644 --- a/src/server/Elsa.Server.Api/Mapping/ActivityBlueprintConverter.cs +++ b/src/server/Elsa.Server.Api/Mapping/ActivityBlueprintConverter.cs @@ -19,6 +19,7 @@ namespace Elsa.Server.Api.Mapping DisplayName = source.DisplayName, Description = source.Description, Type = source.Type, + ParentId = source.Parent?.Id, PersistWorkflow = source.PersistWorkflow, LoadWorkflowContext = source.LoadWorkflowContext, SaveWorkflowContext = source.SaveWorkflowContext,