From 2a1308bb874f8128b289415ca6cbae37a3dd95a3 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Tue, 16 Jun 2026 01:29:35 +0800 Subject: [PATCH] fix(example-crm): make formula fields compute + allow historical closed deals; bump objectui MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release-prep browser test of example-crm surfaced three "passes build, silently wrong at runtime" defects: - Formula fields (`full_name`, `is_closed`, `expected_revenue`) referenced bare field identifiers (`amount`, `status`, `first_name`). Formula scope exposes only `{record, previous, input, os}` with no field flattening, so bare names resolve to nothing and every formula evaluated to null. Rewrite to `record.`. - `expected_revenue` additionally divided by the int literal `100`. cel-js has no `double × int` arithmetic overload, so ` / 100` faults and the formula returns null. Use the float literal `100.0` (both operands `double`). The systemic guardrail for this footgun is tracked separately. - `opp_close_date_not_past` validation fired on every insert, rejecting the legitimate `closed_won`/`closed_lost` seed rows that carry historical close dates (5 of 9 wins were dropped, breaking the pipeline dashboard's "Won This Quarter" KPIs). Scope the rule to open stages only. Also bumps `.objectui-sha` 26da1a2a3731 → ac2de168d487 and rebuilds the bundled @objectstack/console dist for the release. Verified live: opportunities seed 4 → 12; expected_revenue computes (84000, 180000, …); full_name / is_closed populate. Co-Authored-By: Claude Opus 4.8 (1M context) --- .objectui-sha | 2 +- examples/app-crm/src/objects/contact.object.ts | 2 +- examples/app-crm/src/objects/lead.object.ts | 2 +- examples/app-crm/src/objects/opportunity.object.ts | 10 +++++++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.objectui-sha b/.objectui-sha index 6255f2b42e..ea2dbba844 100644 --- a/.objectui-sha +++ b/.objectui-sha @@ -1 +1 @@ -26da1a2a37312669c4ad9cffd3b1fb7714afd74b +ac2de168d48712ce31213a5dae05fb5a29b0c5e2 diff --git a/examples/app-crm/src/objects/contact.object.ts b/examples/app-crm/src/objects/contact.object.ts index d5823a7a16..0aeb6207b7 100644 --- a/examples/app-crm/src/objects/contact.object.ts +++ b/examples/app-crm/src/objects/contact.object.ts @@ -23,7 +23,7 @@ export const Contact = ObjectSchema.create({ }), full_name: Field.formula({ label: 'Full Name', - expression: cel`(first_name == null ? '' : first_name + ' ') + last_name`, + expression: cel`(record.first_name == null ? '' : record.first_name + ' ') + record.last_name`, }), email: Field.email({ label: 'Email', diff --git a/examples/app-crm/src/objects/lead.object.ts b/examples/app-crm/src/objects/lead.object.ts index 806cd7bfe7..70792e7f31 100644 --- a/examples/app-crm/src/objects/lead.object.ts +++ b/examples/app-crm/src/objects/lead.object.ts @@ -78,7 +78,7 @@ export const Lead = ObjectSchema.create({ /** CEL formula: is this lead in a terminal converted/disqualified state? */ is_closed: Field.formula({ label: 'Is Closed', - expression: cel`status == "converted" || status == "disqualified"`, + expression: cel`record.status == "converted" || record.status == "disqualified"`, }), }, diff --git a/examples/app-crm/src/objects/opportunity.object.ts b/examples/app-crm/src/objects/opportunity.object.ts index 66af04b5ed..9241242323 100644 --- a/examples/app-crm/src/objects/opportunity.object.ts +++ b/examples/app-crm/src/objects/opportunity.object.ts @@ -45,7 +45,11 @@ export const Opportunity = ObjectSchema.create({ }), expected_revenue: Field.formula({ label: 'Expected Revenue', - expression: cel`(amount == null ? 0 : amount) * (probability == null ? 0 : probability) / 100`, + // NOTE: the divisor is the float literal `100.0`, not `100`. cel-js has no + // `double int` arithmetic overload, so ` / 100` + // faults at runtime and the formula silently evaluates to null. Using a + // float literal keeps both operands `double`. See objectstack-formula skill. + expression: cel`(record.amount == null ? 0.0 : record.amount) * (record.probability == null ? 0.0 : record.probability) / 100.0`, }), close_date: Field.date({ label: 'Close Date', @@ -88,9 +92,9 @@ export const Opportunity = ObjectSchema.create({ type: 'cross_field' as const, name: 'opp_close_date_not_past', label: 'Close Date Must Be Future', - description: 'Prevent setting close_date to a date in the past on new records.', + description: 'Prevent back-dating the close_date of an OPEN opportunity. Closed (won/lost) deals legitimately carry a historical close date, so they are exempt.', fields: ['close_date'], - condition: P`has(record.close_date) && record.close_date < now()`, + condition: P`has(record.close_date) && record.close_date < now() && record.stage != "closed_won" && record.stage != "closed_lost"`, message: 'Close Date must be today or a future date.', events: ['insert'], },