feat(app-shell,plugin-detail): read-gated Approvals tab on the record detail page (#3461) - #3493
Merged
Conversation
…3461) A record in approval exposed NOTHING about the running approval to anyone but the current pending approver: useRecordApprovals was consumed solely to inject the header Approve/Reject buttons, while the pending-approver list, decision progress, and the sys_approval_action timeline lived only in the Approval Center — a setup-app surface business roles cannot navigate to, whose backing object is tenant-wide (granting read there is over-broad). The submitter could not tell whom to nudge, and record history was no help: the engine mirrors business fields as runAs:'system' and decisions never enter record history. RecordDetailView now renders a RecordApprovalsPanel below the record body for EVERY viewer who can read the record (read-gated, not approver-gated): - current flow/step, status badge, ADR-0044 round chip, and the enriched flow-steps strip; - server-computed decision progress (quorum tally, per-group ticks) — never re-derived client-side; - "waiting on" chips with server-resolved display names, group labels and collapsed slot counts (never raw ids); - ONE chronological action timeline merged across all of the record's requests (a multi-level flow opens one request per node), with decision comments and attachments; - an inline remind button for the submitter (viewer.is_submitter, id-match fallback for older backends) posting the existing POST /approvals/requests/:id/remind. Copy reuses the Approval Center's approvalsInbox.* i18n keys so the two surfaces cannot drift; the one new key (detail.approvalsPanelTitle) lands in all ten locales. useRecordApprovals additionally exposes the full `requests` array plus listApprovalActions / remindApprovalRequest, and ApprovalRequestLite now types the display enrichment the single-read endpoint already sent (process_label, step_label, flow_steps, viewer, round). The panel renders nothing when the approvals plugin is absent or the record has no requests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PvgCPXn9SUzg2iVm5hpLUr
…n Approvals tab (#3461) The panel landed below the discussion feed — the page tree stretches min-h-full, so on most screens the one thing a submitter opens the record to learn ("who is this waiting on") sat below the fold. Same failure mode Attachments had before objectstack#4358 promoted them to a tab; approvals now take the same path. - `buildDefaultTabs` accepts `approvals?: { count, node }` and emits an Approvals tab (peer of Details/Related, after Related) wrapping a `record:approvals` node, with a request-count badge (explicit spec count, the PageTabsRenderer affordance related lists already use). The label localizes through KNOWN_LABEL_DICT (审批), which already carried it. Emitted ONLY when the host reports requests, so approval-free records carry no dead tab. - New `record:approvals` renderer (app-shell): host-first data — the synthesized page threads RecordDetailView's LIVE useRecordApprovals result through the node (the same read behind the header decision buttons, so tab and header can never disagree, and a decision re-renders the tab); on authored pages that place the node bare, it self-fetches via RecordContext. - RecordDetailView passes the payload through synthParts (the history pattern); the bottom append is demoted to an authored-page fallback, gated on `hasExplicitApprovals(renderedPage)` — introspected on the RENDERED tree, not the early `effectivePage` synth, which never contains the runtime-data-dependent tab and would have double-rendered the panel. - Synth tests for tab emission/order/payload; README, console guide and the pending changeset updated to describe the tab surface. Verified in the live showcase stack: tab strip shows "Details | Related 4 | Approvals 1"; the tab renders the panel with group progress, waiting-on chips and the timeline; exactly one panel instance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PvgCPXn9SUzg2iVm5hpLUr
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
…ad key with spaces Control Byte Scan (PR #3493) flagged three raw U+0000 bytes in RecordApprovalsPanel.tsx — the join/split sentinels inside the timeline reload key. A NUL makes grep/ripgrep classify the whole file as binary, so it silently drops out of code search. Request ids (areq_<uuid>) and status values cannot contain a space, so a plain space separator carries the same guarantee a reader can actually verify. Byte-identical semantics otherwise; panel tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PvgCPXn9SUzg2iVm5hpLUr
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
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.
Closes #3461.
Problem
A record sitting in approval exposed nothing about the running approval to anyone but the current pending approver.
useRecordApprovalswas consumed solely to inject the header Approve/Reject buttons; the pending-approver list, decision progress, and thesys_approval_actiontimeline existed only in the Approval Center — asetup-app surface business roles cannot navigate to, whose backing object is tenant-wide (granting read there is over-broad). The record's own audit history is no help either: the engine mirrors business fields asrunAs:'system'and decisions never enter record history. Submitters couldn't tell whom to nudge; QA leads audited stuck records one by one.What this does
Records with approval requests grow an Approvals tab on the detail page — a peer of Details/Related with a request-count badge (same promotion Attachments got in objectstack#4358). Visibility is gated by record read access, not approver status. Records without requests carry no tab.
The tab wraps the new schema-addressable
record:approvalsnode (RecordApprovalsPanel):flow_stepsstrip;decision_progressrendered verbatim (quorum segmented bar, per-group 会签 ticks); never re-derived client-side;pending_approver_names/pending_approver_groupsresolved to name chips with group labels and collapsed ×N slot counts (never raw ids);POST /approvals/requests/:id/remind(viewer.is_submitter, id-match fallback for older backends; throttle surfaced as friendly copy).How
buildDefaultTabsacceptsapprovals?: { count, node }and emits the tab only when the host reports requests. The label localizes through the tab strip's KNOWN_LABEL_DICT (审批), which already carried it.useRecordApprovalsresult through the node — the same read behind the header decision buttons, so tab and header can never disagree, and a decision re-renders the tab. On authored pages the renderer self-fetches via RecordContext; an authored page that omits the node gets a bottom-of-page fallback append (gated onhasExplicitApprovals(renderedPage)— introspected on the rendered tree, not the early synth, which would have double-rendered).useRecordApprovalsnow exposes the fullrequestsarray pluslistApprovalActions/remindApprovalRequest;ApprovalRequestLitetypes the display enrichment the single-read endpoint already sent (process_label,step_label,flow_steps,viewer,round).approvalsInbox.*keys so the two surfaces cannot drift; one new key (detail.approvalsPanelTitle) lands in all ten locales.Verification
plugin-detail+app-shellsuites: 334 files / 2908 tests green; eslint 0 errors.Details | Related 4 | Approvals 1; approving one group ticks the bar to 1/2, flips the group badge, and appends the decision to the timeline; exactly one panel instance renders. Verified in zh-CN too (页签「审批」, 待审批/会签进度/等待以下审批人/审批动态 all localized).Changeset included (
minor, nomajorper repo policy). Docs updated (app-shell README, console guide).Related: #3055 (decision-action parity on the record page — complementary, this PR is the read-only visibility half), #2763 (SDUI rebuild would absorb this as a standard block).
Generated by Claude Code