JavaScript intellisense (#3639)

* Incremental work

* Incremental work

* Incremental work

* Initial support for intellisense

* Implement variable type definition provider
This commit is contained in:
Sipke Schoorstra 2023-01-19 14:09:58 +01:00 committed by GitHub
parent 6759a0d022
commit 28e9922060
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 1254 additions and 214 deletions

View file

@ -1,12 +1,12 @@
{
"name": "@elsa-workflows/elsa-workflows-designer",
"version": "3.0.2-preview.36",
"version": "3.0.2-preview.40",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@elsa-workflows/elsa-workflows-designer",
"version": "3.0.2-preview.2",
"version": "3.0.2-preview.40",
"license": "MIT",
"dependencies": {
"@antv/layout": "^0.3.11",
@ -15,6 +15,7 @@
"@stencil/core": "^2.20.0",
"@stencil/state-tunnel": "^1.0.1",
"@stencil/store": "^2.0.1",
"async-mutex": "^0.4.0",
"axios": "^0.27.2",
"axios-middleware": "^0.3.1",
"el-transition": "^0.0.7",
@ -392,6 +393,14 @@
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true
},
"node_modules/async-mutex": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz",
"integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -1614,6 +1623,11 @@
"node": ">=8.0"
}
},
"node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/typedi": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/typedi/-/typedi-0.10.0.tgz",
@ -1980,6 +1994,14 @@
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true
},
"async-mutex": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz",
"integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==",
"requires": {
"tslib": "^2.4.0"
}
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -2850,6 +2872,11 @@
"is-number": "^7.0.0"
}
},
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"typedi": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/typedi/-/typedi-0.10.0.tgz",

View file

@ -32,6 +32,7 @@
"@stencil/core": "^2.20.0",
"@stencil/state-tunnel": "^1.0.1",
"@stencil/store": "^2.0.1",
"async-mutex": "^0.4.0",
"axios": "^0.27.2",
"axios-middleware": "^0.3.1",
"el-transition": "^0.0.7",

View file

