Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/24735-expression-form-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@objectstack/spec': patch
'@objectstack/platform-objects': patch
---

Field and object designer forms: the `expression` help text now states what the engine does instead of claiming the expression "makes it read-only". On a `formula` field the expression is computed on read with no stored column; on any other field the engine computes it from the row each write stores and stores the result, so a client-sent value never stands (only `readonly: true` makes a client value a readonly drop). The object designer's field grid now shows the `expression` row for every field type, as the field designer already did, since `FieldSchema` accepts `expression` on any type. The zh-CN, ja-JP and es-ES translations of both rows are updated to match.
19 changes: 14 additions & 5 deletions packages/lint/src/validate-predicate-path-refs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,12 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
// It is 89 today, an ADDITION of ONE: #23735 offered the field form's
// `referenceDeletedWhen` composite row, gated like `referenceVia` on
// `data.type == 'text'`. It reaches the literal-comparison census below.
expect(predicates, 'the shipped metadata forms carry no predicates at all').toBe(89);
// It is 88 today, a REMOVAL of ONE: #24735 dropped the object form's
// `object :: expression :: data.type == 'formula'` gate. `FieldSchema`
// accepts `expression` on every type and the engine computes and stores it
// on a non-formula field, so the formula-only row hid a live key; the row
// is now shown for every type, as the field form's own row already was.
expect(predicates, 'the shipped metadata forms carry no predicates at all').toBe(88);

const findings = validatePredicatePathRefs(corrupted);
expect(findings).toHaveLength(predicates);
Expand Down Expand Up @@ -793,7 +798,9 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
// `data.type == 'email'`, a single-quoted literal comparison.
// It is 66 today: #23735's `field :: referenceDeletedWhen` row is gated on
// `data.type == 'text'`, a single-quoted literal comparison.
expect(comparisons, 'no shipped predicate carries an `==`/`!=` literal comparison').toBe(66);
// It is 65 today: #24735 removed the object form's `expression` gate,
// `data.type == 'formula'`, one single-quoted literal comparison.
expect(comparisons, 'no shipped predicate carries an `==`/`!=` literal comparison').toBe(65);

const rhsFindings = validatePredicatePathRefs(corrupted)
.filter((f) => f.rule === PREDICATE_RHS_PATH_SHAPED);
Expand Down Expand Up @@ -822,7 +829,9 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
// tree adds over its merge base — the two corpora were enumerated and
// differenced by `<form>::<field>::<source>`, and the object-form half of
// the two-entry delta is exactly it. #6254's own measurement was 16 and
// stays 16 — that number is history, this one is a census.
// stays 16 — that number is history, this one is a census. It is 18 today:
// #24735 removed the `expression` row's `data.type == 'formula'` gate, so
// that row is shown for every field type.
const objectForm = structuredClone(METADATA_FORM_REGISTRY.object) as Record<string, unknown>;
let restored = 0;
const debare = (node: unknown): void => {
Expand Down Expand Up @@ -852,10 +861,10 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
expect(
restored,
"the object form's `data.type`-rooted predicates are no longer where this test looks",
).toBe(19);
).toBe(18);

const findings = validatePredicatePathRefs({ views: [objectForm] });
expect(findings).toHaveLength(19);
expect(findings).toHaveLength(18);
expect(new Set(findings.map((f) => f.rule))).toEqual(new Set([PREDICATE_PATH_UNROOTED]));
expect(findings[0].message).toContain('`type`');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
},
"fields.expression": {
label: "Expression",
helpText: "CEL formula expression"
helpText: "CEL expression the field's value is computed from. On a formula field: computed on read, no stored column. On any other field: computed by the engine from the row each write stores, and stored, so a client-sent value never stands."
},
"fields.returnType": {
label: "Return Type",
Expand Down Expand Up @@ -763,7 +763,7 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
},
expression: {
label: "Expression",
helpText: "CEL expression to calculate this field (makes it read-only)"
helpText: "CEL expression the field's value is computed from. On a formula field: computed on read, no stored column. On any other field: computed by the engine from the row each write stores, and stored, so a client-sent value never stands."
},
returnType: {
label: "Return Type",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
},
"fields.expression": {
label: "Expresión",
helpText: "Expresión de fórmula CEL"
helpText: "Expresión CEL con la que se calcula el valor del campo. En un campo de fórmula: se calcula al leer, sin columna almacenada. En cualquier otro campo: el motor lo calcula a partir de la fila que guarda cada escritura y lo almacena, por lo que un valor enviado por el cliente nunca prevalece."
},
"fields.returnType": {
label: "Tipo de retorno",
Expand Down Expand Up @@ -763,7 +763,7 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
},
expression: {
label: "Expresión",
helpText: "Expresión CEL para calcular este campo (lo hace de solo lectura)"
helpText: "Expresión CEL con la que se calcula el valor del campo. En un campo de fórmula: se calcula al leer, sin columna almacenada. En cualquier otro campo: el motor lo calcula a partir de la fila que guarda cada escritura y lo almacena, por lo que un valor enviado por el cliente nunca prevalece."
},
returnType: {
label: "Tipo de retorno",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
},
"fields.expression": {
label: "式",
helpText: "CEL 数式"
helpText: "このフィールドの値を計算する CEL 式。数式フィールドでは読み取り時に計算され、列は保存されません。その他のフィールドでは、書き込みごとに保存される行からエンジンが計算して保存するため、クライアントが送信した値は反映されません。"
},
"fields.returnType": {
label: "戻り値の型",
Expand Down Expand Up @@ -763,7 +763,7 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
},
expression: {
label: "式",
helpText: "このフィールドを計算する CEL 式(読み取り専用化)"
helpText: "このフィールドの値を計算する CEL 式。数式フィールドでは読み取り時に計算され、列は保存されません。その他のフィールドでは、書き込みごとに保存される行からエンジンが計算して保存するため、クライアントが送信した値は反映されません。"
},
returnType: {
label: "数式の戻り値型",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,10 @@ const DECISIONS: readonly Decision[] = [
type: 'object',
key: 'fields.expression',
prop: 'helpText',
en: 'CEL formula expression',
en: "CEL expression the field's value is computed from. On a formula field: computed on read, no stored column. On any other field: computed by the engine from the row each write stores, and stored, so a client-sent value never stands.",
verdict: ALL_TRANSLATE,
reason:
'CEL is a language name and stays verbatim; the rest is prose. The formula vocabulary already exists on this very panel — returnType.helpText ("Result type for formulas") is authored in all three locales — so this leaf was simply never reached.',
'CEL is a language name and stays verbatim; the rest is prose. The formula vocabulary already exists on this very panel — returnType.helpText ("Result type for formulas") is authored in all three locales — so this leaf was simply never reached. Re-translated when the row widened past `formula` to every type: the sentence is the field designer\'s own expression.helpText, authored in the same words in all three locales.',
},
{
type: 'object',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
},
"fields.expression": {
label: "表达式",
helpText: "CEL 公式表达式"
helpText: "计算此字段值所用的 CEL 表达式。公式字段:读取时计算,不存储列。其他字段:由引擎根据每次写入所存储的行计算并存储,因此客户端提交的值不会生效。"
},
"fields.returnType": {
label: "返回类型",
Expand Down Expand Up @@ -763,7 +763,7 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
},
expression: {
label: "表达式",
helpText: "用 CEL 表达式计算此字段的值(自动设为只读)"
helpText: "计算此字段值所用的 CEL 表达式。公式字段:读取时计算,不存储列。其他字段:由引擎根据每次写入所存储的行计算并存储,因此客户端提交的值不会生效。"
},
returnType: {
label: "公式返回类型",
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/src/data/field.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ export const fieldForm = defineForm({
collapsible: true,
collapsed: true,
fields: [
{ field: 'expression', widget: 'textarea', helpText: 'CEL expression to calculate this field (makes it read-only)' },
{ field: 'expression', widget: 'textarea', helpText: 'CEL expression the field\'s value is computed from. On a formula field: computed on read, no stored column. On any other field: computed by the engine from the row each write stores, and stored, so a client-sent value never stands.' },
// The five members are what FieldSchema declares — an explicit list, not
// the derived enum, so this row cannot pick up a member the formula
// return type does not have. `currency` joined with #22727; its currency
Expand Down
8 changes: 6 additions & 2 deletions packages/spec/src/data/object.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,12 @@ export const objectForm = defineForm({

// Formula / summary
// `expression`, not `formula` — the key is named for what it holds,
// not for the field type that uses it.
{ field: 'expression', type: 'code', language: 'expression', helpText: 'CEL formula expression', visibleWhen: "data.type == 'formula'" },
// not for the field type that uses it. Shown for every type, as the
// field designer's own row in `field.form.ts` is: `FieldSchema`
// accepts `expression` on any type, and on a non-formula field the
// engine computes and stores it on write (`planStoredExpressions`),
// so a formula-only row hid a live key on a stored field.
{ field: 'expression', type: 'code', language: 'expression', helpText: 'CEL expression the field\'s value is computed from. On a formula field: computed on read, no stored column. On any other field: computed by the engine from the row each write stores, and stored, so a client-sent value never stands.' },
// The five members are what `FieldSchema.returnType` declares, and
// the same explicit list the field designer's own control carries
// in `field.form.ts` (#19677). This grid additionally offered
Expand Down
Loading