mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-13 15:52:56 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
```
|
||||
@@ -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' }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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:**
|
||||
@@ -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
480
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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' }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -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('');
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user