Add RunJavaScript activity

This commit is contained in:
Sipke Schoorstra 2021-03-06 13:04:16 +01:00
parent dc95276abb
commit d7d7bd4a7e
32 changed files with 718 additions and 165 deletions

View file

@ -29,5 +29,10 @@ namespace Elsa.Attributes
/// A value representing options specific to a given UI hint.
/// </summary>
public object? Options { get; set; }
/// <summary>
/// The name of a static method that provides options.
/// </summary>
public string? OptionsProvider { get; set; }
}
}

View file

@ -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";
/// <summary>
/// A list that allows the user to enter zero or more values.

View file

@ -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;

View file

@ -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<HTMLElsaModalDialogElement>;
"elsa-monaco": LocalJSX.ElsaMonaco & JSXBase.HTMLAttributes<HTMLElsaMonacoElement>;
"elsa-multi-text-property": LocalJSX.ElsaMultiTextProperty & JSXBase.HTMLAttributes<HTMLElsaMultiTextPropertyElement>;
"elsa-script-property": LocalJSX.ElsaScriptProperty & JSXBase.HTMLAttributes<HTMLElsaScriptPropertyElement>;
"elsa-text-property": LocalJSX.ElsaTextProperty & JSXBase.HTMLAttributes<HTMLElsaTextPropertyElement>;
"elsa-workflow-editor": LocalJSX.ElsaWorkflowEditor & JSXBase.HTMLAttributes<HTMLElsaWorkflowEditorElement>;
"elsa-workflow-publish-button": LocalJSX.ElsaWorkflowPublishButton & JSXBase.HTMLAttributes<HTMLElsaWorkflowPublishButtonElement>;

View file

@ -51,7 +51,7 @@ export class ElsaDesignerTreeActivity {
return (
<Host id={`activity-${activityId}`}
class="activity border-2 border-solid border-white rounded bg-white text-left text-black text-lg hover:border-blue-600 select-none max-w-md shadow-sm relative">
<div class="p-5 border-b border-b-solid">
<div class="p-5">
<div class="flex justify-between space-x-8">
<div class="flex-shrink-0">
{displayContext.activityIcon}
@ -109,7 +109,7 @@ export class ElsaDesignerTreeActivity {
return undefined;
return (
<div class="p-6 text-gray-400 text-sm">
<div class="p-6 text-gray-400 text-sm border-t border-t-solid">
{this.displayContext.bodyDisplay}
</div>
)

View file

@ -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 <div>
<div class="flex">
<div class="">
<label htmlFor={fieldId} class="block text-sm font-medium text-gray-700">
{fieldLabel}
</label>
</div>
<div class="flex-1">
<div>
<div class="hidden sm:block">
<nav class="flex flex-row-reverse" aria-label="Tabs">
<span class="bg-blue-100 text-blue-700 px-3 py-2 font-medium text-sm rounded-md">
{syntax}
</span>
</nav>
</div>
</div>
</div>
</div>
<div class="mt-1">
<elsa-monaco value={value}
language={monacoLanguage}
editor-height={this.editorHeight}
single-line={this.singleLineMode}
onValueChanged={e => this.onMonacoValueChanged(e.detail)}
ref={el => this.monacoEditor = el}/>
</div>
{fieldHint ? <p class="mt-2 text-sm text-gray-500">{fieldHint}</p> : undefined}
<input type="hidden" name={fieldName} value={value}/>
</div>
}
}
Tunnel.injectProps(ElsaScriptProperty, ['serverUrl', 'workflowDefinitionId']);

View file

@ -0,0 +1,37 @@
# elsa-modal-dialog
<!-- Auto Generated Below -->
## 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/)*

View file

@ -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 <div>
@ -86,18 +87,18 @@ export class ElsaTextProperty {
<div class="sm:hidden">
<label htmlFor="tabs" class="sr-only">Select a tab</label>
<select id="tabs" name="tabs" onChange={e => 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 <option selected={syntax == selectedSyntax}>{syntax}</option>;
})}
</select>
</div>
<div class="hidden sm:block">
<nav class="flex flex-row-reverse" aria-label="Tabs">
{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 <a href="#" onClick={e => this.onSyntaxSelect(e, syntax)} data-syntax={syntax} class={`${className} px-3 py-2 font-medium text-sm rounded-md`}>
return <a href="#" onClick={e => this.onSyntaxSelect(e, syntax)} class={`${className} px-3 py-2 font-medium text-sm rounded-md`}>
{syntax}
</a>;
})}

