diff --git a/src/designer/elsa-workflows-designer/src/components.d.ts b/src/designer/elsa-workflows-designer/src/components.d.ts index dbe458b08..5a5b334b9 100644 --- a/src/designer/elsa-workflows-designer/src/components.d.ts +++ b/src/designer/elsa-workflows-designer/src/components.d.ts @@ -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; "elsa-form-panel": LocalJSX.ElsaFormPanel & JSXBase.HTMLAttributes; "elsa-home-page": LocalJSX.ElsaHomePage & JSXBase.HTMLAttributes; + "elsa-http-status-codes-editor": LocalJSX.ElsaHttpStatusCodesEditor & JSXBase.HTMLAttributes; "elsa-input-control-switch": LocalJSX.ElsaInputControlSwitch & JSXBase.HTMLAttributes; "elsa-input-tags": LocalJSX.ElsaInputTags & JSXBase.HTMLAttributes; "elsa-input-tags-dropdown": LocalJSX.ElsaInputTagsDropdown & JSXBase.HTMLAttributes; diff --git a/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-request-plugin.tsx b/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-request-plugin.tsx index 8fb3ae7a5..2391064a5 100644 --- a/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-request-plugin.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-request-plugin.tsx @@ -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 => ); + inputControlRegistry.add('http-status-codes', c => ); } async initialize(): Promise { diff --git a/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-request-port-provider.ts b/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-request-port-provider.ts index e5d328eb1..91ea2a2e4 100644 --- a/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-request-port-provider.ts +++ b/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-request-port-provider.ts @@ -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 { 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 = 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 { - const cases = activity.expectedStatusCodes; - return !cases ? [] : cases; + matchingStatusCode.activity = activity; } } diff --git a/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-status-codes-editor.tsx b/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-status-codes-editor.tsx new file mode 100644 index 000000000..524b592d6 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-status-codes-editor.tsx @@ -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 = getPropertyValue(inputContext); + const statusCodeTags = expectedStatusCodes.map(x => x.statusCode.toString()); + + return ( + + + + ); + } + + private onPropertyEditorChanged = (e: CustomEvent>) => { + 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(); + }; +} diff --git a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs index 847fa159a..f8c078103 100644 --- a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs +++ b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs @@ -51,10 +51,7 @@ public class SendHttpRequest : SendHttpRequestBase /// [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 ExpectedStatusCodes { get; set; } = new List(); @@ -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. /// [Port] - [Browsable(false)] // TODO: Need to implement a custom UI hint for this. - public IActivity? CatchAll { get; set; } + public IActivity? UnmatchedStatusCode { get; set; } /// 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); } diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index 64fa3f539..54eeeaafc 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -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() + + // Port resolvers. + .AddSingleton() // Add Http endpoint handlers. .AddSingleton(HttpEndpointWorkflowFaultHandler) diff --git a/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs b/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs new file mode 100644 index 000000000..ee98b2092 --- /dev/null +++ b/src/modules/Elsa.Http/PortResolvers/SendHttpRequestActivityPortResolver.cs @@ -0,0 +1,34 @@ +using Elsa.Workflows.Core.Contracts; + +namespace Elsa.Http.PortResolvers; + +/// +/// Returns a list of outbound activities for a given activity's expected status codes. +/// +public class SendHttpRequestActivityPortResolver : IActivityPortResolver +{ + /// + public int Priority => 0; + + /// + public bool GetSupportsActivity(IActivity activity) => activity is SendHttpRequest; + + /// + public ValueTask> GetPortsAsync(IActivity activity, CancellationToken cancellationToken = default) + { + var ports = GetPortsInternal(activity); + return new(ports); + } + + private IEnumerable 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; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs index 790385ccf..d425316c5 100644 --- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs +++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs @@ -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; diff --git a/src/modules/Elsa.Workflows.Core/ActivityNodeResolvers/OutboundActivityPortResolver.cs b/src/modules/Elsa.Workflows.Core/PortResolvers/OutboundActivityPortResolver.cs similarity index 87% rename from src/modules/Elsa.Workflows.Core/ActivityNodeResolvers/OutboundActivityPortResolver.cs rename to src/modules/Elsa.Workflows.Core/PortResolvers/OutboundActivityPortResolver.cs index 9f1236df6..e1dfc090d 100644 --- a/src/modules/Elsa.Workflows.Core/ActivityNodeResolvers/OutboundActivityPortResolver.cs +++ b/src/modules/Elsa.Workflows.Core/PortResolvers/OutboundActivityPortResolver.cs @@ -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; +/// +/// Returns a list of outbound activities for a given activity. +/// public class OutboundActivityPortResolver : IActivityPortResolver { + /// public int Priority => -1; + + /// public bool GetSupportsActivity(IActivity activity) => true; + /// public ValueTask> GetPortsAsync(IActivity activity, CancellationToken cancellationToken = default) => new(GetSinglePorts(activity) .Where(x => x != null) diff --git a/src/modules/Elsa.Workflows.Core/ActivityNodeResolvers/SwitchActivityPortResolver.cs b/src/modules/Elsa.Workflows.Core/PortResolvers/SwitchActivityPortResolver.cs similarity index 89% rename from src/modules/Elsa.Workflows.Core/ActivityNodeResolvers/SwitchActivityPortResolver.cs rename to src/modules/Elsa.Workflows.Core/PortResolvers/SwitchActivityPortResolver.cs index 501506887..f5b7590b7 100644 --- a/src/modules/Elsa.Workflows.Core/ActivityNodeResolvers/SwitchActivityPortResolver.cs +++ b/src/modules/Elsa.Workflows.Core/PortResolvers/SwitchActivityPortResolver.cs @@ -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; /// /// Returns a list of outbound activities for a given activity's branches. /// public class SwitchActivityPortResolver : IActivityPortResolver { + /// public int Priority => 0; + + /// public bool GetSupportsActivity(IActivity activity) => activity is Switch; + /// public ValueTask> GetPortsAsync(IActivity activity, CancellationToken cancellationToken = default) { var ports = GetPortsInternal(activity); diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowStateStore.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowStateStore.cs index 9d64fc471..8ab38c04f 100644 --- a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowStateStore.cs +++ b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowStateStore.cs @@ -13,7 +13,6 @@ public interface IWorkflowStateStore /// ValueTask SaveAsync(string id, WorkflowState state, CancellationToken cancellationToken = default); - /// /// Load the by the specified ID. ///