diff --git a/Samples.sln b/Samples.sln index 060e4580b..1302d5844 100644 --- a/Samples.sln +++ b/Samples.sln @@ -83,6 +83,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample15", "samples\Sample1 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.WorkflowHost.Web", "src\dashboard\Elsa.WorkflowHost.Web\Elsa.WorkflowHost.Web.csproj", "{1680B0F4-4FA3-4ED3-8DA1-23DF034D6009}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.WorkflowDesigner", "src\dashboard\Elsa.WorkflowDesigner\Elsa.WorkflowDesigner.csproj", "{944DAD02-4024-4D3E-8C75-AE313EA02A94}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -209,6 +211,10 @@ Global {1680B0F4-4FA3-4ED3-8DA1-23DF034D6009}.Debug|Any CPU.Build.0 = Debug|Any CPU {1680B0F4-4FA3-4ED3-8DA1-23DF034D6009}.Release|Any CPU.ActiveCfg = Release|Any CPU {1680B0F4-4FA3-4ED3-8DA1-23DF034D6009}.Release|Any CPU.Build.0 = Release|Any CPU + {944DAD02-4024-4D3E-8C75-AE313EA02A94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {944DAD02-4024-4D3E-8C75-AE313EA02A94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {944DAD02-4024-4D3E-8C75-AE313EA02A94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {944DAD02-4024-4D3E-8C75-AE313EA02A94}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -249,6 +255,7 @@ Global {5155F75B-38F2-4B5B-9C8F-6F6641BF986B} = {5E5E1E84-DDBC-40D6-B891-0D563A15A44A} {5E5E1E84-DDBC-40D6-B891-0D563A15A44A} = {DA71CDAA-8DD3-4D5F-9FBD-8E4B37A2D925} {1680B0F4-4FA3-4ED3-8DA1-23DF034D6009} = {0DF567E8-2A21-4A1C-90E8-025F1EC615F3} + {944DAD02-4024-4D3E-8C75-AE313EA02A94} = {0DF567E8-2A21-4A1C-90E8-025F1EC615F3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158} diff --git a/src/activities/Elsa.Activities.Console/Activities/ReadLine.cs b/src/activities/Elsa.Activities.Console/Activities/ReadLine.cs index b9f8e6794..9df718795 100644 --- a/src/activities/Elsa.Activities.Console/Activities/ReadLine.cs +++ b/src/activities/Elsa.Activities.Console/Activities/ReadLine.cs @@ -1,6 +1,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Results; using Elsa.Services; using Elsa.Services.Models; @@ -10,6 +11,15 @@ namespace Elsa.Activities.Console.Activities /// /// Reads input from the console. /// + [ActivityDefinition( + Category = "Console", + Description = "Read text from standard in." + )] + [ActivityDefinitionDesigner( + Description = + "a => !!a.state.variableName ? `Read text from standard in and store into ${ a.state.variableName }.` : 'Read text from standard in.'", + Outcomes = new[] { OutcomeNames.Done } + )] public class ReadLine : Activity { private readonly TextReader input; @@ -23,13 +33,15 @@ namespace Elsa.Activities.Console.Activities this.input = input; } + [ActivityProperty(Hint = "The name of the variable to store the value into.")] public string VariableName { get => GetState(); set => SetState(value); } - protected override async Task OnExecuteAsync(WorkflowExecutionContext context, + protected override async Task OnExecuteAsync( + WorkflowExecutionContext context, CancellationToken cancellationToken) { if (input == null) diff --git a/src/activities/Elsa.Activities.Console/Activities/WriteLine.cs b/src/activities/Elsa.Activities.Console/Activities/WriteLine.cs index bdb5b284d..58b292fdf 100644 --- a/src/activities/Elsa.Activities.Console/Activities/WriteLine.cs +++ b/src/activities/Elsa.Activities.Console/Activities/WriteLine.cs @@ -1,6 +1,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; @@ -12,6 +13,15 @@ namespace Elsa.Activities.Console.Activities /// /// Writes a text string to the console. /// + [ActivityDefinition( + Category = "Console", + Description = "Write text to standard out." + )] + [ActivityDefinitionDesigner( + Description = + "x => !!x.state.textExpression ? `Write ${ x.state.textExpression.expression } to standard out.` : x.definition.description", + Outcomes = new[] { OutcomeNames.Done } + )] public class WriteLine : Activity { public WriteLine(IWorkflowExpressionEvaluator evaluator) @@ -25,6 +35,7 @@ namespace Elsa.Activities.Console.Activities this.output = output; } + [ActivityProperty(Hint = "The text to write.")] public WorkflowExpression TextExpression { get => GetState(() => LiteralEvaluator.Expression(null)); diff --git a/src/activities/Elsa.Activities.Email/Activities/SendEmail.cs b/src/activities/Elsa.Activities.Email/Activities/SendEmail.cs index e357530f1..835b7ef89 100644 --- a/src/activities/Elsa.Activities.Email/Activities/SendEmail.cs +++ b/src/activities/Elsa.Activities.Email/Activities/SendEmail.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Elsa.Activities.Email.Options; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; @@ -11,6 +12,10 @@ using Microsoft.Extensions.Options; namespace Elsa.Activities.Email.Activities { + [ActivityDefinition( + Category = "Email", + Description = "Send an email message." + )] public class SendEmail : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; @@ -18,7 +23,7 @@ namespace Elsa.Activities.Email.Activities private readonly SmtpClient smtpClient; public SendEmail( - IWorkflowExpressionEvaluator expressionEvaluator, + IWorkflowExpressionEvaluator expressionEvaluator, IOptions options, SmtpClient smtpClient) { @@ -27,24 +32,28 @@ namespace Elsa.Activities.Email.Activities this.smtpClient = smtpClient; } + [ActivityProperty(Hint = "The sender's email address.")] public WorkflowExpression From { get => GetState(() => new WorkflowExpression(LiteralEvaluator.SyntaxName, "")); set => SetState(value); } + [ActivityProperty(Hint = "The recipient's email address.")] public WorkflowExpression To { get => GetState(() => new WorkflowExpression(LiteralEvaluator.SyntaxName, "")); set => SetState(value); } + [ActivityProperty(Hint = "The subject of the email message.")] public WorkflowExpression Subject { get => GetState(() => new WorkflowExpression(LiteralEvaluator.SyntaxName, "")); set => SetState(value); } + [ActivityProperty(Hint = "The body of the email message.")] public WorkflowExpression Body { get => GetState(() => new WorkflowExpression(LiteralEvaluator.SyntaxName, "")); @@ -54,7 +63,8 @@ namespace Elsa.Activities.Email.Activities protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) { - var from = (await expressionEvaluator.EvaluateAsync(From, workflowContext, cancellationToken)) ?? options.Value.DefaultSender; + var from = (await expressionEvaluator.EvaluateAsync(From, workflowContext, cancellationToken)) ?? + options.Value.DefaultSender; var to = await expressionEvaluator.EvaluateAsync(To, workflowContext, cancellationToken); var subject = await expressionEvaluator.EvaluateAsync(Subject, workflowContext, cancellationToken); var body = await expressionEvaluator.EvaluateAsync(Body, workflowContext, cancellationToken); diff --git a/src/activities/Elsa.Activities.Http/Activities/HttpRequestAction.cs b/src/activities/Elsa.Activities.Http/Activities/HttpRequestAction.cs index c50442db7..01ab765a6 100644 --- a/src/activities/Elsa.Activities.Http/Activities/HttpRequestAction.cs +++ b/src/activities/Elsa.Activities.Http/Activities/HttpRequestAction.cs @@ -8,16 +8,24 @@ using System.Threading; using System.Threading.Tasks; using Elsa.Activities.Http.Models; using Elsa.Activities.Http.Services; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; using Elsa.Services; using Elsa.Services.Models; +using Elsa.WorkflowDesigner.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; namespace Elsa.Activities.Http.Activities { + [ActivityDefinition( + Category = "HTTP", + DisplayName = "Send HTTP Request", + Description = "Send an HTTP request." + )] + [ActivityDefinitionDesigner(Outcomes = "x => !!x.state.supportedStatusCodes ? x.state.supportedStatusCodes : []")] public class HttpRequestAction : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; @@ -37,6 +45,7 @@ namespace Elsa.Activities.Http.Activities /// /// The URL to invoke. /// + [ActivityProperty(Hint = "The URL to send the HTTP request to.")] public WorkflowExpression Url { get @@ -54,6 +63,11 @@ namespace Elsa.Activities.Http.Activities /// /// The HTTP method to use. /// + [ActivityProperty( + Type = ActivityPropertyTypes.Select, + Hint = "The HTTP method to use when making the request." + )] + [SelectOptions("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD")] public string Method { get => GetState(() => "GET"); @@ -63,7 +77,8 @@ namespace Elsa.Activities.Http.Activities /// /// The body to send along with the request. /// - public WorkflowExpression Body + [ActivityProperty(Hint = "The HTTP content to send along with the request.")] + public WorkflowExpression Content { get => GetState(() => new WorkflowExpression(LiteralEvaluator.SyntaxName, "")); set => SetState(value); @@ -72,6 +87,11 @@ namespace Elsa.Activities.Http.Activities /// /// The Content Type header to send along with the request body. /// + [ActivityProperty( + Type = ActivityPropertyTypes.Select, + Hint = "The content type to send with the request." + )] + [SelectOptions("text/plain", "text/html", "application/json", "application/xml")] public WorkflowExpression ContentType { get => GetState(() => new WorkflowExpression(LiteralEvaluator.SyntaxName, "")); @@ -81,6 +101,7 @@ namespace Elsa.Activities.Http.Activities /// /// The headers to send along with the request. /// + [ActivityProperty(Hint = "The headers to send along with the request. One 'header: value' pair per line.")] public WorkflowExpression RequestHeaders { get => GetState(() => new WorkflowExpression(LiteralEvaluator.SyntaxName, "")); @@ -90,6 +111,10 @@ namespace Elsa.Activities.Http.Activities /// /// A list of HTTP status codes this activity can handle. /// + [ActivityProperty( + Type = ActivityPropertyTypes.List, + Hint = "A list of possible HTTP status codes to handle, comma-separated. Example: 200, 400, 404" + )] public HashSet SupportedStatusCodes { get => GetState(() => new HashSet { 200 }); @@ -102,14 +127,16 @@ namespace Elsa.Activities.Http.Activities { var request = await CreateRequestAsync(workflowContext, cancellationToken); var response = await httpClient.SendAsync(request, cancellationToken); - var content = response.Content != null ? await response.Content.ReadAsStringAsync() : default(string); + var content = response.Content != null ? await response.Content.ReadAsStringAsync() : default; var contentType = response.Content?.Headers.ContentType.MediaType; var formatter = SelectContentFormatter(contentType); var responseModel = new HttpResponseModel { StatusCode = response.StatusCode, - Headers = new HeaderDictionary(response.Headers.ToDictionary(x => x.Key, x => new StringValues(x.Value.ToArray()))), + Headers = new HeaderDictionary( + response.Headers.ToDictionary(x => x.Key, x => new StringValues(x.Value.ToArray())) + ), Content = content, FormattedContent = await formatter.ParseAsync(content, contentType) }; @@ -123,10 +150,13 @@ namespace Elsa.Activities.Http.Activities private IContentFormatter SelectContentFormatter(string contentType) { var formatters = contentFormatters.OrderByDescending(x => x.Priority).ToList(); - return formatters.FirstOrDefault(x => x.SupportedContentTypes.Contains(contentType, StringComparer.OrdinalIgnoreCase)) ?? formatters.Last(); + return formatters.FirstOrDefault( + x => x.SupportedContentTypes.Contains(contentType, StringComparer.OrdinalIgnoreCase) + ) ?? formatters.Last(); } - private async Task CreateRequestAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + private async Task CreateRequestAsync(WorkflowExecutionContext workflowContext, + CancellationToken cancellationToken) { var methodSupportsBody = GetMethodSupportsBody(Method); var uri = await expressionEvaluator.EvaluateAsync(Url, workflowContext, cancellationToken); @@ -135,8 +165,12 @@ namespace Elsa.Activities.Http.Activities if (methodSupportsBody) { - var body = await expressionEvaluator.EvaluateAsync(Body, workflowContext, cancellationToken); - var contentType = await expressionEvaluator.EvaluateAsync(ContentType, workflowContext, cancellationToken); + var body = await expressionEvaluator.EvaluateAsync(Content, workflowContext, cancellationToken); + var contentType = await expressionEvaluator.EvaluateAsync( + ContentType, + workflowContext, + cancellationToken + ); if (!string.IsNullOrWhiteSpace(body)) { @@ -152,9 +186,14 @@ namespace Elsa.Activities.Http.Activities return request; } - private async Task ParseRequestHeadersAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + private async Task ParseRequestHeadersAsync(WorkflowExecutionContext workflowContext, + CancellationToken cancellationToken) { - var headersText = await expressionEvaluator.EvaluateAsync(RequestHeaders, workflowContext, cancellationToken); + var headersText = await expressionEvaluator.EvaluateAsync( + RequestHeaders, + workflowContext, + cancellationToken + ); var headers = new HeaderDictionary(); if (headersText != null) @@ -167,7 +206,11 @@ namespace Elsa.Activities.Http.Activities foreach (var header in headersQuery) { var headerValueExpression = new WorkflowExpression(RequestHeaders.Syntax, header.Value); - var headerValue = await expressionEvaluator.EvaluateAsync(headerValueExpression, workflowContext, cancellationToken); + var headerValue = await expressionEvaluator.EvaluateAsync( + headerValueExpression, + workflowContext, + cancellationToken + ); headers.Add(header.Key, headerValue); } } diff --git a/src/activities/Elsa.Activities.Http/Activities/HttpRequestEvent.cs b/src/activities/Elsa.Activities.Http/Activities/HttpRequestEvent.cs index c04d60740..93db2588c 100644 --- a/src/activities/Elsa.Activities.Http/Activities/HttpRequestEvent.cs +++ b/src/activities/Elsa.Activities.Http/Activities/HttpRequestEvent.cs @@ -7,27 +7,39 @@ using System.Threading.Tasks; using Elsa.Activities.Http.Extensions; using Elsa.Activities.Http.Models; using Elsa.Activities.Http.Services; +using Elsa.Attributes; using Elsa.Extensions; using Elsa.Results; using Elsa.Services; using Elsa.Services.Models; +using Elsa.WorkflowDesigner.Models; using Microsoft.AspNetCore.Http; using Newtonsoft.Json.Linq; namespace Elsa.Activities.Http.Activities { + [ActivityDefinition( + Category = "HTTP", + DisplayName = "Receive HTTP Request", + Description = "Receive an incoming HTTP request." + )] + [ActivityDefinitionDesigner( + Description = + "x => !!x.state.path ? `Handle ${ x.state.method } ${ x.state.path }.` : x.definition.description", + Outcomes = new[] { OutcomeNames.Done } + )] public class HttpRequestEvent : Activity { public static Uri GetPath(JObject state) { return state.GetState(nameof(Path)); } - + public static string GetMethod(JObject state) { return state.GetState(nameof(Method)); } - + private readonly IHttpContextAccessor httpContextAccessor; private readonly IEnumerable contentFormatters; @@ -42,9 +54,7 @@ namespace Elsa.Activities.Http.Activities /// /// The path that triggers this activity. /// - [Display(Description = "The relative path that triggers this activity.")] - [Required] - [UIHint("RelativePath")] + [ActivityProperty(Hint = "The relative path that triggers this activity.")] public Uri Path { get => GetState(); @@ -54,10 +64,13 @@ namespace Elsa.Activities.Http.Activities /// /// The HTTP method that triggers this activity. /// - [Display(Description = "The HTTP method that triggers this activity.")] - [Required] - [UIHint("Dropdown")] - public string Method { + [ActivityProperty( + Type = ActivityPropertyTypes.Select, + Hint = "The HTTP method that triggers this activity." + )] + [SelectOptions("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD")] + public string Method + { get => GetState(); set => SetState(value); } @@ -66,7 +79,10 @@ namespace Elsa.Activities.Http.Activities /// A value indicating whether the HTTP request content body should be read and stored as part of the HTTP request model. /// The stored format depends on the content-type header. /// - [Display(Description = "A value indicating whether the HTTP request content body should be read and stored as part of the HTTP request model. The stored format depends on the content-type header.")] + [ActivityProperty( + Hint = + "A value indicating whether the HTTP request content body should be read and stored as part of the HTTP request model. The stored format depends on the content-type header." + )] public bool ReadContent { get => GetState(); @@ -78,7 +94,8 @@ namespace Elsa.Activities.Http.Activities return Halt(true); } - protected override async Task OnResumeAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + protected override async Task OnResumeAsync(WorkflowExecutionContext workflowContext, + CancellationToken cancellationToken) { var request = httpContextAccessor.HttpContext.Request; var model = new HttpRequestModel @@ -93,7 +110,10 @@ namespace Elsa.Activities.Http.Activities { if (request.HasFormContentType) { - model.Form = (await request.ReadFormAsync(cancellationToken)).ToDictionary(x => x.Key, x => new StringValuesModel(x.Value)); + model.Form = (await request.ReadFormAsync(cancellationToken)).ToDictionary( + x => x.Key, + x => new StringValuesModel(x.Value) + ); } var parser = SelectContentParser(request.ContentType); @@ -111,7 +131,9 @@ namespace Elsa.Activities.Http.Activities private IContentFormatter SelectContentParser(string contentType) { var formatters = contentFormatters.OrderByDescending(x => x.Priority).ToList(); - return formatters.FirstOrDefault(x => x.SupportedContentTypes.Contains(contentType, StringComparer.OrdinalIgnoreCase)) ?? formatters.Last(); + return formatters.FirstOrDefault( + x => x.SupportedContentTypes.Contains(contentType, StringComparer.OrdinalIgnoreCase) + ) ?? formatters.Last(); } } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs b/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs index abd8c28ba..f047d184a 100644 --- a/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs +++ b/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs @@ -1,32 +1,73 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; using Elsa.Services; using Elsa.Services.Models; +using Elsa.WorkflowDesigner.Models; using Microsoft.AspNetCore.Http; namespace Elsa.Activities.Http.Activities { + [ActivityDefinition( + Category = "HTTP", + DisplayName = "Send HTTP Response", + Description = "Send an HTTP response." + )] + [ActivityDefinitionDesigner( + Description = + "x => !!x.state.statusCode ? `Send an HTTP ${ x.state.statusCode }

${ x.state.contentType }
${ !!x.state.content ? x.state.content.expression ? x.state.content.expression.substr(0,100) + '...' : '' : '' }` : x.definition.description", + Outcomes = new[] { OutcomeNames.Done } + )] public class HttpResponseAction : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; private readonly IHttpContextAccessor httpContextAccessor; - public HttpResponseAction(IWorkflowExpressionEvaluator expressionEvaluator, IHttpContextAccessor httpContextAccessor) + public static IEnumerable GetStatusCodes() + { + return new[] + { + new SelectItem + { + Label = "2xx", + Options = new object[] { 200, 201, 202, 203, 204 } + }, + new SelectItem + { + Label = "3xx", + Options = new object[] { 301, 302, 304, 307, 308 } + }, + new SelectItem + { + Label = "4xx", + Options = new object[] { 400, 401, 402, 403, 404, 405, 409, 410, 412, 413, 415, 417, 418, 420, 428, 429 } + } + }; + } + + public HttpResponseAction(IWorkflowExpressionEvaluator expressionEvaluator, + IHttpContextAccessor httpContextAccessor) { this.expressionEvaluator = expressionEvaluator; this.httpContextAccessor = httpContextAccessor; } - + /// /// The HTTP status code to return. /// + [ActivityProperty( + Type = ActivityPropertyTypes.Select, + Hint = "The HTTP status code to write." + )] + [SelectOptions(nameof(GetStatusCodes), typeof(HttpResponseAction))] public HttpStatusCode StatusCode { get => GetState(() => HttpStatusCode.OK); @@ -36,6 +77,8 @@ namespace Elsa.Activities.Http.Activities /// /// The content to send along with the response /// + [ActivityProperty(Hint = "The HTTP content to write.")] + [ExpressionOptions(Multiline = true)] public WorkflowExpression Content { get => GetState(() => new WorkflowExpression(LiteralEvaluator.SyntaxName, "")); @@ -45,29 +88,40 @@ namespace Elsa.Activities.Http.Activities /// /// The Content-Type header to send along with the response. /// + [ActivityProperty( + Type = ActivityPropertyTypes.Select, + Hint = "The HTTP content type header to write." + )] + [SelectOptions("text/plain", "text/html", "application/json", "application/xml")] public string ContentType { get => GetState(); set => SetState(value); } - + /// /// The headers to send along with the response. One 'header: value' pair per line. /// + [ActivityProperty(Hint = "The headers to send along with the response. One 'header: value' pair per line.")] public WorkflowExpression ResponseHeaders { get => GetState(() => new WorkflowExpression(LiteralEvaluator.SyntaxName, "")); set => SetState(value); } - protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, + CancellationToken cancellationToken) { var response = httpContextAccessor.HttpContext.Response; response.StatusCode = (int) StatusCode; response.ContentType = ContentType; - var headersText = await expressionEvaluator.EvaluateAsync(ResponseHeaders, workflowContext, cancellationToken); + var headersText = await expressionEvaluator.EvaluateAsync( + ResponseHeaders, + workflowContext, + cancellationToken + ); if (headersText != null) { @@ -79,7 +133,11 @@ namespace Elsa.Activities.Http.Activities foreach (var header in headersQuery) { var headerValueExpression = new WorkflowExpression(ResponseHeaders.Syntax, header.Value); - response.Headers[header.Key] = await expressionEvaluator.EvaluateAsync(headerValueExpression, workflowContext, cancellationToken); + response.Headers[header.Key] = await expressionEvaluator.EvaluateAsync( + headerValueExpression, + workflowContext, + cancellationToken + ); } } diff --git a/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj b/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj index a5057d360..c70e0db00 100644 --- a/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj +++ b/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj @@ -15,6 +15,7 @@ + diff --git a/src/activities/Elsa.Activities.MassTransit/Activities/ReceiveMassTransitMessage.cs b/src/activities/Elsa.Activities.MassTransit/Activities/ReceiveMassTransitMessage.cs index 89a1b51e5..70257a558 100644 --- a/src/activities/Elsa.Activities.MassTransit/Activities/ReceiveMassTransitMessage.cs +++ b/src/activities/Elsa.Activities.MassTransit/Activities/ReceiveMassTransitMessage.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Extensions; using Elsa.Results; using Elsa.Services; @@ -9,6 +10,11 @@ using Newtonsoft.Json.Linq; namespace Elsa.Activities.MassTransit.Activities { + [ActivityDefinition( + Category = "MassTransit", + DisplayName = "Receive MassTransit Message", + Description = "Receive a message via MassTransit." + )] public class ReceiveMassTransitMessage : Activity { public static Type GetMessageType(JObject state) @@ -16,7 +22,8 @@ namespace Elsa.Activities.MassTransit.Activities var typeName = state.GetState(nameof(MessageType)); return string.IsNullOrWhiteSpace(typeName) ? null : System.Type.GetType(typeName); } - + + [ActivityProperty(Hint = "The assembly-qualified type name of the message to receive.")] public Type MessageType { get => GetMessageType(State); @@ -27,7 +34,7 @@ namespace Elsa.Activities.MassTransit.Activities { var message = context.Workflow.Input["message"]; var messageType = MessageType; - + return message != null && messageType != null && message.GetType() == messageType; } @@ -36,7 +43,8 @@ namespace Elsa.Activities.MassTransit.Activities return Halt(true); } - protected override Task OnResumeAsync(WorkflowExecutionContext context, CancellationToken cancellationToken) + protected override Task OnResumeAsync(WorkflowExecutionContext context, + CancellationToken cancellationToken) { var message = context.Workflow.Input["message"]; context.SetLastResult(message); diff --git a/src/activities/Elsa.Activities.MassTransit/Activities/SendMassTransitMessage.cs b/src/activities/Elsa.Activities.MassTransit/Activities/SendMassTransitMessage.cs index 4b2a9dcd1..e4825b9aa 100644 --- a/src/activities/Elsa.Activities.MassTransit/Activities/SendMassTransitMessage.cs +++ b/src/activities/Elsa.Activities.MassTransit/Activities/SendMassTransitMessage.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Results; using Elsa.Services; @@ -9,6 +10,11 @@ using MassTransit; namespace Elsa.Activities.MassTransit.Activities { + [ActivityDefinition( + Category = "MassTransit", + DisplayName = "Send MassTransit Message", + Description = "Send a message via MassTransit." + )] public class SendMassTransitMessage : Activity { private readonly ISendEndpointProvider sender; @@ -20,6 +26,7 @@ namespace Elsa.Activities.MassTransit.Activities this.evaluator = evaluator; } + [ActivityProperty(Hint = "The assembly-qualified type name of the message to send.")] public Type MessageType { get @@ -30,6 +37,7 @@ namespace Elsa.Activities.MassTransit.Activities set => SetState(value.AssemblyQualifiedName); } + [ActivityProperty(Hint = "An expression that evaluates to the message to send.")] public WorkflowExpression Message { get => GetState(); @@ -41,7 +49,8 @@ namespace Elsa.Activities.MassTransit.Activities return MessageType != null; } - protected override async Task OnExecuteAsync(WorkflowExecutionContext context, CancellationToken cancellationToken) + protected override async Task OnExecuteAsync(WorkflowExecutionContext context, + CancellationToken cancellationToken) { var message = await evaluator.EvaluateAsync(Message, MessageType, context, cancellationToken); await sender.Send(message, cancellationToken); diff --git a/src/activities/Elsa.Activities.Timers/Activities/CronEvent.cs b/src/activities/Elsa.Activities.Timers/Activities/CronEvent.cs index 82a9eb7a3..2d9eaa1a0 100644 --- a/src/activities/Elsa.Activities.Timers/Activities/CronEvent.cs +++ b/src/activities/Elsa.Activities.Timers/Activities/CronEvent.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; @@ -10,6 +11,10 @@ using NodaTime; namespace Elsa.Activities.Timers.Activities { + [ActivityDefinition( + Category = "Timers", + Description = "Triggers periodically based on a specified CRON expression." + )] public class CronEvent : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; @@ -21,6 +26,7 @@ namespace Elsa.Activities.Timers.Activities this.clock = clock; } + [ActivityProperty(Hint = "Specify a CRON expression. See https://crontab.guru/ for help.")] public WorkflowExpression CronExpression { get => GetState(() => new LiteralExpression("* * * * *")); @@ -39,16 +45,22 @@ namespace Elsa.Activities.Timers.Activities return Halt(); } - protected override async Task OnResumeAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + protected override async Task OnResumeAsync(WorkflowExecutionContext workflowContext, + CancellationToken cancellationToken) { var isExpired = await IsExpiredAsync(workflowContext, cancellationToken); return isExpired ? Done() : Halt(); } - private async Task IsExpiredAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + private async Task IsExpiredAsync(WorkflowExecutionContext workflowContext, + CancellationToken cancellationToken) { - var cronExpression = await expressionEvaluator.EvaluateAsync(CronExpression, workflowContext, cancellationToken); + var cronExpression = await expressionEvaluator.EvaluateAsync( + CronExpression, + workflowContext, + cancellationToken + ); var schedule = CrontabSchedule.Parse(cronExpression); var now = clock.GetCurrentInstant(); var startTime = StartTime ?? now; diff --git a/src/activities/Elsa.Activities.Timers/Activities/InstantEvent.cs b/src/activities/Elsa.Activities.Timers/Activities/InstantEvent.cs index 650cc3f93..43d7ff55b 100644 --- a/src/activities/Elsa.Activities.Timers/Activities/InstantEvent.cs +++ b/src/activities/Elsa.Activities.Timers/Activities/InstantEvent.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; @@ -12,6 +13,10 @@ namespace Elsa.Activities.Timers.Activities /// /// Triggers at a specific instant in the future. /// + [ActivityDefinition( + Category = "Timers", + Description = "Triggers at a specified moment in time." + )] public class InstantEvent : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; @@ -26,6 +31,7 @@ namespace Elsa.Activities.Timers.Activities /// /// An expression that evaluates to an /// + [ActivityProperty(Hint = "An expression that evaluates to a NodaTime Instant")] public WorkflowExpression InstantExpression { get => GetState>(); diff --git a/src/activities/Elsa.Activities.Timers/Activities/TimerEvent.cs b/src/activities/Elsa.Activities.Timers/Activities/TimerEvent.cs index f559a507f..0310d961a 100644 --- a/src/activities/Elsa.Activities.Timers/Activities/TimerEvent.cs +++ b/src/activities/Elsa.Activities.Timers/Activities/TimerEvent.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Elsa.Activities.Workflows; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; @@ -11,6 +12,10 @@ using NodaTime; namespace Elsa.Activities.Timers.Activities { + [ActivityDefinition( + Category = "Timers", + Description = "Triggers at a specified interval." + )] public class TimerEvent : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; @@ -22,6 +27,7 @@ namespace Elsa.Activities.Timers.Activities this.clock = clock; } + [ActivityProperty(Hint = "An expression that evaluates to a TimeSpan value")] public WorkflowExpression TimeoutExpression { get => GetState(() => new LiteralExpression("00:01:00")); diff --git a/src/activities/Elsa.Activities.UserTask/Activities/UserTask.cs b/src/activities/Elsa.Activities.UserTask/Activities/UserTask.cs index ec73050c1..ebff58248 100644 --- a/src/activities/Elsa.Activities.UserTask/Activities/UserTask.cs +++ b/src/activities/Elsa.Activities.UserTask/Activities/UserTask.cs @@ -1,16 +1,26 @@ using System; using System.Linq; +using Elsa.Attributes; using Elsa.Results; using Elsa.Services; using Elsa.Services.Models; +using Elsa.WorkflowDesigner.Models; namespace Elsa.Activities.UserTask.Activities { /// /// Stores a set of possible user actions and halts the workflow until one of the actions has been performed. /// + [ActivityDefinition( + Category = "User Tasks", + Description = "Triggers when a user action is received." + )] public class UserTask : Activity { + [ActivityProperty( + Type = ActivityPropertyTypes.List, + Hint = "Enter a comma-separated list of available actions" + )] public string[] Actions { get => GetState(() => new string[0]); @@ -35,6 +45,7 @@ namespace Elsa.Activities.UserTask.Activities return Outcome(userAction); } - private string GetUserAction(WorkflowExecutionContext context) => (string) context.Workflow.Input.GetVariable("UserAction"); + private string GetUserAction(WorkflowExecutionContext context) => + (string) context.Workflow.Input.GetVariable("UserAction"); } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.UserTask/Elsa.Activities.UserTask.csproj b/src/activities/Elsa.Activities.UserTask/Elsa.Activities.UserTask.csproj index 481f8e48b..c25072848 100644 --- a/src/activities/Elsa.Activities.UserTask/Elsa.Activities.UserTask.csproj +++ b/src/activities/Elsa.Activities.UserTask/Elsa.Activities.UserTask.csproj @@ -15,6 +15,7 @@ + diff --git a/src/core/Elsa.Abstractions/Attributes/ActivityDefinitionAttribute.cs b/src/core/Elsa.Abstractions/Attributes/ActivityDefinitionAttribute.cs new file mode 100644 index 000000000..1b1f39bb5 --- /dev/null +++ b/src/core/Elsa.Abstractions/Attributes/ActivityDefinitionAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Elsa.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class ActivityDefinitionAttribute : Attribute + { + public string Type { get; set; } + public string DisplayName { get; set; } + public string Description { get; set; } + public string Category { get; set; } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Attributes/ActivityDefinitionDesignerAttribute.cs b/src/core/Elsa.Abstractions/Attributes/ActivityDefinitionDesignerAttribute.cs new file mode 100644 index 000000000..e2074c0a1 --- /dev/null +++ b/src/core/Elsa.Abstractions/Attributes/ActivityDefinitionDesignerAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Elsa.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class ActivityDefinitionDesignerAttribute : Attribute + { + public string Description { get; set; } + public object Outcomes { get; set; } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Attributes/ActivityPropertyAttribute.cs b/src/core/Elsa.Abstractions/Attributes/ActivityPropertyAttribute.cs new file mode 100644 index 000000000..add2cb01d --- /dev/null +++ b/src/core/Elsa.Abstractions/Attributes/ActivityPropertyAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Elsa.Attributes +{ + [AttributeUsage(AttributeTargets.Property)] + public class ActivityPropertyAttribute : Attribute + { + public string Name { get; set; } + public string Label { get; set; } + public string Type { get; set; } + public string Hint { get; set; } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Attributes/ActivityPropertyOptionsAttribute.cs b/src/core/Elsa.Abstractions/Attributes/ActivityPropertyOptionsAttribute.cs new file mode 100644 index 000000000..3820d993f --- /dev/null +++ b/src/core/Elsa.Abstractions/Attributes/ActivityPropertyOptionsAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace Elsa.Attributes +{ + [AttributeUsage(AttributeTargets.Property)] + public abstract class ActivityPropertyOptionsAttribute : Attribute + { + public abstract object GetOptions(); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Attributes/ExpressionOptionsAttribute.cs b/src/core/Elsa.Abstractions/Attributes/ExpressionOptionsAttribute.cs new file mode 100644 index 000000000..0e892134d --- /dev/null +++ b/src/core/Elsa.Abstractions/Attributes/ExpressionOptionsAttribute.cs @@ -0,0 +1,15 @@ +namespace Elsa.Attributes +{ + public class ExpressionOptionsAttribute : ActivityPropertyOptionsAttribute + { + public bool Multiline { get; set; } + + public override object GetOptions() + { + return new + { + Multiline + }; + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Attributes/SelectOptionsAttribute.cs b/src/core/Elsa.Abstractions/Attributes/SelectOptionsAttribute.cs new file mode 100644 index 000000000..3dde3cfbd --- /dev/null +++ b/src/core/Elsa.Abstractions/Attributes/SelectOptionsAttribute.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Elsa.Attributes +{ + public class SelectOptionsAttribute : ActivityPropertyOptionsAttribute + { + public SelectOptionsAttribute() + { + } + + public SelectOptionsAttribute(params string[] items) + { + Items = items.Cast().ToArray(); + } + + public SelectOptionsAttribute(string optionsProviderMethod, Type providerType) + { + OptionsProviderMethod = optionsProviderMethod; + ProviderType = providerType; + } + + public object[] Items { get; } + public string OptionsProviderMethod { get; } + public Type ProviderType { get; } + + public override object GetOptions() + { + if (Items != null) + return new + { + Items + }; + + + var selectItems = GetItemsFromProvider(); + return new + { + Items = selectItems.Select(ToObject).ToArray() + }; + } + + private IEnumerable GetItemsFromProvider() + { + var method = ProviderType.GetMethod(OptionsProviderMethod, BindingFlags.Public | BindingFlags.Static); + + return (IEnumerable) method.Invoke(null, null); + } + + private object ToObject(object item) + { + if ((item is SelectItem selectItem) && selectItem.Options != null && selectItem.Label == null) + return selectItem.Options; + + return item; + } + } + + public class SelectItem + { + public string Label { get; set; } + public object[] Options { get; set; } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/ControlFlow/ForEach.cs b/src/core/Elsa.Core/Activities/ControlFlow/ForEach.cs index 999db723c..17ee92e1b 100644 --- a/src/core/Elsa.Core/Activities/ControlFlow/ForEach.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/ForEach.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; @@ -9,6 +10,7 @@ using Elsa.Services.Models; namespace Elsa.Activities.ControlFlow { + [ActivityDefinition(Category = "Control Flow", Description = "Iterate over a collection.")] public class ForEach : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; @@ -18,12 +20,14 @@ namespace Elsa.Activities.ControlFlow this.expressionEvaluator = expressionEvaluator; } + [ActivityProperty(Hint = "Enter an expression that evaluates to an array of items to iterate over.")] public WorkflowExpression> CollectionExpression { get => GetState(() => new JavaScriptExpression>("[]")); set => SetState(value); } - + + [ActivityProperty(Hint = "Enter a name for the iterator variable.")] public string IteratorName { get => GetState(() => "CurrentValue"); @@ -36,7 +40,9 @@ namespace Elsa.Activities.ControlFlow set => SetState(value); } - protected override async Task OnExecuteAsync(WorkflowExecutionContext context, CancellationToken cancellationToken) + protected override async Task OnExecuteAsync( + WorkflowExecutionContext context, + CancellationToken cancellationToken) { var collection = await expressionEvaluator.EvaluateAsync(CollectionExpression, context, cancellationToken); var index = CurrentIndex; @@ -55,7 +61,7 @@ namespace Elsa.Activities.ControlFlow { context.BeginScope(); } - + context.CurrentScope.SetVariable(IteratorName, value); return Outcome(OutcomeNames.Iterate); diff --git a/src/core/Elsa.Core/Activities/ControlFlow/Fork.cs b/src/core/Elsa.Core/Activities/ControlFlow/Fork.cs index 094b5ef9f..88f8c2ca3 100644 --- a/src/core/Elsa.Core/Activities/ControlFlow/Fork.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/Fork.cs @@ -1,18 +1,24 @@ using System.Collections.Generic; +using Elsa.Attributes; using Elsa.Results; using Elsa.Services; using Elsa.Services.Models; namespace Elsa.Activities.ControlFlow { + [ActivityDefinition(Category = "Control Flow", Description = "Fork workflow execution into multiple branches.")] + [ActivityDefinitionDesigner(Outcomes = "x => x.state.branches")] public class Fork : Activity { + [ActivityProperty( + Hint = "Enter one or more names representing branches, separated with a comma. Example: Branch 1, Branch 2" + )] public IList Branches { get => GetState(() => new List()); set => SetState(value); } - + protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext) { return Outcomes(Branches); diff --git a/src/core/Elsa.Core/Activities/ControlFlow/IfElse.cs b/src/core/Elsa.Core/Activities/ControlFlow/IfElse.cs index 49936da9b..4bab9e82c 100644 --- a/src/core/Elsa.Core/Activities/ControlFlow/IfElse.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/IfElse.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; @@ -8,6 +9,16 @@ using Elsa.Services.Models; namespace Elsa.Activities.ControlFlow { + [ActivityDefinition( + DisplayName = "If/Else", + Category = "Control Flow", + Description = "Evaluate a Boolean expression and continue execution depending on the result." + )] + [ActivityDefinitionDesigner( + Description = + "x => !!x.state.expression ? `Evaluate ${ x.state.expression.expression } and continue execution depending on the result.` : x.definition.description", + Outcomes = new[] { OutcomeNames.True, OutcomeNames.False } + )] public class IfElse : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; @@ -17,16 +28,22 @@ namespace Elsa.Activities.ControlFlow this.expressionEvaluator = expressionEvaluator; } + [ActivityProperty(Hint = "The expression to evaluate. The evaluated value will be used to switch on.")] public WorkflowExpression ConditionExpression { get => GetState(() => new JavaScriptExpression("true")); set => SetState(value); } - protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, + CancellationToken cancellationToken) { - var result = await expressionEvaluator.EvaluateAsync(ConditionExpression, workflowContext, cancellationToken); - return Outcome(result ? OutcomeNames.True: OutcomeNames.False); + var result = await expressionEvaluator.EvaluateAsync( + ConditionExpression, + workflowContext, + cancellationToken + ); + return Outcome(result ? OutcomeNames.True : OutcomeNames.False); } } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/ControlFlow/Join.cs b/src/core/Elsa.Core/Activities/ControlFlow/Join.cs index faea77e67..129e7bff6 100644 --- a/src/core/Elsa.Core/Activities/ControlFlow/Join.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/Join.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Results; using Elsa.Services; using Elsa.Services.Extensions; @@ -9,19 +10,29 @@ using Elsa.Services.Models; namespace Elsa.Activities.ControlFlow { + [ActivityDefinition( + Category = "Control Flow", + Description = "Merge workflow execution back into a single branch." + )] + [ActivityDefinitionDesigner( + Description = + "x => !!x.state.joinMode ? `Merge workflow execution back into a single branch using mode ${ x.state.joinMode }` : x.definition.description", + Outcomes = new[] { OutcomeNames.Done } + )] public class Join : Activity, IWorkflowEventHandler { public Join() { InboundTransitions = new List().AsReadOnly(); } - + public enum JoinMode { WaitAll, WaitAny } + [ActivityProperty(Hint = "Either 'WaitAll' or 'WaitAny'")] public JoinMode Mode { get => GetState(() => JoinMode.WaitAll); @@ -33,7 +44,7 @@ namespace Elsa.Activities.ControlFlow get => GetState>(); set => SetState(value); } - + protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext) { var recordedInboundTransitions = InboundTransitions ?? new List(); @@ -50,12 +61,13 @@ namespace Elsa.Activities.ControlFlow done = inboundConnections.Any(x => recordedInboundTransitions.Contains(GetTransitionKey(x))); break; } - + if (done) { // Remove any inbound blocking activities. var ancestorActivityIds = workflow.GetInboundActivityPath(Id).ToList(); - var blockingActivities = workflow.BlockingActivities.Where(x => ancestorActivityIds.Contains(x.Id)).ToList(); + var blockingActivities = + workflow.BlockingActivities.Where(x => ancestorActivityIds.Contains(x.Id)).ToList(); foreach (var blockingActivity in blockingActivities) { @@ -87,7 +99,10 @@ namespace Elsa.Activities.ControlFlow { var joinActivity = (Join) inboundConnection.Target.Activity; var inboundTransitions = joinActivity.InboundTransitions ?? new List(); - joinActivity.InboundTransitions = inboundTransitions.Union(new[] { GetTransitionKey(inboundConnection) }).Distinct().ToList(); + joinActivity.InboundTransitions = inboundTransitions + .Union(new[] { GetTransitionKey(inboundConnection) }) + .Distinct() + .ToList(); } } @@ -99,14 +114,18 @@ namespace Elsa.Activities.ControlFlow return $"@{sourceActivityId}_{sourceOutcomeName}"; } - Task IWorkflowEventHandler.ActivityExecutedAsync(WorkflowExecutionContext workflowContext, IActivity activity, CancellationToken cancellationToken) + Task IWorkflowEventHandler.ActivityExecutedAsync(WorkflowExecutionContext workflowContext, IActivity activity, + CancellationToken cancellationToken) { RecordInboundTransitions(workflowContext, activity); return Task.CompletedTask; } - Task IWorkflowEventHandler.InvokingHaltedActivitiesAsync(WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken) => Task.CompletedTask; - Task IWorkflowEventHandler.WorkflowInvokedAsync(WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken) => Task.CompletedTask; + Task IWorkflowEventHandler.InvokingHaltedActivitiesAsync(WorkflowExecutionContext workflowExecutionContext, + CancellationToken cancellationToken) => Task.CompletedTask; + + Task IWorkflowEventHandler.WorkflowInvokedAsync(WorkflowExecutionContext workflowExecutionContext, + CancellationToken cancellationToken) => Task.CompletedTask; } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/ControlFlow/Switch.cs b/src/core/Elsa.Core/Activities/ControlFlow/Switch.cs index 1e6c63617..e55f18678 100644 --- a/src/core/Elsa.Core/Activities/ControlFlow/Switch.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/Switch.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; @@ -9,29 +10,41 @@ using Elsa.Services.Models; namespace Elsa.Activities.ControlFlow { + [ActivityDefinition( + Category = "Control Flow", + Description = "Switch execution based on a given expression." + )] + [ActivityDefinitionDesigner( + Description = + "x => !!x.state.expression ? `Switch execution based on ${ x.state.expression.expression }.` : x.definition.description", + Outcomes = "x => x.state.cases.map(c => c.toString())" + )] public class Switch : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; - + public Switch(IWorkflowExpressionEvaluator expressionEvaluator) { this.expressionEvaluator = expressionEvaluator; Cases = new List(); } - + + [ActivityProperty(Hint = "The expression to evaluate. The evaluated value will be used to switch on.")] public WorkflowExpression Expression { get => GetState>(); set => SetState(value); } + [ActivityProperty(Hint = "A comma-separated list of possible outcomes of the expression.")] public IReadOnlyCollection Cases { get => GetState>(); set => SetState(value); } - - protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + + protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, + CancellationToken cancellationToken) { var result = await expressionEvaluator.EvaluateAsync(Expression, workflowContext, cancellationToken); return Outcome(result); diff --git a/src/core/Elsa.Core/Activities/Primitives/SetVariable.cs b/src/core/Elsa.Core/Activities/Primitives/SetVariable.cs index 5222d86e2..fb63e4167 100644 --- a/src/core/Elsa.Core/Activities/Primitives/SetVariable.cs +++ b/src/core/Elsa.Core/Activities/Primitives/SetVariable.cs @@ -1,13 +1,23 @@ using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; -using Elsa.Extensions; using Elsa.Results; using Elsa.Services; using Elsa.Services.Models; namespace Elsa.Activities.Primitives { + [ActivityDefinition( + DisplayName = "Set Variable", + Description = "Set variable on the workflow.", + Category = "Primitives" + )] + [ActivityDefinitionDesigner( + Description = + "x => !!x.state.variableName ? `${x.state.expression.syntax}: ${x.state.variableName} = ${x.state.expression.expression}` : x.definition.description", + Outcomes = new[] { OutcomeNames.Done } + )] public class SetVariable : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; @@ -17,21 +27,30 @@ namespace Elsa.Activities.Primitives this.expressionEvaluator = expressionEvaluator; } + [ActivityProperty(Hint = "The name of the variable to store the value into.")] public string VariableName { get => GetState(); set => SetState(value); } + [ActivityProperty(Hint = "An expression that evaluates to the value to store in the variable.")] public WorkflowExpression ValueExpression { get => GetState(); set => SetState(value); } - protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + protected override async Task OnExecuteAsync( + WorkflowExecutionContext workflowContext, + CancellationToken cancellationToken) { - var value = await expressionEvaluator.EvaluateAsync(ValueExpression, ValueExpression.Type, workflowContext, cancellationToken); + var value = await expressionEvaluator.EvaluateAsync( + ValueExpression, + ValueExpression.Type, + workflowContext, + cancellationToken + ); workflowContext.CurrentScope.SetVariable(VariableName, value); return Done(); } diff --git a/src/core/Elsa.Core/Activities/Workflows/Correlate.cs b/src/core/Elsa.Core/Activities/Workflows/Correlate.cs index ae8423789..58ec79a24 100644 --- a/src/core/Elsa.Core/Activities/Workflows/Correlate.cs +++ b/src/core/Elsa.Core/Activities/Workflows/Correlate.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; @@ -11,6 +12,10 @@ namespace Elsa.Activities.Workflows /// /// Sets the CorrelationId of the workflow to a given value. /// + [ActivityDefinition( + Category = "Workflows", + Description = "Set the CorrelationId of the workflow to a given value." + )] public class Correlate : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; @@ -20,15 +25,18 @@ namespace Elsa.Activities.Workflows this.expressionEvaluator = expressionEvaluator; } - public WorkflowExpression Expression + [ActivityProperty(Hint = "An expression that evaluates to the value to store as the correlation ID.")] + public WorkflowExpression ValueExpression { get => GetState>(); set => SetState(value); } - protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + protected override async Task OnExecuteAsync( + WorkflowExecutionContext workflowContext, + CancellationToken cancellationToken) { - var value = await expressionEvaluator.EvaluateAsync(Expression, workflowContext, cancellationToken); + var value = await expressionEvaluator.EvaluateAsync(ValueExpression, workflowContext, cancellationToken); workflowContext.Workflow.CorrelationId = value; return Done(); } diff --git a/src/core/Elsa.Core/Activities/Workflows/Finish.cs b/src/core/Elsa.Core/Activities/Workflows/Finish.cs index 3953cabe1..8a951d827 100644 --- a/src/core/Elsa.Core/Activities/Workflows/Finish.cs +++ b/src/core/Elsa.Core/Activities/Workflows/Finish.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Models; @@ -9,6 +10,10 @@ using Elsa.Services.Models; namespace Elsa.Activities.Workflows { + [ActivityDefinition( + Category = "Workflows", + Description = "Removes any blocking activities and sets the status of the workflow to Finished." + )] public class Finish : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; @@ -18,6 +23,7 @@ namespace Elsa.Activities.Workflows this.expressionEvaluator = expressionEvaluator; } + [ActivityProperty(Hint = "An expression that evaluates to a dictionary to be set as the workflow's output.'")] public WorkflowExpression WorkflowOutput { get => GetState(() => new JavaScriptExpression("({})")); diff --git a/src/core/Elsa.Core/Activities/Workflows/Signaled.cs b/src/core/Elsa.Core/Activities/Workflows/Signaled.cs index 03ad819e8..24f12e37b 100644 --- a/src/core/Elsa.Core/Activities/Workflows/Signaled.cs +++ b/src/core/Elsa.Core/Activities/Workflows/Signaled.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Results; @@ -11,6 +12,10 @@ namespace Elsa.Activities.Workflows /// /// Halts workflow execution until the specified signal is received. /// + [ActivityDefinition( + Category = "Workflows", + Description = "Halt workflow execution until the specified signal is received." + )] public class Signaled : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; @@ -20,6 +25,7 @@ namespace Elsa.Activities.Workflows this.expressionEvaluator = expressionEvaluator; } + [ActivityProperty(Hint = "An expression that evaluates to the name of the signal to wait for.")] public WorkflowExpression Signal { get => GetState>(); diff --git a/src/core/Elsa.Core/Activities/Workflows/Start.cs b/src/core/Elsa.Core/Activities/Workflows/Start.cs index 6e2b18ee9..b3b7a6da8 100644 --- a/src/core/Elsa.Core/Activities/Workflows/Start.cs +++ b/src/core/Elsa.Core/Activities/Workflows/Start.cs @@ -1,9 +1,14 @@ -using Elsa.Results; +using Elsa.Attributes; +using Elsa.Results; using Elsa.Services; using Elsa.Services.Models; namespace Elsa.Activities.Workflows { + [ActivityDefinition( + Category = "Workflows", + Description = "Useful when you need to invoke the workflow manually." + )] public class Start : Activity { protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext context) diff --git a/src/core/Elsa.Core/Activities/Workflows/Trigger.cs b/src/core/Elsa.Core/Activities/Workflows/Trigger.cs index f3209a940..4c7e651b4 100644 --- a/src/core/Elsa.Core/Activities/Workflows/Trigger.cs +++ b/src/core/Elsa.Core/Activities/Workflows/Trigger.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Models; @@ -12,6 +13,10 @@ namespace Elsa.Activities.Workflows /// /// Triggers the specified signal. /// + [ActivityDefinition( + Category = "Workflows", + Description = "Trigger the specified signal." + )] public class Trigger : Activity { private readonly IWorkflowExpressionEvaluator expressionEvaluator; @@ -23,18 +28,23 @@ namespace Elsa.Activities.Workflows this.workflowInvoker = workflowInvoker; } + [ActivityProperty(Hint = "An expression that evaluates to the name of the signal to trigger.")] public WorkflowExpression Signal { get => GetState>(); set => SetState(value); } + [ActivityProperty( + Hint = "An expression that evaluates to a dictionary to be provided as input when signaling." + )] public WorkflowExpression Input { get => GetState(() => new WorkflowExpression(JavaScriptEvaluator.SyntaxName, "{}")); set => SetState(value); } + [ActivityProperty(Hint = "An expression that evaluates to the correlation ID to use when signaling.")] public WorkflowExpression CorrelationId { get => GetState(() => new WorkflowExpression(LiteralEvaluator.SyntaxName, "")); @@ -57,7 +67,7 @@ namespace Elsa.Activities.Workflows correlationId, cancellationToken: cancellationToken ); - + return Done(); } } diff --git a/src/core/Elsa.Core/Activities/Workflows/TriggerWorkflow.cs b/src/core/Elsa.Core/Activities/Workflows/TriggerWorkflow.cs index b8e41ddfb..28692d3d5 100644 --- a/src/core/Elsa.Core/Activities/Workflows/TriggerWorkflow.cs +++ b/src/core/Elsa.Core/Activities/Workflows/TriggerWorkflow.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Elsa.Attributes; using Elsa.Expressions; using Elsa.Extensions; using Elsa.Models; @@ -10,6 +11,10 @@ using Elsa.Services.Models; namespace Elsa.Activities.Workflows { + [ActivityDefinition( + Category = "Workflows", + Description = "Trigger all workflows that start with or are blocked on the specified activity type." + )] public class TriggerWorkflow : Activity { private readonly IWorkflowInvoker workflowInvoker; @@ -21,18 +26,23 @@ namespace Elsa.Activities.Workflows this.expressionEvaluator = expressionEvaluator; } + [ActivityProperty(Hint = "An expression that evaluates to the activity type to use when triggering workflows.")] public WorkflowExpression ActivityType { get => GetState(() => new LiteralExpression("")); set => SetState(value); } + [ActivityProperty( + Hint = "An expression that evaluates to a dictionary to be provided as input when triggering workflows." + )] public WorkflowExpression Input { get => GetState(() => new JavaScriptExpression("{}")); set => SetState(value); } + [ActivityProperty(Hint = "An expression that evaluates to the correlation ID to use when triggering workflows.")] public WorkflowExpression CorrelationId { get => GetState(() => new LiteralExpression(Guid.NewGuid().ToString())); diff --git a/src/dashboard/Elsa.Dashboard.Web/Activities/SampleActivity.cs b/src/dashboard/Elsa.Dashboard.Web/Activities/SampleActivity.cs new file mode 100644 index 000000000..a2fa315c8 --- /dev/null +++ b/src/dashboard/Elsa.Dashboard.Web/Activities/SampleActivity.cs @@ -0,0 +1,15 @@ +using Elsa.Attributes; +using Elsa.Services; + +namespace Elsa.Dashboard.Web.Activities +{ + public class SampleActivity : Activity + { + [ActivityProperty] + public string FirstName + { + get => GetState(); + set => SetState(value); + } + } +} \ No newline at end of file diff --git a/src/dashboard/Elsa.Dashboard.Web/Elsa.Dashboard.Web.csproj b/src/dashboard/Elsa.Dashboard.Web/Elsa.Dashboard.Web.csproj index 6a3049976..deb11f9e9 100644 --- a/src/dashboard/Elsa.Dashboard.Web/Elsa.Dashboard.Web.csproj +++ b/src/dashboard/Elsa.Dashboard.Web/Elsa.Dashboard.Web.csproj @@ -16,6 +16,12 @@ + + + + + + diff --git a/src/dashboard/Elsa.Dashboard.Web/Startup.cs b/src/dashboard/Elsa.Dashboard.Web/Startup.cs index 64daf8c84..2bce2a0c9 100644 --- a/src/dashboard/Elsa.Dashboard.Web/Startup.cs +++ b/src/dashboard/Elsa.Dashboard.Web/Startup.cs @@ -1,6 +1,7 @@ using System.IO; using Elsa.Dashboard.Extensions; using Elsa.Persistence.EntityFrameworkCore.Extensions; +using Elsa.Services.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; @@ -27,6 +28,8 @@ namespace Elsa.Dashboard.Web services .AddMvc( /*options => options.Conventions.Add(new AddLocalhostFilterConvention())*/) .SetCompatibilityVersion(CompatibilityVersion.Latest) + + // This is entirely optional and only useful when developing the Elsa.Dashboard project. .AddRazorOptions( options => { @@ -47,7 +50,19 @@ namespace Elsa.Dashboard.Web ) .AddEntityFrameworkCoreWorkflowDefinitionStore() .AddEntityFrameworkCoreWorkflowInstanceStore() - .AddElsaDashboard(); + .AddElsaDashboard( + options => options + // Add activity definitions from configuration. + .Bind(Configuration.GetSection("WorkflowDesigner")) + .Configure( + configureOptions => configureOptions.ActivityDefinitions + // Add all activities from all referenced assemblies. + .Discover( + selector => selector.FromApplicationDependencies() + .AddClasses(x => x.AssignableTo()) + ) + ) + ); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) diff --git a/src/dashboard/Elsa.Dashboard.Web/appsettings.json b/src/dashboard/Elsa.Dashboard.Web/appsettings.json index 34f7ca77b..bbf08cf46 100644 --- a/src/dashboard/Elsa.Dashboard.Web/appsettings.json +++ b/src/dashboard/Elsa.Dashboard.Web/appsettings.json @@ -1,5 +1,31 @@ { "ConnectionStrings": { "Sqlite": "Data Source=c:\\data\\elsa.db;Cache=Shared" + }, + "WorkflowDesigner": { + "ActivityDefinitions": [ + { + "Type": "MyCustomActivity2", + "DisplayName": "My Custom Activity 2", + "Category": "Custom", + "Description": "Demonstrates adding custom activities to the designer", + "Properties": [ + { + "Name": "Property1", + "Label": "Property 1", + "Type": "expression", + "Hint": "Specify any value you like.", + "Options": { + "Multiline": false + }, + "Designer": { + "Outcomes": [ + "Done" + ] + } + } + ] + } + ] } -} +} \ No newline at end of file diff --git a/src/dashboard/Elsa.Dashboard/Areas/Elsa/Controllers/WorkflowDefinitionController.cs b/src/dashboard/Elsa.Dashboard/Areas/Elsa/Controllers/WorkflowDefinitionController.cs index bb2cc1422..b31ce5d18 100644 --- a/src/dashboard/Elsa.Dashboard/Areas/Elsa/Controllers/WorkflowDefinitionController.cs +++ b/src/dashboard/Elsa.Dashboard/Areas/Elsa/Controllers/WorkflowDefinitionController.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Elsa.Dashboard.Areas.Elsa.ViewModels; using Elsa.Dashboard.Extensions; using Elsa.Dashboard.Models; +using Elsa.Dashboard.Options; using Elsa.Dashboard.Services; using Elsa.Extensions; using Elsa.Models; @@ -12,6 +13,9 @@ using Elsa.Serialization; using Elsa.Serialization.Formatters; using Elsa.Services; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace Elsa.Dashboard.Areas.Elsa.Controllers { @@ -23,6 +27,7 @@ namespace Elsa.Dashboard.Areas.Elsa.Controllers private readonly IWorkflowInstanceStore workflowInstanceStore; private readonly IWorkflowPublisher publisher; private readonly IWorkflowSerializer serializer; + private readonly IOptions options; private readonly IIdGenerator idGenerator; private readonly INotifier notifier; @@ -31,6 +36,7 @@ namespace Elsa.Dashboard.Areas.Elsa.Controllers IWorkflowInstanceStore workflowInstanceStore, IWorkflowPublisher publisher, IWorkflowSerializer serializer, + IOptions options, IIdGenerator idGenerator, INotifier notifier) { @@ -38,6 +44,7 @@ namespace Elsa.Dashboard.Areas.Elsa.Controllers this.workflowDefinitionStore = workflowDefinitionStore; this.workflowInstanceStore = workflowInstanceStore; this.serializer = serializer; + this.options = options; this.idGenerator = idGenerator; this.notifier = notifier; } @@ -73,7 +80,8 @@ namespace Elsa.Dashboard.Areas.Elsa.Controllers var model = new WorkflowDefinitionEditModel { Name = workflow.Name, - Json = serializer.Serialize(workflow, JsonTokenFormatter.FormatName) + Json = serializer.Serialize(workflow, JsonTokenFormatter.FormatName), + ActivityDefinitions = options.Value.ActivityDefinitions.ToArray() }; return View(model); @@ -110,7 +118,8 @@ namespace Elsa.Dashboard.Areas.Elsa.Controllers Id = workflow.Id, Name = workflow.Name, Description = workflow.Description, - Json = serializer.Serialize(workflow, JsonTokenFormatter.FormatName) + Json = serializer.Serialize(workflow, JsonTokenFormatter.FormatName), + ActivityDefinitions = options.Value.ActivityDefinitions.ToArray() }; return View(model); diff --git a/src/dashboard/Elsa.Dashboard/Areas/Elsa/ViewModels/WorkflowDefinitionEditModel.cs b/src/dashboard/Elsa.Dashboard/Areas/Elsa/ViewModels/WorkflowDefinitionEditModel.cs index f82d5adb5..db28e5a97 100644 --- a/src/dashboard/Elsa.Dashboard/Areas/Elsa/ViewModels/WorkflowDefinitionEditModel.cs +++ b/src/dashboard/Elsa.Dashboard/Areas/Elsa/ViewModels/WorkflowDefinitionEditModel.cs @@ -1,3 +1,5 @@ +using Elsa.WorkflowDesigner.Models; + namespace Elsa.Dashboard.Areas.Elsa.ViewModels { public class WorkflowDefinitionEditModel @@ -7,5 +9,6 @@ namespace Elsa.Dashboard.Areas.Elsa.ViewModels public string SubmitAction { get; set; } public string Name { get; set; } public string Description { get; set; } + public ActivityDefinition[] ActivityDefinitions { get; set; } } } \ No newline at end of file diff --git a/src/dashboard/Elsa.Dashboard/Areas/Elsa/Views/Shared/WorkflowDefinitionEditor.cshtml b/src/dashboard/Elsa.Dashboard/Areas/Elsa/Views/Shared/WorkflowDefinitionEditor.cshtml index 6f9938658..ca80c5bef 100644 --- a/src/dashboard/Elsa.Dashboard/Areas/Elsa/Views/Shared/WorkflowDefinitionEditor.cshtml +++ b/src/dashboard/Elsa.Dashboard/Areas/Elsa/Views/Shared/WorkflowDefinitionEditor.cshtml @@ -24,7 +24,7 @@
- +
diff --git a/src/dashboard/Elsa.Dashboard/Areas/Elsa/Views/Shared/_Layout.cshtml b/src/dashboard/Elsa.Dashboard/Areas/Elsa/Views/Shared/_Layout.cshtml index a646004be..7486f6474 100644 --- a/src/dashboard/Elsa.Dashboard/Areas/Elsa/Views/Shared/_Layout.cshtml +++ b/src/dashboard/Elsa.Dashboard/Areas/Elsa/Views/Shared/_Layout.cshtml @@ -32,7 +32,7 @@ - + @await RenderSectionAsync("HeadScripts", false) diff --git a/src/dashboard/Elsa.Dashboard/Areas/Elsa/Views/_ViewImports.cshtml b/src/dashboard/Elsa.Dashboard/Areas/Elsa/Views/_ViewImports.cshtml index 6d892d13c..da7508a62 100644 --- a/src/dashboard/Elsa.Dashboard/Areas/Elsa/Views/_ViewImports.cshtml +++ b/src/dashboard/Elsa.Dashboard/Areas/Elsa/Views/_ViewImports.cshtml @@ -2,3 +2,4 @@ @using Elsa.Dashboard.Areas.Elsa.ViewModels @addTagHelper *, GravatarHelper.AspNetCore @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Elsa.WorkflowDesigner diff --git a/src/dashboard/Elsa.Dashboard/Elsa.Dashboard.csproj b/src/dashboard/Elsa.Dashboard/Elsa.Dashboard.csproj index 1ecf6b207..d11fe57cc 100644 --- a/src/dashboard/Elsa.Dashboard/Elsa.Dashboard.csproj +++ b/src/dashboard/Elsa.Dashboard/Elsa.Dashboard.csproj @@ -19,6 +19,7 @@ + @@ -26,20 +27,17 @@ - - + + + - - ..\..\..\..\..\..\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.extensions.fileproviders.physical\2.2.0\lib\netstandard2.0\Microsoft.Extensions.FileProviders.Physical.dll - - - - - <_ContentIncludedByDefault Remove="wwwroot\assets\css\argon-dashboard.css" /> - <_ContentIncludedByDefault Remove="wwwroot\assets\css\argon-dashboard.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\assets\css\argon-dashboard.min.css" /> + <_ContentIncludedByDefault Remove="wwwroot\assets\css\argon-dashboard.css" /> + <_ContentIncludedByDefault Remove="wwwroot\assets\css\argon-dashboard.css.map" /> + <_ContentIncludedByDefault Remove="wwwroot\assets\css\argon-dashboard.min.css" /> + <_ContentIncludedByDefault Remove="Views\Shared\Components\WorkflowDesigner\Default.cshtml" /> + <_ContentIncludedByDefault Remove="Views\_ViewImports.cshtml" /> diff --git a/src/dashboard/Elsa.Dashboard/Extensions/ActivityDefinitionListExtensions.cs b/src/dashboard/Elsa.Dashboard/Extensions/ActivityDefinitionListExtensions.cs new file mode 100644 index 000000000..98ba4ae3b --- /dev/null +++ b/src/dashboard/Elsa.Dashboard/Extensions/ActivityDefinitionListExtensions.cs @@ -0,0 +1,33 @@ +using System; +using Elsa.Dashboard.Options; +using Elsa.Services.Models; +using Elsa.WorkflowDesigner; +using Microsoft.Extensions.DependencyInjection; +using Scrutor; + +namespace Elsa.Dashboard.Extensions +{ + public static class ActivityDefinitionListExtensions + { + public static ActivityDefinitionList Add(this ActivityDefinitionList list) where T : IActivity + { + return list.Add(ActivityDescriber.Describe()); + } + + public static ActivityDefinitionList Discover(this ActivityDefinitionList list, Action selector) + { + var typeSourceSelector = new TypeSourceSelector(); + selector(typeSourceSelector); + + var serviceCollection = new ServiceCollection(); + typeSourceSelector.Populate(serviceCollection, RegistrationStrategy.Replace(ReplacementBehavior.All)); + + foreach (var service in serviceCollection) + { + list.Add(ActivityDescriber.Describe(service.ImplementationType)); + } + + return list; + } + } +} \ No newline at end of file diff --git a/src/dashboard/Elsa.Dashboard/Options/ActivityDefinitionList.cs b/src/dashboard/Elsa.Dashboard/Options/ActivityDefinitionList.cs new file mode 100644 index 000000000..c9bcaf944 --- /dev/null +++ b/src/dashboard/Elsa.Dashboard/Options/ActivityDefinitionList.cs @@ -0,0 +1,29 @@ +using System.Collections; +using System.Collections.Generic; +using Elsa.WorkflowDesigner.Models; + +namespace Elsa.Dashboard.Options +{ + public class ActivityDefinitionList : IEnumerable + { + public ActivityDefinitionList() + { + Items = new Dictionary(); + } + + private IDictionary Items { get; } + + public ActivityDefinitionList Add(ActivityDefinition item) + { + Items[item.Type] = item; + return this; + } + + public IEnumerator GetEnumerator() => Items.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/dashboard/Elsa.Dashboard/Options/ElsaDashboardOptions.cs b/src/dashboard/Elsa.Dashboard/Options/ElsaDashboardOptions.cs index 1d49d1b6b..9da2ca879 100644 --- a/src/dashboard/Elsa.Dashboard/Options/ElsaDashboardOptions.cs +++ b/src/dashboard/Elsa.Dashboard/Options/ElsaDashboardOptions.cs @@ -4,6 +4,13 @@ namespace Elsa.Dashboard.Options { public class ElsaDashboardOptions { + public ElsaDashboardOptions() + { + Scripts = new List(); + ActivityDefinitions = new ActivityDefinitionList(); + } + public IList Scripts { get; set; } + public ActivityDefinitionList ActivityDefinitions { get; set; } } } \ No newline at end of file diff --git a/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/blue.png b/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/blue.png deleted file mode 100644 index 88f515f1d..000000000 Binary files a/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/blue.png and /dev/null differ diff --git a/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/favicon-16x16.png b/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/favicon-16x16.png new file mode 100644 index 000000000..8d6de2988 Binary files /dev/null and b/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/favicon-16x16.png differ diff --git a/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/favicon.ico b/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/favicon.ico new file mode 100644 index 000000000..ce861ecff Binary files /dev/null and b/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/favicon.ico differ diff --git a/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/favicon.png b/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/favicon.png index 53997a067..dfa8b3a2c 100644 Binary files a/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/favicon.png and b/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/favicon.png differ diff --git a/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/white.png b/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/white.png deleted file mode 100644 index df2708f39..000000000 Binary files a/src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/white.png and /dev/null differ diff --git a/src/dashboard/Elsa.WorkflowDesigner/ActivityDescriber.cs b/src/dashboard/Elsa.WorkflowDesigner/ActivityDescriber.cs new file mode 100644 index 000000000..c88cfa153 --- /dev/null +++ b/src/dashboard/Elsa.WorkflowDesigner/ActivityDescriber.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Elsa.Attributes; +using Elsa.Expressions; +using Elsa.Services.Models; +using Elsa.WorkflowDesigner.Models; +using Humanizer; + +namespace Elsa.WorkflowDesigner +{ + public static class ActivityDescriber + { + public static ActivityDefinition Describe() where T : IActivity + { + return Describe(typeof(T)); + } + + public static ActivityDefinition Describe(Type activityType) + { + var activityDefinitionAttribute = activityType.GetCustomAttribute(); + var activityDesignerAttribute = activityType.GetCustomAttribute(); + var typeName = activityDefinitionAttribute?.Type ?? activityType.Name; + + var displayName = + activityDefinitionAttribute?.DisplayName ?? + activityType.Name.Humanize(LetterCasing.Title); + + var description = activityDefinitionAttribute?.Description; + var category = activityDefinitionAttribute?.Category ?? "Miscellaneous"; + var designerDescription = activityDesignerAttribute?.Description; + var designerOutcomes = activityDesignerAttribute?.Outcomes ?? new[] { OutcomeNames.Done }; + var properties = DescribeProperties(activityType); + + return new ActivityDefinition + { + Type = typeName.Camelize(), + DisplayName = displayName, + Description = description, + Category = category, + Properties = properties.ToArray(), + Designer = new ActivityDesignerSettings + { + Description = designerDescription, + Outcomes = designerOutcomes + } + }; + } + + private static IEnumerable DescribeProperties(Type activityType) + { + var properties = activityType.GetProperties(); + + foreach (var propertyInfo in properties) + { + var activityProperty = propertyInfo.GetCustomAttribute(); + + if (activityProperty == null) + yield break; + + yield return new ActivityPropertyDescriptor + { + Name = (activityProperty.Name ?? propertyInfo.Name).Camelize(), + Label = activityProperty.Label ?? propertyInfo.Name.Humanize(LetterCasing.Title), + Type = (activityProperty.Type ?? DeterminePropertyType(propertyInfo)).Camelize(), + Hint = activityProperty.Hint, + Options = GetPropertyTypeOptions(propertyInfo) + }; + } + } + + private static object GetPropertyTypeOptions(PropertyInfo propertyInfo) + { + var optionsAttribute = propertyInfo.GetCustomAttribute(); + + return optionsAttribute?.GetOptions() ?? new object(); + } + + private static string DeterminePropertyType(PropertyInfo propertyInfo) + { + var type = propertyInfo.PropertyType; + + if (typeof(IWorkflowExpression).IsAssignableFrom(type)) + return ActivityPropertyTypes.Expression; + + if (type == typeof(bool) || type == typeof(bool?)) + return ActivityPropertyTypes.Boolean; + + if (typeof(IEnumerable).IsAssignableFrom(type)) + return ActivityPropertyTypes.List; + + return ActivityPropertyTypes.Text; + } + } +} \ No newline at end of file diff --git a/src/dashboard/Elsa.WorkflowDesigner/Elsa.WorkflowDesigner.csproj b/src/dashboard/Elsa.WorkflowDesigner/Elsa.WorkflowDesigner.csproj new file mode 100644 index 000000000..2f155cdba --- /dev/null +++ b/src/dashboard/Elsa.WorkflowDesigner/Elsa.WorkflowDesigner.csproj @@ -0,0 +1,25 @@ + + + + netstandard2.0 + latest + 1.0.0 + Sipke Schoorstra + Elsa is a set of workflowing libraries and tools to enable super-fast workflowing capabilities in any .NET Core application. + 2019 + https://github.com/elsa-workflows/elsa-core + https://github.com/elsa-workflows/elsa-core + GitHub + elsa, workflows + + + + + + + + + + + + diff --git a/src/dashboard/Elsa.WorkflowDesigner/Models/ActivityDefinition.cs b/src/dashboard/Elsa.WorkflowDesigner/Models/ActivityDefinition.cs new file mode 100644 index 000000000..a50dd5e02 --- /dev/null +++ b/src/dashboard/Elsa.WorkflowDesigner/Models/ActivityDefinition.cs @@ -0,0 +1,17 @@ +namespace Elsa.WorkflowDesigner.Models +{ + public class ActivityDefinition + { + public ActivityDefinition() + { + Properties = new ActivityPropertyDescriptor[0]; + } + + public string Type { get; set; } + public string DisplayName { get; set; } + public string Description { get; set; } + public string Category { get; set; } + public ActivityPropertyDescriptor[] Properties { get; set; } + public ActivityDesignerSettings Designer { get; set; } + } +} \ No newline at end of file diff --git a/src/dashboard/Elsa.WorkflowDesigner/Models/ActivityDesignerSettings.cs b/src/dashboard/Elsa.WorkflowDesigner/Models/ActivityDesignerSettings.cs new file mode 100644 index 000000000..09fe0a954 --- /dev/null +++ b/src/dashboard/Elsa.WorkflowDesigner/Models/ActivityDesignerSettings.cs @@ -0,0 +1,8 @@ +namespace Elsa.WorkflowDesigner.Models +{ + public class ActivityDesignerSettings + { + public object Description { get; set; } + public object Outcomes { get; set; } + } +} \ No newline at end of file diff --git a/src/dashboard/Elsa.WorkflowDesigner/Models/ActivityPropertyDescriptor.cs b/src/dashboard/Elsa.WorkflowDesigner/Models/ActivityPropertyDescriptor.cs new file mode 100644 index 000000000..dbce67b82 --- /dev/null +++ b/src/dashboard/Elsa.WorkflowDesigner/Models/ActivityPropertyDescriptor.cs @@ -0,0 +1,11 @@ +namespace Elsa.WorkflowDesigner.Models +{ + public class ActivityPropertyDescriptor + { + public string Name { get; set; } + public string Type { get; set; } + public string Label { get; set; } + public string Hint { get; set; } + public object Options { get; set; } + } +} \ No newline at end of file diff --git a/src/dashboard/Elsa.WorkflowDesigner/Models/ActivityPropertyTypes.cs b/src/dashboard/Elsa.WorkflowDesigner/Models/ActivityPropertyTypes.cs new file mode 100644 index 000000000..1c3a1058e --- /dev/null +++ b/src/dashboard/Elsa.WorkflowDesigner/Models/ActivityPropertyTypes.cs @@ -0,0 +1,11 @@ +namespace Elsa.WorkflowDesigner.Models +{ + public static class ActivityPropertyTypes + { + public const string Text = "text"; + public const string Boolean = "boolean"; + public const string Expression = "expression"; + public const string List = "list"; + public const string Select = "select"; + } +} \ No newline at end of file diff --git a/src/dashboard/Elsa.WorkflowDesigner/Models/WorkflowExpressionOptions.cs b/src/dashboard/Elsa.WorkflowDesigner/Models/WorkflowExpressionOptions.cs new file mode 100644 index 000000000..e541e6223 --- /dev/null +++ b/src/dashboard/Elsa.WorkflowDesigner/Models/WorkflowExpressionOptions.cs @@ -0,0 +1,7 @@ +namespace Elsa.WorkflowDesigner.Models +{ + public class WorkflowExpressionOptions + { + public bool Multiline { get; set; } + } +} \ No newline at end of file diff --git a/src/dashboard/Elsa.WorkflowDesigner/ViewComponents/WorkflowDesignerViewComponent.cs b/src/dashboard/Elsa.WorkflowDesigner/ViewComponents/WorkflowDesignerViewComponent.cs new file mode 100644 index 000000000..c2f4b67ac --- /dev/null +++ b/src/dashboard/Elsa.WorkflowDesigner/ViewComponents/WorkflowDesignerViewComponent.cs @@ -0,0 +1,32 @@ +using Elsa.WorkflowDesigner.Models; +using Elsa.WorkflowDesigner.ViewModels; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Elsa.WorkflowDesigner.ViewComponents +{ + public class WorkflowDesignerViewComponent : ViewComponent + { + public IViewComponentResult Invoke(ActivityDefinition[] activityDefinitions) + { + var model = new WorkflowDesignerViewComponentModel + { + ActivityDefinitionsJson = GetActivityDefinitionOptions(activityDefinitions) + }; + + return View(model); + } + + private string GetActivityDefinitionOptions(ActivityDefinition[] activityDefinitions) + { + var definitions = activityDefinitions ?? new ActivityDefinition[0]; + var settings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore + }; + return JsonConvert.SerializeObject(definitions, settings); + } + } +} \ No newline at end of file diff --git a/src/dashboard/Elsa.WorkflowDesigner/ViewModels/WorkflowDesignerViewComponentModel.cs b/src/dashboard/Elsa.WorkflowDesigner/ViewModels/WorkflowDesignerViewComponentModel.cs new file mode 100644 index 000000000..d44b897f4 --- /dev/null +++ b/src/dashboard/Elsa.WorkflowDesigner/ViewModels/WorkflowDesignerViewComponentModel.cs @@ -0,0 +1,7 @@ +namespace Elsa.WorkflowDesigner.ViewModels +{ + public class WorkflowDesignerViewComponentModel + { + public string ActivityDefinitionsJson { get; set; } + } +} \ No newline at end of file diff --git a/src/dashboard/Elsa.WorkflowDesigner/Views/Shared/Components/WorkflowDesigner/Default.cshtml b/src/dashboard/Elsa.WorkflowDesigner/Views/Shared/Components/WorkflowDesigner/Default.cshtml new file mode 100644 index 000000000..0136beb44 --- /dev/null +++ b/src/dashboard/Elsa.WorkflowDesigner/Views/Shared/Components/WorkflowDesigner/Default.cshtml @@ -0,0 +1,3 @@ +@model Elsa.WorkflowDesigner.ViewModels.WorkflowDesignerViewComponentModel + + \ No newline at end of file diff --git a/src/dashboard/Elsa.WorkflowDesigner/Views/_ViewImports.cshtml b/src/dashboard/Elsa.WorkflowDesigner/Views/_ViewImports.cshtml new file mode 100644 index 000000000..b66377318 --- /dev/null +++ b/src/dashboard/Elsa.WorkflowDesigner/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Elsa.Dashboard