From 92bc89b10ca990376107fbed772bf0849ce6bcb1 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 7 Jul 2022 22:08:02 +0200 Subject: [PATCH] Add flow switch editor --- .../Elsa.WorkflowServer.Web/Program.cs | 1 + .../src/components.d.ts | 15 ++ .../flowchart/default-node-handler.ts | 11 +- .../flow-switch/flow-switch-editor.tsx | 151 ++++++++++++++++++ .../flow-switch/flow-switch-plugin.tsx | 34 ++++ .../flow-switch/flow-switch-port-provider.ts | 29 ++++ .../src/plugins/flow-switch/models.ts | 10 ++ .../plugins/switch/switch-port-provider.ts | 4 - .../src/services/default-port-provider.ts | 4 - .../src/services/plugin-registry.ts | 2 + .../src/services/port-provider.ts | 2 - .../Flowchart/Activities/FlowSwitch.cs | 3 +- 12 files changed, 247 insertions(+), 19 deletions(-) create mode 100644 src/designer/elsa-workflows-designer/src/plugins/flow-switch/flow-switch-editor.tsx create mode 100644 src/designer/elsa-workflows-designer/src/plugins/flow-switch/flow-switch-plugin.tsx create mode 100644 src/designer/elsa-workflows-designer/src/plugins/flow-switch/flow-switch-port-provider.ts create mode 100644 src/designer/elsa-workflows-designer/src/plugins/flow-switch/models.ts diff --git a/src/bundles/Elsa.WorkflowServer.Web/Program.cs b/src/bundles/Elsa.WorkflowServer.Web/Program.cs index 538d801ee..6c28eff47 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Program.cs +++ b/src/bundles/Elsa.WorkflowServer.Web/Program.cs @@ -40,6 +40,7 @@ services .AddActivity() .AddActivity() .AddActivity() + .AddActivity() .AddActivity() .AddActivity() .AddActivity() diff --git a/src/designer/elsa-workflows-designer/src/components.d.ts b/src/designer/elsa-workflows-designer/src/components.d.ts index 0537d8226..eeca66618 100644 --- a/src/designer/elsa-workflows-designer/src/components.d.ts +++ b/src/designer/elsa-workflows-designer/src/components.d.ts @@ -83,6 +83,9 @@ export namespace Components { interface ElsaDropdownInput { "inputContext": ActivityInputContext; } + interface ElsaFlowSwitchEditor { + "inputContext": ActivityInputContext; + } interface ElsaFlowchart { "addActivity": (args: AddActivityArgs) => Promise; "export": () => Promise; @@ -347,6 +350,12 @@ declare global { prototype: HTMLElsaDropdownInputElement; new (): HTMLElsaDropdownInputElement; }; + interface HTMLElsaFlowSwitchEditorElement extends Components.ElsaFlowSwitchEditor, HTMLStencilElement { + } + var HTMLElsaFlowSwitchEditorElement: { + prototype: HTMLElsaFlowSwitchEditorElement; + new (): HTMLElsaFlowSwitchEditorElement; + }; interface HTMLElsaFlowchartElement extends Components.ElsaFlowchart, HTMLStencilElement { } var HTMLElsaFlowchartElement: { @@ -593,6 +602,7 @@ declare global { "elsa-default-activity-template": HTMLElsaDefaultActivityTemplateElement; "elsa-dropdown-button": HTMLElsaDropdownButtonElement; "elsa-dropdown-input": HTMLElsaDropdownInputElement; + "elsa-flow-switch-editor": HTMLElsaFlowSwitchEditorElement; "elsa-flowchart": HTMLElsaFlowchartElement; "elsa-form-panel": HTMLElsaFormPanelElement; "elsa-input-control-switch": HTMLElsaInputControlSwitchElement; @@ -683,6 +693,9 @@ declare namespace LocalJSX { interface ElsaDropdownInput { "inputContext"?: ActivityInputContext; } + interface ElsaFlowSwitchEditor { + "inputContext"?: ActivityInputContext; + } interface ElsaFlowchart { "interactiveMode"?: boolean; "onActivityDeleted"?: (event: CustomEvent) => void; @@ -883,6 +896,7 @@ declare namespace LocalJSX { "elsa-default-activity-template": ElsaDefaultActivityTemplate; "elsa-dropdown-button": ElsaDropdownButton; "elsa-dropdown-input": ElsaDropdownInput; + "elsa-flow-switch-editor": ElsaFlowSwitchEditor; "elsa-flowchart": ElsaFlowchart; "elsa-form-panel": ElsaFormPanel; "elsa-input-control-switch": ElsaInputControlSwitch; @@ -939,6 +953,7 @@ declare module "@stencil/core" { "elsa-default-activity-template": LocalJSX.ElsaDefaultActivityTemplate & JSXBase.HTMLAttributes; "elsa-dropdown-button": LocalJSX.ElsaDropdownButton & JSXBase.HTMLAttributes; "elsa-dropdown-input": LocalJSX.ElsaDropdownInput & JSXBase.HTMLAttributes; + "elsa-flow-switch-editor": LocalJSX.ElsaFlowSwitchEditor & JSXBase.HTMLAttributes; "elsa-flowchart": LocalJSX.ElsaFlowchart & JSXBase.HTMLAttributes; "elsa-form-panel": LocalJSX.ElsaFormPanel & JSXBase.HTMLAttributes; "elsa-input-control-switch": LocalJSX.ElsaInputControlSwitch & JSXBase.HTMLAttributes; diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/default-node-handler.ts b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/default-node-handler.ts index 3351e4c20..b80fdd4cb 100644 --- a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/default-node-handler.ts +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/default-node-handler.ts @@ -16,18 +16,13 @@ export class DefaultNodeHandler implements ActivityNodeHandler { createDesignerNode(context: CreateUINodeContext): Node.Metadata { const {activityDescriptor, activity, x, y} = context; const provider = this.portProviderRegistry.get(activityDescriptor.activityType); + debugger; const providerContext: PortProviderContext = {activityDescriptor, activity}; - let inPorts = provider.getInboundPorts(providerContext).filter(x => x.mode == PortMode.Port); + const inPorts = [{name: 'In', displayName: null, mode: PortMode.Port}]; let outPorts = provider.getOutboundPorts(providerContext).filter(x => x.mode == PortMode.Port); - if (inPorts.length == 0) - inPorts = [{name: 'In', displayName: 'In', mode: PortMode.Port}]; - - if (inPorts.length == 1) - inPorts[0].displayName = null; - // In a flowchart, always add a Done port to connect the next node. - if(outPorts.length == 0) + if (outPorts.length == 0) outPorts = [{name: 'Done', displayName: 'Done', mode: PortMode.Port}]; if (outPorts.length == 1) diff --git a/src/designer/elsa-workflows-designer/src/plugins/flow-switch/flow-switch-editor.tsx b/src/designer/elsa-workflows-designer/src/plugins/flow-switch/flow-switch-editor.tsx new file mode 100644 index 000000000..86b7ea0e8 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/plugins/flow-switch/flow-switch-editor.tsx @@ -0,0 +1,151 @@ +import {Component, h, Prop, State, Watch} from "@stencil/core"; +import {camelCase} from 'lodash'; +import {ActivityInputContext} from "../../services/node-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({ + tag: 'elsa-flow-switch-editor', + shadow: false +}) +export class FlowSwitchEditor { + @Prop() inputContext: ActivityInputContext; + @State() private cases: Array = []; + private supportedSyntaxes: Array = [SyntaxNames.JavaScript, SyntaxNames.Literal]; + + @Watch('inputContext') + onInputContextChanged(value: ActivityInputContext){ + this.updateCases(); + } + + componentWillLoad() { + this.updateCases(); + } + + private updateCases(){ + const inputContext = this.inputContext; + const activity = this.inputContext.node; + const inputDescriptor = inputContext.inputDescriptor; + const propertyName = inputDescriptor.name; + const camelCasePropertyName = camelCase(propertyName); + this.cases = activity[camelCasePropertyName] || []; + } + + private onAddCaseClick() { + const caseName = `Case ${this.cases.length + 1}`; + const newCase: FlowSwitchCase = {label: caseName, condition: {type: SyntaxNames.JavaScript, value: ''}}; + this.cases = [...this.cases, newCase]; + this.updateActivity(); + } + + private onDeleteCaseClick(switchCase: FlowSwitchCase) { + this.cases = this.cases.filter(x => x != switchCase); + this.updateActivity(); + } + + private onCaseLabelChanged(e: Event, switchCase: FlowSwitchCase) { + switchCase.label = (e.currentTarget as HTMLInputElement).value.trim(); + this.updateActivity(); + } + + private onCaseExpressionChanged(e: CustomEvent, switchCase: FlowSwitchCase) { + switchCase.condition = {type: switchCase.condition.type, value: e.detail.value}; + this.updateActivity(); + } + + private onCaseSyntaxChanged(e: Event, switchCase: FlowSwitchCase) { + const select = e.currentTarget as HTMLSelectElement; + const syntax = select.value; + switchCase.condition = {...switchCase.condition, type: syntax}; + this.cases = [...this.cases]; + this.updateActivity(); + } + + private updateActivity = () => { + const inputContext = this.inputContext; + const activity = this.inputContext.node; + const inputDescriptor = inputContext.inputDescriptor; + const propertyName = inputDescriptor.name; + const camelCasePropertyName = camelCase(propertyName); + + activity[camelCasePropertyName] = this.cases; + this.inputContext.notifyInputChanged(); + }; + + render() { + const inputContext = this.inputContext; + const inputDescriptor = inputContext.inputDescriptor; + const displayName = inputDescriptor.displayName; + const cases = this.cases; + const supportedSyntaxes = this.supportedSyntaxes; + + return ( +
+
+ +
+ + + + + + + + + + {cases.map((switchCase, index) => { + const condition = switchCase.condition; + const expression = condition.value; + const syntax = condition.type; + const language = mapSyntaxToLanguage(condition.type); + + return ( + + + + + + ); + })} + +
NameExpression 
+ this.onCaseLabelChanged(e, switchCase)}/> + +
+ this.onCaseExpressionChanged(e, switchCase)} + /> +
+ +
+
+
+ +
+
+ +
+
+ ); + } +} diff --git a/src/designer/elsa-workflows-designer/src/plugins/flow-switch/flow-switch-plugin.tsx b/src/designer/elsa-workflows-designer/src/plugins/flow-switch/flow-switch-plugin.tsx new file mode 100644 index 000000000..b53413c99 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/plugins/flow-switch/flow-switch-plugin.tsx @@ -0,0 +1,34 @@ +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 {FlowSwitchPortProvider} from "./flow-switch-port-provider"; +import {ActivityIconSettings, getActivityIconCssClass} from "../../components/icons/activities"; + +@Service() +export class FlowSwitchPlugin implements Plugin { + static readonly ActivityTypeName: string = 'Elsa.FlowSwitch'; + + constructor() { + const activityTypeName = FlowSwitchPlugin.ActivityTypeName; + const inputControlRegistry = Container.get(InputControlRegistry); + const portProviderRegistry = Container.get(PortProviderRegistry); + const iconRegistry = Container.get(ActivityIconRegistry); + + inputControlRegistry.add('flow-switch-editor', c => ); + portProviderRegistry.add(activityTypeName, () => Container.get(FlowSwitchPortProvider)); + iconRegistry.add(FlowSwitchPlugin.ActivityTypeName, settings => ); + } +} + +const FlowSwitchIcon: FunctionalComponent = (settings) => ( + + + + + + + + +); diff --git a/src/designer/elsa-workflows-designer/src/plugins/flow-switch/flow-switch-port-provider.ts b/src/designer/elsa-workflows-designer/src/plugins/flow-switch/flow-switch-port-provider.ts new file mode 100644 index 000000000..ac29edcf7 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/plugins/flow-switch/flow-switch-port-provider.ts @@ -0,0 +1,29 @@ +import 'reflect-metadata'; +import {Service} from "typedi"; +import {Activity, Port, PortMode} from "../../models"; +import {FlowSwitchActivity} from "./models"; +import {PortProvider, PortProviderContext} from "../../services"; + +@Service() +export class FlowSwitchPortProvider implements PortProvider { + + getOutboundPorts(context: PortProviderContext): Array { + const activity = context.activity as FlowSwitchActivity; + + if (activity == null) + return []; + + const cases = activity.cases ?? []; + return cases.map(x => ({name: x.label, displayName: x.label, mode: PortMode.Port})); + } + + resolvePort(portName: string, context: PortProviderContext): Activity | Array { + debugger; + return null; + } + + assignPort(portName: string, activity: Activity, context: PortProviderContext) { + debugger; + return null; + } +} diff --git a/src/designer/elsa-workflows-designer/src/plugins/flow-switch/models.ts b/src/designer/elsa-workflows-designer/src/plugins/flow-switch/models.ts new file mode 100644 index 000000000..f7c4e00cb --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/plugins/flow-switch/models.ts @@ -0,0 +1,10 @@ +import {Activity, Expression} from "../../models"; + +export interface FlowSwitchCase { + label: string; + condition: Expression; +} + +export interface FlowSwitchActivity extends Activity { + cases: Array; +} diff --git a/src/designer/elsa-workflows-designer/src/plugins/switch/switch-port-provider.ts b/src/designer/elsa-workflows-designer/src/plugins/switch/switch-port-provider.ts index 2858935a9..b6a12f240 100644 --- a/src/designer/elsa-workflows-designer/src/plugins/switch/switch-port-provider.ts +++ b/src/designer/elsa-workflows-designer/src/plugins/switch/switch-port-provider.ts @@ -8,10 +8,6 @@ import {PortProvider, PortProviderContext} from "../../services"; @Service() export class SwitchPortProvider implements PortProvider { - getInboundPorts(context: PortProviderContext): Array { - return []; - } - getOutboundPorts(context: PortProviderContext): Array { const activity = context.activity as SwitchActivity; diff --git a/src/designer/elsa-workflows-designer/src/services/default-port-provider.ts b/src/designer/elsa-workflows-designer/src/services/default-port-provider.ts index 1bfe19222..b855efd98 100644 --- a/src/designer/elsa-workflows-designer/src/services/default-port-provider.ts +++ b/src/designer/elsa-workflows-designer/src/services/default-port-provider.ts @@ -6,10 +6,6 @@ import {Activity, Port} from "../models"; @Service() export class DefaultPortProvider implements PortProvider { - getInboundPorts(context: PortProviderContext): Array { - return []; - } - getOutboundPorts(context: PortProviderContext): Array { const {activityDescriptor} = context; return [...activityDescriptor.ports]; 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 b32d42087..389a014db 100644 --- a/src/designer/elsa-workflows-designer/src/services/plugin-registry.ts +++ b/src/designer/elsa-workflows-designer/src/services/plugin-registry.ts @@ -3,6 +3,7 @@ import {Container, Service} from "typedi"; import {SwitchPlugin} from "../plugins/switch/switch-plugin"; import {Plugin} from "../models"; import {SequencePlugin} from "../plugins/sequence/sequence-plugin"; +import {FlowSwitchPlugin} from "../plugins/flow-switch/flow-switch-plugin"; // A registry of plugins. @Service() @@ -12,6 +13,7 @@ export class PluginRegistry { constructor() { this.add(Container.get(SequencePlugin)); this.add(Container.get(SwitchPlugin)); + this.add(Container.get(FlowSwitchPlugin)); } public add(plugin: Plugin) { diff --git a/src/designer/elsa-workflows-designer/src/services/port-provider.ts b/src/designer/elsa-workflows-designer/src/services/port-provider.ts index 846b69a9a..d0fdb8f67 100644 --- a/src/designer/elsa-workflows-designer/src/services/port-provider.ts +++ b/src/designer/elsa-workflows-designer/src/services/port-provider.ts @@ -1,8 +1,6 @@ import {Activity, ActivityDescriptor, Port} from "../models"; export interface PortProvider { - getInboundPorts(context: PortProviderContext): Array; - getOutboundPorts(context: PortProviderContext): Array; resolvePort(portName: string, context: PortProviderContext): Activity | Array; diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowSwitch.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowSwitch.cs index a67f0a905..fa234426b 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowSwitch.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowSwitch.cs @@ -3,6 +3,7 @@ using Elsa.Expressions; using Elsa.Expressions.Models; using Elsa.Expressions.Services; using Elsa.Workflows.Core.Activities.Flowchart.Attributes; +using Elsa.Workflows.Core.Activities.Flowchart.Models; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; @@ -19,7 +20,7 @@ public class FlowSwitch : ActivityBase var matchingCase = await FindMatchingCaseAsync(context.ExpressionExecutionContext); var outcome = matchingCase?.Label ?? "Default"; - await context.CompleteActivityAsync(outcome); + await context.CompleteActivityAsync(new Outcome(outcome)); } private async Task FindMatchingCaseAsync(ExpressionExecutionContext context)