Switch editor UI

This commit is contained in:
Sipke Schoorstra 2022-01-26 15:48:59 +01:00
parent ba05ce7ee7
commit 2a1ff4c392
29 changed files with 415 additions and 147 deletions

View file

@ -10,7 +10,7 @@ public class WriteHttpResponse : Activity
public Input<HttpStatusCode> StatusCode { get; set; } = new(HttpStatusCode.OK);
public Input<string?> Content { get; set; } = new("");
public override async ValueTask ExecuteAsync(ActivityExecutionContext context)
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var httpContextAccessor = context.GetRequiredService<IHttpContextAccessor>();
var httpContext = httpContextAccessor.HttpContext;

View file

@ -1,37 +1,61 @@
using Elsa.Attributes;
using Elsa.Contracts;
using Elsa.Expressions;
using Elsa.Models;
namespace Elsa.Activities.ControlFlow;
/// <summary>
/// The Switch activity is an approximation of the `switch` construct in C#.
/// When a case evaluates to true, the associated activity is then scheduled for execution.
/// </summary>
public class Switch : Activity
{
public ICollection<SwitchCase> Cases { get; set; } = new List<SwitchCase>();
[Input(UIHint = UIHints.SwitchEditor)] public ICollection<SwitchCase> Cases { get; set; } = new List<SwitchCase>();
public IActivity? Default { get; set; }
protected override void Execute(ActivityExecutionContext context)
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var firstMatch = Cases.FirstOrDefault(x => context.Get(x.Condition));
var matchingCase = await FindMatchingCaseAsync(context.ExpressionExecutionContext);
if (firstMatch == null)
if (matchingCase != null)
{
if (Default != null)
context.ScheduleActivity(Default);
if (matchingCase.Activity != null)
context.ScheduleActivity(matchingCase.Activity);
return;
}
if (firstMatch.Activity != null)
context.ScheduleActivity(firstMatch.Activity);
if (Default != null)
context.ScheduleActivity(Default);
}
private async Task<SwitchCase?> FindMatchingCaseAsync(ExpressionExecutionContext context)
{
var expressionEvaluator = context.GetRequiredService<IExpressionEvaluator>();
foreach (var switchCase in Cases)
{
var result = await expressionEvaluator.EvaluateAsync<bool>(switchCase.Condition, context);
if (result)
return switchCase;
}
return null;
}
}
/// <summary>
/// Represents an individual case of the <see cref="Switch"/> activity.
/// </summary>
public class SwitchCase
{
// ReSharper disable once EmptyConstructor
public SwitchCase()
{
}
public string Label { get; set; } = default!;
public Input<bool> Condition { get; set; } = new(false);
public IExpression Condition { get; set; } = new LiteralExpression(false);
public IActivity? Activity { get; set; }
}

View file

@ -0,0 +1,6 @@
namespace Elsa.Activities;
public static class UIHints
{
public const string SwitchEditor = "switch-editor";
}

View file

@ -11,10 +11,10 @@ public class SwitchActivityNodeResolver : IActivityNodeResolver
public IEnumerable<IActivity> GetPorts(IActivity activity)
{
var @switch = (Switch)activity;
var cases = @switch.Cases;
var cases = @switch.Cases.Where(x => x.Activity != null);
foreach (var @case in cases)
yield return @case.Activity;
yield return @case.Activity!;
if (@switch.Default != null)
yield return @switch.Default;

View file

@ -1,65 +1,72 @@
namespace Elsa.Attributes;
/// <summary>
/// Specifies various metadata about an activity's input property.
/// This metadata can be used by visual designers to control various aspects of the input editor control.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class InputAttribute : Attribute
{
/// <summary>
/// The technical name to use for the input property.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// A hint to workflow tooling what input control to use.
/// </summary>
public string? UIHint { get; set; }
/// <summary>
/// The user-friendly name of the activity property.
/// </summary>
public string? DisplayName { get; set; }
// /// <summary>
// /// A brief description about this property for workflow tooling to use when displaying activity editors.
// /// </summary>
// public string? Description { get; set; }
/// <summary>
/// A category to group this property with.
/// </summary>
public string? Category { get; set; }
/// <summary>
/// A value representing options specific to a given UI hint.
/// </summary>
public object? Options { get; set; }
/// <summary>
/// The type that provides options. If specified, this overrules any value specified via <see cref="Options"/>.
/// </summary>
public Type? OptionsProvider { get; set; }
/// <summary>
/// A value to order this property by. Properties are displayed in ascending order (lower appears before higher).
/// </summary>
public float Order { get; set; }
/// <summary>
/// The default value to set.
/// </summary>
public object? DefaultValue { get; set; }
/// <summary>
/// The type that provides a default value. When specified, the <see cref="DefaultValue"/> will be ignored.
/// </summary>
public Type? DefaultValueProvider { get; set; }
/// <summary>
/// The syntax to use by default when evaluating the value. Only used when the property definition doesn't have a syntax specified.
/// </summary>
public string? DefaultSyntax { get; set; }
/// <summary>
/// The syntax to use by default when evaluating the value. Only used when the property definition doesn't have a syntax specified.
/// </summary>
public string[]? SupportedSyntaxes { get; set; }
/// <summary>
/// A value indicating whether this property should be displayed but as read-only.
/// </summary>

View file

@ -6,8 +6,13 @@ namespace Elsa.Expressions;
public class LiteralExpression : IExpression
{
// ReSharper disable once UnusedMember.Global
public LiteralExpression()
{
}
public LiteralExpression(object? value) => Value = value;
public object? Value { get; }
public object? Value { get; set; }
}
public class LiteralExpression<T> : LiteralExpression

View file

@ -12,7 +12,7 @@ public abstract class Activity : IActivity
public string NodeType { get; set; }
public IDictionary<string, object> Metadata { get; set; } = new Dictionary<string, object>();
public virtual ValueTask ExecuteAsync(ActivityExecutionContext context)
protected virtual ValueTask ExecuteAsync(ActivityExecutionContext context)
{
Execute(context);
return ValueTask.CompletedTask;
@ -21,6 +21,8 @@ public abstract class Activity : IActivity
protected virtual void Execute(ActivityExecutionContext context)
{
}
ValueTask IActivity.ExecuteAsync(ActivityExecutionContext context) => ExecuteAsync(context);
}
public abstract class ActivityWithResult : Activity

View file

@ -10,5 +10,5 @@ public class DynamicActivity : Activity
public IDictionary<string, object?> Properties { get; set; } = new Dictionary<string, object?>();
public ExecuteActivityDelegate ExecuteHandler { get; set; } = _ => ValueTask.CompletedTask;
public override ValueTask ExecuteAsync(ActivityExecutionContext context) => ExecuteHandler(context);
protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => ExecuteHandler(context);
}

View file

@ -19,7 +19,7 @@ public class ActivityJsonConverter : JsonConverter<IActivity>
_activityRegistry = activityRegistry;
_serviceProvider = serviceProvider;
}
public override IActivity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (!JsonDocument.TryParseValue(ref reader, out var doc))
@ -36,7 +36,7 @@ public class ActivityJsonConverter : JsonConverter<IActivity>
var newOptions = new JsonSerializerOptions(options);
newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider));
var context = new ActivityConstructorContext(doc.RootElement, newOptions);
var activity = activityDescriptor.Constructor(context);

View file

