diff --git a/src/core/Elsa.Abstractions/Attributes/ActivityPropertyAttribute.cs b/src/core/Elsa.Abstractions/Attributes/ActivityPropertyAttribute.cs index 0c81638e5..2c8da6441 100644 --- a/src/core/Elsa.Abstractions/Attributes/ActivityPropertyAttribute.cs +++ b/src/core/Elsa.Abstractions/Attributes/ActivityPropertyAttribute.cs @@ -29,5 +29,10 @@ namespace Elsa.Attributes /// A value representing options specific to a given UI hint. /// public object? Options { get; set; } + + /// + /// The name of a static method that provides options. + /// + public string? OptionsProvider { get; set; } } } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Design/ActivityPropertyUIHints.cs b/src/core/Elsa.Abstractions/Design/ActivityPropertyUIHints.cs index 14d687349..8accafebf 100644 --- a/src/core/Elsa.Abstractions/Design/ActivityPropertyUIHints.cs +++ b/src/core/Elsa.Abstractions/Design/ActivityPropertyUIHints.cs @@ -9,6 +9,7 @@ namespace Elsa.Design public const string RadioList = "radio-list"; public const string Dropdown = "dropdown"; public const string MultiText = "multi-text"; + public const string CodeEditor = "code-editor"; /// /// A list that allows the user to enter zero or more values. diff --git a/src/core/Elsa.Core/Metadata/ActivityDescriber.cs b/src/core/Elsa.Core/Metadata/ActivityDescriber.cs index 09a07238f..79fe9df19 100644 --- a/src/core/Elsa.Core/Metadata/ActivityDescriber.cs +++ b/src/core/Elsa.Core/Metadata/ActivityDescriber.cs @@ -57,17 +57,30 @@ namespace Elsa.Metadata if (activityProperty == null) continue; + var options = activityProperty.OptionsProvider is not null and not "" ? GetOptions(activityType, activityProperty.OptionsProvider) : activityProperty.Options; + yield return new ActivityPropertyDescriptor ( (activityProperty.Name ?? propertyInfo.Name).Pascalize(), (activityProperty.UIHint ?? InferPropertyUIHint(propertyInfo)), activityProperty.Label ?? propertyInfo.Name.Humanize(LetterCasing.Title), activityProperty.Hint, - activityProperty.Options + options ); } } + private object? GetOptions(Type activityType, string providerMethodName) + { + var providerMethod = activityType.GetMethod(providerMethodName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); + + if (providerMethod == null) + throw new MissingMethodException(activityType.Name, providerMethodName); + + var options = providerMethod.Invoke(null, new object[0]); + return options; + } + private string InferPropertyUIHint(PropertyInfo propertyInfo) { var type = propertyInfo.PropertyType; diff --git a/src/designer/elsa-workflows-studio/src/components.d.ts b/src/designer/elsa-workflows-studio/src/components.d.ts index 5f5405abf..a0a0f5134 100644 --- a/src/designer/elsa-workflows-studio/src/components.d.ts +++ b/src/designer/elsa-workflows-studio/src/components.d.ts @@ -52,7 +52,18 @@ export namespace Components { "propertyDescriptor": ActivityPropertyDescriptor; "propertyModel": ActivityDefinitionProperty; } + interface ElsaScriptProperty { + "context"?: string; + "editorHeight": string; + "propertyDescriptor": ActivityPropertyDescriptor; + "propertyModel": ActivityDefinitionProperty; + "serverUrl": string; + "singleLineMode": boolean; + "syntax"?: string; + "workflowDefinitionId": string; + } interface ElsaTextProperty { + "context"?: string; "editorHeight": string; "propertyDescriptor": ActivityPropertyDescriptor; "propertyModel": ActivityDefinitionProperty; @@ -139,6 +150,12 @@ declare global { prototype: HTMLElsaMultiTextPropertyElement; new (): HTMLElsaMultiTextPropertyElement; }; + interface HTMLElsaScriptPropertyElement extends Components.ElsaScriptProperty, HTMLStencilElement { + } + var HTMLElsaScriptPropertyElement: { + prototype: HTMLElsaScriptPropertyElement; + new (): HTMLElsaScriptPropertyElement; + }; interface HTMLElsaTextPropertyElement extends Components.ElsaTextProperty, HTMLStencilElement { } var HTMLElsaTextPropertyElement: { @@ -175,6 +192,7 @@ declare global { "elsa-modal-dialog": HTMLElsaModalDialogElement; "elsa-monaco": HTMLElsaMonacoElement; "elsa-multi-text-property": HTMLElsaMultiTextPropertyElement; + "elsa-script-property": HTMLElsaScriptPropertyElement; "elsa-text-property": HTMLElsaTextPropertyElement; "elsa-workflow-editor": HTMLElsaWorkflowEditorElement; "elsa-workflow-publish-button": HTMLElsaWorkflowPublishButtonElement; @@ -227,7 +245,18 @@ declare namespace LocalJSX { "propertyDescriptor"?: ActivityPropertyDescriptor; "propertyModel"?: ActivityDefinitionProperty; } + interface ElsaScriptProperty { + "context"?: string; + "editorHeight"?: string; + "propertyDescriptor"?: ActivityPropertyDescriptor; + "propertyModel"?: ActivityDefinitionProperty; + "serverUrl"?: string; + "singleLineMode"?: boolean; + "syntax"?: string; + "workflowDefinitionId"?: string; + } interface ElsaTextProperty { + "context"?: string; "editorHeight"?: string; "propertyDescriptor"?: ActivityPropertyDescriptor; "propertyModel"?: ActivityDefinitionProperty; @@ -257,6 +286,7 @@ declare namespace LocalJSX { "elsa-modal-dialog": ElsaModalDialog; "elsa-monaco": ElsaMonaco; "elsa-multi-text-property": ElsaMultiTextProperty; + "elsa-script-property": ElsaScriptProperty; "elsa-text-property": ElsaTextProperty; "elsa-workflow-editor": ElsaWorkflowEditor; "elsa-workflow-publish-button": ElsaWorkflowPublishButton; @@ -278,6 +308,7 @@ declare module "@stencil/core" { "elsa-modal-dialog": LocalJSX.ElsaModalDialog & JSXBase.HTMLAttributes; "elsa-monaco": LocalJSX.ElsaMonaco & JSXBase.HTMLAttributes; "elsa-multi-text-property": LocalJSX.ElsaMultiTextProperty & JSXBase.HTMLAttributes; + "elsa-script-property": LocalJSX.ElsaScriptProperty & JSXBase.HTMLAttributes; "elsa-text-property": LocalJSX.ElsaTextProperty & JSXBase.HTMLAttributes; "elsa-workflow-editor": LocalJSX.ElsaWorkflowEditor & JSXBase.HTMLAttributes; "elsa-workflow-publish-button": LocalJSX.ElsaWorkflowPublishButton & JSXBase.HTMLAttributes; diff --git a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree-activity/elsa-designer-tree-activity.tsx b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree-activity/elsa-designer-tree-activity.tsx index 0669971e8..10fe31003 100644 --- a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree-activity/elsa-designer-tree-activity.tsx +++ b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree-activity/elsa-designer-tree-activity.tsx @@ -51,7 +51,7 @@ export class ElsaDesignerTreeActivity { return ( - + {displayContext.activityIcon} @@ -109,7 +109,7 @@ export class ElsaDesignerTreeActivity { return undefined; return ( - + {this.displayContext.bodyDisplay} ) diff --git a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-script-property/elsa-script-property.css b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-script-property/elsa-script-property.css new file mode 100644 index 000000000..5d4e87f30 --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-script-property/elsa-script-property.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-script-property/elsa-script-property.tsx b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-script-property/elsa-script-property.tsx new file mode 100644 index 000000000..1d5618a0a --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-script-property/elsa-script-property.tsx @@ -0,0 +1,99 @@ +import {Component, h, Prop, State} from '@stencil/core'; +import {ActivityDefinitionProperty, ActivityPropertyDescriptor} from "../../../../models"; +import {createElsaClient} from "../../../../services/elsa-client"; +import Tunnel from '../../../data/workflow-editor'; +import {MonacoValueChangedArgs} from "../../monaco/elsa-monaco/elsa-monaco"; + +@Component({ + tag: 'elsa-script-property', + styleUrl: 'elsa-script-property.css', + shadow: false, +}) +export class ElsaScriptProperty { + + @Prop() propertyDescriptor: ActivityPropertyDescriptor; + @Prop() propertyModel: ActivityDefinitionProperty; + @Prop({attribute: 'editor-height', reflect: true}) editorHeight: string = '6em'; + @Prop({attribute: 'single-line', reflect: true}) singleLineMode: boolean = false; + @Prop({attribute: 'context', reflect: true}) context?: string; + @Prop() syntax?: string; + @Prop({mutable: true}) serverUrl: string; + @Prop({mutable: true}) workflowDefinitionId: string; + @State() currentValue?: string + monacoEditor: HTMLElsaMonacoElement; + + async componentWillLoad() { + this.currentValue = this.propertyModel.expression; + } + + async componentDidLoad() { + const elsaClient = createElsaClient(this.serverUrl); + const libSource = await elsaClient.scriptingApi.getJavaScriptTypeDefinitions(this.workflowDefinitionId, this.context); + const libUri = 'defaultLib:lib.es6.d.ts'; + await this.monacoEditor.addJavaScriptLib(libSource, libUri); + } + + mapSyntaxToLanguage(syntax: string): any { + switch (syntax) { + case 'JavaScript': + return 'javascript'; + case 'Liquid': + return 'handlebars'; + case 'Literal': + default: + return 'plaintext'; + } + } + + onMonacoValueChanged(e: MonacoValueChangedArgs) { + this.currentValue = e.value; + } + + render() { + const propertyDescriptor = this.propertyDescriptor; + const options = propertyDescriptor.options || {}; + const syntax = this.syntax; + const monacoLanguage = this.mapSyntaxToLanguage(syntax); + const propertyName = propertyDescriptor.name; + const fieldId = propertyName; + const fieldName = propertyName; + const fieldLabel = propertyDescriptor.label || propertyName; + const fieldHint = propertyDescriptor.hint; + const property = this.propertyModel; + const value = this.currentValue; + + return + + + + + {fieldLabel} + + + + + + + + {syntax} + + + + + + + + this.onMonacoValueChanged(e.detail)} + ref={el => this.monacoEditor = el}/> + + {fieldHint ? {fieldHint} : undefined} + + + } +} + +Tunnel.injectProps(ElsaScriptProperty, ['serverUrl', 'workflowDefinitionId']); diff --git a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-script-property/readme.md b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-script-property/readme.md new file mode 100644 index 000000000..3c564bdc9 --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-script-property/readme.md @@ -0,0 +1,37 @@ +# elsa-modal-dialog + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ---------------------- | ------------------------ | ----------- | ---------------------------- | ----------- | +| `editorHeight` | `editor-height` | | `string` | `'6em'` | +| `propertyDescriptor` | -- | | `ActivityPropertyDescriptor` | `undefined` | +| `propertyModel` | -- | | `ActivityDefinitionProperty` | `undefined` | +| `serverUrl` | `server-url` | | `string` | `undefined` | +| `singleLineMode` | `single-line` | | `boolean` | `false` | +| `workflowDefinitionId` | `workflow-definition-id` | | `string` | `undefined` | + + +## Dependencies + +### Depends on + +- [elsa-monaco](../../monaco/elsa-monaco) +- context-consumer + +### Graph +```mermaid +graph TD; + elsa-text-property --> elsa-monaco + elsa-text-property --> context-consumer + style elsa-text-property fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-text-property/elsa-text-property.tsx b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-text-property/elsa-text-property.tsx index e4381ab6f..6ec9058fc 100644 --- a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-text-property/elsa-text-property.tsx +++ b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-text-property/elsa-text-property.tsx @@ -15,6 +15,7 @@ export class ElsaTextProperty { @Prop() propertyModel: ActivityDefinitionProperty; @Prop({attribute: 'editor-height', reflect: true}) editorHeight: string = '6em'; @Prop({attribute: 'single-line', reflect: true}) singleLineMode: boolean = false; + @Prop({attribute: 'context', reflect: true}) context?: string; @Prop({mutable: true}) serverUrl: string; @Prop({mutable: true}) workflowDefinitionId: string; @State() selectedSyntax?: string; @@ -28,7 +29,7 @@ export class ElsaTextProperty { async componentDidLoad() { const elsaClient = createElsaClient(this.serverUrl); - const libSource = await elsaClient.scriptingApi.getJavaScriptTypeDefinitions(this.workflowDefinitionId); + const libSource = await elsaClient.scriptingApi.getJavaScriptTypeDefinitions(this.workflowDefinitionId, this.context); const libUri = 'defaultLib:lib.es6.d.ts'; await this.monacoEditor.addJavaScriptLib(libSource, libUri); } @@ -59,19 +60,19 @@ export class ElsaTextProperty { } render() { - const syntaxes = ['Literal', 'JavaScript', 'Liquid'].reverse(); - const selectedSyntax = this.selectedSyntax ?? 'Literal'; - const monacoLanguage = this.mapSyntaxToLanguage(selectedSyntax); const propertyDescriptor = this.propertyDescriptor; + const options = propertyDescriptor.options || {}; + const syntaxes = options.syntaxes || ['Literal', 'JavaScript', 'Liquid']; + const selectedSyntax = this.selectedSyntax ?? (syntaxes.length > 0 ? syntaxes[0] : ''); + const monacoLanguage = this.mapSyntaxToLanguage(selectedSyntax); const propertyName = propertyDescriptor.name; const fieldId = propertyName; const fieldName = propertyName; const fieldLabel = propertyDescriptor.label || propertyName; const fieldHint = propertyDescriptor.hint; - const syntaxFieldId = `${fieldId}Syntax`; const syntaxFieldName = `${fieldName}Syntax`; - const property = this.propertyModel; const value = this.currentValue; + const reversedSyntaxes = [...syntaxes].reverse(); return @@ -86,18 +87,18 @@ export class ElsaTextProperty { Select a tab this.onSyntaxListChange(e)} class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"> - {syntaxes.map(syntax => { + {reversedSyntaxes.map(syntax => { return {syntax}; })} - {syntaxes.map(syntax => { + {reversedSyntaxes.map(syntax => { const isSelected = syntax == selectedSyntax; const className = isSelected ? 'bg-blue-100 text-blue-700' : 'text-gray-500 hover:text-gray-700'; - return this.onSyntaxSelect(e, syntax)} data-syntax={syntax} class={`${className} px-3 py-2 font-medium text-sm rounded-md`}> + return this.onSyntaxSelect(e, syntax)} class={`${className} px-3 py-2 font-medium text-sm rounded-md`}> {syntax} ; })} diff --git a/src/designer/elsa-workflows-studio/src/components/icons/fork-icon.tsx b/src/designer/elsa-workflows-studio/src/components/icons/fork-icon.tsx new file mode 100644 index 000000000..78255e294 --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/icons/fork-icon.tsx @@ -0,0 +1,15 @@ +import {h} from "@stencil/core"; + +export const ForkIcon = props => + ( + + + + + + + + + + + ); diff --git a/src/designer/elsa-workflows-studio/src/components/icons/join-icon.tsx b/src/designer/elsa-workflows-studio/src/components/icons/join-icon.tsx new file mode 100644 index 000000000..e76120ba5 --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/icons/join-icon.tsx @@ -0,0 +1,15 @@ +import {h} from "@stencil/core"; + +export const JoinIcon = props => + ( + + + + + + + + + + + ); diff --git a/src/designer/elsa-workflows-studio/src/components/icons/script-icon.tsx b/src/designer/elsa-workflows-studio/src/components/icons/script-icon.tsx new file mode 100644 index 000000000..e019a700a --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/components/icons/script-icon.tsx @@ -0,0 +1,13 @@ +import {h} from "@stencil/core"; + +export const ScriptIcon = props => + ( + + + + + + + + + ); diff --git a/src/designer/elsa-workflows-studio/src/plugins/default-drivers-plugin.ts b/src/designer/elsa-workflows-studio/src/plugins/default-drivers-plugin.ts index 8760698b5..4256b59d1 100644 --- a/src/designer/elsa-workflows-studio/src/plugins/default-drivers-plugin.ts +++ b/src/designer/elsa-workflows-studio/src/plugins/default-drivers-plugin.ts @@ -3,6 +3,7 @@ import {propertyDisplayManager} from '../services/property-display-manager'; import {CheckboxDriver, CheckListDriver, DropdownDriver, MultilineDriver, SingleLineDriver} from "../property-display-drivers"; import {PropertyDisplayDriver} from "../services/property-display-driver"; import {MultiTextDriver} from "../property-display-drivers/multi-text-driver"; +import {CodeEditorDriver} from "../property-display-drivers/code-editor-driver"; export class DefaultDriversPlugin implements ElsaPlugin { constructor() { @@ -12,6 +13,7 @@ export class DefaultDriversPlugin implements ElsaPlugin { this.addDriver('checkbox', CheckboxDriver); this.addDriver('dropdown', DropdownDriver); this.addDriver('multi-text', MultiTextDriver); + this.addDriver('code-editor', CodeEditorDriver); } addDriver(controlType: string, c: new () => T) { diff --git a/src/designer/elsa-workflows-studio/src/plugins/fork-plugin.tsx b/src/designer/elsa-workflows-studio/src/plugins/fork-plugin.tsx index afddb9b96..d69a88194 100644 --- a/src/designer/elsa-workflows-studio/src/plugins/fork-plugin.tsx +++ b/src/designer/elsa-workflows-studio/src/plugins/fork-plugin.tsx @@ -2,8 +2,8 @@ import {eventBus} from '../services/event-bus'; import {ActivityDescriptorDisplayContext, ActivityDesignDisplayContext, EventTypes} from "../models"; import {h} from "@stencil/core"; -import {ActivityIcon} from "../components/icons/activity-icon"; import {parseJson} from "../utils/utils"; +import {ForkIcon} from "../components/icons/fork-icon"; export class ForkPlugin implements ElsaPlugin { constructor() { @@ -17,7 +17,7 @@ export class ForkPlugin implements ElsaPlugin { if (descriptor.type !== 'Fork') return; - context.activityIcon = ; + context.activityIcon = ; } onActivityDesignDisplaying(context: ActivityDesignDisplayContext) { @@ -31,7 +31,7 @@ export class ForkPlugin implements ElsaPlugin { const expression = branches.expression; const branchList = parseJson(expression) || []; - context.activityIcon = ; + context.activityIcon = ; context.outcomes = branchList; } } diff --git a/src/designer/elsa-workflows-studio/src/plugins/run-javascript-plugin.tsx b/src/designer/elsa-workflows-studio/src/plugins/run-javascript-plugin.tsx new file mode 100644 index 000000000..05e25152f --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/plugins/run-javascript-plugin.tsx @@ -0,0 +1,38 @@ +import {ElsaPlugin} from "../services/elsa-plugin"; +import {eventBus} from '../services/event-bus'; +import {ActivityDescriptorDisplayContext, ActivityDesignDisplayContext, EventTypes} from "../models"; +import {h} from "@stencil/core"; +import {parseJson} from "../utils/utils"; +import {ScriptIcon} from "../components/icons/script-icon"; + +export class RunJavascriptPlugin implements ElsaPlugin { + constructor() { + eventBus.on(EventTypes.ActivityDescriptorDisplaying, this.onActivityDescriptorDisplaying); + eventBus.on(EventTypes.ActivityDesignDisplaying, this.onActivityDesignDisplaying); + } + + onActivityDescriptorDisplaying(context: ActivityDescriptorDisplayContext) { + const descriptor = context.activityDescriptor; + + if (descriptor.type !== 'RunJavaScript') + return; + + context.activityIcon = ; + } + + onActivityDesignDisplaying(context: ActivityDesignDisplayContext) { + const activityModel = context.activityModel; + + if (activityModel.type !== 'RunJavaScript') + return; + + const props = activityModel.properties || []; + const outcomes = props.find(x => x.name == 'Outcomes') || { expression: '' }; + const expression = outcomes.expression; + const outcomeList = parseJson(expression) || ['Done']; + + context.activityIcon = ; + context.outcomes = outcomeList; + context.bodyDisplay = undefined; + } +} diff --git a/src/designer/elsa-workflows-studio/src/property-display-drivers/code-editor-driver.tsx b/src/designer/elsa-workflows-studio/src/property-display-drivers/code-editor-driver.tsx new file mode 100644 index 000000000..d7ece4c66 --- /dev/null +++ b/src/designer/elsa-workflows-studio/src/property-display-drivers/code-editor-driver.tsx @@ -0,0 +1,39 @@ +import {PropertyDisplayDriver} from "../services/property-display-driver"; +import {ActivityModel, ActivityPropertyDescriptor} from "../models"; +import {h, State} from "@stencil/core"; +import {getProperty, setActivityModelProperty} from "../utils/utils"; + +export class CodeEditorDriver implements PropertyDisplayDriver { + + display(activity: ActivityModel, property: ActivityPropertyDescriptor) { + const key = `${activity.activityId}:${property.name}`; + const prop = getProperty(activity.properties, property.name); + const options = property.options || {}; + const editorHeight = this.getEditorHeight(options); + const context: string = options.context; + const syntax = options.syntax; + + return ( + + + + ); + } + + update(activity: ActivityModel, property: ActivityPropertyDescriptor, form: FormData) { + const value = form.get(property.name) as string; + setActivityModelProperty(activity, property.name, value, "Literal"); + } + + getEditorHeight(options: any) { + const editorHeightName = options.editorHeight || 'Default'; + + switch (editorHeightName) { + case 'Large': + return '20em' + case 'Default': + default: + return '8em'; + } + } +} diff --git a/src/designer/elsa-workflows-studio/src/property-display-drivers/multiline-driver.tsx b/src/designer/elsa-workflows-studio/src/property-display-drivers/multiline-driver.tsx index 46c25a8c1..e4b5b3e7b 100644 --- a/src/designer/elsa-workflows-studio/src/property-display-drivers/multiline-driver.tsx +++ b/src/designer/elsa-workflows-studio/src/property-display-drivers/multiline-driver.tsx @@ -8,12 +8,15 @@ export class MultilineDriver implements PropertyDisplayDriver { display(activity: ActivityModel, property: ActivityPropertyDescriptor) { const key = `${activity.activityId}:${property.name}`; const prop = getProperty(activity.properties, property.name); + const options = property.options || {}; + const editorHeight = this.getEditorHeight(options); + const context: string = options.context; return ( - + - ) + ); } update(activity: ActivityModel, property: ActivityPropertyDescriptor, form: FormData) { @@ -21,4 +24,16 @@ export class MultilineDriver implements PropertyDisplayDriver { const syntax = form.get(`${property.name}Syntax`) as string || 'Literal'; setActivityModelProperty(activity, property.name, value, syntax); } + + getEditorHeight(options: any) { + const editorHeightName = options.editorHeight || 'Default'; + + switch (editorHeightName) { + case 'Large': + return '20em' + case 'Default': + default: + return '8em'; + } + } } diff --git a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts index 5c969689d..37257e5f8 100644 --- a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts +++ b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts @@ -27,8 +27,9 @@ export const createElsaClient = function (serverUrl: string): ElsaClient { } }, scriptingApi: { - getJavaScriptTypeDefinitions: async (workflowDefinitionId: string): Promise => { - const response = await httpClient.get(`v1/scripting/javascript/type-definitions/${workflowDefinitionId}?t=${new Date().getTime()}`); + getJavaScriptTypeDefinitions: async (workflowDefinitionId: string, context?: string): Promise => { + context = context || ''; + const response = await httpClient.get(`v1/scripting/javascript/type-definitions/${workflowDefinitionId}?t=${new Date().getTime()}&context=${context}`); return response.data; } } @@ -52,7 +53,7 @@ export interface WorkflowDefinitionsApi { } export interface ScriptingApi { - getJavaScriptTypeDefinitions(workflowDefinitionId: string): Promise + getJavaScriptTypeDefinitions(workflowDefinitionId: string, context?: string): Promise } export interface SaveWorkflowDefinitionRequest { diff --git a/src/designer/elsa-workflows-studio/src/services/plugin-manager.ts b/src/designer/elsa-workflows-studio/src/services/plugin-manager.ts index 4853a4d07..582e473f8 100644 --- a/src/designer/elsa-workflows-studio/src/services/plugin-manager.ts +++ b/src/designer/elsa-workflows-studio/src/services/plugin-manager.ts @@ -6,6 +6,7 @@ import {WriteLinePlugin} from "../plugins/write-line-plugin"; import {SendEmailPlugin} from "../plugins/send-email-plugin"; import {DefaultDriversPlugin} from "../plugins/default-drivers-plugin"; import {ForkPlugin} from "../plugins/fork-plugin"; +import {RunJavascriptPlugin} from "../plugins/run-javascript-plugin"; export class PluginManager { @@ -19,6 +20,7 @@ export class PluginManager { new HttpEndpointPlugin(), new TimerPlugin(), new WriteLinePlugin(), + new RunJavascriptPlugin(), new SendEmailPlugin() ]; } diff --git a/src/samples/dashboard/ElsaDashboard.Samples.Monolith/Startup.cs b/src/samples/dashboard/ElsaDashboard.Samples.Monolith/Startup.cs index 234179ad7..4740c68a4 100644 --- a/src/samples/dashboard/ElsaDashboard.Samples.Monolith/Startup.cs +++ b/src/samples/dashboard/ElsaDashboard.Samples.Monolith/Startup.cs @@ -35,6 +35,7 @@ namespace ElsaDashboard.Samples.Monolith .AddHttpActivities(elsaSection.GetSection("Http").Bind) .AddEmailActivities(elsaSection.GetSection("Smtp").Bind) .AddQuartzTemporalActivities() + .AddJavaScriptActivities() .AddWorkflowsFrom() ); diff --git a/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScript.cs b/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScript.cs new file mode 100644 index 000000000..42f90dd3a --- /dev/null +++ b/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScript.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.ActivityResults; +using Elsa.Attributes; +using Elsa.Design; +using Elsa.Scripting.JavaScript.Events; +using Elsa.Scripting.JavaScript.Messages; +using Elsa.Scripting.JavaScript.Services; +using Elsa.Services; +using Elsa.Services.Models; +using Jint; +using MediatR; +using NetBox.Extensions; + +// ReSharper disable once CheckNamespace +namespace Elsa.Activities.JavaScript +{ + [Action(Category = "Scripting", Description = "Run JavaScript code.")] + public class RunJavaScript : Activity, INotificationHandler + { + private readonly IJavaScriptService _javaScriptService; + + public RunJavaScript(IJavaScriptService javaScriptService) + { + _javaScriptService = javaScriptService; + } + + [ActivityProperty(Hint = "The JavaScript to run.", UIHint = ActivityPropertyUIHints.CodeEditor, OptionsProvider = nameof(GetOptions))] + public string? Script { get; set; } + + [ActivityProperty(Hint = "The possible outcomes that can be set by the script.", UIHint = ActivityPropertyUIHints.MultiText)] + public ICollection Outcomes { get; set; } = new List { OutcomeNames.Done }; + + protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) + { + var script = Script; + + if (script is null or "") + return Done(); + + var outcomes = new HashSet(); + + void ConfigureEngine(Engine engine) + { + void SetOutcome(string value) => outcomes.Add(value); + void SetOutcomes(IEnumerable values) => outcomes.AddRange(values); + + engine.SetValue("setOutcome", (Action) SetOutcome); + engine.SetValue("setOutcomes", (Action>) SetOutcomes); + } + + var output = await _javaScriptService.EvaluateAsync(script, typeof(object), context, ConfigureEngine, context.CancellationToken); + + if (!outcomes.Any()) + outcomes.Add(OutcomeNames.Done); + + return new CombinedResult(Output(output), new OutcomeResult(outcomes)); + } + + private static object GetOptions() => new + { + EditorHeight = "Large", + Context = nameof(RunJavaScript), + Syntax = JavaScriptExpressionHandler.SyntaxName + }; + + public Task Handle(RenderingTypeScriptDefinitions notification, CancellationToken cancellationToken) + { + var context = notification.Context; + + if (context != nameof(RunJavaScript)) + return Task.CompletedTask; + + notification.Output.AppendLine("declare function setOutcome(name: string);"); + notification.Output.AppendLine("declare function setOutcomes(names: Array);"); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScriptBuilderExtensions.cs b/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScriptBuilderExtensions.cs new file mode 100644 index 000000000..899f15bc6 --- /dev/null +++ b/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScriptBuilderExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.CompilerServices; +using Elsa.Builders; + +// ReSharper disable ExplicitCallerInfoArgument +// ReSharper disable once CheckNamespace +namespace Elsa.Activities.JavaScript +{ + public static class RunJavaScriptBuilderExtensions + { + public static IActivityBuilder RunJavaScript(this IBuilder builder, Action> setup, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => + builder.Then(setup, null, lineNumber, sourceFile); + + public static IActivityBuilder RunJavaScript(this IBuilder builder, string script, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => + builder.RunJavaScript(setup => setup.WithScript(script), lineNumber, sourceFile); + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScriptExtensions.cs b/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScriptExtensions.cs new file mode 100644 index 000000000..5e580bf34 --- /dev/null +++ b/src/scripting/Elsa.Scripting.JavaScript/Activities/RunJavaScript/RunJavaScriptExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using Elsa.Builders; +using Elsa.Services.Models; + +// ReSharper disable once CheckNamespace +namespace Elsa.Activities.JavaScript +{ + public static class RunJavaScriptExtensions + { + public static ISetupActivity WithScript(this ISetupActivity activity, Func> value) => activity.Set(x => x.Script, value); + public static ISetupActivity WithScript(this ISetupActivity activity, Func value) => activity.Set(x => x.Script, value); + public static ISetupActivity WithScript(this ISetupActivity activity, Func> value) => activity.Set(x => x.Script, value); + public static ISetupActivity WithScript(this ISetupActivity activity, Func value) => activity.Set(x => x.Script, value); + public static ISetupActivity WithScript(this ISetupActivity activity, string? value) => activity.Set(x => x.Script, value); + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Events/CollectingTypeScriptDefinitionTypes.cs b/src/scripting/Elsa.Scripting.JavaScript/Events/CollectingTypeScriptDefinitionTypes.cs new file mode 100644 index 000000000..a3ca0fbc3 --- /dev/null +++ b/src/scripting/Elsa.Scripting.JavaScript/Events/CollectingTypeScriptDefinitionTypes.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using Elsa.Models; +using MediatR; + +namespace Elsa.Scripting.JavaScript.Events +{ + public class CollectingTypeScriptDefinitionTypes : INotification + { + internal CollectingTypeScriptDefinitionTypes(WorkflowDefinition? workflowDefinition, Action> collectTypes) + { + WorkflowDefinition = workflowDefinition; + CollectTypes = collectTypes; + } + + private Action> CollectTypes { get; } + public WorkflowDefinition? WorkflowDefinition { get; } + + public void CollectType() => CollectType(typeof(T)); + public void CollectType(Type type) => CollectTypes(new[] { type }); + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Events/RenderingTypeScriptDefinitions.cs b/src/scripting/Elsa.Scripting.JavaScript/Events/RenderingTypeScriptDefinitions.cs new file mode 100644 index 000000000..0d70e640d --- /dev/null +++ b/src/scripting/Elsa.Scripting.JavaScript/Events/RenderingTypeScriptDefinitions.cs @@ -0,0 +1,26 @@ +using System; +using System.Text; +using Elsa.Models; +using MediatR; + +namespace Elsa.Scripting.JavaScript.Events +{ + public class RenderingTypeScriptDefinitions : INotification + { + internal RenderingTypeScriptDefinitions(WorkflowDefinition? workflowDefinition, Func getTypeScriptType, string? context, StringBuilder output) + { + WorkflowDefinition = workflowDefinition; + GetTypeScriptTypeInternal = getTypeScriptType; + Context = context; + Output = output; + } + + private Func GetTypeScriptTypeInternal { get; } + public string? Context { get; } + public StringBuilder Output { get; } + public WorkflowDefinition? WorkflowDefinition { get; } + + public string GetTypeScriptType(Type type) => GetTypeScriptTypeInternal(type); + public string GetTypeScriptType() => GetTypeScriptType(typeof(Type)); + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Extensions/JavaScriptServiceCollectionExtensions.cs b/src/scripting/Elsa.Scripting.JavaScript/Extensions/JavaScriptServiceCollectionExtensions.cs index 7adca3b9f..8184125ed 100644 --- a/src/scripting/Elsa.Scripting.JavaScript/Extensions/JavaScriptServiceCollectionExtensions.cs +++ b/src/scripting/Elsa.Scripting.JavaScript/Extensions/JavaScriptServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using Elsa.Scripting.JavaScript.Options; using Elsa.Scripting.JavaScript.Services; using System; using Elsa; +using Elsa.Activities.JavaScript; using Elsa.Expressions; using Elsa.Scripting.JavaScript.Typings; @@ -13,10 +14,11 @@ namespace Microsoft.Extensions.DependencyInjection public static IServiceCollection AddJavaScriptExpressionEvaluator(this IServiceCollection services) { return services - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() .TryAddProvider(ServiceLifetime.Scoped) .AddNotificationHandlers(typeof(JavaScriptServiceCollectionExtensions)); } @@ -27,5 +29,10 @@ namespace Microsoft.Extensions.DependencyInjection return services; } + + public static ElsaOptions AddJavaScriptActivities(this ElsaOptions options) + { + return options.AddActivitiesFrom(); + } } } \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Services/IJavaScriptService.cs b/src/scripting/Elsa.Scripting.JavaScript/Services/IJavaScriptService.cs new file mode 100644 index 000000000..e494bac90 --- /dev/null +++ b/src/scripting/Elsa.Scripting.JavaScript/Services/IJavaScriptService.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Services.Models; +using Jint; + +namespace Elsa.Scripting.JavaScript.Services +{ + public interface IJavaScriptService + { + Task EvaluateAsync(string expression, Type returnType, ActivityExecutionContext context, Action? configureEngine = default, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Services/ITypeScriptDefinitionService.cs b/src/scripting/Elsa.Scripting.JavaScript/Services/ITypeScriptDefinitionService.cs index 20feb717f..c1c0d88d8 100644 --- a/src/scripting/Elsa.Scripting.JavaScript/Services/ITypeScriptDefinitionService.cs +++ b/src/scripting/Elsa.Scripting.JavaScript/Services/ITypeScriptDefinitionService.cs @@ -1,10 +1,11 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; using Elsa.Models; namespace Elsa.Scripting.JavaScript.Services { public interface ITypeScriptDefinitionService { - string GenerateTypeScriptDefinition(WorkflowDefinition? workflowDefinition = default); + Task GenerateTypeScriptDefinitionsAsync(WorkflowDefinition? workflowDefinition = default, string? context = default, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Services/JavaScriptExpressionHandler.cs b/src/scripting/Elsa.Scripting.JavaScript/Services/JavaScriptExpressionHandler.cs index e1b552d0b..c3a9dd02f 100644 --- a/src/scripting/Elsa.Scripting.JavaScript/Services/JavaScriptExpressionHandler.cs +++ b/src/scripting/Elsa.Scripting.JavaScript/Services/JavaScriptExpressionHandler.cs @@ -1,146 +1,24 @@ using System; -using System.Dynamic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Elsa.Expressions; -using Elsa.Scripting.JavaScript.Converters; -using Elsa.Scripting.JavaScript.Extensions; -using Elsa.Scripting.JavaScript.Messages; -using Elsa.Scripting.JavaScript.Options; using Elsa.Services.Models; -using Jint; -using Jint.Native; -using MediatR; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NodaTime; -using NodaTime.Serialization.JsonNet; namespace Elsa.Scripting.JavaScript.Services { public class JavaScriptExpressionHandler : IExpressionHandler { + private readonly IJavaScriptService _javaScriptService; public const string SyntaxName = "JavaScript"; - private readonly IMediator _mediator; - private readonly IOptions _options; - private readonly JsonSerializerSettings _serializerSettings; - public JavaScriptExpressionHandler(IMediator mediator, IOptions options) + public JavaScriptExpressionHandler(IJavaScriptService javaScriptService) { - _mediator = mediator; - _options = options; - - _serializerSettings = new JsonSerializerSettings().ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - _serializerSettings.Converters.Add(new TruncatingNumberJsonConverter()); + _javaScriptService = javaScriptService; } public string Syntax => SyntaxName; - public async Task EvaluateAsync(string expression, Type returnType, ActivityExecutionContext context, CancellationToken cancellationToken) - { - var engine = new Engine(ConfigureJintEngine); - - await ConfigureEngineAsync(engine, context, cancellationToken); - engine.Execute(expression); - - return ConvertValue(engine.GetCompletionValue(), returnType); - } - - private void ConfigureJintEngine(Jint.Options options) - { - if (_options.Value.AllowClr) - options.AllowClr(); - } - - private async Task ConfigureEngineAsync(Engine engine, ActivityExecutionContext activityExecutionContext, CancellationToken cancellationToken) - { - await _mediator.Publish(new EvaluatingJavaScriptExpression(engine, activityExecutionContext), cancellationToken); - } - - private object? ConvertValue(JsValue value, Type targetType) - { - if (value.IsUndefined()) - return default; - - if (value.IsNull()) - return default; - - var targetIsObject = targetType == typeof(object); - - if (value.IsBoolean()) - return value.AsBoolean(); - - if ((targetIsObject || targetType == typeof(DateTime)) && value.IsDate()) - return value.AsDate().ToDateTime(); - - if (targetType.IsNumeric() && value.IsNumber()) - { - var numericType = targetType.IsNumeric() ? targetType : typeof(double); - return Convert.ChangeType(value.AsNumber(), numericType); - } - - if (targetType == typeof(string)) - return value.ToString(); - - if (value.IsString()) - { - var stringValue = value.AsString(); - return targetType != null - ? stringValue.Parse(targetType) - : value.AsString(); - } - - if (value.IsArray()) - { - var arrayInstance = value.AsArray(); - var elementType = targetType.GetElementType() ?? targetType.GenericTypeArguments.First(); - - if (elementType == typeof(byte)) - { - var bytes = new byte[arrayInstance.Length]; - - for (uint i = 0; i < arrayInstance.Length; i++) - { - var jsValue = arrayInstance[i]; - bytes[i] = (byte) jsValue.AsNumber(); - } - - return bytes; - } - - var array = Array.CreateInstance(elementType, arrayInstance.Length); - - for (uint i = 0; i < array.Length; i++) - { - var jsValue = arrayInstance[i]; - var convertedValue = ConvertValue(jsValue, elementType); - array.SetValue(convertedValue, i); - } - - return array; - } - - if (value.IsObject()) - { - var obj = value.AsObject().ToObject(); - - switch (obj) - { - case ExpandoObject _: - { - var json = JsonConvert.SerializeObject(obj, _serializerSettings); - return JsonConvert.DeserializeObject(json, targetType, _serializerSettings); - } - case JValue jValue: - return jValue.Value; - default: - return obj; - } - } - - throw new ArgumentException($"Value type {value.Type} is not supported.", nameof(value)); - } + public async Task EvaluateAsync(string expression, Type returnType, ActivityExecutionContext context, CancellationToken cancellationToken) => + await _javaScriptService.EvaluateAsync(expression, returnType, context, cancellationToken: cancellationToken); } } \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Services/JavaScriptService.cs b/src/scripting/Elsa.Scripting.JavaScript/Services/JavaScriptService.cs new file mode 100644 index 000000000..a9c841877 --- /dev/null +++ b/src/scripting/Elsa.Scripting.JavaScript/Services/JavaScriptService.cs @@ -0,0 +1,138 @@ +using System; +using System.Dynamic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Scripting.JavaScript.Converters; +using Elsa.Scripting.JavaScript.Extensions; +using Elsa.Scripting.JavaScript.Messages; +using Elsa.Scripting.JavaScript.Options; +using Elsa.Services.Models; +using Jint; +using Jint.Native; +using MediatR; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Serialization.JsonNet; + +namespace Elsa.Scripting.JavaScript.Services +{ + public class JavaScriptService : IJavaScriptService + { + private readonly IMediator _mediator; + private readonly ScriptOptions _options; + private readonly JsonSerializerSettings _serializerSettings; + + public JavaScriptService(IMediator mediator, IOptions options) + { + _mediator = mediator; + _options = options.Value; + + _serializerSettings = new JsonSerializerSettings().ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + _serializerSettings.Converters.Add(new TruncatingNumberJsonConverter()); + } + + public async Task EvaluateAsync(string expression, Type returnType, ActivityExecutionContext context, Action? configureEngine = default, CancellationToken cancellationToken = default) + { + var engine = new Engine(ConfigureJintEngine); + + configureEngine?.Invoke(engine); + await _mediator.Publish(new EvaluatingJavaScriptExpression(engine, context), cancellationToken); + engine.Execute(expression); + + return ConvertValue(engine.GetCompletionValue(), returnType); + } + + private void ConfigureJintEngine(Jint.Options options) + { + if (_options.AllowClr) + options.AllowClr(); + } + + private object? ConvertValue(JsValue value, Type targetType) + { + if (value.IsUndefined()) + return default; + + if (value.IsNull()) + return default; + + var targetIsObject = targetType == typeof(object); + + if (value.IsBoolean()) + return value.AsBoolean(); + + if ((targetIsObject || targetType == typeof(DateTime)) && value.IsDate()) + return value.AsDate().ToDateTime(); + + if (targetType.IsNumeric() && value.IsNumber()) + { + var numericType = targetType.IsNumeric() ? targetType : typeof(double); + return Convert.ChangeType(value.AsNumber(), numericType); + } + + if (targetType == typeof(string)) + return value.ToString(); + + if (value.IsString()) + { + var stringValue = value.AsString(); + return targetType != null + ? stringValue.Parse(targetType) + : value.AsString(); + } + + if (value.IsArray()) + { + var arrayInstance = value.AsArray(); + var elementType = targetType.GetElementType() ?? targetType.GenericTypeArguments.First(); + + if (elementType == typeof(byte)) + { + var bytes = new byte[arrayInstance.Length]; + + for (uint i = 0; i < arrayInstance.Length; i++) + { + var jsValue = arrayInstance[i]; + bytes[i] = (byte) jsValue.AsNumber(); + } + + return bytes; + } + + var array = Array.CreateInstance(elementType, arrayInstance.Length); + + for (uint i = 0; i < array.Length; i++) + { + var jsValue = arrayInstance[i]; + var convertedValue = ConvertValue(jsValue, elementType); + array.SetValue(convertedValue, i); + } + + return array; + } + + if (value.IsObject()) + { + var obj = value.AsObject().ToObject(); + + switch (obj) + { + case ExpandoObject _: + { + var json = JsonConvert.SerializeObject(obj, _serializerSettings); + return JsonConvert.DeserializeObject(json, targetType, _serializerSettings); + } + case JValue jValue: + return jValue.Value; + default: + return obj; + } + } + + throw new ArgumentException($"Value type {value.Type} is not supported.", nameof(value)); + } + } +} \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Services/TypeScriptDefinitionService.cs b/src/scripting/Elsa.Scripting.JavaScript/Services/TypeScriptDefinitionService.cs index c9453dd39..9b0fa1651 100644 --- a/src/scripting/Elsa.Scripting.JavaScript/Services/TypeScriptDefinitionService.cs +++ b/src/scripting/Elsa.Scripting.JavaScript/Services/TypeScriptDefinitionService.cs @@ -3,8 +3,12 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; +using System.Threading; +using System.Threading.Tasks; using AutoMapper.Internal; using Elsa.Models; +using Elsa.Scripting.JavaScript.Events; +using MediatR; using NodaTime; namespace Elsa.Scripting.JavaScript.Services @@ -12,17 +16,19 @@ namespace Elsa.Scripting.JavaScript.Services public class TypeScriptDefinitionService : ITypeScriptDefinitionService { private readonly IEnumerable _providers; + private readonly IMediator _mediator; - public TypeScriptDefinitionService(IEnumerable providers) + public TypeScriptDefinitionService(IEnumerable providers, IMediator mediator) { _providers = providers; + _mediator = mediator; } - public string GenerateTypeScriptDefinition(WorkflowDefinition? workflowDefinition = default) + public async Task GenerateTypeScriptDefinitionsAsync(WorkflowDefinition? workflowDefinition = default, string? context = default, CancellationToken cancellationToken = default) { var builder = new StringBuilder(); - var types = CollectTypes(workflowDefinition); - + var types = await CollectTypesAsync(workflowDefinition, cancellationToken); + // Render type declarations for anything except those listed in TypeConverters. foreach (var type in types) { @@ -31,6 +37,8 @@ namespace Elsa.Scripting.JavaScript.Services if (shouldRenderDeclaration) RenderTypeDeclaration(type, types, builder); } + + string GetTypeScriptTypeInternal(Type type) => GetTypeScriptType(type, types); if (workflowDefinition != null) { @@ -38,22 +46,25 @@ namespace Elsa.Scripting.JavaScript.Services if (contextType != null) { - var typeScriptType = GetTypeScriptType(contextType, types); + var typeScriptType = GetTypeScriptTypeInternal(contextType); builder.AppendLine($"declare const workflowContext: {typeScriptType}"); } foreach (var variable in workflowDefinition.Variables!.Data) { var variableType = variable.Value?.GetType() ?? typeof(object); - var typeScriptType = GetTypeScriptType(variableType, types); + var typeScriptType = GetTypeScriptTypeInternal(variableType); builder.AppendLine($"declare const {variable.Key}: {typeScriptType}"); } } + var renderingTypeScriptDefinitions = new RenderingTypeScriptDefinitions(workflowDefinition, GetTypeScriptTypeInternal, context, builder); + await _mediator.Publish(renderingTypeScriptDefinitions, cancellationToken); + return builder.ToString(); } - private HashSet CollectTypes(WorkflowDefinition? workflowDefinition = default) + private async Task> CollectTypesAsync(WorkflowDefinition? workflowDefinition = default, CancellationToken cancellationToken = default) { var collectedTypes = new HashSet(); @@ -75,6 +86,15 @@ namespace Elsa.Scripting.JavaScript.Services CollectType(collectedTypes); CollectType(collectedTypes); + void CollectTypesInternal(IEnumerable types) + { + foreach (var type in types) + CollectType(type, collectedTypes); + } + + var collectTypesEvent = new CollectingTypeScriptDefinitionTypes(workflowDefinition, CollectTypesInternal); + await _mediator.Publish(collectTypesEvent, cancellationToken); + return collectedTypes; } @@ -120,12 +140,12 @@ namespace Elsa.Scripting.JavaScript.Services } } - private void RenderTypeDeclaration(Type type, HashSet collectedTypes, StringBuilder output) + private void RenderTypeDeclaration(Type type, ISet collectedTypes, StringBuilder output) { RenderTypeDeclaration("class", type, collectedTypes, output); } - private void RenderTypeDeclaration(string symbol, Type type, HashSet collectedTypes, StringBuilder output) + private void RenderTypeDeclaration(string symbol, Type type, ISet collectedTypes, StringBuilder output) { var typeName = type.Name; var properties = type.GetProperties(); @@ -161,7 +181,7 @@ namespace Elsa.Scripting.JavaScript.Services output.AppendLine("}"); } - private string GetTypeScriptType(Type type, HashSet collectedTypes) + private string GetTypeScriptType(Type type, ISet collectedTypes) { if (type.IsNullableType()) type = type.GetTypeOfNullable(); diff --git a/src/server/Elsa.Server.Api/Endpoints/Scripting/JavaScript/TypeDefinitions/Get.cs b/src/server/Elsa.Server.Api/Endpoints/Scripting/JavaScript/TypeDefinitions/Get.cs index ea1546afa..a3a0f958b 100644 --- a/src/server/Elsa.Server.Api/Endpoints/Scripting/JavaScript/TypeDefinitions/Get.cs +++ b/src/server/Elsa.Server.Api/Endpoints/Scripting/JavaScript/TypeDefinitions/Get.cs @@ -33,11 +33,11 @@ namespace Elsa.Server.Api.Endpoints.Scripting.JavaScript.TypeDefinitions OperationId = "JavaScriptLanguageServices.GetTypeDefinitions", Tags = new[] { "JavaScriptLanguageServices" }) ] - public async Task Handle(string workflowDefinitionId, VersionOptions? version = default, CancellationToken cancellationToken = default) + public async Task Handle(string workflowDefinitionId, string? context = default, VersionOptions? version = default, CancellationToken cancellationToken = default) { version ??= VersionOptions.Latest; var workflowDefinition = await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowDefinitionId, version.Value, cancellationToken); - var typeDefinitions = _typeScriptDefinitionService.GenerateTypeScriptDefinition(workflowDefinition); + var typeDefinitions = await _typeScriptDefinitionService.GenerateTypeScriptDefinitionsAsync(workflowDefinition, context, cancellationToken); var fileName = $"elsa.{workflowDefinitionId}.d.ts"; var data = Encoding.UTF8.GetBytes(typeDefinitions); return File(data, "application/x-typescript", fileName);
{fieldHint}