From b00eea3c6d0468c8b4ecfc6eb458191044fe723b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 12 Apr 2021 21:33:52 +0200 Subject: [PATCH] Implement Switch UI with multi-expression support --- .../Activities/ControlFlow/Switch/Switch.cs | 2 +- .../elsa-workflows-studio/src/components.d.ts | 4 +- .../elsa-expression-editor.tsx | 4 +- .../elsa-multi-expression-editor.tsx | 22 +-- .../elsa-dropdown-property.tsx | 1 - .../elsa-switch-cases-property.tsx | 176 ++++++++++++++++++ .../elsa-switch-cases-property/models.ts | 7 + .../elsa-workflows-studio/src/utils/utils.ts | 17 ++ 8 files changed, 206 insertions(+), 27 deletions(-) create mode 100644 src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-switch-cases-property/elsa-switch-cases-property.tsx create mode 100644 src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-switch-cases-property/models.ts diff --git a/src/core/Elsa.Core/Activities/ControlFlow/Switch/Switch.cs b/src/core/Elsa.Core/Activities/ControlFlow/Switch/Switch.cs index 795884c22..3ab46962e 100644 --- a/src/core/Elsa.Core/Activities/ControlFlow/Switch/Switch.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/Switch/Switch.cs @@ -22,7 +22,7 @@ namespace Elsa.Activities.ControlFlow )] public class Switch : Activity, INotificationHandler { - [ActivityProperty(Hint = "The conditions to evaluate.")] + [ActivityProperty(Hint = "The conditions to evaluate.", UIHint = "switch-case-builder", DefaultSyntax = "Switch")] public ICollection Cases { get; set; } = new List(); [ActivityProperty( diff --git a/src/designer/elsa-workflows-studio/src/components.d.ts b/src/designer/elsa-workflows-studio/src/components.d.ts index 9f209aa08..ec4d7f8b9 100644 --- a/src/designer/elsa-workflows-studio/src/components.d.ts +++ b/src/designer/elsa-workflows-studio/src/components.d.ts @@ -55,7 +55,6 @@ export namespace Components { "context"?: string; "editorHeight": string; "expression": string; - "fieldName": string; "language": string; "serverUrl": string; "setExpression": (value: string) => Promise; @@ -85,7 +84,7 @@ export namespace Components { "defaultSyntax": string; "editorHeight": string; "expressions": Map; - "fieldName": string; + "fieldName"?: string; "label": string; "singleLineMode": boolean; "supportedSyntaxes": Array; @@ -465,7 +464,6 @@ declare namespace LocalJSX { "context"?: string; "editorHeight"?: string; "expression"?: string; - "fieldName"?: string; "language"?: string; "onExpressionChanged"?: (event: CustomEvent) => void; "serverUrl"?: string; diff --git a/src/designer/elsa-workflows-studio/src/components/editors/elsa-expression-editor/elsa-expression-editor.tsx b/src/designer/elsa-workflows-studio/src/components/editors/elsa-expression-editor/elsa-expression-editor.tsx index 3d719426d..f18076f95 100644 --- a/src/designer/elsa-workflows-studio/src/components/editors/elsa-expression-editor/elsa-expression-editor.tsx +++ b/src/designer/elsa-workflows-studio/src/components/editors/elsa-expression-editor/elsa-expression-editor.tsx @@ -11,7 +11,6 @@ import {MonacoValueChangedArgs} from "../../controls/elsa-monaco/elsa-monaco"; export class ElsaExpressionEditor { @Event() expressionChanged: EventEmitter; - @Prop() fieldName: string; @Prop() language: string; @Prop() expression: string; @Prop({attribute: 'editor-height', reflect: true}) editorHeight: string = '6em'; @@ -25,6 +24,7 @@ export class ElsaExpressionEditor { @Watch("expression") expressionChangedHandler(newValue: string) { + debugger; this.currentExpression = newValue; } @@ -51,7 +51,6 @@ export class ElsaExpressionEditor { render() { const language = this.language; - const fieldName = this.fieldName; const value = this.currentExpression; return ( @@ -62,7 +61,6 @@ export class ElsaExpressionEditor { single-line={this.singleLineMode} onValueChanged={e => this.onMonacoValueChanged(e.detail)} ref={el => this.monacoEditor = el}/> - ) } diff --git a/src/designer/elsa-workflows-studio/src/components/editors/elsa-multi-expression-editor/elsa-multi-expression-editor.tsx b/src/designer/elsa-workflows-studio/src/components/editors/elsa-multi-expression-editor/elsa-multi-expression-editor.tsx index 6411004d0..a5062892c 100644 --- a/src/designer/elsa-workflows-studio/src/components/editors/elsa-multi-expression-editor/elsa-multi-expression-editor.tsx +++ b/src/designer/elsa-workflows-studio/src/components/editors/elsa-multi-expression-editor/elsa-multi-expression-editor.tsx @@ -2,7 +2,7 @@ import {Component, Event, EventEmitter, h, Prop, State} from '@stencil/core'; import {SyntaxNames} from "../../../models"; import {registerClickOutside} from "stencil-click-outside"; import {enter, leave, toggle} from 'el-transition' -import {Map} from "../../../utils/utils"; +import {Map, mapSyntaxToLanguage} from "../../../utils/utils"; @Component({ tag: 'elsa-multi-expression-editor', @@ -11,7 +11,7 @@ import {Map} from "../../../utils/utils"; export class ElsaMultiExpressionEditor { @Prop() label: string; - @Prop() fieldName: string; + @Prop() fieldName?: string; @Prop() syntax?: string; @Prop() defaultSyntax: string = SyntaxNames.Literal; @Prop() expressions: Map = {}; @@ -65,20 +65,6 @@ export class ElsaMultiExpressionEditor { this.closeContextMenu(); } - mapSyntaxToLanguage(syntax: string): any { - switch (syntax) { - case 'Json': - return 'json'; - case 'JavaScript': - return 'javascript'; - case 'Liquid': - return 'handlebars'; - case 'Literal': - default: - return 'plaintext'; - } - } - onSettingsClick(e: Event) { this.toggleContextMenu(); } @@ -166,8 +152,7 @@ export class ElsaMultiExpressionEditor { renderEditor() { const selectedSyntax = this.selectedSyntax; - const monacoLanguage = this.mapSyntaxToLanguage(selectedSyntax); - const fieldName = this.fieldName; + const monacoLanguage = mapSyntaxToLanguage(selectedSyntax); const value = this.currentValue; const expressionEditorClass = selectedSyntax ? 'block' : 'hidden'; const defaultEditorClass = selectedSyntax ? 'hidden' : 'block'; @@ -177,7 +162,6 @@ export class ElsaMultiExpressionEditor {
this.expressionEditor = el} onExpressionChanged={e => this.onExpressionChanged(e)} - fieldName={fieldName} expression={value} language={monacoLanguage} editorHeight={this.editorHeight} diff --git a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-dropdown-property/elsa-dropdown-property.tsx b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-dropdown-property/elsa-dropdown-property.tsx index ff81626d8..69a0a9558 100644 --- a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-dropdown-property/elsa-dropdown-property.tsx +++ b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-dropdown-property/elsa-dropdown-property.tsx @@ -47,7 +47,6 @@ export class ElsaDropdownProperty { const optionIsString = typeof(option) == 'string'; const value = optionIsString ? option : option.value; const text = optionIsString ? option : option.text; - debugger; return ; })} diff --git a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-switch-cases-property/elsa-switch-cases-property.tsx b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-switch-cases-property/elsa-switch-cases-property.tsx new file mode 100644 index 000000000..59411aba7 --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-switch-cases-property/elsa-switch-cases-property.tsx @@ -0,0 +1,176 @@ +import {Component, h, Prop, State} from '@stencil/core'; +import {ActivityDefinitionProperty, ActivityPropertyDescriptor, SyntaxNames} from "../../../../models"; +import {mapSyntaxToLanguage, parseJson} from "../../../../utils/utils"; +import {SwitchCase} from "./models"; +import {languages} from "monaco-editor"; +import json = languages.json; + +@Component({ + tag: 'elsa-switch-cases-property', + shadow: false, +}) +export class ElsaSwitchCasesProperty { + + @Prop() propertyDescriptor: ActivityPropertyDescriptor; + @Prop() propertyModel: ActivityDefinitionProperty; + @State() cases: Array = []; + + supportedSyntaxes: Array = [SyntaxNames.JavaScript, SyntaxNames.Liquid]; + multiExpressionEditor: HTMLElsaMultiExpressionEditorElement; + syntaxSwitchCount: number = 0; + + async componentWillLoad() { + const propertyModel = this.propertyModel; + const casesJson = propertyModel.expressions['Switch'] + this.cases = parseJson(casesJson) || []; + } + + updatePropertyModel() { + this.propertyModel.expressions['Switch'] = JSON.stringify(this.cases); + this.multiExpressionEditor.expressions[SyntaxNames.Json] = JSON.stringify(this.cases, null, 2); + } + + onDefaultSyntaxValueChanged(e: CustomEvent) { + this.cases = e.detail; + } + + onAddCaseClick() { + const caseName = `Case ${this.cases.length + 1}`; + const newCase = {name: caseName, syntax: SyntaxNames.JavaScript, expressions: {[SyntaxNames.JavaScript]: ''}}; + this.cases = [...this.cases, newCase]; + this.updatePropertyModel(); + } + + onDeleteCaseClick(switchCase: SwitchCase) { + this.cases = this.cases.filter(x => x != switchCase); + this.updatePropertyModel(); + } + + onCaseNameChanged(e: Event, switchCase: SwitchCase) { + switchCase.name = (e.currentTarget as HTMLInputElement).value.trim(); + this.updatePropertyModel(); + } + + onCaseExpressionChanged(e: CustomEvent, switchCase: SwitchCase) { + switchCase.expressions[switchCase.syntax] = e.detail; + this.updatePropertyModel(); + } + + onCaseSyntaxChanged(e: Event, switchCase: SwitchCase, expressionEditor: HTMLElsaExpressionEditorElement) { + const select = e.currentTarget as HTMLSelectElement; + switchCase.syntax = select.value; + expressionEditor.language = mapSyntaxToLanguage(switchCase.syntax); + this.updatePropertyModel(); + } + + onMultiExpressionEditorValueChanged(e: CustomEvent) { + const json = e.detail; + const parsed = parseJson(json); + + if (!parsed) + return; + + if (!Array.isArray(parsed)) + return; + + this.propertyModel.expressions['Switch'] = json; + this.cases = parsed; + } + + onMultiExpressionEditorSyntaxChanged(e: CustomEvent){ + this.syntaxSwitchCount++; + } + + render() { + const cases = this.cases; + const supportedSyntaxes = this.supportedSyntaxes; + const json = JSON.stringify(cases, null, 2); + + const renderCaseEditor = (switchCase: SwitchCase, index: number) => { + const expression = switchCase.expressions[switchCase.syntax]; + const syntax = switchCase.syntax; + const monacoLanguage = mapSyntaxToLanguage(syntax); + let expressionEditor = null; + + return ( + + + this.onCaseNameChanged(e, switchCase)} class="focus:ring-blue-500 focus:border-blue-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300"/> + + + +
+ expressionEditor = el} + expression={expression} + language={monacoLanguage} + single-line={true} + editor-height="2.75em" + onExpressionChanged={e => this.onCaseExpressionChanged(e, switchCase)} + /> +
+ +
+
+ + + + + + ); + }; + + return ( +
+ + this.multiExpressionEditor = el} + label={this.propertyDescriptor.label} + defaultSyntax={SyntaxNames.Json} + supportedSyntaxes={[SyntaxNames.Json]} + expressions={{'Json': json}} + editor-height="20rem" + onExpressionChanged={e => this.onMultiExpressionEditorValueChanged(e)} + onSyntaxChanged={e => this.onMultiExpressionEditorSyntaxChanged(e)} + > + + + + + + + + + + + {cases.map(renderCaseEditor)} + +
NameExpression 
+ +
+
+ ); + } +} diff --git a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-switch-cases-property/models.ts b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-switch-cases-property/models.ts new file mode 100644 index 000000000..ad54a2e30 --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-switch-cases-property/models.ts @@ -0,0 +1,7 @@ +import {Map} from "../../../../utils/utils"; + +export interface SwitchCase { + name: string; + expressions?: Map; + syntax?: string; +} \ No newline at end of file diff --git a/src/designer/elsa-workflows-studio/src/utils/utils.ts b/src/designer/elsa-workflows-studio/src/utils/utils.ts index d7dc7f93c..f89816af0 100644 --- a/src/designer/elsa-workflows-studio/src/utils/utils.ts +++ b/src/designer/elsa-workflows-studio/src/utils/utils.ts @@ -134,6 +134,9 @@ export function getOrCreateProperty(activity: ActivityModel, name: string, defau } export function parseJson(json: string): any { + if(!json) + return null; + try { return JSON.parse(json); } catch (e) { @@ -156,6 +159,20 @@ export function parseQuery(queryString?: string): any { return query; } +export function mapSyntaxToLanguage(syntax: string): any { + switch (syntax) { + case 'Json': + return 'json'; + case 'JavaScript': + return 'javascript'; + case 'Liquid': + return 'handlebars'; + case 'Literal': + default: + return 'plaintext'; + } +} + export function timeSince(time) { switch (typeof time) { case 'number':