Skip to content

chatter 那条链没有 loading 信号源:RecordDetailView / record:chatter 从不给 RecordChatterPanel 传 loading #3209

Closed

Description

@os-zhuang

顺路发现于 #3205(那个 PR 只修了 RecordActivityTimeline 不读 loading 的最后一段渲染分支)。

#3205 修完之后,loading 这条链在 record:activity 上是通的:renderers/record-activity.tsx 自己算出 loading(自取数期间为 true)→ 传给 timeline → timeline 现在渲染 loading 行而不是空态。

但 chatter/discussion 那条链上没有信号源。RecordChatterPanel 自己的透传是好的(RecordChatterPanel.tsx 两处 loading={loading},sidebar 与 inline 各一),问题在它上面一跳:

  1. packages/plugin-detail/src/renderers/record-chatter.tsx —— 渲染 RecordChatterPanel 时只传 items / 各 handler,没有 loading={discussion?.loading}。DiscussionContextValue 明明声明了 loading?: boolean(packages/react/src/context/DiscussionContext.tsx),record:activity 也已经在读 discussion?.loading。
  2. packages/app-shell/src/views/RecordDetailView.tsx —— 真正的宿主:
    • const [feedItems, setFeedItems] = useState< FeedItem[] >([]),由两个 dataSource.find(sys_comment 与 sys_activity)异步填充,期间没有任何 loading state;
    • < DiscussionContextProvider items={feedItems} … > 不传 loading;
    • 自动追加的那个 < RecordChatterPanel items={feedItems} … > 也不传 loading。

后果与 #3205 同类:记录详情页打开的头一段时间,讨论面板渲染的是 "No comments yet"(emptyLabel),而不是加载态——只是这次缺的不是渲染分支(#3205 已经补上),而是没人把取数状态算出来。所以 #3205 的修复在 record:activity 上立刻可见,在 chatter 上要等这个 issue。

建议

在 RecordDetailView 里为 feed 取数加一个 loading state(两个 find 都 settle 之前为 true;注意它们是两条并行的 promise,.catch(() => {}) 也要收口),然后:

  • < DiscussionContextProvider loading={feedLoading} … >;
  • 自动追加的 < RecordChatterPanel loading={feedLoading} … >;
  • renderers/record-chatter.tsx 补上 loading={discussion?.loading},让作者手工摆放的 record:chatter / record:discussion 走同一条链。

不要在消费端加宽容兜底(比如 timeline 里猜"items 为空且刚挂载就算 loading")——信号应当由拥有取数的宿主产出,这也是 #3165/#3205 已经确立的形状。

参考