@ -0,0 +1,57 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Contracts;
using Elsa.Management.Contracts;
using Elsa.Management.Models;
namespace Elsa.Management.Serialization.Converters;
/// <summary>
/// (De)serializes objects of type <see cref="IExpression"/>.
/// </summary>
public class ExpressionJsonConverter : JsonConverter<IExpression>
{
private readonly IExpressionSyntaxRegistry _expressionSyntaxRegistry;
public ExpressionJsonConverter(IExpressionSyntaxRegistry expressionSyntaxRegistry)
{
_expressionSyntaxRegistry = expressionSyntaxRegistry;
}
public override IExpression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (!JsonDocument.TryParseValue(ref reader, out var doc))
throw new JsonException("Failed to parse JsonDocument");
if (!doc.RootElement.TryGetProperty("type", out var syntaxElement))
throw new JsonException("Failed to extract expression type property");
// if (!doc.RootElement.TryGetProperty("expression", out var expressionElement))
// throw new JsonException("Failed to extract expression type property");
var syntax = syntaxElement.GetString()!;
var expressionSyntaxDescriptor = _expressionSyntaxRegistry.Find(syntax);
if (expressionSyntaxDescriptor == null)
throw new Exception($"Expression with syntax {syntax} not found in registry");
//var context = new ExpressionConstructorContext(expressionElement, options);
var context = new ExpressionConstructorContext(doc.RootElement, options);
var expression = expressionSyntaxDescriptor.CreateExpression(context);
return expression;
}
public override void Write(Utf8JsonWriter writer, IExpression value, JsonSerializerOptions options)
{
var expressionType = value.GetType();
var descriptor = _expressionSyntaxRegistry.Find(x => x.Type == expressionType);
if(descriptor == null)
throw new Exception($"Expression of type {expressionType} not found in registry");
var model = descriptor.CreateSerializableObject(new SerializableObjectConstructorContext(value));
JsonSerializer.Serialize(writer, model, options);
}
}

View file

@ -0,0 +1,22 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Contracts;
using Elsa.Management.Contracts;
namespace Elsa.Management.Serialization.Converters;
public class ExpressionJsonConverterFactory : JsonConverterFactory
{
private readonly IExpressionSyntaxRegistry _expressionSyntaxRegistry;
public ExpressionJsonConverterFactory(IExpressionSyntaxRegistry expressionSyntaxRegistry)
{
_expressionSyntaxRegistry = expressionSyntaxRegistry;
}
// This factory only creates converters when the type to convert is IExpression.
// The ExpressionJsonConverter will create concrete expression objects, which then uses regular serialization
public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(IExpression);
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new ExpressionJsonConverter(_expressionSyntaxRegistry);
}

View file

@ -13,19 +13,25 @@ public class WorkflowSerializerOptionsProvider
public JsonSerializerOptions CreateApiOptions() => CreateDefaultOptions();
public JsonSerializerOptions CreatePersistenceOptions() => CreateDefaultOptions(ReferenceHandler.Preserve);
public JsonSerializerOptions CreateDefaultOptions(ReferenceHandler? referenceHandler = default) => new()
public JsonSerializerOptions CreateDefaultOptions(ReferenceHandler? referenceHandler = default)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReferenceHandler = referenceHandler,
Converters =
var options = new JsonSerializerOptions()
{
Create<JsonStringEnumConverter>(),
Create<TypeJsonConverter>(),
Create<ActivityJsonConverterFactory>(),
Create<TriggerJsonConverterFactory>(),
Create<FlowchartJsonConverter>()
}
};
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReferenceHandler = referenceHandler,
Converters =
{
Create<JsonStringEnumConverter>(),
Create<TypeJsonConverter>(),
Create<ActivityJsonConverterFactory>(),
Create<TriggerJsonConverterFactory>(),
Create<ExpressionJsonConverterFactory>(),
Create<FlowchartJsonConverter>()
}
};
return options;
}
private T Create<T>() => ActivatorUtilities.CreateInstance<T>(_serviceProvider);
}

View file

