Merge remote-tracking branch 'upstream/feature/elsa-2.0' into elsa-2.0/fileactivities

This commit is contained in:
Cameron Cowen 2021-05-02 19:29:40 +01:00
commit 619421e098
59 changed files with 1224 additions and 74 deletions

View file

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

View file

@ -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<QueueWorker>(_serviceProvider, receiver, (Func<IReceiverClient, Task>) 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<IWorkflowRegistry>();
var workflowBlueprintReflector = scope.ServiceProvider.GetRequiredService<IWorkflowBlueprintReflector>();
var workflows = await workflowRegistry.ListAsync(cancellationToken);
var workflows = await workflowRegistry.ListActiveAsync(cancellationToken);
var query =
from workflow in workflows

View file

@ -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<ServiceBusTopicsStarter> _logger;
private readonly ICollection<TopicWorker> _workers;
public ServiceBusTopicsStarter(
ITopicMessageReceiverFactory receiverFactory,
IServiceScopeFactory scopeFactory,
IServiceProvider serviceProvider)
IServiceProvider serviceProvider,
ILogger<ServiceBusTopicsStarter> logger)
{
_receiverFactory = receiverFactory;
_scopeFactory = scopeFactory;
_serviceProvider = serviceProvider;
_logger = logger;
_workers = new List<TopicWorker>();
}
@ -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<TopicWorker>(_serviceProvider, receiver, (Func<IReceiverClient, Task>) 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<IWorkflowRegistry>();
var workflowBlueprintReflector = scope.ServiceProvider.GetRequiredService<IWorkflowBlueprintReflector>();
var workflowInstanceStore = scope.ServiceProvider.GetRequiredService<IWorkflowInstanceStore>();
var workflows = await workflowRegistry.ListAsync(cancellationToken);
var workflows = await workflowRegistry.ListActiveAsync(cancellationToken);
var query =
from workflow in workflows

View file

@ -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<ISetupActivity<SendHttpRequest>> setup, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) =>
builder.Then(setup, null, lineNumber, sourceFile);
public static IActivityBuilder SendHttpRequest(this IActivityBuilder builder,
Func<ActivityExecutionContext, Uri?> url,
Func<ActivityExecutionContext, string?> method,
Func<ActivityExecutionContext, string?> content,
Func<ActivityExecutionContext, string?> contentType,
Func<ActivityExecutionContext, string?> authorization,
Func<ActivityExecutionContext, HttpRequestHeaders> requestHeaders,
Func<ActivityExecutionContext, bool> readContent,
Func<ActivityExecutionContext, ICollection<int>> 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<Uri?> url,
Func<string?> method,
Func<string?> content,
Func<string?> contentType,
Func<string?> authorization,
Func<HttpRequestHeaders> requestHeaders,
Func<bool> readContent,
Func<ICollection<int>> 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<int> 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);
}
}

View file

