From 32a0a0885fa8769ba80a7c064f153f946293e411 Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Fri, 16 Jan 2026 12:00:31 +0100 Subject: [PATCH] Finished prototype local backends and expression editor --- .clinerules | 15 + dev-docs/reference/LEARNINGS.md | 89 ++ dev-docs/reference/PANEL-UI-STYLE-GUIDE.md | 511 +++++++ .../PHASE-2B-COMPLETE.md | 219 +++ .../TASK-007H-COMPLETE.md | 107 ++ .../TASK-007H-schema-manager-ui.md | 1189 +++++++++++++++ .../TASK-007I-COMPLETE.md | 205 +++ .../TASK-007I-data-browser-editor.md | 1290 +++++++++++++++++ .../TASK-007J-COMPLETE.md | 90 ++ .../TASK-007J-DRAFT.md | 260 ++++ .../TASK-007K-DRAFT.md | 120 ++ .../PROGRESS.md | 47 +- .../ExpressionInput.module.scss | 28 + .../ExpressionInput/ExpressionInput.tsx | 11 + .../ExpressionToggle.module.scss | 39 + .../ExpressionToggle/ExpressionToggle.tsx | 44 +- .../PropertyPanelInput.module.scss | 2 + .../PropertyPanelInput/PropertyPanelInput.tsx | 8 +- .../LocalBackendCard.module.scss | 25 + .../LocalBackendCard/LocalBackendCard.tsx | 44 + .../panels/databrowser/CellEditor.module.scss | 91 ++ .../views/panels/databrowser/CellEditor.tsx | 222 +++ .../databrowser/DataBrowser.module.scss | 152 ++ .../views/panels/databrowser/DataBrowser.tsx | 475 ++++++ .../panels/databrowser/DataGrid.module.scss | 145 ++ .../src/views/panels/databrowser/DataGrid.tsx | 210 +++ .../databrowser/NewRecordModal.module.scss | 121 ++ .../panels/databrowser/NewRecordModal.tsx | 218 +++ .../src/views/panels/databrowser/index.ts | 13 + .../propertyeditor/DataTypes/BasicType.ts | 10 +- .../ExpressionEditorModal.module.scss | 80 + .../ExpressionEditorModal.tsx | 126 ++ .../ExpressionEditorModal/index.ts | 2 + .../PropertyPanelInputWithExpressionModal.tsx | 68 + .../schemamanager/AddColumnForm.module.scss | 113 ++ .../panels/schemamanager/AddColumnForm.tsx | 239 +++ .../CreateTableModal.module.scss | 175 +++ .../panels/schemamanager/CreateTableModal.tsx | 426 ++++++ .../schemamanager/SchemaPanel.module.scss | 47 + .../panels/schemamanager/SchemaPanel.tsx | 239 +++ .../panels/schemamanager/TableRow.module.scss | 117 ++ .../views/panels/schemamanager/TableRow.tsx | 189 +++ .../src/views/panels/schemamanager/index.ts | 20 + .../main/src/local-backend/BackendManager.js | 402 ++++- .../api/adapters/local-sql/LocalSQLAdapter.js | 168 ++- .../api/adapters/local-sql/QueryBuilder.js | 40 +- .../api/adapters/local-sql/SchemaManager.js | 83 ++ packages/noodl-runtime/src/node.js | 87 +- 48 files changed, 8513 insertions(+), 108 deletions(-) create mode 100644 dev-docs/reference/PANEL-UI-STYLE-GUIDE.md create mode 100644 dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-006-expressions-overhaul/PHASE-2B-COMPLETE.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-COMPLETE.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-schema-manager-ui.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-COMPLETE.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-data-browser-editor.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007J-COMPLETE.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007J-DRAFT.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007K-DRAFT.md create mode 100644 packages/noodl-editor/src/editor/src/views/panels/databrowser/CellEditor.module.scss create mode 100644 packages/noodl-editor/src/editor/src/views/panels/databrowser/CellEditor.tsx create mode 100644 packages/noodl-editor/src/editor/src/views/panels/databrowser/DataBrowser.module.scss create mode 100644 packages/noodl-editor/src/editor/src/views/panels/databrowser/DataBrowser.tsx create mode 100644 packages/noodl-editor/src/editor/src/views/panels/databrowser/DataGrid.module.scss create mode 100644 packages/noodl-editor/src/editor/src/views/panels/databrowser/DataGrid.tsx create mode 100644 packages/noodl-editor/src/editor/src/views/panels/databrowser/NewRecordModal.module.scss create mode 100644 packages/noodl-editor/src/editor/src/views/panels/databrowser/NewRecordModal.tsx create mode 100644 packages/noodl-editor/src/editor/src/views/panels/databrowser/index.ts create mode 100644 packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/ExpressionEditorModal.module.scss create mode 100644 packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/ExpressionEditorModal.tsx create mode 100644 packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/index.ts create mode 100644 packages/noodl-editor/src/editor/src/views/panels/propertyeditor/components/PropertyPanelInputWithExpressionModal.tsx create mode 100644 packages/noodl-editor/src/editor/src/views/panels/schemamanager/AddColumnForm.module.scss create mode 100644 packages/noodl-editor/src/editor/src/views/panels/schemamanager/AddColumnForm.tsx create mode 100644 packages/noodl-editor/src/editor/src/views/panels/schemamanager/CreateTableModal.module.scss create mode 100644 packages/noodl-editor/src/editor/src/views/panels/schemamanager/CreateTableModal.tsx create mode 100644 packages/noodl-editor/src/editor/src/views/panels/schemamanager/SchemaPanel.module.scss create mode 100644 packages/noodl-editor/src/editor/src/views/panels/schemamanager/SchemaPanel.tsx create mode 100644 packages/noodl-editor/src/editor/src/views/panels/schemamanager/TableRow.module.scss create mode 100644 packages/noodl-editor/src/editor/src/views/panels/schemamanager/TableRow.tsx create mode 100644 packages/noodl-editor/src/editor/src/views/panels/schemamanager/index.ts diff --git a/.clinerules b/.clinerules index 4197372..a75fc97 100644 --- a/.clinerules +++ b/.clinerules @@ -123,6 +123,7 @@ packages/ - `dev-docs/reference/NODE-PATTERNS.md` - How to create/modify nodes - `dev-docs/reference/LEARNINGS.md` - Accumulated knowledge - `dev-docs/reference/UI-STYLING-GUIDE.md` - Styling rules (NO hardcoded colors!) +- `dev-docs/reference/PANEL-UI-STYLE-GUIDE.md` - **Panels & Modals (READ BEFORE making UI!)** - `dev-docs/reference/LEARNINGS-NODE-CREATION.md` - Node creation gotchas --- @@ -418,6 +419,20 @@ Before marking any task complete: - [ ] **Discoveries added to LEARNINGS.md or COMMON-ISSUES.md** - [ ] Task CHANGELOG.md updated with progress +### Visual Components (Panels, Modals, Forms) + +**MUST read `dev-docs/reference/PANEL-UI-STYLE-GUIDE.md` before building UI!** + +- [ ] NO emojis in buttons, labels, or headers +- [ ] Using CSS variables for ALL colors (`var(--theme-color-*)`) +- [ ] Using `Text` component with proper `textType` for typography +- [ ] Using `PrimaryButton` with correct variant (Cta/Muted/Ghost/Danger) +- [ ] Panel structure: Header → Toolbar → Content → Footer +- [ ] Modal structure: Overlay → Modal → Header → Body → Footer +- [ ] Form inputs styled with proper tokens (bg-1, bg-3 borders) +- [ ] Empty states, loading states, and error states handled +- [ ] Dark theme first - ensure contrast with light text + ### Git - [ ] Meaningful commit messages (conventional commits format) diff --git a/dev-docs/reference/LEARNINGS.md b/dev-docs/reference/LEARNINGS.md index 6161af3..4439550 100644 --- a/dev-docs/reference/LEARNINGS.md +++ b/dev-docs/reference/LEARNINGS.md @@ -10,6 +10,95 @@ These fundamental patterns apply across ALL Noodl development. Understanding the --- +## 📊 Property Expression Runtime Context (Jan 16, 2026) + +### The Context Trap: Why evaluateExpression() Needs undefined, Not this.context + +**Context**: TASK-006 Expressions Overhaul - Property expressions in the properties panel weren't evaluating. Error: "scope.get is not a function". + +**The Problem**: The `evaluateExpression()` function in `expression-evaluator.js` expects either `undefined` (to use global Model) or a Model scope object. Passing `this.context` (the runtime node context with editorConnection, styles, etc.) caused the error because it lacks a `.get()` method. + +**The Broken Pattern**: + +```javascript +// ❌ WRONG - this.context is NOT a Model scope +Node.prototype._evaluateExpressionParameter = function (paramValue, portName) { + const compiled = compileExpression(paramValue.expression); + const result = evaluateExpression(compiled, this.context); // ☠️ scope.get is not a function +}; +``` + +**The Correct Pattern**: + +```javascript +// ✅ RIGHT - Pass undefined to use global Model +Node.prototype._evaluateExpressionParameter = function (paramValue, portName) { + const compiled = compileExpression(paramValue.expression); + const result = evaluateExpression(compiled, undefined); // ✅ Uses global Model +}; +``` + +**Why This Works**: + +The expression-evaluator's `createNoodlContext()` function does: + +```javascript +const scope = modelScope || Model; // Falls back to global Model +const variablesModel = scope.get('--ndl--global-variables'); +``` + +When `undefined` is passed, it uses the global `Model` which has the `.get()` method. The runtime's `this.context` is a completely different object containing `editorConnection`, `styles`, etc. + +**Reactive Expression Subscriptions**: + +To make expressions update when Variables change, the expression-evaluator already has: + +- `detectDependencies(expression)` - finds what Variables/Objects/Arrays are referenced +- `subscribeToChanges(dependencies, callback)` - subscribes to Model changes + +Wire these up in `_evaluateExpressionParameter`: + +```javascript +// Set up reactive subscription +if (!this._expressionSubscriptions[portName]) { + const dependencies = detectDependencies(paramValue.expression); + if (dependencies.variables.length > 0) { + this._expressionSubscriptions[portName] = subscribeToChanges( + dependencies, + function () { + if (this._deleted) return; + this.queueInput(portName, paramValue); // Re-evaluate + }.bind(this) + ); + } +} +``` + +**Critical Rules**: + +1. **NEVER** pass `this.context` to `evaluateExpression()` - it's not a Model scope +2. **ALWAYS** pass `undefined` to use the global Model for Variables/Objects/Arrays +3. **Clean up subscriptions** in `_onNodeDeleted()` to prevent memory leaks +4. **Track subscriptions by port name** to avoid duplicate listeners + +**Applies To**: + +- Property panel expression fields +- Any runtime expression evaluation +- Future expression support in other property types + +**Time Saved**: This pattern prevents 1-2 hours debugging "scope.get is not a function" errors. + +**Location**: + +- Fixed in: `packages/noodl-runtime/src/node.js` +- Expression evaluator: `packages/noodl-runtime/src/expression-evaluator.js` +- Task: Phase 3 TASK-006 Expressions Overhaul + +**Keywords**: expression, evaluateExpression, this.context, Model scope, scope.get, Variables, reactive, subscribeToChanges, detectDependencies + +--- + ## 🔴 Editor/Runtime Window Separation (Jan 2026) ### The Invisible Boundary: Why Editor Methods Don't Exist in Runtime diff --git a/dev-docs/reference/PANEL-UI-STYLE-GUIDE.md b/dev-docs/reference/PANEL-UI-STYLE-GUIDE.md new file mode 100644 index 0000000..3f2ba39 --- /dev/null +++ b/dev-docs/reference/PANEL-UI-STYLE-GUIDE.md @@ -0,0 +1,511 @@ +# Panel & Modal UI Style Guide + +This guide documents the visual patterns used in OpenNoodl's editor panels and modals. **Always follow these patterns when creating new UI components.** + +--- + +## Core Principles + +### 1. Professional, Not Playful + +- **NO emojis** in UI labels, buttons, or headers +- **NO decorative icons** unless they serve a functional purpose +- Clean, minimal aesthetic that respects the user's intelligence +- Think "developer tool" not "consumer app" + +### 2. Consistent Visual Language + +- Use design tokens (CSS variables) for ALL colors +- Consistent spacing using the spacing system (4px base unit) +- Typography hierarchy using the Text component types +- All interactive elements must have hover/active states + +### 3. Dark Theme First + +- Design for dark backgrounds (`--theme-color-bg-2`, `--theme-color-bg-3`) +- Ensure sufficient contrast with light text +- Colored elements should be muted, not neon + +--- + +## Panel Structure + +### Standard Panel Layout + +```tsx +
+ {/* Header with title and close button */} +
+ + + + Panel Title + + Subtitle or context + + + + +
+ + {/* Toolbar with actions */} +
{/* Filters, search, action buttons */}
+ + {/* Content area (scrollable) */} +
{/* Main panel content */}
+ + {/* Footer (optional - pagination, status) */} +
{/* Page controls, counts */}
+
+``` + +### Panel CSS Pattern + +```scss +.Root { + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--theme-color-bg-2); + color: var(--theme-color-fg-default); +} + +.Header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--theme-color-bg-3); + flex-shrink: 0; +} + +.Toolbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; + background-color: var(--theme-color-bg-3); + gap: 8px; + flex-shrink: 0; +} + +.Content { + flex: 1; + overflow-y: auto; + padding: 16px; +} + +.Footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; + border-top: 1px solid var(--theme-color-bg-3); + background-color: var(--theme-color-bg-2); + flex-shrink: 0; +} +``` + +--- + +## Modal Structure + +### Standard Modal Layout + +```tsx +
+
+ {/* Header */} +
+ Modal Title + +
+ + {/* Body */} +
{/* Form fields, content */}
+ + {/* Footer with actions */} +
+ + +
+
+
+``` + +### Modal CSS Pattern + +```scss +.Overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.Modal { + background-color: var(--theme-color-bg-2); + border-radius: 8px; + width: 480px; + max-width: 90vw; + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +.Header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid var(--theme-color-bg-3); +} + +.Body { + padding: 20px; + overflow-y: auto; + flex: 1; +} + +.Footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 16px 20px; + border-top: 1px solid var(--theme-color-bg-3); +} +``` + +--- + +## Form Elements + +### Text Inputs + +```scss +.Input { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--theme-color-bg-3); + border-radius: 4px; + background-color: var(--theme-color-bg-1); + color: var(--theme-color-fg-default); + font-size: 13px; + font-family: inherit; + + &:focus { + outline: none; + border-color: var(--theme-color-primary); + } + + &::placeholder { + color: var(--theme-color-fg-default-shy); + } +} +``` + +### Select Dropdowns + +```scss +.Select { + padding: 8px 12px; + border: 1px solid var(--theme-color-bg-3); + border-radius: 4px; + background-color: var(--theme-color-bg-1); + color: var(--theme-color-fg-default); + font-size: 13px; + min-width: 120px; + + &:focus { + outline: none; + border-color: var(--theme-color-primary); + } +} +``` + +### Form Groups + +```scss +.FormGroup { + margin-bottom: 16px; + + &:last-child { + margin-bottom: 0; + } +} + +.Label { + display: block; + margin-bottom: 6px; + font-size: 12px; + font-weight: 500; + color: var(--theme-color-fg-default); +} + +.HelpText { + margin-top: 4px; + font-size: 11px; + color: var(--theme-color-fg-default-shy); +} +``` + +--- + +## Data Tables & Grids + +### Table Pattern + +```scss +.Grid { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + th { + text-align: left; + padding: 10px 12px; + font-weight: 500; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--theme-color-fg-default-shy); + background-color: var(--theme-color-bg-3); + border-bottom: 1px solid var(--theme-color-bg-3); + position: sticky; + top: 0; + z-index: 1; + } + + td { + padding: 8px 12px; + border-bottom: 1px solid var(--theme-color-bg-3); + color: var(--theme-color-fg-default); + } + + tr:hover td { + background-color: var(--theme-color-bg-3); + } +} +``` + +### Type Badges + +For showing data types, use subtle colored badges: + +```scss +.TypeBadge { + display: inline-block; + padding: 2px 6px; + border-radius: 3px; + font-size: 10px; + font-weight: 500; + color: white; +} + +// Use semantic colors, not hardcoded +.TypeString { + background-color: var(--theme-color-primary); +} +.TypeNumber { + background-color: var(--theme-color-success); +} +.TypeBoolean { + background-color: var(--theme-color-notice); +} +.TypeDate { + background-color: #8b5cf6; +} // Purple - no token available +.TypePointer { + background-color: var(--theme-color-danger); +} +``` + +--- + +## Expandable Rows + +For tree-like or expandable content: + +```scss +.ExpandableRow { + border: 1px solid var(--theme-color-bg-3); + border-radius: 6px; + margin-bottom: 8px; + overflow: hidden; + background-color: var(--theme-color-bg-2); + + &[data-expanded='true'] { + border-color: var(--theme-color-primary); + } +} + +.RowHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + cursor: pointer; + transition: background-color 0.15s; + + &:hover { + background-color: var(--theme-color-bg-3); + } +} + +.RowContent { + padding: 0 16px 16px; + background-color: var(--theme-color-bg-1); + border-top: 1px solid var(--theme-color-bg-3); +} + +.ExpandIcon { + transition: transform 0.2s; + color: var(--theme-color-fg-default-shy); +} +``` + +--- + +## Empty States + +When there's no content to show: + +```scss +.EmptyState { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 24px; + text-align: center; + color: var(--theme-color-fg-default-shy); + + .EmptyIcon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; + } + + .EmptyText { + margin-bottom: 8px; + } +} +``` + +--- + +## Loading & Error States + +### Loading + +```scss +.Loading { + display: flex; + align-items: center; + justify-content: center; + padding: 48px; + color: var(--theme-color-fg-default-shy); +} +``` + +### Error + +```scss +.Error { + padding: 12px 16px; + background-color: rgba(239, 68, 68, 0.1); + border: 1px solid var(--theme-color-danger); + border-radius: 4px; + color: var(--theme-color-danger); + font-size: 13px; +} +``` + +--- + +## Button Patterns + +### Use PrimaryButton Variants Correctly + +| Variant | Use For | +| -------- | ------------------------------------------ | +| `Cta` | Primary action (Create, Save, Submit) | +| `Muted` | Secondary action (Cancel, Close, Refresh) | +| `Ghost` | Tertiary action (Edit, View, minor action) | +| `Danger` | Destructive action (Delete) | + +### Button Sizing + +- `Small` - In toolbars, table rows, compact spaces +- `Medium` - Modal footers, standalone actions +- `Large` - Rarely used, hero actions only + +--- + +## Spacing System + +Use consistent spacing based on 4px unit: + +| Token | Value | Use For | +| ----- | ----- | ------------------------ | +| `xs` | 4px | Tight spacing, icon gaps | +| `sm` | 8px | Related elements | +| `md` | 12px | Standard padding | +| `lg` | 16px | Section padding | +| `xl` | 24px | Large gaps | +| `xxl` | 32px | Major sections | + +--- + +## Typography + +### Use Text Component Types + +| Type | Use For | +| ----------------- | ------------------------------- | +| `Proud` | Panel titles, modal headers | +| `DefaultContrast` | Primary content, item names | +| `Default` | Body text, descriptions | +| `Shy` | Secondary text, hints, metadata | + +### Font Sizes + +- Headers: 14-16px +- Body: 13px +- Labels: 12px +- Small text: 11px +- Badges: 10px + +--- + +## Don'ts + +❌ **Don't use emojis** in buttons or labels +❌ **Don't use hardcoded colors** - always use CSS variables +❌ **Don't use bright/neon colors** - keep it muted +❌ **Don't use decorative icons** that don't convey meaning +❌ **Don't use rounded corners > 8px** - keep it subtle +❌ **Don't use shadows > 0.4 opacity** - stay subtle +❌ **Don't use animation duration > 200ms** - keep it snappy +❌ **Don't mix different styling approaches** - be consistent + +--- + +## Reference Components + +For working examples, see: + +- `packages/noodl-editor/src/editor/src/views/panels/schemamanager/` +- `packages/noodl-editor/src/editor/src/views/panels/databrowser/` +- `packages/noodl-editor/src/editor/src/views/panels/BackendServicesPanel/` + +--- + +_Last Updated: January 2026_ diff --git a/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-006-expressions-overhaul/PHASE-2B-COMPLETE.md b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-006-expressions-overhaul/PHASE-2B-COMPLETE.md new file mode 100644 index 0000000..e3f9722 --- /dev/null +++ b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-006-expressions-overhaul/PHASE-2B-COMPLETE.md @@ -0,0 +1,219 @@ +# Phase 2B: Inline Property Expressions - COMPLETE ✅ + +**Started:** 2026-01-10 +**Completed:** 2026-01-16 +**Status:** ✅ COMPLETE + +--- + +## Summary + +Inline property expressions are now fully functional! Users can set property values using JavaScript expressions that reference Noodl.Variables, Noodl.Objects, and Noodl.Arrays. Expressions update reactively when their dependencies change. + +--- + +## ✅ What Was Implemented + +### 1. ExpressionInput Component - COMPLETE ✅ + +**Files Created:** + +- `packages/noodl-core-ui/src/components/property-panel/ExpressionInput/ExpressionInput.tsx` +- `packages/noodl-core-ui/src/components/property-panel/ExpressionInput/ExpressionInput.module.scss` + +**Features:** + +- Monospace input field with "fx" badge +- Expression mode toggle +- Expand button to open full editor modal +- Error state display + +--- + +### 2. ExpressionEditorModal - COMPLETE ✅ + +**Files Created:** + +- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/ExpressionEditorModal.tsx` +- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/ExpressionEditorModal.module.scss` +- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/index.ts` + +**Features:** + +- Full-screen modal for editing complex expressions +- Uses JavaScriptEditor (CodeMirror) for syntax highlighting +- Help documentation showing available globals (Noodl.Variables, etc.) +- Apply/Cancel buttons + +--- + +### 3. PropertyPanelInput Integration - COMPLETE ✅ + +**Files Modified:** + +- `packages/noodl-core-ui/src/components/property-panel/PropertyPanelInput/PropertyPanelInput.tsx` + +**Features:** + +- Added expression-related props (supportsExpression, expressionMode, expression, etc.) +- Conditional rendering: expression input vs fixed input +- Mode change handlers + +--- + +### 4. PropertyPanelInputWithExpressionModal Wrapper - COMPLETE ✅ + +**Files Created:** + +- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/components/PropertyPanelInputWithExpressionModal.tsx` + +**Features:** + +- Combines PropertyPanelInput with ExpressionEditorModal +- Manages modal open/close state +- Handles expression expand functionality + +--- + +### 5. BasicType Property Editor Wiring - COMPLETE ✅ + +**Files Modified:** + +- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/BasicType.ts` + +**Features:** + +- Uses React for rendering via createRoot +- Supports expression mode toggle +- Integrates with ExpressionParameter model +- Uses ParameterValueResolver for safe value display + +--- + +### 6. Runtime Reactive Subscriptions - COMPLETE ✅ + +**Files Modified:** + +- `packages/noodl-runtime/src/node.js` + +**Features:** + +- `_evaluateExpressionParameter()` now sets up reactive subscriptions +- Uses `detectDependencies()` to find referenced Variables/Objects/Arrays +- Uses `subscribeToChanges()` to listen for dependency updates +- Re-queues input when dependencies change +- Cleanup in `_onNodeDeleted()` + +--- + +## 🎯 How It Works + +### User Experience + +1. Select a node with text/number properties +2. Click the "fx" button to toggle expression mode +3. Enter an expression like `Noodl.Variables.userName` +4. The value updates automatically when the variable changes +5. Click the expand button for a full code editor modal + +### Supported Expressions + +```javascript +// Simple variable access +Noodl.Variables.userName; + +// Math operations +Noodl.Variables.count * 2 + 1; + +// Ternary conditionals +Noodl.Variables.isLoggedIn ? 'Logout' : 'Login'; + +// String concatenation +'Hello, ' + Noodl.Variables.userName + '!'; + +// Math helpers +min(Noodl.Variables.price, 100); +max(10, Noodl.Variables.quantity); +round(Noodl.Variables.total); + +// Object/Array access +Noodl.Objects['user'].name; +Noodl.Arrays['items'].length; +``` + +--- + +## 📁 Files Changed + +### Created + +- `packages/noodl-core-ui/src/components/property-panel/ExpressionInput/ExpressionInput.tsx` +- `packages/noodl-core-ui/src/components/property-panel/ExpressionInput/ExpressionInput.module.scss` +- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/ExpressionEditorModal.tsx` +- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/ExpressionEditorModal.module.scss` +- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/index.ts` +- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/components/PropertyPanelInputWithExpressionModal.tsx` + +### Modified + +- `packages/noodl-core-ui/src/components/property-panel/PropertyPanelInput/PropertyPanelInput.tsx` +- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/BasicType.ts` +- `packages/noodl-runtime/src/node.js` (added reactive subscriptions) + +--- + +## 🐛 Bug Fixes (2026-01-16) + +### Modal Showing Stale Expression + +**Problem:** When editing an expression in the inline input field and then opening the modal, the modal showed the old expression text instead of the current one. + +**Root Cause:** `BasicType.ts` was not re-rendering after `onExpressionChange`, so the modal component never received the updated expression prop. + +**Fix:** Added `setTimeout(() => this.renderReact(), 0)` at the end of `onExpressionChange` in `BasicType.ts` to ensure the React tree updates with the new expression value. + +### Variable Changes Not Updating Node Values + +**Problem:** When using an expression like `Noodl.Variables.label_text`, changing the variable value didn't update the node's property until the expression was manually saved again. + +**Root Cause:** The subscription callback in `node.js` captured the old `paramValue` object in a closure. When the subscription fired, it re-queued the old expression, not the current one stored in `_inputValues`. + +**Fix:** Updated subscription management in `_evaluateExpressionParameter()`: + +- Subscriptions now track both the unsubscribe function AND the expression string +- When expression changes, old subscription is unsubscribed before creating a new one +- The callback now reads from `this._inputValues[portName]` (current value) instead of using a closure + +**Files Modified:** + +- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/BasicType.ts` +- `packages/noodl-runtime/src/node.js` + +--- + +## 🎓 Key Learnings + +### Context Issue + +The `evaluateExpression()` function in expression-evaluator.js expects either `undefined` or a Model scope as the second parameter. Passing `this.context` (the runtime context with editorConnection, styles, etc.) caused "scope.get is not a function" errors. **Solution:** Pass `undefined` to use the global Model. + +### Reactive Updates + +The expression-evaluator module already had `detectDependencies()` and `subscribeToChanges()` functions. We just needed to wire them into `_evaluateExpressionParameter()` in node.js to enable reactive updates. + +### Value Display + +Expression parameters are objects (`{ mode: 'expression', expression: '...', fallback: ... }`). When displaying the value in the properties panel, we need to extract the fallback value, not display the object. The `ParameterValueResolver.toString()` helper handles this. + +--- + +## 🚀 Future Enhancements + +1. **Canvas rendering** - Show expression indicator on node ports (TASK-006B) +2. **More property types** - Extend to color, enum, and other types +3. **Expression autocomplete** - IntelliSense for Noodl.Variables names +4. **Expression validation** - Real-time syntax checking + +--- + +**Last Updated:** 2026-01-16 diff --git a/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-COMPLETE.md b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-COMPLETE.md new file mode 100644 index 0000000..61fd8dd --- /dev/null +++ b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-COMPLETE.md @@ -0,0 +1,107 @@ +# TASK-007H: Schema Manager UI - COMPLETE + +## Summary + +Implemented a Schema Manager UI panel for viewing and managing database schemas in local SQLite backends. + +## Files Created + +### Schema Manager Panel Components + +- `packages/noodl-editor/src/editor/src/views/panels/schemamanager/SchemaPanel.tsx` - Main panel showing tables list +- `packages/noodl-editor/src/editor/src/views/panels/schemamanager/SchemaPanel.module.scss` - Panel styles +- `packages/noodl-editor/src/editor/src/views/panels/schemamanager/TableRow.tsx` - Expandable table row with columns +- `packages/noodl-editor/src/editor/src/views/panels/schemamanager/TableRow.module.scss` - Table row styles +- `packages/noodl-editor/src/editor/src/views/panels/schemamanager/index.ts` - Module exports + +## Files Modified + +### BackendManager.js + +Added IPC handlers for schema operations: + +- `backend:getSchema` - Get full schema for a backend +- `backend:getTableSchema` - Get single table schema +- `backend:getRecordCount` - Get record count for a table +- `backend:createTable` - Create a new table +- `backend:addColumn` - Add column to existing table + +### LocalBackendCard.tsx + +- Added "Schema" button (shows when backend is running) +- Added schema panel overlay state +- Integrated SchemaPanel component + +### LocalBackendCard.module.scss + +- Added `.SchemaPanelOverlay` styles for full-screen modal: + - Uses React Portal (`createPortal`) to render at `document.body` + - `z-index: 9999` ensures overlay above all UI elements + - Dark backdrop (`rgba(0, 0, 0, 0.85)`) + - Centered modal with max-width 900px + - Proper border-radius, shadow, and background styling + +## Features Implemented + +### SchemaPanel + +- Shows list of all tables in the backend +- Displays table count in header +- Refresh button to reload schema +- Empty state with "Create First Table" prompt (placeholder) +- Close button to dismiss panel + +### TableRow + +- Collapsible rows with expand/collapse toggle +- Table icon with "T" badge +- Shows field count and record count +- Expanded view shows: + - System columns (objectId, createdAt, updatedAt) + - User-defined columns with type badges + - Required indicator + - Default value display +- Type badges with color-coded data types: + - String (primary) + - Number (success/green) + - Boolean (notice/yellow) + - Date (purple) + - Object (pink) + - Array (indigo) + - Pointer/Relation (red) + - GeoPoint (teal) + - File (orange) + +## Usage + +1. Create and start a local backend +2. Click "Schema" button on the backend card +3. View tables and expand rows to see columns +4. Click "Refresh" to reload schema +5. Click "Close" to dismiss panel + +## Future Enhancements (Deferred) + +The following were scoped but not implemented in this initial version: + +- CreateTableModal - For creating new tables +- ColumnEditor - For editing column definitions +- SchemaEditor - Full table editor +- ExportDialog - Export schema to SQL formats (handlers exist in BackendManager) + +These can be added incrementally as needed. + +## Verification + +The implementation follows project patterns: + +- ✅ Uses theme tokens (no hardcoded colors) +- ✅ Uses IPC pattern from useLocalBackends +- ✅ Proper TypeScript types +- ✅ JSDoc documentation +- ✅ Module SCSS with CSS modules +- ✅ Follows existing component structure + +--- + +Completed: 15/01/2026 diff --git a/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-schema-manager-ui.md b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-schema-manager-ui.md new file mode 100644 index 0000000..e8a72da --- /dev/null +++ b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-schema-manager-ui.md @@ -0,0 +1,1189 @@ +# TASK-007H: Schema Manager UI + +## Overview + +Build a native Nodegx UI for viewing and editing database schemas in local backends, providing a visual interface over the existing SchemaManager implementation. + +**Parent Task:** TASK-007 (Integrated Local Backend) +**Phase:** H (Schema Management) +**Effort:** 16-20 hours (2-3 days) +**Priority:** HIGH (Unblocks user productivity) +**Dependencies:** TASK-007A (LocalSQL Adapter) + +--- + +## Objectives + +1. Create a schema browser panel showing all tables/collections +2. Build a visual schema editor for adding/modifying columns +3. Integrate with existing SchemaManager (no reimplementation) +4. Support all Nodegx field types (String, Number, Boolean, Date, Object, Array, Pointer, Relation) +5. Enable table creation from UI +6. Provide schema export (PostgreSQL, MySQL, Supabase formats) +7. Reuse existing Nodegx UI components (grid, forms, modals) + +--- + +## Background + +### Current State + +Users can create local backends through the Backend Services panel, but have no way to: +- View existing tables/schema +- Add new tables +- Modify table structure (add/remove columns) +- Understand what data structures exist + +The `SchemaManager` class in `TASK-007A` already provides all backend functionality: +```typescript +class SchemaManager { + createTable(schema: TableSchema): void; + addColumn(tableName: string, column: ColumnDefinition): void; + getTableSchema(tableName: string): TableSchema | null; + getSchema(): SchemaDefinition; + exportToPostgres(): string; + exportToSupabase(): string; +} +``` + +**We need to build the UI layer on top of this existing logic.** + +### Design Principles + +1. **No Backend Reimplementation** - Only UI, all logic delegates to SchemaManager +2. **Leverage Existing Components** - Reuse PropertyPanel, DataGrid, Modal patterns +3. **MVP First** - Ship basic functionality fast, enhance in Phase 3 +4. **Consistent with Nodegx** - Match editor's visual language + +--- + +## User Flows + +### Flow 1: View Schema + +``` +User clicks "Manage Schema" on Backend Services panel + ↓ +Schema Manager panel opens + ↓ +Shows list of tables with: + - Table name + - Column count + - Record count (async load) + - Last modified + ↓ +User clicks table name + ↓ +Expands to show columns with types +``` + +### Flow 2: Create Table + +``` +User clicks "New Table" button + ↓ +Modal opens: "Create Table" + - Table name input + - Optional: Add initial columns + ↓ +User enters "Products" + ↓ +User clicks "Add Column" + ↓ +Column editor appears: + - Name: "name" + - Type: String (dropdown) + - Required: checkbox + ↓ +User clicks "Create Table" + ↓ +SchemaManager.createTable() called + ↓ +Table appears in schema list +``` + +### Flow 3: Modify Schema + +``` +User clicks "Edit Schema" on table + ↓ +Schema editor opens: + - List of existing columns (read-only editing) + - Add column button + - Remove column button (with warning) + ↓ +User clicks "Add Column" + ↓ +Column form appears + ↓ +User fills: name="price", type=Number, required=true + ↓ +SchemaManager.addColumn() called + ↓ +Column added to table +``` + +--- + +## Implementation Steps + +### Step 1: Schema Panel Component (4 hours) + +Create the main schema browser UI component. + +**File:** `packages/noodl-editor/src/editor/src/views/panels/schemamanager/SchemaPanel.tsx` + +```typescript +import React, { useState, useEffect } from 'react'; +import { ipcRenderer } from 'electron'; +import styles from './SchemaPanel.module.css'; + +interface TableInfo { + name: string; + columns: ColumnDefinition[]; + recordCount?: number; + lastModified?: string; +} + +interface ColumnDefinition { + name: string; + type: string; + required: boolean; + default?: any; +} + +export function SchemaPanel({ backendId }: { backendId: string }) { + const [tables, setTables] = useState([]); + const [selectedTable, setSelectedTable] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadSchema(); + }, [backendId]); + + async function loadSchema() { + setLoading(true); + try { + // Call IPC to get schema from backend + const schema = await ipcRenderer.invoke('backend:getSchema', backendId); + + // Enrich with record counts (async, non-blocking) + const tablesWithCounts = await Promise.all( + schema.tables.map(async (table) => { + const count = await ipcRenderer.invoke('backend:getRecordCount', backendId, table.name); + return { ...table, recordCount: count }; + }) + ); + + setTables(tablesWithCounts); + } catch (error) { + console.error('Failed to load schema:', error); + } finally { + setLoading(false); + } + } + + function handleCreateTable() { + // Open create table modal + setShowCreateModal(true); + } + + function handleEditTable(tableName: string) { + setSelectedTable(tableName); + } + + if (loading) { + return
Loading schema...
; + } + + return ( +
+
+

Database Schema

+ +
+ +
+ {tables.length === 0 ? ( +
+

No tables yet

+ +
+ ) : ( + tables.map((table) => ( + handleEditTable(table.name)} + onExpand={() => setSelectedTable( + selectedTable === table.name ? null : table.name + )} + expanded={selectedTable === table.name} + /> + )) + )} +
+ + {showCreateModal && ( + setShowCreateModal(false)} + onSuccess={loadSchema} + /> + )} + + {selectedTable && ( + setSelectedTable(null)} + onUpdate={loadSchema} + /> + )} +
+ ); +} +``` + +**File:** `packages/noodl-editor/src/editor/src/views/panels/schemamanager/TableRow.tsx` + +```typescript +import React from 'react'; +import styles from './TableRow.module.css'; + +interface TableRowProps { + table: TableInfo; + expanded: boolean; + onExpand: () => void; + onEdit: () => void; +} + +export function TableRow({ table, expanded, onExpand, onEdit }: TableRowProps) { + return ( +
+
+
+ {expanded ? '▼' : '▶'} +
+
{table.name}
+
+ + {table.columns.length} {table.columns.length === 1 ? 'field' : 'fields'} + + {table.recordCount !== undefined && ( + + {table.recordCount.toLocaleString()} {table.recordCount === 1 ? 'record' : 'records'} + + )} +
+ +
+ + {expanded && ( +
+ + + + + + + + + + + {table.columns.map((col) => ( + + + + + + + ))} + +
Field NameTypeRequiredDefault
{col.name} + + + {col.required ? '✓' : ''} + + {col.default || '—'} +
+
+ )} +
+ ); +} + +function TypeBadge({ type }: { type: string }) { + const typeColors = { + String: '#3b82f6', + Number: '#10b981', + Boolean: '#f59e0b', + Date: '#8b5cf6', + Object: '#ec4899', + Array: '#6366f1', + Pointer: '#ef4444', + Relation: '#ef4444', + }; + + return ( + + {type} + + ); +} +``` + +--- + +### Step 2: Create Table Modal (3 hours) + +**File:** `packages/noodl-editor/src/editor/src/views/panels/schemamanager/CreateTableModal.tsx` + +```typescript +import React, { useState } from 'react'; +import { ipcRenderer } from 'electron'; +import { Modal } from '@noodl-core-ui/components/modal'; +import { TextInput } from '@noodl-core-ui/components/inputs/TextInput'; +import { PrimaryButton, SecondaryButton } from '@noodl-core-ui/components/buttons'; +import styles from './CreateTableModal.module.css'; + +interface CreateTableModalProps { + backendId: string; + onClose: () => void; + onSuccess: () => void; +} + +export function CreateTableModal({ backendId, onClose, onSuccess }: CreateTableModalProps) { + const [tableName, setTableName] = useState(''); + const [columns, setColumns] = useState([ + { name: 'name', type: 'String', required: true } + ]); + const [creating, setCreating] = useState(false); + const [error, setError] = useState(null); + + async function handleCreate() { + // Validation + if (!tableName.trim()) { + setError('Table name is required'); + return; + } + + if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(tableName)) { + setError('Table name must start with a letter and contain only letters, numbers, and underscores'); + return; + } + + if (columns.length === 0) { + setError('At least one column is required'); + return; + } + + setCreating(true); + setError(null); + + try { + await ipcRenderer.invoke('backend:createTable', backendId, { + name: tableName, + columns: columns + }); + + onSuccess(); + onClose(); + } catch (err: any) { + setError(err.message || 'Failed to create table'); + } finally { + setCreating(false); + } + } + + function handleAddColumn() { + setColumns([...columns, { name: '', type: 'String', required: false }]); + } + + function handleRemoveColumn(index: number) { + setColumns(columns.filter((_, i) => i !== index)); + } + + function handleColumnChange(index: number, field: string, value: any) { + const newColumns = [...columns]; + newColumns[index] = { ...newColumns[index], [field]: value }; + setColumns(newColumns); + } + + return ( + +
+
+ + +
+ Use lowercase with underscores (e.g., "blog_posts") +
+
+ +
+
+

Initial Columns

+ +
+ +
+ {columns.map((col, index) => ( + handleColumnChange(index, field, value)} + onRemove={() => handleRemoveColumn(index)} + canRemove={columns.length > 1} + /> + ))} +
+ +
+ Note: objectId, createdAt, and updatedAt are added automatically +
+
+ + {error && ( +
{error}
+ )} + +
+ + +
+
+
+ ); +} +``` + +**File:** `packages/noodl-editor/src/editor/src/views/panels/schemamanager/ColumnEditor.tsx` + +```typescript +import React from 'react'; +import { TextInput } from '@noodl-core-ui/components/inputs/TextInput'; +import { Select } from '@noodl-core-ui/components/inputs/Select'; +import { Checkbox } from '@noodl-core-ui/components/inputs/Checkbox'; +import styles from './ColumnEditor.module.css'; + +const FIELD_TYPES = [ + { value: 'String', label: 'String' }, + { value: 'Number', label: 'Number' }, + { value: 'Boolean', label: 'Boolean' }, + { value: 'Date', label: 'Date' }, + { value: 'Object', label: 'Object' }, + { value: 'Array', label: 'Array' }, + { value: 'Pointer', label: 'Pointer' }, + { value: 'Relation', label: 'Relation' }, +]; + +interface ColumnEditorProps { + column: ColumnDefinition; + onChange: (field: string, value: any) => void; + onRemove: () => void; + canRemove: boolean; +} + +export function ColumnEditor({ column, onChange, onRemove, canRemove }: ColumnEditorProps) { + return ( +
+
+
+ onChange('name', value)} + placeholder="field_name" + /> +
+ +
+ +
+ +
+ + +
+ + ) : ( + <> +
+
{result}
+
+ +
+ + + +
+ + )} +
+ + ); +} +``` + +--- + +## Files to Create + +``` +packages/noodl-editor/src/editor/src/views/panels/schemamanager/ +├── SchemaPanel.tsx # Main schema browser +├── SchemaPanel.module.css +├── TableRow.tsx # Individual table display +├── TableRow.module.css +├── CreateTableModal.tsx # New table creation +├── CreateTableModal.module.css +├── SchemaEditor.tsx # Edit existing table schema +├── SchemaEditor.module.css +├── ColumnEditor.tsx # Column configuration UI +├── ColumnEditor.module.css +├── ExportSchemaDialog.tsx # Export to SQL +├── ExportSchemaDialog.module.css +└── index.ts # Public exports + +packages/noodl-editor/src/main/src/ipc/ +└── backend-schema-handlers.ts # IPC handlers for schema operations +``` + +## Files to Modify + +``` +packages/noodl-editor/src/editor/src/views/panels/BackendServicesPanel/BackendCard.tsx + - Add "Manage Schema" button + - Add "Browse Data" button + +packages/noodl-editor/src/main/src/ipc/index.ts + - Register backend-schema-handlers + +packages/noodl-editor/src/editor/src/views/panels/index.ts + - Export SchemaPanel for use in other panels +``` + +--- + +## Testing Checklist + +### Schema Viewing +- [ ] Schema panel opens for running backend +- [ ] All tables displayed with accurate counts +- [ ] Table expand/collapse works +- [ ] Column details show correct types +- [ ] Record counts load asynchronously +- [ ] Empty state shows when no tables exist + +### Table Creation +- [ ] Create table modal opens +- [ ] Table name validation works +- [ ] Can add multiple initial columns +- [ ] Column removal works +- [ ] Type dropdown shows all types +- [ ] Required checkbox toggles +- [ ] Pointer/Relation shows target selector +- [ ] Table created successfully +- [ ] Schema refreshes after creation + +### Schema Editing +- [ ] Schema editor opens for existing table +- [ ] Existing columns displayed (read-only) +- [ ] Can add new columns +- [ ] Column validation works +- [ ] Duplicate column names rejected +- [ ] Columns saved successfully +- [ ] Schema refreshes after save + +### Schema Export +- [ ] Export dialog opens +- [ ] Format selector shows all options +- [ ] PostgreSQL export generates valid SQL +- [ ] MySQL export generates valid SQL +- [ ] SQLite export generates valid SQL +- [ ] Supabase export includes RLS policies +- [ ] Copy to clipboard works +- [ ] Download file works + +### Integration +- [ ] "Manage Schema" button disabled when backend stopped +- [ ] Schema panel only accessible for running backends +- [ ] Schema changes reflected in data browser +- [ ] Schema changes reflected in node property dropdowns + +### Edge Cases +- [ ] Schema panel handles backend with no tables +- [ ] Create table handles name conflicts +- [ ] Schema editor handles invalid column types +- [ ] Export handles large schemas (100+ tables) +- [ ] UI handles backend disconnect gracefully + +--- + +## Success Criteria + +1. Users can view all tables and columns in their local backend +2. Users can create new tables with initial columns +3. Users can add columns to existing tables +4. Users can export schema to PostgreSQL, MySQL, or Supabase +5. Schema changes are immediately reflected in the UI +6. All operations properly delegate to existing SchemaManager +7. UI follows Nodegx design patterns and component standards +8. Performance: Schema loads in <500ms for 50 tables +9. Zero reimplementation of backend logic (only UI layer) + +--- + +## Dependencies + +**Requires:** +- TASK-007A (LocalSQL Adapter with SchemaManager) +- TASK-007B (Backend Server with IPC) +- TASK-007C (Backend Services Panel) + +**Blocked by:** None + +**Blocks:** +- TASK-007I (Data Browser) - needs schema info for table selection +- Phase 3 AI features - schema used for AI-powered suggestions + +--- + +## Estimated Session Breakdown + +| Session | Focus | Hours | +|---------|-------|-------| +| 1 | SchemaPanel + TableRow components | 4 | +| 2 | CreateTableModal + ColumnEditor | 3 | +| 3 | SchemaEditor component | 4 | +| 4 | IPC handlers + integration | 2 | +| 5 | ExportSchemaDialog + polish | 3 | +| 6 | Testing + bug fixes | 4 | +| **Total** | | **20** | + +--- + +## Future Enhancements (Phase 3) + +These features are **out of scope for MVP** but should be considered for Phase 3: + +1. **Visual Relationship Diagram** - Canvas view showing table relationships +2. **Schema Migrations UI** - Track and apply schema changes over time +3. **AI Schema Suggestions** - Claude suggests optimal schema based on description +4. **Schema Versioning** - Integrate with git for schema history +5. **Column Removal** - Safe column deletion with data migration +6. **Index Management** - UI for creating/managing database indexes +7. **Virtual Fields** - Define computed columns using Nodegx expressions +8. **Schema Templates** - Pre-built schemas for common use cases (users, posts, products) +9. **Validation Rules UI** - Visual editor for field validation +10. **Schema Diff** - Compare schemas between dev/staging/prod + +--- + +## Notes + +- This task focuses **only** on schema management UI +- Data browsing/editing is covered in TASK-007I +- All backend logic already exists in SchemaManager (TASK-007A) +- Reuse existing Nodegx UI components wherever possible +- Follow Storybook patterns from noodl-core-ui +- Schema panel should feel like a natural extension of Backend Services panel +- Export feature enables migration path to production databases diff --git a/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-COMPLETE.md b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-COMPLETE.md new file mode 100644 index 0000000..b8bb690 --- /dev/null +++ b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-COMPLETE.md @@ -0,0 +1,205 @@ +# TASK-007I: Data Browser Panel - COMPLETE + +## Summary + +Implemented a full-featured data browser panel for viewing and editing records in local backend SQLite tables, providing a spreadsheet-like interface with inline editing, search, pagination, and CRUD operations. + +## Status: ✅ Complete + +## Date: 2026-01-15 + +--- + +## What Was Implemented + +### 1. Backend IPC Handlers (BackendManager.js) + +Added four new IPC handlers for data operations: + +- **`backend:queryRecords`** - Query records with pagination, sorting, search filters +- **`backend:createRecord`** - Create a new record in a table +- **`backend:saveRecord`** - Update an existing record (inline edit) +- **`backend:deleteRecord`** - Delete a single record + +These handlers wrap the existing LocalSQLAdapter CRUD methods (query, create, save, delete) and expose them to the renderer process. + +### 2. DataBrowser Component (`panels/databrowser/`) + +Created a complete data browser panel with the following components: + +#### DataBrowser.tsx (Main Component) + +- Table selector dropdown +- Search input with cross-field text search +- Pagination with page navigation (50 records per page) +- Bulk selection and delete operations +- CSV export functionality +- Loading states and error handling +- New record modal trigger + +#### DataGrid.tsx (Spreadsheet View) + +- Column headers with type badges (String, Number, Boolean, Date, Object, Array) +- System columns (objectId, createdAt, updatedAt) displayed as read-only +- Inline editing - click any editable cell to edit +- Row selection with checkbox +- Select all/none toggle +- Delete action per row +- Scrollable with sticky header + +#### CellEditor.tsx (Inline Editor) + +- Type-aware input controls: + - **String**: Text input + - **Number**: Number input with step="any" + - **Boolean**: Checkbox with immediate save + - **Date**: datetime-local input + - **Object/Array**: Multi-line JSON editor with Save/Cancel buttons +- Auto-focus and select on mount +- Enter to save, Escape to cancel (for simple types) +- Validation for numbers and JSON parsing +- Error display for invalid input + +#### NewRecordModal.tsx (Create Record Form) + +- Form fields generated from column schema +- Type-aware input controls for each field type +- Required field validation +- Default values based on column defaults or type defaults +- Success callback to refresh data grid + +### 3. Styling + +All components use theme CSS variables per UI-STYLING-GUIDE.md: + +- `--theme-color-bg-*` for backgrounds +- `--theme-color-fg-*` for text +- `--theme-color-border-*` for borders +- `--theme-color-primary*` for accents +- `--theme-color-success/danger/notice` for status colors + +### 4. Integration + +Added "Data" button to LocalBackendCard (visible when backend is running): + +- Opens DataBrowser in a full-screen portal overlay +- Same pattern as Schema panel + +--- + +## Files Created/Modified + +### Created Files: + +``` +packages/noodl-editor/src/editor/src/views/panels/databrowser/ +├── DataBrowser.tsx - Main data browser component +├── DataBrowser.module.scss - Styles for DataBrowser +├── DataGrid.tsx - Spreadsheet grid component +├── DataGrid.module.scss - Styles for DataGrid +├── CellEditor.tsx - Inline cell editor +├── CellEditor.module.scss - Styles for CellEditor +├── NewRecordModal.tsx - Create record modal +├── NewRecordModal.module.scss - Styles for modal +└── index.ts - Exports +``` + +### Modified Files: + +``` +packages/noodl-editor/src/main/src/local-backend/BackendManager.js + - Added queryRecords(), createRecord(), saveRecord(), deleteRecord() methods + - Added corresponding IPC handlers + +packages/noodl-editor/src/editor/src/views/panels/BackendServicesPanel/LocalBackendCard/LocalBackendCard.tsx + - Added DataBrowser import + - Added showDataBrowser state + - Added "Data" button + - Added DataBrowser portal rendering +``` + +--- + +## How To Use + +1. Start a local backend from the Backend Services panel +2. Click the "Data" button on the running backend card +3. Select a table from the dropdown (auto-selects first table if available) +4. Browse records with pagination +5. Click any editable cell to edit inline +6. Use "+ New Record" to add records +7. Use checkboxes for bulk selection and delete +8. Use "Export CSV" to download current view + +--- + +## Technical Notes + +### Query API + +The query API supports: + +```javascript +{ + collection: string, // Table name + limit: number, // Max records (default 50) + skip: number, // Pagination offset + where: object, // Filter conditions ($or, contains, etc.) + sort: string[], // Sort order (e.g., ['-createdAt']) + count: boolean // Return total count for pagination +} +``` + +### Search Implementation + +Search uses `$or` with `contains` operator across all String columns and objectId. The LocalSQLAdapter converts this to SQLite LIKE queries. + +### Type Handling + +- System fields (objectId, createdAt, updatedAt) are always read-only +- Boolean cells save immediately on checkbox toggle +- Object/Array cells require explicit Save button due to JSON editing +- Dates are stored as ISO8601 strings + +--- + +## Future Improvements (Not in scope) + +- Column sorting by clicking headers +- Filter by specific field values +- Relation/Pointer field expansion +- Inline record duplication +- Undo for delete operations +- Import CSV/JSON + +--- + +## Verification Checklist + +- [x] IPC handlers added to BackendManager.js +- [x] DataBrowser component created +- [x] DataGrid with inline editing +- [x] CellEditor with type-aware controls +- [x] NewRecordModal for creating records +- [x] CSV export functionality +- [x] Pagination working +- [x] Search functionality +- [x] Bulk delete operations +- [x] Integration with LocalBackendCard +- [x] Theme-compliant styling +- [x] No hardcoded colors +- [x] JSDoc comments added + +--- + +## Related Tasks + +- **TASK-007A**: Core SQLite adapter ✅ +- **TASK-007B**: Workflow integration ✅ +- **TASK-007C**: Schema management ✅ +- **TASK-007D**: Backend lifecycle ✅ +- **TASK-007E**: Express HTTP API ✅ +- **TASK-007F**: Node integration ✅ +- **TASK-007G**: UI components ✅ +- **TASK-007H**: Schema Panel UI ✅ +- **TASK-007I**: Data Browser ✅ (this task) diff --git a/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-data-browser-editor.md b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-data-browser-editor.md new file mode 100644 index 0000000..a870e58 --- /dev/null +++ b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-data-browser-editor.md @@ -0,0 +1,1290 @@ +# TASK-007I: Data Browser & Editor UI + +## Overview + +Build a native Nodegx UI for browsing, searching, and editing records in local backend tables, providing a visual interface similar to Parse Dashboard's data browser. + +**Parent Task:** TASK-007 (Integrated Local Backend) +**Phase:** I (Data Management) +**Effort:** 16-20 hours (2-3 days) +**Priority:** HIGH (Unblocks user productivity) +**Dependencies:** TASK-007A (LocalSQL Adapter), TASK-007H (Schema Manager) + +--- + +## Objectives + +1. Create a data browser panel for viewing records in tables +2. Build inline editing capabilities for all field types +3. Implement search and filtering +4. Support pagination for large datasets +5. Enable record creation, update, and deletion +6. Handle all Nodegx field types (String, Number, Boolean, Date, Object, Array, Pointer, Relation) +7. Provide bulk operations (delete multiple, export) +8. Integrate with SchemaManager for type-aware editing + +--- + +## Background + +### Current State + +With TASK-007H, users can now manage database schemas, but they cannot: +- View the actual data in tables +- Add/edit/delete records +- Search or filter data +- Import/export data + +The `LocalSQLAdapter` already provides all backend CRUD operations: +```typescript +class LocalSQLAdapter { + async query(options: QueryOptions): Promise; + async fetch(options: FetchOptions): Promise; + async create(options: CreateOptions): Promise; + async save(options: SaveOptions): Promise; + async delete(options: DeleteOptions): Promise; +} +``` + +**We need to build the UI layer on top of these existing operations.** + +### Design Principles + +1. **Spreadsheet-like Interface** - Users think of databases as tables +2. **Inline Editing** - Click to edit, no separate forms for simple edits +3. **Type-Aware Editors** - Each field type gets appropriate input (date picker, JSON editor, etc.) +4. **Performance** - Handle tables with 100K+ records via pagination +5. **Safety** - Confirm destructive operations + +--- + +## User Flows + +### Flow 1: Browse Data + +``` +User clicks "Browse Data" on Backend Services panel + ↓ +Table selector appears (list of all tables) + ↓ +User selects "products" table + ↓ +Data Browser opens showing: + - Table name + - Total record count + - Paginated grid (50 records per page) + - Search bar + - Action buttons (New Record, Export, etc.) + ↓ +User scrolls through records +User clicks page 2 +``` + +### Flow 2: Edit Record + +``` +User clicks on a cell in the grid + ↓ +Cell becomes editable + ↓ +Type-specific editor appears: + - String: Text input + - Number: Number input + - Boolean: Checkbox + - Date: Date picker + - Object/Array: JSON editor modal + - Pointer: Table selector + record picker + ↓ +User edits value + ↓ +User presses Enter or clicks outside + ↓ +Value saves to database + ↓ +Cell shows updated value +``` + +### Flow 3: Create Record + +``` +User clicks "New Record" button + ↓ +Modal opens with form fields for all columns + ↓ +User fills in: + - name: "Widget Pro" + - price: 99.99 + - inStock: true + ↓ +User clicks "Create" + ↓ +Record saved to database + ↓ +Grid refreshes with new record +``` + +### Flow 4: Search & Filter + +``` +User types "pro" in search bar + ↓ +Grid filters to show only records with "pro" in any field + ↓ +User clicks "Advanced Filters" + ↓ +Filter builder opens: + - Field: "price" + - Operator: "greater than" + - Value: 50 + ↓ +Grid shows only records where price > 50 +``` + +### Flow 5: Bulk Operations + +``` +User clicks checkboxes on multiple records + ↓ +Bulk action bar appears: "3 records selected" + ↓ +User clicks "Delete Selected" + ↓ +Confirmation dialog: "Delete 3 records?" + ↓ +User confirms + ↓ +Records deleted, grid refreshes +``` + +--- + +## Implementation Steps + +### Step 1: Data Browser Component (5 hours) + +**File:** `packages/noodl-editor/src/editor/src/views/panels/databrowser/DataBrowser.tsx` + +```typescript +import React, { useState, useEffect, useCallback } from 'react'; +import { ipcRenderer } from 'electron'; +import { DataGrid } from './DataGrid'; +import { TableSelector } from './TableSelector'; +import { SearchBar } from './SearchBar'; +import { FilterBuilder } from './FilterBuilder'; +import styles from './DataBrowser.module.css'; + +interface DataBrowserProps { + backendId: string; + initialTable?: string; + onClose?: () => void; +} + +export function DataBrowser({ backendId, initialTable, onClose }: DataBrowserProps) { + const [tables, setTables] = useState([]); + const [selectedTable, setSelectedTable] = useState(initialTable || null); + const [schema, setSchema] = useState(null); + const [records, setRecords] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [page, setPage] = useState(0); + const [pageSize] = useState(50); + const [loading, setLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [filters, setFilters] = useState(null); + const [selectedRecords, setSelectedRecords] = useState>(new Set()); + const [showNewRecord, setShowNewRecord] = useState(false); + + // Load table list on mount + useEffect(() => { + loadTables(); + }, [backendId]); + + // Load data when table, page, search, or filters change + useEffect(() => { + if (selectedTable) { + loadData(); + } + }, [selectedTable, page, searchQuery, filters]); + + async function loadTables() { + try { + const schema = await ipcRenderer.invoke('backend:getSchema', backendId); + setTables(schema.tables.map((t: any) => t.name)); + + if (!selectedTable && schema.tables.length > 0) { + setSelectedTable(schema.tables[0].name); + } + } catch (error) { + console.error('Failed to load tables:', error); + } + } + + async function loadData() { + if (!selectedTable) return; + + setLoading(true); + try { + // Load schema for this table + const tableSchema = await ipcRenderer.invoke('backend:getTableSchema', backendId, selectedTable); + setSchema(tableSchema); + + // Build query + const query: any = { + collection: selectedTable, + limit: pageSize, + skip: page * pageSize, + sort: ['-createdAt'] // Newest first + }; + + // Apply search + if (searchQuery) { + // Search across all string fields + query.where = { + $or: tableSchema.columns + .filter((col: any) => col.type === 'String') + .map((col: any) => ({ + [col.name]: { contains: searchQuery } + })) + }; + } + + // Apply filters + if (filters) { + query.where = { ...query.where, ...filters }; + } + + // Load records + const result = await ipcRenderer.invoke('backend:query', backendId, query); + setRecords(result.results); + + // Load total count + const countQuery = { ...query, count: true, limit: 0 }; + const countResult = await ipcRenderer.invoke('backend:query', backendId, countQuery); + setTotalCount(countResult.count || 0); + + } catch (error) { + console.error('Failed to load data:', error); + } finally { + setLoading(false); + } + } + + async function handleSaveCell(recordId: string, field: string, value: any) { + try { + await ipcRenderer.invoke('backend:saveRecord', backendId, selectedTable, recordId, { + [field]: value + }); + + // Update local state + setRecords(records.map(r => + r.objectId === recordId ? { ...r, [field]: value } : r + )); + } catch (error) { + console.error('Failed to save cell:', error); + throw error; + } + } + + async function handleDeleteRecord(recordId: string) { + if (!confirm('Delete this record?')) return; + + try { + await ipcRenderer.invoke('backend:deleteRecord', backendId, selectedTable, recordId); + loadData(); // Refresh + } catch (error) { + console.error('Failed to delete record:', error); + } + } + + async function handleBulkDelete() { + if (selectedRecords.size === 0) return; + + if (!confirm(`Delete ${selectedRecords.size} records?`)) return; + + try { + for (const recordId of selectedRecords) { + await ipcRenderer.invoke('backend:deleteRecord', backendId, selectedTable, recordId); + } + setSelectedRecords(new Set()); + loadData(); // Refresh + } catch (error) { + console.error('Failed to bulk delete:', error); + } + } + + function handleExport() { + // Export to CSV + const csv = recordsToCSV(records, schema!); + downloadCSV(csv, `${selectedTable}.csv`); + } + + const totalPages = Math.ceil(totalCount / pageSize); + + return ( +
+
+ + + + +
+ + +
+
+ + {selectedRecords.size > 0 && ( +
+ {selectedRecords.size} records selected + + +
+ )} + +
+ {loading ? ( +
Loading...
+ ) : schema ? ( + { + const newSelected = new Set(selectedRecords); + if (selected) { + newSelected.add(id); + } else { + newSelected.delete(id); + } + setSelectedRecords(newSelected); + }} + onSaveCell={handleSaveCell} + onDeleteRecord={handleDeleteRecord} + /> + ) : ( +
+ Select a table to browse data +
+ )} +
+ +
+ + Showing {page * pageSize + 1}-{Math.min((page + 1) * pageSize, totalCount)} of {totalCount.toLocaleString()} records + +
+ + + Page {page + 1} of {totalPages} + + +
+
+ + {showNewRecord && schema && ( + setShowNewRecord(false)} + onSuccess={() => { + setShowNewRecord(false); + loadData(); + }} + /> + )} +
+ ); +} + +function recordsToCSV(records: any[], schema: TableSchema): string { + const headers = schema.columns.map(c => c.name).join(','); + const rows = records.map(record => + schema.columns.map(col => { + const value = record[col.name]; + if (value === null || value === undefined) return ''; + if (typeof value === 'object') return JSON.stringify(value); + if (typeof value === 'string' && value.includes(',')) return `"${value}"`; + return value; + }).join(',') + ); + + return [headers, ...rows].join('\n'); +} + +function downloadCSV(csv: string, filename: string) { + const blob = new Blob([csv], { type: 'text/csv' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} +``` + +--- + +### Step 2: Data Grid Component (5 hours) + +**File:** `packages/noodl-editor/src/editor/src/views/panels/databrowser/DataGrid.tsx` + +```typescript +import React, { useState } from 'react'; +import { CellEditor } from './CellEditor'; +import styles from './DataGrid.module.css'; + +interface DataGridProps { + schema: TableSchema; + records: any[]; + selectedRecords: Set; + onSelectRecord: (id: string, selected: boolean) => void; + onSaveCell: (recordId: string, field: string, value: any) => Promise; + onDeleteRecord: (recordId: string) => void; +} + +export function DataGrid({ + schema, + records, + selectedRecords, + onSelectRecord, + onSaveCell, + onDeleteRecord +}: DataGridProps) { + const [editingCell, setEditingCell] = useState<{ recordId: string; field: string } | null>(null); + + // Show system fields + user fields + const displayColumns = [ + { name: 'objectId', type: 'String', readOnly: true }, + { name: 'createdAt', type: 'Date', readOnly: true }, + { name: 'updatedAt', type: 'Date', readOnly: true }, + ...schema.columns + ]; + + async function handleCellSave(recordId: string, field: string, value: any) { + try { + await onSaveCell(recordId, field, value); + setEditingCell(null); + } catch (error) { + console.error('Failed to save cell:', error); + alert('Failed to save changes'); + } + } + + function handleCellClick(recordId: string, field: string, readOnly: boolean) { + if (readOnly) return; + setEditingCell({ recordId, field }); + } + + function formatCellValue(value: any, type: string): string { + if (value === null || value === undefined) return ''; + + switch (type) { + case 'Boolean': + return value ? '✓' : ''; + case 'Date': + return new Date(value).toLocaleString(); + case 'Object': + case 'Array': + return JSON.stringify(value); + default: + return String(value); + } + } + + return ( +
+ + + + + {displayColumns.map(col => ( + + ))} + + + + + {records.map(record => ( + + + {displayColumns.map(col => { + const isEditing = editingCell?.recordId === record.objectId && + editingCell?.field === col.name; + const value = record[col.name]; + const readOnly = col.readOnly || col.name === 'objectId'; + + return ( + + ); + })} + + + ))} + +
+ 0} + onChange={(e) => { + records.forEach(r => onSelectRecord(r.objectId, e.target.checked)); + }} + /> + +
+ {col.name} + {col.type} +
+
Actions
+ onSelectRecord(record.objectId, e.target.checked)} + /> + handleCellClick(record.objectId, col.name, readOnly)} + > + {isEditing ? ( + handleCellSave(record.objectId, col.name, newValue)} + onCancel={() => setEditingCell(null)} + /> + ) : ( +
+ {formatCellValue(value, col.type)} +
+ )} +
+ +
+ + {records.length === 0 && ( +
+ No records found +
+ )} +
+ ); +} +``` + +--- + +### Step 3: Cell Editor Component (4 hours) + +**File:** `packages/noodl-editor/src/editor/src/views/panels/databrowser/CellEditor.tsx` + +```typescript +import React, { useState, useEffect, useRef } from 'react'; +import { TextInput } from '@noodl-core-ui/components/inputs/TextInput'; +import { Checkbox } from '@noodl-core-ui/components/inputs/Checkbox'; +import { DatePicker } from '@noodl-core-ui/components/inputs/DatePicker'; +import styles from './CellEditor.module.css'; + +interface CellEditorProps { + value: any; + type: string; + onSave: (value: any) => void; + onCancel: () => void; +} + +export function CellEditor({ value, type, onSave, onCancel }: CellEditorProps) { + const [editValue, setEditValue] = useState(value); + const [showJsonEditor, setShowJsonEditor] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, []); + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSave(); + } else if (e.key === 'Escape') { + onCancel(); + } + } + + function handleSave() { + let finalValue = editValue; + + // Type conversion + switch (type) { + case 'Number': + finalValue = parseFloat(editValue); + if (isNaN(finalValue)) { + alert('Invalid number'); + return; + } + break; + case 'Boolean': + finalValue = Boolean(editValue); + break; + case 'Date': + if (typeof editValue === 'string') { + finalValue = new Date(editValue).toISOString(); + } + break; + case 'Object': + case 'Array': + try { + finalValue = JSON.parse(editValue); + } catch (err) { + alert('Invalid JSON'); + return; + } + break; + } + + onSave(finalValue); + } + + // Type-specific editors + switch (type) { + case 'Boolean': + return ( +
+ { + setEditValue(checked); + onSave(checked); + }} + /> +
+ ); + + case 'Date': + return ( +
+ setEditValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleSave} + /> +
+ ); + + case 'Object': + case 'Array': + return ( +
+ {showJsonEditor ? ( + { + setShowJsonEditor(false); + onCancel(); + }} + /> + ) : ( + + )} +
+ ); + + case 'Number': + return ( +
+ setEditValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleSave} + /> +
+ ); + + case 'String': + default: + return ( +
+ setEditValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleSave} + /> +
+ ); + } +} +``` + +**File:** `packages/noodl-editor/src/editor/src/views/panels/databrowser/JsonEditorModal.tsx` + +```typescript +import React, { useState } from 'react'; +import { Modal } from '@noodl-core-ui/components/modal'; +import { PrimaryButton, SecondaryButton } from '@noodl-core-ui/components/buttons'; +import styles from './JsonEditorModal.module.css'; + +interface JsonEditorModalProps { + value: any; + onSave: (value: any) => void; + onCancel: () => void; +} + +export function JsonEditorModal({ value, onSave, onCancel }: JsonEditorModalProps) { + const [text, setText] = useState(JSON.stringify(value, null, 2)); + const [error, setError] = useState(null); + + function handleSave() { + try { + const parsed = JSON.parse(text); + onSave(parsed); + } catch (err: any) { + setError(err.message); + } + } + + return ( + +
+