View file

@ -0,0 +1,15 @@
import {h} from "@stencil/core";
export const ForkIcon = props =>
(
<span class="rounded-lg inline-flex p-3 bg-light-blue-50 text-light-blue-700 ring-4 ring-white">
<svg class="h-6 w-6 transform rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="18" r="2"/>
<circle cx="7" cy="6" r="2"/>
<circle cx="17" cy="6" r="2"/>
<path d="M7 8v2a2 2 0 0 0 2 2h6a2 2 0 0 0 2 -2v-2"/>
<line x1="12" y1="12" x2="12" y2="16"/>
</svg>
</span>
);

View file

@ -0,0 +1,15 @@
import {h} from "@stencil/core";
export const JoinIcon = props =>
(
<span class="rounded-lg inline-flex p-3 bg-light-blue-50 text-light-blue-700 ring-4 ring-white">
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="18" r="2"/>
<circle cx="7" cy="6" r="2"/>
<circle cx="17" cy="6" r="2"/>
<path d="M7 8v2a2 2 0 0 0 2 2h6a2 2 0 0 0 2 -2v-2"/>
<line x1="12" y1="12" x2="12" y2="16"/>
</svg>
</span>
);

View file

@ -0,0 +1,13 @@
import {h} from "@stencil/core";
export const ScriptIcon = props =>
(
<span class={`rounded-lg inline-flex p-3 bg-light-blue-50 text-light-blue-700 ring-4 ring-white`}>
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke="none" d="M0 0h24v24H0z"/>
<polyline points="7 8 3 12 7 16"/>
<polyline points="17 8 21 12 17 16"/>
<line x1="14" y1="4" x2="10" y2="20"/>
</svg>
</span>
);

View file

@ -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<T extends PropertyDisplayDriver>(controlType: string, c: new () => T) {

View file

@ -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 = <ActivityIcon/>;
context.activityIcon = <ForkIcon/>;
}
onActivityDesignDisplaying(context: ActivityDesignDisplayContext) {
@ -31,7 +31,7 @@ export class ForkPlugin implements ElsaPlugin {
const expression = branches.expression;
const branchList = parseJson(expression) || [];
context.activityIcon = <ActivityIcon/>;
context.activityIcon = <ForkIcon/>;
context.outcomes = branchList;
}
}

View file

@ -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 = <ScriptIcon/>;
}
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 = <ScriptIcon/>;
context.outcomes = outcomeList;
context.bodyDisplay = undefined;
}
}

View file

@ -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 (
<div class="sm:col-span-6">
<elsa-script-property propertyDescriptor={property} propertyModel={prop} editor-height={editorHeight} syntax={syntax} context={context}/>
</div>
);
}
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';
}
}
}

View file

@ -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 (
<div class="sm:col-span-6">
<elsa-text-property propertyDescriptor={property} propertyModel={prop} editor-height="8em"/>
<elsa-text-property propertyDescriptor={property} propertyModel={prop} editor-height={editorHeight} context={context}/>
</div>
)
);
}
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';
}
}
}

View file

