From 2a1ff4c3920a3d0ededb24f9744f2ff22bb3f7bd Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 26 Jan 2022 15:48:59 +0100 Subject: [PATCH] Switch editor UI --- .../Elsa.Activities.Http/WriteHttpResponse.cs | 2 +- .../Activities/ControlFlow/Switch.cs | 44 ++++-- src/core/Elsa.Core/Activities/UIHints.cs | 6 + .../SwitchActivityNodeResolver.cs | 4 +- .../Elsa.Core/Attributes/InputAttribute.cs | 31 ++-- .../Expressions/LiteralExpression.cs | 7 +- src/core/Elsa.Core/Models/Activity.cs | 4 +- src/core/Elsa.Core/Models/DynamicActivity.cs | 2 +- .../Converters/ActivityJsonConverter.cs | 4 +- .../Converters/ExpressionJsonConverter.cs | 57 +++++++ .../ExpressionJsonConverterFactory.cs | 22 +++ .../WorkflowSerializerOptionsProvider.cs | 28 ++-- .../Services/ActivityDescriber.cs | 4 +- .../src/components.d.ts | 15 ++ .../activity-properties-editor.tsx | 3 +- .../input-control-switch.tsx | 16 +- .../trigger-properties-editor.tsx | 1 + .../workflow-properties-editor.tsx | 2 +- .../src/components/icons/buttons/plus.tsx | 12 ++ .../components/icons/buttons/trash-bin.tsx | 13 ++ .../components/shared/forms/form-entry.tsx | 6 +- .../src/global/tailwind.css | 24 +-- .../src/models/core.ts | 1 + .../src/plugins/switch/models.ts | 4 +- .../src/plugins/switch/switch-editor.tsx | 142 ++++++++++++++++++ .../src/plugins/switch/switch-plugin.ts | 74 --------- .../src/plugins/switch/switch-plugin.tsx | 20 +++ .../src/services/node-input-driver.ts | 1 + .../src/utils/utils.ts | 13 ++ 29 files changed, 415 insertions(+), 147 deletions(-) create mode 100644 src/core/Elsa.Core/Activities/UIHints.cs create mode 100644 src/core/Elsa.Management/Serialization/Converters/ExpressionJsonConverter.cs create mode 100644 src/core/Elsa.Management/Serialization/Converters/ExpressionJsonConverterFactory.cs create mode 100644 src/designer/elsa-workflows-designer/src/components/icons/buttons/plus.tsx create mode 100644 src/designer/elsa-workflows-designer/src/components/icons/buttons/trash-bin.tsx create mode 100644 src/designer/elsa-workflows-designer/src/plugins/switch/switch-editor.tsx delete mode 100644 src/designer/elsa-workflows-designer/src/plugins/switch/switch-plugin.ts create mode 100644 src/designer/elsa-workflows-designer/src/plugins/switch/switch-plugin.tsx diff --git a/src/activities/Elsa.Activities.Http/WriteHttpResponse.cs b/src/activities/Elsa.Activities.Http/WriteHttpResponse.cs index 4d41bf7c2..75be7fd29 100644 --- a/src/activities/Elsa.Activities.Http/WriteHttpResponse.cs +++ b/src/activities/Elsa.Activities.Http/WriteHttpResponse.cs @@ -10,7 +10,7 @@ public class WriteHttpResponse : Activity public Input StatusCode { get; set; } = new(HttpStatusCode.OK); public Input Content { get; set; } = new(""); - public override async ValueTask ExecuteAsync(ActivityExecutionContext context) + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { var httpContextAccessor = context.GetRequiredService(); var httpContext = httpContextAccessor.HttpContext; diff --git a/src/core/Elsa.Core/Activities/ControlFlow/Switch.cs b/src/core/Elsa.Core/Activities/ControlFlow/Switch.cs index c2f53016a..7bbd60a6e 100644 --- a/src/core/Elsa.Core/Activities/ControlFlow/Switch.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/Switch.cs @@ -1,37 +1,61 @@ +using Elsa.Attributes; using Elsa.Contracts; +using Elsa.Expressions; using Elsa.Models; namespace Elsa.Activities.ControlFlow; +/// +/// 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. +/// public class Switch : Activity { - public ICollection Cases { get; set; } = new List(); + [Input(UIHint = UIHints.SwitchEditor)] public ICollection Cases { get; set; } = new List(); 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 FindMatchingCaseAsync(ExpressionExecutionContext context) + { + var expressionEvaluator = context.GetRequiredService(); + + foreach (var switchCase in Cases) + { + var result = await expressionEvaluator.EvaluateAsync(switchCase.Condition, context); + + if (result) + return switchCase; + } + + return null; } } +/// +/// Represents an individual case of the activity. +/// public class SwitchCase { + // ReSharper disable once EmptyConstructor public SwitchCase() { } public string Label { get; set; } = default!; - public Input Condition { get; set; } = new(false); + public IExpression Condition { get; set; } = new LiteralExpression(false); public IActivity? Activity { get; set; } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/UIHints.cs b/src/core/Elsa.Core/Activities/UIHints.cs new file mode 100644 index 000000000..8c4d131b8 --- /dev/null +++ b/src/core/Elsa.Core/Activities/UIHints.cs @@ -0,0 +1,6 @@ +namespace Elsa.Activities; + +public static class UIHints +{ + public const string SwitchEditor = "switch-editor"; +} \ No newline at end of file diff --git a/src/core/Elsa.Core/ActivityNodeResolvers/SwitchActivityNodeResolver.cs b/src/core/Elsa.Core/ActivityNodeResolvers/SwitchActivityNodeResolver.cs index 539850fdd..7ef1b5fd5 100644 --- a/src/core/Elsa.Core/ActivityNodeResolvers/SwitchActivityNodeResolver.cs +++ b/src/core/Elsa.Core/ActivityNodeResolvers/SwitchActivityNodeResolver.cs @@ -11,10 +11,10 @@ public class SwitchActivityNodeResolver : IActivityNodeResolver public IEnumerable 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; diff --git a/src/core/Elsa.Core/Attributes/InputAttribute.cs b/src/core/Elsa.Core/Attributes/InputAttribute.cs index 7acc9e7ba..40fd569d7 100644 --- a/src/core/Elsa.Core/Attributes/InputAttribute.cs +++ b/src/core/Elsa.Core/Attributes/InputAttribute.cs @@ -1,65 +1,72 @@ namespace Elsa.Attributes; +/// +/// 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. +/// [AttributeUsage(AttributeTargets.Property)] public class InputAttribute : Attribute { + /// + /// The technical name to use for the input property. + /// public string? Name { get; set; } - + /// /// A hint to workflow tooling what input control to use. /// public string? UIHint { get; set; } - + /// /// The user-friendly name of the activity property. /// public string? DisplayName { get; set; } - + // /// // /// A brief description about this property for workflow tooling to use when displaying activity editors. // /// // public string? Description { get; set; } - + /// /// A category to group this property with. /// public string? Category { get; set; } - + /// /// A value representing options specific to a given UI hint. /// public object? Options { get; set; } - + /// /// The type that provides options. If specified, this overrules any value specified via . /// public Type? OptionsProvider { get; set; } - + /// /// A value to order this property by. Properties are displayed in ascending order (lower appears before higher). /// public float Order { get; set; } - + /// /// The default value to set. /// public object? DefaultValue { get; set; } - + /// /// The type that provides a default value. When specified, the will be ignored. /// public Type? DefaultValueProvider { get; set; } - + /// /// The syntax to use by default when evaluating the value. Only used when the property definition doesn't have a syntax specified. /// public string? DefaultSyntax { get; set; } - + /// /// The syntax to use by default when evaluating the value. Only used when the property definition doesn't have a syntax specified. /// public string[]? SupportedSyntaxes { get; set; } - + /// /// A value indicating whether this property should be displayed but as read-only. /// diff --git a/src/core/Elsa.Core/Expressions/LiteralExpression.cs b/src/core/Elsa.Core/Expressions/LiteralExpression.cs index ac32f81b1..65ede3bf7 100644 --- a/src/core/Elsa.Core/Expressions/LiteralExpression.cs +++ b/src/core/Elsa.Core/Expressions/LiteralExpression.cs @@ -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 : LiteralExpression diff --git a/src/core/Elsa.Core/Models/Activity.cs b/src/core/Elsa.Core/Models/Activity.cs index b43bb2e28..ed3f4ba29 100644 --- a/src/core/Elsa.Core/Models/Activity.cs +++ b/src/core/Elsa.Core/Models/Activity.cs @@ -12,7 +12,7 @@ public abstract class Activity : IActivity public string NodeType { get; set; } public IDictionary Metadata { get; set; } = new Dictionary(); - 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 diff --git a/src/core/Elsa.Core/Models/DynamicActivity.cs b/src/core/Elsa.Core/Models/DynamicActivity.cs index d6c4a8852..8c2438970 100644 --- a/src/core/Elsa.Core/Models/DynamicActivity.cs +++ b/src/core/Elsa.Core/Models/DynamicActivity.cs @@ -10,5 +10,5 @@ public class DynamicActivity : Activity public IDictionary Properties { get; set; } = new Dictionary(); public ExecuteActivityDelegate ExecuteHandler { get; set; } = _ => ValueTask.CompletedTask; - public override ValueTask ExecuteAsync(ActivityExecutionContext context) => ExecuteHandler(context); + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => ExecuteHandler(context); } \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/Converters/ActivityJsonConverter.cs b/src/core/Elsa.Management/Serialization/Converters/ActivityJsonConverter.cs index eccbdfc7c..0d2423f9f 100644 --- a/src/core/Elsa.Management/Serialization/Converters/ActivityJsonConverter.cs +++ b/src/core/Elsa.Management/Serialization/Converters/ActivityJsonConverter.cs @@ -19,7 +19,7 @@ public class ActivityJsonConverter : JsonConverter _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 var newOptions = new JsonSerializerOptions(options); newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider)); - + var context = new ActivityConstructorContext(doc.RootElement, newOptions); var activity = activityDescriptor.Constructor(context); diff --git a/src/core/Elsa.Management/Serialization/Converters/ExpressionJsonConverter.cs b/src/core/Elsa.Management/Serialization/Converters/ExpressionJsonConverter.cs new file mode 100644 index 000000000..f1ce9a162 --- /dev/null +++ b/src/core/Elsa.Management/Serialization/Converters/ExpressionJsonConverter.cs @@ -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; + +/// +/// (De)serializes objects of type . +/// +public class ExpressionJsonConverter : JsonConverter +{ + 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); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/Converters/ExpressionJsonConverterFactory.cs b/src/core/Elsa.Management/Serialization/Converters/ExpressionJsonConverterFactory.cs new file mode 100644 index 000000000..1b84e4ae3 --- /dev/null +++ b/src/core/Elsa.Management/Serialization/Converters/ExpressionJsonConverterFactory.cs @@ -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); +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Serialization/WorkflowSerializerOptionsProvider.cs b/src/core/Elsa.Management/Serialization/WorkflowSerializerOptionsProvider.cs index 886b3cee6..b75ef4d78 100644 --- a/src/core/Elsa.Management/Serialization/WorkflowSerializerOptionsProvider.cs +++ b/src/core/Elsa.Management/Serialization/WorkflowSerializerOptionsProvider.cs @@ -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(), - Create(), - Create(), - Create(), - Create() - } - }; + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReferenceHandler = referenceHandler, + Converters = + { + Create(), + Create(), + Create(), + Create(), + Create(), + Create() + } + }; + + return options; + } private T Create() => ActivatorUtilities.CreateInstance(_serviceProvider); } \ No newline at end of file diff --git a/src/core/Elsa.Management/Services/ActivityDescriber.cs b/src/core/Elsa.Management/Services/ActivityDescriber.cs index 982fb8ed0..c77a54466 100644 --- a/src/core/Elsa.Management/Services/ActivityDescriber.cs +++ b/src/core/Elsa.Management/Services/ActivityDescriber.cs @@ -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() != 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 DescribeInputProperties(IEnumerable properties) { foreach (var propertyInfo in properties) diff --git a/src/designer/elsa-workflows-designer/src/components.d.ts b/src/designer/elsa-workflows-designer/src/components.d.ts index 62d06928e..5cbe582a9 100644 --- a/src/designer/elsa-workflows-designer/src/components.d.ts +++ b/src/designer/elsa-workflows-designer/src/components.d.ts @@ -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; "elsa-slide-over-panel": LocalJSX.ElsaSlideOverPanel & JSXBase.HTMLAttributes; "elsa-studio": LocalJSX.ElsaStudio & JSXBase.HTMLAttributes; + "elsa-switch-editor": LocalJSX.ElsaSwitchEditor & JSXBase.HTMLAttributes; "elsa-toolbox": LocalJSX.ElsaToolbox & JSXBase.HTMLAttributes; "elsa-toolbox-activities": LocalJSX.ElsaToolboxActivities & JSXBase.HTMLAttributes; "elsa-toolbox-triggers": LocalJSX.ElsaToolboxTriggers & JSXBase.HTMLAttributes; diff --git a/src/designer/elsa-workflows-designer/src/components/designer/activity-properties-editor/activity-properties-editor.tsx b/src/designer/elsa-workflows-designer/src/components/designer/activity-properties-editor/activity-properties-editor.tsx index 93c26eeaa..f5699026e 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/activity-properties-editor/activity-properties-editor.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/activity-properties-editor/activity-properties-editor.tsx @@ -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) }; diff --git a/src/designer/elsa-workflows-designer/src/components/designer/input-control-switch/input-control-switch.tsx b/src/designer/elsa-workflows-designer/src/components/designer/input-control-switch/input-control-switch.tsx index f32dccf95..28f378c59 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/input-control-switch/input-control-switch.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/input-control-switch/input-control-switch.tsx @@ -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); } diff --git a/src/designer/elsa-workflows-designer/src/components/designer/trigger-properties-editor/trigger-properties-editor.tsx b/src/designer/elsa-workflows-designer/src/components/designer/trigger-properties-editor/trigger-properties-editor.tsx index 346a684dc..a6b7a4f87 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/trigger-properties-editor/trigger-properties-editor.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/trigger-properties-editor/trigger-properties-editor.tsx @@ -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) }; diff --git a/src/designer/elsa-workflows-designer/src/components/designer/workflow-properties-editor/workflow-properties-editor.tsx b/src/designer/elsa-workflows-designer/src/components/designer/workflow-properties-editor/workflow-properties-editor.tsx index 9c2547140..b0fd3160e 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/workflow-properties-editor/workflow-properties-editor.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/workflow-properties-editor/workflow-properties-editor.tsx @@ -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' }; diff --git a/src/designer/elsa-workflows-designer/src/components/icons/buttons/plus.tsx b/src/designer/elsa-workflows-designer/src/components/icons/buttons/plus.tsx new file mode 100644 index 000000000..0461c7ae0 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/icons/buttons/plus.tsx @@ -0,0 +1,12 @@ +import {FunctionalComponent, h} from "@stencil/core"; + +export const PlusButtonIcon: FunctionalComponent = () => + + + + + ; diff --git a/src/designer/elsa-workflows-designer/src/components/icons/buttons/trash-bin.tsx b/src/designer/elsa-workflows-designer/src/components/icons/buttons/trash-bin.tsx new file mode 100644 index 000000000..72958fffe --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/icons/buttons/trash-bin.tsx @@ -0,0 +1,13 @@ +import {FunctionalComponent, h} from "@stencil/core"; + +export const TrashBinButtonIcon: FunctionalComponent = () => + + + + + + ; diff --git a/src/designer/elsa-workflows-designer/src/components/shared/forms/form-entry.tsx b/src/designer/elsa-workflows-designer/src/components/shared/forms/form-entry.tsx index 31d1f81fe..0ed5f13f0 100644 --- a/src/designer/elsa-workflows-designer/src/components/shared/forms/form-entry.tsx +++ b/src/designer/elsa-workflows-designer/src/components/shared/forms/form-entry.tsx @@ -6,11 +6,13 @@ export interface FormEntryProps { fieldId: string; key?: string; hint?: string; + padding?: string; } -export const FormEntry: FunctionalComponent = ({label, hint, fieldId, key}, children) => { +export const FormEntry: FunctionalComponent = ({label, hint, fieldId, key, padding}, children) => { + padding ??= 'p-4'; return ( -
+
diff --git a/src/designer/elsa-workflows-designer/src/global/tailwind.css b/src/designer/elsa-workflows-designer/src/global/tailwind.css index ddc7fde70..20bf2f199 100644 --- a/src/designer/elsa-workflows-designer/src/global/tailwind.css +++ b/src/designer/elsa-workflows-designer/src/global/tailwind.css @@ -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; } diff --git a/src/designer/elsa-workflows-designer/src/models/core.ts b/src/designer/elsa-workflows-designer/src/models/core.ts index 9bf7cc02a..e9bd1b24d 100644 --- a/src/designer/elsa-workflows-designer/src/models/core.ts +++ b/src/designer/elsa-workflows-designer/src/models/core.ts @@ -90,5 +90,6 @@ export interface RegisterLocation { export enum SyntaxNames { Literal = 'Literal', JavaScript = 'JavaScript', + Liquid = 'Liquid', Json = 'Json' } diff --git a/src/designer/elsa-workflows-designer/src/plugins/switch/models.ts b/src/designer/elsa-workflows-designer/src/plugins/switch/models.ts index d18fe6020..e0a2b2d30 100644 --- a/src/designer/elsa-workflows-designer/src/plugins/switch/models.ts +++ b/src/designer/elsa-workflows-designer/src/plugins/switch/models.ts @@ -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; + cases: ActivityInput; } diff --git a/src/designer/elsa-workflows-designer/src/plugins/switch/switch-editor.tsx b/src/designer/elsa-workflows-designer/src/plugins/switch/switch-editor.tsx new file mode 100644 index 000000000..f38475f17 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/plugins/switch/switch-editor.tsx @@ -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 = []; + private supportedSyntaxes: Array = [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 ( +
+
+ +
+ + + + + + + + + + {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)} + /> +
+ +
+
+
+ +
+
+ +
+
+ ); + } + + 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, 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(); + }; +} diff --git a/src/designer/elsa-workflows-designer/src/plugins/switch/switch-plugin.ts b/src/designer/elsa-workflows-designer/src/plugins/switch/switch-plugin.ts deleted file mode 100644 index 5033ec8e6..000000000 --- a/src/designer/elsa-workflows-designer/src/plugins/switch/switch-plugin.ts +++ /dev/null @@ -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); - } -} diff --git a/src/designer/elsa-workflows-designer/src/plugins/switch/switch-plugin.tsx b/src/designer/elsa-workflows-designer/src/plugins/switch/switch-plugin.tsx new file mode 100644 index 000000000..6f965c211 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/plugins/switch/switch-plugin.tsx @@ -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 => ) + nodeHandlerRegistry.add('ControlFlow.Switch', () => Container.get(SwitchNodeHandler)); + } +} diff --git a/src/designer/elsa-workflows-designer/src/services/node-input-driver.ts b/src/designer/elsa-workflows-designer/src/services/node-input-driver.ts index 5b7cde3b0..bd5ee4658 100644 --- a/src/designer/elsa-workflows-designer/src/services/node-input-driver.ts +++ b/src/designer/elsa-workflows-designer/src/services/node-input-driver.ts @@ -4,6 +4,7 @@ export interface NodeInputContext { node: Node; nodeDescriptor: NodeDescriptor; inputDescriptor: InputDescriptor; + notifyInputChanged: () => void; inputChanged: (value: any, syntax: string) => void; } diff --git a/src/designer/elsa-workflows-designer/src/utils/utils.ts b/src/designer/elsa-workflows-designer/src/utils/utils.ts index 3970a6e8c..ee0266400 100644 --- a/src/designer/elsa-workflows-designer/src/utils/utils.ts +++ b/src/designer/elsa-workflows-designer/src/utils/utils.ts @@ -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;