Skip to content

feat: typed computed fields - ComputedNumber and ComputedText (#2197)#2199

Open
Caedous wants to merge 5 commits into
mainfrom
feature/2197-expanded-computed-fields
Open

feat: typed computed fields - ComputedNumber and ComputedText (#2197)#2199
Caedous wants to merge 5 commits into
mainfrom
feature/2197-expanded-computed-fields

Conversation

@Caedous

@Caedous Caedous commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Ticket

#2197

Description

Extends the computed field expression engine to handle strings, numbers and booleans in and out, and splits the field into ComputedNumber and ComputedText so each has a fixed type-returned. Typing is strict with no implicit coercion, per Peter's direction on the issue: ill-typed expressions are rejected when the notebook is designed rather than producing a silently wrong value, and the two-field split avoids union return types that would complicate export flows such as GeoPackage.

Operators are discrete and unambiguous: + - * / % are numeric only, & is string concatenation, && || ! are boolean only, < > <= >= compare two numbers or two strings, == != require matching types, and the ternary requires a boolean condition with same-typed branches. Mixed-type inputs are fine so long as the overall result matches the field's type — {vegType} == 'test' || {flag} ? 3 : 5 is a valid ComputedNumber, while ... ? 3 : 'Not valid' is rejected. Field references keep the brace syntax ({Field-ID}).

Proposed Changes

The engine in data-model compiles each expression bottom-up into a typed closure, inferring every node's type at compile time and selecting operator implementations once. Field reference types come from the spec's type-returned values via a shared FAIMS_TYPE_TO_EXPR_TYPE map, built at notebook load in compileUiSpecConditionals, and each component's expression is validated against its required return type (number for ComputedNumber, string for ComputedText). Mismatches throw ExpressionError naming the types involved. The source cache is removed, since the same source legitimately compiles differently against different field types and compile-once-at-load makes it redundant.

In forms, ComputedField is renamed to ComputedNumber and ComputedText is added: one shared read-only component with per-type value schemas in a single ComputedFields folder, following the DateFields multi-spec pattern. The recompute resolves scope values per each referenced field's declared type instead of coercing everything to Number; an empty string counts as missing, so partially filled forms stay blank rather than showing a partial result.

The Designer gains two chooser cards (Computed Number under NUMBERS, Computed Text under TEXT) sharing one editor. The insert chips now list all referenceable fields — number, text and checkbox, with derived fields excluded — and the helper text shows a per-variant example. Invalid expressions are compiled as the user types and the error is shown inline under the expression input; if one still gets through via raw JSON, the uiSpec compilation logs it and the field stays blank.

inline errors

The expression tests are rewritten for the typed engine (26 cases) covering the ticket's fireRisk example, concatenation, string comparison, mixed-type inputs with a matching result type, and the compile-time error battery: type mismatches per operator, ternary rules, wrong return type, unknown fields, and brace validation. The user documentation is updated, replacing the Computed Value page with Computed Number and adding a Computed Text page under text fields.

How to Test

  1. Build data-model, forms and web; run the data-model and forms test suites.
  2. In the Designer, open the ADD A FIELD dialog: Computed Number appears on the NUMBERS tab and Computed Text on the TEXT tab.
computed number computed text
  1. Regression: add two number fields and a Computed Number with an arithmetic expression; the chips insert braced references and the preview computes as before.
computed number configured
  1. Recreate the ticket's fireRisk example as a Computed Text, with a text field vegType and a number field vegHeight, using the expression {vegType} == 'hedge' ? ({vegHeight} < 3 ? 'Low' : ({vegHeight} < 5 ? 'Medium' : 'High')) : 'Medium'. Entering hedge/2 shows Low, hedge/4 Medium, hedge/6 High, and tree with any height Medium.
fireRisk example fireRisk example 2 fireRisk example 3 fireRisk example 4
  1. Concatenation: a Computed Text with {vegType} & '-test' renders tree-test and stays blank until vegType has a value.
concat
  1. Strict typing: enter {vegHeight} & 'x' as a Computed Text expression. The console reports ExpressionError: & (concatenation) requires strings, got number and string at compile time and the field stays blank.
invalid expression

Checklist

  • I have confirmed all commits have been signed.
  • I have added JSDoc style comments to any new functions or classes.
  • I have added tests for new code if appropriate
  • Relevant documentation such as READMEs, guides, and class comments are updated.

Signed-off-by: Caedous <tombevs_2@hotmail.com>
@Caedous Caedous self-assigned this Jul 20, 2026
@Caedous
Caedous requested a review from PeterBaker0 July 20, 2026 04:03
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown

E2E ✅ passed

Result ✅ Suite passed
Artifacts Download e2e-artifacts-29986811651.zip
Run View workflow run

After downloading and extracting the .zip file, open artifacts/index.html (failures listed first).

Updated for commit 656d8d3

@PeterBaker0 PeterBaker0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some documentation updates.

We should also write a migration to move the old ComputedValue -> ComputedNumber. While there might not be any in the wild, would be good to do so to follow best practise here for backwards compatibility. There is existing hooks for 'migrate notebooks on startup' which run these migrations.

The Computed Number's key feature is the **Expression** text area, which
defines the calculated value. Each referenced field is written by
wrapping its Field ID in braces, e.g.
`{Wet-Soil-Mass-g} * {Number-of-Samples}`. Below the expression, a chip

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Chip is shown" is out of date

| **Expression** | A text area where you define the calculation using literals, operators, and field references in single-brace syntax (e.g., `{Width} * {Height}`). |
| **Insert field** | A searchable picker of the referenceable fields in the form. Selecting one inserts its braced reference into the expression. |

The expression is typed and checked when the {{notebook}} is designed —

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"rather than a silent wrong answer" reads like an agent correcting an earlier prompt


## Tips

- **Use the field picker to build expressions** rather than typing Field

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of date

- **The result stays blank until all referenced fields have values.**
This is intentional — a partially complete record shows no value rather
than a misleading partial calculation.
- **Type mismatches are reported when the {{notebook}} is designed**, not

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rewrite to not refer to previous lesser implementations - "not hidden at collection time" is not necessary

Caedous added 2 commits July 23, 2026 16:41
Signed-off-by: Caedous <tombevs_2@hotmail.com>
Signed-off-by: Caedous <tombevs_2@hotmail.com>

@stevecassidy stevecassidy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that's missing is a check when fields are deleted that they don't occur in a computed expression. We have this for conditions, see findFieldDependencyReferences, and field delete checks for references before allowing deletion. We should have the same for computed fields.

<FormHelperText>
Reference other numeric fields by wrapping their ID in braces.
{numericFieldCount > 0 &&
Reference other fields by wrapping their ID in braces.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some inline help would be useful on writing expressions, maybe just the list of operators you have in the docs might be enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants