Add RunJavaScript activity
This commit is contained in:
parent
dc95276abb
commit
d7d7bd4a7e
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -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']);
|
||||
|
|
@ -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/)*
|
||||
|
|
@ -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>;
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ namespace ElsaDashboard.Samples.Monolith
|
|||
.AddHttpActivities(elsaSection.GetSection("Http").Bind)
|
||||
.AddEmailActivities(elsaSection.GetSection("Smtp").Bind)
|
||||
.AddQuartzTemporalActivities()
|
||||
.AddJavaScriptActivities()
|
||||
.AddWorkflowsFrom<Startup>()
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue