Incremental work on expression editor

This commit is contained in:
Sipke Schoorstra 2022-01-11 13:28:21 +01:00
parent fe2b4b75cf
commit 848d9d7b5d
16 changed files with 376 additions and 45 deletions

View file

@ -5,12 +5,12 @@ namespace Elsa.Expressions;
public class ElsaExpression : IExpression
{
public ElsaExpression(string scriptExpression)
public ElsaExpression(string value)
{
ScriptExpression = scriptExpression;
Value = value;
}
public string ScriptExpression { get; }
public string Value { get; }
}
public class ElsaExpressionHandler : IExpressionHandler
@ -20,6 +20,6 @@ public class ElsaExpressionHandler : IExpressionHandler
var elsaExpression = (ElsaExpression)expression;
// TODO: evaluate expression.
return new ValueTask<object?>(elsaExpression.ScriptExpression);
return new ValueTask<object?>(elsaExpression.Value);
}
}

View file

@ -33,7 +33,7 @@ public class InputJsonConverter<T> : JsonConverter<Input<T>>
if (!expressionElement.TryGetProperty("type", out var expressionTypeNameElement))
throw new JsonException("Failed to extract expression type property");
var expressionTypeName = expressionTypeNameElement.GetString()!;
var expressionTypeName = expressionTypeNameElement.GetString() ?? "Literal";
var expressionSyntaxDescriptor = _expressionSyntaxRegistry.Find(expressionTypeName);
if (expressionSyntaxDescriptor == null)

View file

