From b1564bf646c61684b7fac99ce04dcad0f4cee4c5 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 7 Mar 2023 13:45:58 +0100 Subject: [PATCH] HTTP Request with status codes as outcomes (#3768) * Implement configurable expected status outcomes on HTTP Request activity * Add Catch all outcome * Implement dynamic port update --- .../src/components/icons/activities/index.tsx | 1 - .../flowchart/activity-node-handler.ts | 12 +- .../modules/flowchart/default-node-handler.ts | 31 +++-- .../src/modules/flowchart/flowchart.tsx | 30 ++++- .../src/modules/flowchart/models.ts | 1 + .../src/modules/flowchart/node-factory.ts | 5 + .../flow/flow-http-request-plugin.tsx | 25 ++++ .../flow/flow-http-request-port-provider.ts | 36 +++++ .../src/modules/http-request/flow/models.ts | 5 + .../http-request/icons.tsx} | 2 +- .../sequence/http-request-plugin.tsx | 25 ++++ .../sequence/http-request-port-provider.ts | 45 +++++++ .../modules/http-request/sequence/models.ts | 11 ++ .../flow}/flow-switch-editor.tsx | 12 +- .../flow}/flow-switch-plugin.tsx | 6 +- .../flow}/flow-switch-port-provider.ts | 4 +- .../{flow-switch => switch/flow}/models.ts | 2 +- .../modules/switch/{ => sequence}/models.ts | 2 +- .../switch/{ => sequence}/switch-editor.tsx | 13 +- .../switch/{ => sequence}/switch-plugin.tsx | 6 +- .../{ => sequence}/switch-port-provider.ts | 5 +- .../components/editor.tsx | 1 + .../src/services/activity-icon-registry.tsx | 4 +- .../src/services/plugin-registry.ts | 8 +- .../src/utils/graph.ts | 2 +- .../Elsa.Http/Activities/SendHttpRequest.cs | 125 +++++++++++++++++- src/modules/Elsa.Http/Features/HttpFeature.cs | 4 +- ...DefaultHttpEndpointWorkflowFaultHandler.cs | 3 + .../Expressions/JsonExpression.cs | 7 +- .../Converters/IntegerArrayConverter.cs | 28 ++++ .../Workflows/GetUsersWorkflow.cs | 1 - 31 files changed, 398 insertions(+), 64 deletions(-) create mode 100644 src/designer/elsa-workflows-designer/src/modules/http-request/flow/flow-http-request-plugin.tsx create mode 100644 src/designer/elsa-workflows-designer/src/modules/http-request/flow/flow-http-request-port-provider.ts create mode 100644 src/designer/elsa-workflows-designer/src/modules/http-request/flow/models.ts rename src/designer/elsa-workflows-designer/src/{components/icons/activities/http-request.tsx => modules/http-request/icons.tsx} (86%) create mode 100644 src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-request-plugin.tsx create mode 100644 src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-request-port-provider.ts create mode 100644 src/designer/elsa-workflows-designer/src/modules/http-request/sequence/models.ts rename src/designer/elsa-workflows-designer/src/modules/{flow-switch => switch/flow}/flow-switch-editor.tsx (92%) rename src/designer/elsa-workflows-designer/src/modules/{flow-switch => switch/flow}/flow-switch-plugin.tsx (93%) rename src/designer/elsa-workflows-designer/src/modules/{flow-switch => switch/flow}/flow-switch-port-provider.ts (84%) rename src/designer/elsa-workflows-designer/src/modules/{flow-switch => switch/flow}/models.ts (75%) rename src/designer/elsa-workflows-designer/src/modules/switch/{ => sequence}/models.ts (72%) rename src/designer/elsa-workflows-designer/src/modules/switch/{ => sequence}/switch-editor.tsx (91%) rename src/designer/elsa-workflows-designer/src/modules/switch/{ => sequence}/switch-plugin.tsx (93%) rename src/designer/elsa-workflows-designer/src/modules/switch/{ => sequence}/switch-port-provider.ts (89%) create mode 100644 src/modules/Elsa.Workflows.Core/Serialization/Converters/IntegerArrayConverter.cs diff --git a/src/designer/elsa-workflows-designer/src/components/icons/activities/index.tsx b/src/designer/elsa-workflows-designer/src/components/icons/activities/index.tsx index 574a61a4d..0183a9ceb 100644 --- a/src/designer/elsa-workflows-designer/src/components/icons/activities/index.tsx +++ b/src/designer/elsa-workflows-designer/src/components/icons/activities/index.tsx @@ -7,7 +7,6 @@ export * from "./flow-node"; export * from "./flowchart"; export * from "./foreach"; export * from "./http-endpoint"; -export * from "./http-request"; export * from "./http-response"; export * from "./if"; export * from "./models"; diff --git a/src/designer/elsa-workflows-designer/src/modules/flowchart/activity-node-handler.ts b/src/designer/elsa-workflows-designer/src/modules/flowchart/activity-node-handler.ts index 58d03c031..6f18dcff2 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flowchart/activity-node-handler.ts +++ b/src/designer/elsa-workflows-designer/src/modules/flowchart/activity-node-handler.ts @@ -2,12 +2,16 @@ import {Node} from "@antv/x6"; import {Activity, ActivityDescriptor} from "../../models"; export interface ActivityNodeHandler { - createDesignerNode: (context: CreateUINodeContext) => Node.Metadata; + createDesignerNode: (context: UINodeContext) => Node.Metadata; + createPorts: (context: UIPortContext) => Array; } -export interface CreateUINodeContext { - activityDescriptor: ActivityDescriptor; - activity: Activity; +export interface UINodeContext extends UIPortContext{ x: number; y: number; } + +export interface UIPortContext { + activityDescriptor: ActivityDescriptor; + activity: Activity; +} diff --git a/src/designer/elsa-workflows-designer/src/modules/flowchart/default-node-handler.ts b/src/designer/elsa-workflows-designer/src/modules/flowchart/default-node-handler.ts index 487702ec2..dc07e6699 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flowchart/default-node-handler.ts +++ b/src/designer/elsa-workflows-designer/src/modules/flowchart/default-node-handler.ts @@ -1,7 +1,7 @@ import 'reflect-metadata'; import {Node} from "@antv/x6"; import {Container, Service} from "typedi" -import {ActivityNodeHandler, CreateUINodeContext} from "./activity-node-handler"; +import {ActivityNodeHandler, UINodeContext, UIPortContext} from "./activity-node-handler"; import {PortProviderContext, PortProviderRegistry} from "../../services"; import {PortMode} from "../../models"; import {v4 as uuid} from 'uuid'; @@ -14,8 +14,24 @@ export class DefaultNodeHandler implements ActivityNodeHandler { this.portProviderRegistry = Container.get(PortProviderRegistry); } - createDesignerNode(context: CreateUINodeContext): Node.Metadata { + createDesignerNode(context: UINodeContext): Node.Metadata { const {activityDescriptor, activity, x, y} = context; + const portModels = this.createPorts(context); + + return { + id: activity.id, + shape: 'activity', + activity: activity, + activityDescriptor: activityDescriptor, + x: x, + y: y, + data: activity, + ports: portModels + } as Node.Metadata; + } + + createPorts(context: UIPortContext): Array { + const {activityDescriptor, activity} = context; const provider = this.portProviderRegistry.get(activityDescriptor.typeName); const providerContext: PortProviderContext = {activityDescriptor, activity}; const inPorts = [{name: 'In', displayName: null, mode: PortMode.Port}]; @@ -54,15 +70,6 @@ export class DefaultNodeHandler implements ActivityNodeHandler { const portModels = [...leftPortModels, ...rightPortModels]; - return { - id: activity.id, - shape: 'activity', - activity: activity, - activityDescriptor: activityDescriptor, - x: x, - y: y, - data: activity, - ports: portModels - } as Node.Metadata; + return portModels; } } diff --git a/src/designer/elsa-workflows-designer/src/modules/flowchart/flowchart.tsx b/src/designer/elsa-workflows-designer/src/modules/flowchart/flowchart.tsx index 6b44c8fcb..5e3e827b4 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flowchart/flowchart.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/flowchart/flowchart.tsx @@ -20,7 +20,7 @@ import FromJSONData = Model.FromJSONData; import PointLike = Point.PointLike; import {generateUniqueActivityName} from "../../utils/generate-activity-name"; import {DagreLayout, OutNode} from '@antv/layout'; -import {adjustPortMarkupByNode, rebuildGraph} from '../../utils/graph'; +import {adjustPortMarkupByNode, getPortNameByPortId, rebuildGraph} from '../../utils/graph'; import FlowchartTunnel, {FlowchartState} from "./state"; const FlowchartTypeName = 'Elsa.Flowchart'; @@ -196,19 +196,25 @@ export class FlowchartComponent { const originalId = args.originalId; const nodeId = originalId; const activity = args.activity; - const node = this.graph.getNodes().find(x => x.id == nodeId) as ActivityNodeShape; + const node = this.graph.getNodes().find(x => x.id == nodeId); + const nodeShape = node as ActivityNodeShape; if (!!node) { // Update the node's data with the activity. - node.setData(activity, {overwrite: true}); + nodeShape.setData(activity, {overwrite: true}); // Updating the node's activity property to trigger a rerender. - node.activity = activity; + nodeShape.activity = activity; // If the ID of the activity changed, we need to update connection references (X6 stores deep copies of data). if (activityId !== originalId) this.syncEdgeData(nodeId, activity); + + // Update ports. + if (args.updatePorts) { + this.updatePorts(node, activity); + } } this.updateLookups(); @@ -671,6 +677,22 @@ export class FlowchartComponent { await this.setupGraph(childFlowchart); } + private updatePorts = (node: any, activity: Activity) => { + const descriptor = this.getActivityDescriptor(activity.type); + const desiredPorts = this.nodeFactory.createPorts(descriptor, activity); + const actualPorts = node.ports.items; + + const addedPorts = desiredPorts.filter(x => !actualPorts.some(y => getPortNameByPortId(y.id) == getPortNameByPortId(x.id))); + const removedPorts = actualPorts.filter(x => !desiredPorts.some(y => getPortNameByPortId(y.id) == getPortNameByPortId(x.id))); + + if(addedPorts.length > 0 ) + node.addPorts(addedPorts); + + if(removedPorts.length > 0) + node.removePorts(removedPorts); + + }; + render() { const path = this.path; diff --git a/src/designer/elsa-workflows-designer/src/modules/flowchart/models.ts b/src/designer/elsa-workflows-designer/src/modules/flowchart/models.ts index 2c1bb7a04..eb8fee637 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flowchart/models.ts +++ b/src/designer/elsa-workflows-designer/src/modules/flowchart/models.ts @@ -40,6 +40,7 @@ export interface UpdateActivityArgs { id: string; originalId: string; activity: Activity; + updatePorts?: boolean; } export interface RenameActivityArgs { diff --git a/src/designer/elsa-workflows-designer/src/modules/flowchart/node-factory.ts b/src/designer/elsa-workflows-designer/src/modules/flowchart/node-factory.ts index fbb05d9f0..52630a1b7 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flowchart/node-factory.ts +++ b/src/designer/elsa-workflows-designer/src/modules/flowchart/node-factory.ts @@ -16,4 +16,9 @@ export class NodeFactory { const handler = this.handlerRegistry.createHandler(activityDescriptor.typeName); return handler.createDesignerNode({activityDescriptor, activity, x, y}); } + + public createPorts(activityDescriptor: ActivityDescriptor, activity: Activity): Array { + const handler = this.handlerRegistry.createHandler(activityDescriptor.typeName); + return handler.createPorts({activityDescriptor, activity}); + } } diff --git a/src/designer/elsa-workflows-designer/src/modules/http-request/flow/flow-http-request-plugin.tsx b/src/designer/elsa-workflows-designer/src/modules/http-request/flow/flow-http-request-plugin.tsx new file mode 100644 index 000000000..ed824a11c --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/modules/http-request/flow/flow-http-request-plugin.tsx @@ -0,0 +1,25 @@ +import 'reflect-metadata'; +import {h} from '@stencil/core'; +import {Container, Service} from "typedi"; +import {ActivityIconRegistry, PortProviderRegistry} from "../../../services"; +import {Plugin} from "../../../models"; +import {FlowHttpRequestPortProvider} from "./flow-http-request-port-provider"; +import {HttpRequestIcon} from "../icons"; + +@Service() +export class FlowHttpRequestPlugin implements Plugin { + static readonly ActivityTypeName: string = 'Elsa.FlowSendHttpRequest'; + + constructor() { + const activityTypeName = FlowHttpRequestPlugin.ActivityTypeName; + const portProviderRegistry = Container.get(PortProviderRegistry); + const iconRegistry = Container.get(ActivityIconRegistry); + + portProviderRegistry.add(activityTypeName, () => Container.get(FlowHttpRequestPortProvider)); + iconRegistry.add(FlowHttpRequestPlugin.ActivityTypeName, settings => ); + } + + async initialize(): Promise { + + } +} diff --git a/src/designer/elsa-workflows-designer/src/modules/http-request/flow/flow-http-request-port-provider.ts b/src/designer/elsa-workflows-designer/src/modules/http-request/flow/flow-http-request-port-provider.ts new file mode 100644 index 000000000..6915cd112 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/modules/http-request/flow/flow-http-request-port-provider.ts @@ -0,0 +1,36 @@ +import 'reflect-metadata'; +import {Service} from "typedi"; +import {Activity, ActivityInput, InputDescriptor, JsonExpression, Port, PortMode} from "../../../models"; +import {PortProvider, PortProviderContext} from "../../../services"; +import {FlowSendHttpRequest} from "./models"; + +@Service() +export class FlowHttpRequestPortProvider implements PortProvider { + + getOutboundPorts(context: PortProviderContext): Array { + const activity = context.activity as FlowSendHttpRequest; + + if (activity == null) + return []; + + const expectedStatusCodes = activity.expectedStatusCodes as ActivityInput; + + if(!expectedStatusCodes) + return []; + + const statusCodesJson = (expectedStatusCodes.expression as JsonExpression).value; + const statusCodes = JSON.parse(statusCodesJson) as Array; + const catchAllPort = {name: 'Catch all', displayName: 'Catch all', mode: PortMode.Port}; + const outcomes = [...statusCodes.map(x => ({name: x.toString(), displayName: x.toString(), mode: PortMode.Port})), catchAllPort]; + + return outcomes; + } + + resolvePort(portName: string, context: PortProviderContext): Activity | Array { + return null; + } + + assignPort(portName: string, activity: Activity, context: PortProviderContext) { + return null; + } +} diff --git a/src/designer/elsa-workflows-designer/src/modules/http-request/flow/models.ts b/src/designer/elsa-workflows-designer/src/modules/http-request/flow/models.ts new file mode 100644 index 000000000..b1c6f0c84 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/modules/http-request/flow/models.ts @@ -0,0 +1,5 @@ +import {Activity, ActivityInput} from "../../../models"; + +export interface FlowSendHttpRequest extends Activity { + expectedStatusCodes: ActivityInput; +} diff --git a/src/designer/elsa-workflows-designer/src/components/icons/activities/http-request.tsx b/src/designer/elsa-workflows-designer/src/modules/http-request/icons.tsx similarity index 86% rename from src/designer/elsa-workflows-designer/src/components/icons/activities/http-request.tsx rename to src/designer/elsa-workflows-designer/src/modules/http-request/icons.tsx index 94497c75e..737dd3126 100644 --- a/src/designer/elsa-workflows-designer/src/components/icons/activities/http-request.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/http-request/icons.tsx @@ -1,5 +1,5 @@ import {FunctionalComponent, h} from '@stencil/core'; -import {ActivityIconSettings, getActivityIconCssClass} from "./models"; +import {ActivityIconSettings, getActivityIconCssClass} from "../../components/icons/activities"; export const HttpRequestIcon: FunctionalComponent = (settings) => ( Container.get(HttpRequestPortProvider)); + iconRegistry.add(HttpRequestPlugin.ActivityTypeName, settings => ); + } + + 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 new file mode 100644 index 000000000..e5d328eb1 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/http-request-port-provider.ts @@ -0,0 +1,45 @@ +import 'reflect-metadata'; +import {Service} from "typedi"; +import {Activity, Port, PortMode} from "../../../models"; +import {PortProvider, PortProviderContext} from "../../../services"; +import {HttpStatusCodeCase, SendHttpRequest} from "./models"; + +@Service() +export class HttpRequestPortProvider implements PortProvider { + + getOutboundPorts(context: PortProviderContext): Array { + const activity = context.activity as SendHttpRequest; + + 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})); + + 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; + } + + 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) + return; + + matchingCase.activity = activity; + } + + private getCases(activity: SendHttpRequest): Array { + const cases = activity.expectedStatusCodes; + return !cases ? [] : cases; + } +} diff --git a/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/models.ts b/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/models.ts new file mode 100644 index 000000000..8331b62b9 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/modules/http-request/sequence/models.ts @@ -0,0 +1,11 @@ +import {Activity} from "../../../models"; + +export interface SendHttpRequest extends Activity { + expectedStatusCodes: Array; + unmatchedStatusCode?: Activity; +} + +export interface HttpStatusCodeCase { + statusCode: number; + activity?: Activity; +} diff --git a/src/designer/elsa-workflows-designer/src/modules/flow-switch/flow-switch-editor.tsx b/src/designer/elsa-workflows-designer/src/modules/switch/flow/flow-switch-editor.tsx similarity index 92% rename from src/designer/elsa-workflows-designer/src/modules/flow-switch/flow-switch-editor.tsx rename to src/designer/elsa-workflows-designer/src/modules/switch/flow/flow-switch-editor.tsx index 5d216f1ff..e027f6f9c 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flow-switch/flow-switch-editor.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/switch/flow/flow-switch-editor.tsx @@ -1,11 +1,11 @@ import {Component, h, Prop, State, Watch} from "@stencil/core"; import {camelCase} from 'lodash'; -import {ActivityInputContext} from "../../services/activity-input-driver"; -import {mapSyntaxToLanguage} from "../../utils"; -import {SyntaxNames} from "../../models"; -import {MonacoValueChangedArgs} from "../../components/shared/monaco-editor/monaco-editor"; -import {TrashBinButtonIcon} from "../../components/icons/buttons/trash-bin"; -import {PlusButtonIcon} from "../../components/icons/buttons/plus"; +import {ActivityInputContext} from "../../../services/activity-input-driver"; +import {mapSyntaxToLanguage} from "../../../utils"; +import {SyntaxNames} from "../../../models"; +import {MonacoValueChangedArgs} from "../../../components/shared/monaco-editor/monaco-editor"; +import {TrashBinButtonIcon} from "../../../components/icons/buttons/trash-bin"; +import {PlusButtonIcon} from "../../../components/icons/buttons/plus"; import {FlowSwitchCase} from "./models"; @Component({ diff --git a/src/designer/elsa-workflows-designer/src/modules/flow-switch/flow-switch-plugin.tsx b/src/designer/elsa-workflows-designer/src/modules/switch/flow/flow-switch-plugin.tsx similarity index 93% rename from src/designer/elsa-workflows-designer/src/modules/flow-switch/flow-switch-plugin.tsx rename to src/designer/elsa-workflows-designer/src/modules/switch/flow/flow-switch-plugin.tsx index 5f50aa187..be9bc0209 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flow-switch/flow-switch-plugin.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/switch/flow/flow-switch-plugin.tsx @@ -1,10 +1,10 @@ import 'reflect-metadata'; import {FunctionalComponent, h} from '@stencil/core'; import {Container, Service} from "typedi"; -import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../services"; -import {Plugin} from "../../models"; +import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../../services"; +import {Plugin} from "../../../models"; import {FlowSwitchPortProvider} from "./flow-switch-port-provider"; -import {ActivityIconSettings, getActivityIconCssClass} from "../../components/icons/activities"; +import {ActivityIconSettings, getActivityIconCssClass} from "../../../components/icons/activities"; @Service() export class FlowSwitchPlugin implements Plugin { diff --git a/src/designer/elsa-workflows-designer/src/modules/flow-switch/flow-switch-port-provider.ts b/src/designer/elsa-workflows-designer/src/modules/switch/flow/flow-switch-port-provider.ts similarity index 84% rename from src/designer/elsa-workflows-designer/src/modules/flow-switch/flow-switch-port-provider.ts rename to src/designer/elsa-workflows-designer/src/modules/switch/flow/flow-switch-port-provider.ts index f85b66168..ff7a16380 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flow-switch/flow-switch-port-provider.ts +++ b/src/designer/elsa-workflows-designer/src/modules/switch/flow/flow-switch-port-provider.ts @@ -1,8 +1,8 @@ import 'reflect-metadata'; import {Service} from "typedi"; -import {Activity, Port, PortMode} from "../../models"; +import {Activity, Port, PortMode} from "../../../models"; import {FlowSwitchActivity} from "./models"; -import {PortProvider, PortProviderContext} from "../../services"; +import {PortProvider, PortProviderContext} from "../../../services"; @Service() export class FlowSwitchPortProvider implements PortProvider { diff --git a/src/designer/elsa-workflows-designer/src/modules/flow-switch/models.ts b/src/designer/elsa-workflows-designer/src/modules/switch/flow/models.ts similarity index 75% rename from src/designer/elsa-workflows-designer/src/modules/flow-switch/models.ts rename to src/designer/elsa-workflows-designer/src/modules/switch/flow/models.ts index f7c4e00cb..d538f5a0d 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flow-switch/models.ts +++ b/src/designer/elsa-workflows-designer/src/modules/switch/flow/models.ts @@ -1,4 +1,4 @@ -import {Activity, Expression} from "../../models"; +import {Activity, Expression} from "../../../models"; export interface FlowSwitchCase { label: string; diff --git a/src/designer/elsa-workflows-designer/src/modules/switch/models.ts b/src/designer/elsa-workflows-designer/src/modules/switch/sequence/models.ts similarity index 72% rename from src/designer/elsa-workflows-designer/src/modules/switch/models.ts rename to src/designer/elsa-workflows-designer/src/modules/switch/sequence/models.ts index 3bdf5c49b..325c5d5de 100644 --- a/src/designer/elsa-workflows-designer/src/modules/switch/models.ts +++ b/src/designer/elsa-workflows-designer/src/modules/switch/sequence/models.ts @@ -1,4 +1,4 @@ -import {Activity, ActivityInput, Expression} from "../../models"; +import {Activity, ActivityInput, Expression} from "../../../models"; export interface SwitchCase { label: string; diff --git a/src/designer/elsa-workflows-designer/src/modules/switch/switch-editor.tsx b/src/designer/elsa-workflows-designer/src/modules/switch/sequence/switch-editor.tsx similarity index 91% rename from src/designer/elsa-workflows-designer/src/modules/switch/switch-editor.tsx rename to src/designer/elsa-workflows-designer/src/modules/switch/sequence/switch-editor.tsx index 6d8854cba..41c9704d6 100644 --- a/src/designer/elsa-workflows-designer/src/modules/switch/switch-editor.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/switch/sequence/switch-editor.tsx @@ -1,13 +1,12 @@ import {Component, h, Prop, State, Watch} from "@stencil/core"; import {camelCase} from 'lodash'; -import {ActivityInputContext} from "../../services/activity-input-driver"; -import {mapSyntaxToLanguage} from "../../utils"; -import {SyntaxNames} from "../../models"; +import {ActivityInputContext} from "../../../services/activity-input-driver"; +import {mapSyntaxToLanguage} from "../../../utils"; +import {SyntaxNames} from "../../../models"; import {SwitchCase} from "./models"; -import {MonacoValueChangedArgs} from "../../components/shared/monaco-editor/monaco-editor"; -import {TrashBinButtonIcon} from "../../components/icons/buttons/trash-bin"; -import {PlusButtonIcon} from "../../components/icons/buttons/plus"; -import {FormEntry} from "../../components/shared/forms/form-entry"; +import {MonacoValueChangedArgs} from "../../../components/shared/monaco-editor/monaco-editor"; +import {TrashBinButtonIcon} from "../../../components/icons/buttons/trash-bin"; +import {PlusButtonIcon} from "../../../components/icons/buttons/plus"; @Component({ tag: 'elsa-switch-editor', diff --git a/src/designer/elsa-workflows-designer/src/modules/switch/switch-plugin.tsx b/src/designer/elsa-workflows-designer/src/modules/switch/sequence/switch-plugin.tsx similarity index 93% rename from src/designer/elsa-workflows-designer/src/modules/switch/switch-plugin.tsx rename to src/designer/elsa-workflows-designer/src/modules/switch/sequence/switch-plugin.tsx index 40b5c6643..38daa6e49 100644 --- a/src/designer/elsa-workflows-designer/src/modules/switch/switch-plugin.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/switch/sequence/switch-plugin.tsx @@ -1,10 +1,10 @@ import 'reflect-metadata'; import {FunctionalComponent, h} from '@stencil/core'; import {Container, Service} from "typedi"; -import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../services"; -import {Plugin} from "../../models"; +import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../../services"; +import {Plugin} from "../../../models"; import {SwitchPortProvider} from "./switch-port-provider"; -import {ActivityIconSettings, getActivityIconCssClass} from "../../components/icons/activities"; +import {ActivityIconSettings, getActivityIconCssClass} from "../../../components/icons/activities"; @Service() export class SwitchPlugin implements Plugin { diff --git a/src/designer/elsa-workflows-designer/src/modules/switch/switch-port-provider.ts b/src/designer/elsa-workflows-designer/src/modules/switch/sequence/switch-port-provider.ts similarity index 89% rename from src/designer/elsa-workflows-designer/src/modules/switch/switch-port-provider.ts rename to src/designer/elsa-workflows-designer/src/modules/switch/sequence/switch-port-provider.ts index db0104ba0..ce996dd15 100644 --- a/src/designer/elsa-workflows-designer/src/modules/switch/switch-port-provider.ts +++ b/src/designer/elsa-workflows-designer/src/modules/switch/sequence/switch-port-provider.ts @@ -1,9 +1,8 @@ import 'reflect-metadata'; import {Service} from "typedi"; -import {camelCase} from 'lodash'; -import {Activity, Port, PortMode} from "../../models"; +import {Activity, Port, PortMode} from "../../../models"; import {SwitchActivity, SwitchCase} from "./models"; -import {PortProvider, PortProviderContext} from "../../services"; +import {PortProvider, PortProviderContext} from "../../../services"; @Service() export class SwitchPortProvider implements PortProvider { diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/editor.tsx b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/editor.tsx index 4fb42ccb8..1f173fc34 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/editor.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/editor.tsx @@ -227,6 +227,7 @@ export class WorkflowDefinitionEditor { } private updateActivityInternal = async (args: UpdateActivityArgs) => { + args.updatePorts = true; // TODO: Make this configurable from a activity plugin. await this.flowchart.updateActivity(args); this.saveChangesDebounced(); } diff --git a/src/designer/elsa-workflows-designer/src/services/activity-icon-registry.tsx b/src/designer/elsa-workflows-designer/src/services/activity-icon-registry.tsx index ec033aa37..487319f62 100644 --- a/src/designer/elsa-workflows-designer/src/services/activity-icon-registry.tsx +++ b/src/designer/elsa-workflows-designer/src/services/activity-icon-registry.tsx @@ -11,8 +11,7 @@ import { IfIcon, ReadLineIcon, WriteLineIcon, RunJavaScriptIcon, - HttpEndpointIcon, HttpResponseIcon, HttpRequestIcon, - CorrelateIcon + HttpEndpointIcon, HttpResponseIcon, CorrelateIcon } from "../components/icons/activities"; import {ForIcon} from "../components/icons/activities/for"; import {FinishIcon} from "../components/icons/activities/finish"; @@ -56,7 +55,6 @@ export class ActivityIconRegistry { this.add('Elsa.RunJavaScript', settings => ); this.add('Elsa.FlowJoin', settings => ); this.add('Elsa.FlowNode', settings => ); - this.add('Elsa.SendHttpRequest', settings => ); this.add("Elsa.Correlate", settings => ) this.add("Elsa.Start", settings => ) this.add("Elsa.Finish", settings => ) diff --git a/src/designer/elsa-workflows-designer/src/services/plugin-registry.ts b/src/designer/elsa-workflows-designer/src/services/plugin-registry.ts index 8905854b6..6c78f80be 100644 --- a/src/designer/elsa-workflows-designer/src/services/plugin-registry.ts +++ b/src/designer/elsa-workflows-designer/src/services/plugin-registry.ts @@ -1,15 +1,17 @@ import 'reflect-metadata'; import {Container, Service} from "typedi"; -import {SwitchPlugin} from "../modules/switch/switch-plugin"; import {Plugin} from "../models"; import {SequencePlugin} from "../modules/sequence/sequence-plugin"; -import {FlowSwitchPlugin} from "../modules/flow-switch/flow-switch-plugin"; import {WorkflowDefinitionsPlugin} from "../modules/workflow-definitions/plugins/workflow-definitions-plugin"; import {CompositeActivityVersionPlugin} from "../modules/workflow-definitions/plugins/composite-version-plugin"; import {WorkflowInstancesPlugin} from "../modules/workflow-instances/plugin"; import {LoginPlugin} from "../modules/login/plugin"; import {HomePagePlugin} from "../modules/home/plugin"; import {FlowchartPlugin} from "../modules/flowchart/plugin"; +import { SwitchPlugin } from '../modules/switch/sequence/switch-plugin'; +import {FlowSwitchPlugin} from "../modules/switch/flow/flow-switch-plugin"; +import {FlowHttpRequestPlugin} from "../modules/http-request/flow/flow-http-request-plugin"; +import {HttpRequestPlugin} from "../modules/http-request/sequence/http-request-plugin"; // A registry of plugins. @Service() @@ -26,6 +28,8 @@ export class PluginRegistry { this.add(Container.get(SequencePlugin)); this.add(Container.get(SwitchPlugin)); this.add(Container.get(FlowSwitchPlugin)); + this.add(Container.get(FlowHttpRequestPlugin)); + this.add(Container.get(HttpRequestPlugin)); } add(plugin: Plugin) { diff --git a/src/designer/elsa-workflows-designer/src/utils/graph.ts b/src/designer/elsa-workflows-designer/src/utils/graph.ts index 9a07abd70..4897576ee 100644 --- a/src/designer/elsa-workflows-designer/src/utils/graph.ts +++ b/src/designer/elsa-workflows-designer/src/utils/graph.ts @@ -147,7 +147,7 @@ function findMatchingPortForEdge(node: Node, position: string, return node.getPorts().find(p => p.position == position && p.type == portType && getPortNameByPortId(p.id) == portName); } -function getPortNameByPortId(portId: string) { +export function getPortNameByPortId(portId: string) { return portId.includes('_') ? (portId.split('_')[1] == 'null' ? null : portId.split('_')[1]) : portId; } diff --git a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs index ff60e5ed1..57977acde 100644 --- a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs +++ b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs @@ -1,10 +1,13 @@ +using System.ComponentModel; using System.Net.Http.Headers; +using System.Text.Json.Serialization; using Elsa.Extensions; using Elsa.Http.ContentWriters; using Elsa.Workflows.Core; using Elsa.Workflows.Core.Attributes; +using Elsa.Workflows.Core.Contracts; using Elsa.Workflows.Core.Models; -using Elsa.Workflows.Management.Models; +using JetBrains.Annotations; using Microsoft.AspNetCore.Http; using HttpRequestHeaders = Elsa.Http.Models.HttpRequestHeaders; @@ -13,9 +16,112 @@ namespace Elsa.Http; /// /// Send an HTTP request. /// -[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "HTTP Request", Kind = ActivityKind.Task)] -public class SendHttpRequest : CodeActivity +[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "Flow HTTP Request", Kind = ActivityKind.Task)] +[PublicAPI] +public class FlowSendHttpRequest : SendHttpRequestBase { + /// + /// A list of expected status codes to handle. + /// + [Input(Description = "A list of expected status codes to handle.", UIHint = InputUIHints.MultiText)] + public Input> ExpectedStatusCodes { get; set; } = default!; + + /// + protected override async ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response) + { + var expectedStatusCodes = ExpectedStatusCodes.TryGet(context) ?? new List(0); + var statusCode = (int)response.StatusCode; + var hasMatchingStatusCode = expectedStatusCodes.Contains(statusCode); + var outcome = hasMatchingStatusCode ? statusCode.ToString() : "Catch all"; + + await context.CompleteActivityWithOutcomesAsync(outcome); + } +} + +/// +/// Send an HTTP request. +/// +[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "HTTP Request", Kind = ActivityKind.Task)] +[PublicAPI] +public class SendHttpRequest : SendHttpRequestBase +{ + /// + /// A list of expected status codes to handle and the corresponding activity to execute when the status code matches. + /// + [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 + )] + public ICollection ExpectedStatusCodes { get; set; } = new List(); + + /// + /// 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; } + + /// + protected override async ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response) + { + var expectedStatusCodes = ExpectedStatusCodes; + var statusCode = (int)response.StatusCode; + var matchingCase = expectedStatusCodes.FirstOrDefault(x => x.StatusCode == statusCode); + var activity = matchingCase?.Activity ?? CatchAll; + + await context.ScheduleActivityAsync(activity, OnChildActivityCompletedAsync); + } + + private async ValueTask OnChildActivityCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) + { + await context.CompleteActivityAsync(); + } +} + +/// +/// A binding between an HTTP status code and an activity. +/// +public class HttpStatusCodeCase +{ + /// + /// Creates a new instance of the class. + /// + [JsonConstructor] + public HttpStatusCodeCase() + { + } + + /// + /// Creates a new instance of the class. + /// + public HttpStatusCodeCase(int statusCode, IActivity activity) + { + StatusCode = statusCode; + Activity = activity; + } + + /// + /// The HTTP status code to match. + /// + public int StatusCode { get; set; } + + /// + /// The activity to execute when the HTTP status code matches. + /// + public IActivity? Activity { get; set; } +} + +/// +/// Base class for activities that send HTTP requests. +/// +public abstract class SendHttpRequestBase : Activity +{ + /// + /// The URL to send the request to. + /// [Input] public Input Url { get; set; } = default!; /// @@ -72,15 +178,22 @@ public class SendHttpRequest : CodeActivity { var request = PrepareRequest(context); var httpClientFactory = context.GetRequiredService(); - var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequest)); + var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequestBase)); var cancellationToken = context.CancellationToken; var response = await httpClient.SendAsync(request, cancellationToken); var parsedContent = await ParseContentAsync(context, response.Content); context.Set(Result, response); context.Set(ParsedContent, parsedContent); + + await HandleResponseAsync(context, response); } + /// + /// Handles the response. + /// + protected abstract ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response); + private async Task ParseContentAsync(ActivityExecutionContext context, HttpContent httpContent) { if (!HasContent(httpContent)) @@ -90,10 +203,10 @@ public class SendHttpRequest : CodeActivity var targetType = ParsedContent.GetTargetType(context); var contentStream = await httpContent.ReadAsStreamAsync(cancellationToken); var contentType = httpContent.Headers.ContentType?.MediaType!; - + return await context.ParseContentAsync(contentStream, contentType, targetType, cancellationToken); } - + private static bool HasContent(HttpContent httpContent) => httpContent.Headers.ContentLength > 0; private HttpRequestMessage PrepareRequest(ActivityExecutionContext context) diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index 1f651e606..a6a5d2cef 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -48,7 +48,7 @@ public class HttpFeature : FeatureBase public Func HttpEndpointWorkflowFaultHandler { get; set; } = ActivatorUtilities.GetServiceOrCreateInstance; /// - /// A delegate to configure the used when by the activity. + /// A delegate to configure the used when by the activity. /// public Action HttpClient { get; set; } = (_, _) => { }; @@ -84,7 +84,7 @@ public class HttpFeature : FeatureBase Services.Configure(configureOptions); - var httpClientBuilder = Services.AddHttpClient(HttpClient); + var httpClientBuilder = Services.AddHttpClient(HttpClient); HttpClientBuilder(httpClientBuilder); Services diff --git a/src/modules/Elsa.Http/Handlers/DefaultHttpEndpointWorkflowFaultHandler.cs b/src/modules/Elsa.Http/Handlers/DefaultHttpEndpointWorkflowFaultHandler.cs index c014c075b..c98e1162e 100644 --- a/src/modules/Elsa.Http/Handlers/DefaultHttpEndpointWorkflowFaultHandler.cs +++ b/src/modules/Elsa.Http/Handlers/DefaultHttpEndpointWorkflowFaultHandler.cs @@ -15,6 +15,9 @@ public class DefaultHttpEndpointWorkflowFaultHandler : IHttpEndpointWorkflowFaul { private readonly SerializerOptionsProvider _serializerOptionsProvider; + /// + /// Initializes a new instance of the class. + /// public DefaultHttpEndpointWorkflowFaultHandler(SerializerOptionsProvider serializerOptionsProvider) { _serializerOptionsProvider = serializerOptionsProvider; diff --git a/src/modules/Elsa.Workflows.Core/Expressions/JsonExpression.cs b/src/modules/Elsa.Workflows.Core/Expressions/JsonExpression.cs index 45acdc71d..27c296f9c 100644 --- a/src/modules/Elsa.Workflows.Core/Expressions/JsonExpression.cs +++ b/src/modules/Elsa.Workflows.Core/Expressions/JsonExpression.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Elsa.Expressions.Contracts; using Elsa.Expressions.Models; +using Elsa.Workflows.Core.Serialization.Converters; namespace Elsa.Workflows.Core.Expressions; @@ -19,6 +20,7 @@ public class JsonExpression : JsonExpression public class JsonExpressionHandler : IExpressionHandler { + /// public ValueTask EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context) { var jsonExpression = (JsonExpression)expression; @@ -27,7 +29,10 @@ public class JsonExpressionHandler : IExpressionHandler if (string.IsNullOrWhiteSpace(value)) return ValueTask.FromResult(default(object?)); - var model = JsonSerializer.Deserialize(value, returnType); + var serializerOptions = new JsonSerializerOptions(); + serializerOptions.Converters.Add(new IntegerConverter()); + + var model = JsonSerializer.Deserialize(value, returnType, serializerOptions); return ValueTask.FromResult(model); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/IntegerArrayConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/IntegerArrayConverter.cs new file mode 100644 index 000000000..844c3d20d --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/IntegerArrayConverter.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Elsa.Workflows.Core.Serialization.Converters; + +/// +/// Converts integers to and from JSON strings. +/// +public class IntegerConverter : JsonConverter +{ + /// + public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Read the JSON string value and parse it as an integer + var value = reader.GetString()!; + var integer = int.Parse(value); + + // Return the parsed integer + return integer; + } + + /// + public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) + { + // Write the integer as a JSON number + writer.WriteNumberValue(value); + } +} \ No newline at end of file diff --git a/src/samples/console/Elsa.Samples.OutboundHttpRequests/Workflows/GetUsersWorkflow.cs b/src/samples/console/Elsa.Samples.OutboundHttpRequests/Workflows/GetUsersWorkflow.cs index bdd31fa34..4a274d74c 100644 --- a/src/samples/console/Elsa.Samples.OutboundHttpRequests/Workflows/GetUsersWorkflow.cs +++ b/src/samples/console/Elsa.Samples.OutboundHttpRequests/Workflows/GetUsersWorkflow.cs @@ -3,7 +3,6 @@ using Elsa.Http; using Elsa.Workflows.Core.Abstractions; using Elsa.Workflows.Core.Activities; using Elsa.Workflows.Core.Contracts; -using Elsa.Workflows.Core.Services; using Microsoft.AspNetCore.Http; namespace Elsa.Samples.OutboundHttpRequests.Workflows;