@ -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<SendHttpRequest> WithUrl(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, ValueTask<Uri?>> url) => activity.Set(x => x.Url, url);
public static ISetupActivity<SendHttpRequest> WithUrl(this ISetupActivity<SendHttpRequest> activity, Func<ValueTask<Uri?>> url) => activity.Set(x => x.Url, url);
public static ISetupActivity<SendHttpRequest> WithUrl(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, Uri?> url) => activity.Set(x => x.Url, url);
public static ISetupActivity<SendHttpRequest> WithUrl(this ISetupActivity<SendHttpRequest> activity, Func<Uri?> url) => activity.Set(x => x.Url, url);
public static ISetupActivity<SendHttpRequest> WithUrl(this ISetupActivity<SendHttpRequest> activity, Uri? url) => activity.Set(x => x.Url, url);
public static ISetupActivity<SendHttpRequest> WithMethod(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, ValueTask<string?>> method) => activity.Set(x => x.Method, method);
public static ISetupActivity<SendHttpRequest> WithMethod(this ISetupActivity<SendHttpRequest> activity, Func<ValueTask<string?>> method) => activity.Set(x => x.Method, method);
public static ISetupActivity<SendHttpRequest> WithMethod(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, string?> method) => activity.Set(x => x.Method, method);
public static ISetupActivity<SendHttpRequest> WithMethod(this ISetupActivity<SendHttpRequest> activity, Func<string?> method) => activity.Set(x => x.Method, method);
public static ISetupActivity<SendHttpRequest> WithMethod(this ISetupActivity<SendHttpRequest> activity, string? method) => activity.Set(x => x.Method, method);
public static ISetupActivity<SendHttpRequest> WithContent(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, ValueTask<string?>> content
) => activity.Set(x => x.Content, content);
public static ISetupActivity<SendHttpRequest> WithContent(this ISetupActivity<SendHttpRequest> activity, Func<ValueTask<string?>> content) => activity.Set(x => x.Content, content);
public static ISetupActivity<SendHttpRequest> WithContent(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, string?> content) => activity.Set(x => x.Content, content);
public static ISetupActivity<SendHttpRequest> WithContent(this ISetupActivity<SendHttpRequest> activity, Func<string?> content) => activity.Set(x => x.Content, content);
public static ISetupActivity<SendHttpRequest> WithContent(this ISetupActivity<SendHttpRequest> activity, string? content) => activity.Set(x => x.Content, content);
public static ISetupActivity<SendHttpRequest> WithContentType(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, ValueTask<string?>> contentType
) => activity.Set(x => x.ContentType, contentType);
public static ISetupActivity<SendHttpRequest> WithContentType(this ISetupActivity<SendHttpRequest> activity, Func<ValueTask<string?>> contentType) => activity.Set(x => x.ContentType, contentType);
public static ISetupActivity<SendHttpRequest> WithContentType(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, string?> contentType) => activity.Set(x => x.ContentType, contentType);
public static ISetupActivity<SendHttpRequest> WithContentType(this ISetupActivity<SendHttpRequest> activity, Func<string?> contentType) => activity.Set(x => x.ContentType, contentType);
public static ISetupActivity<SendHttpRequest> WithContentType(this ISetupActivity<SendHttpRequest> activity, string? contentType) => activity.Set(x => x.ContentType, contentType);
public static ISetupActivity<SendHttpRequest> WithAuthorization(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, ValueTask<string?>> authorization
) => activity.Set(x => x.Authorization, authorization);
public static ISetupActivity<SendHttpRequest> WithAuthorization(this ISetupActivity<SendHttpRequest> activity, Func<ValueTask<string?>> authorization) => activity.Set(x => x.Authorization, authorization);
public static ISetupActivity<SendHttpRequest> WithAuthorization(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, string?> authorization) => activity.Set(x => x.Authorization, authorization);
public static ISetupActivity<SendHttpRequest> WithAuthorization(this ISetupActivity<SendHttpRequest> activity, Func<string?> authorization) => activity.Set(x => x.Authorization, authorization);
public static ISetupActivity<SendHttpRequest> WithAuthorization(this ISetupActivity<SendHttpRequest> activity, string? authorization) => activity.Set(x => x.Authorization, authorization);
public static ISetupActivity<SendHttpRequest> WithRequestHeaders(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, ValueTask<HttpRequestHeaders?>> requestHeaders
) => activity.Set(x => x.RequestHeaders, requestHeaders);
public static ISetupActivity<SendHttpRequest> WithRequestHeaders(this ISetupActivity<SendHttpRequest> activity, Func<ValueTask<HttpRequestHeaders?>> requestHeaders) => activity.Set(x => x.RequestHeaders, requestHeaders);
public static ISetupActivity<SendHttpRequest> WithRequestHeaders(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, HttpRequestHeaders?> requestHeaders) => activity.Set(x => x.RequestHeaders, requestHeaders);
public static ISetupActivity<SendHttpRequest> WithRequestHeaders(this ISetupActivity<SendHttpRequest> activity, Func<HttpRequestHeaders?> requestHeaders) => activity.Set(x => x.RequestHeaders, requestHeaders);
public static ISetupActivity<SendHttpRequest> WithRequestHeaders(this ISetupActivity<SendHttpRequest> activity, HttpRequestHeaders? requestHeaders) => activity.Set(x => x.RequestHeaders, requestHeaders);
public static ISetupActivity<SendHttpRequest> WithReadContent(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, ValueTask<bool?>> readContent
) => activity.Set(x => x.ReadContent, readContent);
public static ISetupActivity<SendHttpRequest> WithReadContent(this ISetupActivity<SendHttpRequest> activity, Func<ValueTask<bool?>> readContent) => activity.Set(x => x.ReadContent, readContent);
public static ISetupActivity<SendHttpRequest> WithReadContent(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, bool?> readContent) => activity.Set(x => x.ReadContent, readContent);
public static ISetupActivity<SendHttpRequest> WithReadContent(this ISetupActivity<SendHttpRequest> activity, Func<bool?> readContent) => activity.Set(x => x.ReadContent, readContent);
public static ISetupActivity<SendHttpRequest> WithReadContent(this ISetupActivity<SendHttpRequest> activity, bool? readContent) => activity.Set(x => x.ReadContent, readContent);
public static ISetupActivity<SendHttpRequest> WithSupportedHttpCodes(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, ValueTask<ICollection<int>?>> supportedStatusCodes
) => activity.Set(x => x.SupportedStatusCodes, supportedStatusCodes);
public static ISetupActivity<SendHttpRequest> WithSupportedHttpCodes(this ISetupActivity<SendHttpRequest> activity, Func<ValueTask<ICollection<int>?>> supportedStatusCodes) => activity.Set(x => x.SupportedStatusCodes, supportedStatusCodes);
public static ISetupActivity<SendHttpRequest> WithSupportedHttpCodes(this ISetupActivity<SendHttpRequest> activity, Func<ActivityExecutionContext, ICollection<int>> supportedStatusCodes) => activity.Set(x => x.SupportedStatusCodes, supportedStatusCodes);
public static ISetupActivity<SendHttpRequest> WithSupportedHttpCodes(this ISetupActivity<SendHttpRequest> activity, Func<ICollection<int>> supportedStatusCodes) => activity.Set(x => x.SupportedStatusCodes, supportedStatusCodes);
public static ISetupActivity<SendHttpRequest> WithSupportedHttpCodes(this ISetupActivity<SendHttpRequest> activity, ICollection<int> supportedStatusCodes) => activity.Set(x => x.SupportedStatusCodes, supportedStatusCodes);
}
}

