From e1da5f5f1925fcef5d10a99cd2368b326ff58baf Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 22 Sep 2019 00:02:04 +0200 Subject: [PATCH] Define activities (#78) * Annotate activities with design-time attributes --- Samples.sln | 7 ++ .../Activities/ReadLine.cs | 14 ++- .../Activities/WriteLine.cs | 11 ++ .../Activities/SendEmail.cs | 14 ++- .../Activities/HttpRequestAction.cs | 63 ++++++++++-- .../Activities/HttpRequestEvent.cs | 48 ++++++--- .../Activities/HttpResponseAction.cs | 72 +++++++++++-- .../Elsa.Activities.Http.csproj | 1 + .../Activities/ReceiveMassTransitMessage.cs | 14 ++- .../Activities/SendMassTransitMessage.cs | 11 +- .../Activities/CronEvent.cs | 18 +++- .../Activities/InstantEvent.cs | 6 ++ .../Activities/TimerEvent.cs | 6 ++ .../Activities/UserTask.cs | 13 ++- .../Elsa.Activities.UserTask.csproj | 1 + .../Attributes/ActivityDefinitionAttribute.cs | 13 +++ .../ActivityDefinitionDesignerAttribute.cs | 11 ++ .../Attributes/ActivityPropertyAttribute.cs | 13 +++ .../ActivityPropertyOptionsAttribute.cs | 10 ++ .../Attributes/ExpressionOptionsAttribute.cs | 15 +++ .../Attributes/SelectOptionsAttribute.cs | 66 ++++++++++++ .../Activities/ControlFlow/ForEach.cs | 12 ++- .../Elsa.Core/Activities/ControlFlow/Fork.cs | 8 +- .../Activities/ControlFlow/IfElse.cs | 23 ++++- .../Elsa.Core/Activities/ControlFlow/Join.cs | 35 +++++-- .../Activities/ControlFlow/Switch.cs | 21 +++- .../Activities/Primitives/SetVariable.cs | 25 ++++- .../Activities/Workflows/Correlate.cs | 14 ++- .../Elsa.Core/Activities/Workflows/Finish.cs | 6 ++ .../Activities/Workflows/Signaled.cs | 6 ++ .../Elsa.Core/Activities/Workflows/Start.cs | 7 +- .../Elsa.Core/Activities/Workflows/Trigger.cs | 12 ++- .../Activities/Workflows/TriggerWorkflow.cs | 10 ++ .../Activities/SampleActivity.cs | 15 +++ .../Elsa.Dashboard.Web.csproj | 6 ++ src/dashboard/Elsa.Dashboard.Web/Startup.cs | 17 ++- .../Elsa.Dashboard.Web/appsettings.json | 28 ++++- .../WorkflowDefinitionController.cs | 13 ++- .../ViewModels/WorkflowDefinitionEditModel.cs | 3 + .../Shared/WorkflowDefinitionEditor.cshtml | 2 +- .../Areas/Elsa/Views/Shared/_Layout.cshtml | 2 +- .../Areas/Elsa/Views/_ViewImports.cshtml | 1 + .../Elsa.Dashboard/Elsa.Dashboard.csproj | 20 ++-- .../ActivityDefinitionListExtensions.cs | 33 ++++++ .../Options/ActivityDefinitionList.cs | 29 ++++++ .../Options/ElsaDashboardOptions.cs | 7 ++ .../argon-dashboard/assets/img/brand/blue.png | Bin 7883 -> 0 bytes .../assets/img/brand/favicon-16x16.png | Bin 0 -> 537 bytes .../assets/img/brand/favicon.ico | Bin 0 -> 15406 bytes .../assets/img/brand/favicon.png | Bin 4976 -> 1073 bytes .../assets/img/brand/white.png | Bin 7714 -> 0 bytes .../ActivityDescriber.cs | 97 ++++++++++++++++++ .../Elsa.WorkflowDesigner.csproj | 25 +++++ .../Models/ActivityDefinition.cs | 17 +++ .../Models/ActivityDesignerSettings.cs | 8 ++ .../Models/ActivityPropertyDescriptor.cs | 11 ++ .../Models/ActivityPropertyTypes.cs | 11 ++ .../Models/WorkflowExpressionOptions.cs | 7 ++ .../WorkflowDesignerViewComponent.cs | 32 ++++++ .../WorkflowDesignerViewComponentModel.cs | 7 ++ .../WorkflowDesigner/Default.cshtml | 3 + .../Views/_ViewImports.cshtml | 2 + 62 files changed, 917 insertions(+), 85 deletions(-) create mode 100644 src/core/Elsa.Abstractions/Attributes/ActivityDefinitionAttribute.cs create mode 100644 src/core/Elsa.Abstractions/Attributes/ActivityDefinitionDesignerAttribute.cs create mode 100644 src/core/Elsa.Abstractions/Attributes/ActivityPropertyAttribute.cs create mode 100644 src/core/Elsa.Abstractions/Attributes/ActivityPropertyOptionsAttribute.cs create mode 100644 src/core/Elsa.Abstractions/Attributes/ExpressionOptionsAttribute.cs create mode 100644 src/core/Elsa.Abstractions/Attributes/SelectOptionsAttribute.cs create mode 100644 src/dashboard/Elsa.Dashboard.Web/Activities/SampleActivity.cs create mode 100644 src/dashboard/Elsa.Dashboard/Extensions/ActivityDefinitionListExtensions.cs create mode 100644 src/dashboard/Elsa.Dashboard/Options/ActivityDefinitionList.cs delete mode 100644 src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/blue.png create mode 100644 src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/favicon-16x16.png create mode 100644 src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/favicon.ico delete mode 100644 src/dashboard/Elsa.Dashboard/Theme/argon-dashboard/assets/img/brand/white.png create mode 100644 src/dashboard/Elsa.WorkflowDesigner/ActivityDescriber.cs create mode 100644 src/dashboard/Elsa.WorkflowDesigner/Elsa.WorkflowDesigner.csproj create mode 100644 src/dashboard/Elsa.WorkflowDesigner/Models/ActivityDefinition.cs create mode 100644 src/dashboard/Elsa.WorkflowDesigner/Models/ActivityDesignerSettings.cs create mode 100644 src/dashboard/Elsa.WorkflowDesigner/Models/ActivityPropertyDescriptor.cs create mode 100644 src/dashboard/Elsa.WorkflowDesigner/Models/ActivityPropertyTypes.cs create mode 100644 src/dashboard/Elsa.WorkflowDesigner/Models/WorkflowExpressionOptions.cs create mode 100644 src/dashboard/Elsa.WorkflowDesigner/ViewComponents/WorkflowDesignerViewComponent.cs create mode 100644 src/dashboard/Elsa.WorkflowDesigner/ViewModels/WorkflowDesignerViewComponentModel.cs create mode 100644 src/dashboard/Elsa.WorkflowDesigner/Views/Shared/Components/WorkflowDesigner/Default.cshtml create mode 100644 src/dashboard/Elsa.WorkflowDesigner/Views/_ViewImports.cshtml 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 88f515f1dd49975cb6fe95655b9db81e2d58a41e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7883 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 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 0000000000000000000000000000000000000000..8d6de2988db5f91f37272b8432145f7214805ffb GIT binary patch literal 537 zcmV+!0_OdRP)f~(*-x^a445I>drQk-P7Vc{kv?*~HiQ}gbqgA49f{CDS&_Xwn zBZw5J;9E5z6FFw%I5Xb4ckZ1d=hzIBB$Rd*2cGBrpC8ZLA)Gj)^Dny?v0(xYaN=#^ znK&gl_?*xE>B+taAF4yBY&@BN6J+E*1MrZ5ioaUS0S@4W&-KSo`|jEjE-n$Q!g&j# z{*xK~(dPV}2jh-_NWfS;Hch~zzY6{(;=s&A@BLvCH7mrHPD}eVs3@nz{T_}=v|?gt38LD7Yn(-gGy4yze!=_GdS3mb#B6d!jKHk`+CueM+fO3xP*$Zq32AUe~jl|r2n$YWyqO7Q}w$3 b>vui^A7|!0=pa=E00000NkvXXu0mjfwR-y8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ce861ecff24370614944044fd50d892fb4dc707c GIT binary patch literal 15406 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 literal 4976 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 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