@ -49,7 +49,7 @@ public class ActivityDescriber : IActivityDescriber
};
var properties = activityType.GetProperties();
var inputProperties = properties.Where(x => typeof(Input).IsAssignableFrom(x.PropertyType)).ToList();
var inputProperties = properties.Where(x => typeof(Input).IsAssignableFrom(x.PropertyType) || x.GetCustomAttribute<InputAttribute>() != null).ToList();
var outputProperties = properties.Where(x => typeof(Output).IsAssignableFrom(x.PropertyType)).ToList();
var isTrigger = activityType.IsAssignableTo(typeof(ITrigger));
@ -73,7 +73,7 @@ public class ActivityDescriber : IActivityDescriber
return ValueTask.FromResult(descriptor);
}
private IEnumerable<InputDescriptor> DescribeInputProperties(IEnumerable<PropertyInfo> properties)
{
foreach (var propertyInfo in properties)

View file

@ -143,6 +143,9 @@ export namespace Components {
"monacoLibPath": string;
"serverUrl": string;
}
interface ElsaSwitchEditor {
"inputContext": NodeInputContext;
}
interface ElsaToolbox {
"graph": Graph;
}
@ -331,6 +334,12 @@ declare global {
prototype: HTMLElsaStudioElement;
new (): HTMLElsaStudioElement;
};
interface HTMLElsaSwitchEditorElement extends Components.ElsaSwitchEditor, HTMLStencilElement {
}
var HTMLElsaSwitchEditorElement: {
prototype: HTMLElsaSwitchEditorElement;
new (): HTMLElsaSwitchEditorElement;
};
interface HTMLElsaToolboxElement extends Components.ElsaToolbox, HTMLStencilElement {
}
var HTMLElsaToolboxElement: {
@ -426,6 +435,7 @@ declare global {
"elsa-single-line-input": HTMLElsaSingleLineInputElement;
"elsa-slide-over-panel": HTMLElsaSlideOverPanelElement;
"elsa-studio": HTMLElsaStudioElement;
"elsa-switch-editor": HTMLElsaSwitchEditorElement;
"elsa-toolbox": HTMLElsaToolboxElement;
"elsa-toolbox-activities": HTMLElsaToolboxActivitiesElement;
"elsa-toolbox-triggers": HTMLElsaToolboxTriggersElement;
@ -566,6 +576,9 @@ declare namespace LocalJSX {
"monacoLibPath"?: string;
"serverUrl"?: string;
}
interface ElsaSwitchEditor {
"inputContext"?: NodeInputContext;
}
interface ElsaToolbox {
"graph"?: Graph;
}
@ -644,6 +657,7 @@ declare namespace LocalJSX {
"elsa-single-line-input": ElsaSingleLineInput;
"elsa-slide-over-panel": ElsaSlideOverPanel;
"elsa-studio": ElsaStudio;
"elsa-switch-editor": ElsaSwitchEditor;
"elsa-toolbox": ElsaToolbox;
"elsa-toolbox-activities": ElsaToolboxActivities;
"elsa-toolbox-triggers": ElsaToolboxTriggers;
@ -684,6 +698,7 @@ declare module "@stencil/core" {
"elsa-single-line-input": LocalJSX.ElsaSingleLineInput & JSXBase.HTMLAttributes<HTMLElsaSingleLineInputElement>;
"elsa-slide-over-panel": LocalJSX.ElsaSlideOverPanel & JSXBase.HTMLAttributes<HTMLElsaSlideOverPanelElement>;
"elsa-studio": LocalJSX.ElsaStudio & JSXBase.HTMLAttributes<HTMLElsaStudioElement>;
"elsa-switch-editor": LocalJSX.ElsaSwitchEditor & JSXBase.HTMLAttributes<HTMLElsaSwitchEditorElement>;
"elsa-toolbox": LocalJSX.ElsaToolbox & JSXBase.HTMLAttributes<HTMLElsaToolboxElement>;
"elsa-toolbox-activities": LocalJSX.ElsaToolboxActivities & JSXBase.HTMLAttributes<HTMLElsaToolboxActivitiesElement>;
"elsa-toolbox-triggers": LocalJSX.ElsaToolboxTriggers & JSXBase.HTMLAttributes<HTMLElsaToolboxTriggersElement>;

View file

@ -10,7 +10,7 @@ import {
TabChangedArgs,
TabDefinition
} from '../../../models';
import {InputDriverRegistry} from "../../../services/input-driver-registry";
import {InputDriverRegistry} from "../../../services";
import {Container} from "typedi";
import {NodeInputContext} from "../../../services/node-input-driver";
import {FormEntry} from "../../shared/forms/form-entry";
@ -63,6 +63,7 @@ export class ActivityPropertiesEditor {
node: activity,
nodeDescriptor: activityDescriptor,
inputDescriptor,
notifyInputChanged: () => this.activityUpdated.emit({activity}),
inputChanged: (v, s) => this.onPropertyEditorChanged(inputDescriptor, v, s)
};

View file

@ -4,6 +4,7 @@ import {enter, leave, toggle} from 'el-transition'
import {SyntaxSelectorIcon} from "../../icons/tooling/syntax-selector";
import {MonacoValueChangedArgs} from "../../shared/monaco-editor/monaco-editor";
import {Hint} from "../../shared/forms/hint";
import {mapSyntaxToLanguage} from "../../../utils";
export interface ExpressionChangedArs {
expression: string;
@ -119,7 +120,7 @@ export class InputControlSwitch {
private renderEditor = () => {
const selectedSyntax = this.syntax;
const monacoLanguage = this.mapSyntaxToLanguage(selectedSyntax);
const monacoLanguage = mapSyntaxToLanguage(selectedSyntax);
const value = this.expression;
const showMonaco = !!selectedSyntax && selectedSyntax != 'Literal' && !!this.supportedSyntaxes.find(x => x === selectedSyntax);
const expressionEditorClass = showMonaco ? 'block' : 'hidden';
@ -144,19 +145,6 @@ export class InputControlSwitch {
);
}
private mapSyntaxToLanguage = (syntax: string): string => {
switch (syntax) {
case 'Json':
return 'json';
case 'JavaScript':
return 'javascript';
case 'Liquid':
return 'handlebars';
default:
return 'plaintext';
}
};
private toggleContextMenu() {
toggle(this.contextMenu);
}

View file

@ -64,6 +64,7 @@ export class TriggerPropertiesEditor {
node: trigger,
nodeDescriptor: triggerDescriptor,
inputDescriptor,
notifyInputChanged: () => this.triggerUpdated.emit({trigger}),
inputChanged: (v, s) => this.onPropertyEditorChanged(inputDescriptor, v, s)
};

View file

@ -66,8 +66,8 @@ export class WorkflowPropertiesEditor {
const publication = workflow.publication;
const workflowDetails = {
'ID': identity.id,
'Definition ID': identity.definitionId,
'Version ID': identity.id,
'Version': identity.version,
'Status': publication.isPublished ? 'Published' : 'Draft'
};

View file

@ -0,0 +1,12 @@
import {FunctionalComponent, h} from "@stencil/core";
export const PlusButtonIcon: FunctionalComponent = () =>
<svg
class="-ml-1 mr-2 h-5 w-5"
width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="transparent" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>;

View file

@ -0,0 +1,13 @@
import {FunctionalComponent, h} from "@stencil/core";
export const TrashBinButtonIcon: FunctionalComponent = () =>
<svg
class="h-5 w-5 text-gray-500"
width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="transparent" stroke-linecap="round"
stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<line x1="10" y1="11" x2="10" y2="17"/>
<line x1="14" y1="11" x2="14" y2="17"/>
</svg>;

View file

@ -6,11 +6,13 @@ export interface FormEntryProps {
fieldId: string;
key?: string;
hint?: string;
padding?: string;
}
export const FormEntry: FunctionalComponent<FormEntryProps> = ({label, hint, fieldId, key}, children) => {
export const FormEntry: FunctionalComponent<FormEntryProps> = ({label, hint, fieldId, key, padding}, children) => {
padding ??= 'p-4';
return (
<div class="p-4">
<div class={padding}>
<label htmlFor={fieldId}>
{label}
</label>

View file

@ -4,7 +4,11 @@
/* Controls */
.btn {
@apply px-4 py-2 bg-blue-600 text-white rounded font-sans;
@apply inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 mt-2;
}
.icon-button {
@apply h-5 w-5 mx-auto outline-none focus:outline-none;
}
/* Activities */
@ -64,35 +68,35 @@ textarea {
}
/* Tables */
elsa-modal-dialog table {
table {
@apply min-w-full;
}
elsa-modal-dialog thead tr {
thead tr {
@apply border-t border-gray-200;
}
elsa-modal-dialog thead tr th {
@apply px-6 py-3 border-b border-gray-200 bg-gray-50 text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider;
thead tr th {
@apply px-6 py-3 border-b border-gray-200 bg-gray-50 text-xs leading-4 font-medium text-left text-gray-500 uppercase tracking-wider;
}
elsa-modal-dialog thead tr th.optional, elsa-modal-dialog tbody tr td.optional {
thead tr th.optional, tbody tr td.optional {
@apply hidden md:table-cell;
}
elsa-modal-dialog thead tr th.align-right, elsa-modal-dialog tbody tr td.align-right {
table thead tr th.align-right, tbody tr td.align-right {
@apply text-right;
}
elsa-modal-dialog table tbody {
tbody {
@apply bg-white divide-y divide-gray-100;
}
elsa-modal-dialog table tbody tr td {
tbody tr td {
@apply px-6 py-3 text-sm leading-5 text-gray-500 font-medium;
}
elsa-modal-dialog table tbody tr td:first-child {
tbody tr td:first-child {
@apply px-6 py-3 whitespace-nowrap text-sm leading-5 font-medium text-gray-900;
}

View file

@ -90,5 +90,6 @@ export interface RegisterLocation {
export enum SyntaxNames {
Literal = 'Literal',
JavaScript = 'JavaScript',
Liquid = 'Liquid',
Json = 'Json'
}

View file

@ -2,9 +2,9 @@ import {Activity, ActivityInput, Expression} from "../../models";
export interface SwitchCase {
label: string;
condition: ActivityInput
condition: Expression;
}
export interface SwitchActivity extends Activity {
cases: Array<SwitchCase>;
cases: ActivityInput;
}

View file

@ -0,0 +1,142 @@
import {Component, h, Prop, State} from "@stencil/core";
import {camelCase} from 'lodash';
import {NodeInputContext} from "../../services/node-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";
@Component({
tag: 'elsa-switch-editor',
shadow: false
})
export class SwitchEditor {
@Prop() public inputContext: NodeInputContext;
@State() private cases: Array<SwitchCase> = [];
private supportedSyntaxes: Array<string> = [SyntaxNames.JavaScript, SyntaxNames.Literal];
public componentWillLoad() {
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] || [];
}
public 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">&nbsp;</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>
);
}
onAddCaseClick() {
const caseName = `Case ${this.cases.length + 1}`;
const newCase: SwitchCase = {label: caseName, condition: {type: SyntaxNames.JavaScript, value: ''}};
this.cases = [...this.cases, newCase];
this.updateActivity();
}
onDeleteCaseClick(switchCase: SwitchCase) {
this.cases = this.cases.filter(x => x != switchCase);
this.updateActivity();
}
private onCaseLabelChanged(e: Event, switchCase: SwitchCase) {
switchCase.label = (e.currentTarget as HTMLInputElement).value.trim();
this.updateActivity();
}
private onCaseExpressionChanged(e: CustomEvent<MonacoValueChangedArgs>, switchCase: SwitchCase) {
switchCase.condition = {type: switchCase.condition.type, value: e.detail.value};
this.updateActivity();
}
private onCaseSyntaxChanged(e: Event, switchCase: SwitchCase) {
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();
};
}

View file

@ -1,74 +0,0 @@
import 'reflect-metadata';
import {Container, Service} from "typedi";
import {EventBus} from "../../services";
import {ConnectionCreatedEventArgs, FlowchartEvents} from "../../components/activities/flowchart/events";
import {Plugin, Port} from "../../models";
import {NodeHandlerRegistry} from "../../components/activities/flowchart/node-handler-registry";
import {SwitchNodeHandler} from "./switch-node-handler";
import {SwitchActivity, SwitchCase} from "./models";
import {PortManager} from "@antv/x6/lib/model/port";
import PortMetadata = PortManager.PortMetadata;
@Service()
export class SwitchPlugin implements Plugin {
private static readonly ActivityTypeName = 'ControlFlow.Switch';
constructor() {
const eventBus = Container.get(EventBus);
const nodeHandlerRegistry = Container.get(NodeHandlerRegistry);
eventBus.on(FlowchartEvents.ConnectionCreated, this.onConnectionCreated);
nodeHandlerRegistry.add('ControlFlow.Switch', () => Container.get(SwitchNodeHandler));
}
private onConnectionCreated = (e: ConnectionCreatedEventArgs) => {
if (e.sourceActivity.nodeType !== SwitchPlugin.ActivityTypeName)
return;
const graph = e.graph;
// Remove created edge.
graph.removeEdge(e.edge);
const switchActivity = e.sourceActivity as SwitchActivity;
const currentCases = switchActivity.cases || [];
const newLabel = `Case ${currentCases.length + 1}`;
// Create Switch Case.
const switchCase: SwitchCase = {
label: newLabel,
condition: {type: 'Boolean', expression: {type: 'JavaScript', value: ''}}
}
currentCases.push(switchCase);
switchActivity.cases = currentCases;
// Update source node with new port.
const newPort: PortMetadata = {
id: switchCase.label,
group: 'out',
attrs: {
text: {
text: switchCase.label
}
}
}
const sourceNode = e.sourceNode;
sourceNode.addPort(newPort);
// Create new connection between new port and target node.
const targetNode = e.targetNode;
const targetPort = e.connection.targetPort;
const edge = graph.createEdge({
source: sourceNode,
sourcePort: switchCase.label,
target: targetNode,
targetPort: targetPort
});
graph.addEdge(edge);
}
}

View file

@ -0,0 +1,20 @@
import 'reflect-metadata';
import {h} from '@stencil/core';
import {Container, Service} from "typedi";
import {InputControlRegistry} from "../../services";
import {Plugin} from "../../models";
import {NodeHandlerRegistry} from "../../components/activities/flowchart/node-handler-registry";
import {SwitchNodeHandler} from "./switch-node-handler";
@Service()
export class SwitchPlugin implements Plugin {
constructor() {
const inputControlRegistry = Container.get(InputControlRegistry);
const nodeHandlerRegistry = Container.get(NodeHandlerRegistry);
inputControlRegistry.add('switch-editor', c => <elsa-switch-editor inputContext={c} />)
nodeHandlerRegistry.add('ControlFlow.Switch', () => Container.get(SwitchNodeHandler));
}
}

View file

@ -4,6 +4,7 @@ export interface NodeInputContext {
node: Node;
nodeDescriptor: NodeDescriptor;
inputDescriptor: InputDescriptor;
notifyInputChanged: () => void;
inputChanged: (value: any, syntax: string) => void;
}

View file

@ -37,6 +37,19 @@ export const getVersionOptionsString = (versionOptions?: VersionOptions) => {
: versionOptions.version.toString();
};
export const mapSyntaxToLanguage = (syntax: string): string => {
switch (syntax) {
case 'Json':
return 'json';
case 'JavaScript':
return 'javascript';
case 'Liquid':
return 'handlebars';
default:
return 'plaintext';
}
};
export const getInputPropertyName = (inputContext: NodeInputContext) => {
const inputProperty = inputContext.inputDescriptor;
const propertyName = inputProperty.name;