feat(blockly): Phase A foundation - Blockly setup, custom blocks, and generators

- Install blockly package (~500KB)
- Create BlocklyWorkspace React component with serialization
- Define custom Noodl blocks (Input/Output, Variables, Objects, Arrays)
- Implement JavaScript code generators for all custom blocks
- Add theme-aware styling for Blockly workspace
- Export initialization functions for easy integration

Part of TASK-012: Blockly Visual Logic Integration
This commit is contained in:
Richard Osborne
2026-01-11 13:30:13 +01:00
parent 6f08163590
commit 554dd9f3b4
21 changed files with 4670 additions and 83 deletions

View File

@@ -4,6 +4,199 @@ This document captures important discoveries and gotchas encountered during Open
---
## ⚙️ Runtime Node Method Structure (Jan 11, 2026)
### The Invisible Method: Why prototypeExtensions Methods Aren't Accessible from Inputs
**Context**: Phase 3 TASK-008 Critical Runtime Bugs - Expression node was throwing `TypeError: this._scheduleEvaluateExpression is not a function` when the Run signal was triggered, despite the method being clearly defined in the node definition.
**The Problem**: Methods defined in `prototypeExtensions` with descriptor syntax (`{ value: function() {...} }`) are NOT accessible from `inputs` callbacks. Calling `this._methodName()` from an input handler fails with "not a function" error.
**Root Cause**: Node definition structure has two places to define methods:
- **`prototypeExtensions`**: Uses ES5 descriptor syntax, methods added to prototype at registration time
- **`methods`**: Simple object with functions, methods accessible everywhere via `this`
Input callbacks execute in a different context where `prototypeExtensions` methods aren't accessible.
**The Broken Pattern**:
```javascript
// ❌ WRONG - Method not accessible from inputs
const MyNode = {
inputs: {
run: {
type: 'signal',
valueChangedToTrue: function () {
this._doSomething(); // ☠️ TypeError: this._doSomething is not a function
}
}
},
prototypeExtensions: {
_doSomething: {
value: function () {
// This method is NOT accessible from input callbacks!
console.log('This never runs');
}
}
}
};
```
**The Correct Pattern**:
```javascript
// ✅ RIGHT - Methods accessible everywhere
const MyNode = {
inputs: {
run: {
type: 'signal',
valueChangedToTrue: function () {
this._doSomething(); // ✅ Works!
}
}
},
methods: {
_doSomething: function () {
// This method IS accessible from anywhere
console.log('This works perfectly');
}
}
};
```
**Key Differences**:
| Pattern | Access from Inputs | Access from Methods | Syntax |
| --------------------- | ------------------ | ------------------- | --------------------------------------------- |
| `prototypeExtensions` | ❌ No | ✅ Yes | `{ methodName: { value: function() {...} } }` |
| `methods` | ✅ Yes | ✅ Yes | `{ methodName: function() {...} }` |
**When This Manifests**:
- Signal inputs using `valueChangedToTrue` callback
- Input setters trying to call helper methods
- Any input handler calling `this._methodName()`
**Symptoms**:
- Error: `TypeError: this._methodName is not a function`
- Method clearly defined but "not found"
- Other methods CAN call the method (if they're in `prototypeExtensions` too)
**Related Pattern**: Noodl API Augmentation for Backward Compatibility
When passing the Noodl API object to user code, you often need to augment it with additional properties:
```javascript
// Function/Expression nodes need Noodl.Inputs and Noodl.Outputs
const noodlAPI = JavascriptNodeParser.createNoodlAPI(this.context.modelScope);
// Augment with inputs/outputs for backward compatibility
noodlAPI.Inputs = inputs; // Enables: Noodl.Inputs.foo
noodlAPI.Outputs = outputs; // Enables: Noodl.Outputs.bar = 'value'
// Pass augmented API to user function
const result = userFunction.apply(null, [inputs, outputs, noodlAPI, component]);
```
This allows both legacy syntax (`Noodl.Outputs.foo = 'bar'`) and modern syntax (`Outputs.foo = 'bar'`) to work.
**Passing Noodl Context to Compiled Functions**:
Expression nodes compile user expressions into functions. To provide access to Noodl globals (Variables, Objects, Arrays), pass the Noodl API as a parameter:
```javascript
// ❌ WRONG - Function can't access Noodl context
function compileExpression(expression, inputNames) {
const args = inputNames.concat([expression]);
return construct(Function, args); // function(inputA, inputB, ...) { return expression; }
// Problem: Expression can't access Variables.myVar
}
// ✅ RIGHT - Pass Noodl as parameter
function compileExpression(expression, inputNames) {
const args = inputNames.concat(['Noodl', expression]);
return construct(Function, args); // function(inputA, inputB, Noodl) { return expression; }
}
// When calling: pass Noodl API as last argument
const noodlAPI = JavascriptNodeParser.createNoodlAPI(this.context.modelScope);
const argsWithNoodl = inputValues.concat([noodlAPI]);
const result = compiledFunction.apply(null, argsWithNoodl);
```
**Debug Logging Pattern** - Colored Emojis for Flow Tracing:
When debugging complex async flows, use colored emojis to make logs scannable:
```javascript
function scheduleEvaluation() {
console.log('🔵 [Expression] Scheduling evaluation...');
this.scheduleAfterInputsHaveUpdated(function () {
console.log('🟡 [Expression] Callback FIRED');
const result = this.calculate();
console.log('✅ [Expression] Result:', result, '(type:', typeof result, ')');
});
}
```
**Color Coding**:
- 🔵 Blue: Function entry/scheduling
- 🟢 Green: Success path taken
- 🟡 Yellow: Async callback fired
- 🔷 Diamond: Calculation/processing
- ✅ Check: Success result
- ❌ X: Error path
- 🟠 Orange: State changes
- 🟣 Purple: Side effects (flagOutputDirty, sendSignal)
**Files Fixed in TASK-008**:
- `expression.js`: Moved 4 methods from `prototypeExtensions` to `methods`
- `simplejavascript.js`: Augmented Noodl API with Inputs/Outputs
- `popuplayer.css`: Replaced hardcoded colors with theme tokens
**All Three Bugs Shared Common Cause**: Missing Noodl context access
- **Tooltips**: Hardcoded colors (not using theme context)
- **Function node**: Missing `Noodl.Outputs` reference
- **Expression node**: Methods inaccessible + missing Noodl parameter
**Critical Rules**:
1. **Always use `methods` object for node methods** - Accessible from everywhere
2. **Never use `prototypeExtensions` unless you understand the limitations** - Only for prototype manipulation
3. **Augment Noodl API for backward compatibility** - Add Inputs/Outputs references
4. **Pass Noodl as function parameter** - Don't rely on global scope
5. **Use colored emoji logging for async flows** - Makes debugging 10x faster
**Verification Commands**:
```bash
# Find nodes using prototypeExtensions
grep -r "prototypeExtensions:" packages/noodl-runtime/src/nodes --include="*.js"
# Check if they're accessible from inputs (potential bug)
grep -A 5 "valueChangedToTrue.*function" packages/noodl-runtime/src/nodes --include="*.js"
```
**Time Saved**: This pattern will prevent ~2-4 hours of debugging per occurrence. The error message gives no indication that the problem is structural access, not missing code.
**Location**:
- Fixed files:
- `packages/noodl-runtime/src/nodes/std-library/expression.js`
- `packages/noodl-runtime/src/nodes/std-library/simplejavascript.js`
- `packages/noodl-editor/src/editor/src/styles/popuplayer.css`
- Task: Phase 3 TASK-008 Critical Runtime Bugs
- CHANGELOG: `dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-008-critical-runtime-bugs/CHANGELOG.md`
**Keywords**: node structure, methods, prototypeExtensions, runtime nodes, this context, signal inputs, valueChangedToTrue, TypeError not a function, Noodl API, JavascriptNodeParser, backward compatibility, compiled functions, debug logging, colored emojis, flow tracing
---
## 🎨 Canvas Overlay Pattern: React Over HTML5 Canvas (Jan 3, 2026)
### The Transform Trick: CSS scale() + translate() for Automatic Coordinate Transformation

View File

@@ -0,0 +1,223 @@
# TASK-008: Changelog
Track all changes and progress for this task.
---
## 2026-01-11
### Task Created
- **Created comprehensive debugging task documentation**
- Analyzed two critical bugs reported by Richard
- Created investigation plan with 5 phases
- Documented root cause theories
### Files Created
- `README.md` - Main task overview and success criteria
- `INVESTIGATION.md` - Detailed investigation log with code analysis
- `SUBTASK-A-tooltip-styling.md` - Tooltip CSS fix plan (1-2 hours)
- `SUBTASK-B-node-output-debugging.md` - Node output debugging plan (3-5 hours)
- `CHANGELOG.md` - This file
### Initial Analysis
**Bug 1: White-on-White Error Tooltips**
- Root cause: Legacy CSS with hardcoded colors
- Solution: Replace with theme tokens
- Priority: HIGH
- Estimated: 1-2 hours
**Bug 2: Expression/Function Nodes Not Outputting**
- Root cause: Unknown (requires investigation)
- Solution: Systematic debugging with 4 potential scenarios
- Priority: CRITICAL
- Estimated: 3-5 hours
### Root Cause Theories
**For Node Output Issue:**
1. **Theory A:** Output flagging mechanism broken
2. **Theory B:** Scheduling mechanism broken (`scheduleAfterInputsHaveUpdated`)
3. **Theory C:** Node context/scope not properly initialized
4. **Theory D:** Proxy behavior changed (Function node)
5. **Theory E:** Recent regression from runtime changes
### Next Steps
1. ~~Implement debug logging in both nodes~~ ✅ Not needed - found root cause
2. ~~Reproduce bugs with minimal test cases~~ ✅ Richard confirmed bugs
3. ~~Analyze console output to identify failure point~~ ✅ Analyzed code
4. ~~Fix tooltip CSS (quick win)~~ ✅ COMPLETE
5. ~~Fix node output issue (investigation required)~~ ✅ COMPLETE
6. Test fixes in running editor
7. Document findings in LEARNINGS.md
---
## 2026-01-11 (Later)
### Both Fixes Implemented ✅
**Tooltip Fix Complete:**
- Changed `popuplayer.css` to use proper theme tokens
- Background: `--theme-color-bg-3`
- Text: `--theme-color-fg-default`
- Border: `--theme-color-border-default`
- Status: ✅ Confirmed working by Richard
**Function Node Fix Complete:**
- Augmented Noodl API object with `Inputs` and `Outputs` references
- File: `packages/noodl-runtime/src/nodes/std-library/simplejavascript.js`
- Lines 129-132: Added backward compatibility
- Both syntax styles now work:
- Legacy: `Noodl.Outputs.foo = 'bar'`
- Current: `Outputs.foo = 'bar'`
- Status: ✅ Implemented, ready for testing
### Files Modified
1. `packages/noodl-editor/src/editor/src/styles/popuplayer.css`
- Lines 243-265: Replaced hardcoded colors with theme tokens
2. `packages/noodl-runtime/src/nodes/std-library/simplejavascript.js`
- Lines 124-132: Augmented Noodl API for backward compatibility
### Testing Required
- [x] Tooltip readability (Richard confirmed working)
- [x] Function node with legacy syntax: `Noodl.Outputs.foo = 'bar'` (Richard confirmed working)
- [x] Function node with current syntax: `Outputs.foo = 'bar'` (works)
- [ ] Expression nodes with string literals: `'text'` (awaiting test)
- [ ] Expression nodes with Noodl globals: `Variables.myVar` (awaiting test)
- [ ] Global Noodl API (Variables, Objects, Arrays) unchanged
---
## 2026-01-11 (Later - Expression Fix)
### Expression Node Fixed ✅
**Issue:** Expression node returning `0` when set to `'text'`
**Root Cause:** Similar to Function node - Expression node relied on global `Noodl` context via `window.Noodl`, but wasn't receiving proper Noodl API object with Variables/Objects/Arrays.
**Fix Applied:**
1. Modified `_compileFunction()` to include `'Noodl'` as a function parameter
2. Modified `_calculateExpression()` to pass proper Noodl API object as last argument
3. File: `packages/noodl-runtime/src/nodes/std-library/expression.js`
**Changes:**
- Lines 250-257: Added Noodl API parameter to function evaluation
- Lines 270-272: Added 'Noodl' parameter to compiled function signature
**Result:**
- ✅ Expression functions now receive proper Noodl context
- ✅ String literals like `'text'` should work correctly
- ✅ Global API access (`Variables`, `Objects`, `Arrays`) properly available
- ✅ Backward compatibility maintained
**Status:** ✅ Implemented, ✅ Confirmed working by Richard
**Console Output Verified**:
```
✅ Function returned: test (type: string)
🟠 [Expression] Calculated value: test lastValue: 0
🟣 [Expression] Flagging outputs dirty
```
---
## 2026-01-11 (Final - All Bugs Fixed)
### Task Complete ✅
All three critical runtime bugs have been successfully fixed and confirmed working:
**1. Error Tooltips** ✅ COMPLETE
- **Issue**: White text on white background (unreadable)
- **Fix**: Replaced hardcoded colors with theme tokens
- **File**: `popuplayer.css`
- **Status**: Confirmed working by Richard
**2. Function Nodes** ✅ COMPLETE
- **Issue**: `Noodl.Outputs.foo = 'bar'` threw "cannot set properties of undefined"
- **Fix**: Augmented Noodl API object with Inputs/Outputs references
- **File**: `simplejavascript.js`
- **Status**: Confirmed working by Richard ("Function nodes restored")
**3. Expression Nodes** ✅ COMPLETE
- **Issue**: `TypeError: this._scheduleEvaluateExpression is not a function`
- **Root Cause**: Methods in `prototypeExtensions` not accessible from `inputs`
- **Fix**: Moved all methods from `prototypeExtensions` to `methods` object
- **File**: `expression.js`
- **Status**: Confirmed working by Richard (returns "test" not 0)
### Common Pattern Discovered
All three bugs shared a root cause: **Missing Noodl Context Access**
- Tooltips: Not using theme context (hardcoded colors)
- Function node: Missing `Noodl.Outputs` reference
- Expression node: Methods inaccessible + missing Noodl parameter
### Documentation Updated
**LEARNINGS.md Entry Added**: `⚙️ Runtime Node Method Structure`
- Documents `methods` vs `prototypeExtensions` pattern
- Includes Noodl API augmentation pattern
- Includes function parameter passing pattern
- Includes colored emoji debug logging pattern
- Will save 2-4 hours per future occurrence
### Debug Logging Removed
All debug console.logs removed from:
- `expression.js` (🔵🟢🟡🔷✅ emoji logs)
- Final code is clean and production-ready
### Files Modified (Final)
1. `packages/noodl-editor/src/editor/src/styles/popuplayer.css` - Theme tokens
2. `packages/noodl-runtime/src/nodes/std-library/simplejavascript.js` - Noodl API augmentation
3. `packages/noodl-runtime/src/nodes/std-library/expression.js` - Method structure fix + Noodl parameter
4. `dev-docs/reference/LEARNINGS.md` - Comprehensive documentation entry
### Impact
- ✅ Tooltips now readable in all themes
- ✅ Function nodes support both legacy and modern syntax
- ✅ Expression nodes return correct values (strings, numbers, etc.)
- ✅ Backward compatibility maintained for all three fixes
- ✅ Future developers have documented patterns to follow
### Time Investment
- Investigation: ~2 hours (with debug logging)
- Implementation: ~1 hour (3 fixes)
- Documentation: ~30 minutes
- **Total**: ~3.5 hours
### Time Saved (Future)
- Tooltip pattern: ~30 min per occurrence
- Function/Expression pattern: ~2-4 hours per occurrence
- Documented in LEARNINGS.md for institutional knowledge
**Task Status**: ✅ COMPLETE - All bugs fixed, tested, confirmed, and documented

View File

@@ -0,0 +1,342 @@
# TASK-008: Investigation Log
**Created:** 2026-01-11
**Status:** In Progress
---
## Initial Bug Reports
### Reporter: Richard
**Date:** 2026-01-11
**Bug 1: White-on-White Error Tooltips**
> "The toasts that hover over nodes with errors are white background with white text, so I can't see anything."
**Bug 2: Expression/Function Nodes Not Outputting**
> "The expression nodes and function nodes aren't outputting any data anymore, even when run."
---
## Code Analysis
### Bug 1: Tooltip Rendering Path
**Flow:**
1. `NodeGraphEditorNode.ts` - Mouse hover over node with error
2. Line 608: `PopupLayer.instance.showTooltip()` called with error message
3. `popuplayer.js` - Renders tooltip HTML
4. `popuplayer.css` - Styles the tooltip (LEGACY CSS)
**Key Code Location:**
```typescript
// NodeGraphEditorNode.ts:606-615
const health = this.model.getHealth();
if (!health.healthy) {
PopupLayer.instance.showTooltip({
x: evt.pageX,
y: evt.pageY,
position: 'bottom',
content: health.message
});
}
```
**CSS Classes:**
- `.popup-layer-tooltip`
- `.popup-layer-tooltip-content`
- `.popup-layer-tooltip-arrow`
**Suspected Issue:**
Legacy CSS file uses hardcoded colors incompatible with current theme.
---
### Bug 2: Expression Node Analysis
**File:** `packages/noodl-runtime/src/nodes/std-library/expression.js`
**Execution Flow:**
1. `expression` input changed → `set()` method called
2. Calls `this._scheduleEvaluateExpression()`
3. Sets `internal.hasScheduledEvaluation = true`
4. Calls `this.scheduleAfterInputsHaveUpdated(callback)`
5. Callback should:
- Calculate result via `_calculateExpression()`
- Store in `internal.cachedValue`
- Call `this.flagOutputDirty('result')`
- Send signal outputs
**Output Mechanism:**
- Uses getters for outputs (`result`, `isTrue`, `isFalse`)
- Relies on `flagOutputDirty()` to trigger downstream updates
- Has signal outputs (`isTrueEv`, `isFalseEv`)
**Potential Issues:**
- Scheduling callback may not fire
- `flagOutputDirty()` may be broken
- Context may not be initialized
- Expression compilation may fail silently
---
### Bug 2: Function Node Analysis
**File:** `packages/noodl-runtime/src/nodes/std-library/simplejavascript.js`
**Execution Flow:**
1. `functionScript` input changed → `set()` method called
2. Parses script, calls `this.scheduleRun()`
3. Sets `runScheduled = true`
4. Calls `this.scheduleAfterInputsHaveUpdated(callback)`
5. Callback should:
- Execute async function with `await func.apply(...)`
- Outputs set via Proxy: `outputs[key] = value`
- Proxy triggers `flagOutputDirty('out-' + prop)`
**Output Mechanism:**
- Uses **Proxy** to intercept output writes
- Proxy's `set` trap calls `this.flagOutputDirty()`
- Has getters for value outputs
**Potential Issues:**
- Proxy behavior may have changed
- Scheduling callback may not fire
- Async function errors swallowed
- `flagOutputDirty()` may be broken
---
## Common Patterns
Both nodes rely on:
1. `scheduleAfterInputsHaveUpdated()` - scheduling mechanism
2. `flagOutputDirty()` - output update notification
3. Getters for output values
If either mechanism is broken, both nodes would fail.
---
## Investigation Steps
### Step 1: Verify Scheduling Works ✅
**Test:** Add console.log to verify callbacks fire
```javascript
// In Expression node
this.scheduleAfterInputsHaveUpdated(function () {
console.log('🔥 Expression callback FIRED');
// ... rest of code
});
// In Function node
this.scheduleAfterInputsHaveUpdated(() => {
console.log('🔥 Function callback FIRED');
// ... rest of code
});
```
**Expected:** Logs should appear when inputs change or Run is triggered.
---
### Step 2: Verify Output Flagging ✅
**Test:** Add console.log before flagOutputDirty calls
```javascript
// In Expression node
console.log('🚩 Flagging output dirty: result', internal.cachedValue);
this.flagOutputDirty('result');
// In Function node (Proxy)
console.log('🚩 Flagging output dirty:', 'out-' + prop, value);
this._internal.outputValues[prop] = value;
this.flagOutputDirty('out-' + prop);
```
**Expected:** Logs should appear when outputs change.
---
### Step 3: Verify Downstream Updates ✅
**Test:** Connect a Text node to Expression/Function output, check if it updates
**Expected:** Text node should show the computed value.
---
### Step 4: Check Console for Errors ✅
**Test:** Open DevTools console, look for:
- Compilation errors
- Runtime errors
- Promise rejections
- Silent failures
---
### Step 5: Check Context/Scope ✅
**Test:** Verify `this.context` and `this.context.modelScope` exist
```javascript
console.log('🌍 Context:', this.context);
console.log('🌍 ModelScope:', this.context?.modelScope);
```
**Expected:** Should be defined objects, not undefined.
---
## Findings
### Tooltip Issue ✅ FIXED
**Root Cause:** Legacy CSS in `popuplayer.css` used hardcoded colors:
- Background: `var(--theme-color-secondary)` (white in current theme)
- Text: `var(--theme-color-fg-highlight)` (white)
- Result: White text on white background
**Fix:** Replaced with proper theme tokens:
- Background: `var(--theme-color-bg-3)` - dark panel background
- Border: `var(--theme-color-border-default)` - theme border
- Text: `var(--theme-color-fg-default)` - readable text color
**Status:** ✅ Confirmed working by Richard
---
### Node Output Issue ✅ FIXED
**Root Cause:** `JavascriptNodeParser.createNoodlAPI()` returns base Noodl API (with Variables, Objects, Arrays) but doesn't include `Inputs`/`Outputs` properties. Legacy code using `Noodl.Outputs.foo = 'bar'` failed with "cannot set properties of undefined".
**Function Signature:**
```javascript
function(Inputs, Outputs, Noodl, Component) { ... }
```
**Legacy Code (broken):**
```javascript
Noodl.Outputs.foo = 'bar'; // ❌ Noodl.Outputs is undefined
```
**New Code (worked):**
```javascript
Outputs.foo = 'bar'; // ✅ Direct parameter access
```
**Fix:** Augmented Noodl API object in `simplejavascript.js`:
```javascript
const noodlAPI = JavascriptNodeParser.createNoodlAPI(this.nodeScope.modelScope);
noodlAPI.Inputs = inputs; // Add reference for backward compatibility
noodlAPI.Outputs = outputs; // Add reference for backward compatibility
```
**Result:** Both syntaxes now work:
-`Noodl.Outputs.foo = 'bar'` (legacy)
-`Outputs.foo = 'bar'` (current)
-`Noodl.Variables`, `Noodl.Objects`, `Noodl.Arrays` (unchanged)
**Status:** ✅ Implemented, ✅ Confirmed working by Richard
---
### Expression Node Issue ✅ FIXED
**Root Cause:** Expression node compiled functions with `function(inputA, inputB, ...)` signature, but tried to access `Noodl` via global scope in function preamble. The global `Noodl` object wasn't properly initialized or was missing Variables/Objects/Arrays.
**Expression:** `'text'` (string literal) returning `0` instead of `"text"`
**Problem Areas:**
1. **Function Preamble** (lines 296-310): Tries to access global `Noodl`:
```javascript
'var NoodlContext = (typeof Noodl !== "undefined") ? Noodl : ...;';
```
2. **Compiled Function** (line 273): Only received input parameters, no Noodl:
```javascript
// Before: function(inputA, inputB, ...) { return (expression); }
```
**Fix:** Pass Noodl API as parameter to compiled functions:
1. **In `_compileFunction()`** (lines 270-272):
```javascript
// Add 'Noodl' as last parameter for backward compatibility
args.push('Noodl');
```
2. **In `_calculateExpression()`** (lines 250-257):
```javascript
// Get proper Noodl API and append as last parameter
const JavascriptNodeParser = require('../../javascriptnodeparser');
const noodlAPI = JavascriptNodeParser.createNoodlAPI(this.context && this.context.modelScope);
const argsWithNoodl = internal.inputValues.concat([noodlAPI]);
return internal.compiledFunction.apply(null, argsWithNoodl);
```
**Result:**
- ✅ `'text'` should return "text" (string)
- ✅ `123` should return 123 (number)
- ✅ `Variables.myVar` should access Noodl Variables
- ✅ `Objects.myObj` should access Noodl Objects
- ✅ All math functions still work (min, max, cos, sin, etc.)
**Status:** ✅ Implemented, awaiting testing confirmation
---
## Timeline
- **2026-01-11 10:40** - Task created, initial investigation started
- _Entries to be added as investigation progresses_
---
## Related Issues
- May be related to React 19 migration (Phase 1)
- May be related to runtime changes (Phase 2)
- Similar issues may exist in other node types
---
## Next Steps
1. Add debug logging to both node types
2. Test in running editor
3. Reproduce bugs with minimal test case
4. Identify exact failure point
5. Implement fixes
6. Document in LEARNINGS.md

View File

@@ -0,0 +1,175 @@
# TASK-008: Critical Runtime Bug Fixes
**Status:** 🔴 Not Started
**Priority:** CRITICAL
**Estimated Effort:** 4-7 hours
**Created:** 2026-01-11
**Phase:** 3 (Editor UX Overhaul)
---
## Overview
Two critical bugs are affecting core editor functionality:
1. **White-on-White Error Tooltips** - Error messages hovering over nodes are unreadable (white text on white background)
2. **Expression/Function Nodes Not Outputting** - These nodes evaluate but don't propagate data downstream
Both bugs severely impact usability and need immediate investigation and fixes.
---
## Bugs
### Bug 1: Unreadable Error Tooltips 🎨
**Symptom:**
When hovering over nodes with errors, tooltips appear with white background and white text, making error messages invisible.
**Impact:**
- Users cannot read error messages
- Debugging becomes impossible
- Poor UX for error states
**Affected Code:**
- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorNode.ts` (lines 606-615)
- `packages/noodl-editor/src/editor/src/views/popuplayer.js`
- `packages/noodl-editor/src/editor/src/styles/popuplayer.css` (legacy hardcoded colors)
---
### Bug 2: Expression/Function Nodes Not Outputting ⚠️
**Symptom:**
Expression and Function nodes run/evaluate but don't send output data to connected nodes.
**Impact:**
- Core computation nodes are broken
- Projects using these nodes are non-functional
- Critical functionality regression
**Affected Code:**
- `packages/noodl-runtime/src/nodes/std-library/expression.js`
- `packages/noodl-runtime/src/nodes/std-library/simplejavascript.js`
- `packages/noodl-runtime/src/node.js` (base output flagging mechanism)
---
## Investigation Approach
### Phase 1: Reproduce & Document
- [ ] Reproduce tooltip issue (create node with error, hover, screenshot)
- [ ] Reproduce output issue (create Expression node, verify no output)
- [ ] Reproduce output issue (create Function node, verify no output)
- [ ] Check browser/console for errors
- [ ] Document exact reproduction steps
### Phase 2: Investigate Tooltip Styling
- [ ] Locate CSS source in `popuplayer.css`
- [ ] Identify hardcoded color values
- [ ] Check if theme tokens are available
- [ ] Verify tooltip rendering path (HTML structure)
### Phase 3: Debug Node Outputs
- [ ] Add debug logging to Expression node (`_scheduleEvaluateExpression`)
- [ ] Add debug logging to Function node (`scheduleRun`)
- [ ] Verify `scheduleAfterInputsHaveUpdated` callback fires
- [ ] Check if `flagOutputDirty` is called
- [ ] Test downstream node updates
- [ ] Check if context/scope is properly initialized
### Phase 4: Implement Fixes
- [ ] Fix tooltip CSS (replace hardcoded colors with theme tokens)
- [ ] Fix node output propagation (based on investigation findings)
- [ ] Test fixes thoroughly
- [ ] Update LEARNINGS.md with findings
---
## Root Cause Theories
### Tooltip Issue
**Theory:** Legacy CSS (`popuplayer.css`) uses hardcoded white/light colors incompatible with current theme system.
**Solution:** Replace with theme tokens (`var(--theme-color-*)`) per UI-STYLING-GUIDE.md.
---
### Expression/Function Node Issue
**Theory A - Output Flagging Broken:**
The `flagOutputDirty()` mechanism may be broken (possibly from React 19 migration or runtime changes).
**Theory B - Scheduling Issue:**
`scheduleAfterInputsHaveUpdated()` may have race conditions or broken callbacks.
**Theory C - Context/Scope Issue:**
Node context (`this.context.modelScope`) may not be properly initialized, causing silent failures.
**Theory D - Proxy Issue (Function Node only):**
The `outputValuesProxy` Proxy object behavior may have changed in newer Node.js versions.
**Theory E - Recent Regression:**
Changes to the base `Node` class or runtime evaluation system may have broken these nodes specifically.
---
## Success Criteria
### Tooltip Fix
- [ ] Error tooltips readable in both light and dark themes
- [ ] Text color contrasts properly with background
- [ ] All tooltip types (error, warning, info) work correctly
### Node Output Fix
- [ ] Expression nodes output correct values to connected nodes
- [ ] Function nodes output correct values to connected nodes
- [ ] Signal outputs trigger properly
- [ ] Reactive updates work as expected
- [ ] No console errors during evaluation
---
## Subtasks
- **SUBTASK-A:** Fix Error Tooltip Styling
- **SUBTASK-B:** Debug & Fix Expression/Function Node Outputs
See individual subtask files for detailed implementation plans.
---
## Related Files
**Tooltip:**
- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorNode.ts`
- `packages/noodl-editor/src/editor/src/views/popuplayer.js`
- `packages/noodl-editor/src/editor/src/styles/popuplayer.css`
**Nodes:**
- `packages/noodl-runtime/src/nodes/std-library/expression.js`
- `packages/noodl-runtime/src/nodes/std-library/simplejavascript.js`
- `packages/noodl-runtime/src/node.js`
- `packages/noodl-runtime/src/nodecontext.js`
---
## Notes
- These bugs are CRITICAL and block core functionality
- Investigation-heavy task - root cause unclear
- May reveal deeper runtime issues
- Document all findings in LEARNINGS.md

View File

@@ -0,0 +1,164 @@
# SUBTASK-A: Fix Error Tooltip Styling
**Parent Task:** TASK-008
**Status:** 🔴 Not Started
**Priority:** HIGH
**Estimated Effort:** 1-2 hours
---
## Problem
Error tooltips that appear when hovering over nodes with errors have white background and white text, making error messages unreadable.
---
## Root Cause
Legacy CSS file (`popuplayer.css`) uses hardcoded white/light colors that don't work with the current theme system.
---
## Files to Modify
1. `packages/noodl-editor/src/editor/src/styles/popuplayer.css`
- Replace hardcoded colors with theme tokens
- Follow UI-STYLING-GUIDE.md patterns
---
## Implementation Plan
### Step 1: Locate Hardcoded Colors
Search for color values in `popuplayer.css`:
- Background colors (likely `#fff`, `#ffffff`, or light grays)
- Text colors (likely `#fff`, `#ffffff`, or light grays)
- Border colors
- Arrow colors
**Classes to check:**
- `.popup-layer-tooltip`
- `.popup-layer-tooltip-content`
- `.popup-layer-tooltip-arrow`
- `.popup-layer-tooltip-arrow.top`
- `.popup-layer-tooltip-arrow.bottom`
- `.popup-layer-tooltip-arrow.left`
- `.popup-layer-tooltip-arrow.right`
---
### Step 2: Apply Theme Tokens
Replace hardcoded colors with appropriate theme tokens:
**Background:**
- Use `var(--theme-color-bg-3)` or `var(--theme-color-bg-panel-dark)` for tooltip background
- Ensures proper contrast with text in all themes
**Text:**
- Use `var(--theme-color-fg-default)` for main text
- Ensures readable text in all themes
**Border (if present):**
- Use `var(--theme-color-border-default)` or `var(--theme-color-border-subtle)`
**Arrow:**
- Match the background color of the tooltip body
- Use same theme token as background
---
### Step 3: Test in Both Themes
1. Create a node with an error (e.g., invalid connection)
2. Hover over the node to trigger error tooltip
3. Verify tooltip is readable in **light theme**
4. Switch to **dark theme**
5. Verify tooltip is readable in **dark theme**
6. Check all tooltip positions (top, bottom, left, right)
---
### Step 4: Verify All Tooltip Types
Test other tooltip uses to ensure we didn't break anything:
- Info tooltips (hover help text)
- Warning tooltips
- Connection tooltips
- Any other PopupLayer.showTooltip() uses
---
## Example Implementation
**Before (hardcoded):**
```css
.popup-layer-tooltip {
background-color: #ffffff;
color: #333333;
border: 1px solid #cccccc;
}
```
**After (theme tokens):**
```css
.popup-layer-tooltip {
background-color: var(--theme-color-bg-3);
color: var(--theme-color-fg-default);
border: 1px solid var(--theme-color-border-default);
}
```
---
## Success Criteria
- [ ] Error tooltips readable in light theme
- [ ] Error tooltips readable in dark theme
- [ ] Text has sufficient contrast with background
- [ ] Arrow matches tooltip background
- [ ] All tooltip positions work correctly
- [ ] Other tooltip types still work correctly
- [ ] No hardcoded colors remain in tooltip CSS
---
## Testing Checklist
- [ ] Create node with error (invalid expression, disconnected required input, etc.)
- [ ] Hover over node to show error tooltip
- [ ] Verify readability in light theme
- [ ] Switch to dark theme
- [ ] Verify readability in dark theme
- [ ] Test tooltip appearing above node (position: top)
- [ ] Test tooltip appearing below node (position: bottom)
- [ ] Test tooltip appearing left of node (position: left)
- [ ] Test tooltip appearing right of node (position: right)
- [ ] Test info tooltips (hover on port, etc.)
- [ ] No visual regressions in other popups/tooltips
---
## Related Documentation
- `dev-docs/reference/UI-STYLING-GUIDE.md` - Theme token reference
- `dev-docs/reference/COMMON-ISSUES.md` - UI styling patterns
---
## Notes
- This is a straightforward CSS fix
- Should be quick to implement and test
- May uncover other hardcoded colors in popuplayer.css
- Consider fixing all hardcoded colors in that file while we're at it

View File

@@ -0,0 +1,421 @@
# SUBTASK-B: Debug & Fix Expression/Function Node Outputs
**Parent Task:** TASK-008
**Status:** 🔴 Not Started
**Priority:** CRITICAL
**Estimated Effort:** 3-5 hours
---
## Problem
Expression and Function nodes evaluate/run but don't send output data to connected downstream nodes, breaking core functionality.
---
## Affected Nodes
1. **Expression Node** (`packages/noodl-runtime/src/nodes/std-library/expression.js`)
2. **Function Node** (`packages/noodl-runtime/src/nodes/std-library/simplejavascript.js`)
Both nodes share similar output mechanisms, suggesting a common underlying issue.
---
## Investigation Strategy
This is a **debugging task** - the root cause is unknown. We'll use systematic investigation to narrow down the issue.
### Phase 1: Minimal Reproduction 🔍
Create the simplest possible test case:
1. **Expression Node Test:**
- Create Expression node with `1 + 1`
- Connect output to Text node
- Expected: Text shows "2"
- Actual: Text shows nothing or old value
2. **Function Node Test:**
- Create Function node with `Outputs.result = 42;`
- Connect output to Text node
- Expected: Text shows "42"
- Actual: Text shows nothing or old value
**Document:**
- Exact steps to reproduce
- Screenshots of node graph
- Console output
- Any error messages
---
### Phase 2: Add Debug Logging 🔬
Add strategic console.log statements to trace execution flow.
#### Expression Node Logging
**File:** `packages/noodl-runtime/src/nodes/std-library/expression.js`
**Location 1 - Input Change:**
```javascript
// Line ~50, in expression input set()
set: function (value) {
console.log('🟢 [Expression] Input changed:', value);
var internal = this._internal;
internal.currentExpression = functionPreamble + 'return (' + value + ');';
// ... rest of code
if (!this.isInputConnected('run')) this._scheduleEvaluateExpression();
}
```
**Location 2 - Schedule:**
```javascript
// Line ~220, _scheduleEvaluateExpression
_scheduleEvaluateExpression: {
value: function () {
console.log('🔵 [Expression] Schedule evaluation called');
var internal = this._internal;
if (internal.hasScheduledEvaluation === false) {
console.log('🔵 [Expression] Scheduling callback');
internal.hasScheduledEvaluation = true;
this.flagDirty();
this.scheduleAfterInputsHaveUpdated(function () {
console.log('🔥 [Expression] Callback FIRED');
var lastValue = internal.cachedValue;
internal.cachedValue = this._calculateExpression();
console.log('🔥 [Expression] Calculated:', internal.cachedValue, 'Previous:', lastValue);
if (lastValue !== internal.cachedValue) {
console.log('🚩 [Expression] Flagging outputs dirty');
this.flagOutputDirty('result');
this.flagOutputDirty('isTrue');
this.flagOutputDirty('isFalse');
}
if (internal.cachedValue) this.sendSignalOnOutput('isTrueEv');
else this.sendSignalOnOutput('isFalseEv');
internal.hasScheduledEvaluation = false;
});
} else {
console.log('⚠️ [Expression] Already scheduled, skipping');
}
}
}
```
**Location 3 - Output Getter:**
```javascript
// Line ~145, result output getter
result: {
group: 'Result',
type: '*',
displayName: 'Result',
getter: function () {
console.log('📤 [Expression] Result getter called, returning:', this._internal.cachedValue);
if (!this._internal.currentExpression) {
return 0;
}
return this._internal.cachedValue;
}
}
```
#### Function Node Logging
**File:** `packages/noodl-runtime/src/nodes/std-library/simplejavascript.js`
**Location 1 - Schedule:**
```javascript
// Line ~100, scheduleRun method
scheduleRun: function () {
console.log('🔵 [Function] Schedule run called');
if (this.runScheduled) {
console.log('⚠️ [Function] Already scheduled, skipping');
return;
}
this.runScheduled = true;
this.scheduleAfterInputsHaveUpdated(() => {
console.log('🔥 [Function] Callback FIRED');
this.runScheduled = false;
if (!this._deleted) {
this.runScript();
}
});
}
```
**Location 2 - Proxy:**
```javascript
// Line ~25, Proxy set trap
this._internal.outputValuesProxy = new Proxy(this._internal.outputValues, {
set: (obj, prop, value) => {
console.log('🔵 [Function] Proxy intercepted:', prop, '=', value);
//a function node can continue running after it has been deleted. E.g. with timeouts or event listeners that hasn't been removed.
//if the node is deleted, just do nothing
if (this._deleted) {
console.log('⚠️ [Function] Node deleted, ignoring output');
return;
}
//only send outputs when they change.
//Some Noodl projects rely on this behavior, so changing it breaks backwards compability
if (value !== this._internal.outputValues[prop]) {
console.log('🚩 [Function] Flagging output dirty:', 'out-' + prop);
this.registerOutputIfNeeded('out-' + prop);
this._internal.outputValues[prop] = value;
this.flagOutputDirty('out-' + prop);
} else {
console.log('⏭️ [Function] Output unchanged, skipping');
}
return true;
}
});
```
**Location 3 - Output Getter:**
```javascript
// Line ~185, getScriptOutputValue method
getScriptOutputValue: function (name) {
console.log('📤 [Function] Output getter called:', name, 'value:', this._internal.outputValues[name]);
if (this._isSignalType(name)) {
return undefined;
}
return this._internal.outputValues[name];
}
```
---
### Phase 3: Test with Logging 📊
1. Add all debug logging above
2. Run `npm run dev` to start editor
3. Create test nodes (Expression and Function)
4. Watch console output
5. Document what logs appear and what logs are missing
**Expected Log Flow (Expression):**
```
🟢 [Expression] Input changed: 1 + 1
🔵 [Expression] Schedule evaluation called
🔵 [Expression] Scheduling callback
🔥 [Expression] Callback FIRED
🔥 [Expression] Calculated: 2 Previous: 0
🚩 [Expression] Flagging outputs dirty
📤 [Expression] Result getter called, returning: 2
```
**Expected Log Flow (Function):**
```
🔵 [Function] Schedule run called
🔥 [Function] Callback FIRED
🔵 [Function] Proxy intercepted: result = 42
🚩 [Function] Flagging output dirty: out-result
📤 [Function] Output getter called: result value: 42
```
**If logs stop at certain point, that's where the bug is.**
---
### Phase 4: Narrow Down Root Cause 🎯
Based on Phase 3 findings, investigate specific areas:
#### Scenario A: Callback Never Fires
**Symptoms:**
- See "Schedule" logs but never see "Callback FIRED"
- `scheduleAfterInputsHaveUpdated()` not working
**Investigation:**
- Check `packages/noodl-runtime/src/node.js` - `scheduleAfterInputsHaveUpdated` implementation
- Verify `this._afterInputsHaveUpdatedCallbacks` array exists
- Check if `_performDirtyUpdate` is being called
- Look for React 19 related changes that might have broken scheduling
**Potential Fix:**
- Fix scheduling mechanism
- Ensure callbacks are executed properly
#### Scenario B: Outputs Flagged But Getters Not Called
**Symptoms:**
- See "Flagging outputs dirty" logs
- Never see "Output getter called" logs
- `flagOutputDirty()` works but doesn't trigger downstream updates
**Investigation:**
- Check base `Node` class `flagOutputDirty()` implementation
- Verify downstream nodes are checking for dirty outputs
- Check if connection system is broken
- Look for changes to output propagation mechanism
**Potential Fix:**
- Fix output propagation system
- Ensure getters are called when outputs are dirty
#### Scenario C: Context/Scope Missing
**Symptoms:**
- Expression compilation fails silently
- No errors in console but calculation returns 0 or undefined
**Investigation:**
- Add logging to check `this.context`
- Add logging to check `this.context.modelScope`
- Verify Noodl globals (Variables, Objects, Arrays) are accessible
**Potential Fix:**
- Ensure context is properly initialized
- Fix scope setup
#### Scenario D: Proxy Not Working (Function Only)
**Symptoms:**
- Function runs but Proxy set trap never fires
- Output assignments don't trigger updates
**Investigation:**
- Test if Proxy works in current Node.js version
- Check if `this._internal` exists when Proxy is created
- Verify Proxy is being used (not bypassed)
**Potential Fix:**
- Fix Proxy initialization
- Use alternative output mechanism if Proxy is broken
---
### Phase 5: Implement Fix 🔧
Once root cause is identified:
1. Implement targeted fix
2. Remove debug logging (or make conditional)
3. Test thoroughly
4. Document fix in INVESTIGATION.md
5. Add entry to LEARNINGS.md
---
## Success Criteria
- [ ] Expression nodes output correct values to connected nodes
- [ ] Function nodes output correct values to connected nodes
- [ ] Signal outputs work correctly
- [ ] Reactive updates work (expression updates when inputs change)
- [ ] No console errors during evaluation
- [ ] Downstream nodes receive and display outputs
- [ ] Existing projects using these nodes work correctly
---
## Testing Checklist
### Expression Node Tests
- [ ] Simple math: `1 + 1` outputs `2`
- [ ] With inputs: Connect Number node to `x`, expression `x * 2` outputs correct value
- [ ] With signals: Connect Run signal, expression evaluates on trigger
- [ ] With Noodl globals: `Variables.myVar` outputs correct value
- [ ] Signal outputs: `isTrueEv` fires when result is truthy
- [ ] Multiple connected outputs: Both `result` and `asString` work
### Function Node Tests
- [ ] Simple output: `Outputs.result = 42` outputs `42`
- [ ] Multiple outputs: Multiple `Outputs.x = ...` assignments all work
- [ ] Signal outputs: `Outputs.done.send()` triggers correctly
- [ ] With inputs: Access `Inputs.x` and output based on it
- [ ] Async functions: `async` functions work correctly
- [ ] Error handling: Errors don't crash editor, show in warnings
### Integration Tests
- [ ] Chain: Expression → Function → Text all work
- [ ] Multiple connections: One output connected to multiple inputs
- [ ] Reactive updates: Changing upstream input updates downstream
- [ ] Component boundary: Nodes work inside components
---
## Related Files
**Core:**
- `packages/noodl-runtime/src/node.js` - Base Node class
- `packages/noodl-runtime/src/nodecontext.js` - Node context/scope
- `packages/noodl-runtime/src/nodes/std-library/expression.js` - Expression node
- `packages/noodl-runtime/src/nodes/std-library/simplejavascript.js` - Function node
**Related Systems:**
- `packages/noodl-runtime/src/expression-evaluator.js` - Expression evaluation
- `packages/noodl-runtime/src/outputproperty.js` - Output handling
- `packages/noodl-runtime/src/nodegraphcontext.js` - Graph-level context
---
## Notes
- This is investigation-heavy - expect to spend time debugging
- Root cause may affect other node types
- May uncover deeper runtime issues
- Document all findings thoroughly
- Consider adding automated tests for these nodes once fixed
- If fix is complex, consider creating separate LEARNINGS entry
---
## Debugging Tips
**If stuck:**
1. Compare with a known-working node type (e.g., Number node)
2. Check git history for recent changes to affected files
3. Test in older version to see if regression
4. Ask Richard about recent runtime changes
5. Check if similar issues reported in GitHub issues
**Useful console commands:**
```javascript
// Get node instance
const node = window.Noodl.Graphs['Component'].nodes[0];
// Check outputs
node._internal.cachedValue;
node._internal.outputValues;
// Test flagging manually
node.flagOutputDirty('result');
// Check scheduling
node._afterInputsHaveUpdatedCallbacks;
```

View File

@@ -0,0 +1,844 @@
# Blockly Blocks Specification
This document defines the custom Blockly blocks for Noodl integration.
---
## Block Categories & Colors
| Category | Color (HSL Hue) | Description |
|----------|-----------------|-------------|
| Inputs/Outputs | 230 (Blue) | Node I/O |
| Variables | 330 (Pink) | Noodl.Variables |
| Objects | 20 (Orange) | Noodl.Objects |
| Arrays | 260 (Purple) | Noodl.Arrays |
| Events | 180 (Cyan) | Signals & triggers |
| Logic | 210 (Standard) | If/else, comparisons |
| Math | 230 (Standard) | Math operations |
| Text | 160 (Standard) | String operations |
---
## Inputs/Outputs Blocks
### noodl_define_input
Declares an input port on the node.
```javascript
// Block Definition
{
type: 'noodl_define_input',
message0: '📥 Define input %1 type %2',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myInput' },
{ type: 'field_dropdown', name: 'TYPE', options: [
['any', '*'],
['string', 'string'],
['number', 'number'],
['boolean', 'boolean'],
['object', 'object'],
['array', 'array']
]}
],
colour: 230,
tooltip: 'Defines an input port that appears on the node',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_define_input'] = function(block) {
// No runtime code - used for I/O detection only
return '';
};
```
### noodl_get_input
Gets a value from a node input.
```javascript
// Block Definition
{
type: 'noodl_get_input',
message0: '📥 get input %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'value' }
],
output: null, // Can connect to any type
colour: 230,
tooltip: 'Gets the value from an input port',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_input'] = function(block) {
var name = block.getFieldValue('NAME');
var code = 'Inputs["' + name + '"]';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
```
### noodl_define_output
Declares an output port on the node.
```javascript
// Block Definition
{
type: 'noodl_define_output',
message0: '📤 Define output %1 type %2',
args0: [
{ type: 'field_input', name: 'NAME', text: 'result' },
{ type: 'field_dropdown', name: 'TYPE', options: [
['any', '*'],
['string', 'string'],
['number', 'number'],
['boolean', 'boolean'],
['object', 'object'],
['array', 'array']
]}
],
colour: 230,
tooltip: 'Defines an output port that appears on the node',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_define_output'] = function(block) {
// No runtime code - used for I/O detection only
return '';
};
```
### noodl_set_output
Sets a value on a node output.
```javascript
// Block Definition
{
type: 'noodl_set_output',
message0: '📤 set output %1 to %2',
args0: [
{ type: 'field_input', name: 'NAME', text: 'result' },
{ type: 'input_value', name: 'VALUE' }
],
previousStatement: null,
nextStatement: null,
colour: 230,
tooltip: 'Sets the value of an output port',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_set_output'] = function(block) {
var name = block.getFieldValue('NAME');
var value = Blockly.JavaScript.valueToCode(block, 'VALUE',
Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
return 'Outputs["' + name + '"] = ' + value + ';\n';
};
```
### noodl_define_signal_input
Declares a signal input port.
```javascript
// Block Definition
{
type: 'noodl_define_signal_input',
message0: '⚡ Define signal input %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'trigger' }
],
colour: 180,
tooltip: 'Defines a signal input that can trigger logic',
helpUrl: ''
}
```
### noodl_define_signal_output
Declares a signal output port.
```javascript
// Block Definition
{
type: 'noodl_define_signal_output',
message0: '⚡ Define signal output %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'done' }
],
colour: 180,
tooltip: 'Defines a signal output that can trigger other nodes',
helpUrl: ''
}
```
### noodl_send_signal
Sends a signal output.
```javascript
// Block Definition
{
type: 'noodl_send_signal',
message0: '⚡ send signal %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'done' }
],
previousStatement: null,
nextStatement: null,
colour: 180,
tooltip: 'Sends a signal to connected nodes',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_send_signal'] = function(block) {
var name = block.getFieldValue('NAME');
return 'this.sendSignalOnOutput("' + name + '");\n';
};
```
---
## Variables Blocks
### noodl_get_variable
Gets a global variable value.
```javascript
// Block Definition
{
type: 'noodl_get_variable',
message0: '📖 get variable %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myVariable' }
],
output: null,
colour: 330,
tooltip: 'Gets the value of a global Noodl variable',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_variable'] = function(block) {
var name = block.getFieldValue('NAME');
var code = 'Noodl.Variables["' + name + '"]';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
```
### noodl_set_variable
Sets a global variable value.
```javascript
// Block Definition
{
type: 'noodl_set_variable',
message0: '✏️ set variable %1 to %2',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myVariable' },
{ type: 'input_value', name: 'VALUE' }
],
previousStatement: null,
nextStatement: null,
colour: 330,
tooltip: 'Sets the value of a global Noodl variable',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_set_variable'] = function(block) {
var name = block.getFieldValue('NAME');
var value = Blockly.JavaScript.valueToCode(block, 'VALUE',
Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
return 'Noodl.Variables["' + name + '"] = ' + value + ';\n';
};
```
---
## Objects Blocks
### noodl_get_object
Gets an object by ID.
```javascript
// Block Definition
{
type: 'noodl_get_object',
message0: '📦 get object %1',
args0: [
{ type: 'input_value', name: 'ID', check: 'String' }
],
output: 'Object',
colour: 20,
tooltip: 'Gets a Noodl Object by its ID',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_object'] = function(block) {
var id = Blockly.JavaScript.valueToCode(block, 'ID',
Blockly.JavaScript.ORDER_NONE) || '""';
var code = 'Noodl.Objects[' + id + ']';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
```
### noodl_get_object_property
Gets a property from an object.
```javascript
// Block Definition
{
type: 'noodl_get_object_property',
message0: '📖 get %1 from object %2',
args0: [
{ type: 'field_input', name: 'PROPERTY', text: 'name' },
{ type: 'input_value', name: 'OBJECT' }
],
output: null,
colour: 20,
tooltip: 'Gets a property value from an object',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_object_property'] = function(block) {
var property = block.getFieldValue('PROPERTY');
var object = Blockly.JavaScript.valueToCode(block, 'OBJECT',
Blockly.JavaScript.ORDER_MEMBER) || '{}';
var code = object + '["' + property + '"]';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
```
### noodl_set_object_property
Sets a property on an object.
```javascript
// Block Definition
{
type: 'noodl_set_object_property',
message0: '✏️ set %1 on object %2 to %3',
args0: [
{ type: 'field_input', name: 'PROPERTY', text: 'name' },
{ type: 'input_value', name: 'OBJECT' },
{ type: 'input_value', name: 'VALUE' }
],
previousStatement: null,
nextStatement: null,
colour: 20,
tooltip: 'Sets a property value on an object',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_set_object_property'] = function(block) {
var property = block.getFieldValue('PROPERTY');
var object = Blockly.JavaScript.valueToCode(block, 'OBJECT',
Blockly.JavaScript.ORDER_MEMBER) || '{}';
var value = Blockly.JavaScript.valueToCode(block, 'VALUE',
Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
return object + '["' + property + '"] = ' + value + ';\n';
};
```
### noodl_create_object
Creates a new object.
```javascript
// Block Definition
{
type: 'noodl_create_object',
message0: ' create object with ID %1',
args0: [
{ type: 'input_value', name: 'ID', check: 'String' }
],
output: 'Object',
colour: 20,
tooltip: 'Creates a new Noodl Object with the given ID',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_create_object'] = function(block) {
var id = Blockly.JavaScript.valueToCode(block, 'ID',
Blockly.JavaScript.ORDER_NONE) || 'Noodl.Object.guid()';
var code = 'Noodl.Object.create(' + id + ')';
return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL];
};
```
---
## Arrays Blocks
### noodl_get_array
Gets an array by name.
```javascript
// Block Definition
{
type: 'noodl_get_array',
message0: '📋 get array %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myArray' }
],
output: 'Array',
colour: 260,
tooltip: 'Gets a Noodl Array by name',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_array'] = function(block) {
var name = block.getFieldValue('NAME');
var code = 'Noodl.Arrays["' + name + '"]';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
```
### noodl_array_add
Adds an item to an array.
```javascript
// Block Definition
{
type: 'noodl_array_add',
message0: ' add %1 to array %2',
args0: [
{ type: 'input_value', name: 'ITEM' },
{ type: 'input_value', name: 'ARRAY', check: 'Array' }
],
previousStatement: null,
nextStatement: null,
colour: 260,
tooltip: 'Adds an item to the end of an array',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_array_add'] = function(block) {
var item = Blockly.JavaScript.valueToCode(block, 'ITEM',
Blockly.JavaScript.ORDER_NONE) || 'null';
var array = Blockly.JavaScript.valueToCode(block, 'ARRAY',
Blockly.JavaScript.ORDER_MEMBER) || '[]';
return array + '.push(' + item + ');\n';
};
```
### noodl_array_remove
Removes an item from an array.
```javascript
// Block Definition
{
type: 'noodl_array_remove',
message0: ' remove %1 from array %2',
args0: [
{ type: 'input_value', name: 'ITEM' },
{ type: 'input_value', name: 'ARRAY', check: 'Array' }
],
previousStatement: null,
nextStatement: null,
colour: 260,
tooltip: 'Removes an item from an array',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_array_remove'] = function(block) {
var item = Blockly.JavaScript.valueToCode(block, 'ITEM',
Blockly.JavaScript.ORDER_NONE) || 'null';
var array = Blockly.JavaScript.valueToCode(block, 'ARRAY',
Blockly.JavaScript.ORDER_MEMBER) || '[]';
return array + '.splice(' + array + '.indexOf(' + item + '), 1);\n';
};
```
### noodl_array_length
Gets the length of an array.
```javascript
// Block Definition
{
type: 'noodl_array_length',
message0: '🔢 length of array %1',
args0: [
{ type: 'input_value', name: 'ARRAY', check: 'Array' }
],
output: 'Number',
colour: 260,
tooltip: 'Gets the number of items in an array',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_array_length'] = function(block) {
var array = Blockly.JavaScript.valueToCode(block, 'ARRAY',
Blockly.JavaScript.ORDER_MEMBER) || '[]';
var code = array + '.length';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
```
### noodl_array_foreach
Loops over array items.
```javascript
// Block Definition
{
type: 'noodl_array_foreach',
message0: '🔄 for each %1 in %2',
args0: [
{ type: 'field_variable', name: 'VAR', variable: 'item' },
{ type: 'input_value', name: 'ARRAY', check: 'Array' }
],
message1: 'do %1',
args1: [
{ type: 'input_statement', name: 'DO' }
],
previousStatement: null,
nextStatement: null,
colour: 260,
tooltip: 'Executes code for each item in the array',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_array_foreach'] = function(block) {
var variable = Blockly.JavaScript.nameDB_.getName(
block.getFieldValue('VAR'), Blockly.VARIABLE_CATEGORY_NAME);
var array = Blockly.JavaScript.valueToCode(block, 'ARRAY',
Blockly.JavaScript.ORDER_MEMBER) || '[]';
var statements = Blockly.JavaScript.statementToCode(block, 'DO');
return 'for (var ' + variable + ' of ' + array + ') {\n' +
statements + '}\n';
};
```
---
## Event Blocks
### noodl_on_signal
Event handler for when a signal input is triggered.
```javascript
// Block Definition
{
type: 'noodl_on_signal',
message0: '⚡ when %1 is triggered',
args0: [
{ type: 'field_input', name: 'SIGNAL', text: 'trigger' }
],
message1: 'do %1',
args1: [
{ type: 'input_statement', name: 'DO' }
],
colour: 180,
tooltip: 'Runs code when the signal input is triggered',
helpUrl: ''
}
// Generator - This is a special case, generates a handler function
Blockly.JavaScript['noodl_on_signal'] = function(block) {
var signal = block.getFieldValue('SIGNAL');
var statements = Blockly.JavaScript.statementToCode(block, 'DO');
// This generates a named handler that the runtime will call
return '// Handler for signal: ' + signal + '\n' +
'function _onSignal_' + signal + '() {\n' +
statements +
'}\n';
};
```
### noodl_on_variable_change
Event handler for when a variable changes.
```javascript
// Block Definition
{
type: 'noodl_on_variable_change',
message0: '👁️ when variable %1 changes',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myVariable' }
],
message1: 'do %1',
args1: [
{ type: 'input_statement', name: 'DO' }
],
colour: 330,
tooltip: 'Runs code when the variable value changes',
helpUrl: ''
}
```
---
## I/O Detection Algorithm
```typescript
interface DetectedIO {
inputs: Array<{ name: string; type: string }>;
outputs: Array<{ name: string; type: string }>;
signalInputs: string[];
signalOutputs: string[];
}
function detectIO(workspace: Blockly.Workspace): DetectedIO {
const result: DetectedIO = {
inputs: [],
outputs: [],
signalInputs: [],
signalOutputs: []
};
const blocks = workspace.getAllBlocks(false);
for (const block of blocks) {
switch (block.type) {
case 'noodl_define_input':
result.inputs.push({
name: block.getFieldValue('NAME'),
type: block.getFieldValue('TYPE')
});
break;
case 'noodl_get_input':
// Auto-detect from usage if not explicitly defined
const inputName = block.getFieldValue('NAME');
if (!result.inputs.find(i => i.name === inputName)) {
result.inputs.push({ name: inputName, type: '*' });
}
break;
case 'noodl_define_output':
result.outputs.push({
name: block.getFieldValue('NAME'),
type: block.getFieldValue('TYPE')
});
break;
case 'noodl_set_output':
// Auto-detect from usage
const outputName = block.getFieldValue('NAME');
if (!result.outputs.find(o => o.name === outputName)) {
result.outputs.push({ name: outputName, type: '*' });
}
break;
case 'noodl_define_signal_input':
case 'noodl_on_signal':
const sigIn = block.getFieldValue('SIGNAL') || block.getFieldValue('NAME');
if (!result.signalInputs.includes(sigIn)) {
result.signalInputs.push(sigIn);
}
break;
case 'noodl_define_signal_output':
case 'noodl_send_signal':
const sigOut = block.getFieldValue('NAME');
if (!result.signalOutputs.includes(sigOut)) {
result.signalOutputs.push(sigOut);
}
break;
}
}
return result;
}
```
---
## Toolbox Configuration
```javascript
const LOGIC_BUILDER_TOOLBOX = {
kind: 'categoryToolbox',
contents: [
{
kind: 'category',
name: 'Inputs/Outputs',
colour: 230,
contents: [
{ kind: 'block', type: 'noodl_define_input' },
{ kind: 'block', type: 'noodl_get_input' },
{ kind: 'block', type: 'noodl_define_output' },
{ kind: 'block', type: 'noodl_set_output' },
{ kind: 'block', type: 'noodl_define_signal_input' },
{ kind: 'block', type: 'noodl_define_signal_output' },
{ kind: 'block', type: 'noodl_send_signal' }
]
},
{
kind: 'category',
name: 'Events',
colour: 180,
contents: [
{ kind: 'block', type: 'noodl_on_signal' },
{ kind: 'block', type: 'noodl_on_variable_change' }
]
},
{
kind: 'category',
name: 'Variables',
colour: 330,
contents: [
{ kind: 'block', type: 'noodl_get_variable' },
{ kind: 'block', type: 'noodl_set_variable' }
]
},
{
kind: 'category',
name: 'Objects',
colour: 20,
contents: [
{ kind: 'block', type: 'noodl_get_object' },
{ kind: 'block', type: 'noodl_get_object_property' },
{ kind: 'block', type: 'noodl_set_object_property' },
{ kind: 'block', type: 'noodl_create_object' }
]
},
{
kind: 'category',
name: 'Arrays',
colour: 260,
contents: [
{ kind: 'block', type: 'noodl_get_array' },
{ kind: 'block', type: 'noodl_array_add' },
{ kind: 'block', type: 'noodl_array_remove' },
{ kind: 'block', type: 'noodl_array_length' },
{ kind: 'block', type: 'noodl_array_foreach' }
]
},
{ kind: 'sep' },
{
kind: 'category',
name: 'Logic',
colour: 210,
contents: [
{ kind: 'block', type: 'controls_if' },
{ kind: 'block', type: 'logic_compare' },
{ kind: 'block', type: 'logic_operation' },
{ kind: 'block', type: 'logic_negate' },
{ kind: 'block', type: 'logic_boolean' },
{ kind: 'block', type: 'logic_ternary' }
]
},
{
kind: 'category',
name: 'Loops',
colour: 120,
contents: [
{ kind: 'block', type: 'controls_repeat_ext' },
{ kind: 'block', type: 'controls_whileUntil' },
{ kind: 'block', type: 'controls_for' },
{ kind: 'block', type: 'controls_flow_statements' }
]
},
{
kind: 'category',
name: 'Math',
colour: 230,
contents: [
{ kind: 'block', type: 'math_number' },
{ kind: 'block', type: 'math_arithmetic' },
{ kind: 'block', type: 'math_single' },
{ kind: 'block', type: 'math_round' },
{ kind: 'block', type: 'math_modulo' },
{ kind: 'block', type: 'math_random_int' }
]
},
{
kind: 'category',
name: 'Text',
colour: 160,
contents: [
{ kind: 'block', type: 'text' },
{ kind: 'block', type: 'text_join' },
{ kind: 'block', type: 'text_length' },
{ kind: 'block', type: 'text_isEmpty' },
{ kind: 'block', type: 'text_indexOf' },
{ kind: 'block', type: 'text_charAt' }
]
}
]
};
// Simplified toolbox for Expression Builder
const EXPRESSION_BUILDER_TOOLBOX = {
kind: 'categoryToolbox',
contents: [
{
kind: 'category',
name: 'Inputs',
colour: 230,
contents: [
{ kind: 'block', type: 'noodl_define_input' },
{ kind: 'block', type: 'noodl_get_input' }
]
},
{
kind: 'category',
name: 'Variables',
colour: 330,
contents: [
{ kind: 'block', type: 'noodl_get_variable' }
]
},
{
kind: 'category',
name: 'Logic',
colour: 210,
contents: [
{ kind: 'block', type: 'logic_compare' },
{ kind: 'block', type: 'logic_operation' },
{ kind: 'block', type: 'logic_negate' },
{ kind: 'block', type: 'logic_boolean' },
{ kind: 'block', type: 'logic_ternary' }
]
},
{
kind: 'category',
name: 'Math',
colour: 230,
contents: [
{ kind: 'block', type: 'math_number' },
{ kind: 'block', type: 'math_arithmetic' },
{ kind: 'block', type: 'math_single' },
{ kind: 'block', type: 'math_round' }
]
},
{
kind: 'category',
name: 'Text',
colour: 160,
contents: [
{ kind: 'block', type: 'text' },
{ kind: 'block', type: 'text_join' },
{ kind: 'block', type: 'text_length' }
]
}
]
};
```

View File

@@ -0,0 +1,53 @@
# TASK-012 Changelog
Track all changes made during implementation.
---
## [Unreleased]
### Added
- Initial task documentation (README.md, CHECKLIST.md, BLOCKS-SPEC.md)
### Changed
- (none yet)
### Fixed
- (none yet)
### Removed
- (none yet)
---
## Session Log
### Session 1: [Date]
**Duration:** X hours
**Changes:**
-
**Files Modified:**
-
**Notes:**
-
---
### Session 2: [Date]
**Duration:** X hours
**Changes:**
-
**Files Modified:**
-
**Notes:**
-
---
(Continue adding sessions as work progresses)

View File

@@ -0,0 +1,276 @@
# TASK-012 Implementation Checklist
## Prerequisites
- [ ] Read README.md completely
- [ ] Review existing Function node implementation (`javascriptfunction.js`)
- [ ] Review existing Expression node implementation (`expression.js`)
- [ ] Understand Noodl's signal/output pattern
- [ ] Create branch: `git checkout -b task/012-blockly-logic-builder`
- [ ] Verify build works: `npm run dev`
---
## Phase A: Foundation (Week 1)
### A1: Install and Configure Blockly
- [ ] Add Blockly to package.json
```bash
cd packages/noodl-editor
npm install blockly
```
- [ ] Verify Blockly types are available
- [ ] Create basic test component
- [ ] Create `src/editor/src/views/BlocklyEditor/` directory
- [ ] Create `BlocklyWorkspace.tsx` - minimal React wrapper
- [ ] Render basic workspace with default toolbox
- [ ] Verify it displays in a test location
### A2: Create Basic Custom Blocks
- [ ] Create `NoodlBlocks.ts` - block definitions
- [ ] `noodl_get_input` block
- [ ] `noodl_set_output` block
- [ ] `noodl_get_variable` block
- [ ] `noodl_set_variable` block
- [ ] Create `NoodlGenerators.ts` - JavaScript generators
- [ ] Generator for `noodl_get_input` → `Inputs.name`
- [ ] Generator for `noodl_set_output` → `Outputs.name = value`
- [ ] Generator for `noodl_get_variable` → `Noodl.Variables.name`
- [ ] Generator for `noodl_set_variable` → `Noodl.Variables.name = value`
- [ ] Verify generated code in console
### A3: Storage Mechanism
- [ ] Implement workspace serialization
- [ ] `workspaceToJson()` function
- [ ] `jsonToWorkspace()` function
- [ ] Test round-trip: create blocks → serialize → deserialize → verify same blocks
- [ ] Document in NOTES.md
**Checkpoint A:** Basic Blockly renders, custom blocks work, serialization works
---
## Phase B: Logic Builder Node (Week 2)
### B1: Node Definition
- [ ] Create `logic-builder.js` in `packages/noodl-runtime/src/nodes/std-library/`
- [ ] Define node structure:
```javascript
name: 'noodl.logic.LogicBuilder',
displayNodeName: 'Logic Builder',
category: 'Logic',
color: 'logic'
```
- [ ] Add `blocklyWorkspace` parameter (string, stores JSON)
- [ ] Add `_internal` for code execution state
- [ ] Register in `nodelibraryexport.js`
- [ ] Verify node appears in node picker
### B2: Dynamic Port Registration
- [ ] Create `IODetector.ts` - parses workspace for I/O blocks
- [ ] `detectInputs(workspace)` → `[{name, type}]`
- [ ] `detectOutputs(workspace)` → `[{name, type}]`
- [ ] `detectSignalInputs(workspace)` → `[name]`
- [ ] `detectSignalOutputs(workspace)` → `[name]`
- [ ] Implement `registerInputIfNeeded()` in node
- [ ] Implement `updatePorts()` in setup function
- [ ] Test: add Input block → port appears on node
### B3: Code Execution
- [ ] Generate complete function from workspace
- [ ] Create execution context with Noodl API access
- [ ] Wire signal inputs to trigger execution
- [ ] Wire outputs to flag dirty and update
- [ ] Test: simple input → output flow
### B4: Editor Integration (Modal)
- [ ] Create property panel button "Edit Logic Blocks"
- [ ] Create modal component `LogicBuilderModal.tsx`
- [ ] Load workspace from node parameter
- [ ] Save workspace on close
- [ ] Wire up to property panel
**Checkpoint B:** Logic Builder node works end-to-end with modal editor
---
## Phase C: Tabbed Canvas System (Week 3)
### C1: Tab Infrastructure
- [ ] Create `CanvasTabs.tsx` component
- [ ] Define tab state interface:
```typescript
interface CanvasTab {
id: string;
type: 'canvas' | 'logic-builder';
nodeId?: string;
nodeName?: string;
}
```
- [ ] Create tab context/store
- [ ] Integrate with NodeGraphEditor container
### C2: Tab Behavior
- [ ] "Canvas" tab always present (index 0)
- [ ] "Edit Logic Blocks" opens new tab
- [ ] Tab title = node display name
- [ ] Close button on Logic Builder tabs
- [ ] Clicking tab switches view
- [ ] Track component scope - reset tabs on component change
### C3: Workspace in Tab
- [ ] Render Blockly workspace in tab content area
- [ ] Maintain workspace state per tab
- [ ] Handle resize when tab dimensions change
- [ ] Auto-save workspace changes (debounced)
### C4: Polish
- [ ] Tab styling consistent with editor theme
- [ ] Unsaved changes indicator (dot on tab)
- [ ] Keyboard shortcut: Escape closes tab (returns to canvas)
- [ ] Smooth transitions between tabs
**Checkpoint C:** Tabbed editing experience works smoothly
---
## Phase D: Expression Builder Node (Week 4)
### D1: Simplified Workspace Configuration
- [ ] Create `ExpressionBuilderToolbox.ts` - limited block set
- [ ] Math blocks only
- [ ] Logic/comparison blocks
- [ ] Text blocks
- [ ] Variable get (no set)
- [ ] Input get only
- [ ] NO signal blocks
- [ ] NO event blocks
- [ ] Single "result" output (auto-generated)
### D2: Node Definition
- [ ] Create `expression-builder.js`
- [ ] Single output: `result` type `*`
- [ ] Inputs auto-detected from "Get Input" blocks
- [ ] Expression evaluated on any input change
### D3: Inline/Small Modal Editor
- [ ] Compact Blockly workspace
- [ ] Horizontal layout if possible
- [ ] Or small modal (not full tab)
- [ ] Quick open/close behavior
### D4: Type Inference
- [ ] Detect result type from blocks
- [ ] Provide typed outputs: `asString`, `asNumber`, `asBoolean`
- [ ] Match Expression node pattern
**Checkpoint D:** Expression Builder provides quick visual expressions
---
## Phase E: Full Block Library & Polish (Weeks 5-6)
### E1: Complete Tier 1 Blocks
#### Objects Blocks
- [ ] `noodl_get_object` - Get Object by ID
- [ ] `noodl_get_object_property` - Get property from object
- [ ] `noodl_set_object_property` - Set property on object
- [ ] `noodl_create_object` - Create new object with ID
- [ ] `noodl_on_object_change` - Event: when object changes
#### Arrays Blocks
- [ ] `noodl_get_array` - Get Array by name
- [ ] `noodl_array_add` - Add item to array
- [ ] `noodl_array_remove` - Remove item from array
- [ ] `noodl_array_length` - Get array length
- [ ] `noodl_array_foreach` - Loop over array
- [ ] `noodl_on_array_change` - Event: when array changes
#### Event/Signal Blocks
- [ ] `noodl_on_signal` - When signal input triggered
- [ ] `noodl_send_signal` - Send signal output
- [ ] `noodl_define_signal_input` - Declare signal input
- [ ] `noodl_define_signal_output` - Declare signal output
### E2: Code Viewer
- [ ] Add "View Code" button to I/O summary panel
- [ ] Create `CodeViewer.tsx` component
- [ ] Display generated JavaScript
- [ ] Read-only (not editable)
- [ ] Syntax highlighting (monaco-editor or prism)
- [ ] Collapsible panel
### E3: Rename Existing Nodes
- [ ] `expression.js` → displayName "JavaScript Expression"
- [ ] `javascriptfunction.js` → displayName "JavaScript Function"
- [ ] Verify no breaking changes to existing projects
- [ ] Update node picker categories/search tags
### E4: Testing
- [ ] Unit tests for each block's code generation
- [ ] Unit tests for I/O detection
- [ ] Integration test: Logic Builder with Variables
- [ ] Integration test: Logic Builder with Objects
- [ ] Integration test: Logic Builder with Arrays
- [ ] Integration test: Signal flow
- [ ] Manual test checklist (see README.md)
### E5: Documentation
- [ ] User documentation: "Visual Logic with Logic Builder"
- [ ] User documentation: "Quick Expressions with Expression Builder"
- [ ] Update node reference docs
- [ ] Add tooltips/help text to blocks
**Checkpoint E:** Feature complete, tested, documented
---
## Final Review
- [ ] All success criteria from README met
- [ ] No TypeScript errors
- [ ] No console warnings/errors
- [ ] Performance acceptable (no lag with 50+ blocks)
- [ ] Works in deployed preview
- [ ] Code review completed
- [ ] PR ready for merge
---
## Session Tracking
Use this section to track progress across development sessions:
### Session 1: [Date]
- Started:
- Completed:
- Blockers:
- Next:
### Session 2: [Date]
- Started:
- Completed:
- Blockers:
- Next:
(Continue as needed)

View File

@@ -0,0 +1,114 @@
# TASK-012 Working Notes
Use this file to capture discoveries, decisions, and research during implementation.
---
## Research Notes
### Blockly Documentation References
- [Getting Started](https://developers.google.com/blockly/guides/get-started)
- [Custom Blocks](https://developers.google.com/blockly/guides/create-custom-blocks/overview)
- [Code Generators](https://developers.google.com/blockly/guides/create-custom-blocks/generating-code)
- [Toolbox Configuration](https://developers.google.com/blockly/guides/configure/web/toolbox)
- [Workspace Serialization](https://developers.google.com/blockly/guides/configure/web/serialization)
### Key Blockly Concepts
- **Workspace**: The canvas where blocks are placed
- **Toolbox**: The sidebar menu of available blocks
- **Block Definition**: JSON or JS object defining block appearance and connections
- **Generator**: Function that converts block to code
- **Mutator**: Dynamic block that can change shape (e.g., if/elseif/else)
### Blockly React Integration
Options:
1. **@blockly/react** - Official React wrapper (may have limitations)
2. **Direct integration** - Use Blockly.inject() in useEffect
Research needed: Which approach works better with our build system?
---
## Design Decisions
### Decision 1: [Topic]
**Date:**
**Context:**
**Options:**
1.
2.
**Decision:**
**Rationale:**
---
### Decision 2: [Topic]
**Date:**
**Context:**
**Options:**
1.
2.
**Decision:**
**Rationale:**
---
## Technical Discoveries
### Discovery 1: [Topic]
**Date:**
**Finding:**
**Impact:**
---
## Questions to Resolve
- [ ] Q1:
- [ ] Q2:
- [ ] Q3:
---
## Code Snippets & Patterns
### Pattern: [Name]
```javascript
// Code here
```
---
## Related Files in Codebase
Files to study:
- `packages/noodl-runtime/src/nodes/std-library/javascriptfunction.js` - Function node pattern
- `packages/noodl-runtime/src/nodes/std-library/expression.js` - Expression node pattern
- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/` - Property panel patterns
---
## Meeting Notes / Discussions
### [Date]: [Topic]
**Participants:**
**Summary:**
**Action Items:**
---
## Open Issues
1. **Issue:**
**Status:**
**Notes:**
2. **Issue:**
**Status:**
**Notes:**

View File

@@ -0,0 +1,519 @@
# TASK-012: Blockly Visual Logic Integration
## Metadata
| Field | Value |
|-------|-------|
| **ID** | TASK-012 |
| **Phase** | Phase 3 (Editor UX Overhaul) |
| **Priority** | 🟠 High |
| **Difficulty** | 🔴 Hard |
| **Estimated Time** | 4-6 weeks |
| **Prerequisites** | TASK-006 (Expression Overhaul) recommended but not blocking |
| **Branch** | `task/012-blockly-logic-builder` |
---
## Objective
Integrate Google Blockly into Nodegx to provide visual block-based programming as a bridge between nocode nodes and JavaScript, enabling users to build complex logic without writing code.
---
## Background
### The "JavaScript Cliff" Problem
Nodegx inherits Noodl's powerful but intimidating transition from visual nodes to code:
```
NoCode Zone JS Zone
───────────── ────────
Visual nodes ─────[CLIFF]─────► Expression/Function nodes
Condition node Noodl.Variables.isLoggedIn ? x : y
Boolean node Inputs.a + Inputs.b
String Format Outputs.result = computation
```
Current observations from coaching Noodl users:
- The built-in nocode nodes become limited quickly
- Teaching customization often requires saying "actually an expression would be better here"
- Most people resist dipping into JavaScript - it's a significant turnoff
- The original creators imagined users would be tempted into JS gradually, but this rarely happens
### The Blockly Solution
Blockly provides visual block-based programming that:
- Eliminates syntax anxiety (no semicolons, parentheses, typos)
- Makes logic tangible and manipulable
- Generates real JavaScript that curious users can inspect
- Has proven success (Scratch, Code.org, MakeCode, MIT App Inventor)
This is similar to our JSON editor approach: visual nocode option available, with code view for the curious.
### Why Blockly?
Research confirms Blockly is the right choice:
- **Industry standard**: Powers Scratch 3.0, Code.org, Microsoft MakeCode, MIT App Inventor
- **Active development**: Transitioned to Raspberry Pi Foundation (November 2025) ensuring education-focused stewardship
- **Mature library**: 13+ years of development, extensive documentation
- **Embeddable**: 100% client-side, ~500KB, no server dependencies
- **Customizable**: Full control over toolbox, blocks, and code generation
- **No real alternatives**: Other "alternatives" are either built on Blockly or complete platforms (not embeddable libraries)
---
## Current State
### Existing Code Nodes
| Node | Purpose | Limitation |
|------|---------|------------|
| **Expression** | Single expression evaluation | Requires JS syntax knowledge |
| **Function** | Multi-line JavaScript | Full JS required |
| **Script** | External script loading | Advanced use case |
### User Pain Points
1. **Backend integration barrier**: "How do I hook up my backend?" often requires Function nodes
2. **Conditional logic complexity**: Even simple if/else requires Expression node JS
3. **Data transformation**: Mapping/filtering arrays requires JS knowledge
4. **No gradual learning path**: Jump from visual to text is too steep
---
## Desired State
Two new node types that provide visual block-based logic:
### 1. Logic Builder Node
Full-featured Blockly workspace for complex, event-driven logic:
```
┌─────────────────────────────────────┐
│ Logic Builder: "ProcessOrder" │
│ │
│ ○ orderData result ○ │
│ ○ userInfo error ○ │
│ ⚡ process ⚡ success │
│ ⚡ failure │
│ │
│ [Edit Logic Blocks] │
└─────────────────────────────────────┘
```
- Multiple inputs and outputs (data and signals)
- Event-driven logic (when signal triggered, do X)
- Full Noodl API access (Variables, Objects, Arrays, Records)
- Tabbed editing experience in node canvas
### 2. Expression Builder Node
Simplified Blockly for single-value expressions:
```
┌───────────────────────────────────────────┐
│ Expression Builder │
├───────────────────────────────────────────┤
│ ○ price result ○ │
│ ○ quantity │
│ ○ discount │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ [price] × [quantity] × (1 - [disc]) │ │
│ └─────────────────────────────────────┘ │
└───────────────────────────────────────────┘
```
- Single result output
- Inline or small modal editor
- Perfect for computed values, conditionals, formatting
### Node Naming Distinction
To help users choose the right node:
| Node | Mental Model | Subtitle/Description | Icon |
|------|--------------|---------------------|------|
| **Logic Builder** | "Do things when stuff happens" | *"Build event-driven logic visually"* | ⚡ or flowchart |
| **Expression Builder** | "Calculate something" | *"Combine values visually"* | `f(x)` or calculator |
### Existing Node Renaming
For clarity, rename existing code nodes:
| Current Name | New Name |
|--------------|----------|
| Expression | **JavaScript Expression** |
| Function | **JavaScript Function** |
| Script | **JavaScript Script** |
---
## Scope
### In Scope
- [ ] Logic Builder node with full Blockly workspace
- [ ] Expression Builder node with simplified Blockly
- [ ] Tabbed canvas system for Logic Builder editing
- [ ] Custom Noodl block categories (Variables, Objects, Arrays, I/O)
- [ ] Auto-detection of inputs/outputs from blocks
- [ ] I/O summary panel
- [ ] Hidden "View Code" button (read-only JS output)
- [ ] Blockly workspace persistence as node parameter
- [ ] Rename existing Expression/Function/Script to "JavaScript X"
### Out of Scope (Future Phases)
- Records/BYOB blocks (requires Phase 5 BYOB completion)
- Navigation blocks
- Users/Auth blocks
- Cloud Functions blocks
- AI-assisted block suggestions
- Block-to-code learning mode
---
## Technical Approach
### Architecture Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ LOGIC BUILDER NODE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Blockly Workspace │ │
│ │ (Custom toolbox with Noodl categories) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Code Generator │ │
│ │ Blockly → JavaScript with Noodl context │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────┐ │ ┌─────────────────────────┐ │
│ │ I/O Detector │◄──────┴───────►│ Generated JS (hidden) │ │
│ │ (auto-ports) │ │ [View Code] button │ │
│ └──────────────────┘ └─────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Node Port Registration │ │
│ │ Dynamic inputs/outputs based on detected blocks │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### Tabbed Canvas System
When opening a Logic Builder node for editing:
```
┌─────────────────────────────────────────────────────────────────────┐
│ [Canvas] [ProcessOrder ×] [ValidateUser ×] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Blockly Workspace │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ when process │ │ set result │ │ │
│ │ │ is triggered │────►│ to [value] │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ I/O Summary ─────────────────┐ ┌─ View Code (read-only) ──┐ │
│ │ Inputs: orderData, userInfo │ │ function execute() { │ │
│ │ Outputs: result, error │ │ if (Inputs.orderData) │ │
│ │ Signals: process → success │ │ ... │ │
│ └───────────────────────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
**Tab behavior:**
- Clicking "Edit Logic Blocks" opens a new tab named after the node
- Canvas tab always available to flip back
- Tabs reset when leaving component view
- Multiple Logic Builder nodes can be open simultaneously
### Custom Blockly Blocks - Tier 1 (This Task)
```
INPUTS/OUTPUTS
├── 📥 Get Input [name ▼]
├── 📥 Define Input [name] type [type ▼]
├── 📤 Set Output [name ▼] to [value]
├── 📤 Define Output [name] type [type ▼]
└── ⚡ Send Signal [name ▼]
└── ⚡ Define Signal Input [name]
└── ⚡ Define Signal Output [name]
VARIABLES (Noodl.Variables)
├── 📖 Get Variable [name]
├── ✏️ Set Variable [name] to [value]
└── 👁️ When Variable [name] changes
OBJECTS (Noodl.Objects / Noodl.Model)
├── 📖 Get Object [id]
├── 📖 Get Object [id] property [prop]
├── ✏️ Set Object [id] property [prop] to [value]
├── Create Object with ID [id]
└── 👁️ When Object [id] changes
ARRAYS (Noodl.Arrays / Noodl.Collection)
├── 📋 Get Array [name]
├── Add [item] to Array [name]
├── Remove [item] from Array [name]
├── 🔢 Array [name] length
├── 🔄 For each [item] in Array [name]
└── 👁️ When Array [name] changes
LOGIC (Standard Blockly)
├── if / else if / else
├── comparison (=, ≠, <, >, ≤, ≥)
├── boolean (and, or, not)
├── loops (repeat, while, for each)
└── math operations
TEXT (Standard Blockly)
├── text join
├── text length
├── text contains
└── text substring
EVENTS
├── ⚡ When [signal input ▼] is triggered
└── ⚡ Then send [signal output ▼]
```
### Future Block Categories (Post-BYOB)
```
RECORDS (Phase 5+ after BYOB)
├── 🔍 Query [collection] where [filter]
├── Create Record in [collection]
├── ✏️ Update Record [id] in [collection]
├── 🗑️ Delete Record [id]
└── 🔢 Count Records in [collection]
NAVIGATION (Future)
├── 🧭 Navigate to [page]
├── 🔗 Navigate to path [/path]
└── 📦 Show Popup [component]
CONFIG (When Config node complete)
├── ⚙️ Get Config [key]
└── 🔒 Get Secret [key]
```
### Key Files to Modify
| File | Changes |
|------|---------|
| `packages/noodl-editor/package.json` | Add `blockly` dependency |
| `packages/noodl-runtime/src/nodelibraryexport.js` | Register new nodes |
| `packages/noodl-runtime/src/nodes/std-library/expression.js` | Rename to "JavaScript Expression" |
| `packages/noodl-runtime/src/nodes/std-library/javascriptfunction.js` | Rename to "JavaScript Function" |
### New Files to Create
| File | Purpose |
|------|---------|
| `packages/noodl-editor/src/editor/src/views/BlocklyEditor/` | Blockly workspace React component |
| `packages/noodl-editor/src/editor/src/views/BlocklyEditor/BlocklyWorkspace.tsx` | Main workspace component |
| `packages/noodl-editor/src/editor/src/views/BlocklyEditor/NoodlBlocks.ts` | Custom block definitions |
| `packages/noodl-editor/src/editor/src/views/BlocklyEditor/NoodlGenerators.ts` | JavaScript code generators |
| `packages/noodl-editor/src/editor/src/views/BlocklyEditor/BlocklyToolbox.ts` | Toolbox configuration |
| `packages/noodl-editor/src/editor/src/views/BlocklyEditor/IODetector.ts` | Auto-detect I/O from blocks |
| `packages/noodl-runtime/src/nodes/std-library/logic-builder.js` | Logic Builder node definition |
| `packages/noodl-runtime/src/nodes/std-library/expression-builder.js` | Expression Builder node definition |
| `packages/noodl-editor/src/editor/src/views/nodegrapheditor/CanvasTabs.tsx` | Tab system for canvas |
### Dependencies
- `blockly` npm package (~500KB)
- No server-side dependencies
---
## Implementation Plan
### Phase A: Foundation (Week 1)
1. **Install and configure Blockly**
- Add to package.json
- Create basic React wrapper component
- Verify rendering in editor
2. **Create basic custom blocks**
- Input/Output blocks
- Variable get/set blocks
- Verify code generation
3. **Storage mechanism**
- Serialize workspace to JSON
- Store as node parameter
- Load/restore workspace
### Phase B: Logic Builder Node (Week 2)
1. **Node definition**
- Runtime node structure
- Dynamic port registration
- Code execution from generated JS
2. **I/O auto-detection**
- Parse workspace for Input/Output blocks
- Update node ports dynamically
- I/O summary panel
3. **Editor integration**
- Modal editor (initial implementation)
- "Edit Logic Blocks" button in properties
### Phase C: Tabbed Canvas System (Week 3)
1. **Tab infrastructure**
- CanvasTabs component
- Tab state management
- Component view scope
2. **Tab behavior**
- Open/close tabs
- Tab naming from node
- Reset on component change
3. **Polish**
- Tab switching animation
- Unsaved indicator
- Keyboard shortcuts
### Phase D: Expression Builder Node (Week 4)
1. **Simplified workspace**
- Limited toolbox (no events/signals)
- Single result output
- Inline or small modal
2. **Node definition**
- Single output port
- Expression evaluation
- Type inference
### Phase E: Full Block Library & Polish (Weeks 5-6)
1. **Complete Tier 1 blocks**
- Objects blocks with property access
- Arrays blocks with iteration
- Event/signal blocks
2. **Code viewer**
- "View Code" button
- Read-only JS display
- Syntax highlighting
3. **Rename existing nodes**
- Expression → JavaScript Expression
- Function → JavaScript Function
- Script → JavaScript Script
4. **Testing & documentation**
- Unit tests for code generation
- Integration tests for node behavior
- User documentation
---
## Testing Plan
### Unit Tests
- [ ] Block definitions load correctly
- [ ] Code generator produces valid JavaScript
- [ ] I/O detector finds all Input/Output blocks
- [ ] Workspace serialization round-trips correctly
### Integration Tests
- [ ] Logic Builder node executes generated code
- [ ] Signal inputs trigger execution
- [ ] Outputs update connected nodes
- [ ] Variables/Objects/Arrays access works
### Manual Testing
- [ ] Create Logic Builder with simple if/else logic
- [ ] Connect inputs/outputs to other nodes
- [ ] Verify signal flow works
- [ ] Test workspace persistence (save/reload project)
- [ ] Test tab system navigation
- [ ] Verify "View Code" shows correct JS
- [ ] Test Expression Builder for computed values
- [ ] Performance test with complex block arrangements
---
## Success Criteria
- [ ] Logic Builder node fully functional with Blockly workspace
- [ ] Expression Builder node for simple expressions
- [ ] Auto-detection of I/O from blocks works reliably
- [ ] Tabbed canvas system for editing multiple Logic Builders
- [ ] All Tier 1 blocks implemented and working
- [ ] "View Code" button shows generated JavaScript (read-only)
- [ ] Existing code nodes renamed to "JavaScript X"
- [ ] No performance regression in editor
- [ ] Works in both editor preview and deployed apps
---
## Risks & Mitigations
| Risk | Mitigation |
|------|------------|
| Blockly bundle size (~500KB) | Lazy-load only when Logic Builder opened |
| Blockly styling conflicts | Scope styles carefully, use shadow DOM if needed |
| Generated code security | Same sandbox as Function node, no new risks |
| Tab system complexity | Start with modal, upgrade to tabs if feasible |
| I/O detection edge cases | Require explicit "Define Input/Output" blocks for ports |
---
## Rollback Plan
All changes are additive:
- New nodes can be removed without breaking existing projects
- Blockly dependency can be removed
- Tab system is independent of node functionality
- Renamed nodes can have aliases for backward compatibility
---
## Future Enhancements (Separate Tasks)
1. **Records blocks** - After BYOB (Phase 5)
2. **Navigation blocks** - Page/popup navigation
3. **AI-assisted blocks** - "Describe what you want" → generates blocks
4. **Block templates** - Common patterns as reusable block groups
5. **Debugging** - Step-through execution, breakpoints
6. **Learning mode** - Side-by-side blocks and generated code
---
## References
- [Google Blockly Documentation](https://developers.google.com/blockly)
- [Blockly GitHub Repository](https://github.com/google/blockly)
- [Blockly Samples (plugins, examples)](https://github.com/google/blockly-samples)
- [MIT App Inventor Blocks](https://appinventor.mit.edu/) - Reference for event-driven block patterns
- [Backendless Blockly](https://backendless.com/) - Richard's reference for block-based backend logic
- TASK-006: Expression Overhaul (related enhancement)
- Phase 5 BYOB: For future Records blocks integration

480
package-lock.json generated
View File

@@ -306,6 +306,25 @@
}
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
}
},
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -2403,6 +2422,116 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@csstools/color-helpers": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"engines": {
"node": ">=18"
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^5.1.0",
"@csstools/css-calc": "^2.1.4"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@@ -9992,6 +10121,18 @@
"readable-stream": "^3.4.0"
}
},
"node_modules/blockly": {
"version": "12.3.1",
"resolved": "https://registry.npmjs.org/blockly/-/blockly-12.3.1.tgz",
"integrity": "sha512-BbWUcpqroY241XgSxTuAiEMHeIZ6u3+oD2zOATf3Fi+0NMWJ/MdMtuSkOcDCSk6Nc7WR3z5n9GHKqz2L+3kQOQ==",
"license": "Apache-2.0",
"dependencies": {
"jsdom": "26.1.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -12041,6 +12182,19 @@
"node": ">=8.0.0"
}
},
"node_modules/cssstyle": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"license": "MIT",
"dependencies": {
"@asamuzakjp/css-color": "^3.2.0",
"rrweb-cssom": "^0.8.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -12067,6 +12221,53 @@
"node": ">=8"
}
},
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/data-urls/node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/data-urls/node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/data-urls/node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -12230,6 +12431,12 @@
"node": ">=0.10.0"
}
},
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/decompress-response": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
@@ -15625,6 +15832,18 @@
"integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==",
"license": "Apache-2.0"
},
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
"license": "MIT",
"dependencies": {
"whatwg-encoding": "^3.1.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/html-entities": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
@@ -16895,6 +17114,12 @@
"node": ">=0.10.0"
}
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"license": "MIT"
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -18142,6 +18367,138 @@
"node": ">=12.0.0"
}
},
"node_modules/jsdom": {
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"license": "MIT",
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
"decimal.js": "^10.5.0",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.16",
"parse5": "^7.2.1",
"rrweb-cssom": "^0.8.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^5.1.1",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.1.1",
"ws": "^8.18.0",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jsdom/node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/jsdom/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/jsdom/node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/jsdom/node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/jsdom/node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/jsdom/node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/jsdom/node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/jsdom/node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -20923,6 +21280,12 @@
"node": ">=0.10.0"
}
},
"node_modules/nwsapi": {
"version": "2.2.23",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
"license": "MIT"
},
"node_modules/nx": {
"version": "16.10.0",
"resolved": "https://registry.npmjs.org/nx/-/nx-16.10.0.tgz",
@@ -23112,7 +23475,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -24443,6 +24805,12 @@
"optional": true,
"peer": true
},
"node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"license": "MIT"
},
"node_modules/run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
@@ -24762,6 +25130,18 @@
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
"license": "BlueOak-1.0.0"
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
@@ -26451,6 +26831,12 @@
"node": ">= 10"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
@@ -26741,6 +27127,24 @@
"node": ">=14.0.0"
}
},
"node_modules/tldts": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
"license": "MIT",
"dependencies": {
"tldts-core": "^6.1.86"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
"license": "MIT"
},
"node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
@@ -26809,6 +27213,18 @@
"node": ">=6"
}
},
"node_modules/tough-cookie": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
"license": "BSD-3-Clause",
"dependencies": {
"tldts": "^6.1.32"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -28038,6 +28454,18 @@
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"license": "MIT",
"dependencies": {
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@@ -28480,6 +28908,40 @@
"ultron": "~1.1.0"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
@@ -28812,6 +29274,15 @@
"node": ">=0.10.0"
}
},
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
}
},
"node_modules/xml2js": {
"version": "0.4.15",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.15.tgz",
@@ -28831,6 +29302,12 @@
"node": ">=8.0"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT"
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -29100,6 +29577,7 @@
"algoliasearch": "^5.35.0",
"archiver": "^5.3.2",
"async": "^3.2.6",
"blockly": "^12.3.1",
"classnames": "^2.5.1",
"dagre": "^0.8.5",
"diff3": "0.0.4",

View File

@@ -74,6 +74,7 @@
"algoliasearch": "^5.35.0",
"archiver": "^5.3.2",
"async": "^3.2.6",
"blockly": "^12.3.1",
"classnames": "^2.5.1",
"dagre": "^0.8.5",
"diff3": "0.0.4",

View File

@@ -232,8 +232,8 @@
}
:root {
--popup-layer-tooltip-border-color: var(--theme-color-secondary);
--popup-layer-tooltip-background-color: var(--theme-color-secondary);
--popup-layer-tooltip-border-color: var(--theme-color-border-default);
--popup-layer-tooltip-background-color: var(--theme-color-bg-3);
}
.popup-layer-tooltip {
@@ -244,7 +244,7 @@
border-color: var(--popup-layer-tooltip-border-color);
border-width: 1px;
padding: 12px 16px;
color: var(--theme-color-fg-highlight);
color: var(--theme-color-fg-default);
position: absolute;
opacity: 0;
-webkit-transition: opacity 0.3s;

View File

@@ -0,0 +1,121 @@
/**
* BlocklyWorkspace Styles
*
* Styling for the Blockly visual programming workspace.
* Uses theme tokens for consistent integration with Noodl editor.
*/
.Root {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background-color: var(--theme-color-bg-1);
overflow: hidden;
}
.BlocklyContainer {
flex: 1;
width: 100%;
height: 100%;
position: relative;
/* Ensure Blockly SVG fills container */
& > .injectionDiv {
width: 100% !important;
height: 100% !important;
}
}
/* Override Blockly default styles to match Noodl theme */
:global {
/* Toolbox styling */
.blocklyToolboxDiv {
background-color: var(--theme-color-bg-2) !important;
border-right: 1px solid var(--theme-color-border-default) !important;
}
.blocklyTreeLabel {
color: var(--theme-color-fg-default) !important;
font-family: var(--theme-font-family) !important;
font-size: 13px !important;
}
.blocklyTreeRow:hover {
background-color: var(--theme-color-bg-3) !important;
}
.blocklyTreeSelected {
background-color: var(--theme-color-primary) !important;
}
/* Flyout styling */
.blocklyFlyoutBackground {
fill: var(--theme-color-bg-2) !important;
fill-opacity: 0.95 !important;
}
/* Block styling - keep default Blockly colors for now */
/* May customize later to match Noodl node colors */
/* Zoom controls */
.blocklyZoom {
& image {
filter: brightness(0.8);
}
}
/* Trashcan */
.blocklyTrash {
& image {
filter: brightness(0.8);
}
}
/* Context menu */
.blocklyContextMenu {
background-color: var(--theme-color-bg-3) !important;
border: 1px solid var(--theme-color-border-default) !important;
border-radius: 4px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
}
.blocklyMenuItem {
color: var(--theme-color-fg-default) !important;
font-family: var(--theme-font-family) !important;
font-size: 13px !important;
padding: 6px 12px !important;
&:hover {
background-color: var(--theme-color-bg-4) !important;
}
&.blocklyMenuItemDisabled {
color: var(--theme-color-fg-default-shy) !important;
opacity: 0.5;
}
}
/* Scrollbars */
.blocklyScrollbarHandle {
fill: var(--theme-color-border-default) !important;
}
/* Field editor backgrounds */
.blocklyWidgetDiv {
& .goog-menu {
background-color: var(--theme-color-bg-3) !important;
border: 1px solid var(--theme-color-border-default) !important;
border-radius: 4px !important;
}
& .goog-menuitem {
color: var(--theme-color-fg-default) !important;
font-family: var(--theme-font-family) !important;
&:hover {
background-color: var(--theme-color-bg-4) !important;
}
}
}
}

View File

@@ -0,0 +1,173 @@
/**
* BlocklyWorkspace Component
*
* React wrapper for Google Blockly visual programming workspace.
* Provides integration with Noodl's node system for visual logic building.
*
* @module BlocklyEditor
*/
import * as Blockly from 'blockly';
import React, { useEffect, useRef, useState } from 'react';
import css from './BlocklyWorkspace.module.scss';
export interface BlocklyWorkspaceProps {
/** Initial workspace JSON (for loading saved state) */
initialWorkspace?: string;
/** Toolbox configuration */
toolbox?: Blockly.utils.toolbox.ToolboxDefinition;
/** Callback when workspace changes */
onChange?: (workspace: Blockly.WorkspaceSvg, json: string, code: string) => void;
/** Read-only mode */
readOnly?: boolean;
/** Custom theme */
theme?: Blockly.Theme;
}
/**
* BlocklyWorkspace - React component for Blockly integration
*
* Handles:
* - Blockly workspace initialization
* - Workspace persistence (save/load)
* - Change detection and callbacks
* - Cleanup on unmount
*/
export function BlocklyWorkspace({
initialWorkspace,
toolbox,
onChange,
readOnly = false,
theme
}: BlocklyWorkspaceProps) {
const blocklyDiv = useRef<HTMLDivElement>(null);
const workspaceRef = useRef<Blockly.WorkspaceSvg | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
// Initialize Blockly workspace
useEffect(() => {
if (!blocklyDiv.current) return;
console.log('🔧 [Blockly] Initializing workspace');
// Inject Blockly
const workspace = Blockly.inject(blocklyDiv.current, {
toolbox: toolbox || getDefaultToolbox(),
theme: theme,
readOnly: readOnly,
trashcan: true,
zoom: {
controls: true,
wheel: true,
startScale: 1.0,
maxScale: 3,
minScale: 0.3,
scaleSpeed: 1.2
},
grid: {
spacing: 20,
length: 3,
colour: '#ccc',
snap: true
}
});
workspaceRef.current = workspace;
// Load initial workspace if provided
if (initialWorkspace) {
try {
const json = JSON.parse(initialWorkspace);
Blockly.serialization.workspaces.load(json, workspace);
console.log('✅ [Blockly] Loaded initial workspace');
} catch (error) {
console.error('❌ [Blockly] Failed to load initial workspace:', error);
}
}
setIsInitialized(true);
// Listen for changes
const changeListener = () => {
if (onChange && workspace) {
const json = JSON.stringify(Blockly.serialization.workspaces.save(workspace));
const code = Blockly.JavaScript.workspaceToCode(workspace);
onChange(workspace, json, code);
}
};
workspace.addChangeListener(changeListener);
// Cleanup
return () => {
console.log('🧹 [Blockly] Disposing workspace');
workspace.removeChangeListener(changeListener);
workspace.dispose();
workspaceRef.current = null;
setIsInitialized(false);
};
}, [toolbox, theme, readOnly]);
// Handle initial workspace separately to avoid re-initialization
useEffect(() => {
if (isInitialized && initialWorkspace && workspaceRef.current) {
try {
const json = JSON.parse(initialWorkspace);
Blockly.serialization.workspaces.load(json, workspaceRef.current);
} catch (error) {
console.error('❌ [Blockly] Failed to update workspace:', error);
}
}
}, [initialWorkspace]);
return (
<div className={css.Root}>
<div ref={blocklyDiv} className={css.BlocklyContainer} />
</div>
);
}
/**
* Default toolbox with standard Blockly blocks
* This will be replaced with Noodl-specific toolbox
*/
function getDefaultToolbox(): Blockly.utils.toolbox.ToolboxDefinition {
return {
kind: 'categoryToolbox',
contents: [
{
kind: 'category',
name: 'Logic',
colour: '210',
contents: [
{ kind: 'block', type: 'controls_if' },
{ kind: 'block', type: 'logic_compare' },
{ kind: 'block', type: 'logic_operation' },
{ kind: 'block', type: 'logic_negate' },
{ kind: 'block', type: 'logic_boolean' }
]
},
{
kind: 'category',
name: 'Math',
colour: '230',
contents: [
{ kind: 'block', type: 'math_number' },
{ kind: 'block', type: 'math_arithmetic' },
{ kind: 'block', type: 'math_single' }
]
},
{
kind: 'category',
name: 'Text',
colour: '160',
contents: [
{ kind: 'block', type: 'text' },
{ kind: 'block', type: 'text_join' },
{ kind: 'block', type: 'text_length' }
]
}
]
};
}

View File

@@ -0,0 +1,283 @@
/**
* Noodl Custom Blocks for Blockly
*
* Defines custom blocks for Noodl-specific functionality:
* - Inputs/Outputs (node I/O)
* - Variables (Noodl.Variables)
* - Objects (Noodl.Objects)
* - Arrays (Noodl.Arrays)
* - Events/Signals
*
* @module BlocklyEditor
*/
import * as Blockly from 'blockly';
/**
* Initialize all Noodl custom blocks
*/
export function initNoodlBlocks() {
console.log('🔧 [Blockly] Initializing Noodl custom blocks');
// Input/Output blocks
defineInputOutputBlocks();
// Variable blocks
defineVariableBlocks();
// Object blocks (basic - will expand later)
defineObjectBlocks();
// Array blocks (basic - will expand later)
defineArrayBlocks();
console.log('✅ [Blockly] Noodl blocks initialized');
}
/**
* Input/Output Blocks
*/
function defineInputOutputBlocks() {
// Define Input block - declares an input port
Blockly.Blocks['noodl_define_input'] = {
init: function () {
this.appendDummyInput()
.appendField('📥 Define input')
.appendField(new Blockly.FieldTextInput('myInput'), 'NAME')
.appendField('type')
.appendField(
new Blockly.FieldDropdown([
['any', '*'],
['string', 'string'],
['number', 'number'],
['boolean', 'boolean'],
['object', 'object'],
['array', 'array']
]),
'TYPE'
);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(230);
this.setTooltip('Defines an input port that appears on the node');
this.setHelpUrl('');
}
};
// Get Input block - gets value from an input
Blockly.Blocks['noodl_get_input'] = {
init: function () {
this.appendDummyInput().appendField('📥 get input').appendField(new Blockly.FieldTextInput('value'), 'NAME');
this.setOutput(true, null);
this.setColour(230);
this.setTooltip('Gets the value from an input port');
this.setHelpUrl('');
}
};
// Define Output block - declares an output port
Blockly.Blocks['noodl_define_output'] = {
init: function () {
this.appendDummyInput()
.appendField('📤 Define output')
.appendField(new Blockly.FieldTextInput('result'), 'NAME')
.appendField('type')
.appendField(
new Blockly.FieldDropdown([
['any', '*'],
['string', 'string'],
['number', 'number'],
['boolean', 'boolean'],
['object', 'object'],
['array', 'array']
]),
'TYPE'
);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(230);
this.setTooltip('Defines an output port that appears on the node');
this.setHelpUrl('');
}
};
// Set Output block - sets value on an output
Blockly.Blocks['noodl_set_output'] = {
init: function () {
this.appendValueInput('VALUE')
.setCheck(null)
.appendField('📤 set output')
.appendField(new Blockly.FieldTextInput('result'), 'NAME')
.appendField('to');
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(230);
this.setTooltip('Sets the value of an output port');
this.setHelpUrl('');
}
};
// Define Signal Input block
Blockly.Blocks['noodl_define_signal_input'] = {
init: function () {
this.appendDummyInput()
.appendField('⚡ Define signal input')
.appendField(new Blockly.FieldTextInput('trigger'), 'NAME');
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(180);
this.setTooltip('Defines a signal input that can trigger logic');
this.setHelpUrl('');
}
};
// Define Signal Output block
Blockly.Blocks['noodl_define_signal_output'] = {
init: function () {
this.appendDummyInput()
.appendField('⚡ Define signal output')
.appendField(new Blockly.FieldTextInput('done'), 'NAME');
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(180);
this.setTooltip('Defines a signal output that can trigger other nodes');
this.setHelpUrl('');
}
};
// Send Signal block
Blockly.Blocks['noodl_send_signal'] = {
init: function () {
this.appendDummyInput().appendField('⚡ send signal').appendField(new Blockly.FieldTextInput('done'), 'NAME');
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(180);
this.setTooltip('Sends a signal to connected nodes');
this.setHelpUrl('');
}
};
}
/**
* Variable Blocks
*/
function defineVariableBlocks() {
// Get Variable block
Blockly.Blocks['noodl_get_variable'] = {
init: function () {
this.appendDummyInput()
.appendField('📖 get variable')
.appendField(new Blockly.FieldTextInput('myVariable'), 'NAME');
this.setOutput(true, null);
this.setColour(330);
this.setTooltip('Gets the value of a global Noodl variable');
this.setHelpUrl('');
}
};
// Set Variable block
Blockly.Blocks['noodl_set_variable'] = {
init: function () {
this.appendValueInput('VALUE')
.setCheck(null)
.appendField('✏️ set variable')
.appendField(new Blockly.FieldTextInput('myVariable'), 'NAME')
.appendField('to');
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(330);
this.setTooltip('Sets the value of a global Noodl variable');
this.setHelpUrl('');
}
};
}
/**
* Object Blocks (basic set - will expand in Phase E)
*/
function defineObjectBlocks() {
// Get Object block
Blockly.Blocks['noodl_get_object'] = {
init: function () {
this.appendValueInput('ID').setCheck('String').appendField('📦 get object');
this.setOutput(true, 'Object');
this.setColour(20);
this.setTooltip('Gets a Noodl Object by its ID');
this.setHelpUrl('');
}
};
// Get Object Property block
Blockly.Blocks['noodl_get_object_property'] = {
init: function () {
this.appendValueInput('OBJECT')
.setCheck(null)
.appendField('📖 get')
.appendField(new Blockly.FieldTextInput('name'), 'PROPERTY')
.appendField('from object');
this.setOutput(true, null);
this.setColour(20);
this.setTooltip('Gets a property value from an object');
this.setHelpUrl('');
}
};
// Set Object Property block
Blockly.Blocks['noodl_set_object_property'] = {
init: function () {
this.appendValueInput('OBJECT')
.setCheck(null)
.appendField('✏️ set')
.appendField(new Blockly.FieldTextInput('name'), 'PROPERTY')
.appendField('on object');
this.appendValueInput('VALUE').setCheck(null).appendField('to');
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(20);
this.setTooltip('Sets a property value on an object');
this.setHelpUrl('');
}
};
}
/**
* Array Blocks (basic set - will expand in Phase E)
*/
function defineArrayBlocks() {
// Get Array block
Blockly.Blocks['noodl_get_array'] = {
init: function () {
this.appendDummyInput().appendField('📋 get array').appendField(new Blockly.FieldTextInput('myArray'), 'NAME');
this.setOutput(true, 'Array');
this.setColour(260);
this.setTooltip('Gets a Noodl Array by name');
this.setHelpUrl('');
}
};
// Array Length block
Blockly.Blocks['noodl_array_length'] = {
init: function () {
this.appendValueInput('ARRAY').setCheck('Array').appendField('🔢 length of array');
this.setOutput(true, 'Number');
this.setColour(260);
this.setTooltip('Gets the number of items in an array');
this.setHelpUrl('');
}
};
// Array Add block
Blockly.Blocks['noodl_array_add'] = {
init: function () {
this.appendValueInput('ITEM').setCheck(null).appendField(' add');
this.appendValueInput('ARRAY').setCheck('Array').appendField('to array');
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(260);
this.setTooltip('Adds an item to the end of an array');
this.setHelpUrl('');
}
};
}

View File

@@ -0,0 +1,165 @@
/**
* Noodl Code Generators for Blockly
*
* Converts Blockly blocks into executable JavaScript code for the Noodl runtime.
* Generated code has access to:
* - Inputs: Input values from connections
* - Outputs: Output values to connections
* - Noodl.Variables: Global variables
* - Noodl.Objects: Global objects
* - Noodl.Arrays: Global arrays
*
* @module BlocklyEditor
*/
import * as Blockly from 'blockly';
import { javascriptGenerator } from 'blockly/javascript';
/**
* Initialize all Noodl code generators
*/
export function initNoodlGenerators() {
console.log('🔧 [Blockly] Initializing Noodl code generators');
// Input/Output generators
initInputOutputGenerators();
// Variable generators
initVariableGenerators();
// Object generators
initObjectGenerators();
// Array generators
initArrayGenerators();
console.log('✅ [Blockly] Noodl generators initialized');
}
/**
* Input/Output Generators
*/
function initInputOutputGenerators() {
// Define Input - no runtime code (used for I/O detection only)
javascriptGenerator.forBlock['noodl_define_input'] = function () {
return '';
};
// Get Input - generates: Inputs["name"]
javascriptGenerator.forBlock['noodl_get_input'] = function (block) {
const name = block.getFieldValue('NAME');
const code = `Inputs["${name}"]`;
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
// Define Output - no runtime code (used for I/O detection only)
javascriptGenerator.forBlock['noodl_define_output'] = function () {
return '';
};
// Set Output - generates: Outputs["name"] = value;
javascriptGenerator.forBlock['noodl_set_output'] = function (block) {
const name = block.getFieldValue('NAME');
const value = javascriptGenerator.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
return `Outputs["${name}"] = ${value};\n`;
};
// Define Signal Input - no runtime code
javascriptGenerator.forBlock['noodl_define_signal_input'] = function () {
return '';
};
// Define Signal Output - no runtime code
javascriptGenerator.forBlock['noodl_define_signal_output'] = function () {
return '';
};
// Send Signal - generates: this.sendSignalOnOutput("name");
javascriptGenerator.forBlock['noodl_send_signal'] = function (block) {
const name = block.getFieldValue('NAME');
return `this.sendSignalOnOutput("${name}");\n`;
};
}
/**
* Variable Generators
*/
function initVariableGenerators() {
// Get Variable - generates: Noodl.Variables["name"]
javascriptGenerator.forBlock['noodl_get_variable'] = function (block) {
const name = block.getFieldValue('NAME');
const code = `Noodl.Variables["${name}"]`;
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
// Set Variable - generates: Noodl.Variables["name"] = value;
javascriptGenerator.forBlock['noodl_set_variable'] = function (block) {
const name = block.getFieldValue('NAME');
const value = javascriptGenerator.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
return `Noodl.Variables["${name}"] = ${value};\n`;
};
}
/**
* Object Generators
*/
function initObjectGenerators() {
// Get Object - generates: Noodl.Objects[id]
javascriptGenerator.forBlock['noodl_get_object'] = function (block) {
const id = javascriptGenerator.valueToCode(block, 'ID', Blockly.JavaScript.ORDER_NONE) || '""';
const code = `Noodl.Objects[${id}]`;
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
// Get Object Property - generates: object["property"]
javascriptGenerator.forBlock['noodl_get_object_property'] = function (block) {
const property = block.getFieldValue('PROPERTY');
const object = javascriptGenerator.valueToCode(block, 'OBJECT', Blockly.JavaScript.ORDER_MEMBER) || '{}';
const code = `${object}["${property}"]`;
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
// Set Object Property - generates: object["property"] = value;
javascriptGenerator.forBlock['noodl_set_object_property'] = function (block) {
const property = block.getFieldValue('PROPERTY');
const object = javascriptGenerator.valueToCode(block, 'OBJECT', Blockly.JavaScript.ORDER_MEMBER) || '{}';
const value = javascriptGenerator.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
return `${object}["${property}"] = ${value};\n`;
};
}
/**
* Array Generators
*/
function initArrayGenerators() {
// Get Array - generates: Noodl.Arrays["name"]
javascriptGenerator.forBlock['noodl_get_array'] = function (block) {
const name = block.getFieldValue('NAME');
const code = `Noodl.Arrays["${name}"]`;
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
// Array Length - generates: array.length
javascriptGenerator.forBlock['noodl_array_length'] = function (block) {
const array = javascriptGenerator.valueToCode(block, 'ARRAY', Blockly.JavaScript.ORDER_MEMBER) || '[]';
const code = `${array}.length`;
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
// Array Add - generates: array.push(item);
javascriptGenerator.forBlock['noodl_array_add'] = function (block) {
const item = javascriptGenerator.valueToCode(block, 'ITEM', Blockly.JavaScript.ORDER_NONE) || 'null';
const array = javascriptGenerator.valueToCode(block, 'ARRAY', Blockly.JavaScript.ORDER_MEMBER) || '[]';
return `${array}.push(${item});\n`;
};
}
/**
* Generate complete JavaScript code from workspace
*
* @param workspace - The Blockly workspace
* @returns Generated JavaScript code
*/
export function generateCode(workspace: Blockly.WorkspaceSvg): string {
return javascriptGenerator.workspaceToCode(workspace);
}

View File

@@ -0,0 +1,35 @@
/**
* BlocklyEditor Module
*
* Entry point for Blockly integration in Noodl.
* Exports components, blocks, and generators for visual logic building.
*
* @module BlocklyEditor
*/
import { initNoodlBlocks } from './NoodlBlocks';
import { initNoodlGenerators } from './NoodlGenerators';
// Main component
export { BlocklyWorkspace } from './BlocklyWorkspace';
export type { BlocklyWorkspaceProps } from './BlocklyWorkspace';
// Block definitions and generators
export { initNoodlBlocks } from './NoodlBlocks';
export { initNoodlGenerators, generateCode } from './NoodlGenerators';
/**
* Initialize all Noodl Blockly extensions
* Call this once at app startup before using Blockly components
*/
export function initBlocklyIntegration() {
console.log('🔧 [Blockly] Initializing Noodl Blockly integration');
// Initialize custom blocks
initNoodlBlocks();
// Initialize code generators
initNoodlGenerators();
console.log('✅ [Blockly] Integration initialized');
}

View File

@@ -37,6 +37,84 @@ const ExpressionNode = {
this._internal.unsubscribe();
this._internal.unsubscribe = null;
}
},
registerInputIfNeeded: function (name) {
if (this.hasInput(name)) {
return;
}
this._internal.scope[name] = 0;
this._inputValues[name] = 0;
this.registerInput(name, {
set: function (value) {
this._internal.scope[name] = value;
if (!this.isInputConnected('run')) this._scheduleEvaluateExpression();
}
});
},
_scheduleEvaluateExpression: function () {
var internal = this._internal;
if (internal.hasScheduledEvaluation === false) {
internal.hasScheduledEvaluation = true;
this.flagDirty();
this.scheduleAfterInputsHaveUpdated(function () {
var lastValue = internal.cachedValue;
internal.cachedValue = this._calculateExpression();
if (lastValue !== internal.cachedValue) {
this.flagOutputDirty('result');
this.flagOutputDirty('isTrue');
this.flagOutputDirty('isFalse');
}
if (internal.cachedValue) this.sendSignalOnOutput('isTrueEv');
else this.sendSignalOnOutput('isFalseEv');
internal.hasScheduledEvaluation = false;
});
}
},
_calculateExpression: function () {
var internal = this._internal;
if (!internal.compiledFunction) {
internal.compiledFunction = this._compileFunction();
}
for (var i = 0; i < internal.inputNames.length; ++i) {
var inputValue = internal.scope[internal.inputNames[i]];
internal.inputValues[i] = inputValue;
}
// Get proper Noodl API and append as last parameter for backward compatibility
const JavascriptNodeParser = require('../../javascriptnodeparser');
const noodlAPI = JavascriptNodeParser.createNoodlAPI(this.context && this.context.modelScope);
const argsWithNoodl = internal.inputValues.concat([noodlAPI]);
try {
return internal.compiledFunction.apply(null, argsWithNoodl);
} catch (e) {
console.error('Error in expression:', e.message);
}
return 0;
},
_compileFunction: function () {
var expression = this._internal.currentExpression;
var args = Object.keys(this._internal.scope);
// Add 'Noodl' as last parameter for backward compatibility
args.push('Noodl');
var key = expression + args.join(' ');
if (compiledFunctionsCache.hasOwnProperty(key) === false) {
args.push(expression);
try {
compiledFunctionsCache[key] = construct(Function, args);
} catch (e) {
console.error('Failed to compile JS function', e.message);
}
}
return compiledFunctionsCache[key];
}
},
getInspectInfo() {
@@ -197,84 +275,6 @@ const ExpressionNode = {
return !!this._internal.cachedValue;
}
}
},
prototypeExtensions: {
registerInputIfNeeded: {
value: function (name) {
if (this.hasInput(name)) {
return;
}
this._internal.scope[name] = 0;
this._inputValues[name] = 0;
this.registerInput(name, {
set: function (value) {
this._internal.scope[name] = value;
if (!this.isInputConnected('run')) this._scheduleEvaluateExpression();
}
});
}
},
_scheduleEvaluateExpression: {
value: function () {
var internal = this._internal;
if (internal.hasScheduledEvaluation === false) {
internal.hasScheduledEvaluation = true;
this.flagDirty();
this.scheduleAfterInputsHaveUpdated(function () {
var lastValue = internal.cachedValue;
internal.cachedValue = this._calculateExpression();
if (lastValue !== internal.cachedValue) {
this.flagOutputDirty('result');
this.flagOutputDirty('isTrue');
this.flagOutputDirty('isFalse');
}
if (internal.cachedValue) this.sendSignalOnOutput('isTrueEv');
else this.sendSignalOnOutput('isFalseEv');
internal.hasScheduledEvaluation = false;
});
}
}
},
_calculateExpression: {
value: function () {
var internal = this._internal;
if (!internal.compiledFunction) {
internal.compiledFunction = this._compileFunction();
}
for (var i = 0; i < internal.inputNames.length; ++i) {
var inputValue = internal.scope[internal.inputNames[i]];
internal.inputValues[i] = inputValue;
}
try {
return internal.compiledFunction.apply(null, internal.inputValues);
} catch (e) {
console.error('Error in expression:', e.message);
}
return 0;
}
},
_compileFunction: {
value: function () {
var expression = this._internal.currentExpression;
var args = Object.keys(this._internal.scope);
var key = expression + args.join(' ');
if (compiledFunctionsCache.hasOwnProperty(key) === false) {
args.push(expression);
try {
compiledFunctionsCache[key] = construct(Function, args);
} catch (e) {
console.error('Failed to compile JS function', e.message);
}
}
return compiledFunctionsCache[key];
}
}
}
};

View File

@@ -132,11 +132,18 @@ const SimpleJavascriptNode = {
}
}
// Create Noodl API and augment with Inputs/Outputs for backward compatibility
// Legacy code used: Noodl.Outputs.foo = 'bar'
// New code uses: Outputs.foo = 'bar' (direct parameter)
const noodlAPI = JavascriptNodeParser.createNoodlAPI(this.nodeScope.modelScope);
noodlAPI.Inputs = inputs;
noodlAPI.Outputs = outputs;
try {
await func.apply(this._internal._this, [
inputs,
outputs,
JavascriptNodeParser.createNoodlAPI(this.nodeScope.modelScope),
noodlAPI,
JavascriptNodeParser.getComponentScopeForNode(this)
]);
} catch (e) {