diff --git a/.changeset/24735-expression-form-help.md b/.changeset/24735-expression-form-help.md new file mode 100644 index 00000000000..cb02773d6fe --- /dev/null +++ b/.changeset/24735-expression-form-help.md @@ -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. diff --git a/packages/lint/src/validate-predicate-path-refs.test.ts b/packages/lint/src/validate-predicate-path-refs.test.ts index 1866c338b79..628cd97b4e4 100644 --- a/packages/lint/src/validate-predicate-path-refs.test.ts +++ b/packages/lint/src/validate-predicate-path-refs.test.ts @@ -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); @@ -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); @@ -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 `
::::`, 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; let restored = 0; const debare = (node: unknown): void => { @@ -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`'); }); diff --git a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts index 9915f9d164d..6fee6735a78 100644 --- a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts @@ -226,7 +226,7 @@ export const enMetadataForms: NonNullable = { }, "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", @@ -763,7 +763,7 @@ export const enMetadataForms: NonNullable = { }, 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", diff --git a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts index 89fa8bbea09..04a4bb290ed 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts @@ -226,7 +226,7 @@ export const esESMetadataForms: NonNullable = }, "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", @@ -763,7 +763,7 @@ export const esESMetadataForms: NonNullable = }, 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", diff --git a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts index 5650f54f428..fe2a353a84c 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts @@ -226,7 +226,7 @@ export const jaJPMetadataForms: NonNullable = }, "fields.expression": { label: "式", - helpText: "CEL 数式" + helpText: "このフィールドの値を計算する CEL 式。数式フィールドでは読み取り時に計算され、列は保存されません。その他のフィールドでは、書き込みごとに保存される行からエンジンが計算して保存するため、クライアントが送信した値は反映されません。" }, "fields.returnType": { label: "戻り値の型", @@ -763,7 +763,7 @@ export const jaJPMetadataForms: NonNullable = }, expression: { label: "式", - helpText: "このフィールドを計算する CEL 式(読み取り専用化)" + helpText: "このフィールドの値を計算する CEL 式。数式フィールドでは読み取り時に計算され、列は保存されません。その他のフィールドでは、書き込みごとに保存される行からエンジンが計算して保存するため、クライアントが送信した値は反映されません。" }, returnType: { label: "数式の戻り値型", diff --git a/packages/platform-objects/src/apps/translations/object-field-editor-panel-echo-decisions.test.ts b/packages/platform-objects/src/apps/translations/object-field-editor-panel-echo-decisions.test.ts index 7003fe8f49f..39452985094 100644 --- a/packages/platform-objects/src/apps/translations/object-field-editor-panel-echo-decisions.test.ts +++ b/packages/platform-objects/src/apps/translations/object-field-editor-panel-echo-decisions.test.ts @@ -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', diff --git a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts index eee06b25d1e..32e12c44f45 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts @@ -226,7 +226,7 @@ export const zhCNMetadataForms: NonNullable = }, "fields.expression": { label: "表达式", - helpText: "CEL 公式表达式" + helpText: "计算此字段值所用的 CEL 表达式。公式字段:读取时计算,不存储列。其他字段:由引擎根据每次写入所存储的行计算并存储,因此客户端提交的值不会生效。" }, "fields.returnType": { label: "返回类型", @@ -763,7 +763,7 @@ export const zhCNMetadataForms: NonNullable = }, expression: { label: "表达式", - helpText: "用 CEL 表达式计算此字段的值(自动设为只读)" + helpText: "计算此字段值所用的 CEL 表达式。公式字段:读取时计算,不存储列。其他字段:由引擎根据每次写入所存储的行计算并存储,因此客户端提交的值不会生效。" }, returnType: { label: "公式返回类型", diff --git a/packages/spec/src/data/field.form.ts b/packages/spec/src/data/field.form.ts index d6dfb7781ea..f40ee308f2f 100644 --- a/packages/spec/src/data/field.form.ts +++ b/packages/spec/src/data/field.form.ts @@ -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 diff --git a/packages/spec/src/data/object.form.ts b/packages/spec/src/data/object.form.ts index 2d4b78c7a90..cfcbc82604c 100644 --- a/packages/spec/src/data/object.form.ts +++ b/packages/spec/src/data/object.form.ts @@ -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