@ -27,8 +27,9 @@ export const createElsaClient = function (serverUrl: string): ElsaClient {
}
},
scriptingApi: {
getJavaScriptTypeDefinitions: async (workflowDefinitionId: string): Promise<string> => {
const response = await httpClient.get<string>(`v1/scripting/javascript/type-definitions/${workflowDefinitionId}?t=${new Date().getTime()}`);
getJavaScriptTypeDefinitions: async (workflowDefinitionId: string, context?: string): Promise<string> => {
context = context || '';
const response = await httpClient.get<string>(`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<string>
getJavaScriptTypeDefinitions(workflowDefinitionId: string, context?: string): Promise<string>
}
export interface SaveWorkflowDefinitionRequest {

View file

@ -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()
];
}

View file

@ -35,6 +35,7 @@ namespace ElsaDashboard.Samples.Monolith
.AddHttpActivities(elsaSection.GetSection("Http").Bind)
.AddEmailActivities(elsaSection.GetSection("Smtp").Bind)
.AddQuartzTemporalActivities()
.AddJavaScriptActivities()
.AddWorkflowsFrom<Startup>()
);

View file

@ -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<RenderingTypeScriptDefinitions>
{
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<string> Outcomes { get; set; } = new List<string> { OutcomeNames.Done };
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
var script = Script;
if (script is null or "")
return Done();
var outcomes = new HashSet<string>();
void ConfigureEngine(Engine engine)
{
void SetOutcome(string value) => outcomes.Add(value);
void SetOutcomes(IEnumerable<string> values) => outcomes.AddRange(values);
engine.SetValue("setOutcome", (Action<string>) SetOutcome);
engine.SetValue("setOutcomes", (Action<IEnumerable<string>>) 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<string>);");
return Task.CompletedTask;
}
}
}

View file

@ -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<ISetupActivity<RunJavaScript>> 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);
}
}

View file

@ -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<RunJavaScript> WithScript(this ISetupActivity<RunJavaScript> activity, Func<ActivityExecutionContext, ValueTask<string?>> value) => activity.Set(x => x.Script, value);
public static ISetupActivity<RunJavaScript> WithScript(this ISetupActivity<RunJavaScript> activity, Func<ActivityExecutionContext, string?> value) => activity.Set(x => x.Script, value);
public static ISetupActivity<RunJavaScript> WithScript(this ISetupActivity<RunJavaScript> activity, Func<ValueTask<string?>> value) => activity.Set(x => x.Script, value);
public static ISetupActivity<RunJavaScript> WithScript(this ISetupActivity<RunJavaScript> activity, Func<string?> value) => activity.Set(x => x.Script, value);
public static ISetupActivity<RunJavaScript> WithScript(this ISetupActivity<RunJavaScript> activity, string? value) => activity.Set(x => x.Script, value);
}
}

View file

@ -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<IEnumerable<Type>> collectTypes)
{
WorkflowDefinition = workflowDefinition;
CollectTypes = collectTypes;
}
private Action<IEnumerable<Type>> CollectTypes { get; }
public WorkflowDefinition? WorkflowDefinition { get; }
public void CollectType<T>() => CollectType(typeof(T));
public void CollectType(Type type) => CollectTypes(new[] { type });
}
}

View file

@ -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<Type, string> getTypeScriptType, string? context, StringBuilder output)
{
WorkflowDefinition = workflowDefinition;
GetTypeScriptTypeInternal = getTypeScriptType;
Context = context;
Output = output;
}
private Func<Type, string> GetTypeScriptTypeInternal { get; }
public string? Context { get; }
public StringBuilder Output { get; }
public WorkflowDefinition? WorkflowDefinition { get; }
public string GetTypeScriptType(Type type) => GetTypeScriptTypeInternal(type);
public string GetTypeScriptType<T>() => GetTypeScriptType(typeof(Type));
}
}

View file

@ -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<ITypeScriptDefinitionService, TypeScriptDefinitionService>()
.AddSingleton<ITypeDefinitionProvider, PrimitiveTypeDefinitionProvider>()
.AddSingleton<ITypeDefinitionProvider, EnumTypeDefinitionProvider>()
.AddSingleton<ITypeDefinitionProvider, EnumerableTypeDefinitionProvider>()
.AddScoped<ITypeScriptDefinitionService, TypeScriptDefinitionService>()
.AddScoped<ITypeDefinitionProvider, PrimitiveTypeDefinitionProvider>()
.AddScoped<ITypeDefinitionProvider, EnumTypeDefinitionProvider>()
.AddScoped<ITypeDefinitionProvider, EnumerableTypeDefinitionProvider>()
.AddScoped<IJavaScriptService, JavaScriptService>()
.TryAddProvider<IExpressionHandler, JavaScriptExpressionHandler>(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<RunJavaScript>();
}
}
}

View file

@ -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<object?> EvaluateAsync(string expression, Type returnType, ActivityExecutionContext context, Action<Engine>? configureEngine = default, CancellationToken cancellationToken = default);
}
}

View file

@ -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<string> GenerateTypeScriptDefinitionsAsync(WorkflowDefinition? workflowDefinition = default, string? context = default, CancellationToken cancellationToken = default);
}
}

View file

@ -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<ScriptOptions> _options;
private readonly JsonSerializerSettings _serializerSettings;
public JavaScriptExpressionHandler(IMediator mediator, IOptions<ScriptOptions> 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<object?> 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<object?> EvaluateAsync(string expression, Type returnType, ActivityExecutionContext context, CancellationToken cancellationToken) =>
await _javaScriptService.EvaluateAsync(expression, returnType, context, cancellationToken: cancellationToken);
}
}

View file

@ -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<ScriptOptions> options)
{
_mediator = mediator;
_options = options.Value;
_serializerSettings = new JsonSerializerSettings().ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
_serializerSettings.Converters.Add(new TruncatingNumberJsonConverter());
}
public async Task<object?> EvaluateAsync(string expression, Type returnType, ActivityExecutionContext context, Action<Engine>? 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));
}
}
}

View file

@ -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<ITypeDefinitionProvider> _providers;
private readonly IMediator _mediator;
public TypeScriptDefinitionService(IEnumerable<ITypeDefinitionProvider> providers)
public TypeScriptDefinitionService(IEnumerable<ITypeDefinitionProvider> providers, IMediator mediator)
{
_providers = providers;
_mediator = mediator;
}
public string GenerateTypeScriptDefinition(WorkflowDefinition? workflowDefinition = default)
public async Task<string> 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<Type> CollectTypes(WorkflowDefinition? workflowDefinition = default)
private async Task<ISet<Type>> CollectTypesAsync(WorkflowDefinition? workflowDefinition = default, CancellationToken cancellationToken = default)
{
var collectedTypes = new HashSet<Type>();
@ -75,6 +86,15 @@ namespace Elsa.Scripting.JavaScript.Services
CollectType<LocalTime>(collectedTypes);
CollectType<LocalDateTime>(collectedTypes);
void CollectTypesInternal(IEnumerable<Type> 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<Type> collectedTypes, StringBuilder output)
private void RenderTypeDeclaration(Type type, ISet<Type> collectedTypes, StringBuilder output)
{
RenderTypeDeclaration("class", type, collectedTypes, output);
}
private void RenderTypeDeclaration(string symbol, Type type, HashSet<Type> collectedTypes, StringBuilder output)
private void RenderTypeDeclaration(string symbol, Type type, ISet<Type> 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<Type> collectedTypes)
private string GetTypeScriptType(Type type, ISet<Type> collectedTypes)
{
if (type.IsNullableType())
type = type.GetTypeOfNullable();

View file

@ -33,11 +33,11 @@ namespace Elsa.Server.Api.Endpoints.Scripting.JavaScript.TypeDefinitions
OperationId = "JavaScriptLanguageServices.GetTypeDefinitions",
Tags = new[] { "JavaScriptLanguageServices" })
]
public async Task<IActionResult> Handle(string workflowDefinitionId, VersionOptions? version = default, CancellationToken cancellationToken = default)
public async Task<IActionResult> 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);