@ -8,14 +8,14 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { ActivityDefinition, ActivityDefinitionPropsUpdatedArgs, ActivityDefinitionSummary, ActivityDefinitionUpdatedArgs } from "./modules/activity-definitions/models";
import { ActivityDriverRegistry } from "./services";
import { Activity, ActivityDeletedArgs, ActivitySelectedArgs, ChildActivitySelectedArgs, ContainerSelectedArgs, EditChildActivityArgs, GraphUpdatedArgs, IntellisenseContext, SelectListItem, TabChangedArgs, TabDefinition, Variable, WorkflowInstance, WorkflowInstanceSummary, WorkflowUpdatedArgs } from "./models";
import { WorkflowDefinition, WorkflowDefinitionSummary } from "./modules/workflow-definitions/models/entities";
import { ActivityUpdatedArgs, DeleteActivityRequestedArgs, Widget, WorkflowDefinitionPropsUpdatedArgs, WorkflowDefinitionUpdatedArgs } from "./modules/workflow-definitions/models/ui";
import { PublishClickedArgs } from "./modules/activity-definitions/components/publish-button";
import { NotificationType } from "./modules/notifications/models";
import { Button } from "./components/shared/button-group/models";
import { ActivityInputContext } from "./services/node-input-driver";
import { ActivityInputContext } from "./services/activity-input-driver";
import { ContextMenuAnchorPoint, ContextMenuItem } from "./components/shared/context-menu/models";
import { DropdownButtonItem, DropdownButtonOrigin } from "./components/shared/dropdown-button/models";
import { WorkflowDefinition, WorkflowDefinitionSummary } from "./modules/workflow-definitions/models/entities";
import { Graph } from "@antv/x6";
import { AddActivityArgs, FlowchartPathItem, LayoutDirection, RenameActivityArgs, UpdateActivityArgs } from "./modules/flowchart/models";
import { OutNode } from "@antv/layout";
@ -25,7 +25,7 @@ import { ExpressionChangedArs } from "./components/designer/input-control-switch
import { SignedInArgs } from "./modules/login/models";
import { ModalActionClickArgs, ModalActionDefinition, ModalDialogInstance } from "./components/shared/modal-dialog/models";
import { ModalType } from "./components/shared/modal-dialog/modal-type";
import { MonacoLib, MonacoValueChangedArgs } from "./components/shared/monaco-editor/monaco-editor";
import { MonacoValueChangedArgs } from "./components/shared/monaco-editor/monaco-editor";
import { PagerData } from "./components/shared/pager/pager";
import { PanelPosition, PanelStateChangedArgs } from "./components/panel/models";
import { PublishClickedArgs as PublishClickedArgs1 } from "./modules/workflow-definitions/components/publish-button";
@ -54,10 +54,11 @@ export namespace Components {
}
interface ElsaActivityPropertiesEditor {
"activity"?: Activity;
"containerId": string;
"containerType": string;
"hide": () => Promise<void>;
"show": () => Promise<void>;
"variables": Array<Variable>;
"workflowDefinition": WorkflowDefinition;
}
interface ElsaActivityPublishButton {
"publishing": boolean;
@ -135,8 +136,11 @@ export namespace Components {
interface ElsaHomePage {
}
interface ElsaInputControlSwitch {
"activityType": string;
"codeEditorHeight": string;
"codeEditorSingleLineMode": boolean;
"containerId": string;
"containerType": string;
"context"?: IntellisenseContext;
"defaultSyntax": string;
"expression"?: string;
@ -145,6 +149,7 @@ export namespace Components {
"hint": string;
"isReadOnly"?: boolean;
"label": string;
"propertyName": string;
"supportedSyntaxes": Array<string>;
"syntax"?: string;
}
@ -180,11 +185,11 @@ export namespace Components {
interface ElsaModalDialogContainer {
}
interface ElsaMonacoEditor {
"addJavaScriptLib": (libSource: string, libUri: string) => Promise<void>;
"editorHeight": string;
"language": string;
"monacoLibPath"?: string;
"padding": string;
"setJavaScriptLibs": (libs: Array<MonacoLib>) => Promise<void>;
"setValue": (value: string) => Promise<void>;
"singleLineMode": boolean;
"value": string;
}
@ -879,10 +884,11 @@ declare namespace LocalJSX {
}
interface ElsaActivityPropertiesEditor {
"activity"?: Activity;
"containerId"?: string;
"containerType"?: string;
"onActivityUpdated"?: (event: ElsaActivityPropertiesEditorCustomEvent<ActivityUpdatedArgs>) => void;
"onDeleteActivityRequested"?: (event: ElsaActivityPropertiesEditorCustomEvent<DeleteActivityRequestedArgs>) => void;
"variables"?: Array<Variable>;
"workflowDefinition"?: WorkflowDefinition;
}
interface ElsaActivityPublishButton {
"onExportClicked"?: (event: ElsaActivityPublishButtonCustomEvent<any>) => void;
@ -962,8 +968,11 @@ declare namespace LocalJSX {
interface ElsaHomePage {
}
interface ElsaInputControlSwitch {
"activityType"?: string;
"codeEditorHeight"?: string;
"codeEditorSingleLineMode"?: boolean;
"containerId"?: string;
"containerType"?: string;
"context"?: IntellisenseContext;
"defaultSyntax"?: string;
"expression"?: string;
@ -974,6 +983,7 @@ declare namespace LocalJSX {
"label"?: string;
"onExpressionChanged"?: (event: ElsaInputControlSwitchCustomEvent<ExpressionChangedArs>) => void;
"onSyntaxChanged"?: (event: ElsaInputControlSwitchCustomEvent<string>) => void;
"propertyName"?: string;
"supportedSyntaxes"?: Array<string>;
"syntax"?: string;
}
@ -1016,7 +1026,6 @@ declare namespace LocalJSX {
interface ElsaMonacoEditor {
"editorHeight"?: string;
"language"?: string;
"monacoLibPath"?: string;
"onValueChanged"?: (event: ElsaMonacoEditorCustomEvent<MonacoValueChangedArgs>) => void;
"padding"?: string;
"singleLineMode"?: boolean;

View file

@ -7,6 +7,9 @@ import {MonacoValueChangedArgs} from "../../shared/monaco-editor/monaco-editor";
import {Hint} from "../../shared/forms/hint";
import {mapSyntaxToLanguage} from "../../../utils";
import descriptorsStore from "../../../data/descriptors-store";
import {Container} from "typedi";
import {ElsaClient, ElsaClientProvider} from "../../../services/api-client/elsa-client";
import InputControlSwitchContextState from "./state";
export interface ExpressionChangedArs {
expression: string;
@ -27,6 +30,12 @@ export class InputControlSwitch {
this.onExpressionChangedDebounced = debounce(this.onExpressionChanged, 10);
}
// Tunneled props.
@Prop() containerType: string;
@Prop() containerId: string;
@Prop() activityType: string;
@Prop() propertyName: string;
@Prop() label: string;
@Prop() hideLabel: boolean;
@Prop() hint: string;
@ -53,10 +62,21 @@ export class InputControlSwitch {
this.closeContextMenu();
}
componentWillLoad() {
async componentWillLoad() {
this.currentExpression = this.expression;
}
async componentDidLoad() {
const elsaClient = await Container.get(ElsaClientProvider).getElsaClient();
const containerType = this.containerType;
const containerId = this.containerId;
const activityTypeName = this.activityType;
const propertyName = this.propertyName;
const typeDefinitions = await elsaClient.scripting.javaScriptApi.getTypeDefinitions({containerType, containerId, activityTypeName, propertyName});
const libUri = 'defaultLib:lib.es6.d.ts';
await this.monacoEditor.addJavaScriptLib(typeDefinitions, libUri);
}
render() {
if (this.hideLabel && !this.shouldRenderMonaco()) {
@ -200,3 +220,5 @@ export class InputControlSwitch {
this.expressionChanged.emit({expression, syntax: this.syntax});
}
}
InputControlSwitchContextState.injectProps(InputControlSwitch, ['containerType', 'containerId', 'activityType', 'propertyName'])

View file

@ -0,0 +1,19 @@
import {h} from "@stencil/core";
import {createProviderConsumer} from "@stencil/state-tunnel";
export interface InputControlSwitchContextState {
containerType: string;
containerId: string;
activityType: string;
propertyName: string;
}
export default createProviderConsumer<InputControlSwitchContextState>(
{
containerType: null,
containerId: null,
activityType: null,
propertyName: null
},
(subscribe, child) => (<context-consumer subscribe={subscribe} renderer={child}/>)
);

View file

@ -1,7 +1,7 @@
import {Component, h, Prop, State} from '@stencil/core';
import {uniq} from 'lodash'
import {JsonExpression, LiteralExpression, SelectList, SyntaxNames} from "../../models";
import {ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputContext} from "../../services/activity-input-driver";
import {getSelectListItems, getInputPropertyValue, parseJson} from "../../utils";
import {ExpressionChangedArs} from "../designer/input-control-switch/input-control-switch";

View file

@ -1,6 +1,6 @@
import {Component, Prop, h, State} from '@stencil/core';
import {LiteralExpression, SyntaxNames} from "../../models";
import {ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputContext} from "../../services/activity-input-driver";
import {getInputPropertyValue } from "../../utils";
import {ExpressionChangedArs} from "../designer/input-control-switch/input-control-switch";

View file

@ -1,6 +1,6 @@
import {Component, h, Prop} from '@stencil/core';
import {EditorHeight, LiteralExpression, SyntaxNames} from "../../models";
import {ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputContext} from "../../services/activity-input-driver";
import {getInputPropertyValue} from "../../utils";
import {MonacoValueChangedArgs} from "../shared/monaco-editor/monaco-editor";
import {ExpressionChangedArs} from "../designer/input-control-switch/input-control-switch";

View file

@ -1,6 +1,6 @@
import {Component, Prop, h} from '@stencil/core';
import {LiteralExpression, SelectList, SyntaxNames} from "../../models";
import {ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputContext} from "../../services/activity-input-driver";
import {getInputPropertyValue, getSelectListItems} from "../../utils";
import {ExpressionChangedArs} from "../designer/input-control-switch/input-control-switch";

View file

@ -1,6 +1,6 @@
import {Component, Prop, h} from '@stencil/core';
import {LiteralExpression, SyntaxNames} from "../../models";
import {ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputContext} from "../../services/activity-input-driver";
import {getInputPropertyValue} from "../../utils";
import {ExpressionChangedArs} from "../designer/input-control-switch/input-control-switch";

View file

@ -1,6 +1,6 @@
import {Component, Prop, h} from '@stencil/core';
import {JsonExpression, LiteralExpression, SyntaxNames} from "../../models";
import {ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputContext} from "../../services/activity-input-driver";
import {getInputPropertyValue, parseJson} from "../../utils";
import {ExpressionChangedArs} from "../designer/input-control-switch/input-control-switch";

View file

@ -1,6 +1,6 @@
import {Component, h, Prop, State} from '@stencil/core';
import {LiteralExpression, SelectList, SyntaxNames} from "../../models";
import {ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputContext} from "../../services/activity-input-driver";
import {getSelectListItems, getInputPropertyValue} from "../../utils";
import {ExpressionChangedArs} from "../designer/input-control-switch/input-control-switch";

View file

@ -1,6 +1,6 @@
import {Component, h, Prop} from '@stencil/core';
import {LiteralExpression, SyntaxNames} from "../../models";
import {ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputContext} from "../../services/activity-input-driver";
import {getInputPropertyValue} from "../../utils";
import {ExpressionChangedArs} from "../designer/input-control-switch/input-control-switch";
import descriptorsStore from "../../data/descriptors-store";

View file

@ -1,7 +1,7 @@
import {Component, Prop, h} from '@stencil/core';
import {groupBy} from 'lodash';
import {LiteralExpression, SyntaxNames} from "../../models";
import {ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputContext} from "../../services/activity-input-driver";
import {getInputPropertyValue} from "../../utils";
import descriptorsStore from '../../data/descriptors-store';
import {VariableDescriptor} from "../../services/api-client/variable-descriptors-api";

View file

@ -1,9 +1,9 @@
import {Component, Prop, h} from '@stencil/core';
import {SyntaxNames, Variable} from "../../models";
import {ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputContext} from "../../services/activity-input-driver";
import {getPropertyValue} from "../../utils";
import {FormEntry} from "../shared/forms/form-entry";
import WorkflowDefinitionTunnel from "../../state/workflow-definition-state";
import WorkflowDefinitionTunnel from "../../modules/workflow-definitions/state";
@Component({
tag: 'elsa-variable-picker-input',

View file

@ -1,5 +1,5 @@
import {Component, Event, EventEmitter, h, Host, Method, Prop, Watch} from '@stencil/core';
import {initializeMonacoWorker, Monaco} from "./utils";
import { Component, Event, EventEmitter, h, Host, Method, Prop, Watch } from '@stencil/core';
import { initializeMonacoWorker, Monaco, EditorVariables } from "./utils";
import monacoStore from '../../../data/monaco-store';
export interface MonacoValueChangedArgs {
@ -7,28 +7,23 @@ export interface MonacoValueChangedArgs {
markers: Array<any>;
}
export interface MonacoLib {
content: string;
filePath?: string;
}
@Component({
tag: 'elsa-monaco-editor',
styleUrl: 'monaco-editor.scss',
shadow: false
})
export class MonacoEditor {
export class ElsaMonaco {
private monaco: Monaco;
private container: HTMLElement;
private editor: any;
@Prop() public monacoLibPath?: string;
@Prop() public editorHeight: string = '10em';
@Prop() public value: string;
@Prop() public language: string;
@Prop() public singleLineMode: boolean = false;
@Prop() public padding: string;
@Event() public valueChanged: EventEmitter<MonacoValueChangedArgs>;
@Prop({ attribute: 'editor-height', reflect: true }) editorHeight: string = '5em';
@Prop() value: string;
@Prop() language: string;
@Prop({ attribute: 'single-line', reflect: true }) singleLineMode: boolean = false;
@Prop() padding: string;
@Event({ eventName: 'valueChanged' }) valueChanged: EventEmitter<MonacoValueChangedArgs>;
container: HTMLElement;
editor: any;
@Watch('language')
languageChangeHandler(newValue: string) {
@ -40,35 +35,46 @@ export class MonacoEditor {
}
@Method()
async setJavaScriptLibs(libs: Array<MonacoLib>) {
async setValue(value: string) {
if (!this.editor)
return;
const model = this.editor.getModel();
model.setValue(value || '');
}
@Method()
async addJavaScriptLib(libSource: string, libUri: string) {
const monaco = this.monaco;
const editor = this.editor;
const defaultLib: MonacoLib = {
content: "<reference lib=\"es5\" />",
filePath: "lib.d.ts"
};
monaco.languages.typescript.javascriptDefaults.setExtraLibs([{
filePath: "lib.es5.d.ts"
}, {
content: libSource,
filePath: libUri
}]);
libs = [...libs];
monaco.languages.typescript.javascriptDefaults.setExtraLibs(libs);
const oldModel = monaco.editor.getModel(libUri);
for (const lib of libs) {
if (oldModel)
oldModel.dispose();
const oldModel = editor.getModel(lib.filePath);
const newModel = monaco.editor.createModel(libSource, 'typescript', monaco.Uri.parse(libUri));
const matches = libSource.matchAll(/declare const (\w+): (string|number)/g);
if (oldModel)
oldModel.dispose();
EditorVariables.splice(0, EditorVariables.length);
const newModel = monaco.editor.createModel(lib.content, 'typescript', monaco.Uri.parse(lib.filePath));
for(const match of matches) {
EditorVariables.push({variableName: match[1], type: match[2] });
}
}
public async componentWillLoad() {
const monacoLibPath = this.monacoLibPath || monacoStore.monacoLibPath;
async componentWillLoad() {
const monacoLibPath = monacoStore.monacoLibPath;
this.monaco = await initializeMonacoWorker(monacoLibPath);
}
public async componentDidLoad() {
componentDidLoad() {
const monaco = this.monaco;
const language = this.language;
@ -118,21 +124,18 @@ export class MonacoEditor {
lineNumbersMinChars: 0,
folding: false,
scrollBeyondLastColumn: 0,
scrollbar: {horizontal: 'hidden', vertical: 'hidden'},
find: {addExtraSpaceOnTop: false, autoFindInSelection: 'never', seedSearchStringFromSelection: false},
scrollbar: { horizontal: 'hidden', vertical: 'hidden' },
find: { addExtraSpaceOnTop: false, autoFindInSelection: 'never', seedSearchStringFromSelection: false },
}
}
}
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});
this.valueChanged.emit({value: value, markers: markers});
const markers = monaco.editor.getModelMarkers({ owner: language });
this.valueChanged.emit({ value: value, markers: markers });
});
if (this.singleLineMode) {
@ -166,47 +169,13 @@ export class MonacoEditor {
editor.dispose();
}
registerLiquid() {
const monaco = this.monaco;
monaco.languages.register({id: 'liquid'});
monaco.languages.registerCompletionItemProvider('liquid', {
provideCompletionItems: () => {
const autocompleteProviderItems = [];
const keywords = ['assign', 'capture', 'endcapture', 'increment', 'decrement',
'if', 'else', 'elsif', 'endif', 'for', 'endfor', 'break',
'continue', 'limit', 'offset', 'range', 'reversed', 'cols',
'case', 'endcase', 'when', 'block', 'endblock', 'true', 'false',
'in', 'unless', 'endunless', 'cycle', 'tablerow', 'endtablerow',
'contains', 'startswith', 'endswith', 'comment', 'endcomment',
'raw', 'endraw', 'editable', 'endentitylist', 'endentityview', 'endinclude',
'endmarker', 'entitylist', 'entityview', 'forloop', 'image', 'include',
'marker', 'outputcache', 'plugin', 'style', 'text', 'widget',
'abs', 'append', 'at_least', 'at_most', 'capitalize', 'ceil', 'compact',
'concat', 'date', 'default', 'divided_by', 'downcase', 'escape',
'escape_once', 'first', 'floor', 'join', 'last', 'lstrip', 'map',
'minus', 'modulo', 'newline_to_br', 'plus', 'prepend', 'remove',
'remove_first', 'replace', 'replace_first', 'reverse', 'round',
'rstrip', 'size', 'slice', 'sort', 'sort_natural', 'split', 'strip',
'strip_html', 'strip_newlines', 'times', 'truncate', 'truncatewords',
'uniq', 'upcase', 'url_decode', 'url_encode'];
for (let i = 0; i < keywords.length; i++) {
autocompleteProviderItems.push({'label': keywords[i], kind: monaco.languages.CompletionItemKind.Keyword});
}
return {suggestions: autocompleteProviderItems};
}
});
}
render() {
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"
style={{'min-height': this.editorHeight}}>
<div ref={el => this.container = el} class={`monaco-editor-container ${padding}`}/>
style={{ 'min-height': this.editorHeight }}>
<div ref={el => this.container = el} class={`monaco-editor-container ${padding}`} />
</Host>
)
}

View file

@ -1,4 +1,5 @@
const win = window as any;
import { Mutex } from "async-mutex";
const win = window as any;
const require = win.require;
export interface Monaco {
@ -8,32 +9,102 @@ export interface Monaco {
Uri: any;
}
export function initializeMonacoWorker(libPath?: string): Promise<Monaco> {
export interface EditorVariable {
variableName: string;
type: string;
}
if (win.monaco)
return win.monaco;
export var EditorVariables: Array<EditorVariable> = [];
if (!libPath)
return win.monaco;
let isInitialized: boolean;
const mutex = new Mutex();
const origin = document.location.origin;
const baseUrl = libPath.startsWith('http') ? libPath : `${origin}/${libPath}`;
export async function initializeMonacoWorker(libPath?: string): Promise<Monaco> {
require.config({paths: {'vs': `${baseUrl}/vs`}});
win.MonacoEnvironment = {getWorkerUrl: () => proxy};
return await mutex.runExclusive(async () => {
let proxy = URL.createObjectURL(new Blob([`
if (isInitialized) {
return win.monaco;
}
const origin = document.location.origin;
const baseUrl = libPath.startsWith('http') ? libPath : `${origin}/${libPath}`;
require.config({ paths: { 'vs': `${baseUrl}/vs` } });
win.MonacoEnvironment = { getWorkerUrl: () => proxy };
let proxy = URL.createObjectURL(new Blob([`
self.MonacoEnvironment = {
baseUrl: '${baseUrl}'
};
importScripts('${baseUrl}/vs/base/worker/workerMain.js');
`], {type: 'text/javascript'}));
`], { type: 'text/javascript' }));
return new Promise(resolve => {
require(["vs/editor/editor.main"], () => {
resolve(win.monaco);
return new Promise(resolve => {
require(["vs/editor/editor.main"], () => {
isInitialized = true;
registerLiquid(win.monaco);
registerSql(win.monaco);
resolve(win.monaco);
});
});
});
}
export const DefaultLibUri: string = 'defaultLib:lib.es6.d.ts';
function registerLiquid(monaco: any) {
monaco.languages.register({ id: 'liquid' });
monaco.languages.registerCompletionItemProvider('liquid', {
provideCompletionItems: () => {
const autocompleteProviderItems = [];
const keywords = ['assign', 'capture', 'endcapture', 'increment', 'decrement',
'if', 'else', 'elsif', 'endif', 'for', 'endfor', 'break',
'continue', 'limit', 'offset', 'range', 'reversed', 'cols',
'case', 'endcase', 'when', 'block', 'endblock', 'true', 'false',
'in', 'unless', 'endunless', 'cycle', 'tablerow', 'endtablerow',
'contains', 'startswith', 'endswith', 'comment', 'endcomment',
'raw', 'endraw', 'editable', 'endentitylist', 'endentityview', 'endinclude',
'endmarker', 'entitylist', 'entityview', 'forloop', 'image', 'include',
'marker', 'outputcache', 'plugin', 'style', 'text', 'widget',
'abs', 'append', 'at_least', 'at_most', 'capitalize', 'ceil', 'compact',
'concat', 'date', 'default', 'divided_by', 'downcase', 'escape',
'escape_once', 'first', 'floor', 'join', 'last', 'lstrip', 'map',
'minus', 'modulo', 'newline_to_br', 'plus', 'prepend', 'remove',
'remove_first', 'replace', 'replace_first', 'reverse', 'round',
'rstrip', 'size', 'slice', 'sort', 'sort_natural', 'split', 'strip',
'strip_html', 'strip_newlines', 'times', 'truncate', 'truncatewords',
'uniq', 'upcase', 'url_decode', 'url_encode'];
for (let i = 0; i < keywords.length; i++) {
autocompleteProviderItems.push({ 'label': keywords[i], kind: monaco.languages.CompletionItemKind.Keyword });
}
return { suggestions: autocompleteProviderItems };
}
});
}
function registerSql(monaco: any) {
monaco.languages.registerCompletionItemProvider('sql', {
triggerCharacters: ["@"],
provideCompletionItems: (model, position) => {
const word = model.getWordUntilPosition(position)
const autocompleteProviderItems = [];
for (const variable of EditorVariables) {
autocompleteProviderItems.push({
label: `${variable.variableName}: ${variable.type}`,
kind: monaco.languages.CompletionItemKind.Variable,
insertText: variable.variableName
});
}
return { suggestions: autocompleteProviderItems };
}
});
}

View file

@ -1,11 +1,11 @@
import {NodeInputDriver, ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputDriver, ActivityInputContext} from "../../services/activity-input-driver";
import {Container} from "typedi";
import {InputControlRegistry} from "../../services/input-control-registry";
import {h} from "@stencil/core";
import {FormEntry} from "../../components/shared/forms/form-entry";
// A standard input driver that determines the UI to be displayed based on the UI hint of the activity input property.
export class DefaultInputDriver implements NodeInputDriver {
export class DefaultInputDriver implements ActivityInputDriver {
private inputControlRegistry: InputControlRegistry;
constructor() {

View file

@ -2,6 +2,6 @@ import 'reflect-metadata';
export {Components, JSX} from './components';
export {Container} from 'typedi';
export {ElsaApiClientProvider} from './services/api-client/api-client';
export {ElsaClientProvider} from './services/api-client/elsa-client';
export {stripActivityNameSpace} from './utils';
export {PluginRegistry, ActivityNameFormatter} from './services';

View file

@ -1,4 +1,4 @@
import {ActivityInputContext} from "../services/node-input-driver";
import {ActivityInputContext} from "../services/activity-input-driver";
import {Activity, ActivityDescriptor} from "./core";
export interface TabDefinition {

View file

@ -153,6 +153,8 @@ export class Editor {
private renderSelectedObject = () => {
if (!!this.selectedActivity)
return <elsa-activity-properties-editor
containerType="activity-definition"
containerId={this.activityDefinition.definitionId}
activity={this.selectedActivity}
variables={this.activityDefinitionState.variables}
onActivityUpdated={e => this.onActivityUpdated(e)}/>;

View file

@ -1,5 +1,4 @@
import {Service} from "typedi";
import {ElsaApiClientProvider} from "../../../services";
import {
ActivityDefinition,
ActivityDefinitionSummary,
@ -12,12 +11,13 @@ import {
} from "../models";
import {getVersionOptionsString, serializeQueryString} from "../../../utils";
import {PagedList} from "../../../models";
import {ElsaClientProvider} from "../../../services";
@Service()
export class ActivityDefinitionsApi {
private provider: ElsaApiClientProvider;
private provider: ElsaClientProvider;
constructor(provider: ElsaApiClientProvider) {
constructor(provider: ElsaClientProvider) {
this.provider = provider;
}

View file

@ -1,6 +1,6 @@
import {Component, h, Prop, State, Watch} from "@stencil/core";
import {camelCase} from 'lodash';
import {ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputContext} from "../../services/activity-input-driver";
import {mapSyntaxToLanguage} from "../../utils";
import {SyntaxNames} from "../../models";
import {MonacoValueChangedArgs} from "../../components/shared/monaco-editor/monaco-editor";
@ -28,7 +28,7 @@ export class FlowSwitchEditor {
private updateCases(){
const inputContext = this.inputContext;
const activity = this.inputContext.node;
const activity = this.inputContext.activity;
const inputDescriptor = inputContext.inputDescriptor;
const propertyName = inputDescriptor.name;
const camelCasePropertyName = camelCase(propertyName);
@ -67,7 +67,7 @@ export class FlowSwitchEditor {
private updateActivity = () => {
const inputContext = this.inputContext;
const activity = this.inputContext.node;
const activity = this.inputContext.activity;
const inputDescriptor = inputContext.inputDescriptor;
const propertyName = inputDescriptor.name;
const camelCasePropertyName = camelCase(propertyName);

View file

@ -1,13 +1,13 @@
import {List} from "../../models";
import {Label} from "./models";
import {ElsaApiClientProvider} from "../../services";
import {ElsaClientProvider} from "../../services";
import {Service} from "typedi";
@Service()
export class LabelsApi {
private provider: ElsaApiClientProvider;
private provider: ElsaClientProvider;
constructor(provider: ElsaApiClientProvider) {
constructor(provider: ElsaClientProvider) {
this.provider = provider;
}
@ -38,9 +38,9 @@ export class LabelsApi {
@Service()
export class WorkflowDefinitionLabelsApi {
private provider: ElsaApiClientProvider;
private provider: ElsaClientProvider;
constructor(provider: ElsaApiClientProvider) {
constructor(provider: ElsaClientProvider) {
this.provider = provider;
}

View file

@ -3,7 +3,7 @@ import {h} from "@stencil/core";
import {Service as MiddlewareService} from 'axios-middleware';
import {EventTypes, Plugin} from "../../models";
import {Container, Service} from "typedi";
import {StudioService, AuthContext, EventBus, ElsaClient, ElsaApiClientProvider} from "../../services";
import {StudioService, AuthContext, EventBus, ElsaClient, ElsaClientProvider} from "../../services";
import descriptorsStore from '../../data/descriptors-store';
import {SignedInArgs} from "./models";
import {AxiosInstance} from "axios";
@ -32,7 +32,7 @@ export class LoginPlugin implements Plugin {
}
private loadDescriptors = async (): Promise<void> => {
const elsaClientProvider = Container.get(ElsaApiClientProvider);
const elsaClientProvider = Container.get(ElsaClientProvider);
this.elsaClient = await elsaClientProvider.getElsaClient();
const activityDescriptors = await this.elsaClient.descriptors.activities.list();

View file

@ -1,6 +1,6 @@
import 'reflect-metadata';
import {Container, Service} from "typedi";
import {ElsaApiClientProvider, EventBus, ServerSettings} from "../../services";
import {ElsaClientProvider, EventBus, ServerSettings} from "../../services";
import {LoginResponse} from "./models";
import axios, {AxiosError, AxiosRequestConfig} from "axios";
import {EventTypes} from "../../models";
@ -8,7 +8,7 @@ import {EventTypes} from "../../models";
@Service()
export class LoginApi {
constructor(private provider: ElsaApiClientProvider, private serverSettings: ServerSettings) {
constructor(private provider: ElsaClientProvider, private serverSettings: ServerSettings) {
this.provider = provider;
}

View file

@ -1,6 +1,6 @@
import {Component, h, Prop, State, Watch} from "@stencil/core";
import {camelCase} from 'lodash';
import {ActivityInputContext} from "../../services/node-input-driver";
import {ActivityInputContext} from "../../services/activity-input-driver";
import {mapSyntaxToLanguage} from "../../utils";
import {SyntaxNames} from "../../models";
import {SwitchCase} from "./models";
@ -29,7 +29,7 @@ export class SwitchEditor {
private updateCases(){
const inputContext = this.inputContext;
const activity = this.inputContext.node;
const activity = this.inputContext.activity;
const inputDescriptor = inputContext.inputDescriptor;
const propertyName = inputDescriptor.name;
const camelCasePropertyName = camelCase(propertyName);
@ -68,7 +68,7 @@ export class SwitchEditor {
private updateActivity = () => {
const inputContext = this.inputContext;
const activity = this.inputContext.node;
const activity = this.inputContext.activity;
const inputDescriptor = inputContext.inputDescriptor;
const propertyName = inputDescriptor.name;
const camelCasePropertyName = camelCase(propertyName);

View file

@ -4,12 +4,12 @@ import {v4 as uuid} from 'uuid';
import {Activity, ActivityDescriptor, ActivityInput, ActivityKind, ActivityOutput, Expression, InputDescriptor, OutputDescriptor, PropertyDescriptor, RenderActivityInputContext, RenderActivityPropsContext, TabChangedArgs, TabDefinition, Variable} from '../../../models';
import {InputDriverRegistry} from "../../../services";
import {Container} from "typedi";
import {ActivityInputContext} from "../../../services/node-input-driver";
import {ActivityInputContext} from "../../../services/activity-input-driver";
import {CheckboxFormEntry, FormEntry} from "../../../components/shared/forms/form-entry";
import {isNullOrWhitespace} from "../../../utils";
import descriptorsStore from "../../../data/descriptors-store";
import {ActivityUpdatedArgs, DeleteActivityRequestedArgs} from "../models/ui";
import {WorkflowDefinition} from "../models/entities";
import InputControlSwitchContextState from "../../../components/designer/input-control-switch/state";
@Component({
tag: 'elsa-activity-properties-editor',
@ -23,7 +23,8 @@ export class ActivityPropertiesEditor {
this.inputDriverRegistry = Container.get(InputDriverRegistry);
}
@Prop() workflowDefinition: WorkflowDefinition;
@Prop() containerType: string;
@Prop() containerId: string;
@Prop() activity?: Activity;
@Prop() variables: Array<Variable> = [];
@ -59,15 +60,23 @@ export class ActivityPropertiesEditor {
const renderInputPropertyContexts: Array<RenderActivityInputContext> = activityDescriptor.inputs.map(inputDescriptor => {
const renderInputContext: ActivityInputContext = {
node: activity,
nodeDescriptor: activityDescriptor,
activity: activity,
activityDescriptor: activityDescriptor,
inputDescriptor,
notifyInputChanged: () => onInputChanged(inputDescriptor),
inputChanged: (v, s) => this.onInputPropertyEditorChanged(inputDescriptor, v, s)
};
const driver = driverRegistry.get(renderInputContext);
const control = driver?.renderInput(renderInputContext);
const containerType = this.containerType;
const containerId = this.containerId;
const activityType = activityDescriptor.typeName;
const propertyName = inputDescriptor.name;
const control =
<InputControlSwitchContextState.Provider state={{containerType, containerId, activityType, propertyName}}>
{driver?.renderInput(renderInputContext)}
</InputControlSwitchContextState.Provider>;
return {
inputContext: renderInputContext,
@ -86,6 +95,7 @@ export class ActivityPropertiesEditor {
render() {
const {activity, activityDescriptor} = this.renderContext;
const isTask = activityDescriptor.kind == ActivityKind.Task;
const activityType = activityDescriptor.typeName;
const commonTab: TabDefinition = {
displayText: 'General',

View file

@ -14,7 +14,7 @@ import {MonacoEditorSettings} from "../../../services/monaco-editor-settings";
import {ActivityPropertyChangedEventArgs, WorkflowDefinitionPropsUpdatedArgs, WorkflowDefinitionUpdatedArgs, ActivityUpdatedArgs, WorkflowEditorEventTypes} from "../models/ui";
import {WorkflowDefinition} from "../models/entities";
import {WorkflowDefinitionsApi} from "../services/api"
import WorkflowDefinitionTunnel, {WorkflowDefinitionState} from "../../../state/workflow-definition-state";
import WorkflowDefinitionTunnel, {WorkflowDefinitionState} from "../state";
import {LayoutDirection} from "../../flowchart/models";
import { cloneDeep } from '@antv/x6/lib/util/object/object';
import { removeGuidsFromPortNames } from '../../../utils/graph';
@ -36,15 +36,13 @@ export class WorkflowDefinitionEditor {
private readonly emitActivityChangedDebounced: (e: ActivityPropertyChangedEventArgs) => void;
private readonly saveChangesDebounced: () => void;
private readonly workflowDefinitionApi: WorkflowDefinitionsApi;
private currentFlowchartConnections;
constructor() {
this.eventBus = Container.get(EventBus);
this.pluginRegistry = Container.get(PluginRegistry);
this.activityNameFormatter = Container.get(ActivityNameFormatter);
this.portProviderRegistry = Container.get(PortProviderRegistry);
//this.emitActivityChangedDebounced = debounce(this.emitActivityChanged, 100);
this.emitActivityChangedDebounced = e => this.emitActivityChanged(e.activity, e.propertyName);
this.emitActivityChangedDebounced = debounce(this.emitActivityChanged, 10);
this.saveChangesDebounced = debounce(this.saveChanges, 1000);
this.workflowDefinitionApi = Container.get(WorkflowDefinitionsApi);
}
@ -174,9 +172,10 @@ export class WorkflowDefinitionEditor {
private renderSelectedObject = () => {
if (!!this.selectedActivity)
return <elsa-activity-properties-editor
workflowDefinition={this.workflowDefinitionState}
activity={this.selectedActivity}
variables={this.workflowDefinitionState.variables}
containerType="workflow-definition"
containerId={this.workflowDefinition.definitionId}
onActivityUpdated={e => this.onActivityUpdated(e)}/>;
}
@ -203,7 +202,7 @@ export class WorkflowDefinitionEditor {
}
};
// To prevent redundant post requests to server, save changes only if there is a difference
// To prevent redundant post requests to server, save changes only if there is a difference
// between existing workflow definition on server side and updated workflow definition on client side.
private hasWorkflowDefinitionAnyUpdatedData = async (updatedWorkflowDefinition : WorkflowDefinition): Promise<boolean> => {
const existingWorkflowDefinition = await this.workflowDefinitionApi.get({definitionId: updatedWorkflowDefinition.definitionId, versionOptions: {version: updatedWorkflowDefinition.version}});

View file

@ -2,16 +2,16 @@ import {getVersionOptionsString, serializeQueryString} from '../../../utils';
import {WorkflowDefinition, WorkflowDefinitionSummary, WorkflowOptions} from "../models/entities";
import {Activity, PagedList, Variable, VersionedEntity, VersionOptions} from "../../../models";
import {Service} from "typedi";
import {ElsaApiClientProvider} from "../../../services";
import {AxiosResponse} from "axios";
import { removeGuidsFromPortNames, addGuidsToPortNames } from '../../../utils/graph';
import { cloneDeep } from '@antv/x6/lib/util/object/object';
import {ElsaClientProvider} from "../../../services";
@Service()
export class WorkflowDefinitionsApi {
private provider: ElsaApiClientProvider;
private provider: ElsaClientProvider;
constructor(provider: ElsaApiClientProvider) {
constructor(provider: ElsaClientProvider) {
this.provider = provider;
}

View file

@ -1,6 +1,6 @@
import {h} from "@stencil/core";
import {createProviderConsumer} from "@stencil/state-tunnel";
import {WorkflowDefinition} from "../modules/workflow-definitions/models/entities";
import {WorkflowDefinition} from "./models/entities";
export interface WorkflowDefinitionState {
workflowDefinition: WorkflowDefinition;

View file

@ -1,14 +1,14 @@
import {OrderBy, OrderDirection, PagedList, VersionOptions, WorkflowExecutionLogRecord, WorkflowInstance, WorkflowInstanceSummary, WorkflowStatus, WorkflowSubStatus} from "../../../models";
import {getVersionOptionsString, serializeQueryString} from "../../../utils";
import {ElsaApiClientProvider} from "../../../services";
import {Service} from "typedi";
import {ElsaClientProvider} from "../../../services";
@Service()
export class WorkflowInstancesApi {
private provider: ElsaApiClientProvider;
private provider: ElsaClientProvider;
constructor(provider: ElsaApiClientProvider) {
constructor(provider: ElsaClientProvider) {
this.provider = provider;
}

View file

@ -1,14 +1,14 @@
import 'reflect-metadata';
import {Container, Service} from "typedi";
import {ElsaApiClientProvider} from "./api-client/api-client";
import descriptorsStore from "../data/descriptors-store";
import {ElsaClientProvider} from "./api-client/elsa-client";
@Service()
export class ActivityDescriptorManager {
private readonly elsaClientProvider: ElsaApiClientProvider;
private readonly elsaClientProvider: ElsaClientProvider;
constructor() {
this.elsaClientProvider = Container.get(ElsaApiClientProvider);
this.elsaClientProvider = Container.get(ElsaClientProvider);
}
async refresh(): Promise<void> {

View file

@ -1,14 +1,14 @@
import {Activity, ActivityDescriptor, InputDescriptor} from "../models";
export interface ActivityInputContext {
node: Activity;
nodeDescriptor: ActivityDescriptor;
activity: Activity;
activityDescriptor: ActivityDescriptor;
inputDescriptor: InputDescriptor;
notifyInputChanged: () => void;
inputChanged: (value: any, syntax: string) => void;
}
export interface NodeInputDriver {
export interface ActivityInputDriver {
supportsInput(context: ActivityInputContext): boolean;
get priority(): number;

View file

@ -1,11 +1,7 @@
import {RuntimeSelectListApi} from "./runtime-select-list-api";
import {AxiosInstance} from "axios";
export interface DesignerApi {
runtimeSelectListApi: RuntimeSelectListApi;
}
export class DesignerApiImpl implements DesignerApi {
export class DesignerApi {
runtimeSelectListApi: RuntimeSelectListApi;
private httpClient: AxiosInstance;

View file

@ -1,27 +1,28 @@
import axios, {AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse} from "axios";
import axios, {AxiosInstance, AxiosRequestConfig} from "axios";
import {Service as MiddlewareService} from 'axios-middleware';
import {Container, Service} from 'typedi';
import {EventBus} from '../event-bus';
import 'reflect-metadata';
import {ServerSettings} from '../server-settings';
import {DescriptorsApi} from "./descriptors-api";
import {DesignerApi, DesignerApiImpl} from "./designer-api";
import {DesignerApi} from "./designer-api";
import {EventTypes} from "../../models";
import {ScriptingApi} from "./scripting-api";
export class ElsaClient {
httpClient: AxiosInstance;
descriptors: DescriptorsApi;
designer: DesignerApi;
scripting: ScriptingApi;
constructor(httpClient: AxiosInstance) {
this.httpClient = httpClient;
this.descriptors = new DescriptorsApi(httpClient);
this.designer = new DesignerApiImpl(httpClient);
this.designer = new DesignerApi(httpClient);
this.scripting = new ScriptingApi(httpClient);
}
}
@Service()
export class ElsaApiClientProvider {
export class ElsaClientProvider {
private httpClient: AxiosInstance;
private elsaClient: ElsaClient;

View file

@ -0,0 +1,19 @@
import {AxiosInstance} from "axios";
export class JavaScriptApi {
constructor(private httpClient: AxiosInstance) {
}
async getTypeDefinitions(request: GetTypeDefinitionsRequest): Promise<string> {
const response = await this.httpClient.post(`scripting/javascript/type-definitions/${request.containerId}`, request);
return response.data;
}
}
export interface GetTypeDefinitionsRequest {
containerType: string;
containerId: string;
activityTypeName?: string;
propertyName?: string;
}

View file

@ -0,0 +1,11 @@
import {AxiosInstance} from "axios";
import {JavaScriptApi} from "./javascript-api";
export class ScriptingApi {
javaScriptApi: JavaScriptApi;
constructor(httpClient: AxiosInstance) {
this.javaScriptApi = new JavaScriptApi(httpClient);
}
}

View file

@ -4,7 +4,7 @@ export * from './activity-driver-registry';
export * from './activity-icon-registry';
export * from './auth';
export * from './custom-event-bus';
export * from './api-client/api-client';
export * from './api-client/elsa-client';
export * from './event-bus';
export * from './input-control-registry';
export * from './input-driver-registry';

View file

@ -1,18 +1,18 @@
import _ from 'lodash';
import {Service} from 'typedi';
import {NodeInputDriver, ActivityInputContext} from './node-input-driver';
import {ActivityInputDriver, ActivityInputContext} from './activity-input-driver';
import {DefaultInputDriver} from "../drivers/input/default-input-driver";
@Service()
export class InputDriverRegistry {
private drivers: Array<NodeInputDriver> = [new DefaultInputDriver()];
private drivers: Array<ActivityInputDriver> = [new DefaultInputDriver()];
public add(driver: NodeInputDriver) {
const drivers: Array<NodeInputDriver> = [...this.drivers, driver]
public add(driver: ActivityInputDriver) {
const drivers: Array<ActivityInputDriver> = [...this.drivers, driver]
this.drivers = _.orderBy(drivers, x => x.priority, 'desc');
}
public get(context: ActivityInputContext): NodeInputDriver {
public get(context: ActivityInputContext): ActivityInputDriver {
return this.drivers.find(x => x.supportsInput(context));
}
}

View file

@ -3,11 +3,11 @@ import {
RuntimeSelectListProviderSettings,
SelectList,
} from "../models";
import {ElsaApiClientProvider} from "../services";
import {Container} from "typedi";
import {ElsaClientProvider} from "../services";
async function fetchRuntimeItems(options: RuntimeSelectListProviderSettings): Promise<SelectList> {
const elsaClientProvider = Container.get(ElsaApiClientProvider);
const elsaClientProvider = Container.get(ElsaClientProvider);
const elsaClient = await elsaClientProvider.getElsaClient();
return await elsaClient.designer.runtimeSelectListApi.get(options.runtimeSelectListProviderType, options.context || {});
}

View file

@ -1,7 +1,7 @@
import moment from 'moment';
import _, {camelCase} from 'lodash';
import {ActivityInput, VersionOptions} from '../models';
import {ActivityInputContext} from '../services/node-input-driver';
import {ActivityInputContext} from '../services/activity-input-driver';
export interface Hash<TValue> {
[key: string]: TValue;
@ -71,12 +71,12 @@ export const getInputPropertyName = (inputContext: ActivityInputContext) => {
export const getInputPropertyValue = (inputContext: ActivityInputContext): ActivityInput => {
const propName = getInputPropertyName(inputContext);
return inputContext.node[propName] as ActivityInput;
return inputContext.activity[propName] as ActivityInput;
};
export const getPropertyValue = (inputContext: ActivityInputContext): any => {
const propName = getInputPropertyName(inputContext);
return inputContext.node[propName] as any
return inputContext.activity[propName] as any
};
export const stripActivityNameSpace = (name: string): string => {

View file

@ -7,8 +7,7 @@
"emitDecoratorMetadata": true,
"lib": [
"dom",
"es2017",
"es2018.promise"
"ES2020"
],
"moduleResolution": "node",
"module": "esnext",

View file

@ -0,0 +1,43 @@
using Elsa.Expressions.Services;
using Elsa.Extensions;
namespace Elsa.Expressions.Extensions;
/// <summary>
/// Extends <see cref="IWellKnownTypeRegistry"/>.
/// </summary>
public static class WellKnowTypeRegistryExtensions
{
/// <summary>
/// Register type <see cref="T"/> with the specified alias.
/// </summary>
public static void RegisterType<T>(this IWellKnownTypeRegistry registry, string alias) => registry.RegisterType(typeof(T), alias);
/// <summary>
/// Attempt to return a type with the specified alias.
/// </summary>
public static bool TryGetTypeOrDefault(this IWellKnownTypeRegistry registry, string alias, out Type type)
{
if (registry.TryGetType(alias, out type))
return true;
var t = Type.GetType(alias);
if (t == null)
return false;
type = t;
return true;
}
/// <summary>
/// Returns the alias for the specified type. If no alias was found, the assembly qualified type name is returned instead.
/// </summary>
public static string GetAliasOrDefault(this IWellKnownTypeRegistry registry, Type type) =>
registry.TryGetAlias(type, out var alias) ? alias : type.GetSimpleAssemblyQualifiedName();
/// <summary>
/// Returns the type associated with the specified alias. If no type was found, the alias is interpreted as a type name/
/// </summary>
public static Type GetTypeOrDefault(this IWellKnownTypeRegistry registry, string alias) => registry.TryGetType(alias, out var type) ? type : Type.GetType(alias)!;
}

View file

@ -3,6 +3,7 @@ using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Expressions.Exceptions;
using Elsa.Expressions.Extensions;
using Elsa.Expressions.Models;
using Elsa.Expressions.Services;
using Elsa.Extensions;

View file

@ -1,3 +1,4 @@
using Elsa.Expressions.Extensions;
using Elsa.Expressions.Services;
namespace Elsa.Expressions.Implementations;
@ -41,4 +42,7 @@ public class WellKnownTypeRegistry : IWellKnownTypeRegistry
/// <inheritdoc />
public bool TryGetType(string alias, out Type type) => _aliasTypeDictionary.TryGetValue(alias, out type!);
/// <inheritdoc />
public IEnumerable<Type> ListTypes() => _typeAliasDictionary.Keys;
}

View file

@ -1,34 +1,27 @@
using Elsa.Extensions;
namespace Elsa.Expressions.Services;
/// <summary>
/// A central repository of well known types.
/// </summary>
public interface IWellKnownTypeRegistry
{
/// <summary>
/// Register a type with an alias.
/// </summary>
void RegisterType(Type type, string alias);
bool TryGetAlias(Type type, out string alias);
bool TryGetType(string alias, out Type type);
}
public static class WellKnowTypeRegistryExtensions
{
public static void RegisterType<T>(this IWellKnownTypeRegistry registry, string alias) => registry.RegisterType(typeof(T), alias);
public static bool TryGetTypeOrDefault(this IWellKnownTypeRegistry registry, string alias, out Type type)
{
if (registry.TryGetType(alias, out type))
return true;
var t = Type.GetType(alias);
if (t == null)
return false;
type = t;
return true;
}
public static string GetAliasOrDefault(this IWellKnownTypeRegistry registry, Type type) =>
registry.TryGetAlias(type, out var alias) ? alias : type.GetSimpleAssemblyQualifiedName();
public static Type GetTypeOrDefault(this IWellKnownTypeRegistry registry, string alias) => registry.TryGetType(alias, out var type) ? type : Type.GetType(alias)!;
/// <summary>
/// Attempts to get an alias for the specified type.
/// </summary>
bool TryGetAlias(Type type, out string alias);
/// <summary>
/// Attempts to get the type associated with the specified alias.
/// </summary>
bool TryGetType(string alias, out Type type);
/// <summary>
/// Returns all registered types.
/// </summary>
IEnumerable<Type> ListTypes();
}

View file

@ -0,0 +1,40 @@
using Elsa.JavaScript.Builders;
using Elsa.JavaScript.Models;
using Elsa.JavaScript.Services;
namespace Elsa.JavaScript.Abstractions;
/// <summary>
/// A base class for function definition providers.
/// </summary>
public abstract class FunctionDefinitionProvider : IFunctionDefinitionProvider
{
/// <summary>
/// Returns a list of type definitions to the system.
/// </summary>
protected virtual ValueTask<IEnumerable<FunctionDefinition>> GetFunctionDefinitionsAsync(TypeDefinitionContext context)
{
var functions = GetFunctionDefinitions(context);
return new(functions);
}
/// <summary>
/// Returns a list of type definitions to the system.
/// </summary>
protected virtual IEnumerable<FunctionDefinition> GetFunctionDefinitions(TypeDefinitionContext context)
{
yield break;
}
async ValueTask<IEnumerable<FunctionDefinition>> IFunctionDefinitionProvider.GetFunctionDefinitionsAsync(TypeDefinitionContext context) => await GetFunctionDefinitionsAsync(context);
/// <summary>
/// Provides a fluid API to build a function definition.
/// </summary>
protected FunctionDefinition CreateFunctionDefinition(Action<FunctionDefinitionBuilder> setup)
{
var builder = new FunctionDefinitionBuilder();
setup(builder);
return builder.BuildFunctionDefinition();
}
}

View file

@ -0,0 +1,40 @@
using Elsa.JavaScript.Builders;
using Elsa.JavaScript.Models;
using Elsa.JavaScript.Services;
namespace Elsa.JavaScript.Abstractions;
/// <summary>
/// A base class for type definition providers.
/// </summary>
public abstract class TypeDefinitionProvider : ITypeDefinitionProvider
{
/// <summary>
/// Returns a list of type definitions to the system.
/// </summary>
protected virtual ValueTask<IEnumerable<TypeDefinition>> GetTypeDefinitionsAsync(TypeDefinitionContext context)
{
var functions = GetTypeDefinitions(context);
return new(functions);
}
/// <summary>
/// Returns a list of type definitions to the system.
/// </summary>
protected virtual IEnumerable<TypeDefinition> GetTypeDefinitions(TypeDefinitionContext context)
{
yield break;
}
async ValueTask<IEnumerable<TypeDefinition>> ITypeDefinitionProvider.GetTypeDefinitionsAsync(TypeDefinitionContext context) => await GetTypeDefinitionsAsync(context);
/// <summary>
/// Provides a fluid API to build a type definition.
/// </summary>
protected TypeDefinition CreateTypeDefinition(Action<TypeDefinitionBuilder> setup)
{
var builder = new TypeDefinitionBuilder();
setup(builder);
return builder.BuildTypeDefinition();
}
}

View file

@ -0,0 +1,43 @@
using Elsa.JavaScript.Models;
namespace Elsa.JavaScript.Builders;
/// <summary>
/// A builder API for building <see cref="FunctionDefinition"/>s.
/// </summary>
public class FunctionDefinitionBuilder
{
private readonly FunctionDefinition _functionDefinition = new();
/// <summary>
/// Set the name of the function.
/// </summary>
public FunctionDefinitionBuilder Name(string name)
{
_functionDefinition.Name = name;
return this;
}
/// <summary>
/// Set the return type of the function.
/// </summary>
public FunctionDefinitionBuilder ReturnType(string? type)
{
_functionDefinition.ReturnType = type;
return this;
}
/// <summary>
/// Add a parameter to the function.
/// </summary>
public FunctionDefinitionBuilder Parameter(string name, string? type, bool isOptional = false)
{
_functionDefinition.Parameters.Add(new (name, type, isOptional));
return this;
}
/// <summary>
/// Build a <see cref="FunctionDefinition"/> using the collected information.
/// </summary>
public FunctionDefinition BuildFunctionDefinition() => new(_functionDefinition.Name, _functionDefinition.ReturnType, _functionDefinition.Parameters);
}

View file

@ -0,0 +1,17 @@
using Elsa.JavaScript.Models;
namespace Elsa.JavaScript.Builders;
/// <summary>
/// A builder API for building <see cref="TypeDefinition"/>s.
/// </summary>
public class TypeDefinitionBuilder
{
private readonly TypeDefinition _typeDefinition = new();
/// <summary>
/// Build a <see cref="TypeDefinition"/> using the collected information.
/// </summary>
public TypeDefinition BuildTypeDefinition() => new();
}

View file

@ -4,7 +4,13 @@ using Jint.Runtime.Interop;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Extends <see cref="Engine"/>.
/// </summary>
public static class EngineExtensions
{
/// <summary>
/// Register type <see cref="T"/> with the engine.
/// </summary>
public static void RegisterType<T>(this Engine engine) => engine.SetValue(typeof(T).Name, TypeReference.CreateTypeReference(engine, typeof(T)));
}

View file

@ -0,0 +1,21 @@
using Elsa.Extensions;
using Elsa.JavaScript.Services;
namespace Elsa.JavaScript.Extensions;
/// <summary>
/// Extends <see cref="ITypeAliasRegistry"/>.
/// </summary>
public static class TypeAliasRegistryExtensions
{
/// <summary>
/// Register type <see cref="T"/> with the specified alias.
/// </summary>
public static void RegisterType<T>(this ITypeAliasRegistry registry, string alias) => registry.RegisterType(typeof(T), alias);
/// <summary>
/// Returns the alias for the specified type. If no alias was found, the assembly qualified type name is returned instead.
/// </summary>
public static string GetAliasOrDefault(this ITypeAliasRegistry registry, Type type) =>
registry.TryGetAlias(type, out var alias) ? alias : type.GetSimpleAssemblyQualifiedName();
}

View file

@ -34,9 +34,18 @@ public class JavaScriptFeature : FeatureBase
.AddSingleton<IExpressionSyntaxProvider, JavaScriptExpressionSyntaxProvider>()
.AddSingleton<IJavaScriptEvaluator, JintJavaScriptEvaluator>()
.AddSingleton<IActivityPropertyOptionsProvider, RunJavaScriptOptionsProvider>()
.AddSingleton<ITypeDefinitionService, TypeDefinitionService>()
.AddExpressionHandler<JavaScriptExpressionHandler, JavaScriptExpression>()
;
Services
.AddSingleton<ITypeDefinitionService, TypeDefinitionService>()
.AddSingleton<ITypeDescriber, TypeDescriber>()
.AddSingleton<ITypeDefinitionDocumentRenderer, TypeDefinitionDocumentRenderer>()
.AddSingleton<ITypeAliasRegistry, TypeAliasRegistry>()
.AddSingleton<IFunctionDefinitionProvider, CommonFunctionDefinitionProvider>()
.AddSingleton<ITypeDefinitionProvider, VariableTypeDefinitionProvider>();
Module.UseWorkflowManagement(management => management.AddActivitiesFrom<JavaScriptFeature>());
}
}

View file

@ -0,0 +1,39 @@
using Elsa.JavaScript.Extensions;
using Elsa.JavaScript.Services;
namespace Elsa.JavaScript.Implementations;
/// <inheritdoc />
public class TypeAliasRegistry : ITypeAliasRegistry
{
private readonly IDictionary<Type, string> _typeAliasDictionary = new Dictionary<Type, string>();
/// <summary>
/// Constructor.
/// </summary>
public TypeAliasRegistry()
{
this.RegisterType<object>("any");
this.RegisterType<string>("string");
this.RegisterType<bool>("boolean");
this.RegisterType<short>("number");
this.RegisterType<int>("number");
this.RegisterType<long>("number");
this.RegisterType<decimal>("Decimal");
this.RegisterType<float>("Single");
this.RegisterType<double>("Double");
this.RegisterType<DateTime>("Date");
this.RegisterType<DateTimeOffset>("Date");
this.RegisterType<DateOnly>("Date");
this.RegisterType<TimeOnly>("Date");
}
/// <inheritdoc />
public void RegisterType(Type type, string alias)
{
_typeAliasDictionary[type] = alias;
}
/// <inheritdoc />
public bool TryGetAlias(Type type, out string alias) => _typeAliasDictionary.TryGetValue(type, out alias!);
}

View file

@ -0,0 +1,45 @@
using System.Text;
using Elsa.JavaScript.Extensions;
using Elsa.JavaScript.Models;
using Elsa.JavaScript.Services;
namespace Elsa.JavaScript.Implementations;
/// <inheritdoc />
public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer
{
/// <inheritdoc />
public string Render(TypeDefinitionsDocument document)
{
var stringBuilder = new StringBuilder();
foreach (var functionDefinition in document.Functions)
Render(functionDefinition, stringBuilder);
foreach (var typeDefinition in document.Types)
Render(typeDefinition, stringBuilder);
return stringBuilder.ToString();
}
private void Render(FunctionDefinition functionDefinition, StringBuilder output)
{
string RenderParameter(ParameterDefinition parameter) => $"{parameter.Name}{(parameter.IsOptional ? "?" : "")}: {parameter.Type}";
string RenderParameters(IEnumerable<ParameterDefinition> parameters) => string.Join(", ", parameters.Select(RenderParameter));
var returnType = functionDefinition.ReturnType != null ? $": {functionDefinition.ReturnType}" : "";
output.AppendLine($"declare function {functionDefinition.Name}({RenderParameters(functionDefinition.Parameters)}){returnType}");
}
private void Render(TypeDefinition typeDefinition, StringBuilder output)
{
output.AppendLine($"declare {typeDefinition.DeclarationKeyword} {typeDefinition.Name} {{");
foreach (var property in typeDefinition.Properties)
Render(property, output);
output.AppendLine("}");
}
private void Render(PropertyDefinition property, StringBuilder output) => output.AppendLine($"{property.Name}{(property.IsOptional ? "?" : "")}: {property.Type}");
}

View file

@ -0,0 +1,62 @@
using Elsa.JavaScript.Models;
using Elsa.JavaScript.Services;
namespace Elsa.JavaScript.Implementations;
/// <inheritdoc />
public class TypeDefinitionService : ITypeDefinitionService
{
private readonly IEnumerable<ITypeDefinitionProvider> _typeDefinitionProviders;
private readonly IEnumerable<IFunctionDefinitionProvider> _functionDefinitionProviders;
private readonly ITypeDefinitionDocumentRenderer _typeDefinitionDocumentRenderer;
/// <summary>
/// Constructor.
/// </summary>
public TypeDefinitionService(
IEnumerable<ITypeDefinitionProvider> typeDefinitionProviders,
IEnumerable<IFunctionDefinitionProvider> functionDefinitionProviders,
ITypeDefinitionDocumentRenderer typeDefinitionDocumentRenderer)
{
_typeDefinitionProviders = typeDefinitionProviders;
_functionDefinitionProviders = functionDefinitionProviders;
_typeDefinitionDocumentRenderer = typeDefinitionDocumentRenderer;
}
/// <inheritdoc />
public async Task<string> GenerateTypeDefinitionsAsync(TypeDefinitionContext context)
{
var typeDefinitions = await GetTypeDefinitionsAsync(context).ToListAsync(context.CancellationToken);
var functionDefinitions = await GetFunctionDefinitionsAsync(context).ToListAsync(context.CancellationToken);
var document = new TypeDefinitionsDocument
{
Functions = functionDefinitions,
Types = typeDefinitions
};
return _typeDefinitionDocumentRenderer.Render(document);
}
private async IAsyncEnumerable<FunctionDefinition> GetFunctionDefinitionsAsync(TypeDefinitionContext context)
{
foreach (var provider in _functionDefinitionProviders)
{
var definitions = await provider.GetFunctionDefinitionsAsync(context);
foreach (var definition in definitions)
yield return definition;
}
}
private async IAsyncEnumerable<TypeDefinition> GetTypeDefinitionsAsync(TypeDefinitionContext context)
{
foreach (var provider in _typeDefinitionProviders)
{
var definitions = await provider.GetTypeDefinitionsAsync(context);
foreach (var definition in definitions)
yield return definition;
}
}
}

View file

@ -0,0 +1,90 @@
using System.Reflection;
using Elsa.Extensions;
using Elsa.JavaScript.Extensions;
using Elsa.JavaScript.Models;
using Elsa.JavaScript.Services;
namespace Elsa.JavaScript.Implementations;
/// <inheritdoc />
public class TypeDescriber : ITypeDescriber
{
private readonly ITypeAliasRegistry _typeAliasRegistry;
/// <summary>
/// Constructor.
/// </summary>
public TypeDescriber(ITypeAliasRegistry typeAliasRegistry)
{
_typeAliasRegistry = typeAliasRegistry;
}
/// <inheritdoc />
public TypeDefinition DescribeType(Type type)
{
var typeDefinition = new TypeDefinition
{
DeclarationKeyword = GetDeclarationKeyword(type),
Name = type.Name,
Properties = GetPropertyDefinitions(type).ToList(),
Methods = GetMethodDefinitions(type).ToList()
};
return typeDefinition;
}
private IEnumerable<FunctionDefinition> GetMethodDefinitions(Type type)
{
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).Where(x => !x.IsSpecialName).ToList();
foreach (var method in methods)
{
yield return new FunctionDefinition
{
Name = method.Name,
Parameters = GetMethodParameters(method).ToList(),
ReturnType = _typeAliasRegistry.TryGetAlias(method.ReturnType, out var alias) ? alias : "any"
};
}
}
private IEnumerable<ParameterDefinition> GetMethodParameters(MethodInfo method)
{
var parameters = method.GetParameters();
foreach (var parameter in parameters)
{
yield return new ParameterDefinition
{
Name = parameter.Name!,
Type = _typeAliasRegistry.TryGetAlias(parameter.ParameterType, out var alias) ? alias : "any",
IsOptional = parameter.IsOptional
};
}
}
private IEnumerable<PropertyDefinition> GetPropertyDefinitions(Type type)
{
var properties = type.GetProperties();
foreach (var property in properties)
{
yield return new PropertyDefinition
{
Name = property.Name,
Type = _typeAliasRegistry.TryGetAlias(property.PropertyType, out var alias) ? alias : "any",
IsOptional = property.PropertyType.IsNullableType()
};
}
}
private static string GetDeclarationKeyword(Type type) =>
type switch
{
{ IsInterface: true } => "interface",
{ IsClass: true } => "class",
{ IsValueType: true } => "class",
{ IsEnum: true } => "enum",
_ => "interface"
};
}

View file

@ -0,0 +1,48 @@
namespace Elsa.JavaScript.Models;
/// <summary>
/// A method definition.
/// </summary>
public class FunctionDefinition
{
/// <summary>
/// Constructor.
/// </summary>
public FunctionDefinition()
{
}
/// <summary>
/// Constructor.
/// </summary>
public FunctionDefinition(string name, IEnumerable<ParameterDefinition> parameters)
{
Name = name;
Parameters = parameters.ToList();
}
/// <summary>
/// Constructor.
/// </summary>
public FunctionDefinition(string name, string? returnType, IEnumerable<ParameterDefinition> parameters)
{
Name = name;
ReturnType = returnType;
Parameters = parameters.ToList();
}
/// <summary>
/// The name of the method.
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// The return type name of the method.
/// </summary>
public string? ReturnType { get; set; }
/// <summary>
/// The parameters of the method.
/// </summary>
public ICollection<ParameterDefinition> Parameters { get; set; } = new List<ParameterDefinition>();
}

View file

@ -0,0 +1,39 @@
namespace Elsa.JavaScript.Models;
/// <summary>
/// A method parameter definition.
/// </summary>
public class ParameterDefinition
{
/// <summary>
/// Constructor.
/// </summary>
public ParameterDefinition()
{
}
/// <summary>
/// Constructor.
/// </summary>
public ParameterDefinition(string name, string? type, bool isOptional = false)
{
Name = name;
Type = type;
IsOptional = isOptional;
}
/// <summary>
/// The name of the parameter.
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// The type name of the parameter.
/// </summary>
public string? Type { get; set; }
/// <summary>
/// A value indicating whether the parameter is optional or not.
/// </summary>
public bool IsOptional { get; set; }
}

View file

@ -0,0 +1,22 @@
namespace Elsa.JavaScript.Models;
/// <summary>
/// A property definition.
/// </summary>
public class PropertyDefinition
{
/// <summary>
/// The name of the property.
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// The type name of the property.
/// </summary>
public string Type { get; set; } = default!;
/// <summary>
/// A value indicating whether the property is optional or not.
/// </summary>
public bool IsOptional { get; set; }
}

View file

@ -0,0 +1,28 @@
namespace Elsa.JavaScript.Models;
/// <summary>
/// A base type that represents a type.
/// </summary>
public sealed class TypeDefinition
{
/// <summary>
/// The keyword that declares this type. E.g. "class", "type" or "interface".
/// </summary>
public string DeclarationKeyword { get; set; } = default!;
/// <summary>
/// The name of the type.
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// A list of fields.
/// </summary>
public ICollection<PropertyDefinition> Properties { get; set; } = new List<PropertyDefinition>();
/// <summary>
/// A list of methods.
/// </summary>
public ICollection<FunctionDefinition> Methods { get; set; } = new List<FunctionDefinition>();
}

View file

@ -0,0 +1,8 @@
using Elsa.Workflows.Core.Models;
namespace Elsa.JavaScript.Models;
/// <summary>
/// Provides context to intellisense providers.
/// </summary>
public record TypeDefinitionContext(ICollection<Variable> Variables, string? ActivityTypeName, string? PropertyName, CancellationToken CancellationToken);

View file

@ -0,0 +1,10 @@
namespace Elsa.JavaScript.Models;
/// <summary>
/// Represents a type definition document model.
/// </summary>
public sealed class TypeDefinitionsDocument
{
public ICollection<FunctionDefinition> Functions { get; set; } = new List<FunctionDefinition>();
public ICollection<TypeDefinition> Types { get; set; } = new List<TypeDefinition>();
}

View file

@ -0,0 +1,58 @@
using Elsa.Extensions;
using Elsa.JavaScript.Abstractions;
using Elsa.JavaScript.Extensions;
using Elsa.JavaScript.Models;
using Elsa.JavaScript.Services;
using Humanizer;
namespace Elsa.JavaScript.Providers;
/// <summary>
/// Produces <see cref="FunctionDefinition"/>s for common functions.
/// </summary>
internal class CommonFunctionDefinitionProvider : FunctionDefinitionProvider
{
private readonly ITypeAliasRegistry _typeAliasRegistry;
public CommonFunctionDefinitionProvider(ITypeAliasRegistry typeAliasRegistry)
{
_typeAliasRegistry = typeAliasRegistry;
}
protected override IEnumerable<FunctionDefinition> GetFunctionDefinitions(TypeDefinitionContext context)
{
yield return CreateFunctionDefinition(builder => builder
.Name("setVariable")
.Parameter("name", "string")
.Parameter("value", "any"));
yield return CreateFunctionDefinition(builder => builder
.Name("getVariable")
.Parameter("name", "string")
.ReturnType("any"));
yield return CreateFunctionDefinition(builder => builder
.Name("isNullOrWhiteSpace")
.Parameter("value", "string")
.ReturnType("boolean"));
yield return CreateFunctionDefinition(builder => builder
.Name("isNullOrEmpty")
.Parameter("value", "string")
.ReturnType("boolean"));
// Variable getter and setters.
foreach (var variable in context.Variables)
{
var pascalName = variable.Name.Pascalize();
var variableType = variable.GetVariableType();
var typeAlias = _typeAliasRegistry.TryGetAlias(variableType, out var alias) ? alias : "any";
// get{Variable}.
yield return CreateFunctionDefinition(builder => builder.Name($"get{pascalName}").ReturnType(typeAlias));
// set{Variable}.
yield return CreateFunctionDefinition(builder => builder.Name($"set{pascalName}").Parameter("value", typeAlias));
}
}
}

View file

@ -5,9 +5,9 @@ using Elsa.Workflows.Core.Services;
namespace Elsa.JavaScript.Providers;
public class JavaScriptExpressionSyntaxProvider : IExpressionSyntaxProvider
internal class JavaScriptExpressionSyntaxProvider : IExpressionSyntaxProvider
{
public const string SyntaxName = "JavaScript";
private const string SyntaxName = "JavaScript";
private readonly IIdentityGenerator _identityGenerator;
public JavaScriptExpressionSyntaxProvider(IIdentityGenerator identityGenerator)

View file

@ -0,0 +1,38 @@
using System.Dynamic;
using Elsa.Extensions;
using Elsa.JavaScript.Abstractions;
using Elsa.JavaScript.Models;
using Elsa.JavaScript.Services;
namespace Elsa.JavaScript.Providers;
/// <summary>
/// Produces <see cref="TypeDefinition"/>s for variable types.
/// </summary>
internal class VariableTypeDefinitionProvider : TypeDefinitionProvider
{
private readonly ITypeDescriber _typeDescriber;
public VariableTypeDefinitionProvider(ITypeDescriber typeDescriber)
{
_typeDescriber = typeDescriber;
}
protected override IEnumerable<TypeDefinition> GetTypeDefinitions(TypeDefinitionContext context)
{
var excludedTypes = new[] { typeof(ExpandoObject), typeof(IDictionary<string, object>) };
var variableTypeQuery =
from variable in context.Variables
let variableType = variable.GetVariableType()
where (variableType.IsClass || variableType.IsInterface) && !variableType.IsPrimitive && !excludedTypes.Any(x => x.IsAssignableFrom(variableType))
select variableType;
var variableTypes = variableTypeQuery.Distinct();
foreach (var variableType in variableTypes)
{
yield return _typeDescriber.DescribeType(variableType);
}
}
}

View file

@ -0,0 +1,14 @@
using Elsa.JavaScript.Models;
namespace Elsa.JavaScript.Services;
/// <summary>
/// Provides <see cref="FunctionDefinition"/>s to the type definition document being constructed.
/// </summary>
public interface IFunctionDefinitionProvider
{
/// <summary>
/// Returns a list of type definitions to the system.
/// </summary>
ValueTask<IEnumerable<FunctionDefinition>> GetFunctionDefinitionsAsync(TypeDefinitionContext context);
}

View file

@ -0,0 +1,17 @@
namespace Elsa.JavaScript.Services;
/// <summary>
/// A central registry of type aliases.
/// </summary>
public interface ITypeAliasRegistry
{
/// <summary>
/// Register a type with an alias.
/// </summary>
void RegisterType(Type type, string alias);
/// <summary>
/// Attempts to get an alias for the specified type.
/// </summary>
bool TryGetAlias(Type type, out string alias);
}

View file

@ -0,0 +1,12 @@
using Elsa.JavaScript.Models;
namespace Elsa.JavaScript.Services;
/// <summary>
/// Renders a <see cref="TypeDefinitionsDocument"/> to a string.
/// </summary>
public interface ITypeDefinitionDocumentRenderer
{
/// Renders a <see cref="TypeDefinitionsDocument"/> to a string.
string Render(TypeDefinitionsDocument document);
}

View file

@ -0,0 +1,14 @@
using Elsa.JavaScript.Models;
namespace Elsa.JavaScript.Services;
/// <summary>
/// Provides <see cref="TypeDefinition"/>s to the type definition document being constructed.
/// </summary>
public interface ITypeDefinitionProvider
{
/// <summary>
/// Returns a list of type definitions to the system.
/// </summary>
ValueTask<IEnumerable<TypeDefinition>> GetTypeDefinitionsAsync(TypeDefinitionContext context);
}

View file

@ -0,0 +1,14 @@
using Elsa.JavaScript.Models;
namespace Elsa.JavaScript.Services;
/// <summary>
/// Generates type definitions for the specified context.
/// </summary>
public interface ITypeDefinitionService
{
/// <summary>
/// Generates type definitions for the specified context.
/// </summary>
Task<string> GenerateTypeDefinitionsAsync(TypeDefinitionContext context);
}

View file

@ -0,0 +1,14 @@
using Elsa.JavaScript.Models;
namespace Elsa.JavaScript.Services;
/// <summary>
/// Returns a <see cref="TypeDefinition"/> from a given <see cref="Type"/>.
/// </summary>
public interface ITypeDescriber
{
/// <summary>
/// Returns a <see cref="TypeDefinition"/> from a given <see cref="Type"/>.
/// </summary>
TypeDefinition DescribeType(Type type);
}

View file

@ -13,8 +13,14 @@
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Api.Common\Elsa.Api.Common.csproj" />
<ProjectReference Include="..\Elsa.ActivityDefinitions\Elsa.ActivityDefinitions.csproj" />
<ProjectReference Include="..\Elsa.JavaScript\Elsa.JavaScript.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Endpoints\Scripting" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,78 @@
using System.Text;
using Elsa.ActivityDefinitions.Services;
using Elsa.Common.Models;
using Elsa.JavaScript.Models;
using Elsa.JavaScript.Services;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Runtime.Services;
using FastEndpoints;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Api.Endpoints.Scripting.JavaScript.TypeDefinitions;
/// <summary>
/// Returns a TypeScript definition that is used by the Monaco editor to display intellisense for JavaScript expressions.
/// </summary>
[PublicAPI]
internal class Get : Endpoint<Request>
{
private readonly ITypeDefinitionService _typeDefinitionService;
private readonly IServiceProvider _serviceProvider;
/// <inheritdoc />
public Get(ITypeDefinitionService typeDefinitionService, IServiceProvider serviceProvider)
{
_typeDefinitionService = typeDefinitionService;
_serviceProvider = serviceProvider;
}
/// <inheritdoc />
public override void Configure()
{
Post("scripting/javascript/type-definitions/{containerId}");
}
/// <inheritdoc />
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var variables = await GetVariables(request.ContainerType, request.ContainerId, cancellationToken);
var intellisenseContext = new TypeDefinitionContext(variables, request.ActivityTypeName, request.PropertyName, cancellationToken);
var typeDefinitions = await _typeDefinitionService.GenerateTypeDefinitionsAsync(intellisenseContext);
var fileName = $"elsa.{request.ContainerId}.d.ts";
var data = Encoding.UTF8.GetBytes(typeDefinitions);
await SendBytesAsync(data, fileName, "application/x-typescript", cancellation: cancellationToken);
}
private async Task<ICollection<Variable>> GetVariables(string containerType, string containerId, CancellationToken cancellationToken)
{
// TODO: Make this extensible.
switch (containerType)
{
case "workflow-definition":
{
var workflowDefinitionService = _serviceProvider.GetService<IWorkflowDefinitionService>();
var workflowDefinition = workflowDefinitionService != null ? await workflowDefinitionService.FindAsync(containerId, VersionOptions.Latest, cancellationToken) : default;
var x = workflowDefinition;
return workflowDefinition?.Variables ?? new List<Variable>();
}
case "activity-definition":
{
var activityDefinitionStore = _serviceProvider.GetService<IActivityDefinitionStore>();
var activityDefinition = activityDefinitionStore != null ? await activityDefinitionStore.FindByDefinitionIdAsync(containerId, VersionOptions.Latest, cancellationToken) : default;
return activityDefinition?.Variables ?? new List<Variable>();
}
}
return new List<Variable>();
}
}
internal record Request(string ContainerType, string ContainerId, string? ActivityTypeName, string? PropertyName)
{
public Request() : this(default!, default!, default, default)
{
}
}

View file

@ -53,4 +53,13 @@ public static class VariableExtensions
var genericType = variable.GetType().GenericTypeArguments.FirstOrDefault();
return genericType == null ? value : value.ConvertTo(genericType)!;
}
/// <summary>
/// Return the type of the variable.
/// </summary>
public static Type GetVariableType(this Variable variable)
{
var variableType = variable.GetType();
return variableType.GenericTypeArguments.Any() ? variableType.GetGenericArguments().First() : typeof(object);
}
}

View file

@ -1,5 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Expressions.Extensions;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Services;
using Elsa.Extensions;

View file

@ -1,3 +1,4 @@
using Elsa.Expressions.Extensions;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Services;
using Elsa.Extensions;

View file

@ -1,5 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Expressions.Extensions;
using Elsa.Expressions.Services;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Services;