BREAKING: Refactor SendHttpRequest content parser APIs

Allows user to select what content parser to use to handle response of requests made by SendHttpRequest activity.
This commit is contained in:
Sipke Schoorstra 2021-09-18 11:39:45 +02:00
parent 2c9b7eb17f
commit 30ec4b20b5
24 changed files with 338 additions and 147 deletions

View file

@ -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<IHttpResponseContentReader> _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; }
/// <summary>
/// A list of HTTP status codes this activity can handle.
/// </summary>
@ -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)
{

View file

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

View file

@ -43,9 +43,12 @@ namespace Microsoft.Extensions.DependencyInjection
.AddSingleton<IHttpRequestBodyParser, DefaultHttpRequestBodyParser>()
.AddSingleton<IHttpRequestBodyParser, JsonHttpRequestBodyParser>()
.AddSingleton<IHttpRequestBodyParser, FormHttpRequestBodyParser>()
.AddSingleton<IHttpResponseContentReader, DefaultHttpResponseContentReader>()
.AddSingleton<IHttpResponseContentReader, JsonHttpResponseContentReader>()
.AddSingleton<IHttpResponseContentReader, FileResponseContentReader>()
.AddSingleton<IHttpResponseContentReader, PlainTextHttpResponseContentReader>()
.AddSingleton<IHttpResponseContentReader, JsonRawHttpResponseContentReader>()
.AddSingleton<IHttpResponseContentReader, TypedHttpResponseContentReader>()
.AddSingleton<IHttpResponseContentReader, ExpandoObjectHttpResponseContentReader>()
.AddSingleton<IHttpResponseContentReader, JTokenHttpResponseContentReader>()
.AddSingleton<IHttpResponseContentReader, FileHttpResponseContentReader>()
.AddSingleton<IActionContextAccessor, ActionContextAccessor>()
.AddSingleton<IAbsoluteUrlProvider, DefaultAbsoluteUrlProvider>()
.AddSingleton(sp => sp.GetRequiredService<IOptions<HttpActivityOptions>>().Value.HttpEndpointAuthorizationHandlerFactory(sp))

View file

@ -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<object> ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsStringAsync();
}
}

View file

@ -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<object> ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken)
{
var json = (await response.Content.ReadAsStringAsync()).Trim();
return json.StartsWith('[') ? JsonConvert.DeserializeObject<ExpandoObject[]>(json)! : JsonConvert.DeserializeObject<ExpandoObject>(json)!;
}
}
}

View file

@ -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<object> ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsByteArrayAsync();
}
}

View file

@ -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<object> ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsByteArrayAsync();
}
}

View file

@ -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<object> ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken)
{
var json = (await response.Content.ReadAsStringAsync()).Trim();
return JToken.Parse(json);
}
}
}

View file

@ -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<object> 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<ExpandoObject[]>(json)!;
return JsonConvert.DeserializeObject<ExpandoObject>(json)!;
}
}
}

View file

@ -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<object> ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsStringAsync();
}
}

View file

@ -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<object> ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsStringAsync();
}
}

View file

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

View file

@ -6,8 +6,9 @@ namespace Elsa.Activities.Http.Services
{
public interface IHttpResponseContentReader
{
string Name { get; }
int Priority { get; }
bool GetSupportsContentType(string contentType);
Task<object> ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken);
Task<object> ReadAsync(SendHttpRequest activity, HttpResponseMessage response, CancellationToken cancellationToken);
}
}

View file

@ -4,6 +4,6 @@ namespace Elsa.Metadata
{
public interface IActivityPropertyOptionsProvider
{
object GetOptions(PropertyInfo property);
object? GetOptions(PropertyInfo property);
}
}

View file

@ -811,6 +811,8 @@ declare namespace LocalJSX {
"propertyModel"?: ActivityDefinitionProperty;
}
interface ElsaModalDialog {
"onHidden"?: (event: CustomEvent<any>) => void;
"onShown"?: (event: CustomEvent<any>) => void;
}
interface ElsaMonaco {
"editorHeight"?: string;

View file

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

View file

@ -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<TabModel> = [];
@ -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 (
<Host class="elsa-block">
<elsa-modal-dialog ref={el => this.dialog = el}>
<elsa-modal-dialog ref={el => this.dialog = el} onShown={this.onDialogShown} onHidden={this.onDialogHidden}>
<div slot="content" class="elsa-py-8 elsa-pb-0">
<form onSubmit={e => this.onSubmit(e)} ref={el => this.form = el} key={this.timestamp.getTime().toString()} onKeyDown={this.onKeyDown}>
<form onSubmit={e => this.onSubmit(e)} key={this.timestamp.getTime().toString()} onKeyDown={this.onKeyDown}
class='activity-editor-form'>
<div class="elsa-flex elsa-px-8">
<div class="elsa-space-y-8 elsa-divide-y elsa-divide-gray-200 elsa-w-full">
<div>
@ -183,7 +224,8 @@ export class ElsaActivityEditorModal {
{tabs.map(tab => {
const isSelected = tab.tabName === selectedTabName;
const cssClass = isSelected ? selectedClass : inactiveClass;
return <a href="#" onClick={e => 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}</a>;
return <a href="#" onClick={e => 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}</a>;
})}
</nav>
</div>
@ -229,7 +271,10 @@ export class ElsaActivityEditorModal {
renderStorageTab(activityModel: ActivityModel, activityDescriptor: ActivityDescriptor) {
const formContext = this.formContext;
const t = this.t;
let storageDescriptorOptions: Array<SelectOption> = this.workflowStorageDescriptors.map(x => ({value: x.name, text: x.displayName}));
let storageDescriptorOptions: Array<SelectOption> = 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 <elsa-control key={key} class="sm:elsa-col-span-6" content={display}/>;
const id = `${property.name}Control`;
return <elsa-control key={key} id={id} class="sm:elsa-col-span-6" content={display}/>;
}
getHiddenClass(tab: string) {

View file

@ -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 (
<Host class={{'hidden': !this.isVisible, 'elsa-block': true}}>
<div class="elsa-fixed elsa-z-10 elsa-inset-0 elsa-overflow-y-auto">
<div class="elsa-flex elsa-items-end elsa-justify-center elsa-min-h-screen elsa-pt-4 elsa-px-4 elsa-pb-20 elsa-text-center sm:elsa-block sm:elsa-p-0">
<div
class="elsa-flex elsa-items-end elsa-justify-center elsa-min-h-screen elsa-pt-4 elsa-px-4 elsa-pb-20 elsa-text-center sm:elsa-block sm:elsa-p-0">
<div ref={el => this.overlay = el}
onClick={() => this.hide(true)}
data-transition-enter="elsa-ease-out elsa-duration-300" data-transition-enter-start="elsa-opacity-0"

View file

@ -20,7 +20,7 @@
<!-- Instead of using the full dashboard component, you can display lower-level components instead-->
<!--<elsa-workflow-instance-list-screen></elsa-workflow-instance-list-screen>-->
<!--<elsa-workflow-definition-editor-screen></elsa-workflow-definition-editor-screen>-->
<!--<elsa-workflow-definition-editor-screen workflow-definition-id="{workflowDefinitionId}"></elsa-workflow-definition-editor-screen>-->
</elsa-studio-root>
<!-- Keep this to avoid Stencil from stripping classes added by dagre-d3 and dynamically computed classes <!-->

View file

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

View file

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

View file

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

View file

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

View file

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