473 lines
21 KiB
Plaintext
473 lines
21 KiB
Plaintext
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
|