mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-13 15:52:56 +01:00
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>
297 lines
8.7 KiB
JavaScript
297 lines
8.7 KiB
JavaScript
'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');
|
|
});
|
|
});
|
|
}
|
|
};
|