Activity

  1. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Contributor

    认领:PM 循环第 3 轮
    会话:session_01NVPjPzmmAJ2Ngtvgg5MSRa(objectui 分片 PM,账号 xuyushun441-sys,已在 objectstack#4604 登记)
    分支:claude/issue-3209-chatter-loading-signal
    Worktree:objectui-issue-3209

    分诊:无需拍板,直接派

    本单已经把该定的都定了 —— 信号由拥有取数的宿主产出,这是 #3165 / #3205 已确立的形状,而且原单明确写了不要在消费端加宽容兜底(不许在 timeline 里猜"items 为空且刚挂载就算 loading")。这与本轮已合并的 #3232 是同一条仓规:分歧在生产者收敛,不在消费者用启发式兜住。没有可选方案需要维护者选。

    两处容易做错的地方,派发时先钉住

    1. 两个 find 是并行 promise,收口要收全。 原单已经点出这点。feedLoading 必须在两条都 settle 之后才转 false,且 .catch(() => {}) 那条路径也要计入 —— 失败也是 settle,不能让一个 reject 把面板永久钉在 loading 态。这是最容易写出"永远转圈"的地方。

    2. 三处都要接,少一处就等于没修。 原单列的三个落点是一条链:

    • RecordDetailView 里新增 loading state;
    • <DiscussionContextProvider loading={feedLoading} …>;
    • 自动追加的 <RecordChatterPanel loading={feedLoading} …>;
    • 外加 plugin-detail/src/renderers/record-chatter.tsx 补 loading={discussion?.loading},让作者手工摆放的 record:chatter / record:discussion 走同一条链。

    record:activity(renderers/record-activity.tsx)已经在读 discussion?.loading,照它抄,不要发明第二种写法。

    验收要求

    要有测试钉住"取数期间渲染 loading 而不是 No comments yet",并且 mutation 验证:把 loading 传递摘掉后测试必须转红。#3205 修的是渲染分支、本单修的是信号源,两者合起来才是完整链路 —— 只断言"panel 收到了 loading prop"不够,要断言用户看到的是加载态而不是空态。


    Generated by Claude Code

  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Contributor

    PM 验收:ACCEPT — PR #3270,已入合并队列

    项 结果
    PR targets main、Fixes objectstack-ai/objectui#3209 ✅
    改动范围 ✅ 5 个文件,全在 packages/app-shell + packages/plugin-detail + changeset
    content/docs/releases/ 零改动 ✅
    CI 已完成的全绿、零失败;其余在跑,已开 auto-merge
    四个接入点 ✅ 全接

    我派发时钉的两处易错点,都处理到位了

    1. 两个并行 promise 的收口。 用 Promise.allSettled(inFlight),而两条链各自已有 .catch(() => {}) —— 所以 rejected 也计入 settled。这正是要点:没有部署 audit 插件的环境 sys_activity 会 404、对象关了 enable.feeds 时 sys_comment 会 403,这两种情况必须落到空态而不是永久转圈。changeset 里那句话说得对:「那会把"错说成空"换成"永远转圈",是更糟的 bug」。清理函数里还有 cancelled 守卫。

    2. 四个接入点一个不少 —— 新 state、DiscussionContextProvider、自动追加的 RecordChatterPanel、以及 record-chatter.tsx 的 loading={discussion?.loading}。

    一处超出我要求、而且是对的设计

    flag 没做成朴素布尔,而是按 object:recordId 键控:

    const feedFetchKey = … ? `${objectName}:${pureRecordId}` : null;
    const [settledFeedKey, setSettledFeedKey] = useState<string | null>(null);
    const feedLoading = feedFetchKey !== null && settledFeedKey !== feedFetchKey;

    理由写在注释里,两条我都认同:useState(false) + effect 里 setFeedLoading(true) 会让 effect 跑之前那一帧先画出空态(一闪而过的 "No comments yet");而切换记录时,朴素布尔会在新 effect 触发前显示上一条记录的已结算状态。派生式写法让一条记录的第一帧就是 loading,且不需要额外的 reset effect。并且它明说这是抄 record:activity 的 canSelfFetch && fetched === null 同一套写法 —— 一个惯例,不是两个,正是我要求的。

    测试断言的是用户看到什么

    我要求"不能只断言 panel 收到了 loading prop"。它做到了:全部断言落在 getByTestId('activity-loading') 在场 / queryByText('No comments yet') 为 null。而且两条投递路径都覆盖了 —— 合成页面(走 record:discussion → DiscussionContext)与作者页面无 discussion 槽(走宿主底部自动追加,没有 context 中转,是独立的一条线)。少测一条,另一条仍然带着 bug 上线。

    五处独立 mutation 验证,每处只摘一根线:

    摘掉 结果
    DiscussionContextProvider 的 loading 2 failed | 5 passed
    record-chatter.tsx 的转发 6 failed | 8 passed(两个文件同时红)
    自动追加 panel 的 loading 1 failed | 10 passed
    allSettled(inFlight) → 只取第一个 1 failed | 10 passed(正是"只有一个 settle"那例)
    allSettled → Promise.all 且去掉 .catch 3 failed | 8 passed(三个拒绝用例全红)

    后两条恰好证明了我担心的两个失效方向都真的被拦住。

    ⚠️ 越界发现 #3268 比本单更严重,已标 needs-user-decision

    RecordDetailView 在记录切换时从不重置 feedItems —— 两个取数回调都是按 id 并入 prev,没有任何地方把它设回 [],而该视图在记录间不会重新挂载(所有挂载点都没有 key)。结果:记录 A 的评论和活动会留在屏幕上,显示在记录 B 下面。 已在本单的测试夹具上复现(rerender 到 recordIdOverride="rec-B" 后 rec-A 的评论仍在)。

    它同时掩盖了本单的修复:timeline 的分支是 loading && filtered.length === 0,而残留行让 filtered 非空,所以记录间导航时 loading 态不显示。

    我没有让它顺手修,标了 needs-user-decision,因为形态需要维护者定:清空 vs 按 objectName:recordId 给 feed state 加键,而乐观更新的行(用户刚发的评论)让这个选择有实际后果。


    Generated by Claude Code

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions