fix(app-shell): inspectors can block Save — the field inspector gates on CEL errors (#4306) - #4536
Merged
Merged
Conversation
… on CEL errors (#4306) A formula that fails to parse showed its inline error and saved anyway: `record.est_hours *` left "Save draft" enabled, PUT returned 200 with a success toast, and publishing made the malformed expression the live field definition. The RLS editor refused the same input, so one console disagreed with itself. `CelPredicateField` always reported findings through `onLintChange`, but only `PermissionAdvancedFacets` listened, and both ends of that channel live inside the permission editor which owns its own Save button. The field inspector is a registry component whose Save belongs to a host, reached only through `MetadataInspectorProps` — which had no way to say "what I am showing is not saveable". So there was no channel to wire, and adding one is the fix. - `MetadataInspectorProps` gains an optional `onBlockingIssuesChange(count)`, named for blocking issues rather than CEL so the five other inspectors with the same gap (#4527) wire against this contract instead of a renamed one. - `ObjectFieldInspector` aggregates its four CEL editors into a per-site map, not one running total: four editors lint independently, so a shared counter would let fixing one of two broken rules hand back a writable Save. - Stale verdicts are settled by derivation, not repair — the map is stamped with the field it describes, and the formula site counts only while the field is a formula, so no render window exists in which Save is gated by an editor already gone. - Both hosts hold the count, disable their own Save with the message the RLS editor already shows, and stamp it with the selection so it expires on selection change and unmount. In the metadata editor all three save doors are gated — button, autosave timer and ⌘S. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
PM step-7 复核 — ACCEPT (session_017Qqyix2QcnpUC9XeYVDzx3)
Auto-merge armed (squash). Generated by Claude Code Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 13, 2026 06:05
This was referenced Aug 13, 2026
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 17, 2026
…objectstack-ai#4527) (objectstack-ai#4547) CelPredicateField has always reported its lint verdict through onLintChange, but ConditionBuilder and ConditionalFormattingEditor passed no listener, so a predicate that does not parse rendered its inline error and Save still saved -- the objectui#4306 defect, one editor family over. Both editors gain an optional callback surfacing their blocking-error count (severity 'error' only), and the inspectors above them aggregate and report through MetadataInspectorProps.onBlockingIssuesChange, the channel PR objectstack-ai#4536 shipped. PageBlockInspector gates on a page block's visibleWhen; ViewVariantInspector gates on a view's conditionalFormatting rules, forwarded through ViewInspector on the scoped path. Counts are DERIVED from what they describe, never repaired by reset effects. ConditionBuilder's CEL editor exists only in raw mode and can vanish while its last verdict was an error -- the adopt effect flips back to rows in the same commit, unmounting it and cancelling its pending lint -- so the count is read as 0 whenever that editor is not mounted. ConditionalFormattingEditor keys a per-rule map and counts only rules that still exist: a shared counter would let whichever rule linted last overwrite the others, and a deleted rule's remembered error would wedge Save shut with no editor left to fix it in. Three sites named in the report are NOT wired: HookDefaultInspector, ActionDefaultInspector and the view's home panel are MetadataDefaultInspectorProps components whose contract has no blocking-issues member, and widgets.tsx's condition widget is a SchemaForm widget rather than an inspector. Wiring those needs a second contract decision plus edits to the hosts, so they are escalated rather than guessed. Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4306
record.est_hours *in a formula field showed its inline parse error and saved anyway — PUT 200, success toast, and publishing made the malformed expression the live field definition. The RLS editor refused the same class of input, so one console disagreed with itself.Why this needed a contract, not a one-file fix
The card's stated root cause did not survive measurement, and the PM ruling that governs this PR was issued on the corrected picture (measurement: #4306 comment 5276045542, ruling: comment 5276058655).
CelPredicateFieldhas always reported findings throughonLintChange(issues)— it has noonCelErrorsChangeprop at all.onCelErrorsChangeis the aggregating parent's prop onPermissionAdvancedFacets, consumed byPermissionMatrixEditor. Both ends live inside the permission editor, which owns its own Save button.The field inspector is a registry component. The button it must gate belongs to a host — the Studio Data pillar's "Save draft", or the metadata editor's Save — reachable only through
MetadataInspectorProps, which had no error channel, and there is no context alternative (zerocreateContexthits in metadata-admin and studio-design). So there was no existing channel to connect to; the fix is to add one.Ruling compliance
MetadataInspectorPropsgains optionalonBlockingIssuesChange(count). Optional, so all existing inspectors stay valid. Named generally so the five other inspectors with the same gap (CEL blocking errors are dropped on the floor in ConditionBuilder and ConditionalFormattingEditor too — same ungated-Save family as #4306 #4527, gated on this landing) wire against this contract rather than a renamed one.formula/visibleWhen/readonlyWhen/requiredWhenwith identity-preserving writes and a summed memo. Four editors lint independently and asynchronously, so one shared counter would let whichever reported last overwrite the others — fixing one of two broken rules would hand back a writable Save.perm.cel.saveBlockedverbatim ("Fix the CEL syntax errors before saving."). No i18n edit was needed: the key already exists in both maps (en 1148, zh 2935) and both hosts already importtfrom metadata-admin/i18n. Itsperm.prefix is now a misnomer — recorded as a naming smell, not renamed here.One declared deviation on mechanism, not behavior
Both the stale-verdict prune and the host reset are done by derivation rather than reset effects. The map is stamped with the field it describes; each host stamps its count with the selection it came from. Behavior is exactly what the ruling specifies — the count expires on selection change and on unmount, and the formula verdict cannot outlive its editor — but it is stronger in two ways: an effect-based reset leaves a one-render window in which Save is still gated by an editor that is already gone, and the effect version added 4
react-hooks/set-state-in-effectwarnings over baseline. The derived version adds zero warnings.Beyond the ruling: all three save doors
The metadata editor has three ways to save, and gating only the button would have left two open — the autosave timer would have written the malformed definition a second later, and so would the keyboard shortcut. The button, the autosave effect and ⌘S are all gated.
Red-first evidence
Predicted in writing before running: inspector suite all red (reporter never called); host suite all red at its first
toBeDisabled, with the valid-formula half passing even unfixed as the must-not-change pin.Measured against unfixed sources — 11 failed / 11:
AssertionError: expected undefined to be 1(andto be +0) — nothing was ever reported.expect(element).toBeDisabled()→ "Received element is not disabled" on the Save draft button. That is the card's symptom reproduced as a test.Reverse verification after the fix, by patch-file revert of the four source files (never
git stash— shared stack) and re-run: 11 failed again, split 8 + 3 on the same two signatures; restored withgit applyand sha256 verified byte-identical on all four files.Pins (must-not-change)
PermissionAdvancedFacets.tsx,PermissionMatrixEditor.tsxandpermission-slice.tswere read-only references ([security] An RLS policy authored in Studio's package door is silently discarded — Save succeeds, no row filter is ever persisted #4302 owns that surface), and their suites stay green.Verification
metadata-admin+studio-design, 168 files / 1671 tests passed, 1 skipped.tsc --noEmit,tsc -p tsconfig.test.json) — clean.check-control-bytes,check-changeset-presence,check-changeset-no-major,check-changeset-fixed,check-phantom-dependencies— all green..d.tsmeasured both ways with clean builds (dist + tsbuildinfo removed between): exactly two files differ — the added optional prop ininspector-registry.d.tsand the destructured parameter list inObjectFieldInspector.d.ts. No other API movement. Graded minor per the entry-reachable-additive precedent, sinceMetadataInspectorPropsis re-exported from the package entry.Generated by Claude Code