mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-11 14:52:55 +01:00
Finished HTTP node creation and extensive node creation documentation in project
This commit is contained in:
152
.clinerules
152
.clinerules
@@ -927,3 +927,155 @@ If you don't see the log, the subscription isn't working.
|
|||||||
|
|
||||||
_Last Updated: December 2025_
|
_Last Updated: December 2025_
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Node Creation Checklist
|
||||||
|
|
||||||
|
> **🚨 CRITICAL:** Before creating or modifying runtime nodes, read `dev-docs/reference/LEARNINGS-NODE-CREATION.md`
|
||||||
|
|
||||||
|
Creating nodes in OpenNoodl is deceptively tricky. This checklist prevents the most common (and hardest to debug) issues.
|
||||||
|
|
||||||
|
### 14.1 Pre-Flight Checklist
|
||||||
|
|
||||||
|
Before writing any node code:
|
||||||
|
|
||||||
|
- [ ] Read `dev-docs/reference/LEARNINGS-NODE-CREATION.md` (especially the CRITICAL GOTCHAS section)
|
||||||
|
- [ ] Study an existing working node of similar complexity (e.g., `restnode.js` for data nodes)
|
||||||
|
- [ ] Understand the difference between `inputs` (static) vs `prototypeExtensions` (instance methods)
|
||||||
|
- [ ] Know where your node should be registered (noodl-runtime vs noodl-viewer-react)
|
||||||
|
|
||||||
|
### 14.2 Input Handler Rules
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ✅ CORRECT: Signal inputs use valueChangedToTrue
|
||||||
|
inputs: {
|
||||||
|
fetch: {
|
||||||
|
type: 'signal',
|
||||||
|
valueChangedToTrue: function() {
|
||||||
|
this.scheduleFetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ WRONG: Signal inputs with set() - NEVER TRIGGERS
|
||||||
|
inputs: {
|
||||||
|
fetch: {
|
||||||
|
type: 'signal',
|
||||||
|
set: function(value) { // ☠️ Never called for signals
|
||||||
|
this.scheduleFetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14.3 Never Override setInputValue
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ BREAKS EVERYTHING - Never define setInputValue in prototypeExtensions
|
||||||
|
prototypeExtensions: {
|
||||||
|
setInputValue: function(name, value) { // ☠️ Overrides base - signals stop working
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Use a different name for custom storage
|
||||||
|
prototypeExtensions: {
|
||||||
|
_storeInputValue: function(name, value) { // ✅ Doesn't override anything
|
||||||
|
this._internal.inputValues[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14.4 Dynamic Ports Must Include Static Ports
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ WRONG - Static ports disappear
|
||||||
|
function updatePorts(nodeId, parameters, editorConnection) {
|
||||||
|
const ports = [];
|
||||||
|
// Only adds dynamic ports...
|
||||||
|
editorConnection.sendDynamicPorts(nodeId, ports); // Static inputs gone!
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ CORRECT - Include all ports
|
||||||
|
function updatePorts(nodeId, parameters, editorConnection) {
|
||||||
|
const ports = [
|
||||||
|
// Re-add static inputs
|
||||||
|
{ name: 'url', displayName: 'URL', type: 'string', plug: 'input', group: 'Request' },
|
||||||
|
{ name: 'fetch', displayName: 'Fetch', type: 'signal', plug: 'input', group: 'Actions' },
|
||||||
|
// Then add dynamic ports...
|
||||||
|
];
|
||||||
|
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14.5 Register Config Inputs Explicitly
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Config inputs (from stringlist editors) need explicit registration
|
||||||
|
registerInputIfNeeded: function(name) {
|
||||||
|
if (this.hasInput(name)) return;
|
||||||
|
|
||||||
|
// Map config names to their setters
|
||||||
|
const configSetters = {
|
||||||
|
'method': this.setMethod.bind(this),
|
||||||
|
'headers': this.setHeaders.bind(this),
|
||||||
|
'queryParams': this.setQueryParams.bind(this)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (configSetters[name]) {
|
||||||
|
return this.registerInput(name, { set: configSetters[name] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle prefixed dynamic inputs
|
||||||
|
if (name.startsWith('header-')) {
|
||||||
|
return this.registerInput(name, {
|
||||||
|
set: this._storeInputValue.bind(this, name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14.6 Export Format Matters
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ✅ CORRECT: Export with setup function
|
||||||
|
module.exports = {
|
||||||
|
node: MyNode,
|
||||||
|
setup: function (context, graphModel) {
|
||||||
|
// Port management goes here
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ❌ WRONG: Direct export (setup never runs)
|
||||||
|
module.exports = MyNode;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14.7 Post-Creation Verification
|
||||||
|
|
||||||
|
After creating a node:
|
||||||
|
|
||||||
|
1. **Check ports appear**: All static AND dynamic inputs/outputs visible in editor?
|
||||||
|
2. **Check signals work**: Add console.log in `valueChangedToTrue` - does it print?
|
||||||
|
3. **Check config inputs work**: Change dropdown/stringlist values - does setter get called?
|
||||||
|
4. **Clear caches if needed**: `npm run clean:all` and restart
|
||||||
|
|
||||||
|
### 14.8 Quick Reference
|
||||||
|
|
||||||
|
| Input Type | Handler | Callback Format |
|
||||||
|
| ---------------------------- | --------------------------- | --------------------------- |
|
||||||
|
| Signal | `valueChangedToTrue` | `function() { ... }` |
|
||||||
|
| Value (string, number, etc.) | `set` | `function(value) { ... }` |
|
||||||
|
| Enum (dropdown) | `set` | `function(value) { ... }` |
|
||||||
|
| StringList (config) | Needs explicit registration | Via `registerInputIfNeeded` |
|
||||||
|
|
||||||
|
### 14.9 Where to Find Examples
|
||||||
|
|
||||||
|
| Pattern | Example File |
|
||||||
|
| ------------------------------------ | ---------------------------------------------------------------- |
|
||||||
|
| Complex data node with dynamic ports | `noodl-runtime/src/nodes/std-library/data/restnode.js` |
|
||||||
|
| HTTP node (fixed, working) | `noodl-runtime/src/nodes/std-library/data/httpnode.js` |
|
||||||
|
| Simple value node | `noodl-runtime/src/nodes/std-library/variables/numbernode.js` |
|
||||||
|
| Signal-based node | `noodl-runtime/src/nodes/std-library/timer.js` (in viewer-react) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -34,39 +34,39 @@ packages/noodl-runtime/src/nodes/std-library/
|
|||||||
var MyNode = {
|
var MyNode = {
|
||||||
// REQUIRED: Unique identifier for the node
|
// REQUIRED: Unique identifier for the node
|
||||||
name: 'net.noodl.MyNode',
|
name: 'net.noodl.MyNode',
|
||||||
|
|
||||||
// REQUIRED: Display name in Node Picker and canvas
|
// REQUIRED: Display name in Node Picker and canvas
|
||||||
displayNodeName: 'My Node',
|
displayNodeName: 'My Node',
|
||||||
|
|
||||||
// OPTIONAL: Documentation URL
|
// OPTIONAL: Documentation URL
|
||||||
docs: 'https://docs.noodl.net/nodes/category/my-node',
|
docs: 'https://docs.noodl.net/nodes/category/my-node',
|
||||||
|
|
||||||
// REQUIRED: Category for organization (Data, Visual, Logic, etc.)
|
// REQUIRED: Category for organization (Data, Visual, Logic, etc.)
|
||||||
category: 'Data',
|
category: 'Data',
|
||||||
|
|
||||||
// OPTIONAL: Node color theme
|
// OPTIONAL: Node color theme
|
||||||
// Options: 'data' (green), 'visual' (blue), 'component' (purple), 'javascript' (pink), 'default' (gray)
|
// Options: 'data' (green), 'visual' (blue), 'component' (purple), 'javascript' (pink), 'default' (gray)
|
||||||
color: 'data',
|
color: 'data',
|
||||||
|
|
||||||
// OPTIONAL: Search keywords for Node Picker
|
// OPTIONAL: Search keywords for Node Picker
|
||||||
searchTags: ['my', 'node', 'custom', 'example'],
|
searchTags: ['my', 'node', 'custom', 'example'],
|
||||||
|
|
||||||
// OPTIONAL: Called when node instance is created
|
// OPTIONAL: Called when node instance is created
|
||||||
initialize: function () {
|
initialize: function () {
|
||||||
this._internal.myData = {};
|
this._internal.myData = {};
|
||||||
},
|
},
|
||||||
|
|
||||||
// OPTIONAL: Data shown in debug inspector
|
// OPTIONAL: Data shown in debug inspector
|
||||||
getInspectInfo() {
|
getInspectInfo() {
|
||||||
return this._internal.inspectData;
|
return this._internal.inspectData;
|
||||||
},
|
},
|
||||||
|
|
||||||
// REQUIRED: Define input ports
|
// REQUIRED: Define input ports
|
||||||
inputs: {
|
inputs: {
|
||||||
inputName: {
|
inputName: {
|
||||||
type: 'string', // See "Port Types" section below
|
type: 'string', // See "Port Types" section below
|
||||||
displayName: 'Input Name',
|
displayName: 'Input Name',
|
||||||
group: 'General', // Group in property panel
|
group: 'General', // Group in property panel
|
||||||
default: 'default value'
|
default: 'default value'
|
||||||
},
|
},
|
||||||
doAction: {
|
doAction: {
|
||||||
@@ -75,7 +75,7 @@ var MyNode = {
|
|||||||
group: 'Actions'
|
group: 'Actions'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// REQUIRED: Define output ports
|
// REQUIRED: Define output ports
|
||||||
outputs: {
|
outputs: {
|
||||||
outputValue: {
|
outputValue: {
|
||||||
@@ -94,7 +94,7 @@ var MyNode = {
|
|||||||
group: 'Events'
|
group: 'Events'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// OPTIONAL: Methods to handle input changes
|
// OPTIONAL: Methods to handle input changes
|
||||||
methods: {
|
methods: {
|
||||||
setInputName: function (value) {
|
setInputName: function (value) {
|
||||||
@@ -102,19 +102,19 @@ var MyNode = {
|
|||||||
// Optionally trigger output update
|
// Optionally trigger output update
|
||||||
this.flagOutputDirty('outputValue');
|
this.flagOutputDirty('outputValue');
|
||||||
},
|
},
|
||||||
|
|
||||||
// Signal handler - name must match input name with 'Trigger' suffix
|
// Signal handler - name must match input name with 'Trigger' suffix
|
||||||
doActionTrigger: function () {
|
doActionTrigger: function () {
|
||||||
// Perform the action
|
// Perform the action
|
||||||
const result = this.processInput(this._internal.inputName);
|
const result = this.processInput(this._internal.inputName);
|
||||||
this._internal.outputValue = result;
|
this._internal.outputValue = result;
|
||||||
|
|
||||||
// Update outputs
|
// Update outputs
|
||||||
this.flagOutputDirty('outputValue');
|
this.flagOutputDirty('outputValue');
|
||||||
this.sendSignalOnOutput('success');
|
this.sendSignalOnOutput('success');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// OPTIONAL: Return output values
|
// OPTIONAL: Return output values
|
||||||
getOutputValue: function (name) {
|
getOutputValue: function (name) {
|
||||||
if (name === 'outputValue') {
|
if (name === 'outputValue') {
|
||||||
@@ -126,7 +126,7 @@ var MyNode = {
|
|||||||
// REQUIRED: Export the node
|
// REQUIRED: Export the node
|
||||||
module.exports = {
|
module.exports = {
|
||||||
node: MyNode,
|
node: MyNode,
|
||||||
|
|
||||||
// OPTIONAL: Setup function for dynamic ports
|
// OPTIONAL: Setup function for dynamic ports
|
||||||
setup: function (context, graphModel) {
|
setup: function (context, graphModel) {
|
||||||
// See "Dynamic Ports" section below
|
// See "Dynamic Ports" section below
|
||||||
@@ -144,10 +144,10 @@ Add the node to the `registerNodes` function in `packages/noodl-runtime/noodl-ru
|
|||||||
function registerNodes(noodlRuntime) {
|
function registerNodes(noodlRuntime) {
|
||||||
[
|
[
|
||||||
// ... existing nodes ...
|
// ... existing nodes ...
|
||||||
|
|
||||||
// Add your new node
|
// Add your new node
|
||||||
require('./src/nodes/std-library/data/mynode'),
|
require('./src/nodes/std-library/data/mynode')
|
||||||
|
|
||||||
// ... more nodes ...
|
// ... more nodes ...
|
||||||
].forEach((node) => noodlRuntime.registerNode(node));
|
].forEach((node) => noodlRuntime.registerNode(node));
|
||||||
}
|
}
|
||||||
@@ -174,10 +174,10 @@ const coreNodes = [
|
|||||||
// ... other subcategories ...
|
// ... other subcategories ...
|
||||||
{
|
{
|
||||||
name: 'External Data',
|
name: 'External Data',
|
||||||
items: ['net.noodl.MyNode', 'REST2'] // Add your node name here
|
items: ['net.noodl.MyNode', 'REST2'] // Add your node name here
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
// ... more categories ...
|
// ... more categories ...
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
@@ -188,24 +188,24 @@ const coreNodes = [
|
|||||||
|
|
||||||
### Common Input/Output Types
|
### Common Input/Output Types
|
||||||
|
|
||||||
| Type | Description | Example Use |
|
| Type | Description | Example Use |
|
||||||
|------|-------------|-------------|
|
| --------- | -------------------- | ------------------------------ |
|
||||||
| `string` | Text value | URLs, names, content |
|
| `string` | Text value | URLs, names, content |
|
||||||
| `number` | Numeric value | Counts, sizes, coordinates |
|
| `number` | Numeric value | Counts, sizes, coordinates |
|
||||||
| `boolean` | True/false | Toggles, conditions |
|
| `boolean` | True/false | Toggles, conditions |
|
||||||
| `signal` | Trigger without data | Action buttons, events |
|
| `signal` | Trigger without data | Action buttons, events |
|
||||||
| `object` | JSON object | API responses, data structures |
|
| `object` | JSON object | API responses, data structures |
|
||||||
| `array` | List of items | Collections, results |
|
| `array` | List of items | Collections, results |
|
||||||
| `color` | Color value | Styling |
|
| `color` | Color value | Styling |
|
||||||
| `*` | Any type | Generic ports |
|
| `*` | Any type | Generic ports |
|
||||||
|
|
||||||
### Input-Specific Types
|
### Input-Specific Types
|
||||||
|
|
||||||
| Type | Description |
|
| Type | Description |
|
||||||
|------|-------------|
|
| -------------------------------- | --------------------------------- |
|
||||||
| `{ name: 'enum', enums: [...] }` | Dropdown selection |
|
| `{ name: 'enum', enums: [...] }` | Dropdown selection |
|
||||||
| `{ name: 'stringlist' }` | List of strings (comma-separated) |
|
| `{ name: 'stringlist' }` | List of strings (comma-separated) |
|
||||||
| `{ name: 'number', min, max }` | Number with constraints |
|
| `{ name: 'number', min, max }` | Number with constraints |
|
||||||
|
|
||||||
### Example Enum Input
|
### Example Enum Input
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ Dynamic ports are created at runtime based on configuration. This is useful when
|
|||||||
```javascript
|
```javascript
|
||||||
module.exports = {
|
module.exports = {
|
||||||
node: MyNode,
|
node: MyNode,
|
||||||
|
|
||||||
setup: function (context, graphModel) {
|
setup: function (context, graphModel) {
|
||||||
// Only run in editor, not deployed
|
// Only run in editor, not deployed
|
||||||
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
if (!context.editorConnection || !context.editorConnection.isRunningLocally()) {
|
||||||
@@ -248,7 +248,7 @@ module.exports = {
|
|||||||
|
|
||||||
function updatePorts(nodeId, parameters, editorConnection) {
|
function updatePorts(nodeId, parameters, editorConnection) {
|
||||||
const ports = [];
|
const ports = [];
|
||||||
|
|
||||||
// Always include base ports from node definition
|
// Always include base ports from node definition
|
||||||
// Add dynamic ports based on parameters
|
// Add dynamic ports based on parameters
|
||||||
if (parameters.items) {
|
if (parameters.items) {
|
||||||
@@ -262,7 +262,7 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send ports to editor
|
// Send ports to editor
|
||||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||||
}
|
}
|
||||||
@@ -348,7 +348,7 @@ For asynchronous operations (API calls, file I/O), use standard async patterns:
|
|||||||
methods: {
|
methods: {
|
||||||
fetchTrigger: function () {
|
fetchTrigger: function () {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
fetch(this._internal.url)
|
fetch(this._internal.url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -394,6 +394,348 @@ getInspectInfo() {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ⚠️ CRITICAL GOTCHAS - READ BEFORE CREATING NODES
|
||||||
|
|
||||||
|
These issues will cause silent failures with NO error messages. They were discovered during the HTTP node debugging session (December 2024) and cost hours of debugging time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔴 GOTCHA #1: Never Override `setInputValue` in prototypeExtensions
|
||||||
|
|
||||||
|
**THE BUG:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ DEADLY - This silently breaks ALL signal inputs
|
||||||
|
prototypeExtensions: {
|
||||||
|
setInputValue: function (name, value) {
|
||||||
|
this._internal.inputValues[name] = value; // Signal setters NEVER called!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**WHY IT BREAKS:**
|
||||||
|
|
||||||
|
- `prototypeExtensions.setInputValue` **completely overrides** `Node.prototype.setInputValue`
|
||||||
|
- The base method contains `input.set.call(this, value)` which triggers signal callbacks
|
||||||
|
- Without it, signals never fire - NO errors, just silent failure
|
||||||
|
|
||||||
|
**THE FIX:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ✅ SAFE - Use a different method name for storing dynamic values
|
||||||
|
prototypeExtensions: {
|
||||||
|
_storeInputValue: function (name, value) {
|
||||||
|
this._internal.inputValues[name] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
registerInputIfNeeded: function (name) {
|
||||||
|
// Register dynamic inputs with _storeInputValue, not setInputValue
|
||||||
|
if (name.startsWith('body-')) {
|
||||||
|
return this.registerInput(name, {
|
||||||
|
set: this._storeInputValue.bind(this, name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔴 GOTCHA #2: Dynamic Ports REPLACE Static Ports
|
||||||
|
|
||||||
|
**THE BUG:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Node has static inputs defined in inputs: {}
|
||||||
|
inputs: {
|
||||||
|
url: { type: 'string', set: function(v) {...} },
|
||||||
|
fetch: { type: 'signal', valueChangedToTrue: function() {...} }
|
||||||
|
},
|
||||||
|
|
||||||
|
// But setup function only sends dynamic ports
|
||||||
|
function updatePorts(nodeId, parameters, editorConnection) {
|
||||||
|
const ports = [
|
||||||
|
{ name: 'headers', ... },
|
||||||
|
{ name: 'queryParams', ... }
|
||||||
|
];
|
||||||
|
// ❌ MISSING url, fetch - they won't appear in editor!
|
||||||
|
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**WHY IT BREAKS:**
|
||||||
|
|
||||||
|
- `sendDynamicPorts()` tells the editor "these are ALL the ports for this node"
|
||||||
|
- Static `inputs` are NOT automatically merged
|
||||||
|
- The editor only shows dynamic ports, connections to static ports fail
|
||||||
|
|
||||||
|
**THE FIX:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ✅ SAFE - Include ALL ports in dynamic ports array
|
||||||
|
function updatePorts(nodeId, parameters, editorConnection) {
|
||||||
|
const ports = [];
|
||||||
|
|
||||||
|
// Dynamic configuration ports
|
||||||
|
ports.push({ name: 'headers', type: {...}, plug: 'input' });
|
||||||
|
ports.push({ name: 'queryParams', type: {...}, plug: 'input' });
|
||||||
|
|
||||||
|
// MUST include static ports too!
|
||||||
|
ports.push({ name: 'url', type: 'string', plug: 'input', group: 'Request' });
|
||||||
|
ports.push({ name: 'fetch', type: 'signal', plug: 'input', group: 'Actions' });
|
||||||
|
ports.push({ name: 'cancel', type: 'signal', plug: 'input', group: 'Actions' });
|
||||||
|
|
||||||
|
// Include outputs too
|
||||||
|
ports.push({ name: 'success', type: 'signal', plug: 'output', group: 'Events' });
|
||||||
|
|
||||||
|
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔴 GOTCHA #3: Configuration Inputs Need Explicit Registration
|
||||||
|
|
||||||
|
**THE BUG:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Dynamic ports send method, bodyType, etc. to editor
|
||||||
|
ports.push({ name: 'method', type: { name: 'enum', ... } });
|
||||||
|
ports.push({ name: 'bodyType', type: { name: 'enum', ... } });
|
||||||
|
|
||||||
|
// ❌ Values never reach runtime - no setter registered!
|
||||||
|
// User selects POST in editor, but doFetch() always uses GET
|
||||||
|
doFetch: function() {
|
||||||
|
const method = this._internal.method || 'GET'; // Always undefined!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**WHY IT BREAKS:**
|
||||||
|
|
||||||
|
- Dynamic port values are sent to runtime as input values via `setInputValue`
|
||||||
|
- But `registerInputIfNeeded` is only called for ports not in static `inputs`
|
||||||
|
- If there's no setter, the value is lost
|
||||||
|
|
||||||
|
**THE FIX:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ✅ SAFE - Register setters for all config inputs
|
||||||
|
prototypeExtensions: {
|
||||||
|
// Setter methods
|
||||||
|
setMethod: function (value) { this._internal.method = value || 'GET'; },
|
||||||
|
setBodyType: function (value) { this._internal.bodyType = value; },
|
||||||
|
setHeaders: function (value) { this._internal.headers = value || ''; },
|
||||||
|
|
||||||
|
registerInputIfNeeded: function (name) {
|
||||||
|
if (this.hasInput(name)) return;
|
||||||
|
|
||||||
|
// Configuration inputs - bind to their setters
|
||||||
|
const configSetters = {
|
||||||
|
method: this.setMethod.bind(this),
|
||||||
|
bodyType: this.setBodyType.bind(this),
|
||||||
|
headers: this.setHeaders.bind(this),
|
||||||
|
timeout: this.setTimeout.bind(this)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (configSetters[name]) {
|
||||||
|
return this.registerInput(name, { set: configSetters[name] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic inputs for prefixed ports
|
||||||
|
if (name.startsWith('body-') || name.startsWith('header-')) {
|
||||||
|
return this.registerInput(name, {
|
||||||
|
set: this._storeInputValue.bind(this, name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔴 GOTCHA #4: Signal Inputs Use `valueChangedToTrue`, Not `set`
|
||||||
|
|
||||||
|
**THE BUG:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ WRONG - This won't trigger on signal
|
||||||
|
inputs: {
|
||||||
|
fetch: {
|
||||||
|
type: 'signal',
|
||||||
|
set: function() {
|
||||||
|
this.doFetch(); // Never called!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**WHY IT BREAKS:**
|
||||||
|
|
||||||
|
- Signal inputs don't use `set` - they use `valueChangedToTrue`
|
||||||
|
- The runtime wraps signals with `EdgeTriggeredInput.createSetter()` which tracks state transitions
|
||||||
|
- Signals only fire on FALSE → TRUE transition
|
||||||
|
|
||||||
|
**THE FIX:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ✅ CORRECT - Use valueChangedToTrue for signals
|
||||||
|
inputs: {
|
||||||
|
fetch: {
|
||||||
|
type: 'signal',
|
||||||
|
displayName: 'Fetch',
|
||||||
|
group: 'Actions',
|
||||||
|
valueChangedToTrue: function () {
|
||||||
|
this.scheduleFetch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
type: 'signal',
|
||||||
|
displayName: 'Cancel',
|
||||||
|
group: 'Actions',
|
||||||
|
valueChangedToTrue: function () {
|
||||||
|
this.cancelFetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔴 GOTCHA #5: Node Registration Path Matters
|
||||||
|
|
||||||
|
**THE BUG:**
|
||||||
|
|
||||||
|
- Nodes in `noodl-runtime/noodl-runtime.js` → Go through `defineNode()`
|
||||||
|
- Nodes in `noodl-viewer-react/register-nodes.js` → Go through `defineNode()`
|
||||||
|
- Raw node object passed directly → Does NOT go through `defineNode()`
|
||||||
|
|
||||||
|
**WHY IT MATTERS:**
|
||||||
|
|
||||||
|
- `defineNode()` in `nodedefinition.js` wraps signal inputs with `EdgeTriggeredInput.createSetter()`
|
||||||
|
- Without `defineNode()`, signals are registered but never fire
|
||||||
|
- The `{node, setup}` export format automatically calls `defineNode()`
|
||||||
|
|
||||||
|
**THE FIX:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ✅ CORRECT - Always export with {node, setup} format
|
||||||
|
module.exports = {
|
||||||
|
node: MyNode, // Goes through defineNode()
|
||||||
|
setup: function (context, graphModel) {
|
||||||
|
// Dynamic port setup
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ ALSO CORRECT - Call defineNode explicitly
|
||||||
|
const NodeDefinition = require('./nodedefinition');
|
||||||
|
module.exports = NodeDefinition.defineNode(MyNode);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Working Pattern (HTTP Node Reference)
|
||||||
|
|
||||||
|
Here's the proven pattern from the HTTP node that handles all gotchas:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var MyNode = {
|
||||||
|
name: 'net.noodl.MyNode',
|
||||||
|
displayNodeName: 'My Node',
|
||||||
|
category: 'Data',
|
||||||
|
color: 'data',
|
||||||
|
|
||||||
|
initialize: function () {
|
||||||
|
this._internal.inputValues = {}; // For dynamic input storage
|
||||||
|
this._internal.method = 'GET'; // Config defaults
|
||||||
|
},
|
||||||
|
|
||||||
|
// Static inputs - signals and essential ports
|
||||||
|
inputs: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
set: function (value) { this._internal.url = value; }
|
||||||
|
},
|
||||||
|
fetch: {
|
||||||
|
type: 'signal',
|
||||||
|
valueChangedToTrue: function () { this.scheduleFetch(); }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
outputs: {
|
||||||
|
response: { type: '*', getter: function() { return this._internal.response; } },
|
||||||
|
success: { type: 'signal' },
|
||||||
|
failure: { type: 'signal' }
|
||||||
|
},
|
||||||
|
|
||||||
|
prototypeExtensions: {
|
||||||
|
// Store dynamic values WITHOUT overriding setInputValue
|
||||||
|
_storeInputValue: function (name, value) {
|
||||||
|
this._internal.inputValues[name] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Configuration setters
|
||||||
|
setMethod: function (value) { this._internal.method = value || 'GET'; },
|
||||||
|
setHeaders: function (value) { this._internal.headers = value || ''; },
|
||||||
|
|
||||||
|
// Register ALL dynamic inputs
|
||||||
|
registerInputIfNeeded: function (name) {
|
||||||
|
if (this.hasInput(name)) return;
|
||||||
|
|
||||||
|
// Config inputs
|
||||||
|
const configSetters = {
|
||||||
|
method: this.setMethod.bind(this),
|
||||||
|
headers: this.setHeaders.bind(this)
|
||||||
|
};
|
||||||
|
if (configSetters[name]) {
|
||||||
|
return this.registerInput(name, { set: configSetters[name] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefixed dynamic inputs
|
||||||
|
if (name.startsWith('header-') || name.startsWith('body-')) {
|
||||||
|
return this.registerInput(name, {
|
||||||
|
set: this._storeInputValue.bind(this, name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
scheduleFetch: function () {
|
||||||
|
this.scheduleAfterInputsHaveUpdated(this.doFetch.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
doFetch: function () {
|
||||||
|
const method = this._internal.method; // Now correctly captured!
|
||||||
|
// ... fetch implementation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
node: MyNode,
|
||||||
|
setup: function (context, graphModel) {
|
||||||
|
function updatePorts(nodeId, parameters, editorConnection) {
|
||||||
|
const ports = [];
|
||||||
|
|
||||||
|
// Config ports
|
||||||
|
ports.push({ name: 'method', type: { name: 'enum', enums: [...] }, plug: 'input' });
|
||||||
|
ports.push({ name: 'headers', type: { name: 'stringlist' }, plug: 'input' });
|
||||||
|
|
||||||
|
// MUST include static ports!
|
||||||
|
ports.push({ name: 'url', type: 'string', plug: 'input' });
|
||||||
|
ports.push({ name: 'fetch', type: 'signal', plug: 'input' });
|
||||||
|
|
||||||
|
// Outputs
|
||||||
|
ports.push({ name: 'response', type: '*', plug: 'output' });
|
||||||
|
ports.push({ name: 'success', type: 'signal', plug: 'output' });
|
||||||
|
|
||||||
|
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||||
|
}
|
||||||
|
// ... rest of setup
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Common Issues
|
## Common Issues
|
||||||
|
|
||||||
### Node Not Appearing in Node Picker
|
### Node Not Appearing in Node Picker
|
||||||
@@ -416,9 +758,13 @@ getInspectInfo() {
|
|||||||
|
|
||||||
### Signal Not Firing
|
### Signal Not Firing
|
||||||
|
|
||||||
**Cause:** Method name doesn't match pattern `inputName + 'Trigger'`.
|
**Cause #1:** Method name pattern wrong - use `valueChangedToTrue`, not `inputName + 'Trigger'`.
|
||||||
|
|
||||||
**Fix:** Ensure signal handler method is named correctly (e.g., `fetchTrigger` for input `fetch`).
|
**Cause #2:** Custom `setInputValue` overriding base - see GOTCHA #1.
|
||||||
|
|
||||||
|
**Cause #3:** Signal not in dynamic ports - see GOTCHA #2.
|
||||||
|
|
||||||
|
**Fix:** Review ALL gotchas above!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -437,14 +783,14 @@ getInspectInfo() {
|
|||||||
|
|
||||||
When creating new nodes, reference these existing nodes for patterns:
|
When creating new nodes, reference these existing nodes for patterns:
|
||||||
|
|
||||||
| Node | File | Good Example Of |
|
| Node | File | Good Example Of |
|
||||||
|------|------|-----------------|
|
| --------- | --------------------- | ------------------------------------ |
|
||||||
| REST | `data/restnode.js` | Full-featured data node with scripts |
|
| REST | `data/restnode.js` | Full-featured data node with scripts |
|
||||||
| HTTP | `data/httpnode.js` | Dynamic ports, configuration |
|
| HTTP | `data/httpnode.js` | Dynamic ports, configuration |
|
||||||
| String | `variables/string.js` | Simple variable node |
|
| String | `variables/string.js` | Simple variable node |
|
||||||
| Counter | `counter.js` | Stateful logic node |
|
| Counter | `counter.js` | Stateful logic node |
|
||||||
| Condition | `condition.js` | Boolean logic |
|
| Condition | `condition.js` | Boolean logic |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Last Updated: December 2024*
|
_Last Updated: December 2024_
|
||||||
|
|||||||
@@ -852,6 +852,145 @@ const handleTreeMouseUp = useCallback(() => {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🔥 CRITICAL: Runtime Node Creation - The Unholy Trinity of Silent Failures (Dec 2025)
|
||||||
|
|
||||||
|
### The HTTP Node Debugging Saga: 3+ Hours of Silent Failures
|
||||||
|
|
||||||
|
**Context**: Creating an HTTP node (httpnode.js) as a modern, no-code-friendly alternative to the script-based REST node. Everything looked correct but nothing worked. Signals never fired, config values never set, and no errors anywhere.
|
||||||
|
|
||||||
|
**The Problems Found** (each took significant debugging time):
|
||||||
|
|
||||||
|
#### Problem 1: Signal Input Using `set` Instead of `valueChangedToTrue`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ WHAT I WROTE (never triggers)
|
||||||
|
inputs: {
|
||||||
|
fetch: {
|
||||||
|
type: 'signal',
|
||||||
|
set: function(value) {
|
||||||
|
this.scheduleFetch(); // ☠️ Never called
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ WHAT IT SHOULD BE
|
||||||
|
inputs: {
|
||||||
|
fetch: {
|
||||||
|
type: 'signal',
|
||||||
|
valueChangedToTrue: function() {
|
||||||
|
this.scheduleFetch(); // ✓ Works!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Signals use `EdgeTriggeredInput.createSetter()` which wraps the callback and only calls `valueChangedToTrue` when value transitions from falsy to truthy. The `set` callback is never used.
|
||||||
|
|
||||||
|
#### Problem 2: Custom `setInputValue` Overriding Base Method
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ WHAT I WROTE (breaks ALL inputs including signals)
|
||||||
|
prototypeExtensions: {
|
||||||
|
setInputValue: function(name, value) {
|
||||||
|
this._internal.inputValues[name] = value; // ☠️ Overrides Node.prototype.setInputValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ WHAT IT SHOULD BE
|
||||||
|
prototypeExtensions: {
|
||||||
|
_storeInputValue: function(name, value) { // Different name!
|
||||||
|
this._internal.inputValues[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: `prototypeExtensions` methods are merged into node prototype. Defining `setInputValue` completely replaces the base implementation, which handles signal detection, input.set() calls, and event emission.
|
||||||
|
|
||||||
|
#### Problem 3: Dynamic Ports Replacing Static Ports
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ WHAT I WROTE (static ports disappear)
|
||||||
|
function updatePorts(nodeId, parameters, editorConnection) {
|
||||||
|
const ports = [];
|
||||||
|
// Only add dynamic header/query param ports...
|
||||||
|
if (parameters.headers) { /* add header ports */ }
|
||||||
|
editorConnection.sendDynamicPorts(nodeId, ports); // Static inputs GONE!
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ WHAT IT SHOULD BE
|
||||||
|
function updatePorts(nodeId, parameters, editorConnection) {
|
||||||
|
const ports = [
|
||||||
|
// Re-add ALL static inputs
|
||||||
|
{ name: 'url', displayName: 'URL', type: 'string', plug: 'input', group: 'Request' },
|
||||||
|
{ name: 'fetch', displayName: 'Fetch', type: 'signal', plug: 'input', group: 'Actions' },
|
||||||
|
{ name: 'cancel', displayName: 'Cancel', type: 'signal', plug: 'input', group: 'Actions' },
|
||||||
|
// Then dynamic ports...
|
||||||
|
];
|
||||||
|
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: `sendDynamicPorts` REPLACES all ports, not merges. The `inputs` object in node definition is only for default setup - once dynamic ports are sent, they're the only ports.
|
||||||
|
|
||||||
|
#### Problem 4: Config Inputs (StringList/Enum) Need Explicit Registration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ MISSING (config values never reach setters)
|
||||||
|
// StringList inputs like "headers", "queryParams" appear in editor but their
|
||||||
|
// values never reach the node because there's no registered input handler
|
||||||
|
|
||||||
|
// ✅ WHAT IT NEEDS
|
||||||
|
registerInputIfNeeded: function(name) {
|
||||||
|
if (this.hasInput(name)) return;
|
||||||
|
|
||||||
|
const configSetters = {
|
||||||
|
'method': this.setMethod.bind(this),
|
||||||
|
'headers': this.setHeaders.bind(this),
|
||||||
|
'queryParams': this.setQueryParams.bind(this),
|
||||||
|
'bodyType': this.setBodyType.bind(this),
|
||||||
|
// ... all config inputs
|
||||||
|
};
|
||||||
|
|
||||||
|
if (configSetters[name]) {
|
||||||
|
return this.registerInput(name, { set: configSetters[name] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic inputs (header-X, query-Y, etc.)
|
||||||
|
if (name.startsWith('header-')) {
|
||||||
|
return this.registerInput(name, {
|
||||||
|
set: this._storeInputValue.bind(this, name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Inputs defined in the `inputs` object get registered automatically. Dynamic ports don't - they need `registerInputIfNeeded` to create runtime handlers.
|
||||||
|
|
||||||
|
**Why This Was So Hard to Debug**:
|
||||||
|
|
||||||
|
1. **No errors** - Everything appeared to work, logs said "success", but nothing happened
|
||||||
|
2. **Partial success** - Some things worked (node appeared in palette) making it seem close
|
||||||
|
3. **Multiple bugs** - Each fix revealed the next bug, each taking time to diagnose
|
||||||
|
4. **No TypeScript** - Runtime code is JS, no compile-time checking
|
||||||
|
5. **Unfamiliar patterns** - `valueChangedToTrue`, `registerInputIfNeeded`, etc. aren't obvious
|
||||||
|
|
||||||
|
**Time Lost**: 3+ hours debugging what should have been a straightforward node
|
||||||
|
|
||||||
|
**Prevention**:
|
||||||
|
|
||||||
|
- Created `dev-docs/reference/LEARNINGS-NODE-CREATION.md` with Critical Gotchas section
|
||||||
|
- Added Node Creation Checklist to `.clinerules` Section 14
|
||||||
|
- These gotchas are now documented with THE BUG / WHY IT BREAKS / THE FIX patterns
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
|
||||||
|
- `packages/noodl-runtime/src/nodes/std-library/data/httpnode.js` (fixed)
|
||||||
|
- `dev-docs/reference/LEARNINGS-NODE-CREATION.md` (new documentation)
|
||||||
|
|
||||||
|
**Keywords**: node creation, signal, valueChangedToTrue, setInputValue, prototypeExtensions, sendDynamicPorts, registerInputIfNeeded, stringlist, enum, config inputs, HTTP node, runtime
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🔥 CRITICAL: PopupLayer.dragCompleted() - Not endDrag() (Dec 2025)
|
## 🔥 CRITICAL: PopupLayer.dragCompleted() - Not endDrag() (Dec 2025)
|
||||||
|
|
||||||
### Wrong Method Name Causes TypeError
|
### Wrong Method Name Causes TypeError
|
||||||
|
|||||||
@@ -116,10 +116,14 @@ export function ComponentItem({
|
|||||||
// If this item is a valid drop target, execute the drop
|
// If this item is a valid drop target, execute the drop
|
||||||
if (isDropTarget && onDrop) {
|
if (isDropTarget && onDrop) {
|
||||||
e.stopPropagation(); // Prevent bubble to Tree (for root drop fallback)
|
e.stopPropagation(); // Prevent bubble to Tree (for root drop fallback)
|
||||||
|
|
||||||
|
// End drag IMMEDIATELY at event handler level (before action chain)
|
||||||
|
// This matches the working root drop pattern
|
||||||
|
PopupLayer.instance.dragCompleted();
|
||||||
|
|
||||||
const node: TreeNode = { type: 'component', data: component };
|
const node: TreeNode = { type: 'component', data: component };
|
||||||
onDrop(node);
|
onDrop(node);
|
||||||
setIsDropTarget(false);
|
setIsDropTarget(false);
|
||||||
// Note: dragCompleted() is called by handleDropOn - don't call it here
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isDropTarget, component, onDrop]
|
[isDropTarget, component, onDrop]
|
||||||
|
|||||||
@@ -111,10 +111,14 @@ export function FolderItem({
|
|||||||
// If this folder is a valid drop target, execute the drop
|
// If this folder is a valid drop target, execute the drop
|
||||||
if (isDropTarget && onDrop) {
|
if (isDropTarget && onDrop) {
|
||||||
e.stopPropagation(); // Prevent bubble to Tree (for root drop fallback)
|
e.stopPropagation(); // Prevent bubble to Tree (for root drop fallback)
|
||||||
|
|
||||||
|
// End drag IMMEDIATELY at event handler level (before action chain)
|
||||||
|
// This matches the working root drop pattern
|
||||||
|
PopupLayer.instance.dragCompleted();
|
||||||
|
|
||||||
const node: TreeNode = { type: 'folder', data: folder };
|
const node: TreeNode = { type: 'folder', data: folder };
|
||||||
onDrop(node);
|
onDrop(node);
|
||||||
setIsDropTarget(false);
|
setIsDropTarget(false);
|
||||||
// Note: dragCompleted() is called by handleDropOn - don't call it here
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isDropTarget, folder, onDrop]
|
[isDropTarget, folder, onDrop]
|
||||||
|
|||||||
@@ -523,15 +523,12 @@ export function useComponentActions() {
|
|||||||
// Check for naming conflicts
|
// Check for naming conflicts
|
||||||
if (ProjectModel.instance?.getComponentWithName(newName)) {
|
if (ProjectModel.instance?.getComponentWithName(newName)) {
|
||||||
alert(`Component "${newName}" already exists in that folder`);
|
alert(`Component "${newName}" already exists in that folder`);
|
||||||
PopupLayer.instance.dragCompleted();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldName = component.name;
|
const oldName = component.name;
|
||||||
|
|
||||||
// End drag operation FIRST - before the rename triggers a re-render
|
// Note: dragCompleted() now called by ComponentItem.handleMouseUp before this action
|
||||||
// This prevents the drag state from persisting across the tree rebuild
|
|
||||||
PopupLayer.instance.dragCompleted();
|
|
||||||
|
|
||||||
UndoQueue.instance.pushAndDo(
|
UndoQueue.instance.pushAndDo(
|
||||||
new UndoActionGroup({
|
new UndoActionGroup({
|
||||||
@@ -554,7 +551,6 @@ export function useComponentActions() {
|
|||||||
// Prevent moving folder into itself
|
// Prevent moving folder into itself
|
||||||
if (targetPath.startsWith(sourcePath + '/') || targetPath === sourcePath) {
|
if (targetPath.startsWith(sourcePath + '/') || targetPath === sourcePath) {
|
||||||
alert('Cannot move folder into itself');
|
alert('Cannot move folder into itself');
|
||||||
PopupLayer.instance.dragCompleted();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,7 +561,6 @@ export function useComponentActions() {
|
|||||||
|
|
||||||
if (!componentsToMove || componentsToMove.length === 0) {
|
if (!componentsToMove || componentsToMove.length === 0) {
|
||||||
console.log('Folder is empty, nothing to move');
|
console.log('Folder is empty, nothing to move');
|
||||||
PopupLayer.instance.dragCompleted();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,8 +579,7 @@ export function useComponentActions() {
|
|||||||
renames.push({ component: comp, oldName: comp.name, newName });
|
renames.push({ component: comp, oldName: comp.name, newName });
|
||||||
});
|
});
|
||||||
|
|
||||||
// End drag operation FIRST - before the rename triggers a re-render
|
// Note: dragCompleted() now called by FolderItem.handleMouseUp before this action
|
||||||
PopupLayer.instance.dragCompleted();
|
|
||||||
|
|
||||||
UndoQueue.instance.pushAndDo(
|
UndoQueue.instance.pushAndDo(
|
||||||
new UndoActionGroup({
|
new UndoActionGroup({
|
||||||
@@ -612,14 +606,12 @@ export function useComponentActions() {
|
|||||||
// Check for naming conflicts
|
// Check for naming conflicts
|
||||||
if (ProjectModel.instance?.getComponentWithName(newName)) {
|
if (ProjectModel.instance?.getComponentWithName(newName)) {
|
||||||
alert(`Component "${newName}" already exists`);
|
alert(`Component "${newName}" already exists`);
|
||||||
PopupLayer.instance.dragCompleted();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldName = component.name;
|
const oldName = component.name;
|
||||||
|
|
||||||
// End drag operation FIRST - before the rename triggers a re-render
|
// Note: dragCompleted() now called by ComponentItem.handleMouseUp before this action
|
||||||
PopupLayer.instance.dragCompleted();
|
|
||||||
|
|
||||||
UndoQueue.instance.pushAndDo(
|
UndoQueue.instance.pushAndDo(
|
||||||
new UndoActionGroup({
|
new UndoActionGroup({
|
||||||
@@ -646,7 +638,6 @@ export function useComponentActions() {
|
|||||||
|
|
||||||
if (!componentsToMove || componentsToMove.length === 0) {
|
if (!componentsToMove || componentsToMove.length === 0) {
|
||||||
console.log('Folder is empty, nothing to move');
|
console.log('Folder is empty, nothing to move');
|
||||||
PopupLayer.instance.dragCompleted();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,12 +661,10 @@ export function useComponentActions() {
|
|||||||
|
|
||||||
if (hasConflict) {
|
if (hasConflict) {
|
||||||
alert(`Some components would conflict with existing names`);
|
alert(`Some components would conflict with existing names`);
|
||||||
PopupLayer.instance.dragCompleted();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// End drag operation FIRST - before the rename triggers a re-render
|
// Note: dragCompleted() now called by ComponentItem.handleMouseUp before this action
|
||||||
PopupLayer.instance.dragCompleted();
|
|
||||||
|
|
||||||
UndoQueue.instance.pushAndDo(
|
UndoQueue.instance.pushAndDo(
|
||||||
new UndoActionGroup({
|
new UndoActionGroup({
|
||||||
|
|||||||
@@ -3,11 +3,26 @@ import StringListTemplate from '../../../../../templates/propertyeditor/stringli
|
|||||||
import PopupLayer from '../../../../popuplayer';
|
import PopupLayer from '../../../../popuplayer';
|
||||||
import { ToastLayer } from '../../../../ToastLayer/ToastLayer';
|
import { ToastLayer } from '../../../../ToastLayer/ToastLayer';
|
||||||
|
|
||||||
|
// Helper to normalize list input - handles both string and array formats
|
||||||
|
function normalizeList(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// Already an array (legacy proplist data) - extract labels if objects
|
||||||
|
return value.map((item) => (typeof item === 'object' && item.label ? item.label : String(item)));
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.split(',').filter(Boolean);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
var StringList = function (args) {
|
var StringList = function (args) {
|
||||||
View.call(this);
|
View.call(this);
|
||||||
|
|
||||||
this.parent = args.parent;
|
this.parent = args.parent;
|
||||||
this.list = args.list !== undefined && args.list !== '' ? args.list.split(',') : [];
|
this.list = normalizeList(args.list);
|
||||||
this.type = args.type;
|
this.type = args.type;
|
||||||
this.plug = args.plug;
|
this.plug = args.plug;
|
||||||
this.title = args.title;
|
this.title = args.title;
|
||||||
@@ -32,7 +47,7 @@ StringList.prototype.listUpdated = function () {
|
|||||||
if (this.onUpdate) {
|
if (this.onUpdate) {
|
||||||
var updated = this.onUpdate(this.list.join(','));
|
var updated = this.onUpdate(this.list.join(','));
|
||||||
if (updated) {
|
if (updated) {
|
||||||
this.list = updated.value ? updated.value.split(',') : [];
|
this.list = normalizeList(updated.value);
|
||||||
this.isDefault = updated.isDefault;
|
this.isDefault = updated.isDefault;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function registerNodes(noodlRuntime) {
|
|||||||
|
|
||||||
// Data
|
// Data
|
||||||
require('./src/nodes/std-library/data/restnode'),
|
require('./src/nodes/std-library/data/restnode'),
|
||||||
require('./src/nodes/std-library/data/httpnode'),
|
// require('./src/nodes/std-library/data/httpnode'), // moved to viewer for debugging
|
||||||
|
|
||||||
// Custom code
|
// Custom code
|
||||||
require('./src/nodes/std-library/expression'),
|
require('./src/nodes/std-library/expression'),
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
function createSetter(args) {
|
function createSetter(args) {
|
||||||
|
console.log('[EdgeTriggeredInput] 🔧 createSetter called for signal input');
|
||||||
|
|
||||||
var currentValue = false;
|
var currentValue = false;
|
||||||
|
|
||||||
return function(value) {
|
return function (value) {
|
||||||
value = value ? true : false;
|
console.log('[EdgeTriggeredInput] ⚡ setter called with value:', value, 'currentValue:', currentValue);
|
||||||
//value changed from false to true
|
value = value ? true : false;
|
||||||
if(value && currentValue === false) {
|
//value changed from false to true
|
||||||
args.valueChangedToTrue.call(this);
|
if (value && currentValue === false) {
|
||||||
}
|
console.log('[EdgeTriggeredInput] ✅ Triggering valueChangedToTrue!');
|
||||||
currentValue = value;
|
args.valueChangedToTrue.call(this);
|
||||||
};
|
}
|
||||||
|
currentValue = value;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createSetter: createSetter
|
createSetter: createSetter
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,12 +84,22 @@ Node.prototype.registerInputIfNeeded = function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Node.prototype.setInputValue = function (name, value) {
|
Node.prototype.setInputValue = function (name, value) {
|
||||||
|
// DEBUG: Track input value setting for HTTP node
|
||||||
|
if (this.name === 'net.noodl.HTTP') {
|
||||||
|
console.log('[Node.setInputValue] 🎯 HTTP node setting input:', name, 'value:', value);
|
||||||
|
}
|
||||||
|
|
||||||
const input = this.getInput(name);
|
const input = this.getInput(name);
|
||||||
if (!input) {
|
if (!input) {
|
||||||
console.log("node doesn't have input", name);
|
console.log("node doesn't have input", name, 'for node:', this.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DEBUG: Track if setter exists
|
||||||
|
if (this.name === 'net.noodl.HTTP' && name === 'fetch') {
|
||||||
|
console.log('[Node.setInputValue] 🎯 HTTP fetch input found, calling setter');
|
||||||
|
}
|
||||||
|
|
||||||
//inputs with units always expect objects in the shape of {value, unit, ...}
|
//inputs with units always expect objects in the shape of {value, unit, ...}
|
||||||
//these inputs might sometimes get raw numbers without units, and in those cases
|
//these inputs might sometimes get raw numbers without units, and in those cases
|
||||||
//Noodl should just update the value and not the other parameters
|
//Noodl should just update the value and not the other parameters
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ function defineNode(opts) {
|
|||||||
Object.keys(opts.inputs).forEach(function (name) {
|
Object.keys(opts.inputs).forEach(function (name) {
|
||||||
var input = opts.inputs[name];
|
var input = opts.inputs[name];
|
||||||
if (input.valueChangedToTrue) {
|
if (input.valueChangedToTrue) {
|
||||||
|
console.log('[NodeDefinition] 📌 Registering signal input:', name, 'for node:', opts.name);
|
||||||
node._inputs[name] = {
|
node._inputs[name] = {
|
||||||
set: EdgeTriggeredInput.createSetter({
|
set: EdgeTriggeredInput.createSetter({
|
||||||
valueChangedToTrue: input.valueChangedToTrue
|
valueChangedToTrue: input.valueChangedToTrue
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
|
|
||||||
// Note: This file uses CommonJS module format to match the noodl-runtime pattern
|
// Note: This file uses CommonJS module format to match the noodl-runtime pattern
|
||||||
|
|
||||||
|
// DEBUG: Confirm module is loaded
|
||||||
|
console.log('[HTTP Node Module] 📦 httpnode.js MODULE LOADED');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract value from object using JSONPath-like syntax
|
* Extract value from object using JSONPath-like syntax
|
||||||
* Supports: $.data.users, $.items[0].name, $.meta.pagination.total
|
* Supports: $.data.users, $.items[0].name, $.meta.pagination.total
|
||||||
@@ -88,12 +91,13 @@ var HttpNode = {
|
|||||||
searchTags: ['http', 'request', 'fetch', 'api', 'rest', 'curl'],
|
searchTags: ['http', 'request', 'fetch', 'api', 'rest', 'curl'],
|
||||||
|
|
||||||
initialize: function () {
|
initialize: function () {
|
||||||
|
console.log('[HTTP Node] 🚀 INITIALIZE called - node instance created');
|
||||||
this._internal.inputValues = {};
|
this._internal.inputValues = {};
|
||||||
this._internal.outputValues = {};
|
this._internal.outputValues = {};
|
||||||
this._internal.headers = [];
|
this._internal.headers = '';
|
||||||
this._internal.queryParams = [];
|
this._internal.queryParams = '';
|
||||||
this._internal.bodyFields = [];
|
this._internal.bodyFields = '';
|
||||||
this._internal.responseMapping = [];
|
this._internal.responseMapping = '';
|
||||||
this._internal.inspectData = null;
|
this._internal.inspectData = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -105,6 +109,7 @@ var HttpNode = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
inputs: {
|
inputs: {
|
||||||
|
// Static inputs - these don't need to trigger port regeneration
|
||||||
url: {
|
url: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
displayName: 'URL',
|
displayName: 'URL',
|
||||||
@@ -114,40 +119,12 @@ var HttpNode = {
|
|||||||
this._internal.url = value;
|
this._internal.url = value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
method: {
|
|
||||||
type: {
|
|
||||||
name: 'enum',
|
|
||||||
enums: [
|
|
||||||
{ label: 'GET', value: 'GET' },
|
|
||||||
{ label: 'POST', value: 'POST' },
|
|
||||||
{ label: 'PUT', value: 'PUT' },
|
|
||||||
{ label: 'PATCH', value: 'PATCH' },
|
|
||||||
{ label: 'DELETE', value: 'DELETE' },
|
|
||||||
{ label: 'HEAD', value: 'HEAD' },
|
|
||||||
{ label: 'OPTIONS', value: 'OPTIONS' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
displayName: 'Method',
|
|
||||||
group: 'Request',
|
|
||||||
default: 'GET',
|
|
||||||
set: function (value) {
|
|
||||||
this._internal.method = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
timeout: {
|
|
||||||
type: 'number',
|
|
||||||
displayName: 'Timeout (ms)',
|
|
||||||
group: 'Request',
|
|
||||||
default: 30000,
|
|
||||||
set: function (value) {
|
|
||||||
this._internal.timeout = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetch: {
|
fetch: {
|
||||||
type: 'signal',
|
type: 'signal',
|
||||||
displayName: 'Fetch',
|
displayName: 'Fetch',
|
||||||
group: 'Actions',
|
group: 'Actions',
|
||||||
valueChangedToTrue: function () {
|
valueChangedToTrue: function () {
|
||||||
|
console.log('[HTTP Node] ⚡ FETCH SIGNAL RECEIVED - valueChangedToTrue triggered!');
|
||||||
this.scheduleFetch();
|
this.scheduleFetch();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -159,6 +136,7 @@ var HttpNode = {
|
|||||||
this.cancelFetch();
|
this.cancelFetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Note: method, timeout, and config ports are now dynamic (in updatePorts)
|
||||||
},
|
},
|
||||||
|
|
||||||
outputs: {
|
outputs: {
|
||||||
@@ -212,7 +190,9 @@ var HttpNode = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
prototypeExtensions: {
|
prototypeExtensions: {
|
||||||
setInputValue: function (name, value) {
|
// Store values for dynamic inputs only - static inputs (including signals)
|
||||||
|
// use the base Node.prototype.setInputValue which calls input.set()
|
||||||
|
_storeInputValue: function (name, value) {
|
||||||
this._internal.inputValues[name] = value;
|
this._internal.inputValues[name] = value;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -233,25 +213,45 @@ var HttpNode = {
|
|||||||
registerInputIfNeeded: function (name) {
|
registerInputIfNeeded: function (name) {
|
||||||
if (this.hasInput(name)) return;
|
if (this.hasInput(name)) return;
|
||||||
|
|
||||||
// Dynamic inputs for path params, headers, query params, body fields, auth
|
// Configuration inputs - these set internal state
|
||||||
const inputSetters = {
|
const configSetters = {
|
||||||
path: this.setInputValue.bind(this),
|
method: this.setMethod.bind(this),
|
||||||
header: this.setInputValue.bind(this),
|
timeout: this.setTimeout.bind(this),
|
||||||
query: this.setInputValue.bind(this),
|
headers: this.setHeaders.bind(this),
|
||||||
body: this.setInputValue.bind(this),
|
queryParams: this.setQueryParams.bind(this),
|
||||||
auth: this.setInputValue.bind(this)
|
bodyType: this.setBodyType.bind(this),
|
||||||
|
bodyFields: this.setBodyFields.bind(this),
|
||||||
|
authType: this.setAuthType.bind(this),
|
||||||
|
responseMapping: this.setResponseMapping.bind(this)
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [prefix, setter] of Object.entries(inputSetters)) {
|
if (configSetters[name]) {
|
||||||
if (name.startsWith(prefix + '-')) {
|
return this.registerInput(name, {
|
||||||
return this.registerInput(name, { set: setter.bind(this, name) });
|
set: configSetters[name]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic inputs for path params, headers, query params, body fields, auth, mapping paths
|
||||||
|
// These use _storeInputValue to just store values (no signal trigger needed)
|
||||||
|
const dynamicPrefixes = ['path-', 'header-', 'query-', 'body-', 'auth-', 'mapping-path-'];
|
||||||
|
|
||||||
|
for (const prefix of dynamicPrefixes) {
|
||||||
|
if (name.startsWith(prefix)) {
|
||||||
|
return this.registerInput(name, {
|
||||||
|
set: this._storeInputValue.bind(this, name)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
scheduleFetch: function () {
|
scheduleFetch: function () {
|
||||||
if (this._internal.hasScheduledFetch) return;
|
console.log('[HTTP Node] scheduleFetch called');
|
||||||
|
if (this._internal.hasScheduledFetch) {
|
||||||
|
console.log('[HTTP Node] Already scheduled, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._internal.hasScheduledFetch = true;
|
this._internal.hasScheduledFetch = true;
|
||||||
|
console.log('[HTTP Node] Scheduling doFetch after inputs update');
|
||||||
this.scheduleAfterInputsHaveUpdated(this.doFetch.bind(this));
|
this.scheduleAfterInputsHaveUpdated(this.doFetch.bind(this));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -278,12 +278,16 @@ var HttpNode = {
|
|||||||
// Add query parameters
|
// Add query parameters
|
||||||
const queryParams = {};
|
const queryParams = {};
|
||||||
|
|
||||||
// From visual config
|
// From visual config (stringlist format)
|
||||||
if (this._internal.queryParams) {
|
if (this._internal.queryParams) {
|
||||||
for (const qp of this._internal.queryParams) {
|
const queryList = this._internal.queryParams
|
||||||
const value = this._internal.inputValues['query-' + qp.key];
|
.split(',')
|
||||||
|
.map((q) => q.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
for (const qp of queryList) {
|
||||||
|
const value = this._internal.inputValues['query-' + qp];
|
||||||
if (value !== undefined && value !== null && value !== '') {
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
queryParams[qp.key] = value;
|
queryParams[qp] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,12 +316,16 @@ var HttpNode = {
|
|||||||
buildHeaders: function () {
|
buildHeaders: function () {
|
||||||
const headers = {};
|
const headers = {};
|
||||||
|
|
||||||
// From visual config
|
// From visual config (stringlist format)
|
||||||
if (this._internal.headers) {
|
if (this._internal.headers) {
|
||||||
for (const h of this._internal.headers) {
|
const headerList = this._internal.headers
|
||||||
const value = this._internal.inputValues['header-' + h.key];
|
.split(',')
|
||||||
|
.map((h) => h.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
for (const h of headerList) {
|
||||||
|
const value = this._internal.inputValues['header-' + h];
|
||||||
if (value !== undefined && value !== null) {
|
if (value !== undefined && value !== null) {
|
||||||
headers[h.key] = String(value);
|
headers[h] = String(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,32 +349,38 @@ var HttpNode = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bodyType = this._internal.bodyType || 'json';
|
const bodyType = this._internal.bodyType || 'json';
|
||||||
const bodyFields = this._internal.bodyFields || [];
|
const bodyFieldsStr = this._internal.bodyFields || '';
|
||||||
|
|
||||||
|
// Parse stringlist format
|
||||||
|
const bodyFields = bodyFieldsStr
|
||||||
|
.split(',')
|
||||||
|
.map((f) => f.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
if (bodyType === 'json') {
|
if (bodyType === 'json') {
|
||||||
const body = {};
|
const body = {};
|
||||||
for (const field of bodyFields) {
|
for (const field of bodyFields) {
|
||||||
const value = this._internal.inputValues['body-' + field.key];
|
const value = this._internal.inputValues['body-' + field];
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
body[field.key] = value;
|
body[field] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Object.keys(body).length > 0 ? JSON.stringify(body) : undefined;
|
return Object.keys(body).length > 0 ? JSON.stringify(body) : undefined;
|
||||||
} else if (bodyType === 'form') {
|
} else if (bodyType === 'form') {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
for (const field of bodyFields) {
|
for (const field of bodyFields) {
|
||||||
const value = this._internal.inputValues['body-' + field.key];
|
const value = this._internal.inputValues['body-' + field];
|
||||||
if (value !== undefined && value !== null) {
|
if (value !== undefined && value !== null) {
|
||||||
formData.append(field.key, value);
|
formData.append(field, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return formData;
|
return formData;
|
||||||
} else if (bodyType === 'urlencoded') {
|
} else if (bodyType === 'urlencoded') {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
for (const field of bodyFields) {
|
for (const field of bodyFields) {
|
||||||
const value = this._internal.inputValues['body-' + field.key];
|
const value = this._internal.inputValues['body-' + field];
|
||||||
if (value !== undefined && value !== null) {
|
if (value !== undefined && value !== null) {
|
||||||
params.append(field.key, String(value));
|
params.append(field, String(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return params.toString();
|
return params.toString();
|
||||||
@@ -390,10 +404,20 @@ var HttpNode = {
|
|||||||
this._internal.responseHeaders = responseHeaders;
|
this._internal.responseHeaders = responseHeaders;
|
||||||
|
|
||||||
// Process response mappings
|
// Process response mappings
|
||||||
const mappings = this._internal.responseMapping || [];
|
// Output names are in responseMapping (comma-separated)
|
||||||
for (const mapping of mappings) {
|
// Path for each output is in inputValues['mapping-path-{name}']
|
||||||
const outputName = 'out-' + mapping.name;
|
const mappingStr = this._internal.responseMapping || '';
|
||||||
const value = extractByPath(responseBody, mapping.path);
|
const outputNames = mappingStr
|
||||||
|
.split(',')
|
||||||
|
.map((m) => m.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
for (const name of outputNames) {
|
||||||
|
// Get the path from the corresponding input port
|
||||||
|
const path = this._internal.inputValues['mapping-path-' + name] || '$';
|
||||||
|
|
||||||
|
const outputName = 'out-' + name;
|
||||||
|
const value = extractByPath(responseBody, path);
|
||||||
|
|
||||||
this.registerOutputIfNeeded(outputName);
|
this.registerOutputIfNeeded(outputName);
|
||||||
this._internal.outputValues[outputName] = value;
|
this._internal.outputValues[outputName] = value;
|
||||||
@@ -415,6 +439,7 @@ var HttpNode = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
doFetch: function () {
|
doFetch: function () {
|
||||||
|
console.log('[HTTP Node] doFetch executing');
|
||||||
this._internal.hasScheduledFetch = false;
|
this._internal.hasScheduledFetch = false;
|
||||||
|
|
||||||
const url = this.buildUrl();
|
const url = this.buildUrl();
|
||||||
@@ -423,11 +448,20 @@ var HttpNode = {
|
|||||||
const body = this.buildBody();
|
const body = this.buildBody();
|
||||||
const timeout = this._internal.timeout || 30000;
|
const timeout = this._internal.timeout || 30000;
|
||||||
|
|
||||||
|
console.log('[HTTP Node] Request details:', {
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? '(has body)' : '(no body)',
|
||||||
|
timeout
|
||||||
|
});
|
||||||
|
|
||||||
// Store for inspect
|
// Store for inspect
|
||||||
this._internal.lastRequestUrl = url;
|
this._internal.lastRequestUrl = url;
|
||||||
|
|
||||||
// Validate URL
|
// Validate URL
|
||||||
if (!url) {
|
if (!url) {
|
||||||
|
console.log('[HTTP Node] No URL provided, sending failure');
|
||||||
this._internal.error = 'URL is required';
|
this._internal.error = 'URL is required';
|
||||||
this.flagOutputDirty('error');
|
this.flagOutputDirty('error');
|
||||||
this.sendSignalOnOutput('failure');
|
this.sendSignalOnOutput('failure');
|
||||||
@@ -503,11 +537,11 @@ var HttpNode = {
|
|||||||
|
|
||||||
// Configuration setters called from setup function
|
// Configuration setters called from setup function
|
||||||
setHeaders: function (value) {
|
setHeaders: function (value) {
|
||||||
this._internal.headers = value || [];
|
this._internal.headers = value || '';
|
||||||
},
|
},
|
||||||
|
|
||||||
setQueryParams: function (value) {
|
setQueryParams: function (value) {
|
||||||
this._internal.queryParams = value || [];
|
this._internal.queryParams = value || '';
|
||||||
},
|
},
|
||||||
|
|
||||||
setBodyType: function (value) {
|
setBodyType: function (value) {
|
||||||
@@ -515,15 +549,23 @@ var HttpNode = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setBodyFields: function (value) {
|
setBodyFields: function (value) {
|
||||||
this._internal.bodyFields = value || [];
|
this._internal.bodyFields = value || '';
|
||||||
},
|
},
|
||||||
|
|
||||||
setResponseMapping: function (value) {
|
setResponseMapping: function (value) {
|
||||||
this._internal.responseMapping = value || [];
|
this._internal.responseMapping = value || '';
|
||||||
},
|
},
|
||||||
|
|
||||||
setAuthType: function (value) {
|
setAuthType: function (value) {
|
||||||
this._internal.authType = value;
|
this._internal.authType = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
setMethod: function (value) {
|
||||||
|
this._internal.method = value || 'GET';
|
||||||
|
},
|
||||||
|
|
||||||
|
setTimeout: function (value) {
|
||||||
|
this._internal.timeout = value || 30000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -534,36 +576,6 @@ var HttpNode = {
|
|||||||
function updatePorts(nodeId, parameters, editorConnection) {
|
function updatePorts(nodeId, parameters, editorConnection) {
|
||||||
const ports = [];
|
const ports = [];
|
||||||
|
|
||||||
// URL input (already defined in static inputs, but we want it first)
|
|
||||||
ports.push({
|
|
||||||
name: 'url',
|
|
||||||
displayName: 'URL',
|
|
||||||
type: 'string',
|
|
||||||
plug: 'input',
|
|
||||||
group: 'Request'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Method input
|
|
||||||
ports.push({
|
|
||||||
name: 'method',
|
|
||||||
displayName: 'Method',
|
|
||||||
type: {
|
|
||||||
name: 'enum',
|
|
||||||
enums: [
|
|
||||||
{ label: 'GET', value: 'GET' },
|
|
||||||
{ label: 'POST', value: 'POST' },
|
|
||||||
{ label: 'PUT', value: 'PUT' },
|
|
||||||
{ label: 'PATCH', value: 'PATCH' },
|
|
||||||
{ label: 'DELETE', value: 'DELETE' },
|
|
||||||
{ label: 'HEAD', value: 'HEAD' },
|
|
||||||
{ label: 'OPTIONS', value: 'OPTIONS' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
default: 'GET',
|
|
||||||
plug: 'input',
|
|
||||||
group: 'Request'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse URL for path parameters: /users/{userId} → userId port
|
// Parse URL for path parameters: /users/{userId} → userId port
|
||||||
if (parameters.url) {
|
if (parameters.url) {
|
||||||
const pathParams = parameters.url.match(/\{([A-Za-z0-9_]+)\}/g) || [];
|
const pathParams = parameters.url.match(/\{([A-Za-z0-9_]+)\}/g) || [];
|
||||||
@@ -580,7 +592,7 @@ function updatePorts(nodeId, parameters, editorConnection) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Headers configuration
|
// Headers configuration - comma-separated list
|
||||||
ports.push({
|
ports.push({
|
||||||
name: 'headers',
|
name: 'headers',
|
||||||
displayName: 'Headers',
|
displayName: 'Headers',
|
||||||
@@ -590,21 +602,23 @@ function updatePorts(nodeId, parameters, editorConnection) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Generate input ports for each header
|
// Generate input ports for each header
|
||||||
if (parameters.headers && Array.isArray(parameters.headers)) {
|
if (parameters.headers) {
|
||||||
for (const header of parameters.headers) {
|
const headerList = parameters.headers
|
||||||
if (header.key) {
|
.split(',')
|
||||||
ports.push({
|
.map((h) => h.trim())
|
||||||
name: 'header-' + header.key,
|
.filter(Boolean);
|
||||||
displayName: header.key,
|
for (const header of headerList) {
|
||||||
type: 'string',
|
ports.push({
|
||||||
plug: 'input',
|
name: 'header-' + header,
|
||||||
group: 'Headers'
|
displayName: header,
|
||||||
});
|
type: 'string',
|
||||||
}
|
plug: 'input',
|
||||||
|
group: 'Headers'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query parameters configuration
|
// Query parameters configuration - comma-separated list
|
||||||
ports.push({
|
ports.push({
|
||||||
name: 'queryParams',
|
name: 'queryParams',
|
||||||
displayName: 'Query Parameters',
|
displayName: 'Query Parameters',
|
||||||
@@ -614,20 +628,44 @@ function updatePorts(nodeId, parameters, editorConnection) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Generate input ports for each query param
|
// Generate input ports for each query param
|
||||||
if (parameters.queryParams && Array.isArray(parameters.queryParams)) {
|
if (parameters.queryParams) {
|
||||||
for (const param of parameters.queryParams) {
|
const queryList = parameters.queryParams
|
||||||
if (param.key) {
|
.split(',')
|
||||||
ports.push({
|
.map((q) => q.trim())
|
||||||
name: 'query-' + param.key,
|
.filter(Boolean);
|
||||||
displayName: param.key,
|
for (const param of queryList) {
|
||||||
type: '*',
|
ports.push({
|
||||||
plug: 'input',
|
name: 'query-' + param,
|
||||||
group: 'Query Parameters'
|
displayName: param,
|
||||||
});
|
type: 'string',
|
||||||
}
|
plug: 'input',
|
||||||
|
group: 'Query Parameters'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method selector as dynamic port (so we can track parameter changes properly)
|
||||||
|
ports.push({
|
||||||
|
name: 'method',
|
||||||
|
displayName: 'Method',
|
||||||
|
type: {
|
||||||
|
name: 'enum',
|
||||||
|
enums: [
|
||||||
|
{ label: 'GET', value: 'GET' },
|
||||||
|
{ label: 'POST', value: 'POST' },
|
||||||
|
{ label: 'PUT', value: 'PUT' },
|
||||||
|
{ label: 'PATCH', value: 'PATCH' },
|
||||||
|
{ label: 'DELETE', value: 'DELETE' },
|
||||||
|
{ label: 'HEAD', value: 'HEAD' },
|
||||||
|
{ label: 'OPTIONS', value: 'OPTIONS' }
|
||||||
|
],
|
||||||
|
allowEditOnly: true
|
||||||
|
},
|
||||||
|
default: 'GET',
|
||||||
|
plug: 'input',
|
||||||
|
group: 'Request'
|
||||||
|
});
|
||||||
|
|
||||||
// Body type selector (only shown for POST/PUT/PATCH)
|
// Body type selector (only shown for POST/PUT/PATCH)
|
||||||
const method = parameters.method || 'GET';
|
const method = parameters.method || 'GET';
|
||||||
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
||||||
@@ -649,7 +687,7 @@ function updatePorts(nodeId, parameters, editorConnection) {
|
|||||||
group: 'Body'
|
group: 'Body'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Body fields configuration
|
// Body fields configuration - comma-separated list
|
||||||
const bodyType = parameters.bodyType || 'json';
|
const bodyType = parameters.bodyType || 'json';
|
||||||
if (bodyType === 'json' || bodyType === 'form' || bodyType === 'urlencoded') {
|
if (bodyType === 'json' || bodyType === 'form' || bodyType === 'urlencoded') {
|
||||||
ports.push({
|
ports.push({
|
||||||
@@ -660,25 +698,56 @@ function updatePorts(nodeId, parameters, editorConnection) {
|
|||||||
group: 'Body'
|
group: 'Body'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate input ports for each body field
|
// Type options for body fields
|
||||||
if (parameters.bodyFields && Array.isArray(parameters.bodyFields)) {
|
const _types = [
|
||||||
for (const field of parameters.bodyFields) {
|
{ label: 'String', value: 'string' },
|
||||||
if (field.key) {
|
{ label: 'Number', value: 'number' },
|
||||||
ports.push({
|
{ label: 'Boolean', value: 'boolean' },
|
||||||
name: 'body-' + field.key,
|
{ label: 'Array', value: 'array' },
|
||||||
displayName: field.key,
|
{ label: 'Object', value: 'object' },
|
||||||
type: '*',
|
{ label: 'Any', value: '*' }
|
||||||
plug: 'input',
|
];
|
||||||
group: 'Body'
|
|
||||||
});
|
// Generate type selector and value input ports for each body field
|
||||||
}
|
if (parameters.bodyFields) {
|
||||||
|
const fieldList = parameters.bodyFields
|
||||||
|
.split(',')
|
||||||
|
.map((f) => f.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
for (const field of fieldList) {
|
||||||
|
// Get the selected type for this field (default to string)
|
||||||
|
const fieldType = parameters['body-type-' + field] || 'string';
|
||||||
|
|
||||||
|
// Type selector for this field
|
||||||
|
ports.push({
|
||||||
|
name: 'body-type-' + field,
|
||||||
|
displayName: field + ' Type',
|
||||||
|
type: {
|
||||||
|
name: 'enum',
|
||||||
|
enums: _types,
|
||||||
|
allowEditOnly: true
|
||||||
|
},
|
||||||
|
default: 'string',
|
||||||
|
plug: 'input',
|
||||||
|
group: 'Body'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Value input for this field (type matches selected type)
|
||||||
|
ports.push({
|
||||||
|
name: 'body-' + field,
|
||||||
|
displayName: field,
|
||||||
|
type: fieldType,
|
||||||
|
plug: 'input',
|
||||||
|
group: 'Body'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (bodyType === 'raw') {
|
} else if (bodyType === 'raw') {
|
||||||
|
// Raw body - use code editor with JSON syntax
|
||||||
ports.push({
|
ports.push({
|
||||||
name: 'body-raw',
|
name: 'body-raw',
|
||||||
displayName: 'Body',
|
displayName: 'Body',
|
||||||
type: 'string',
|
type: { name: 'string', allowEditOnly: true, codeeditor: 'json' },
|
||||||
plug: 'input',
|
plug: 'input',
|
||||||
group: 'Body'
|
group: 'Body'
|
||||||
});
|
});
|
||||||
@@ -760,41 +829,28 @@ function updatePorts(nodeId, parameters, editorConnection) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response mapping
|
// Timeout setting
|
||||||
ports.push({
|
|
||||||
name: 'responseMapping',
|
|
||||||
displayName: 'Response Mapping',
|
|
||||||
type: { name: 'stringlist', allowEditOnly: true },
|
|
||||||
plug: 'input',
|
|
||||||
group: 'Response Mapping'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate output ports for each response mapping
|
|
||||||
if (parameters.responseMapping && Array.isArray(parameters.responseMapping)) {
|
|
||||||
for (const mapping of parameters.responseMapping) {
|
|
||||||
if (mapping.name) {
|
|
||||||
ports.push({
|
|
||||||
name: 'out-' + mapping.name,
|
|
||||||
displayName: mapping.name,
|
|
||||||
type: '*',
|
|
||||||
plug: 'output',
|
|
||||||
group: 'Response'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timeout
|
|
||||||
ports.push({
|
ports.push({
|
||||||
name: 'timeout',
|
name: 'timeout',
|
||||||
displayName: 'Timeout (ms)',
|
displayName: 'Timeout (ms)',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
default: 30000,
|
default: 30000,
|
||||||
plug: 'input',
|
plug: 'input',
|
||||||
group: 'Settings'
|
group: 'Request'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actions
|
// Response mapping - add output names, then specify JSONPath for each
|
||||||
|
// User adds output names like: userId, userName, totalCount
|
||||||
|
// For each name, we generate a "Path" input and an output port
|
||||||
|
ports.push({
|
||||||
|
name: 'responseMapping',
|
||||||
|
displayName: 'Output Fields',
|
||||||
|
type: { name: 'stringlist', allowEditOnly: true },
|
||||||
|
plug: 'input',
|
||||||
|
group: 'Response Mapping'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Signal inputs - MUST be in dynamic ports for editor to show them
|
||||||
ports.push({
|
ports.push({
|
||||||
name: 'fetch',
|
name: 'fetch',
|
||||||
displayName: 'Fetch',
|
displayName: 'Fetch',
|
||||||
@@ -811,6 +867,44 @@ function updatePorts(nodeId, parameters, editorConnection) {
|
|||||||
group: 'Actions'
|
group: 'Actions'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// URL input - also needs to be in dynamic ports
|
||||||
|
ports.push({
|
||||||
|
name: 'url',
|
||||||
|
displayName: 'URL',
|
||||||
|
type: 'string',
|
||||||
|
plug: 'input',
|
||||||
|
group: 'Request'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate path input ports and output ports for each response mapping
|
||||||
|
if (parameters.responseMapping && typeof parameters.responseMapping === 'string') {
|
||||||
|
const outputNames = parameters.responseMapping
|
||||||
|
.split(',')
|
||||||
|
.map((m) => m.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
for (const name of outputNames) {
|
||||||
|
// Path input port for specifying JSONPath (e.g., $.data.id)
|
||||||
|
ports.push({
|
||||||
|
name: 'mapping-path-' + name,
|
||||||
|
displayName: name + ' Path',
|
||||||
|
type: 'string',
|
||||||
|
default: '$',
|
||||||
|
plug: 'input',
|
||||||
|
group: 'Response Mapping'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Output port for the extracted value
|
||||||
|
ports.push({
|
||||||
|
name: 'out-' + name,
|
||||||
|
displayName: name,
|
||||||
|
type: '*',
|
||||||
|
plug: 'output',
|
||||||
|
group: 'Response'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Standard outputs
|
// Standard outputs
|
||||||
ports.push({
|
ports.push({
|
||||||
name: 'response',
|
name: 'response',
|
||||||
@@ -891,7 +985,8 @@ module.exports = {
|
|||||||
event.name === 'bodyType' ||
|
event.name === 'bodyType' ||
|
||||||
event.name === 'bodyFields' ||
|
event.name === 'bodyFields' ||
|
||||||
event.name === 'authType' ||
|
event.name === 'authType' ||
|
||||||
event.name === 'responseMapping'
|
event.name === 'responseMapping' ||
|
||||||
|
event.name.startsWith('body-type-') // Body field type changes
|
||||||
) {
|
) {
|
||||||
updatePorts(node.id, node.parameters, context.editorConnection);
|
updatePorts(node.id, node.parameters, context.editorConnection);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ export default function registerNodes(noodlRuntime) {
|
|||||||
require('./nodes/std-library/colorblend'),
|
require('./nodes/std-library/colorblend'),
|
||||||
require('./nodes/std-library/animate-to-value'),
|
require('./nodes/std-library/animate-to-value'),
|
||||||
|
|
||||||
|
// HTTP node - temporarily here for debugging (normally in noodl-runtime)
|
||||||
|
require('@noodl/runtime/src/nodes/std-library/data/httpnode'),
|
||||||
|
|
||||||
//require('./nodes/std-library/variables/number'), // moved to runtime
|
//require('./nodes/std-library/variables/number'), // moved to runtime
|
||||||
//require('./nodes/std-library/variables/string'),
|
//require('./nodes/std-library/variables/string'),
|
||||||
//require('./nodes/std-library/variables/boolean'),
|
//require('./nodes/std-library/variables/boolean'),
|
||||||
|
|||||||
Reference in New Issue
Block a user