Repository navigation
架构:把审批 UI 重建在标准 SDUI 渲染器上(ListView + RecordDetailView + ApiDataSource + 多态记录预览组件) #2763
Description
Activity
- addedenhancementNew feature or requestNew feature or requestsduiServer-Driven UI runtimeServer-Driven UI runtime
on Jul 20, 2026 xuyushun441-sys commented
on Aug 3, 2026 ContributorMore actions维护者裁决(2026-08-03)
方向批准,A2(多态记录预览组件)先行——全平台复用(审计行、活动、评论都引用多态目标),独立成单价值最高。
- 本单是 epic,不整单派发;排期时把 A1/A2/A3 拆成单一 scope 子单,B1–B3 依次跟进。
- composer 富交互按本单 honest caveat 处理:先证明
ActionParamDialog能承载(评论 + 多附件 + 时间线上下文),不能则保留一个薄的定制 approve/reject action——组件级例外,不是手搓页面。 - 裁决理由记录:重建后 AI 改审批面改的是受 spec 校验的元数据,而非无门禁的定制 React;审批界面(Approval Center)优化评估:徽章竖排 / 决策确认 / 进度可视化 / 动作标签 i18n #2762 的每个 polish 缺陷都是偏离标准渲染器的症状。
排期:v17 后按 epic 走(非协议变更)。摘
needs-user-decision(方向已定,余下是拆解与排期)。
Generated by Claude Code
Generated by Claude Code
分诊:
pm:on-hold(决定已做,不是待做)⛔ objectui 仓内单不打
domain:*(#5468)。为什么是
pm:on-hold而不是needs-user-decision/pm:queue维护者 2026-08-03 的裁决(本单上一条评论)已经批准方向并指定了排期:「排期:v17 后按 epic 走(非协议变更)」。那条评论同时摘掉了
needs-user-decision,于是本单从那天起没有任何pm:*标签 —— 每一轮 backlog sweep 都把它当作未分诊新单重新捞起,而它其实早已有结论。这是 #4829 那条教训在本仓的第三个实例(前两个:#3173 的维护者裁决、cloud#799 的分诊结论)。pm:on-hold的定义正是「决定已做,答案是现在不做」;needs-user-decision会把一个已经拍过板的问题重新推回维护者收件箱。三要素(
pm:on-hold必须成对落地的 hold 记录)- 日期:2026-08-03(维护者裁决);分诊落标 2026-08-06。
- 理由:方向已批准,但整单不派发 —— 它是 epic,正确的下一步是拆成单一 scope 的子单而不是交给一个 dev。且 v17 发布窗口内不开这块投入。
- ⛔ 重启条件:v17 发布后,由 objectui 座位按 epic 拆解:A2(多态记录预览组件)先行(维护者点名:全平台复用价值最高——审计行、活动、评论都引用多态目标),随后 A1 / A3,再 B1–B3 依次跟进。届时摘
pm:on-hold,父单转为进度视图(⛔ 父单本身永不派发)。
拆解时必须带上的两条约束(来自裁决与正文,勿丢)
- composer 富交互:先证明
ActionParamDialog能承载「评论 + 多附件 + 时间线上下文」;证不通则保留一个薄的定制 approve/reject action —— 组件级例外,不是手搓页面。 - A1 需要 framework 侧配套契约(approvals api data source 的 list/get 形状,含
viewer与decision_progress计算字段)⇒ 那一段按跨座位转移协议走 objectstack 队列,objectui 侧子单写Blocked-by:。
与同族单的收敛关系(本轮一并理清)
- 审批界面(Approval Center)优化评估:徽章竖排 / 决策确认 / 进度可视化 / 动作标签 i18n #2762 是 polish/stopgap(本轮已入队,范围写死在那条评论里);本单是根治。两者不是重复,是同一问题的两个时间尺度 —— hold 到 v17 后期间,polish 仍然有效。
- approvals UX: surface OOO/quorum/per_group/attachments SDUI-first — stop hand-editing the inbox per feature #2678(approvals SDUI-first)的结构半边与本单是同一件事,本轮已按查重收敛规则关闭并把其未被覆盖的 P1.5 项转记到 审批界面(Approval Center)优化评估:徽章竖排 / 决策确认 / 进度可视化 / 动作标签 i18n #2762,理由见那两单。⇒ 审批 UI 重建只保留本单一个派发入口。
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- marked approvals UX: surface OOO/quorum/per_group/attachments SDUI-first — stop hand-editing the inbox per feature #2678 as a duplicate of this issue
on Aug 6, 2026 重启条件已达成 ——
pm:on-hold→tracking。v17.0.0 is GA. Measured on the npm registry at 2026-08-14 08:3xZ:
@objectstack/spec latest = 17.0.0 published 2026-08-14T07:59:51Z @objectstack/cli latest = 17.0.0 published 2026-08-14T07:59:10Z @objectstack/core latest = 17.0.0 published 2026-08-14T07:59:15Z create-objectstack latest = 17.0.0 published 2026-08-14T07:59:14Z本卡记录的重启条件是「v17 发布后,由 objectui 座位按 epic 拆解」,并且明确写了「届时摘
pm:on-hold,父单转为进度视图(⛔ 父单本身永不派发)」。照此处理:摘
pm:on-hold,落tracking(本仓无pm:epic标签,tracking是「进度视图」在本仓状态机里的拼法)。⛔ 没有落pm:queue—— 那会让本单作为整体被捞进派发池,正是本卡自己禁止的。objectui 座位现在欠的是拆解动作,按维护者点名的顺序:A2(多态记录预览组件)先行(全平台复用价值最高——审计行、活动、评论都引用多态目标),随后 A1 / A3,再 B1–B3。
Authorization: maintainer directive (this session) —「先做 23 张 blocked 解锁 10 张 on-hold 按各自自述处理」· PM session
01JaVVMrSxt7Tgi1uwEuDtH7
Generated by Claude Code
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 23, 2026 Triage:
domain:ui(lane only —trackinguntouched). Landing:apps/setup— rebuilding the approval UI on the standard SDUI renderers is application work.Routed via the maintainer direct-dispatch channel, this session, verbatim: 「然后 批 4–5」. PM session
session_0124Qg8rLvpXnQDwCmpKUmaJ. objectui three-stream split (maintainer 2026-08-21); not a Routine triage fire — the triage seat may re-grade.
Generated by Claude Code
- added a commit that references this issue
on Sep 28, 2026 objectstack-fleet commented
on Oct 6, 2026 ContributorMore actionsRuling: batch #280 item 2 · letter 停放 · maintainer 「2763 要等 v18再启动」 2026-10-06T00:12Z
Director seat, summon #34, objectstack
session_0195diSK6vuqxgiJ38VWBHb6(GitHubos-justin; written asobjectstack-fleet[bot]via the relay). Provenance: the maintainer, in the live director chat of this summon (after marker objectstack#12708 comment 6005840354), verbatim: 「分诊给 4 张没分诊的卡定级(不算交接文档 #7233): #6237、#4425、#2763、#2458。都是 8 月的旧卡,其中 #2763(审批 UI 重建)和 #2458(console-ai 体验清单)比较大,可能要拆卡。我可以去分诊那边挂请求。 2763 要等 v18再启动。」 Freshness gate: the body and all four comments (5166015115 · 5203092261 · 5291319606 · 5384174588) were re-read before this record; nothing newer exists.The ruling
停放 — this card waits for the v18 line. The 2026-08-03 direction (5166015115: rebuild on the standard SDUI renderers, A2 first, the parent is never dispatched whole) stands; only the schedule moves. The 2026-08-14 restart on v17 GA (5291319606) is superseded by the maintainer's word above.
- State:
trackingstays;pm:on-holdis added. Body leading line:Restart-when: closed objectstack-ai/objectstack#15193(the gate card 「the v18 development line is not open」; its closing is the line opening, and theBlocked-by:unlock scan fires on it). - At restart: the objectui
domain:uilane splits this card into single-scope children in the ruled order (A2 polymorphic record preview → A1 approvalsApiDataSourcewith its objectstack-side contract card → A3 progress widget → B1–B3), with the two constraints 5203092261 records (the composer caveat; A1's cross-repoBlocked-by:). ⛔ The parent is never dispatched itself. - Grading (the maintainer's ask):
pm:retriageis hung in the same stroke for the triage seat: give this card itspriority:*andarea:*; the split happens at restart, not now.
四棱(本裁决新记录)
- ① 长远:方向不变(审批面是元数据,不是手搓页面);只改时序到 v18 线。
- ② 拉动:审批界面(Approval Center)优化评估:徽章竖排 / 决策确认 / 进度可视化 / 动作标签 i18n #2762 的 polish 已入队覆盖当下缺陷;重建没有当前用户在等。
- ③ 防 AI:重建后 AI 改审批面改的是受 spec 校验的元数据;等 v18 不改这一点。
- ④ 不扩散:v17 线不开这块投入;等 v18 与 ADR-0131 同车。
- 只看①选 停放;②③④ 是否翻转:否。
Prior rulings read: 5166015115 (direction, 2026-08-03), 5203092261 (hold, 2026-08-06), 5291319606 (restart on v17 GA), 5384174588 (lane); objectstack#15193 (the v18 gate card, open,
pm:blocking); thread: 4 comments.- State:
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 6, 2026 objectstack-fleet commented
on Oct 6, 2026 ContributorMore actionsPath: ② the capabilities an end user meets in the app — approvals | 缺项 | P2
Triage answers
pm:retriage:priority:p3·area:workflow, and the hold stays as ruled (pm:on-hold,Restart-when: closed objectstack-ai/objectstack#15193)Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T00:57Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in the objectui approvals surfaces, to be rebuilt on the standard SDUI renderers (ListView, RecordDetailView, ApiDataSource and the polymorphic record preview) ⇒
domain:ui; rationale: unchanged from the 2026-08-03 direction (5166015115).- Why p3. The maintainer parked it for the v18 line today ("2763 要等 v18再启动", ruling
6006187681). That ruling's own facet ② records that no current user is waiting on the rebuild, and that 审批界面(Approval Center)优化评估:徽章竖排 / 决策确认 / 进度可视化 / 动作标签 i18n #2762's polish already covers today's defects. The priority grades the parent, which is never dispatched whole. - At restart, the
domain:uilane splits it into single-scope children in the ruled order (A2 first), and each child carries its own grade. area:workflow: the approvals surface.- Not added:
target:v18. That label's adoption in objectui is in the maintainer's decision box. The hold'sRestart-when:already keys the restart to the v18 gate card.
Labels:
priority:p3andarea:workfloware added, andpm:retriageis removed.tracking,pm:on-holdand the rest stay.
Generated by Claude Code
- Why p3. The maintainer parked it for the v18 line today ("2763 要等 v18再启动", ruling
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itand removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 6, 2026 objectstack-fleet commented
on Oct 6, 2026 ContributorMore actionstarget:v18added, executing decision card objectstack-ai/objectstack#22010 (ruling6020197009, batch #282 item 6, maintainer 「同意」). The label is a release-board view, not a gate: this card's gate stays itsBlocked-by:/Restart-when:line on objectstack-ai/objectstack#15193. Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·session_01AavokzJ5DndAwitDXvKy4U. ⛔ Not a claim; no other label changes.objectstack-fleet commented
on Oct 7, 2026 ContributorMore actionsTriage: hold released,
pm:on-hold→pm:epic(a parent, ⛔ never dispatched whole).closed objectstack-ai/objectstack#15193, this card'sRestart-when:, now holdsTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-07T12:44Z. ⛔ Not a claim, ⛔ not a dispatch.The restart condition holds: objectstack-ai/objectstack#15193 closed
completed(6037915987) on the maintainer's words in the triage seat's chat: 「我建议直接启动 v18 开发吧」, 「你应该先解锁 v18 所有的卡片」, 「同意」. The ruling record is objectstack-ai/objectstack#220506037890422.What restarts, as the hold record (
6006187681) and the 2026-08-03 direction (5166015115) state it:- the
domain:uilane splits this card A2 → A1 / A3 → B1–B3; - each child is filed as its own card, with a
Blocked-by:line encoding that order; - this card stays the parent,
pm:epic, and is ⛔ never dispatched whole.
For the children: each one re-verifies its surface against current objectui
main. The objectui dependency stays on^17until 18.0 GA (stated on objectstack-ai/objectstack#22050), so a child that needs a v18 spec change says so in its ownRestart-when:.Grade unchanged:
priority:p3·domain:ui·area:workflow·target:v18.- the
- addedpm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtreeParent delegated to a dedicated epic PM — other PMs never dispatch into its subtreeand removed
on Oct 7, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorMore actionsSplit started: A2 filed as objectui#12029 and taken by the
domain:uiseat 3domain:uiexecution seat 3 (seat post objectui#9800) ·session_01CGZy1BGCjdN5cXqL9cnvB8· 2026-10-09T05:37Z. ⛔ This card is not claimed and stayspm:epic, never dispatched whole.- Authority: the maintainer, in this seat's session, verbatim: 「UI 还有很多任务,为什么不派发?包括epic 也可以派发」. A domain seat does not normally take
pm:epiccards; this instruction is why this one does. - The order (triage's restart record
6038179466): A2 → A1 / A3 → B1–B3, each child its own card, withBlocked-by:lines encoding the order. - Checklist:
- A2 · reusable record-preview component: objectui#12029, landed as
049012b(PR objectui#12031). It stays module-internal in app-shell; registering it as an SDUI type is B1's step. - A1 · the approvals data source: objectui#12032, landed as
d99b731(PR objectui#12043). A routedDataSourceper scope inapps/console/src/services/approvalRequestsDataSource.ts, with list-parameter translation and refusal, andApiDataSource.findOnenarrowed to 404. - A3 · progress/quorum widget: objectui#12033, landed as
ba9e820(PR objectui#12039).DecisionProgressIndicatoris module-internal.RecordApprovalsPanelalready draws a step list fromdecision_progress, so A3 extracts it. - B1 · detail page: objectui#12045, ruled 乙 by the maintainer (
6079807016, 2026-10-09T11:17Z). The page is metadata in objectstack's plugin-approvals; objectui registers renderers and mountsRecordDetailView.- The build round (v18-independent parts) landed as
a368ccb1(PR objectui#12062, ACCEPT6082369712): pointer fields draw A2's card in the details grid, the decision panel is built module-internal, and the console's request record route reads through A1. The card is nowpm:blocked, with its claim released (6082765553). - The page mount waits on three
Blocked-by:lines: spec(ui) v18: declare the approval decision panel as a page component type with a strictComponentPropsMaprow (objectui#12045 B1, ruling 乙) objectstack#22472 (the v18ComponentPropsMaprow), plugin-approvals: servesys_approval_request's detail page as a slotted page through the manifestpages, and declare the thread reply asapproval_comment(objectui#12045 B1, ruling 乙) objectstack#22473 (plugin-approvals' page andapproval_comment, blocked on #22472) and objectui#12030 (objectui moves tonext).
- The build round (v18-independent parts) landed as
- B2 list view, B3 delete the bespoke inbox and drawer: not filed yet; they follow B1. Carried to them:
- from B1's build round: the list-cell face of
referenceViapairs (a label read per row) belongs to B2, together with batched target-row reads; - from A2's ACCEPT
6076152376: B1 decides how a platform-asserted deletion renders, and B2 batches target-row reads per object; - from A1's amendment
6077174661: list rows carryviewerbut neverdecision_progress, so a progress column on B2's list needs a spec and server change; ListView honours aschema.dataprovider: 'api'config only on the gantt view; filter and sort on the approvals list are refused until a card shows a pull. - from A3's ACCEPT
6077352104: B3's inbox drawer holds a near twin of thedecision_progressblock, which goes with the drawer.
- from B1's build round: the list-cell face of
- A2 · reusable record-preview component: objectui#12029, landed as
- The remaining children are filed as A2 lands, at most three per fire, each re-verifying its surface against current
main. A child that needs a v18 spec change says so in its ownRestart-when:(objectui resolves@objectstack/spec^17until 18.0 GA). - The children are linked by a
Parent:line in their bodies and by this checklist. The seat's write path has no sub-issue operation.
Generated by Claude Code
- Authority: the maintainer, in this seat's session, verbatim: 「UI 还有很多任务,为什么不派发?包括epic 也可以派发」. A domain seat does not normally take
Ruled: 6006187681 · letter 停放 · 2026-10-06T00:15Z
Restart-when: closed objectstack-ai/objectstack#15193
Hold: 「2763 要等 v18再启动」 — the maintainer, live director chat, summon #34; record 6006187681. The 2026-08-03 direction (5166015115) stands; at restart the
domain:uilane splits this card A2 → A1 / A3 → B1–B3, ⛔ never dispatched whole.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.