@ -5,13 +5,14 @@
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { ActionDefinition, ActionInvokedArgs, Activity, ActivityDescriptor, ActivitySelectedArgs, ContainerSelectedArgs, GraphUpdatedArgs, SelectListItem, TabChangedArgs, TabDefinition, Trigger, TriggerDescriptor, Workflow, WorkflowInstance, WorkflowInstanceSummary, WorkflowSummary } from "./models";
import { ActionDefinition, ActionInvokedArgs, Activity, ActivityDescriptor, ActivitySelectedArgs, ContainerSelectedArgs, GraphUpdatedArgs, IntellisenseContext, SelectListItem, TabChangedArgs, TabDefinition, Trigger, TriggerDescriptor, Workflow, WorkflowInstance, WorkflowInstanceSummary, WorkflowSummary } from "./models";
import { ActivityUpdatedArgs, DeleteActivityRequestedArgs } from "./components/designer/activity-properties-editor/activity-properties-editor";
import { AddActivityArgs } from "./components/designer/canvas/canvas";
import { NodeInputContext } from "./services/node-input-driver";
import { MenuItem } from "./components/shared/context-menu/models";
import { DropdownButtonItem, DropdownButtonOrigin } from "./components/shared/dropdown-button/models";
import { AddActivityArgs as AddActivityArgs1 } from "./components/designer/canvas/canvas";
import { ExpressionChangedArs } from "./components/designer/input-control-switch/input-control-switch";
import { MonacoLib, MonacoValueChangedArgs } from "./components/shared/monaco-editor/monaco-editor";
import { PagerData } from "./components/shared/pager/pager";
import { PanelPosition, PanelStateChangedArgs } from "./components/designer/panel/models";
@ -69,6 +70,19 @@ export namespace Components {
"selectedTabIndex"?: number;
"tabs": Array<TabDefinition>;
}
interface ElsaInputControlSwitch {
"codeEditorHeight": string;
"codeEditorSingleLineMode": boolean;
"context"?: IntellisenseContext;
"defaultSyntax": string;
"expression"?: string;
"fieldName"?: string;
"hint": string;
"isReadOnly"?: boolean;
"label": string;
"supportedSyntaxes": Array<string>;
"syntax"?: string;
}
interface ElsaInputTags {
"fieldId"?: string;
"placeHolder"?: string;
@ -93,7 +107,6 @@ export namespace Components {
"monacoLibPath"?: string;
"padding": string;
"setJavaScriptLibs": (libs: Array<MonacoLib>) => Promise<void>;
"setValue": (value: string) => Promise<void>;
"singleLineMode": boolean;
"value": string;
}
@ -240,6 +253,12 @@ declare global {
prototype: HTMLElsaFormPanelElement;
new (): HTMLElsaFormPanelElement;
};
interface HTMLElsaInputControlSwitchElement extends Components.ElsaInputControlSwitch, HTMLStencilElement {
}
var HTMLElsaInputControlSwitchElement: {
prototype: HTMLElsaInputControlSwitchElement;
new (): HTMLElsaInputControlSwitchElement;
};
interface HTMLElsaInputTagsElement extends Components.ElsaInputTags, HTMLStencilElement {
}
var HTMLElsaInputTagsElement: {
@ -394,6 +413,7 @@ declare global {
"elsa-dropdown-input": HTMLElsaDropdownInputElement;
"elsa-flowchart": HTMLElsaFlowchartElement;
"elsa-form-panel": HTMLElsaFormPanelElement;
"elsa-input-control-switch": HTMLElsaInputControlSwitchElement;
"elsa-input-tags": HTMLElsaInputTagsElement;
"elsa-input-tags-dropdown": HTMLElsaInputTagsDropdownElement;
"elsa-modal-dialog": HTMLElsaModalDialogElement;
@ -466,6 +486,21 @@ declare namespace LocalJSX {
"selectedTabIndex"?: number;
"tabs"?: Array<TabDefinition>;
}
interface ElsaInputControlSwitch {
"codeEditorHeight"?: string;
"codeEditorSingleLineMode"?: boolean;
"context"?: IntellisenseContext;
"defaultSyntax"?: string;
"expression"?: string;
"fieldName"?: string;
"hint"?: string;
"isReadOnly"?: boolean;
"label"?: string;
"onExpressionChanged"?: (event: CustomEvent<ExpressionChangedArs>) => void;
"onSyntaxChanged"?: (event: CustomEvent<string>) => void;
"supportedSyntaxes"?: Array<string>;
"syntax"?: string;
}
interface ElsaInputTags {
"fieldId"?: string;
"onValueChanged"?: (event: CustomEvent<Array<string>>) => void;
@ -596,6 +631,7 @@ declare namespace LocalJSX {
"elsa-dropdown-input": ElsaDropdownInput;
"elsa-flowchart": ElsaFlowchart;
"elsa-form-panel": ElsaFormPanel;
"elsa-input-control-switch": ElsaInputControlSwitch;
"elsa-input-tags": ElsaInputTags;
"elsa-input-tags-dropdown": ElsaInputTagsDropdown;
"elsa-modal-dialog": ElsaModalDialog;
@ -635,6 +671,7 @@ declare module "@stencil/core" {
"elsa-dropdown-input": LocalJSX.ElsaDropdownInput & JSXBase.HTMLAttributes<HTMLElsaDropdownInputElement>;
"elsa-flowchart": LocalJSX.ElsaFlowchart & JSXBase.HTMLAttributes<HTMLElsaFlowchartElement>;
"elsa-form-panel": LocalJSX.ElsaFormPanel & JSXBase.HTMLAttributes<HTMLElsaFormPanelElement>;
"elsa-input-control-switch": LocalJSX.ElsaInputControlSwitch & JSXBase.HTMLAttributes<HTMLElsaInputControlSwitchElement>;
"elsa-input-tags": LocalJSX.ElsaInputTags & JSXBase.HTMLAttributes<HTMLElsaInputTagsElement>;
"elsa-input-tags-dropdown": LocalJSX.ElsaInputTagsDropdown & JSXBase.HTMLAttributes<HTMLElsaInputTagsDropdownElement>;
"elsa-modal-dialog": LocalJSX.ElsaModalDialog & JSXBase.HTMLAttributes<HTMLElsaModalDialogElement>;

View file

@ -127,7 +127,7 @@ export class ActivityPropertiesEditor {
type: inputDescriptor.type,
expression: {
type: syntax,
value: propertyValue
value: propertyValue // TODO: The "value" field is currently hardcoded, but we should be able to be more flexible and potentially have different fields for a given syntax.
}
};
@ -146,7 +146,10 @@ export class ActivityPropertiesEditor {
</FormEntry>
{properties.filter(x => !!x.inputControl).map(propertyContext => {
return propertyContext.inputControl;
const key = `${node.id}-${propertyContext.inputContext.inputDescriptor.name}`;
return <div key={key}>
{propertyContext.inputControl}
</div>;
})}
</div>
};

View file

@ -0,0 +1,194 @@
import {Component, Event, EventEmitter, Listen, h, Prop, State} from '@stencil/core';
import {IntellisenseContext, SyntaxNames} from "../../../models";
import {enter, leave, toggle} from 'el-transition'
import {SyntaxSelectorIcon} from "../../icons/tooling/syntax-selector";
import {MonacoValueChangedArgs} from "../../shared/monaco-editor/monaco-editor";
import {Hint} from "../../shared/forms/hint";
export interface ExpressionChangedArs {
expression: string;
syntax: string;
}
@Component({
tag: 'elsa-input-control-switch',
shadow: false,
})
export class InputControlSwitch {
private contextMenu: HTMLElement;
private monacoEditor: HTMLElsaMonacoEditorElement;
private defaultSyntaxValue: string;
private contextMenuWidget: HTMLElement;
@Prop() label: string;
@Prop() hint: string;
@Prop() fieldName?: string;
@Prop() syntax?: string;
@Prop() expression?: string;
@Prop() defaultSyntax: string = SyntaxNames.Literal;
@Prop() supportedSyntaxes: Array<string> = [];
@Prop() isReadOnly?: boolean;
@Prop() codeEditorHeight: string = '10em';
@Prop() codeEditorSingleLineMode: boolean = false;
@Prop() context?: IntellisenseContext;
@Event() syntaxChanged: EventEmitter<string>;
@Event() expressionChanged: EventEmitter<ExpressionChangedArs>;
@State() currentExpression?: string;
@Listen('click', {target: 'window'})
private onWindowClicked(event: Event) {
const target = event.target as HTMLElement;
if (!this.contextMenuWidget || !this.contextMenuWidget.contains(target))
this.closeContextMenu();
}
public componentWillLoad() {
this.currentExpression = this.expression;
}
public render() {
return <div class="p-4">
<div class="mb-1">
<div class="flex">
<div class="flex-1">
{this.renderLabel()}
</div>
{this.renderContextMenuWidget()}
</div>
</div>
{this.renderEditor()}
</div>
}
private renderLabel() {
if (!this.label)
return undefined;
const fieldId = this.fieldName;
const fieldLabel = this.label || fieldId;
return <label htmlFor={fieldId} class="block text-sm font-medium text-gray-700">
{fieldLabel}
</label>;
}
private renderContextMenuWidget() {
if (this.supportedSyntaxes.length == 0)
return undefined;
const selectedSyntax = this.syntax;
const advancedButtonClass = selectedSyntax ? 'text-blue-500' : 'text-gray-300'
return <div class="relative" ref={el => this.contextMenuWidget = el}>
<button type="button" class={`border-0 focus:outline-none text-sm ${advancedButtonClass}`} onClick={e => this.onSettingsClick(e)}>
{!this.isReadOnly ? this.renderContextMenuButton() : ""}
</button>
<div>
<div ref={el => this.contextMenu = el}
data-transition-enter="transition ease-out duration-100"
data-transition-enter-start="transform opacity-0 scale-95"
data-transition-enter-end="transform opacity-100 scale-100"
data-transition-leave="transition ease-in duration-75"
data-transition-leave-start="transform opacity-100 scale-100"
data-transition-leave-end="transform opacity-0 scale-95"
class="hidden origin-top-right absolute right-1 mt-1 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 divide-y divide-gray-100 focus:outline-none z-10" role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu">
<div class="py-1" role="none">
<a onClick={e => this.selectSyntax(e, null)} href="#" class={`block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900 ${!selectedSyntax ? 'text-blue-700' : 'text-gray-700'}`} role="menuitem">Default</a>
</div>
<div class="py-1" role="none">
{this.supportedSyntaxes.map(syntax => (
<a onClick={e => this.selectSyntax(e, syntax)} href="#"
class={`block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900 ${selectedSyntax == syntax ? 'text-blue-700' : 'text-gray-700'}`} role="menuitem">{syntax}</a>
))}
</div>
</div>
</div>
</div>;
}
private renderContextMenuButton = () => this.syntax ? <span>{this.syntax}</span> : <SyntaxSelectorIcon/>;
private renderEditor = () => {
const selectedSyntax = this.syntax;
const monacoLanguage = this.mapSyntaxToLanguage(selectedSyntax);
const value = this.expression;
const expressionEditorClass = selectedSyntax ? 'block' : 'hidden';
const defaultEditorClass = selectedSyntax ? 'hidden' : 'block';
return (
<div>
<div class={expressionEditorClass}>
<elsa-monaco-editor
value={value}
language={monacoLanguage}
editor-height={this.codeEditorHeight}
single-line={this.codeEditorSingleLineMode}
onValueChanged={e => this.onExpressionChanged(e.detail)}
ref={el => this.monacoEditor = el}/>
</div>
<div class={defaultEditorClass}>
<slot/>
</div>
<Hint text={this.hint}/>
</div>
);
}
private mapSyntaxToLanguage = (syntax: string): string => {
switch (syntax) {
case 'Json':
return 'json';
case 'JavaScript':
return 'javascript';
case 'Liquid':
return 'handlebars';
case 'Literal':
default:
return 'plaintext';
}
};
private toggleContextMenu() {
toggle(this.contextMenu);
}
private openContextMenu() {
enter(this.contextMenu);
}
private closeContextMenu() {
if (!!this.contextMenu)
leave(this.contextMenu);
}
private selectDefaultEditor(e: Event) {
e.preventDefault();
this.syntax = undefined;
this.closeContextMenu();
}
private async selectSyntax(e: Event, syntax: string) {
e.preventDefault();
this.syntax = syntax;
this.syntaxChanged.emit(syntax);
this.expressionChanged.emit({expression: this.currentExpression, syntax});
this.closeContextMenu();
}
private onSettingsClick(e: Event) {
this.toggleContextMenu();
}
private onExpressionChanged(e: MonacoValueChangedArgs) {
const expression = e.value;
this.currentExpression = expression;
this.expressionChanged.emit({expression, syntax: this.syntax});
}
}

View file

@ -0,0 +1,10 @@
import {FunctionalComponent, h} from "@stencil/core";
export const SyntaxSelectorIcon: FunctionalComponent = () =>
<svg class="h-5 w-5 text-gray-400" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="12" r="9"/>
<line x1="8" y1="12" x2="8" y2="12.01"/>
<line x1="12" y1="12" x2="12" y2="12.01"/>
<line x1="16" y1="12" x2="16" y2="12.01"/>
</svg>;

View file

@ -1,27 +1,41 @@
import {Component, Prop, h} from '@stencil/core';
import {Component, h, Prop} from '@stencil/core';
import {LiteralExpression, SyntaxNames} from "../../models";
import {NodeInputContext} from "../../services/node-input-driver";
import {getInputPropertyValue} from "../../utils";
import {ExpressionChangedArs} from "../designer/input-control-switch/input-control-switch";
@Component({
tag: 'elsa-single-line-input',
shadow: false
})
export class SingleLineInput {
@Prop() inputContext: NodeInputContext;
@Prop() public inputContext: NodeInputContext;
public render() {
const inputContext = this.inputContext;
const inputProperty = inputContext.inputDescriptor;
const fieldName = inputProperty.name;
const fieldId = inputProperty.name;
const inputDescriptor = inputContext.inputDescriptor;
const fieldName = inputDescriptor.name;
const fieldId = inputDescriptor.name;
const displayName = inputDescriptor.displayName;
const hint = inputDescriptor.description;
const input = getInputPropertyValue(inputContext);
const value = (input?.expression as LiteralExpression)?.value;
return <input type="text" name={fieldName} id={fieldId} value={value} onChange={this.onPropertyEditorChanged}/>
const value = (input?.expression as LiteralExpression)?.value; // TODO: The "value" field is currently hardcoded, but we should be able to be more flexible and potentially have different fields for a given syntax.
const syntax = input?.expression?.type ?? inputDescriptor.defaultSyntax;
const supportedSyntaxes = ['Literal', 'JavaScript', 'Liquid'];
return (
<elsa-input-control-switch label={displayName} hint={hint} supportedSyntaxes={supportedSyntaxes} syntax={syntax} expression={value} onExpressionChanged={this.onExpressionChanged}>
<input type="text" name={fieldName} id={fieldId} value={value} onChange={this.onPropertyEditorChanged}/>
</elsa-input-control-switch>
);
}
private onPropertyEditorChanged = (e: Event) => {
const inputElement = e.target as HTMLInputElement;
this.inputContext.inputChanged(inputElement.value, SyntaxNames.Literal);
}
private onExpressionChanged = (e: CustomEvent<ExpressionChangedArs>) => {
this.inputContext.inputChanged(e.detail.expression, e.detail.syntax);
}
}

View file

@ -39,45 +39,45 @@ export class MonacoEditor {
this.monaco.editor.setModelLanguage(model, this.language);
}
@Method()
async setValue(value: string) {
@Watch('value')
valueChangeHandler(newValue: string) {
if (!this.editor)
return;
const model = this.editor.getModel();
model.setValue(value || '');
model.setValue(newValue || '');
}
@Method()
async setJavaScriptLibs(libs: Array<MonacoLib>) {
const monaco = this.monaco;
const editor = this.editor;
const defaultLib: MonacoLib = {
content: "<reference lib=\"es5\" />",
filePath: "lib.d.ts"
};
libs = [defaultLib, ...libs];
libs = [...libs];
monaco.languages.typescript.javascriptDefaults.setExtraLibs(libs);
for (const lib of libs) {
const oldModel = monaco.editor.getModel(lib.filePath);
const oldModel = editor.getModel(lib.filePath);
if (oldModel)
oldModel.dispose();
monaco.editor.createModel(lib.content, 'typescript', monaco.Uri.parse(lib.filePath));
const newModel = monaco.editor.createModel(lib.content, 'typescript', monaco.Uri.parse(lib.filePath));
}
}
async componentWillLoad() {
public async componentWillLoad() {
const monacoLibPath = this.monacoLibPath || globalState.monacoLibPath;
this.monaco = await initializeMonacoWorker(monacoLibPath);
await this.setJavaScriptLibs([]);
this.registerLiquid();
}
componentDidLoad() {
public async componentDidLoad() {
const monaco = this.monaco;
const language = this.language;
@ -135,6 +135,9 @@ export class MonacoEditor {
this.editor = monaco.editor.create(this.container, options);
await this.setJavaScriptLibs([]);
this.registerLiquid();
this.editor.onDidChangeModelContent(e => {
const value = this.editor.getValue();
const markers = monaco.editor.getModelMarkers({owner: language});
@ -207,7 +210,7 @@ export class MonacoEditor {
}
render() {
const padding = this.padding || 'elsa-pt-1.5 elsa-pl-1';
const padding = this.padding || 'pt-1.5 pl-1';
return (
<Host
class="monaco-editor-host border focus:ring-blue-500 focus:border-blue-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300 p-4"

View file

@ -20,17 +20,7 @@ export class DefaultInputDriver implements NodeInputDriver {
const inputDescriptor = context.inputDescriptor;
const uiHint = inputDescriptor.uiHint;
const inputControl = this.inputControlRegistry.get(uiHint);
const node = context.node;
const propertyName = inputDescriptor.name;
const displayName = inputDescriptor.displayName || propertyName;
const description = inputDescriptor.description;
const fieldId = inputDescriptor.name;
const key = `${node.id}_${propertyName}`;
return (
<FormEntry label={displayName} fieldId={fieldId} hint={description} key={key}>
{inputControl(context)}
</FormEntry>);
return inputControl(context);
}
supportsInput(context: NodeInputContext): boolean {

View file

@ -121,3 +121,8 @@ export interface VersionOptions {
allVersions?: boolean;
version?: number;
}
export interface IntellisenseContext {
activityTypeName: string;
propertyName: string;
}

View file

@ -8,7 +8,7 @@ export interface LiteralExpression {
export interface JavaScriptExpression {
type: ExpressionType;
scriptExpression: string;
value: string;
}
export interface JsonExpression {

View file

@ -15,6 +15,7 @@ using Elsa.Pipelines.WorkflowExecution.Components;
using Elsa.Runtime.ProtoActor.Extensions;
using Elsa.Samples.Web1.Activities;
using Elsa.Samples.Web1.Workflows;
using Elsa.Scripting.JavaScript;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@ -27,7 +28,6 @@ var services = builder.Services;
services
.AddElsa()
.AddMediator()
//.AddInMemoryPersistence()
.AddEntityFrameworkCorePersistence((_, ef) => ef.UseSqlite())
.IndexWorkflowTriggers()
.AddElsaManagement()
@ -57,6 +57,9 @@ services
services
.AddTrigger<HttpTrigger>();
// Register scripting languages.
services.AddJavaScriptExpressions();
// Configure middleware pipeline.
var app = builder.Build();
var serviceProvider = app.Services;

View file

@ -11,6 +11,7 @@
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
<ProjectReference Include="..\..\core\Elsa.Management\Elsa.Management.csproj" />
</ItemGroup>
</Project>

View file

@ -7,12 +7,12 @@ namespace Elsa.Scripting.JavaScript;
public class JavaScriptExpression : IExpression
{
public JavaScriptExpression(string scriptExpression)
public JavaScriptExpression(string value)
{
ScriptExpression = scriptExpression;
Value = value;
}
public string ScriptExpression { get; }
public string Value { get; }
}
public class JavaScriptExpressionHandler : IExpressionHandler
@ -22,6 +22,6 @@ public class JavaScriptExpressionHandler : IExpressionHandler
var javaScriptExpression = (JavaScriptExpression)expression;
// TODO: evaluate expression.
return new ValueTask<object?>(javaScriptExpression.ScriptExpression);
return new ValueTask<object?>(javaScriptExpression.Value);
}
}

View file

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Contracts;
using Elsa.Expressions;
using Elsa.Management.Contracts;
using Elsa.Management.Models;
using Elsa.Models;
namespace Elsa.Scripting.JavaScript;
public class JavaScriptExpressionSyntaxProvider : IExpressionSyntaxProvider
{
private readonly IIdentityGenerator _identityGenerator;
public JavaScriptExpressionSyntaxProvider(IIdentityGenerator identityGenerator)
{
_identityGenerator = identityGenerator;
}
public ValueTask<IEnumerable<ExpressionSyntaxDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
{
var javaScript = CreateJavaScriptDescriptor();
return ValueTask.FromResult<IEnumerable<ExpressionSyntaxDescriptor>>(new[] { javaScript });
}
private ExpressionSyntaxDescriptor CreateJavaScriptDescriptor() => CreateDescriptor<JavaScriptExpression>(
"JavaScript",
CreateJavaScriptExpression,
context => new JavaScriptExpressionReference(context.GetExpression<JavaScriptExpression>()),
expression => expression.Value);
private ExpressionSyntaxDescriptor CreateDescriptor<TExpression>(
string syntax,
Func<ExpressionConstructorContext, IExpression> constructor,
Func<LocationReferenceConstructorContext, RegisterLocationReference> createLocationReference,
Func<TExpression, object?> expressionValue) =>
new()
{
Syntax = syntax,
Type = typeof(TExpression),
CreateExpression = constructor,
CreateLocationReference = context =>
{
var reference = createLocationReference(context);
if (string.IsNullOrWhiteSpace(reference.Id))
reference.Id = GenerateId();
return reference;
},
CreateSerializableObject = context => new
{
Type = syntax,
Value = expressionValue(context.GetExpression<TExpression>())
}
};
private IExpression CreateJavaScriptExpression(ExpressionConstructorContext context)
{
var script = context.Element.GetProperty("value").ToString();
return new JavaScriptExpression(script);
}
private string GenerateId() => _identityGenerator.GenerateId();
}

View file

@ -1,4 +1,5 @@
using Elsa.Extensions;
using Elsa.Management.Contracts;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Scripting.JavaScript;
@ -7,6 +8,8 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddJavaScriptExpressions(this IServiceCollection services)
{
return services.AddExpressionHandler<JavaScriptExpressionHandler, JavaScriptExpression>();
return services
.AddSingleton<IExpressionSyntaxProvider, JavaScriptExpressionSyntaxProvider>()
.AddExpressionHandler<JavaScriptExpressionHandler, JavaScriptExpression>();
}
}