View file

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Elsa.Design
{
public interface IRuntimeSelectListItemsProvider
{
ValueTask<IEnumerable<SelectListItem>> GetItemsAsync(object? context = default, CancellationToken cancellationToken = default);
}
}

View file

@ -0,0 +1,30 @@
using System;
using Elsa.Serialization.Converters;
using Newtonsoft.Json;
namespace Elsa.Design
{
/// <summary>
/// Represents settings to be used by the designer to invoke an Elsa's API endpoint to retrieve list options at runtime.
/// </summary>
public class RuntimeSelectListItemsProviderSettings
{
public RuntimeSelectListItemsProviderSettings(Type providerType, object? context = default)
{
RuntimeSelectListItemsProviderType = providerType;
Context = context;
}
/// <summary>
/// The type of the list items provider.
/// </summary>
[JsonConverter(typeof(FullTypeJsonConverter))]
public Type RuntimeSelectListItemsProviderType { get; }
/// <summary>
/// Optionally provide an object containing useful information for the list select items provider to determine what items to provide.
/// </summary>
[JsonProperty(TypeNameHandling = TypeNameHandling.All)]
public object? Context { get; }
}
}

View file

@ -0,0 +1,23 @@
using System;
using Newtonsoft.Json;
namespace Elsa.Serialization.Converters
{
public class FullTypeJsonConverter : JsonConverter<Type>
{
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<string>(reader)!;
return typeName is not null and not "" ? Type.GetType(typeName)! : default!;
}
}
}

View file

@ -7,11 +7,34 @@ using Elsa.Services.Models;
namespace Elsa.Services
{
/// <summary>
/// Represents a complete store of all versions of all workflow blueprints.
/// </summary>
public interface IWorkflowRegistry
{
/// <summary>
/// Lists all versions of all workflow blueprints.
/// </summary>
Task<IEnumerable<IWorkflowBlueprint>> ListAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Lists only those workflow blueprints that are published or have at least one non-finished workflow.
/// </summary>
Task<IEnumerable<IWorkflowBlueprint>> ListActiveAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Gets a single workflow blueprint with the specified ID for the specified tenant and version.
/// </summary>
Task<IWorkflowBlueprint?> GetAsync(string id, string? tenantId, VersionOptions version, CancellationToken cancellationToken = default);
/// <summary>
/// Returns all workflow blueprints that fit the specified predicate.
/// </summary>
Task<IEnumerable<IWorkflowBlueprint>> FindManyAsync(Func<IWorkflowBlueprint, bool> predicate, CancellationToken cancellationToken = default);
/// <summary>
/// Returns a single workflow blueprint that fits the specified predicate.
/// </summary>
Task<IWorkflowBlueprint?> FindAsync(Func<IWorkflowBlueprint, bool> predicate, CancellationToken cancellationToken = default);
}
}

View file

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

View file

@ -29,6 +29,7 @@ namespace Elsa.Decorators
}
public async Task<IEnumerable<IWorkflowBlueprint>> ListAsync(CancellationToken cancellationToken) => await GetWorkflowBlueprints(cancellationToken);
public Task<IEnumerable<IWorkflowBlueprint>> ListActiveAsync(CancellationToken cancellationToken = default) => _workflowRegistry.ListActiveAsync(cancellationToken);
public async Task<IWorkflowBlueprint?> GetAsync(string id, string? tenantId, VersionOptions version, CancellationToken cancellationToken) =>
await FindAsync(x => x.Id == id && x.TenantId == tenantId && x.WithVersion(version), cancellationToken);

View file

@ -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<T>(this IServiceCollection services) where T : class, IActivityPropertyOptionsProvider => services.AddSingleton<IActivityPropertyOptionsProvider, T>();
public static IServiceCollection AddRuntimeSelectItemsProvider<T>(this IServiceCollection services) where T : class, IRuntimeSelectListItemsProvider => services.AddScoped<IRuntimeSelectListItemsProvider, T>();
public static IServiceCollection AddActivityTypeProvider<T>(this IServiceCollection services) where T : class, IActivityTypeProvider => services.AddSingleton<IActivityTypeProvider, T>();
private static ElsaOptionsBuilder AddWorkflowsCore(this ElsaOptionsBuilder options)

View file

