You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

473 lines
21 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

ask:Explore Report Editor elements and inspector
Now let me design the implementation plan based on this exploration.
Now let me read a few critical files to verify the plan details before writing the final plan.
Now I have everything I need. Let me write the final plan.
Claudes Plan
Plan: Expression Field Element for Report Editor
Context
The Report Editor currently supports DBTextElement for displaying individual data-bound field values. Users need the ability to compute derived values — e.g., gross - tare, IF(gross > 0, gross - tare, 0) — directly in the report. No expression/formula capability exists today.
Approach
Add a new element type ExpressionElement (type: 'expr') that stores a formula string and evaluates it in preview mode. In design mode it shows the raw expression. The expression editor is embedded inline in the ObjectInspector (no modal needed).
Files to Create
1. frontend/src/components/ReportEditor/utils/expressionEvaluator.js
Standalone recursive-descent parser/evaluator. No eval() or Function() constructor.
Grammar:
expression = comparison
comparison = additive (CMP additive)? ← only one comparator per IF condition
additive = multiplicative (('+' | '-') multiplicative)*
multiplicative = unary (('*' | '/') unary)*
unary = '-' unary | primary
primary = NUMBER | STRING | FIELD_REF | 'IF' '(' expr ',' expr ',' expr ')' | '(' expr ')'
Token types: NUMBER, STRING, FIELD_REF ([...]), IDENT (for IF), OP, CMP, COMMA, LPAREN, RPAREN, EOF
Public API:
export function evaluateExpression(expression, resolveField)
// resolveField('[vehicle.gross]') → actual value (string/number)
// Returns computed value, '#ERR' on parse error, '#DIV/0' on division by zero
The evaluator receives a resolveField(bracketed) closure — not raw data — so it stays decoupled from React context and is independently testable.
Field resolution inside evaluator: Numeric fields become Number(value) for arithmetic; string fields stay as strings. Arithmetic on strings falls back to string concatenation (same as spreadsheet behavior).
Error handling: entire call wrapped in try/catch → returns '#ERR' on any failure.
2. frontend/src/components/ReportEditor/ExpressionField.jsx
Render component for the canvas. Modelled directly on DBTextField.jsx (129 lines).
Same imports: useReportData, useBandContext, resolveDBTextValue, ResizeHandles, useElementDrag, useElementSelection, getElementStyle
Design mode: displayContent = element.expression || '(expr)'
Preview mode:
function buildResolver(reportData, parentBandId, currentBandData) {
return (bracketed) => {
const path = bracketed.slice(1, -1).trim(); // strip [ and ]
const dotIdx = path.indexOf('.');
if (dotIdx === -1) {
return resolveDBTextValue(reportData, '', path, parentBandId, { currentBandData });
}
return resolveDBTextValue(reportData, path.slice(0, dotIdx), path.slice(dotIdx + 1), parentBandId, { currentBandData });
};
}
const rawValue = evaluateExpression(element.expression, buildResolver(...));
const displayContent = formatValue(rawValue, element.dataType || 'general');
formatValue is copied locally from DBTextField.jsx (same function verbatim).
CSS class: expr-field (blue-violet tint rgba(103, 126, 234, 0.05) to distinguish from green db-text-field).
isUnresolved = !element.expression.
3. frontend/src/components/ReportEditor/ExpressionEditorPanel.jsx
Inline panel component used inside ObjectInspector for expr elements. Uses useRef for the textarea to support insert-at-cursor.
UI layout:
[ Textarea showing raw expression — editable directly ]
Insert Field: [ select dropdown ▼ ] [ Insert ]
Operators: [ + ] [ - ] [ * ] [ / ] [ ( ] [ ) ]
[ IF( , , ) ]
Comparisons: [ = ] [ != ] [ > ] [ < ] [ >= ] [ <= ]
[ Clear ]
Data Type: [ general ▼ ] Alignment: [ left ▼ ]
Insert-at-cursor logic:
const insertAtCursor = (text) => {
const ta = textareaRef.current;
const start = ta.selectionStart, end = ta.selectionEnd;
const next = expression.slice(0, start) + text + expression.slice(end);
onUpdate({ expression: next });
requestAnimationFrame(() => {
ta.selectionStart = ta.selectionEnd = start + text.length;
ta.focus();
});
};
Available fields list: Built using the same band-detection logic as ObjectInspector does for dbtext — detect parentBand, resolve bandDataArray, call getAvailableFields(). Fields displayed as vehicle.gross, vehicle.tare (or just gross when inside a band). These are the values inserted between [ and ].
Props: { element, onUpdate, reportData, allElements }
Files to Modify
4. frontend/src/components/ReportEditor/models/Element.jsx
Add ExpressionElement class immediately after DBTextElement class (line ~158):
export class ExpressionElement extends Element {
constructor({ id, x, y, width = 10, height = 1, expression = '', dataType = 'general', alignment = 'left' }) {
super({ id, type: 'expr', x, y });
this.width = width;
this.height = height;
this.expression = expression;
this.dataType = dataType;
this.alignment = alignment;
}
getBounds() { return { minX: this.x, minY: this.y, maxX: this.x + this.width - 1, maxY: this.y + this.height - 1 }; }
getDimensions() { return { width: this.width, height: this.height }; }
toJSON() { return { ...super.toJSON(), width: this.width, height: this.height, expression: this.expression, dataType: this.dataType, alignment: this.alignment }; }
}
Add expr: ExpressionElement to the elementClasses map inside fromJSON (line ~93101).
5. frontend/src/components/ReportEditor/EditorCanvas.jsx
Three surgical changes:
Import: import ExpressionField from './ExpressionField';
In renderElement() switch — add after dbtext branch:
} else if (element.type === 'expr') {
return <ExpressionField {...commonProps} previewMode={previewMode} parentBandId={parentBandId} />;
}
In handleCanvasClick — add after addDBText branch:
} else if (toolMode === 'addExpr') {
onAddElement({ id: `expr-${Date.now()}`, type: 'expr', x: col, y: row, width: 10, height: 1, expression: '', dataType: 'general', alignment: 'left' });
}
6. frontend/src/components/ReportEditor/Toolbar.jsx
Add one button after the DB Text button:
<button className={`toolbar-button ${toolMode === 'addExpr' ? 'active' : ''}`}
onClick={() => onToolChange('addExpr')} title="Add Expression Field">
fx
</button>
7. frontend/src/components/ReportEditor/ObjectInspector.jsx
Two changes:
Import ExpressionEditorPanel at top.
Add section after the dbtext block (after line ~73):
{element.type === 'expr' && (
<ExpressionEditorPanel element={element} onUpdate={onUpdate} reportData={reportData} allElements={allElements} />
)}
8. frontend/src/components/ReportEditor/elements.css
Add .expr-field block (identical structure to .db-text-field but with blue-violet rgba(103,126,234,...) colors instead of green).
9. frontend/src/components/ReportEditor/ObjectInspector.css
Add .expr-btn-row utility class for the operator button rows:
.expr-btn-row { display: flex; flex-wrap: wrap; gap: 3px; margin-bottom: 6px; }
.expr-btn-row button { padding: 2px 6px; font-size: 11px; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; background: #fff; }
.expr-btn-row button:hover { background: #e8eeff; border-color: #667eea; }
ResizeHandles coverage
ResizeHandles.jsx handles resize for elements with width/height. Check whether it already covers any type with these properties generically, or if 'dbtext' is explicitly named — if the latter, add || element.type === 'expr' to those conditions.
Verification
Toolbar: Click fx button → canvas cursor changes, click canvas → ExpressionElement created (type 'expr' in elements array).
Design mode: Selected expr element shows raw expression string (e.g., [vehicle.gross] - [vehicle.tare]).
Inspector - field insert: Open ConfigPanel with vehicle data, select expr element, pick vehicle.gross from dropdown, click Insert → [vehicle.gross] appended at cursor in textarea.
Inspector - operators: Click + → + inserted at cursor.
Preview mode (math): Expression [vehicle.gross] - [vehicle.tare] with gross=28500, tare=8500 → displays 20000.
Preview mode (IF): IF([vehicle.tare] > 0, [vehicle.gross] - [vehicle.tare], 0) → displays 20000.
Error cases: Invalid expression → shows #ERR; division by zero → #DIV/0.
Band context: Expr element inside a band resolves [fieldPath] from current band row.
Persistence: Save report → load report → expr element reloads with expression intact.
Resize: Resize handles appear and work the same as on dbtext elements.
claude created a skill to help him implement the plan:
dispatch skill
Base directory for this skill: c:\dev_projects\ScalesApp\.claude\skills\dispatch
You are the **development orchestrator** for this project. Do not implement changes yourself — always dispatch to the appropriate specialized agent.
## Routing
| Work involves... | Agent |
|-------------------------------------------------------------------|-------------------|
| `frontend/` — React, JSX, hooks, components, state, CSS | `react-expert-dev` |
| `backend/` — Django, Python, models, views, serializers, migrations | `python-pro` |
| Both layers | Both, in parallel |
## Rules
1. Each agent is called **exactly once** — never call the same agent twice.
2. For tasks that span both layers, launch both agents in a **single message** using two parallel Task tool calls.
3. Every agent prompt must include the no-subagent constraint (see below).
## Dispatch Steps
1. **Analyze** — identify which directories and layers the task touches.
2. **Compose** a detailed prompt for each relevant agent:
- Full task description and context
- Relevant file paths to examine
- Clear acceptance criteria
- Append this constraint verbatim at the end:
> **CONSTRAINT: Do NOT use the Task tool. Do not spawn any subagents. Complete all work directly using your available file and shell tools.**
3. **Launch** via the Task tool:
- Frontend → `subagent_type: react-expert-dev`
- Backend → `subagent_type: python-pro`
- Both → two calls in a single message (parallel)
4. **Report** — after all agents complete, summarize:
- What each agent did
- Any required follow-up steps (e.g., run migrations, restart services)
- Any issues or pre-existing problems the agents flagged
ARGUMENTS: Add Expression Field element to the Report Editor. This is a frontend-only change.
## Summary
Add a new `ExpressionElement` (type: `'expr'`) to the report editor that evaluates math expressions in preview mode.
## Files to Create
### 1. `frontend/src/components/ReportEditor/utils/expressionEvaluator.js`
Standalone recursive-descent parser/evaluator. No `eval()` or `Function()`.
Grammar:
- `expression = comparison`
- `comparison = additive (CMP additive)?` — one comparator per IF condition
- `additive = multiplicative (('+' | '-') multiplicative)*`
- `multiplicative = unary (('*' | '/') unary)*`
- `unary = '-' unary | primary`
- `primary = NUMBER | STRING | FIELD_REF | 'IF' '(' expr ',' expr ',' expr ')' | '(' expr ')'`
Token types: `NUMBER`, `STRING`, `FIELD_REF` (text wrapped in `[...]`), `IDENT` (for `IF`), `OP` (`+`, `-`, `*`, `/`), `CMP` (`=`, `!=`, `>`, `<`, `>=`, `<=`), `COMMA`, `LPAREN`, `RPAREN`, `EOF`
Public API:
```js
export function evaluateExpression(expression, resolveField)
// resolveField('[vehicle.gross]') → actual value
// Returns computed value, '#ERR' on error, '#DIV/0' on div by zero
```
`resolveField` is a closure passed in — the evaluator doesn't know about React or reportData.
Field refs in arithmetic: `Number(resolveField(ref))` for arithmetic operations; if NaN, treat as 0.
All errors are caught and return `'#ERR'`. Division by zero returns `'#DIV/0'`.
Implement the tokenizer, recursive-descent parser building an AST, and AST evaluator as three separate internal functions. Export only `evaluateExpression`.
### 2. `frontend/src/components/ReportEditor/ExpressionField.jsx`
Render component. Model it exactly on `DBTextField.jsx` (at `frontend/src/components/ReportEditor/DBTextField.jsx` — read it first).
Differences from DBTextField:
- Imports `evaluateExpression` from `./utils/expressionEvaluator`
- Design mode: `displayContent = element.expression || '(expr)'`
- Preview mode: builds a `resolveField` closure using `resolveDBTextValue` from `./utils/dataResolver`, then calls `evaluateExpression(element.expression, resolveField)`
- The resolveField closure: strips `[` and `]` from the bracketed path, splits on first `.` to get objectKey and fieldPath, then calls `resolveDBTextValue(reportData, objectKey, fieldPath, parentBandId, { currentBandData })`. If no `.` in the path, treat as band-relative (objectKey = `''`).
- `isUnresolved = !element.expression`
- CSS class: `expr-field` (not `db-text-field`)
- Copies `formatValue` function verbatim from `DBTextField.jsx`
- Display text inside span: `displayContent || '(No Expr)'`
### 3. `frontend/src/components/ReportEditor/ExpressionEditorPanel.jsx`
Props: `{ element, onUpdate, reportData, allElements }`
Imports: `React`, `{ useState, useRef }`, `{ getAvailableFields, getAvailableObjects }` from `./utils/dataResolver`
Available fields logic (replicates the `dbtext` block in `ObjectInspector.jsx` lines 36-73):
- Detect parentBand from allElements
- If in a band, resolve bandDataArray and call `getAvailableFields(bandDataArray[0])` → fields are band-relative paths (no `objectKey.` prefix)
- If not in a band: for each objectKey from `getAvailableObjects(reportData)`, call `getAvailableFields(reportData[objectKey])` and prefix each with `objectKey.` → produces `vehicle.gross`, `vehicle.tare`, etc.
- Combine all into a flat `availableFields` array of strings
State: `const [selectedField, setSelectedField] = useState('')`
Ref: `const textareaRef = useRef(null)`
Insert-at-cursor:
```js
const insertAtCursor = (text) => {
const ta = textareaRef.current;
if (!ta) { onUpdate({ expression: (element.expression || '') + text }); return; }
const start = ta.selectionStart;
const end = ta.selectionEnd;
const current = element.expression || '';
const next = current.slice(0, start) + text + current.slice(end);
onUpdate({ expression: next });
requestAnimationFrame(() => {
ta.selectionStart = ta.selectionEnd = start + text.length;
ta.focus();
});
};
```
UI (use existing inspector CSS classes where possible):
```jsx
<div className="inspector-section">
<div className="section-title">Expression</div>
<textarea ref={textareaRef} value={element.expression || ''}
onChange={(e) => onUpdate({ expression: e.target.value })}
rows={3} style={{ width: '100%', fontFamily: 'monospace', fontSize: '11px', resize: 'vertical', boxSizing: 'border-box' }}
/>
<div className="property-row">
<select value={selectedField} onChange={e => setSelectedField(e.target.value)} style={{ flex: 1 }}>
<option value="">-- field --</option>
{availableFields.map(f => <option key={f} value={f}>{f}</option>)}
</select>
<button onClick={() => { if (selectedField) insertAtCursor(`[${selectedField}]`); }}>Insert</button>
</div>
<div className="expr-btn-row">
{['+', '-', '*', '/'].map(op => <button key={op} onClick={() => insertAtCursor(` ${op} `)}>{op}</button>)}
<button onClick={() => insertAtCursor('(')}>(</button>
<button onClick={() => insertAtCursor(')')}>)</button>
</div>
<div className="expr-btn-row">
<button onClick={() => insertAtCursor('IF( , , )')}>IF( , , )</button>
</div>
<div className="expr-btn-row">
{['=', '!=', '>', '<', '>=', '<='].map(op => <button key={op} onClick={() => insertAtCursor(` ${op} `)}>{op}</button>)}
</div>
<div className="expr-btn-row">
<button onClick={() => onUpdate({ expression: '' })}>Clear</button>
</div>
<div className="property-row">
<label>Data Type:</label>
<select value={element.dataType || 'general'} onChange={e => onUpdate({ dataType: e.target.value })}>
<option value="general">General</option>
<option value="number">Number</option>
<option value="date">Date</option>
<option value="time">Time</option>
<option value="datetime">Date &amp; Time</option>
</select>
</div>
<div className="property-row">
<label>Alignment:</label>
<select value={element.alignment || 'left'} onChange={e => onUpdate({ alignment: e.target.value })}>
<option value="left">Left</option>
<option value="center">Center</option>
<option value="right">Right</option>
</select>
</div>
</div>
```
## Files to Modify
### 4. `frontend/src/components/ReportEditor/models/Element.jsx`
Read the file first. Add `ExpressionElement` class after `DBTextElement` class. Same structure as `DBTextElement` but replaces `objectKey`/`fieldPath` with `expression`:
```js
export class ExpressionElement extends Element {
constructor({ id, x, y, width = 10, height = 1, expression = '', dataType = 'general', alignment = 'left' }) {
super({ id, type: 'expr', x, y });
this.width = width; this.height = height;
this.expression = expression; this.dataType = dataType; this.alignment = alignment;
}
getBounds() { return { minX: this.x, minY: this.y, maxX: this.x + this.width - 1, maxY: this.y + this.height - 1 }; }
getDimensions() { return { width: this.width, height: this.height }; }
toJSON() { return { ...super.toJSON(), width: this.width, height: this.height, expression: this.expression, dataType: this.dataType, alignment: this.alignment }; }
}
```
Also add `expr: ExpressionElement` to the `elementClasses` map in `fromJSON`.
### 5. `frontend/src/components/ReportEditor/EditorCanvas.jsx`
Read the file first. Make 3 surgical changes:
1. Add import: `import ExpressionField from './ExpressionField';`
2. In `renderElement()` — add `expr` case after `dbtext` case (same props pattern as dbtext)
3. In canvas click handler — add `addExpr` tool mode after `addDBText` (creates element with type `'expr'`, x, y, width=10, height=1, expression='', dataType='general', alignment='left')
### 6. `frontend/src/components/ReportEditor/Toolbar.jsx`
Read first. Add `fx` toolbar button after the DB Text button. Same pattern as other tool buttons. `toolMode === 'addExpr'` → active class.
### 7. `frontend/src/components/ReportEditor/ObjectInspector.jsx`
Read first. Two changes:
1. Add import: `import ExpressionEditorPanel from './ExpressionEditorPanel';`
2. After the `dbtext` block (around line 73), add:
```jsx
{element.type === 'expr' && (
<ExpressionEditorPanel element={element} onUpdate={onUpdate} reportData={reportData} allElements={allElements} />
)}
```
### 8. `frontend/src/components/ReportEditor/elements.css`
Read first. Add `.expr-field` block after `.db-text-field` block. Identical structure but use blue-violet `rgba(103, 126, 234, ...)` colors instead of green.
### 9. `frontend/src/components/ReportEditor/ObjectInspector.css`
Read first. Add at the end:
```css
.expr-btn-row {
display: flex;
flex-wrap: wrap;
gap: 3px;
margin-bottom: 6px;
}
.expr-btn-row button {
padding: 2px 6px;
font-size: 11px;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
background: #fff;
}
.expr-btn-row button:hover {
background: #e8eeff;
border-color: #667eea;
}
```
### 10. `frontend/src/components/ReportEditor/ResizeHandles.jsx`
Read first. Find where `'dbtext'` is mentioned and add `'expr'` alongside it — for resize handles support and pixel size calculation.
## Important Notes
- Read every file before modifying it
- Match existing code style exactly
- Do not add comments, docstrings, or cleanup unrelated code
- The evaluator must use recursive-descent parsing only — no eval(), no Function(), no regex-based math evaluation
Expression Field — Done
New files
expressionEvaluator.js — Pure recursive-descent parser/evaluator (no eval). Supports +, -, *, /, IF(cond, a, b), comparisons (=, !=, >, <, >=, <=), field refs [path], numbers and strings.
ExpressionField.jsx — Canvas element component. Design mode shows raw expression; preview mode evaluates it.
ExpressionEditorPanel.jsx — Inline expression builder in the inspector.
Modified files
models/Element.jsx — Added ExpressionElement class + registered 'expr' in fromJSON
EditorCanvas.jsx — Import + addExpr click handler + expr render case
Toolbar.jsx — Added fx button
ObjectInspector.jsx — Renders ExpressionEditorPanel for expr elements
elements.css — Blue-violet .expr-field styles
ObjectInspector.css — .expr-btn-row button styles
ResizeHandles.jsx — 'expr' added alongside 'dbtext' in 3 places
How to use
Click the fx button in the toolbar
Click on the canvas to place an expression element
Select it — the Expression panel appears in the inspector
Pick a field from the dropdown and click Insert, or type directly
Use the operator buttons (+, -, *, /), IF( , , ), and comparison buttons to build the expression
Switch to preview mode to see the computed result
Expression syntax
Example Result
[vehicle.gross] - [vehicle.tare] Subtraction
[vehicle.gross] * 0.1 Multiplication
IF([vehicle.tare] > 0, [vehicle.gross] - [vehicle.tare], 0) Conditional
([vehicle.gross] - [vehicle.tare]) / 1000 Grouped