mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-15 08:42:55 +01:00
Initial commit
Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com> Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com> Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com> Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com> Co-Authored-By: Johan <4934465+joolsus@users.noreply.github.com> Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com> Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
416
packages/noodl-runtime/src/nodes/std-library/simplejavascript.js
Normal file
416
packages/noodl-runtime/src/nodes/std-library/simplejavascript.js
Normal file
@@ -0,0 +1,416 @@
|
||||
const JavascriptNodeParser = require('../../javascriptnodeparser');
|
||||
|
||||
const SimpleJavascriptNode = {
|
||||
name: 'JavaScriptFunction',
|
||||
displayNodeName: 'Function',
|
||||
docs: 'https://docs.noodl.net/nodes/javascript/function',
|
||||
category: 'CustomCode',
|
||||
color: 'javascript',
|
||||
nodeDoubleClickAction: {
|
||||
focusPort: 'Script'
|
||||
},
|
||||
searchTags: ['javascript'],
|
||||
exportDynamicPorts: true,
|
||||
initialize: function () {
|
||||
this._internal.inputValues = {};
|
||||
this._internal.outputValues = {};
|
||||
|
||||
this._internal.outputValuesProxy = new Proxy(this._internal.outputValues, {
|
||||
set: (obj, prop, value) => {
|
||||
//a function node can continue running after it has been deleted. E.g. with timeouts or event listeners that hasn't been removed.
|
||||
//if the node is deleted, just do nothing
|
||||
if (this._deleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
//only send outputs when they change.
|
||||
//Some Noodl projects rely on this behavior, so changing it breaks backwards compability
|
||||
if (value !== this._internal.outputValues[prop]) {
|
||||
this.registerOutputIfNeeded('out-' + prop);
|
||||
|
||||
this._internal.outputValues[prop] = value;
|
||||
this.flagOutputDirty('out-' + prop);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
this._internal._this = {};
|
||||
},
|
||||
getInspectInfo() {
|
||||
return [
|
||||
{
|
||||
type: 'value',
|
||||
value: {
|
||||
inputs: this._internal.inputValues,
|
||||
outputs: this._internal.outputValues
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
inputs: {
|
||||
scriptInputs: {
|
||||
type: {
|
||||
name: 'proplist',
|
||||
allowEditOnly: true
|
||||
},
|
||||
group: 'Script Inputs',
|
||||
set(value) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
scriptOutputs: {
|
||||
type: {
|
||||
name: 'proplist',
|
||||
allowEditOnly: true
|
||||
},
|
||||
group: 'Script Outputs',
|
||||
set(value) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
functionScript: {
|
||||
displayName: 'Script',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
codeeditor: 'javascript'
|
||||
},
|
||||
group: 'General',
|
||||
set(script) {
|
||||
if (script === undefined) {
|
||||
this._internal.func = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this._internal.func = this.parseScript(script);
|
||||
|
||||
if (!this.isInputConnected('run')) this.scheduleRun();
|
||||
}
|
||||
},
|
||||
run: {
|
||||
type: 'signal',
|
||||
displayName: 'Run',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.scheduleRun();
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {},
|
||||
methods: {
|
||||
scheduleRun: function () {
|
||||
if (this.runScheduled) return;
|
||||
this.runScheduled = true;
|
||||
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.runScheduled = false;
|
||||
|
||||
if (!this._deleted) {
|
||||
this.runScript();
|
||||
}
|
||||
});
|
||||
},
|
||||
runScript: async function () {
|
||||
const func = this._internal.func;
|
||||
|
||||
if (func === undefined) return;
|
||||
|
||||
const inputs = this._internal.inputValues;
|
||||
const outputs = this._internal.outputValuesProxy;
|
||||
|
||||
// Prepare send signal functions
|
||||
for (const key in this.model.outputPorts) {
|
||||
if (this._isSignalType(key)) {
|
||||
const _sendSignal = () => {
|
||||
if (this.hasOutput(key)) this.sendSignalOnOutput(key);
|
||||
};
|
||||
this._internal.outputValues[key.substring('out-'.length)] = _sendSignal;
|
||||
this._internal.outputValues[key.substring('out-'.length)].send = _sendSignal;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await func.apply(this._internal._this, [
|
||||
inputs,
|
||||
outputs,
|
||||
JavascriptNodeParser.createNoodlAPI(this.nodeScope.modelScope),
|
||||
JavascriptNodeParser.getComponentScopeForNode(this)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'Error in JS node run code.',
|
||||
Object.getPrototypeOf(e).constructor.name + ': ' + e.message,
|
||||
e.stack
|
||||
);
|
||||
if (this.context.editorConnection && this.context.isWarningTypeEnabled('javascriptExecution')) {
|
||||
this.context.editorConnection.sendWarning(
|
||||
this.nodeScope.componentOwner.name,
|
||||
this.id,
|
||||
'js-function-run-waring',
|
||||
{
|
||||
showGlobally: true,
|
||||
message: e.message,
|
||||
stack: e.stack
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
setScriptInputValue: function (name, value) {
|
||||
this._internal.inputValues[name] = value;
|
||||
|
||||
if (!this.isInputConnected('run')) this.scheduleRun();
|
||||
},
|
||||
getScriptOutputValue: function (name) {
|
||||
if (this._isSignalType(name)) {
|
||||
return undefined;
|
||||
}
|
||||
return this._internal.outputValues[name];
|
||||
},
|
||||
setScriptInputType: function (name, type) {
|
||||
this._internal.inputTypes[name] = type;
|
||||
},
|
||||
setScriptOutputType: function (name, type) {
|
||||
this._internal.outputTypes[name] = type;
|
||||
},
|
||||
parseScript: function (script) {
|
||||
var func;
|
||||
try {
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
func = new AsyncFunction(
|
||||
'Inputs',
|
||||
'Outputs',
|
||||
'Noodl',
|
||||
'Component',
|
||||
JavascriptNodeParser.getCodePrefix() + script
|
||||
);
|
||||
} catch (e) {
|
||||
console.log('Error while parsing action script: ' + e);
|
||||
}
|
||||
|
||||
return func;
|
||||
},
|
||||
_isSignalType: function (name) {
|
||||
// This will catch signals in script that may not have been delivered by the editor yet
|
||||
return this.model.outputPorts[name] && this.model.outputPorts[name].type === 'signal';
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('in-')) {
|
||||
const n = name.substring('in-'.length);
|
||||
|
||||
const input = {
|
||||
set: this.setScriptInputValue.bind(this, n)
|
||||
};
|
||||
|
||||
//make sure we register the type as well, so Noodl resolves types like color styles to an actual color
|
||||
if (this.model && this.model.parameters['intype-' + n]) {
|
||||
input.type = this.model.parameters['intype-' + n];
|
||||
}
|
||||
|
||||
this.registerInput(name, input);
|
||||
}
|
||||
|
||||
if (name.startsWith('intype-')) {
|
||||
const n = name.substring('intype-'.length);
|
||||
|
||||
this.registerInput(name, {
|
||||
set(value) {
|
||||
//make sure we register the type as well, so Noodl resolves types like color styles to an actual color
|
||||
if (this.hasInput('in' + n)) {
|
||||
this.getInput('in' + n).type = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (name.startsWith('outtype-')) {
|
||||
this.registerInput(name, {
|
||||
set() {} // Ignore
|
||||
});
|
||||
}
|
||||
},
|
||||
registerOutputIfNeeded: function (name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('out-'))
|
||||
return this.registerOutput(name, {
|
||||
getter: this.getScriptOutputValue.bind(this, name.substring('out-'.length))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function _parseScriptForErrorsAndPorts(script, name, node, context, ports) {
|
||||
// Clear run warnings if the script is edited
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'js-function-run-waring');
|
||||
|
||||
if (script === undefined) {
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'js-function-parse-waring');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||
new AsyncFunction('Inputs', 'Outputs', 'Noodl', 'Component', script);
|
||||
|
||||
context.editorConnection.clearWarning(node.component.name, node.id, 'js-function-parse-waring');
|
||||
} catch (e) {
|
||||
context.editorConnection.sendWarning(node.component.name, node.id, 'js-function-parse-waring', {
|
||||
showGlobally: true,
|
||||
message: e.message
|
||||
});
|
||||
}
|
||||
|
||||
JavascriptNodeParser.parseAndAddPortsFromScript(script, ports, {
|
||||
inputPrefix: 'in-',
|
||||
outputPrefix: 'out-'
|
||||
});
|
||||
}
|
||||
|
||||
const inputTypeEnums = [
|
||||
{
|
||||
value: 'string',
|
||||
label: 'String'
|
||||
},
|
||||
{
|
||||
value: 'boolean',
|
||||
label: 'Boolean'
|
||||
},
|
||||
{
|
||||
value: 'number',
|
||||
label: 'Number'
|
||||
},
|
||||
{
|
||||
value: 'object',
|
||||
label: 'Object'
|
||||
},
|
||||
{
|
||||
value: 'date',
|
||||
label: 'Date'
|
||||
},
|
||||
{
|
||||
value: 'array',
|
||||
label: 'Array'
|
||||
},
|
||||
{
|
||||
value: 'color',
|
||||
label: 'Color'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
node: SimpleJavascriptNode,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function _managePortsForNode(node) {
|
||||
function _updatePorts() {
|
||||
var ports = [];
|
||||
|
||||
const _outputTypeEnums = inputTypeEnums.concat([
|
||||
{
|
||||
value: 'signal',
|
||||
label: 'Signal'
|
||||
}
|
||||
]);
|
||||
|
||||
// Outputs
|
||||
if (node.parameters['scriptOutputs'] !== undefined && node.parameters['scriptOutputs'].length > 0) {
|
||||
node.parameters['scriptOutputs'].forEach((p) => {
|
||||
// Type for output
|
||||
ports.push({
|
||||
name: 'outtype-' + p.label,
|
||||
displayName: 'Type',
|
||||
editorName: p.label + ' | Type',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: _outputTypeEnums,
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'string',
|
||||
parent: 'scriptOutputs',
|
||||
parentItemId: p.id
|
||||
});
|
||||
|
||||
// Value for output
|
||||
ports.push({
|
||||
name: 'out-' + p.label,
|
||||
displayName: p.label,
|
||||
plug: 'output',
|
||||
type: node.parameters['outtype-' + p.label] || '*',
|
||||
group: 'Outputs'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Inputs
|
||||
if (node.parameters['scriptInputs'] !== undefined && node.parameters['scriptInputs'].length > 0) {
|
||||
node.parameters['scriptInputs'].forEach((p) => {
|
||||
// Type for input
|
||||
ports.push({
|
||||
name: 'intype-' + p.label,
|
||||
displayName: 'Type',
|
||||
editorName: p.label + ' | Type',
|
||||
plug: 'input',
|
||||
type: {
|
||||
name: 'enum',
|
||||
enums: inputTypeEnums,
|
||||
allowEditOnly: true
|
||||
},
|
||||
default: 'string',
|
||||
parent: 'scriptInputs',
|
||||
parentItemId: p.id
|
||||
});
|
||||
|
||||
// Default Value for input
|
||||
ports.push({
|
||||
name: 'in-' + p.label,
|
||||
displayName: p.label,
|
||||
plug: 'input',
|
||||
type: node.parameters['intype-' + p.label] || 'string',
|
||||
group: 'Inputs'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_parseScriptForErrorsAndPorts(node.parameters['functionScript'], 'Script ', node, context, ports);
|
||||
|
||||
// Push output ports that are signals directly to the model, it's needed by the initial run of
|
||||
// the script function
|
||||
ports.forEach((p) => {
|
||||
if (p.type === 'signal' && p.plug === 'output') {
|
||||
node.outputPorts[p.name] = p;
|
||||
}
|
||||
});
|
||||
|
||||
context.editorConnection.sendDynamicPorts(node.id, ports);
|
||||
}
|
||||
|
||||
_updatePorts();
|
||||
node.on('parameterUpdated', function (ev) {
|
||||
_updatePorts();
|
||||
});
|
||||
}
|
||||
|
||||
graphModel.on('editorImportComplete', () => {
|
||||
graphModel.on('nodeAdded.JavaScriptFunction', function (node) {
|
||||
_managePortsForNode(node);
|
||||
});
|
||||
|
||||
for (const node of graphModel.getNodesWithType('JavaScriptFunction')) {
|
||||
_managePortsForNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user