diff --git a/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequest.cs b/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequest.cs index a0827dab6..73c122406 100644 --- a/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequest.cs +++ b/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Reflection; using System.Text; using System.Threading.Tasks; using Elsa.Activities.Http.Models; @@ -11,6 +12,7 @@ using Elsa.ActivityResults; using Elsa.Attributes; using Elsa.Design; using Elsa.Expressions; +using Elsa.Metadata; using Elsa.Services; using Elsa.Services.Models; using Microsoft.AspNetCore.Http; @@ -26,7 +28,7 @@ namespace Elsa.Activities.Http Description = "Send an HTTP request.", Outcomes = new[] { OutcomeNames.Done } )] - public class SendHttpRequest : Activity + public class SendHttpRequest : Activity, IActivityPropertyOptionsProvider { private readonly HttpClient _httpClient; private readonly IEnumerable _parsers; @@ -90,6 +92,23 @@ namespace Elsa.Activities.Http [ActivityInput(Hint = "Read the content of the response.", SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })] public bool ReadContent { get; set; } + [ActivityInput( + Label = "Response Content Parser", + Hint = "The parser to use to parse the response content.", + SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid }, + UIHint = ActivityInputUIHints.Dropdown, + OptionsProvider = typeof(SendHttpRequest) + )] + public string? ResponseContentParserName { get; set; } + + [ActivityInput( + Label = "Response Content .NET Type", + Hint = "The assembly-qualified .NET type name to deserialize the received content into.", + SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid }, + OptionsProvider = typeof(SendHttpRequest) + )] + public Type? ResponseContentTargetType { get; set; } + /// /// A list of HTTP status codes this activity can handle. /// @@ -123,11 +142,11 @@ namespace Elsa.Activities.Http if (hasContent && ReadContent) { - var formatter = SelectContentParser(contentType); - ResponseContent = await formatter.ReadAsync(response, cancellationToken); + var formatter = SelectContentParser(ResponseContentParserName, contentType); + ResponseContent = await formatter.ReadAsync(this, response, cancellationToken); } - var statusCode = (int) response.StatusCode; + var statusCode = (int)response.StatusCode; var statusOutcome = statusCode.ToString(); var supportedStatusCodes = SupportedStatusCodes; var isSupportedStatusCode = supportedStatusCodes == null || !supportedStatusCodes.Any() || SupportedStatusCodes?.Contains(statusCode) == true; @@ -140,12 +159,24 @@ namespace Elsa.Activities.Http return Outcomes(outcomes); } - private IHttpResponseContentReader SelectContentParser(string? contentType) + private IHttpResponseContentReader SelectContentParser(string? parserName, string? contentType) { - var simpleContentType = contentType?.Split(';').First() ?? ""; - var formatters = _parsers.OrderByDescending(x => x.Priority).ToList(); - - return formatters.FirstOrDefault(x => x.GetSupportsContentType(simpleContentType)) ?? formatters.Last(); + if (string.IsNullOrWhiteSpace(parserName)) + { + var simpleContentType = contentType?.Split(';').First() ?? ""; + var parser = _parsers.OrderByDescending(x => x.Priority).ToList(); + + return parser.FirstOrDefault(x => x.GetSupportsContentType(simpleContentType)) ?? parser.Last(); + } + else + { + var parser = _parsers.FirstOrDefault(x => x.Name == parserName); + + if (parser == null) + throw new InvalidOperationException("The specified parser does not exist"); + + return parser; + } } private HttpRequestMessage CreateRequest() @@ -174,6 +205,17 @@ namespace Elsa.Activities.Http return request; } + + object? IActivityPropertyOptionsProvider.GetOptions(PropertyInfo property) + { + if (property.Name != nameof(ResponseContentParserName)) + return null; + + var items = _parsers.Select(x => new SelectListItem(x.Name, x.Name)).ToList(); + + items.Insert(0, new SelectListItem("Auto Select", "")); + return items; + } private static bool GetMethodSupportsBody(string method) { diff --git a/src/activities/Elsa.Activities.Http/Activities/WriteHttpResponse/WriteHttpResponse.cs b/src/activities/Elsa.Activities.Http/Activities/WriteHttpResponse/WriteHttpResponse.cs index daf4ae7b7..9bf6486c8 100644 --- a/src/activities/Elsa.Activities.Http/Activities/WriteHttpResponse/WriteHttpResponse.cs +++ b/src/activities/Elsa.Activities.Http/Activities/WriteHttpResponse/WriteHttpResponse.cs @@ -12,6 +12,7 @@ using Elsa.Services; using Elsa.Services.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; +using Newtonsoft.Json; // ReSharper disable once CheckNamespace namespace Elsa.Activities.Http @@ -142,7 +143,7 @@ namespace Elsa.Activities.Http return; } - var json = _contentSerializer.Serialize(content); + var json = JsonConvert.SerializeObject(content, DefaultContentSerializer.CreateDefaultJsonSerializationSettings()); await response.WriteAsync(json, cancellationToken); } } diff --git a/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs b/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs index 3c4f8bfc7..65b653a9a 100644 --- a/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs +++ b/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs @@ -43,9 +43,12 @@ namespace Microsoft.Extensions.DependencyInjection .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(sp => sp.GetRequiredService>().Value.HttpEndpointAuthorizationHandlerFactory(sp)) diff --git a/src/activities/Elsa.Activities.Http/Parsers/Response/DefaultHttpResponseContentReader.cs b/src/activities/Elsa.Activities.Http/Parsers/Response/DefaultHttpResponseContentReader.cs deleted file mode 100644 index a3748bcd1..000000000 --- a/src/activities/Elsa.Activities.Http/Parsers/Response/DefaultHttpResponseContentReader.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Elsa.Activities.Http.Services; - -namespace Elsa.Activities.Http.Parsers.Response -{ - public class DefaultHttpResponseContentReader : IHttpResponseContentReader - { - public int Priority => -1; - public bool GetSupportsContentType(string contentType) => true; - public async Task ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsStringAsync(); - } -} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Parsers/Response/ExpandoObjectHttpResponseContentReader.cs b/src/activities/Elsa.Activities.Http/Parsers/Response/ExpandoObjectHttpResponseContentReader.cs new file mode 100644 index 000000000..301e424ae --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Parsers/Response/ExpandoObjectHttpResponseContentReader.cs @@ -0,0 +1,24 @@ +using System; +using System.Dynamic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Http.Services; +using Newtonsoft.Json; + +namespace Elsa.Activities.Http.Parsers.Response +{ + public class ExpandoObjectHttpResponseContentReader : IHttpResponseContentReader + { + public string Name => "Expando Object"; + public int Priority => 0; + public bool GetSupportsContentType(string contentType) => contentType.Contains("/json", StringComparison.OrdinalIgnoreCase); + + public async Task ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken) + { + var json = (await response.Content.ReadAsStringAsync()).Trim(); + + return json.StartsWith('[') ? JsonConvert.DeserializeObject(json)! : JsonConvert.DeserializeObject(json)!; + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Parsers/Response/FileHttpResponseContentReader.cs b/src/activities/Elsa.Activities.Http/Parsers/Response/FileHttpResponseContentReader.cs new file mode 100644 index 000000000..4e9d5182d --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Parsers/Response/FileHttpResponseContentReader.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Http.Services; + +namespace Elsa.Activities.Http.Parsers.Response +{ + public sealed class FileHttpResponseContentReader : IHttpResponseContentReader + { + public string Name => "File"; + public int Priority => 0; + public bool GetSupportsContentType(string contentType) + { + var types = new[] { "audio", "video", "application", "pdf" }; + return types.Any(x => contentType.Contains(x, StringComparison.OrdinalIgnoreCase)); + } + + public async Task ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsByteArrayAsync(); + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Parsers/Response/FileResponseContentReader.cs b/src/activities/Elsa.Activities.Http/Parsers/Response/FileResponseContentReader.cs deleted file mode 100644 index 22b846e5c..000000000 --- a/src/activities/Elsa.Activities.Http/Parsers/Response/FileResponseContentReader.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Elsa.Activities.Http.Services; - -namespace Elsa.Activities.Http.Parsers.Response -{ - public class FileResponseContentReader : IHttpResponseContentReader - { - public virtual int Priority => 5; - public virtual bool GetSupportsContentType(string contentType) - { - var types = new[] { "audio", "video", "application", "pdf" }; - return types.Any(x => contentType.Contains(x, StringComparison.OrdinalIgnoreCase)); - } - - public async Task ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsByteArrayAsync(); - } -} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Parsers/Response/JTokenHttpResponseContentReader.cs b/src/activities/Elsa.Activities.Http/Parsers/Response/JTokenHttpResponseContentReader.cs new file mode 100644 index 000000000..d59822873 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Parsers/Response/JTokenHttpResponseContentReader.cs @@ -0,0 +1,22 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Http.Services; +using Newtonsoft.Json.Linq; + +namespace Elsa.Activities.Http.Parsers.Response +{ + public class JTokenHttpResponseContentReader : IHttpResponseContentReader + { + public string Name => "JToken"; + public int Priority => 0; + public bool GetSupportsContentType(string contentType) => contentType.Contains("/json", StringComparison.OrdinalIgnoreCase); + + public async Task ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken) + { + var json = (await response.Content.ReadAsStringAsync()).Trim(); + return JToken.Parse(json); + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Parsers/Response/JsonHttpResponseContentReader.cs b/src/activities/Elsa.Activities.Http/Parsers/Response/JsonHttpResponseContentReader.cs deleted file mode 100644 index d6eb8b946..000000000 --- a/src/activities/Elsa.Activities.Http/Parsers/Response/JsonHttpResponseContentReader.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Dynamic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Elsa.Activities.Http.Services; -using Newtonsoft.Json; - -namespace Elsa.Activities.Http.Parsers.Response -{ - public class JsonHttpResponseContentReader : IHttpResponseContentReader - { - public int Priority => 10; - public bool GetSupportsContentType(string contentType) => contentType.Contains("/json", StringComparison.OrdinalIgnoreCase); - - public async Task ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken) - { -#if NET - var json = (await response.Content.ReadAsStringAsync(cancellationToken)).Trim(); -#else - var json = (await response.Content.ReadAsStringAsync()).Trim(); -#endif - if(json.StartsWith('[')) - return JsonConvert.DeserializeObject(json)!; - - return JsonConvert.DeserializeObject(json)!; - } - } -} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Parsers/Response/JsonRawHttpResponseContentReader.cs b/src/activities/Elsa.Activities.Http/Parsers/Response/JsonRawHttpResponseContentReader.cs new file mode 100644 index 000000000..6afb64849 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Parsers/Response/JsonRawHttpResponseContentReader.cs @@ -0,0 +1,16 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Http.Services; + +namespace Elsa.Activities.Http.Parsers.Response +{ + public class JsonRawHttpResponseContentReader : IHttpResponseContentReader + { + public string Name => "JSON"; + public int Priority => 100; + public bool GetSupportsContentType(string contentType) => contentType.Contains("/json", StringComparison.OrdinalIgnoreCase); + public async Task ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsStringAsync(); + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Parsers/Response/PlainTextHttpResponseContentReader.cs b/src/activities/Elsa.Activities.Http/Parsers/Response/PlainTextHttpResponseContentReader.cs new file mode 100644 index 000000000..33ee698f1 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Parsers/Response/PlainTextHttpResponseContentReader.cs @@ -0,0 +1,15 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Http.Services; + +namespace Elsa.Activities.Http.Parsers.Response +{ + public class PlainTextHttpResponseContentReader : IHttpResponseContentReader + { + public string Name => "Plain Text"; + public int Priority => -1; + public bool GetSupportsContentType(string contentType) => true; + public async Task ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsStringAsync(); + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Parsers/Response/TypedHttpResponseContentReader.cs b/src/activities/Elsa.Activities.Http/Parsers/Response/TypedHttpResponseContentReader.cs new file mode 100644 index 000000000..0ff554285 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Parsers/Response/TypedHttpResponseContentReader.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Http.Services; +using Newtonsoft.Json; + +namespace Elsa.Activities.Http.Parsers.Response +{ + public class TypedHttpResponseContentReader : IHttpResponseContentReader + { + public string Name => ".NET Type"; + public int Priority => 0; + public bool GetSupportsContentType(string contentType) => GetIsJsonContentType(contentType); + + public async Task ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken) + { + var contentType = response.Content.Headers.ContentType.MediaType; + + if (GetIsJsonContentType(contentType)) + { + var json = (await response.Content.ReadAsStringAsync()).Trim(); + var targetType = activity.ResponseContentTargetType ?? typeof(ExpandoObject); + return JsonConvert.DeserializeObject(json, targetType)!; + } + + if (GetIsXmlContentType(contentType)) + { + // TODO: parse XML. + throw new NotImplementedException(); + } + + throw new NotSupportedException(); + } + + private bool GetIsJsonContentType(string contentType) => GetIsContentType(contentType, "json"); + private bool GetIsXmlContentType(string contentType) => GetIsContentType(contentType, "xml"); + private bool GetIsContentType(string contentType, string match) => contentType.Contains($"/{match}", StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Services/IHttpResponseContentReader.cs b/src/activities/Elsa.Activities.Http/Services/IHttpResponseContentReader.cs index 236b6f15e..d53f9102e 100644 --- a/src/activities/Elsa.Activities.Http/Services/IHttpResponseContentReader.cs +++ b/src/activities/Elsa.Activities.Http/Services/IHttpResponseContentReader.cs @@ -6,8 +6,9 @@ namespace Elsa.Activities.Http.Services { public interface IHttpResponseContentReader { + string Name { get; } int Priority { get; } bool GetSupportsContentType(string contentType); - Task ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken); + Task ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Metadata/IActivityPropertyOptionsProvider.cs b/src/core/Elsa.Abstractions/Metadata/IActivityPropertyOptionsProvider.cs index 23443b610..976bcbbc9 100644 --- a/src/core/Elsa.Abstractions/Metadata/IActivityPropertyOptionsProvider.cs +++ b/src/core/Elsa.Abstractions/Metadata/IActivityPropertyOptionsProvider.cs @@ -4,6 +4,6 @@ namespace Elsa.Metadata { public interface IActivityPropertyOptionsProvider { - object GetOptions(PropertyInfo property); + object? GetOptions(PropertyInfo property); } } \ No newline at end of file diff --git a/src/designer/elsa-workflows-studio/src/components.d.ts b/src/designer/elsa-workflows-studio/src/components.d.ts index e1261fbfc..c3933a629 100644 --- a/src/designer/elsa-workflows-studio/src/components.d.ts +++ b/src/designer/elsa-workflows-studio/src/components.d.ts @@ -811,6 +811,8 @@ declare namespace LocalJSX { "propertyModel"?: ActivityDefinitionProperty; } interface ElsaModalDialog { + "onHidden"?: (event: CustomEvent) => void; + "onShown"?: (event: CustomEvent) => void; } interface ElsaMonaco { "editorHeight"?: string; diff --git a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx index e4f76c4ab..181e2d247 100644 --- a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx +++ b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx @@ -150,13 +150,13 @@ export class ElsaWorkflowDesigner { @Method() async showActivityEditor(activity: ActivityModel, animate: boolean) { - this.showActivityEditorInternal(activity, animate); + await this.showActivityEditorInternal(activity, animate); } async copyActivitiesToClipboard() { this.checkClipboardPermissions(); await navigator.clipboard.writeText(JSON.stringify(this.selectedActivities)); - eventBus.emit(EventTypes.ClipboardCopied, this); + await eventBus.emit(EventTypes.ClipboardCopied, this); } async pasteActivitiesFromClipboard() { @@ -167,7 +167,7 @@ export class ElsaWorkflowDesigner { await navigator.clipboard.readText().then(data => { copiedActivities = JSON.parse(data); }); - this.addActivitiesFromClipboard(copiedActivities) + await this.addActivitiesFromClipboard(copiedActivities) } async addActivitiesFromClipboard(copiedActivities: Array) { @@ -305,12 +305,8 @@ export class ElsaWorkflowDesigner { }; } - showActivityEditorInternal(activity: ActivityModel, animate: boolean) { - eventBus.emit(EventTypes.ShowActivityEditor, this, activity, animate); - } - - handleEditActivity(activity: ActivityModel) { - this.showActivityEditorInternal(activity, true); + async showActivityEditorInternal(activity: ActivityModel, animate: boolean) { + await eventBus.emit(EventTypes.ActivityEditor.Show, this, activity, animate); } updateWorkflowModel(model: WorkflowModel, emitEvent: boolean = true) { @@ -618,7 +614,7 @@ export class ElsaWorkflowDesigner { const activityDescriptor = args as ActivityDescriptor; const activityModel = this.newActivity(activityDescriptor); this.addingActivity = true; - this.showActivityEditorInternal(activityModel, false); + await this.showActivityEditorInternal(activityModel, false); }; onUpdateActivity = args => { @@ -643,7 +639,7 @@ export class ElsaWorkflowDesigner { this.selectedActivities = {}; activityModel.outcomes[0] = this.parentActivityOutcome; this.selectedActivities[activityModel.activityId] = activityModel; - this.pasteActivitiesFromClipboard(); + await this.pasteActivitiesFromClipboard(); }; onCopyPasteActivityEnabled = () => { diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal.tsx index 8fcb0ec0f..d1c7acde1 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal.tsx @@ -1,7 +1,13 @@ import {Component, Event, h, Host, Prop, State} from '@stencil/core'; import {eventBus, propertyDisplayManager} from '../../../../services'; import state from '../../../../utils/store'; -import {ActivityDescriptor, ActivityModel, ActivityPropertyDescriptor, EventTypes, WorkflowStorageDescriptor} from "../../../../models"; +import { + ActivityDescriptor, + ActivityModel, + ActivityPropertyDescriptor, + EventTypes, + WorkflowStorageDescriptor +} from "../../../../models"; import {checkBox, FormContext, section, selectField, SelectOption, textArea, textInput} from "../../../../utils/forms"; import {i18n} from "i18next"; import {loadTranslations} from "../../../i18n/i18n-loader"; @@ -21,6 +27,11 @@ export interface ActivityEditorRenderProps { selectedTabName?: string; } +export interface ActivityEditorAppearingEventArgs { + activityDescriptor: ActivityDescriptor; + activityModel: ActivityModel; +} + @Component({ tag: 'elsa-activity-editor-modal', shadow: false, @@ -33,7 +44,6 @@ export class ElsaActivityEditorModal { @State() renderProps: ActivityEditorRenderProps = {}; i18next: i18n; dialog: HTMLElsaModalDialogElement; - form: HTMLFormElement; formContext: FormContext; // Force a new key every time we show the editor to make sure Stencil creates new components. @@ -41,11 +51,11 @@ export class ElsaActivityEditorModal { timestamp: Date = new Date(); connectedCallback() { - eventBus.on(EventTypes.ShowActivityEditor, this.onShowActivityEditor); + eventBus.on(EventTypes.ActivityEditor.Show, this.onShowActivityEditor); } disconnectedCallback() { - eventBus.detach(EventTypes.ShowActivityEditor, this.onShowActivityEditor); + eventBus.detach(EventTypes.ActivityEditor.Show, this.onShowActivityEditor); } async componentWillLoad() { @@ -64,10 +74,26 @@ export class ElsaActivityEditorModal { } componentWillRender() { - const activityDescriptor: ActivityDescriptor = this.activityDescriptor || {displayName: '', type: '', outcomes: [], category: '', traits: 0, browsable: false, inputProperties: [], outputProperties: [], description: ''}; + const activityDescriptor: ActivityDescriptor = this.activityDescriptor || { + displayName: '', + type: '', + outcomes: [], + category: '', + traits: 0, + browsable: false, + inputProperties: [], + outputProperties: [], + description: '' + }; const propertyCategories = activityDescriptor.inputProperties.filter(x => x.category).map(x => x.category).distinct(); const defaultProperties = activityDescriptor.inputProperties.filter(x => !x.category || x.category.length == 0); - const activityModel: ActivityModel = this.activityModel || {type: '', activityId: '', outcomes: [], properties: [], propertyStorageProviders: {}}; + const activityModel: ActivityModel = this.activityModel || { + type: '', + activityId: '', + outcomes: [], + properties: [], + propertyStorageProviders: {} + }; const t = this.t; let tabs: Array = []; @@ -106,8 +132,6 @@ export class ElsaActivityEditorModal { selectedTabName: this.renderProps.selectedTabName }; - eventBus.emit(EventTypes.ActivityEditorDisplaying, this, this.renderProps); - let selectedTabName = this.renderProps.selectedTabName tabs = this.renderProps.tabs; @@ -119,7 +143,7 @@ export class ElsaActivityEditorModal { } async onCancelClick() { - await this.dialog.hide(true); + await this.hide(true); } onSubmit = async (e: Event) => { @@ -127,8 +151,8 @@ export class ElsaActivityEditorModal { const form: any = e.target; const formData = new FormData(form); this.updateActivity(formData); - eventBus.emit(EventTypes.UpdateActivity, this, this.activityModel); - await this.dialog.hide(true); + await eventBus.emit(EventTypes.UpdateActivity, this, this.activityModel); + await this.hide(true); }; onTabClick = (e: Event, tab: TabModel) => { @@ -143,15 +167,31 @@ export class ElsaActivityEditorModal { this.formContext = new FormContext(this.activityModel, newValue => this.activityModel = newValue); this.timestamp = new Date(); this.renderProps = {}; - await this.dialog.show(animate); + await this.show(animate); }; + show = async (animate: boolean) => await this.dialog.show(animate); + hide = async (animate: boolean) => await this.dialog.hide(animate); + onKeyDown = async (event: KeyboardEvent) => { if (event.ctrlKey && event.key === 'Enter') { (this.dialog.querySelector('button[type="submit"]') as HTMLButtonElement).click(); } } + onDialogShown = async () => { + const args: ActivityEditorAppearingEventArgs = { + activityModel: this.activityModel, + activityDescriptor: this.activityDescriptor + }; + + await eventBus.emit(EventTypes.ActivityEditor.Appearing, this, args); + }; + + onDialogHidden = async () => { + await eventBus.emit(EventTypes.ActivityEditor.Disappearing); + }; + render() { const renderProps = this.renderProps; const activityDescriptor: ActivityDescriptor = renderProps.activityDescriptor; @@ -163,9 +203,10 @@ export class ElsaActivityEditorModal { return ( - this.dialog = el}> + this.dialog = el} onShown={this.onDialogShown} onHidden={this.onDialogHidden}>
-
this.onSubmit(e)} ref={el => this.form = el} key={this.timestamp.getTime().toString()} onKeyDown={this.onKeyDown}> + this.onSubmit(e)} key={this.timestamp.getTime().toString()} onKeyDown={this.onKeyDown} + class='activity-editor-form'>
@@ -183,7 +224,8 @@ export class ElsaActivityEditorModal { {tabs.map(tab => { const isSelected = tab.tabName === selectedTabName; const cssClass = isSelected ? selectedClass : inactiveClass; - return this.onTabClick(e, tab)} class={`${cssClass} elsa-whitespace-nowrap elsa-py-4 elsa-px-1 elsa-border-b-2 elsa-font-medium elsa-text-sm`}>{tab.tabName}; + return this.onTabClick(e, tab)} + class={`${cssClass} elsa-whitespace-nowrap elsa-py-4 elsa-px-1 elsa-border-b-2 elsa-font-medium elsa-text-sm`}>{tab.tabName}; })}
@@ -229,7 +271,10 @@ export class ElsaActivityEditorModal { renderStorageTab(activityModel: ActivityModel, activityDescriptor: ActivityDescriptor) { const formContext = this.formContext; const t = this.t; - let storageDescriptorOptions: Array = this.workflowStorageDescriptors.map(x => ({value: x.name, text: x.displayName})); + let storageDescriptorOptions: Array = this.workflowStorageDescriptors.map(x => ({ + value: x.name, + text: x.displayName + })); let outputProperties = activityDescriptor.outputProperties.filter(x => !x.disableWorkflowProviderSelection); let inputProperties = activityDescriptor.inputProperties.filter(x => !x.disableWorkflowProviderSelection); @@ -303,7 +348,8 @@ export class ElsaActivityEditorModal { renderPropertyEditor(activity: ActivityModel, property: ActivityPropertyDescriptor) { const key = `activity-property-input:${activity.activityId}:${property.name}`; const display = propertyDisplayManager.display(activity, property); - return ; + const id = `${property.name}Control`; + return ; } getHiddenClass(tab: string) { diff --git a/src/designer/elsa-workflows-studio/src/components/shared/elsa-modal-dialog/elsa-modal-dialog.tsx b/src/designer/elsa-workflows-studio/src/components/shared/elsa-modal-dialog/elsa-modal-dialog.tsx index 7aeab6a7c..271ffc7d7 100644 --- a/src/designer/elsa-workflows-studio/src/components/shared/elsa-modal-dialog/elsa-modal-dialog.tsx +++ b/src/designer/elsa-workflows-studio/src/components/shared/elsa-modal-dialog/elsa-modal-dialog.tsx @@ -1,4 +1,4 @@ -import {Component, Host, h, State, Listen, Method} from '@stencil/core'; +import {Component, Host, h, State, Listen, Method, Event, EventEmitter} from '@stencil/core'; import {enter, leave} from 'el-transition' import {eventBus} from "../../../services"; import {EventTypes} from "../../../models"; @@ -8,7 +8,8 @@ import {EventTypes} from "../../../models"; shadow: false, }) export class ElsaModalDialog { - + @Event() shown: EventEmitter; + @Event() hidden: EventEmitter; @State() isVisible: boolean; overlay: HTMLElement modal: HTMLElement @@ -24,7 +25,7 @@ export class ElsaModalDialog { @Method() async hide(animate: boolean) { - eventBus.emit(EventTypes.HideModalDialog); + await eventBus.emit(EventTypes.HideModalDialog); this.hideInternal(animate); } @@ -37,7 +38,7 @@ export class ElsaModalDialog { } enter(this.overlay); - enter(this.modal); + enter(this.modal).then(this.shown.emit); } hideInternal(animate: boolean) { @@ -46,7 +47,10 @@ export class ElsaModalDialog { } leave(this.overlay); - leave(this.modal).then(() => this.isVisible = false); + leave(this.modal).then(() => { + this.isVisible = false; + this.hidden.emit(); + }); } @Listen('keydown', {target: 'window'}) @@ -60,7 +64,8 @@ export class ElsaModalDialog { return (
-
+
this.overlay = el} onClick={() => this.hide(true)} data-transition-enter="elsa-ease-out elsa-duration-300" data-transition-enter-start="elsa-opacity-0" diff --git a/src/designer/elsa-workflows-studio/src/index.html b/src/designer/elsa-workflows-studio/src/index.html index 44fa81ad8..481c5f2c6 100644 --- a/src/designer/elsa-workflows-studio/src/index.html +++ b/src/designer/elsa-workflows-studio/src/index.html @@ -20,7 +20,7 @@ - + diff --git a/src/designer/elsa-workflows-studio/src/models/events.ts b/src/designer/elsa-workflows-studio/src/models/events.ts index 2a5989e7f..0fe5f07f5 100644 --- a/src/designer/elsa-workflows-studio/src/models/events.ts +++ b/src/designer/elsa-workflows-studio/src/models/events.ts @@ -2,11 +2,14 @@ import {ActivityDescriptor} from "./domain"; export const EventTypes = { + ActivityEditor: { + Show: 'show-activity-editor', + Appearing: 'activity-editor-appearing', + Disappearing: 'activity-editor-disappearing' + }, ShowActivityPicker: 'show-activity-picker', ShowWorkflowSettings: 'show-workflow-settings', ActivityPicked: 'activity-picked', - ShowActivityEditor: 'show-activity-editor', - ActivityEditorDisplaying: 'activity-editor-displaying', UpdateActivity: 'update-activity', UpdateWorkflowSettings: 'update-workflow-settings', WorkflowModelChanged: 'workflow-model-changed', @@ -67,4 +70,4 @@ export interface ConfigureWorkflowRegistryColumnsContext { export interface ConfigureWorkflowRegistryUpdatingContext { params: any; -} \ No newline at end of file +} diff --git a/src/designer/elsa-workflows-studio/src/plugins/send-http-request-plugin.tsx b/src/designer/elsa-workflows-studio/src/plugins/send-http-request-plugin.tsx deleted file mode 100644 index 8c1c7658b..000000000 --- a/src/designer/elsa-workflows-studio/src/plugins/send-http-request-plugin.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import {eventBus, ElsaPlugin} from "../services"; -import {ActivityDesignDisplayContext, EventTypes, SyntaxNames} from "../models"; -import {h} from "@stencil/core"; -import {parseJson} from "../utils/utils"; - -export class SendHttpRequestPlugin implements ElsaPlugin { - constructor() { - eventBus.on(EventTypes.ActivityDesignDisplaying, this.onActivityDesignDisplaying); - } - - onActivityDesignDisplaying(context: ActivityDesignDisplayContext) { - const activityModel = context.activityModel; - - if (activityModel.type !== 'SendHttpRequest') - return; - - const props = activityModel.properties || []; - const syntax = SyntaxNames.Json; - const supportedStatusCodes = props.find(x => x.name == 'SupportedStatusCodes') || {expressions: {'Json': '[]'}, syntax: syntax}; - const expression = supportedStatusCodes.expressions[syntax] || []; - let outcomes = !!expression['$values'] ? expression['$values'] : Array.isArray(expression) ? expression : parseJson(expression) || []; - - outcomes = [...outcomes, 'Done', 'Unsupported Status Code'] - - context.outcomes = outcomes; - } -} diff --git a/src/designer/elsa-workflows-studio/src/plugins/send-http-request/send-http-request-plugin.tsx b/src/designer/elsa-workflows-studio/src/plugins/send-http-request/send-http-request-plugin.tsx new file mode 100644 index 000000000..46a806434 --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/plugins/send-http-request/send-http-request-plugin.tsx @@ -0,0 +1,37 @@ +import {eventBus, ElsaPlugin} from "../../services"; +import {EventTypes} from "../../models"; +import {h} from "@stencil/core"; +import {ActivityEditorAppearingEventArgs} from "../../components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal"; + +export class SendHttpRequestPlugin implements ElsaPlugin { + constructor() { + eventBus.on(EventTypes.ActivityEditor.Appearing, this.onActivityEditorAppearing); + eventBus.on(EventTypes.ActivityEditor.Disappearing, this.onActivityEditorDisappearing); + + } + + onActivityEditorAppearing = (args: ActivityEditorAppearingEventArgs) => { + if (args.activityDescriptor.type != 'SendHttpRequest') + return; + + document.querySelector('#ReadContent').addEventListener('change', this.updateUI); + document.querySelector('#ResponseContentParserName').addEventListener('change', this.updateUI); + this.updateUI(); + }; + + onActivityEditorDisappearing = () => { + document.querySelector('#ReadContent').removeEventListener('change', this.updateUI); + document.querySelector('#ResponseContentParserName').removeEventListener('change', this.updateUI); + }; + + updateUI = () => { + const readContentCheckbox = document.querySelector('#ReadContent') as HTMLInputElement; + const parserList = document.querySelector('#ResponseContentParserName') as HTMLSelectElement; + const responseContentParserListControl = document.querySelector('#ResponseContentParserNameControl') as HTMLElsaControlElement; + const responseContentTargetTypeControl = document.querySelector('#ResponseContentTargetTypeControl') as HTMLElsaControlElement; + const selectedParserName = parserList.value; + + responseContentParserListControl.classList.toggle('hidden', !readContentCheckbox.checked) + responseContentTargetTypeControl.classList.toggle('hidden', (!readContentCheckbox.checked || selectedParserName != '.NET Type')); + }; +} diff --git a/src/designer/elsa-workflows-studio/src/services/plugin-manager.ts b/src/designer/elsa-workflows-studio/src/services/plugin-manager.ts index 0debbfc0c..37400e0b9 100644 --- a/src/designer/elsa-workflows-studio/src/services/plugin-manager.ts +++ b/src/designer/elsa-workflows-studio/src/services/plugin-manager.ts @@ -16,7 +16,7 @@ import {SignalReceivedPlugin} from "../plugins/signal-received-plugin"; import {SendSignalPlugin} from "../plugins/send-signal-plugin"; import {UserTaskPlugin} from "../plugins/user-task-plugin"; import {StatePlugin} from "../plugins/state-plugin"; -import {SendHttpRequestPlugin} from "../plugins/send-http-request-plugin"; +import {SendHttpRequestPlugin} from "../plugins/send-http-request/send-http-request-plugin"; import {RunWorkflowPlugin} from "../plugins/run-workflow-plugin"; import {ElsaStudio} from "../models"; diff --git a/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScript.cs b/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScript.cs index aed7aa7b5..b06b06028 100644 --- a/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScript.cs +++ b/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScript.cs @@ -83,12 +83,17 @@ namespace Elsa.Activities.JavaScript return Task.CompletedTask; } - object IActivityPropertyOptionsProvider.GetOptions(PropertyInfo property) => - new + object? IActivityPropertyOptionsProvider.GetOptions(PropertyInfo property) + { + if (property.Name != nameof(Script)) + return null; + + return new { EditorHeight = "Large", Context = nameof(RunJavaScript), Syntax = JavaScriptExpressionHandler.SyntaxName }; + } } } \ No newline at end of file