Switch editor UI
This commit is contained in:
parent
ba05ce7ee7
commit
2a1ff4c392
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
6
src/core/Elsa.Core/Activities/UIHints.cs
Normal file
6
src/core/Elsa.Core/Activities/UIHints.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace Elsa.Activities;
|
||||
|
||||
public static class UIHints
|
||||
{
|
||||
public const string SwitchEditor = "switch-editor";
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,5 +90,6 @@ export interface RegisterLocation {
|
|||
export enum SyntaxNames {
|
||||
Literal = 'Literal',
|
||||
JavaScript = 'JavaScript',
|
||||
Liquid = 'Liquid',
|
||||
Json = 'Json'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"> </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();
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ export interface NodeInputContext {
|
|||
node: Node;
|
||||
nodeDescriptor: NodeDescriptor;
|
||||
inputDescriptor: InputDescriptor;
|
||||
notifyInputChanged: () => void;
|
||||
inputChanged: (value: any, syntax: string) => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue