Uh oh!
There was an error while loading. Please reload this page.
fix(console): trim hidden: true fields from the approvals drawer summary card - #6031
Merged
yinlianghui merged 1 commit intoAug 24, 2026
Merged
Conversation
…mmary card `payloadSummary` built the drawer's business summary card from the request's `payload_json` snapshot behind five filters — `PAYLOAD_SYSTEM_KEYS`, the lead amount key, null/object/empty values, unresolved opaque ids, a six-field cut — and no field-visibility filter of any kind. A field the object's metadata declares `hidden: true` was an ordinary scalar to that code, so it rendered in the card, labelled via `payload_labels`. Per the platform ruling, `hidden: true` stays UI-only and `internal: true` is the serialization primitive: the producer is correct to ship a `hidden` field in the snapshot, and FLS-restricted fields are already redacted at serve time. `hidden` is a UI contract and this drawer card is default UI, so the UI is the authoritative place that contract is enforced. The drawer now reads the open request's object metadata (`getObjectSchema`, the same cached `GET /meta/object/:name` the record form performs) and drops the declared-hidden keys BEFORE the six-field cut, so the next business field is promoted into the freed slot. The lead amount figure at the top of the same card takes the same trim. An unanswered metadata read leaves the card exactly as it renders today: this is a presentation filter, not an access control. No new published export: the seam consumes `useAdapter`, already exported from `@object-ui/app-shell` and already used by this page's `recordReadability`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 24, 2026 14:09
Uh oh!
There was an error while loading. Please reload this page.
yinlianghui
deleted the
claude/issue-5565-approvals-summary-hidden-fields
branch
August 24, 2026 14:21
This was referenced Aug 24, 2026
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 freeto 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#5565
What was wrong
payloadSummaryinapps/console/src/pages/system/ApprovalsInboxPage.tsxbuilt the drawer's business summary card from the request'spayload_jsonsnapshot behind five filters —PAYLOAD_SYSTEM_KEYS, the lead amount key,null/object/empty values, unresolved opaque ids, then the first 6 survivors — and no field-visibility filter of any kind. A field the object's metadata declareshidden: truewas an ordinary scalar to that code, so if it landed in the first 6 survivors it rendered in the card, labelled viapayload_labels.Re-measured on the post-objectstack#11039 ref, and the defect still reproduces:
getReadableFields(objectstack#11039, both doors) and never reach this page.hidden: truefield still arrives and still renders, by construction: the ruling below giveshiddenno serialization semantic, so the producer keeps shipping it.ApprovalsInboxPageheld no field-level metadata — re-derived: everyhiddenoccurrence in the file before this change is a Tailwind class, anaria-hidden, or prose. So this needed a metadata read wired in, not a filter tweak.Why the fix is client-side, and why that is not a workaround
Maintainer ruling on objectstack#10749, verbatim: 「
hidden: truestays UI-only;internal: trueis the serialization primitive」.A client-side trim is consumer-side compensation only when the producer shipped data it should not have. Under this ruling the producer is correct to ship a
hiddenfield in the snapshot —hiddenwas never a wire concern, and a field an author wants off the wire is declaredinternal: true.hiddenis a UI contract ("hidden from the default UI"), and this drawer card is default UI, so the UI is the authoritative enforcement point, not a compensating one. Server-side trimming ofhiddenwhen writing or servingpayload_jsonis exactly the serialization semantic the ruling refused, and is not done here.internalis deliberately not read anywhere in this change. They are distinct primitives.What changed
apps/console/src/pages/system/hiddenFields.ts—hiddenFieldNames(pure, reads both servedfieldsshapes: the record shape{ name: def }and the array shape[{ name, ...def }]),readHiddenFields(never rejects), anduseHiddenFields(one read per object per mount). It consumesuseAdapter, already exported from@object-ui/app-shelland already used by this page's siblingrecordReadability.ts.payloadSummarytakes ahiddenKeysset and drops those keys before the 6-field cut, so the next business field is promoted into the freed slot rather than the card silently rendering one row shorter. That ordering is what makes this a filter rather than a truncation.decisionAmountEntry) takes the same trim at the drawer call site. Both halves read the same snapshot and sit in the same card, so filtering only the field grid would have moved a hidden amount-like field from the grid into the bold figure at the top of the very card being repaired. Named here because it is one step past the card's literal wording: it is the same defect class, mechanically the same guard, in the same JSX block, under the same gate family.No new published export
Clause-② answer: no published surface widening.
useObjectFieldsis not exported and was not exported; the fix does not need it. It reads object metadata through the adapter'sgetObjectSchema— the runtime read every record form and detail view already performs for an ordinary business user — rather than through the metadata-adminMetadataClientthatuseObjectFieldsuses. Nothing was added topackages/app-shell/src/index.ts;packages/app-shell/**is untouched in this PR.Cost — a cached read, and what invalidates it
One
getObjectSchema(object_name)per distinct object per mount, for the open request's object only:MetadataCache(LRU max 100, 5-minute TTL, in-flight de-duplication), so repeated drawer opens on one object cost zero round trips.clearCache()(which the shell issues on a locale switch), or a page reload. Ahiddenflag flipped in Studio while the page is open is picked up on the next reload — the same stalenessuseRecordReadabilityalready accepts on this page, and this is a presentation flag, not a grant.The queue rows are not trimmed here — different surface, N rows across K objects, and the amount sort is an ordering change with its own acceptance criteria. Filed as #6020.
It fails open, and that is deliberate
An unanswered metadata read (no
getObjectSchemaon the source, a 404, a 403, a transport error) leaves the declaration unknown and renders the field — today's card. The server is still the only authority on what a principal may read and has already answered by the time the payload arrives; degrading an approver's decision surface on a transient metadata error would break the primary workflow to enforce a declaration that was never the security boundary. This is the same directionrecordReadabilityfails on this page, and the opposite of #5553's raw-JSON panel — there the measured defect was a non-holder seeing the panel, so absence of an answer had to deny. Pinned by a test.#5553's "Raw data (JSON)" panel gate is untouched and its ruling is not reopened.
Verification
Predicted before running, then observed. Full local union re-run on the final commit
588c2c5a5.Path-filtered vitest (app-shell whole-package vitest deliberately not run):
The fixture pins the filter, not the render. The 6-field cut is a confounder in both directions, so the snapshot is ordered:
Untrimmed the card shows 1-6. Trimmed it shows 1, 2, 4, 5, 6 and 7 — the seventh is promoted into the vacated slot, which can only happen if the drop precedes the cut.
notes(8) stays out either way, so "promotion" cannot be an off-by-one that simply renders more rows.Counter-probes (the important half — "the hidden field is gone" is satisfiable by breaking the card entirely):
diagnosis_coderenders, andjustificationcorrectly stays out.setup.access, so Approvals inbox detail drawer unconditionally renders a "Raw data (JSON)" panel — exposeshidden: truefields, internal ids and audit columns to business approvers #5553's raw panel — which would print the whole snapshot by a second route — never renders.Reverse verification (two ablations, each with the direction predicted first):
payloadSummaryfindByTexttimes out before the hidden-field assertion is reached) — and additionally RED on the hidden-amount test, becausedecisionAmountEntrystill drops the amount, leavingexcludeKeyundefined so the ungated grid renders that same figure by a different routedecisionAmountEntryEach mutation was proved on disk before the run — the injected marker grepped (1 hit) and separately the remaining guard counted (1 hit, so exactly one of the two identical guards was removed) — because an editor's exit code proves nothing on a zero-hit anchor. Both ran under
trap … EXIT INT TERM;git diff HEAD --statwas empty after each. No build artifact sits between the edit and the ablation: the root Vitest config aliases every@object-uispecifier at that package's source directory, and the page under test is this app's own source, imported relatively.Gates, by name, with exit codes captured before any pipe:
pnpm --filter @object-ui/app-shell type-checkAPPSHELL_TC_EXIT=0tsc --noEmit && tsc -p tsconfig.test.json, so not a zero-match silent passpnpm --filter @object-ui/console type-checkCONSOLE_TC_EXIT=0tsc --noEmit && tsc -b tsconfig.node.json --force; the first run was a genuine RED (TS2322on the test fixture's inferred row type), repaired, and not an unbuilt-closure signaturepnpm --filter "@object-ui/console^..." buildBUILD_EXIT=0pnpm exec eslint --no-inline-configon the 4 changed source filesESLINT_EXIT=0ApprovalsInboxPage.tsxalso reports exactly 20, and no warning is on a touched line. The three new files report nothing.node scripts/check-changeset-presence.mjsCHANGESET_EXIT=0patchchangeset for@object-ui/consoleis owed and added (this is user-visible behaviour, not the empty-frontmatter case)node scripts/check-control-bytes.mjsCTRL_GATE_EXIT=0Merge-base
c0091b82b; the changed-file delta against it is the 5 files in this PR and nothing else.Repo-wide
pnpm lintis CI's run and is not duplicated here.Generated by Claude Code