Problem
The Approval Center is hand-rolled: a bespoke inbox page (/apps/setup/system/approvals, wired from HomePage.onOpenApprovals) plus a bespoke decision drawer. It re-implements list/detail machinery (search, filter, sort, pagination, keyboard nav, bulk-select, status badges, responsive, the record layout) that the platform's own metadata-driven renderers already provide — and does several of them worse. Every polish defect in #2762 (vertical status badge, flat action hierarchy, whitespace, the i18n split where bespoke buttons are localized but DeclaredActionsBar labels aren't) is a symptom of diverging from the standard renderers.
For a metadata-driven platform, the approval surface should be metadata, not a special-case page.
Relationship: #2762 is the polish/stopgap (fix the defects in place). This issue is the root cure (rebuild both surfaces on the standard renderers so those defects can't recur). Discovered while dogfooding v16.0 approvals; the surface is demonstrable via objectstack-ai/objectstack#3364 (part of the objectstack-ai/objectstack#3358 sweep).
Target architecture
Both standard renderers are already page/view-metadata + data-source driven and already accept an ApiDataSource:
plugin-list/src/ListView.tsx — list, api-backed.
app-shell/src/views/RecordDetailView.tsx — detail; takes a dataSource prop, renders an effectivePage, uses dataSource.findOne(...), and has an embedded mode (renders inside a drawer).
core/src/adapters/ApiDataSource.ts + resolveDataSource.ts — the provider:'api' primitive.
views/DeclaredActionsBar.tsx — metadata-driven decision actions (already used).
So the entire approval UI can collapse to: a sys_approval_request list view + a detail page, backed by an ApiDataSource pointed at the approvals endpoints, plus one reusable polymorphic record-preview component (+ a progress widget). The bespoke inbox page and drawer get deleted.
Decision drawer → standard mechanisms
| Drawer element |
Standard SDUI mechanism |
Status |
| Request's own fields |
detail-page sections / field groups |
exists |
| Timeline (审批动态) |
related list on sys_approval_action |
exists |
| Decision composer (comment + attachments) |
declared action's ActionParamDialog (ADR-0059 file/richtext params; framework#3332 attachments param) |
exists |
| Action bar (approve/reject/reassign/…) |
DeclaredActionsBar |
already used |
Progress / viewer (0-of-N, can_act) |
computed fields via ApiDataSource → getRequest |
primitive exists, not wired |
| Target-record card (invoice/expense fields) |
polymorphic reference-preview component |
missing |
Inbox list → standard mechanisms
Same story: a standard ListView fed an ApiDataSource → /approvals/requests (listRequests), so 待我审批 / 我发起的 / 全部 become server-side scopes (not client filters), and it inherits badges/columns/i18n/light-mode/responsive/pagination/a11y/keyboard for free. The target-record columns use the same polymorphic component.
Scope — the two reusable primitives (shared by list + detail)
Then compose as metadata
Honest caveat
Release notes deliberately keep approve/reject in a richer inline composer (excluded from the bar) so a reviewer can stage attachments while reading the timeline. Either prove ActionParamDialog can host that (comment + multi-file + inline context), or keep one thin custom approve/reject action — a component-level exception, not a hand-rolled page.
Why it's worth it
Approvals stop being a special case: new decision capabilities already ship as backend metadata (DeclaredActionsBar), and after this the whole surface is metadata + 2 reusable components. A2 (polymorphic preview) and A1 (api-backed standard views) pay off across the console.
Refs: #2762 (polish), objectstack-ai/objectstack#3364, objectstack-ai/objectstack#3358.
Problem
The Approval Center is hand-rolled: a bespoke inbox page (
/apps/setup/system/approvals, wired fromHomePage.onOpenApprovals) plus a bespoke decision drawer. It re-implements list/detail machinery (search, filter, sort, pagination, keyboard nav, bulk-select, status badges, responsive, the record layout) that the platform's own metadata-driven renderers already provide — and does several of them worse. Every polish defect in #2762 (vertical status badge, flat action hierarchy, whitespace, the i18n split where bespoke buttons are localized butDeclaredActionsBarlabels aren't) is a symptom of diverging from the standard renderers.For a metadata-driven platform, the approval surface should be metadata, not a special-case page.
Relationship: #2762 is the polish/stopgap (fix the defects in place). This issue is the root cure (rebuild both surfaces on the standard renderers so those defects can't recur). Discovered while dogfooding v16.0 approvals; the surface is demonstrable via objectstack-ai/objectstack#3364 (part of the objectstack-ai/objectstack#3358 sweep).
Target architecture
Both standard renderers are already page/view-metadata + data-source driven and already accept an
ApiDataSource:plugin-list/src/ListView.tsx— list, api-backed.app-shell/src/views/RecordDetailView.tsx— detail; takes adataSourceprop, renders aneffectivePage, usesdataSource.findOne(...), and has anembeddedmode (renders inside a drawer).core/src/adapters/ApiDataSource.ts+resolveDataSource.ts— theprovider:'api'primitive.views/DeclaredActionsBar.tsx— metadata-driven decision actions (already used).So the entire approval UI can collapse to: a
sys_approval_requestlist view + a detail page, backed by an ApiDataSource pointed at the approvals endpoints, plus one reusable polymorphic record-preview component (+ a progress widget). The bespoke inbox page and drawer get deleted.Decision drawer → standard mechanisms
sys_approval_actionActionParamDialog(ADR-0059 file/richtext params; framework#3332 attachments param)DeclaredActionsBarviewer(0-of-N,can_act)ApiDataSource→getRequestInbox list → standard mechanisms
Same story: a standard
ListViewfed anApiDataSource→/approvals/requests(listRequests), so 待我审批 / 我发起的 / 全部 become server-side scopes (not client filters), and it inherits badges/columns/i18n/light-mode/responsive/pagination/a11y/keyboard for free. The target-record columns use the same polymorphic component.Scope — the two reusable primitives (shared by list + detail)
ListView/RecordDetailViewto read from the approvals endpoints (/approvals/requests,/approvals/requests/:id) soviewer+decision_progresscome down as fields. Needs a small framework-side contract: the approvals api data source shape (list + get returning the computed fields) — track in objectstack-ai/framework.(object_name, record_id)pair, resolving the target object + its display fields. Reusable well beyond approvals (audit rows, activity, comments all reference polymorphic targets).decision_progress. (Half a primitive; could be a component or a chart.)Then compose as metadata
sys_approval_requestdetail page — sections (request fields), the polymorphic target-record card (A2), the progress widget (A3), asys_approval_actionrelated-list (timeline), andDeclaredActionsBar; data via A1.sys_approval_requestlist view — columns incl. the polymorphic target-record + a key field (amount/urgency); scopes via A1; row + drawer actions viaDeclaredActionsBar.审批中心page + decision drawer once B1/B2 reach parity; keep?request=deep-link semantics.Honest caveat
Release notes deliberately keep approve/reject in a richer inline composer (
excluded from the bar) so a reviewer can stage attachments while reading the timeline. Either proveActionParamDialogcan host that (comment + multi-file + inline context), or keep one thin custom approve/reject action — a component-level exception, not a hand-rolled page.Why it's worth it
Approvals stop being a special case: new decision capabilities already ship as backend metadata (
DeclaredActionsBar), and after this the whole surface is metadata + 2 reusable components. A2 (polymorphic preview) and A1 (api-backed standard views) pay off across the console.Refs: #2762 (polish), objectstack-ai/objectstack#3364, objectstack-ai/objectstack#3358.