|
|
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.
|
|
|
|
|
|
Claude’s 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 ~93–101).
|
|
|
|
|
|
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 & 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
|