Add flow switch editor
This commit is contained in:
parent
fb2a4cc041
commit
92bc89b10c
|
|
@ -40,6 +40,7 @@ services
|
|||
.AddActivity<HttpEndpoint>()
|
||||
.AddActivity<Flowchart>()
|
||||
.AddActivity<FlowDecision>()
|
||||
.AddActivity<FlowSwitch>()
|
||||
.AddActivity<Elsa.Scheduling.Activities.Delay>()
|
||||
.AddActivity<Elsa.Scheduling.Activities.Timer>()
|
||||
.AddActivity<ForEach>()
|
||||
|
|
|
|||
|
|
@ -83,6 +83,9 @@ export namespace Components {
|
|||
interface ElsaDropdownInput {
|
||||
"inputContext": ActivityInputContext;
|
||||
}
|
||||
interface ElsaFlowSwitchEditor {
|
||||
"inputContext": ActivityInputContext;
|
||||
}
|
||||
interface ElsaFlowchart {
|
||||
"addActivity": (args: AddActivityArgs) => Promise<Activity>;
|
||||
"export": () => Promise<Activity>;
|
||||
|
|
@ -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<ActivityDeletedArgs>) => 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<HTMLElsaDefaultActivityTemplateElement>;
|
||||
"elsa-dropdown-button": LocalJSX.ElsaDropdownButton & JSXBase.HTMLAttributes<HTMLElsaDropdownButtonElement>;
|
||||
"elsa-dropdown-input": LocalJSX.ElsaDropdownInput & JSXBase.HTMLAttributes<HTMLElsaDropdownInputElement>;
|
||||
"elsa-flow-switch-editor": LocalJSX.ElsaFlowSwitchEditor & JSXBase.HTMLAttributes<HTMLElsaFlowSwitchEditorElement>;
|
||||
"elsa-flowchart": LocalJSX.ElsaFlowchart & JSXBase.HTMLAttributes<HTMLElsaFlowchartElement>;
|
||||
"elsa-form-panel": LocalJSX.ElsaFormPanel & JSXBase.HTMLAttributes<HTMLElsaFormPanelElement>;
|
||||
"elsa-input-control-switch": LocalJSX.ElsaInputControlSwitch & JSXBase.HTMLAttributes<HTMLElsaInputControlSwitchElement>;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<FlowSwitchCase> = [];
|
||||
private supportedSyntaxes: Array<string> = [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<MonacoValueChangedArgs>, 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 (
|
||||
<div>
|
||||
<div class="p-4">
|
||||
<label>{displayName}</label>
|
||||
</div>
|
||||
<table class="mt-1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-3/12">Name</th>
|
||||
<th class="w-8/12">Expression</th>
|
||||
<th class="w-1/12"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cases.map((switchCase, index) => {
|
||||
const condition = switchCase.condition;
|
||||
const expression = condition.value;
|
||||
const syntax = condition.type;
|
||||
const language = mapSyntaxToLanguage(condition.type);
|
||||
|
||||
return (
|
||||
<tr key={`case-${index}`}>
|
||||
<td class="py-2 pr-5">
|
||||
<input type="text" value={switchCase.label} onChange={e => this.onCaseLabelChanged(e, switchCase)}/>
|
||||
</td>
|
||||
<td class="py-2 pl-5">
|
||||
<div class="mt-1 relative rounded-md shadow-sm h-full">
|
||||
<elsa-monaco-editor
|
||||
key={`monaco-editor-${index}`}
|
||||
value={expression}
|
||||
language={language}
|
||||
singleLineMode={true}
|
||||
editorHeight="2.75em"
|
||||
padding="pt-1.5 pl-1 pr-28"
|
||||
onValueChanged={e => this.onCaseExpressionChanged(e, switchCase)}
|
||||
/>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center">
|
||||
<select onChange={e => this.onCaseSyntaxChanged(e, switchCase)} class="focus:ring-blue-500 focus:border-blue-500 h-full py-0 pl-2 pr-7 border-transparent bg-transparent text-gray-500 sm:text-sm rounded-md">
|
||||
{supportedSyntaxes.map(supportedSyntax => {
|
||||
const selected = supportedSyntax == syntax;
|
||||
return <option selected={selected}>{supportedSyntax}</option>;
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" onClick={() => this.onDeleteCaseClick(switchCase)} class="icon-button">
|
||||
<TrashBinButtonIcon/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="p-4">
|
||||
<button type="button" onClick={() => this.onAddCaseClick()} class="btn">
|
||||
<PlusButtonIcon/>
|
||||
Add Case
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 => <elsa-flow-switch-editor inputContext={c}/>);
|
||||
portProviderRegistry.add(activityTypeName, () => Container.get(FlowSwitchPortProvider));
|
||||
iconRegistry.add(FlowSwitchPlugin.ActivityTypeName, settings => <FlowSwitchIcon size={settings?.size}/>);
|
||||
}
|
||||
}
|
||||
|
||||
const FlowSwitchIcon: FunctionalComponent<ActivityIconSettings> = (settings) => (
|
||||
<svg class={getActivityIconCssClass(settings)} width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z"/>
|
||||
<polyline points="15 4 19 4 19 8"/>
|
||||
<line x1="14.75" y1="9.25" x2="19" y2="4"/>
|
||||
<line x1="5" y1="19" x2="9" y2="15"/>
|
||||
<polyline points="15 19 19 19 19 15"/>
|
||||
<line x1="5" y1="5" x2="19" y2="19"/>
|
||||
</svg>
|
||||
);
|
||||
|
|
@ -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<Port> {
|
||||
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<Activity> {
|
||||
debugger;
|
||||
return null;
|
||||
}
|
||||
|
||||
assignPort(portName: string, activity: Activity, context: PortProviderContext) {
|
||||
debugger;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import {Activity, Expression} from "../../models";
|
||||
|
||||
export interface FlowSwitchCase {
|
||||
label: string;
|
||||
condition: Expression;
|
||||
}
|
||||
|
||||
export interface FlowSwitchActivity extends Activity {
|
||||
cases: Array<FlowSwitchCase>;
|
||||
}
|
||||
|
|
@ -8,10 +8,6 @@ import {PortProvider, PortProviderContext} from "../../services";
|
|||
@Service()
|
||||
export class SwitchPortProvider implements PortProvider {
|
||||
|
||||
getInboundPorts(context: PortProviderContext): Array<Port> {
|
||||
return [];
|
||||
}
|
||||
|
||||
getOutboundPorts(context: PortProviderContext): Array<Port> {
|
||||
const activity = context.activity as SwitchActivity;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@ import {Activity, Port} from "../models";
|
|||
|
||||
@Service()
|
||||
export class DefaultPortProvider implements PortProvider {
|
||||
getInboundPorts(context: PortProviderContext): Array<Port> {
|
||||
return [];
|
||||
}
|
||||
|
||||
getOutboundPorts(context: PortProviderContext): Array<Port> {
|
||||
const {activityDescriptor} = context;
|
||||
return [...activityDescriptor.ports];
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import {Activity, ActivityDescriptor, Port} from "../models";
|
||||
|
||||
export interface PortProvider {
|
||||
getInboundPorts(context: PortProviderContext): Array<Port>;
|
||||
|
||||
getOutboundPorts(context: PortProviderContext): Array<Port>;
|
||||
|
||||
resolvePort(portName: string, context: PortProviderContext): Activity | Array<Activity>;
|
||||
|
|
|
|||
|
|
@ -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<FlowSwitchCase?> FindMatchingCaseAsync(ExpressionExecutionContext context)
|
||||
|
|
|
|||
Loading…
Reference in a new issue