Skip to content

架构:把审批 UI 重建在标准 SDUI 渲染器上(ListView + RecordDetailView + ApiDataSource + 多态记录预览组件) #2763

Description

@os-zhuang

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:ui lane 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 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)

  • A1 · ApiDataSource for approvals — wire ListView/RecordDetailView to read from the approvals endpoints (/approvals/requests, /approvals/requests/:id) so viewer + decision_progress come 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.
  • A2 · Polymorphic record-preview component — render a compact card for a (object_name, record_id) pair, resolving the target object + its display fields. Reusable well beyond approvals (audit rows, activity, comments all reference polymorphic targets).
  • A3 · Progress/quorum widget — a segmented indicator (unanimous / M-of-N / per-group 会签 ticks) bound to decision_progress. (Half a primitive; could be a component or a chart.)

Then compose as metadata

  • B1 · sys_approval_request detail page — sections (request fields), the polymorphic target-record card (A2), the progress widget (A3), a sys_approval_action related-list (timeline), and DeclaredActionsBar; data via A1.
  • B2 · sys_approval_request list view — columns incl. the polymorphic target-record + a key field (amount/urgency); scopes via A1; row + drawer actions via DeclaredActionsBar.
  • B3 · Delete the bespoke 审批中心 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 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.

Activity

  1. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Contributor

    维护者裁决(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

  2. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    分诊: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,父单转为进度视图(⛔ 父单本身永不派发)。

    拆解时必须带上的两条约束(来自裁决与正文,勿丢)

    1. composer 富交互:先证明 ActionParamDialog 能承载「评论 + 多附件 + 时间线上下文」;证不通则保留一个薄的定制 approve/reject action —— 组件级例外,不是手搓页面。
    2. A1 需要 framework 侧配套契约(approvals api data source 的 list/get 形状,含 viewer 与 decision_progress 计算字段)⇒ 那一段按跨座位转移协议走 objectstack 队列,objectui 侧子单写 Blocked-by:。

    与同族单的收敛关系(本轮一并理清)


    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. claude commented on Aug 14, 2026

    @claude
    Contributor

    重启条件已达成 —— 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

  4. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 23, 2026
  5. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Triage: domain:ui (lane only — tracking untouched). 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

  6. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    Contributor

    Ruling: batch #280 item 2 · letter 停放 · maintainer 「2763 要等 v18再启动」 2026-10-06T00:12Z

    Director seat, summon #34, objectstack session_0195diSK6vuqxgiJ38VWBHb6 (GitHub os-justin; written as objectstack-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: tracking stays; pm:on-hold is 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 the Blocked-by: unlock scan fires on it).
    • At restart: the objectui domain:ui lane splits this card into single-scope children in the ruled order (A2 polymorphic record preview → A1 approvals ApiDataSource with its objectstack-side contract card → A3 progress widget → B1–B3), with the two constraints 5203092261 records (the composer caveat; A1's cross-repo Blocked-by:). ⛔ The parent is never dispatched itself.
    • Grading (the maintainer's ask): pm:retriage is hung in the same stroke for the triage seat: give this card its priority:* and area:*; the split happens at restart, not now.

    四棱(本裁决新记录)

    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.

  7. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 6, 2026
  8. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    Contributor

    Path: ② 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:ui lane 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's Restart-when: already keys the restart to the v18 gate card.

    Labels: priority:p3 and area:workflow are added, and pm:retriage is removed. tracking, pm:on-hold and the rest stay.


    Generated by Claude Code

  9. added
    area:workflowApprovals and automation — the work that runs without a person driving it
    and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 6, 2026
  10. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    Contributor

    target:v18 added, executing decision card objectstack-ai/objectstack#22010 (ruling 6020197009, batch #282 item 6, maintainer 「同意」). The label is a release-board view, not a gate: this card's gate stays its Blocked-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.

  11. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    Contributor

    Triage: hold released, pm:on-hold → pm:epic (a parent, ⛔ never dispatched whole). closed objectstack-ai/objectstack#15193, this card's Restart-when:, now holds

    Triage 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#22050 6037890422.

    What restarts, as the hold record (6006187681) and the 2026-08-03 direction (5166015115) state it:

    • the domain:ui lane 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 ^17 until 18.0 GA (stated on objectstack-ai/objectstack#22050), so a child that needs a v18 spec change says so in its own Restart-when:.

    Grade unchanged: priority:p3 · domain:ui · area:workflow · target:v18.

  12. added
    pm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree
    and removed on Oct 7, 2026
  13. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    Contributor

    Split started: A2 filed as objectui#12029 and taken by the domain:ui seat 3

    domain:ui execution seat 3 (seat post objectui#9800) · session_01CGZy1BGCjdN5cXqL9cnvB8 · 2026-10-09T05:37Z. ⛔ This card is not claimed and stays pm:epic, never dispatched whole.

    • Authority: the maintainer, in this seat's session, verbatim: 「UI 还有很多任务,为什么不派发?包括epic 也可以派发」. A domain seat does not normally take pm:epic cards; this instruction is why this one does.
    • The order (triage's restart record 6038179466): A2 → A1 / A3 → B1–B3, each child its own card, with Blocked-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 routed DataSource per scope in apps/console/src/services/approvalRequestsDataSource.ts, with list-parameter translation and refusal, and ApiDataSource.findOne narrowed to 404.
      • A3 · progress/quorum widget: objectui#12033, landed as ba9e820 (PR objectui#12039). DecisionProgressIndicator is module-internal. RecordApprovalsPanel already draws a step list from decision_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 mounts RecordDetailView.
      • 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 referenceVia pairs (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 carry viewer but never decision_progress, so a progress column on B2's list needs a spec and server change; ListView honours a schema.data provider: '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 the decision_progress block, which goes with the drawer.
    • 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 own Restart-when: (objectui resolves @objectstack/spec ^17 until 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:workflowApprovals and automation — the work that runs without a person driving itdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtreepriority:p3sduiServer-Driven UI runtimetarget:v18tracking

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions