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
11 changes: 11 additions & 0 deletions .changeset/20439-hook-condition-expression-row.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@objectstack/spec': patch
---

`hook.form.ts`'s `condition` row now declares `language: 'expression'`, matching the CEL predicate `HookSchema.condition` actually is.

Clause-②: no

The row previously declared `language: 'javascript'` — the same declared language as a real script row (`body.source`) — over a field that is `EvaluatedExpressionInputSchema`, a CEL predicate. A consumer keyed on the row's declared language could not tell the predicate apart from a script. The `helpText` moves from "Optional formula — skip the hook when this evaluates to false" to "CEL predicate — the hook runs only when TRUE", matching the phrasing every sibling predicate row (`field.form.ts` / `object.form.ts`'s `visibleWhen` / `readonlyWhen` / `requiredWhen`, and the formula `expression` row) already uses.

No key is added, removed, narrowed or widened, and no parse verdict changes — `type: 'code'` and `language` are already-declared form-DSL vocabulary. `metadata-form-declared-rows.pin.test.ts` pins the new value, with a control against a sibling predicate row.
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
},
condition: {
label: "Condition",
helpText: "Optional formula — skip the hook when this evaluates to false"
helpText: "CEL predicate — the hook runs only when TRUE"
},
retryPolicy: {
label: "Retry Policy",
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/src/data/hook.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const hookForm = defineForm({
{ label: 'System (elevated)', value: 'system' },
{ label: 'User (triggering user)', value: 'user' },
] },
{ field: 'condition', type: 'code', language: 'javascript', colSpan: 2, helpText: 'Optional formula — skip the hook when this evaluates to false' },
{ field: 'condition', type: 'code', language: 'expression', colSpan: 2, helpText: 'CEL predicate — the hook runs only when TRUE' },
{
field: 'retryPolicy',
type: 'composite',
Expand Down
34 changes: 34 additions & 0 deletions packages/spec/src/system/metadata-form-declared-rows.pin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type FormEntry = {
field?: string;
widget?: string;
type?: string;
language?: string;
helpText?: string;
visibleWhen?: string | { dialect?: string; source?: string };
fields?: FormEntry[];
Expand Down Expand Up @@ -168,3 +169,36 @@ describe('declared keys that now have a form row (#19085)', () => {
expect(row.helpText).toContain('"name"');
});
});

/**
* **A `type: 'code'` row's `language` must name the slot it actually edits**
* (#20439).
*
* `hook.form.ts`'s `condition` row declared `language: 'javascript'` over
* `HookSchema.condition`, which is `EvaluatedExpressionInputSchema` — a CEL
* predicate, not a script. Every sibling predicate row (`field.form.ts` /
* `object.form.ts`'s `visibleWhen` / `readonlyWhen` / `requiredWhen`, and the
* formula `expression` row) already declares `language: 'expression'`; a
* consumer keyed on the declared language (objectui#10963's `CodeWidget`
* fix) cannot otherwise tell this row apart from a real script row
* (`body.source`, `action.source`, both genuinely `'javascript'`).
*/
describe('hook.condition declares the CEL predicate language it edits (#20439)', () => {
it('hook.condition is a `code` row declaring language: expression, not javascript', () => {
const [row, ...extra] = rowFor('hook', 'condition');
expect(row, '`condition` is declared by HookSchema; the hook form must offer it').toBeDefined();
expect(extra, 'one row only').toEqual([]);
expect(row.type).toBe('code');
expect(row.language).toBe('expression');
});

it('CONTROL: a sibling predicate row (field.visibleWhen) also declares expression', () => {
// Proves the assertion above actually discriminates: run the identical
// probe against a row known to already be correct, so a helper that
// stopped reading `language` at all would fail here rather than passing
// hook.condition vacuously.
const [row] = rowFor('field', 'visibleWhen');
expect(row.type).toBe('code');
expect(row.language).toBe('expression');
});
});
Loading