@ -24,6 +24,7 @@ namespace Elsa.Services
}
public async Task<IEnumerable<IWorkflowBlueprint>> ListAsync(CancellationToken cancellationToken) => await GetWorkflowsInternalAsync(cancellationToken).ToListAsync(cancellationToken);
public async Task<IEnumerable<IWorkflowBlueprint>> ListActiveAsync(CancellationToken cancellationToken) => await ListActiveWorkflowsAsync(cancellationToken).ToListAsync(cancellationToken);
public async Task<IWorkflowBlueprint?> 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<IWorkflowBlueprint?> FindAsync(Func<IWorkflowBlueprint, bool> predicate, CancellationToken cancellationToken) =>
(await ListAsync(cancellationToken).Where(predicate).OrderByDescending(x => x.Version)).FirstOrDefault();
private async IAsyncEnumerable<IWorkflowBlueprint> 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<bool> WorkflowHasNonFinishedWorkflowsAsync(IWorkflowBlueprint workflowBlueprint, CancellationToken cancellationToken)
{
var count = await _workflowInstanceStore.CountAsync(new NonFinalizedWorkflowSpecification().WithWorkflowDefinition(workflowBlueprint.Id), cancellationToken);
return count > 0;
}
private async IAsyncEnumerable<IWorkflowBlueprint> GetWorkflowsInternalAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{

View file

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

View file

@ -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<string>;
}
interface ElsaInputTagsDropdown {
"dropdownValues"?: Array<SelectListItem>;
"fieldId"?: string;
"fieldName"?: string;
"placeHolder"?: string;
"values"?: Array<string | SelectListItem>;
}
interface ElsaModalDialog {
"hide": (animate: boolean) => Promise<void>;
"show": (animate: boolean) => Promise<void>;
@ -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<void>;
"show": (options: ToastNotificationOptions) => Promise<void>;
}
interface ElsaWorkflowBlueprintViewerScreen {
"getServerUrl": () => Promise<string>;
"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<string>;
}
interface ElsaInputTagsDropdown {
"dropdownValues"?: Array<SelectListItem>;
"fieldId"?: string;
"fieldName"?: string;
"onValueChanged"?: (event: CustomEvent<Array<string | SelectListItem>>) => void;
"placeHolder"?: string;
"values"?: Array<string | SelectListItem>;
}
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<HTMLElsaDropdownPropertyElement>;
"elsa-expression-editor": LocalJSX.ElsaExpressionEditor & JSXBase.HTMLAttributes<HTMLElsaExpressionEditorElement>;
"elsa-input-tags": LocalJSX.ElsaInputTags & JSXBase.HTMLAttributes<HTMLElsaInputTagsElement>;
"elsa-input-tags-dropdown": LocalJSX.ElsaInputTagsDropdown & JSXBase.HTMLAttributes<HTMLElsaInputTagsDropdownElement>;
"elsa-modal-dialog": LocalJSX.ElsaModalDialog & JSXBase.HTMLAttributes<HTMLElsaModalDialogElement>;
"elsa-monaco": LocalJSX.ElsaMonaco & JSXBase.HTMLAttributes<HTMLElsaMonacoElement>;
"elsa-multi-expression-editor": LocalJSX.ElsaMultiExpressionEditor & JSXBase.HTMLAttributes<HTMLElsaMultiExpressionEditorElement>;
@ -724,6 +787,7 @@ declare module "@stencil/core" {
"elsa-script-property": LocalJSX.ElsaScriptProperty & JSXBase.HTMLAttributes<HTMLElsaScriptPropertyElement>;
"elsa-single-line-property": LocalJSX.ElsaSingleLineProperty & JSXBase.HTMLAttributes<HTMLElsaSingleLinePropertyElement>;
"elsa-studio-root": LocalJSX.ElsaStudioRoot & JSXBase.HTMLAttributes<HTMLElsaStudioRootElement>;
"elsa-studio-workflow-blueprint-view": LocalJSX.ElsaStudioWorkflowBlueprintView & JSXBase.HTMLAttributes<HTMLElsaStudioWorkflowBlueprintViewElement>;
"elsa-studio-workflow-definitions-edit": LocalJSX.ElsaStudioWorkflowDefinitionsEdit & JSXBase.HTMLAttributes<HTMLElsaStudioWorkflowDefinitionsEditElement>;
"elsa-studio-workflow-definitions-list": LocalJSX.ElsaStudioWorkflowDefinitionsList & JSXBase.HTMLAttributes<HTMLElsaStudioWorkflowDefinitionsListElement>;
"elsa-studio-workflow-instances-list": LocalJSX.ElsaStudioWorkflowInstancesList & JSXBase.HTMLAttributes<HTMLElsaStudioWorkflowInstancesListElement>;
@ -731,6 +795,7 @@ declare module "@stencil/core" {
"elsa-studio-workflow-registry": LocalJSX.ElsaStudioWorkflowRegistry & JSXBase.HTMLAttributes<HTMLElsaStudioWorkflowRegistryElement>;
"elsa-switch-cases-property": LocalJSX.ElsaSwitchCasesProperty & JSXBase.HTMLAttributes<HTMLElsaSwitchCasesPropertyElement>;
"elsa-toast-notification": LocalJSX.ElsaToastNotification & JSXBase.HTMLAttributes<HTMLElsaToastNotificationElement>;
"elsa-workflow-blueprint-viewer-screen": LocalJSX.ElsaWorkflowBlueprintViewerScreen & JSXBase.HTMLAttributes<HTMLElsaWorkflowBlueprintViewerScreenElement>;
"elsa-workflow-definition-editor-notifications": LocalJSX.ElsaWorkflowDefinitionEditorNotifications & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionEditorNotificationsElement>;
"elsa-workflow-definition-editor-screen": LocalJSX.ElsaWorkflowDefinitionEditorScreen & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionEditorScreenElement>;
"elsa-workflow-definitions-list-screen": LocalJSX.ElsaWorkflowDefinitionsListScreen & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionsListScreenElement>;

View file

@ -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<string | SelectListItem> = [];
@Prop() dropdownValues?: Array<SelectListItem> = [];
@Event({ bubbles: true }) valueChanged: EventEmitter<Array<string | SelectListItem>>;
@State() currentValues?: Array<SelectListItem> = [];
@State() dropdownTags?: Array<SelectListItem> = [];
@Watch('values')
valuesChangedHandler(newValue: Array<string | SelectListItem>) {
let values: Array<SelectListItem> = [];
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<SelectListItem> = [];
this.values.forEach(value => {
this.dropdownValues.forEach(tag => {
if (value === tag.value) {
values.push(tag);
}
})
})
this.currentValues = values;
const tagsToRemove: Array<string> = 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<SelectListItem> = [...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<SelectListItem> = this.currentValues || [];
if (!Array.isArray(values))
values = [];
const valuesJson = JSON.stringify(values.map(tag => tag.value));
return (
<div class="py-2 px-3 bg-white shadow-sm border border-gray-300 rounded-md">
{values.map(tag =>
(
<a href="#" onClick={e => this.onDeleteTagClick(e, tag)} class="inline-block text-xs bg-blue-400 text-white py-2 px-3 mr-1 mb-1 rounded">
<input type="hidden" value={tag.value} />
<span>{tag.text}</span>
<span class="text-white hover:text-white ml-1">&times;</span>
</a>
))}
<select id={this.fieldId} class="inline-block text-xs bg-green-400 text-white py-2 px-3 mr-1 mb-1 pr-8 rounded" onChange={(e) => this.onTagSelected(e)}>
<option value="Add" disabled selected>{this.placeHolder}</option>
{this.dropdownTags.map(tag =>
(
<option value={tag.value}>
<span>{tag.text}</span>
</option>
)
)}
</select>
<input type="hidden" name={this.fieldName} value={valuesJson} />
</div>
)
}
}

View file

@ -52,6 +52,7 @@ export class ElsaStudioRoot {
<stencil-route-switch scrollTopOffset={0}>
<stencil-route url="/" routeRender={props => <stencil-route-redirect url="/workflow-registry"/>} exact={true}/>
<stencil-route url="/workflow-registry" component="elsa-studio-workflow-registry" componentProps={{'serverUrl': serverUrl}} exact={true}/>
<stencil-route url="/workflow-registry/:id" component="elsa-studio-workflow-blueprint-view" componentProps={{'serverUrl': serverUrl}} />
<stencil-route url="/workflow-definitions" component="elsa-studio-workflow-definitions-list" componentProps={{'serverUrl': serverUrl}} exact={true}/>
<stencil-route url="/workflow-definitions/:id" component="elsa-studio-workflow-definitions-edit" componentProps={{'serverUrl': serverUrl, 'monacoLibPath': monacoLibPath}}/>
<stencil-route url="/workflow-instances" component="elsa-studio-workflow-instances-list" componentProps={{'serverUrl': serverUrl}} exact={true}/>

View file

@ -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 <div>
<elsa-workflow-blueprint-viewer-screen server-url={this.serverUrl} workflowDefinitionId={id}/>
</div>;
}
}

View file

@ -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}
</div>
</div>
${this.renderActivityBody(activity.description)}
${this.renderActivityBody(displayContext)}
</div>`;
}
renderActivityBody(description: string | null) {
if (!description) return '';
renderActivityBody(displayContext: ActivityDesignDisplayContext) {
return (
`<div class="p-6 text-gray-400 text-sm border-t border-t-solid">
${description}
<div>${displayContext.activityModel.activityId}</div>
<div>${displayContext.bodyDisplay}</div>
</div>`
);
}
// private onActivitySelected(e: CustomEvent<ActivityModel>) {
// this.activitySelected.emit(e.detail);
// }
// private onActivityDeselected(e: CustomEvent<ActivityModel>) {
// this.activityDeselected.emit(e.detail);
// }
render() {
return (
<Host class="workflow-canvas flex-1 flex" ref={el => (this.el = el)}>

View file

@ -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<any>;
const items = this.items;
const values = parseJson(this.currentValue) || [];
return (
@ -51,11 +61,11 @@ export class ElsaCheckListProperty {
editor-height="2.75em"
single-line={true}>
<div class="max-w-lg space-y-3 my-4">
{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']);

View file

@ -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<any> || [];
const currentValue = this.currentValue;
const items = this.items;
return (
<elsa-property-editor propertyDescriptor={propertyDescriptor}
@ -43,10 +50,10 @@ export class ElsaDropdownProperty {
editor-height="2.75em"
single-line={true}>
<select id={fieldId} name={fieldName} onChange={e => this.onChange(e)} class="mt-1 block focus:ring-blue-500 focus:border-blue-500 w-full shadow-sm sm:max-w-xs sm:text-sm border-gray-300 rounded-md">
{options.map(option => {
const optionIsObject = typeof (option) == 'object';
const value = optionIsObject ? option.value : option.toString();
const text = optionIsObject ? option.text : option.toString();
{items.map(item => {
const optionIsObject = typeof (item) == 'object';
const value = optionIsObject ? item.value : item.toString();
const text = optionIsObject ? item.text : item.toString();
return <option value={value} selected={value === currentValue}>{text}</option>;
})}
</select>
@ -54,3 +61,5 @@ export class ElsaDropdownProperty {
);
}
}
Tunnel.injectProps(ElsaDropdownProperty, ['serverUrl']);

View file

@ -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<string>){
this.currentValue = JSON.stringify(newValue);
onValueChanged(newValue: Array<string | number | boolean | SelectListItem>) {
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<SelectListItem>) {
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' ?
<elsa-input-tags values={values} fieldId={fieldId} fieldName={fieldName} onValueChanged={e => this.onValueChanged(e.detail)} /> :
<elsa-input-tags-dropdown dropdownValues={propertyOptions} values={values} fieldId={fieldId} fieldName={fieldName} onValueChanged={e => this.onValueChanged(e.detail)} />;
return (
<elsa-property-editor propertyDescriptor={propertyDescriptor}
propertyModel={propertyModel}
onDefaultSyntaxValueChanged={e => this.onDefaultSyntaxValueChanged(e)}
editor-height="2em"
single-line={true}>
<elsa-input-tags values={values} fieldId={fieldId} fieldName={fieldName} onValueChanged={e => this.onValueChanged(e.detail)}/>
{elsaInputTags}
</elsa-property-editor>
)
}
}
Tunnel.injectProps(ElsaMultiTextProperty, ['serverUrl']);

View file

@ -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<string> {
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<ActivityDescriptor> = state.activityDescriptors;
const descriptor = descriptors.find(x => x.type == source.type);
const properties: Array<ActivityDefinitionProperty> = 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 (
<Host class="flex flex-col w-full relative" ref={el => this.el = el}>
{this.renderCanvas()}
</Host>
);
}
renderCanvas() {
return (
<div class="flex-1 flex">
<elsa-designer-tree model={this.workflowModel} class="flex-1" ref={el => this.designer = el} editMode={false}/>
</div>
);
}
}

View file

@ -0,0 +1,51 @@
# elsa-workflow-instance-viewer-screen
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| -------------------- | ---------------------- | ----------- | -------- | ----------- |
| `serverUrl` | `server-url` | | `string` | `undefined` |
| `workflowInstanceId` | `workflow-instance-id` | | `string` | `undefined` |
## Methods
### `getServerUrl() => Promise<string>`
#### Returns
Type: `Promise<string>`
## 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/)*

View file

@ -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 (
<Host>
<elsa-modal-dialog ref={el => this.dialog = el}>
<elsa-modal-dialog ref={el => this.dialog = el} >
<div slot="content" class="py-8 pb-0">
<form onSubmit={e => this.onSubmit(e)} ref={el => this.form = el}>
<form onSubmit={e => this.onSubmit(e)} ref={el => this.form = el} key={this.timestamp.getTime().toString()}>
<div class="flex px-8">
<div class="space-y-8 divide-y divide-gray-200 w-full">
<div>

View file

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

View file

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

View file

@ -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<WorkflowExecutionLogRecord>) {
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<ActivityModel>) {

View file

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

View file

@ -15,6 +15,8 @@
<body class="h-screen" style="background-size: 30px 30px; background-image: url(/build/assets/images/tile.png); background-color: #FBFBFB;">
<elsa-studio-root server-url="https://localhost:11000/" monaco-lib-path="build/assets/js"></elsa-studio-root>
<!--<elsa-studio-root server-url="https://localhost:6080/" monaco-lib-path="build/assets/js"></elsa-studio-root>-->
<!--<elsa-studio-root server-url="https://skynet-workflow.azurewebsites.net" monaco-lib-path="build/assets/js"></elsa-studio-root>-->
</body>
</html>

View file

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

View file

@ -27,7 +27,7 @@ export interface ActivityPickedEventArgs {
export interface ActivityDesignDisplayContext {
activityModel: ActivityModel;
activityIcon: any;
bodyDisplay: any;
bodyDisplay: string;
outcomes: Array<string>;
}

View file

@ -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 = <p>{expression}</p>;
context.bodyDisplay = `<p>${expression}</p>`;
}
}

View file

@ -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 = <p>{path.expressions[path.syntax]}</p>;
const path = props.find(x => x.name == 'Path') || { name: 'Path', expressions: { 'Literal': '', syntax: SyntaxNames.Literal } };
const syntax = path.syntax || SyntaxNames.Literal;
context.bodyDisplay = `<p>${path.expressions[syntax]}</p>`;
}
}

View file

@ -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 = <p>{bodyText}</p>;
context.bodyDisplay = `<p>${bodyText}</p>`;
}
}

View file

@ -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 = <p>To: {bodyText}</p>;
context.bodyDisplay = `<p>To: ${bodyText}</p>`;
}
}

View file

@ -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 = <p>{expression}</p>;
context.bodyDisplay = `<p>${expression}</p>`;
}
}

View file

@ -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 = <p>{expression}</p>;
context.bodyDisplay = `<p>${expression}</p>`;
}
}

View file

@ -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 = <p>{bodyText}</p>;
context.bodyDisplay = `<p>${bodyText}</p>`;
}
}

View file

@ -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 = <p>{expression}</p>;
context.bodyDisplay = `<p>${expression}</p>`;
}
}

View file

@ -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<string>(`v1/scripting/javascript/type-definitions/${workflowDefinitionId}?t=${new Date().getTime()}&context=${context}`);
return response.data;
}
},
designerApi:{
runtimeSelectItemsApi:{
get: async (providerTypeName: string, context?: any): Promise<Array<SelectListItem>> => {
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<string>
}
export interface DesignerApi {
runtimeSelectItemsApi: RuntimeSelectItemsApi;
}
export interface RuntimeSelectItemsApi {
get(providerTypeName: string, context?: any): Promise<Array<SelectListItem>>
}
export interface SaveWorkflowDefinitionRequest {
workflowDefinitionId?: string;
name?: string;

View file

@ -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<Array<SelectListItem>> {
const elsaClient = createElsaClient(serverUrl);
return await elsaClient.designerApi.runtimeSelectItemsApi.get(options.runtimeSelectListItemsProviderType, options.context || {});
}
export async function getSelectListItems(serverUrl: string, propertyDescriptor: ActivityPropertyDescriptor): Promise<Array<SelectListItem>> {
const options = propertyDescriptor.options;
let items = [];
if (!!options.runtimeSelectListItemsProviderType) {
items = await fetchRuntimeItems(serverUrl, options);
} else {
items = options as Array<any> || [];
}
return items || [];
}

View file

@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore;
namespace Elsa.Persistence.EntityFramework.SqlServer
{
public static class DbContextOptionsBuilderExtensions
{
/// <summary>
/// Configures the context to use SqlServer.
/// </summary>
public static DbContextOptionsBuilder UseSqlServer(this DbContextOptionsBuilder builder, string connectionString) =>
builder.UseSqlServer(connectionString, db => db.MigrationsAssembly(typeof(SqlServerElsaContextFactory).Assembly.GetName().Name));
}
}

View file

@ -37,7 +37,7 @@ namespace Elsa.Persistence.YesSql.Documents
public WorkflowFault? Fault { get; set; }
public SimpleStack<ScheduledActivity> ScheduledActivities { get; set; } = new();
public SimpleStack<string> Scopes { get; set; } = new();
public SimpleStack<ActivityScope> Scopes { get; set; } = new();
public ScheduledActivity? CurrentActivity { get; set; }
}
}

View file

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Http\Elsa.Activities.Http.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
</ItemGroup>
</Project>

View file

@ -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<Startup>();
});
}
}

View file

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

View file

@ -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<HttpResponseModel>("TestHttpRequest").Content)));
}
}
}

View file

@ -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<Startup>());
}
// 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();
}
}
}

View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View file

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

View file

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

View file

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

View file

@ -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<IEnumerable<SelectListItem>> 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<IEnumerable<SelectListItem>>(items);
}
protected override IActivityExecutionResult OnExecute() => Done(Brand);
}
public record VehicleContext(int RandomNumber);
}

View file

@ -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<VehicleActivity>()
.AddRuntimeSelectItemsProvider<VehicleActivity>()
.AddElsa(elsa => elsa
//.UseEntityFrameworkPersistence(ef => ef.UseSqlite())
.UseYesSqlPersistence()
@ -62,9 +65,11 @@ namespace Elsa.Samples.Server.Host
.AddJavaScriptActivities()
.AddUserTaskActivities()
.AddTelnyx()
.AddActivitiesFrom<VehicleActivity>()
.AddWorkflowsFrom<Startup>()
);
// Elsa API endpoints.
services
.AddElsaApiEndpoints()
.AddElsaSwagger();

View file

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

View file

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

View file

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