Implement outcomes for SendHttpRequest activity (#3787)

This commit is contained in:
Sipke Schoorstra 2023-03-11 22:14:39 +01:00 committed by GitHub
parent 4fd5c0e52a
commit 08069c40d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 145 additions and 29 deletions

View file

@ -127,6 +127,9 @@ export namespace Components {
}
interface ElsaHomePage {
}
interface ElsaHttpStatusCodesEditor {
"inputContext": ActivityInputContext;
}
interface ElsaInputControlSwitch {
"activityType": string;
"codeEditorHeight": string;
@ -561,6 +564,12 @@ declare global {
prototype: HTMLElsaHomePageElement;
new (): HTMLElsaHomePageElement;
};
interface HTMLElsaHttpStatusCodesEditorElement extends Components.ElsaHttpStatusCodesEditor, HTMLStencilElement {
}
var HTMLElsaHttpStatusCodesEditorElement: {
prototype: HTMLElsaHttpStatusCodesEditorElement;
new (): HTMLElsaHttpStatusCodesEditorElement;
};
interface HTMLElsaInputControlSwitchElement extends Components.ElsaInputControlSwitch, HTMLStencilElement {
}
var HTMLElsaInputControlSwitchElement: {
@ -856,6 +865,7 @@ declare global {
"elsa-flowchart": HTMLElsaFlowchartElement;
"elsa-form-panel": HTMLElsaFormPanelElement;
"elsa-home-page": HTMLElsaHomePageElement;
"elsa-http-status-codes-editor": HTMLElsaHttpStatusCodesEditorElement;
"elsa-input-control-switch": HTMLElsaInputControlSwitchElement;
"elsa-input-tags": HTMLElsaInputTagsElement;
"elsa-input-tags-dropdown": HTMLElsaInputTagsDropdownElement;
@ -996,6 +1006,9 @@ declare namespace LocalJSX {
}
interface ElsaHomePage {
}
interface ElsaHttpStatusCodesEditor {
"inputContext"?: ActivityInputContext;
}
interface ElsaInputControlSwitch {
"activityType"?: string;
"codeEditorHeight"?: string;
@ -1236,6 +1249,7 @@ declare namespace LocalJSX {
"elsa-flowchart": ElsaFlowchart;
"elsa-form-panel": ElsaFormPanel;
"elsa-home-page": ElsaHomePage;
"elsa-http-status-codes-editor": ElsaHttpStatusCodesEditor;
"elsa-input-control-switch": ElsaInputControlSwitch;
"elsa-input-tags": ElsaInputTags;
"elsa-input-tags-dropdown": ElsaInputTagsDropdown;
@ -1306,6 +1320,7 @@ declare module "@stencil/core" {
"elsa-flowchart": LocalJSX.ElsaFlowchart & JSXBase.HTMLAttributes<HTMLElsaFlowchartElement>;
"elsa-form-panel": LocalJSX.ElsaFormPanel & JSXBase.HTMLAttributes<HTMLElsaFormPanelElement>;
"elsa-home-page": LocalJSX.ElsaHomePage & JSXBase.HTMLAttributes<HTMLElsaHomePageElement>;
"elsa-http-status-codes-editor": LocalJSX.ElsaHttpStatusCodesEditor & JSXBase.HTMLAttributes<HTMLElsaHttpStatusCodesEditorElement>;
"elsa-input-control-switch": LocalJSX.ElsaInputControlSwitch & JSXBase.HTMLAttributes<HTMLElsaInputControlSwitchElement>;
"elsa-input-tags": LocalJSX.ElsaInputTags & JSXBase.HTMLAttributes<HTMLElsaInputTagsElement>;
"elsa-input-tags-dropdown": LocalJSX.ElsaInputTagsDropdown & JSXBase.HTMLAttributes<HTMLElsaInputTagsDropdownElement>;

View file

@ -1,7 +1,7 @@
import 'reflect-metadata';
import {h} from '@stencil/core';
import {Container, Service} from "typedi";
import {ActivityIconRegistry, PortProviderRegistry} from "../../../services";
import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../../services";
import {Plugin} from "../../../models";
import {HttpRequestPortProvider} from "./http-request-port-provider";
import {HttpRequestIcon} from "../icons";
@ -14,9 +14,11 @@ export class HttpRequestPlugin implements Plugin {
const activityTypeName = HttpRequestPlugin.ActivityTypeName;
const portProviderRegistry = Container.get(PortProviderRegistry);
const iconRegistry = Container.get(ActivityIconRegistry);
const inputControlRegistry = Container.get(InputControlRegistry);
portProviderRegistry.add(activityTypeName, () => Container.get(HttpRequestPortProvider));
iconRegistry.add(HttpRequestPlugin.ActivityTypeName, settings => <HttpRequestIcon size={settings?.size}/>);
inputControlRegistry.add('http-status-codes', c => <elsa-http-status-codes-editor inputContext={c}/>);
}
async initialize(): Promise<void> {

View file

@ -13,33 +13,38 @@ export class HttpRequestPortProvider implements PortProvider {
if(activity == null)
return [];
const defaultPort = {name: 'unmatchedStatusCode', displayName: 'Unmatched status code', mode: PortMode.Embedded, isBrowsable: false}; // Hide the port from the designer until the editor uI is finished.
const casesArray = this.getCases(activity);
const ports = casesArray.map(x => ({name: x.statusCode.toString(), displayName: x.statusCode.toString(), mode: PortMode.Embedded}));
const defaultPort = {name: 'unmatchedStatusCode', displayName: 'Unmatched status code', mode: PortMode.Embedded}; // Hide the port from the designer until the editor uI is finished.
const statusCodes = activity.expectedStatusCodes ?? [];
const ports = statusCodes.map(x => ({name: x.statusCode.toString(), displayName: x.statusCode.toString(), mode: PortMode.Embedded}));
return [...ports, defaultPort];
}
resolvePort(portName: string, context: PortProviderContext): Activity | Array<Activity> {
const activity = context.activity as SendHttpRequest;
const cases = this.getCases(activity);
const matchingStatusCode = cases.find(x => x.statusCode.toString() == portName);
return !matchingStatusCode ? activity.unmatchedStatusCode : matchingStatusCode.activity;
if(portName == 'unmatchedStatusCode')
return activity.unmatchedStatusCode;
const expectedStatusCodes: Array<HttpStatusCodeCase> = activity.expectedStatusCodes ?? [];
const matchingStatusCode = expectedStatusCodes.find(x => x.statusCode.toString() == portName);
return matchingStatusCode?.activity;
}
assignPort(portName: string, activity: Activity, context: PortProviderContext) {
const sendHttpRequestActivity = context.activity as SendHttpRequest;
const cases = this.getCases(sendHttpRequestActivity);
const matchingCase = cases.find(x => x.statusCode.toString() === portName);
if(!matchingCase)
if(portName == 'unmatchedStatusCode') {
sendHttpRequestActivity.unmatchedStatusCode = activity;
return;
}
const statusCodes = sendHttpRequestActivity.expectedStatusCodes ?? [];
const matchingStatusCode = statusCodes.find(x => x.statusCode.toString() === portName);
if(!matchingStatusCode)
return;
matchingCase.activity = activity;
}
private getCases(activity: SendHttpRequest): Array<HttpStatusCodeCase> {
const cases = activity.expectedStatusCodes;
return !cases ? [] : cases;
matchingStatusCode.activity = activity;
}
}

View file

@ -0,0 +1,51 @@
import {Component, h, Prop} from "@stencil/core";
import {ActivityInputContext} from "../../../services/activity-input-driver";
import {FormEntry} from "../../../components/shared/forms/form-entry";
import {getPropertyValue} from "../../../utils";
import {HttpStatusCodeCase, SendHttpRequest} from "./models";
@Component({
tag: 'elsa-http-status-codes-editor',
shadow: false
})
export class HttpStatusCodesEditor {
@Prop() inputContext: ActivityInputContext;
render() {
const inputContext = this.inputContext;
const inputDescriptor = inputContext.inputDescriptor;
const fieldId = inputDescriptor.name;
const displayName = inputDescriptor.displayName;
const hint = inputDescriptor.description;
const expectedStatusCodes: Array<HttpStatusCodeCase> = getPropertyValue(inputContext);
const statusCodeTags = expectedStatusCodes.map(x => x.statusCode.toString());
return (
<FormEntry fieldId={fieldId} label={displayName} hint={hint}>
<elsa-input-tags fieldId={fieldId} values={statusCodeTags} onValueChanged={this.onPropertyEditorChanged} placeHolder="Add status code"/>
</FormEntry>
);
}
private onPropertyEditorChanged = (e: CustomEvent<Array<string>>) => {
const statusCodes = e.detail;
const activity = this.inputContext.activity as SendHttpRequest;
const expectedStatusCodes = activity.expectedStatusCodes ?? [];
// Push new status codes.
for (const statusCode of statusCodes) {
if (expectedStatusCodes.findIndex(x => x.statusCode.toString() == statusCode) == -1)
expectedStatusCodes.push({statusCode: parseInt(statusCode)});
}
// Remove status codes that are no longer present.
for (let i = expectedStatusCodes.length - 1; i >= 0; i--) {
const statusCode = expectedStatusCodes[i].statusCode.toString();
if (statusCodes.findIndex(x => x == statusCode) == -1)
expectedStatusCodes.splice(i, 1);
}
activity.expectedStatusCodes = expectedStatusCodes;
this.inputContext.notifyInputChanged();
};
}

View file

@ -51,10 +51,7 @@ public class SendHttpRequest : SendHttpRequestBase
/// </summary>
[Input(
Description = "A list of expected status codes to handle and the corresponding activity to execute when the status code matches.",
UIHint = InputUIHints.MultiText,
// TODO: Need to implement a custom UI hint for this.
IsBrowsable = false
UIHint = "http-status-codes"
)]
public ICollection<HttpStatusCodeCase> ExpectedStatusCodes { get; set; } = new List<HttpStatusCodeCase>();
@ -62,8 +59,7 @@ public class SendHttpRequest : SendHttpRequestBase
/// The activity to execute when the HTTP status code does not match any of the expected status codes.
/// </summary>
[Port]
[Browsable(false)] // TODO: Need to implement a custom UI hint for this.
public IActivity? CatchAll { get; set; }
public IActivity? UnmatchedStatusCode { get; set; }
/// <inheritdoc />
protected override async ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response)
@ -71,7 +67,7 @@ public class SendHttpRequest : SendHttpRequestBase
var expectedStatusCodes = ExpectedStatusCodes;
var statusCode = (int)response.StatusCode;
var matchingCase = expectedStatusCodes.FirstOrDefault(x => x.StatusCode == statusCode);
var activity = matchingCase?.Activity ?? CatchAll;
var activity = matchingCase?.Activity ?? UnmatchedStatusCode;
await context.ScheduleActivityAsync(activity, OnChildActivityCompletedAsync);
}

View file

@ -9,10 +9,12 @@ using Elsa.Http.Handlers;
using Elsa.Http.Models;
using Elsa.Http.Options;
using Elsa.Http.Parsers;
using Elsa.Http.PortResolvers;
using Elsa.Http.Providers;
using Elsa.Http.Services;
using Elsa.JavaScript.Features;
using Elsa.Liquid.Features;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Management.Contracts;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
@ -110,6 +112,9 @@ public class HttpFeature : FeatureBase
// Activity property options providers.
.AddSingleton<IActivityPropertyOptionsProvider, WriteHttpResponseContentTypeOptionsProvider>()
// Port resolvers.
.AddSingleton<IActivityPortResolver, SendHttpRequestActivityPortResolver>()
// Add Http endpoint handlers.
.AddSingleton(HttpEndpointWorkflowFaultHandler)

View file

@ -0,0 +1,34 @@
using Elsa.Workflows.Core.Contracts;
namespace Elsa.Http.PortResolvers;
/// <summary>
/// Returns a list of outbound activities for a given <see cref="SendHttpRequest"/> activity's expected status codes.
/// </summary>
public class SendHttpRequestActivityPortResolver : IActivityPortResolver
{
/// <inheritdoc />
public int Priority => 0;
/// <inheritdoc />
public bool GetSupportsActivity(IActivity activity) => activity is SendHttpRequest;
/// <inheritdoc />
public ValueTask<IEnumerable<IActivity>> GetPortsAsync(IActivity activity, CancellationToken cancellationToken = default)
{
var ports = GetPortsInternal(activity);
return new(ports);
}
private IEnumerable<IActivity> GetPortsInternal(IActivity activity)
{
var sendHttpRequest = (SendHttpRequest)activity;
var cases = sendHttpRequest.ExpectedStatusCodes.Where(x => x.Activity != null);
foreach (var @case in cases)
yield return @case.Activity!;
if (sendHttpRequest.UnmatchedStatusCode != null)
yield return sendHttpRequest.UnmatchedStatusCode;
}
}

View file

@ -6,7 +6,6 @@ using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Workflows.Core.ActivationValidators;
using Elsa.Workflows.Core.ActivityNodeResolvers;
using Elsa.Workflows.Core.Builders;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Expressions;
@ -14,6 +13,7 @@ using Elsa.Workflows.Core.Middleware.Activities;
using Elsa.Workflows.Core.Middleware.Workflows;
using Elsa.Workflows.Core.Pipelines.ActivityExecution;
using Elsa.Workflows.Core.Pipelines.WorkflowExecution;
using Elsa.Workflows.Core.PortResolvers;
using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Core.Serialization.Providers;
using Elsa.Workflows.Core.Services;

View file

@ -1,15 +1,21 @@
using System.Reflection;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Services;
namespace Elsa.Workflows.Core.ActivityNodeResolvers;
namespace Elsa.Workflows.Core.PortResolvers;
/// <summary>
/// Returns a list of outbound activities for a given activity.
/// </summary>
public class OutboundActivityPortResolver : IActivityPortResolver
{
/// <inheritdoc />
public int Priority => -1;
/// <inheritdoc />
public bool GetSupportsActivity(IActivity activity) => true;
/// <inheritdoc />
public ValueTask<IEnumerable<IActivity>> GetPortsAsync(IActivity activity, CancellationToken cancellationToken = default) =>
new(GetSinglePorts(activity)
.Where(x => x != null)

View file

@ -1,17 +1,20 @@
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Services;
namespace Elsa.Workflows.Core.ActivityNodeResolvers;
namespace Elsa.Workflows.Core.PortResolvers;
/// <summary>
/// Returns a list of outbound activities for a given <see cref="Switch"/> activity's branches.
/// </summary>
public class SwitchActivityPortResolver : IActivityPortResolver
{
/// <inheritdoc />
public int Priority => 0;
/// <inheritdoc />
public bool GetSupportsActivity(IActivity activity) => activity is Switch;
/// <inheritdoc />
public ValueTask<IEnumerable<IActivity>> GetPortsAsync(IActivity activity, CancellationToken cancellationToken = default)
{
var ports = GetPortsInternal(activity);

View file

@ -13,7 +13,6 @@ public interface IWorkflowStateStore
/// </summary>
ValueTask SaveAsync(string id, WorkflowState state, CancellationToken cancellationToken = default);
/// <summary>
/// Load the <see cref="WorkflowState"/> by the specified ID.
/// </summary>