mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-13 15:52:56 +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:
@@ -0,0 +1,296 @@
|
||||
'use strict';
|
||||
|
||||
const { Node } = require('@noodl/runtime');
|
||||
const Model = require('@noodl/runtime/src/model');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
|
||||
const graphEventEmitter = new EventEmitter();
|
||||
graphEventEmitter.setMaxListeners(1000000);
|
||||
|
||||
const ParentComponentObject = {
|
||||
name: 'net.noodl.ParentComponentObject',
|
||||
displayNodeName: 'Parent Component Object',
|
||||
category: 'Component Utilities',
|
||||
color: 'component',
|
||||
docs: 'https://docs.noodl.net/nodes/component-utilities/parent-component-object',
|
||||
initialize() {
|
||||
this._internal.inputValues = {};
|
||||
|
||||
this._internal.onModelChangedCallback = (args) => {
|
||||
if (this.isInputConnected('fetch') !== false) return;
|
||||
|
||||
if (this.hasOutput('value-' + args.name)) {
|
||||
this.flagOutputDirty('value-' + args.name);
|
||||
}
|
||||
|
||||
if (this.hasOutput('changed-' + args.name)) {
|
||||
this.sendSignalOnOutput('changed-' + args.name);
|
||||
}
|
||||
|
||||
this.sendSignalOnOutput('changed');
|
||||
};
|
||||
|
||||
//TODO: don't listen for delta updates when running deployed
|
||||
this.onComponentStateNodesChanged = () => {
|
||||
const id = this.findParentComponentStateModelId();
|
||||
|
||||
if (this._internal.modelId !== id) {
|
||||
this._internal.modelId = id;
|
||||
|
||||
if (this.isInputConnected('fetch') === false) {
|
||||
this.setModelId(this._internal.modelId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
graphEventEmitter.on('componentStateNodesChanged', this.onComponentStateNodesChanged);
|
||||
|
||||
this.updateComponentState();
|
||||
},
|
||||
//to search up the tree the root nodes in this component must have been initialized
|
||||
//we also need the connections to be setup so we can use isInputConnected
|
||||
//nodeScopeDidInitialize takes care of that
|
||||
nodeScopeDidInitialize() {
|
||||
//FIXME: temporary hack. Our parent's node scope might not have finished created yet
|
||||
//so just wait until after this update. It'll make the parent component state
|
||||
//have a delay in propagating outputs which can cause subtle bugs.
|
||||
//The fix is to call this code when the entire node tree has been created,
|
||||
//before running updating the next update.
|
||||
if (!this._internal.modelId) {
|
||||
this.context.scheduleAfterUpdate(() => {
|
||||
this.updateComponentState();
|
||||
});
|
||||
}
|
||||
},
|
||||
getInspectInfo() {
|
||||
const model = this._internal.model;
|
||||
if (!model) return 'No parent component state found';
|
||||
|
||||
const modelInfo = [{ type: 'text', value: this._internal.parentComponentName }];
|
||||
|
||||
const data = this._internal.model.data;
|
||||
return modelInfo.concat(
|
||||
Object.keys(data).map((key) => {
|
||||
return { type: 'text', value: key + ': ' + data[key] };
|
||||
})
|
||||
);
|
||||
},
|
||||
inputs: {
|
||||
properties: {
|
||||
type: {
|
||||
name: 'stringlist',
|
||||
allowEditOnly: true
|
||||
},
|
||||
displayName: 'Properties',
|
||||
group: 'Properties',
|
||||
set(value) {}
|
||||
},
|
||||
fetch: {
|
||||
displayName: 'Fetch',
|
||||
group: 'Actions',
|
||||
valueChangedToTrue: function () {
|
||||
this.setModelId(this._internal.modelId);
|
||||
}
|
||||
}
|
||||
},
|
||||
outputs: {
|
||||
changed: {
|
||||
type: 'signal',
|
||||
displayName: 'Changed',
|
||||
group: 'Events'
|
||||
},
|
||||
fetched: {
|
||||
type: 'signal',
|
||||
displayName: 'Fetched',
|
||||
group: 'Events'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateComponentState() {
|
||||
this._internal.modelId = this.findParentComponentStateModelId();
|
||||
if (this.isInputConnected('fetch') === false) {
|
||||
this.setModelId(this._internal.modelId);
|
||||
}
|
||||
},
|
||||
findParentComponentStateModelId() {
|
||||
function getParentComponent(component) {
|
||||
let parent;
|
||||
if (component.getRoots().length > 0) {
|
||||
//visual
|
||||
const root = component.getRoots()[0];
|
||||
|
||||
if (root.getVisualParentNode) {
|
||||
//regular visual node
|
||||
if (root.getVisualParentNode()) {
|
||||
parent = root.getVisualParentNode().nodeScope.componentOwner;
|
||||
}
|
||||
} else if (root.parentNodeScope) {
|
||||
//component instance node
|
||||
parent = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
} else if (component.parentNodeScope) {
|
||||
parent = component.parentNodeScope.componentOwner;
|
||||
}
|
||||
|
||||
//check that a parent exists and that the component is different
|
||||
if (parent && parent.nodeScope && parent.nodeScope.componentOwner !== component) {
|
||||
//check if parent has a Component State node
|
||||
if (parent.nodeScope.getNodesWithType('net.noodl.ComponentObject').length > 0) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
//if not, continue searching up the tree
|
||||
return getParentComponent(parent);
|
||||
}
|
||||
}
|
||||
|
||||
const parent = getParentComponent(this.nodeScope.componentOwner);
|
||||
if (!parent) return;
|
||||
|
||||
this._internal.parentComponentName = parent.name;
|
||||
|
||||
return 'componentState' + parent.getInstanceId();
|
||||
},
|
||||
setModelId(id) {
|
||||
this._internal.model && this._internal.model.off('change', this._internal.onModelChangedCallback);
|
||||
this._internal.model = undefined;
|
||||
|
||||
if (!id) return;
|
||||
|
||||
const model = Model.get(id);
|
||||
this._internal.model = model;
|
||||
|
||||
model.on('change', this._internal.onModelChangedCallback);
|
||||
|
||||
for (var key in model.data) {
|
||||
if (this.hasOutput('value-' + key)) {
|
||||
this.flagOutputDirty('value-' + key);
|
||||
}
|
||||
if (this.hasOutput('changed-' + key)) {
|
||||
this.sendSignalOnOutput('changed-' + key);
|
||||
}
|
||||
}
|
||||
|
||||
this.sendSignalOnOutput('changed');
|
||||
this.sendSignalOnOutput('fetched');
|
||||
},
|
||||
scheduleStore() {
|
||||
if (this.hasScheduledStore) return;
|
||||
this.hasScheduledStore = true;
|
||||
|
||||
var internal = this._internal;
|
||||
this.scheduleAfterInputsHaveUpdated(() => {
|
||||
this.hasScheduledStore = false;
|
||||
if (!internal.model) return;
|
||||
for (var i in internal.inputValues) {
|
||||
internal.model.set(i, internal.inputValues[i], { resolve: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
_onNodeDeleted() {
|
||||
Node.prototype._onNodeDeleted.call(this);
|
||||
|
||||
graphEventEmitter.off('componentStateNodesChanged', this.onComponentStateNodesChanged);
|
||||
this._internal.model && this._internal.model.off('change', this._internal.onModelChangedCallback);
|
||||
},
|
||||
registerOutputIfNeeded(name) {
|
||||
if (this.hasOutput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propertyName = name.substring('value-'.length);
|
||||
|
||||
this.registerOutput(name, {
|
||||
get() {
|
||||
if (!this._internal.model) return undefined;
|
||||
return this._internal.model.get(propertyName, { resolve: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
registerInputIfNeeded: function (name) {
|
||||
if (this.hasInput(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.startsWith('value-')) {
|
||||
const propertyName = name.substring('value-'.length);
|
||||
this.registerInput(name, {
|
||||
set(value) {
|
||||
this._internal.inputValues[propertyName] = value;
|
||||
|
||||
this.scheduleStore();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updatePorts(nodeId, parameters, editorConnection) {
|
||||
const ports = [];
|
||||
|
||||
// Add value outputs
|
||||
var properties = parameters.properties && parameters.properties.split(',');
|
||||
for (var i in properties) {
|
||||
var p = properties[i];
|
||||
|
||||
ports.push({
|
||||
type: {
|
||||
name: '*', //parameters['type-' + p] || 'string',
|
||||
allowConnectionsOnly: true
|
||||
},
|
||||
plug: 'input/output',
|
||||
group: 'Properties',
|
||||
name: 'value-' + p,
|
||||
displayName: p
|
||||
});
|
||||
|
||||
ports.push({
|
||||
type: 'signal',
|
||||
plug: 'output',
|
||||
group: 'Changed Events',
|
||||
displayName: p + ' Changed',
|
||||
name: 'changed-' + p
|
||||
});
|
||||
}
|
||||
|
||||
editorConnection.sendDynamicPorts(nodeId, ports, {
|
||||
detectRenamed: {
|
||||
plug: 'input/output'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
node: ParentComponentObject,
|
||||
setup: function (context, graphModel) {
|
||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphModel.on('nodeAdded.net.noodl.ParentComponentObject', (node) => {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||
|
||||
node.on('parameterUpdated', (event) => {
|
||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||
});
|
||||
});
|
||||
|
||||
//TODO: handle additional delta update event:
|
||||
// - visual parent changed
|
||||
|
||||
//this are the same events that'll create and delete the Comopent State instance node
|
||||
//it might not have had a chance to run yet if we're first in the event list, so
|
||||
//use a setTimeout
|
||||
graphModel.on('nodeAdded.net.noodl.ComponentObject', (node) => {
|
||||
setTimeout(() => {
|
||||
graphEventEmitter.emit('componentStateNodesChanged');
|
||||
}, 0);
|
||||
});
|
||||
graphModel.on('nodeRemoved.net.noodl.ComponentObject', (node) => {
|
||||
setTimeout(() => {
|
||||
graphEventEmitter.emit('componentStateNodesChanged');
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user