Repository navigation
Commit e5a24dd
fix(app-shell): a failed comments or activity read on a record page shows a load-failure notice with a Retry, not the empty state (#29358)
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb3KMJXK7TVJeUmNs3XdWYn3)_
Fixes #29188
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- **改了什么**:记录页的评论读取或活动读取失败(5xx
或网络断开)时,讨论面板、「动态」面板和「动态」标签页不再显示「暂无评论」/「暂无活动记录」,而是显示「无法加载此记录的评论/活动」并带「重试」按钮;读取失败期间「动态」标签不再显示数字
0。重试只重新读取失败的那一半。
- **为什么改**:staging 上托管环境的评论读取持续返回
500(cloud#3485),页面却显示「暂无评论」,用户以为自己写的评论丢了,故障只能在开发者工具里看到。
- **风险与回滚**:低。被拒绝(401/403)、空结果、404、接口被对象禁用和 400 的处理都与之前相同;只有 5xx
和无状态的网络失败走新状态。`@object-ui/react` 的 `DiscussionContextValue`
和时间线组件新增三个可选成员,均为加宽。附件面板 main 上已经能显示失败与重试,本 PR 只加了测试。回滚即 revert 本 PR。
- **先看**:① `ui/packages/app-shell/src/views/RecordDetailView.tsx` 的
`feedReadVerdictOf` 与 `handleRetryFeed`,以及 feed effect 里的 `retrying`
分支;② `ui/packages/plugin-detail/src/RecordActivityTimeline.tsx` 的
`failedNotice`;③
`ui/packages/plugin-detail/src/renderers/record-activity.tsx` 的计数置空。
- **两分钟验证**:`pnpm --dir ui exec vitest run
packages/app-shell/src/views/RecordDetailView.feedLoadFailure-29188.test.tsx`,预期
10 个测试全绿,覆盖评论 500、活动 500、重试、空结果对照、拒绝对照和附件 500。
- **需要你做的**:无,席位落地。
## What changes
A record page's comments read and activity read each end in one of three
verdicts per source: `answered`, `refused` or `failed`. Before this
change a read could only be refused or not, so a 5xx fell through to the
empty state.
- **`failed`** is a 5xx, or a rejection with no status at all (the
request never got an answer): `classifyLoadError`'s `network`, minus the
4xx statuses it also folds there (`feedReadVerdictOf` in
`RecordDetailView.tsx`). A 404 (a table the deployment does not serve),
an object that withholds its API (`api-disabled`) and a 400 (`rejected`)
stay `answered`, as before. `refused` is unchanged (`isRefusedFeedRead`,
objectui#11195).
- **The notice.** `RecordActivityTimeline` draws "We couldn't load the
comments for this record." / "…the activity…" (zh: 无法加载此记录的评论。/ 活动。)
with a Retry button, in place of the empty state when no rows came back
and above the rows when some did. This applies to the panel, the 动态 tab
(`stream`) and the 动态 panel (`peek`). Only the i18n sentence is
rendered: no status code, no server text. The header count, the
collapsed chatter label and the 动态 count (`record:activity`'s reported
count and `RecordTimelineCountProbe`) are unknown (no number) while a
read has failed, as for a refusal.
- **Retry** (`handleRetryFeed`) re-reads only the failed sources,
directly through `dataSource.find`, never the page payload (which
already answered). It does not touch the stream count the payload gave.
The button is disabled while the re-read is in flight over rows on
screen, and a retry that fails again keeps the notice.
- **Wiring.** `DiscussionContextValue` (`@object-ui/react`) gains the
optional `activityFailed`, `commentsFailed` and `onRetryFeed`.
`record:activity` and `record:chatter` forward them, and
`RecordChatterPanel` and `RecordActivityTimeline` take them as optional
props. A host that sets none of them renders exactly as before.
- **Locale keys**: `detail.activityLoadFailed`,
`detail.commentsLoadFailed` and `detail.retryLoadFeed` in all ten packs
(en, zh, ar, de, es, fr, ja, ko, pt, ru) at the en place, worded as each
pack's `attachmentsLoadFailed` / `retryLoadAttachments`, plus the
provider-less defaults in `useDetailTranslation.ts`. The eight non-en/zh
packs came in claim amendment 2, after `all-locales-key-parity.test.ts`
went red on Test UI 14/16 at 89ec072.
- **Attachments**: no code change. Measured on main, a 500 on the
attachments read (payload part or direct GET) already shows
`RecordAttachmentsPanel`'s "We couldn't load the attachments for this
record." with a Retry, and the count store holds no value, so no badge
is drawn. The payload path is now pinned.
## Contract review
Seat self-review, per the 10:43Z triage ruling. Every addition is
optional and additive; no member is removed, renamed or narrowed.
| Surface | Addition | Default when absent |
|---|---|---|
| `DiscussionContextValue` (`@object-ui/react`) | `activityFailed?:
boolean`, `commentsFailed?: boolean`, `onRetryFeed?: () => void` | no
failure state, no Retry |
| `RecordActivityTimelineProps` (`@object-ui/plugin-detail`) | the same
three props | `false`, `false`, no Retry button |
| `RecordChatterPanelProps` (`@object-ui/plugin-detail`) | the same
three props, forwarded | as above |
| `detail` locale namespace (`@object-ui/i18n`) | `activityLoadFailed`,
`commentsLoadFailed`, `retryLoadFeed`, in all ten packs | n/a (every
pack carries them) |
No spec, metadata or wire shape changes; the server's page payload is
read as before.
## Pins changed
- `RecordDetailView.feedRefusal-11195.test.tsx`, "a 500 is not \"no
permission\"": an activity 500 now shows the load-failure notice instead
of the empty state. It still asserts the no-permission line is absent.
- `RecordDetailView.feedLoading.test.tsx` (claim amendment 1, test
only): its rejections were bare `new Error('403 FEEDS_DISABLED')` / `new
Error('404 sys_activity')` with no status. A statusless rejection is
what a network failure looks like, so three rows went red. Each
rejection now carries the status its message names, in the ObjectStack
client's shape (`httpStatus`, `code`), as the 11195 test already does.
- "leaves the loading state when a feed read REJECTS (both rejecting)":
the comment read is a real 403 now, so the panel settles on the comments
no-permission line (objectui#11195's behaviour for a 403 since that
card) instead of the empty copy. The no-spinner assertion is unchanged.
- "leaves the loading state when only one read rejects and the other
resolves empty": the activity 404 carries its status and keeps the empty
copy. Assertions unchanged.
- "leaves the loading state when both reads reject" (authored discussion
node): the same as the first row.
## Tests
- New `RecordDetailView.feedLoadFailure-29188.test.tsx` (10 tests). It
uses the real `ObjectStackAdapter` over a held transport, the
`RecordDetailView.recordOpenOneRequest-28470` rig, under StrictMode. It
covers a comments 500 as a payload part and as a direct GET (notice,
Retry, no empty copy, no leak, 动态 tab with no count), Retry re-reading
only `sys_comment` directly (no activity re-read, the notice gone once
it answers), a retry that fails again, an activity 500 both ways, the
empty-200 controls both ways, the 403 refusal control (no-permission
line, no Retry) and the attachments payload 500 pin.
- New `RecordActivityTimeline.loadFailed-29188.test.tsx` (9 tests),
through the real en and zh packs and the `record:chatter` block under a
host `DiscussionContext`, plus the `peek` face. It covers the notice in
place of "No comments yet" (zh: 无法加载此记录的评论。, never 暂无评论), Retry calling
the host, rows kept with the notice above them, the activity line, no
Retry when the host offers none, a refusal and a failure side by side,
the empty control, and the disabled Retry while loading.
- **Measured before the change**, on main db6b408, with a temporary
copy of the 28470 rig (deleted before code): comments 500 → "No activity
recorded" and an `Activity0` tab badge; attachments 500 → the panel's
unavailable state with Retry and no count.
- **Ablation** (`scripts/ablation-replace.mjs`, `feedReadVerdictOf`
forced back to `answered` for a 5xx): 7 of 17 red across the new page
test and the 11195 file. Those are the six failure rows and the flipped
11195 row; the controls, the refusal row and the attachments pin stayed
green. Restored: blob equals HEAD, `git diff HEAD` empty.
- **Related sweep** from `ui/` under `scripts/pm/os-verify-lock.sh`,
`--maxWorkers=2`, at merged head 89ec072: the 109 test files that name
the timeline, the chatter panel, `DiscussionContext`, the feed renderers
or the refusal flags, plus the `RecordDetailView` tests that read
`sys_comment` / `sys_activity` / the payload / the timeline, plus the
two new files. 111 files, 2609 tests, exit 0.
`apps/console/src/__tests__/record-block-record-reach.test.tsx`: 13
passed.
- `pnpm exec eslint` on the 13 changed files: 0 errors; the warnings are
the files' existing `any` and hook warnings.
- At 53d0b77 (the eight locale packs): `packages/i18n/` suite 95 files,
1443 passed, 13 skipped, exit 0; `all-locales-key-parity.test.ts` with
the timeline test: 54 passed.
## Acceptance notes
- Claim 6576137247, amendment 1
(`RecordDetailView.feedLoading.test.tsx`, test only) and amendment 2
(the eight other locale packs). The diff stays inside them.
- The verdict reads `classifyLoadError`'s `network` kind and then
excludes the 4xx statuses that kind also holds, so a status-carrying 404
keeps the empty state the feedLoading pins describe. This matches the
claim's "5xx or network" reading.
- An object that withholds its API (`api-disabled`) or answers 400 still
shows the empty state, as the claim keeps. Neither is retryable, so a
Retry would be wrong advice there.
- Premise check: the issue's attachments half does not reproduce on main
(see Tests); it is pinned rather than changed.
## Verification
- `node scripts/pm/dispatch-gates.mjs --ran` at 89ec072: 34 derived, 34
run, 0 NOT-MEASURED, 0 UNRUN; every recorded exit is 0.
- `pnpm --dir ui exec vitest run scripts/` at 89ec072: 169 files
passed, 2 skipped (4903 tests). `node
ui/scripts/check-new-cross-file-line-citations.mjs`: 0 new citations.
- `pnpm exec turbo run type-check --filter=@object-ui/app-shell
--filter=@object-ui/react --filter=@object-ui/i18n
--filter=@object-ui/plugin-detail --concurrency=2` at 89ec072: exit 0,
68 of 68 tasks.
- `node scripts/check-sdui-manifest.mjs`: exit 0.
- `node scripts/check-changeset-no-major.mjs --base origin/main`, `node
scripts/check-adr-0087-registration.mjs --base origin/main`, `node
scripts/check-closing-keyword-parity.mjs` and `node
scripts/check-partof-closing-keyword.mjs`, with this body as the event:
exit 0 each.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (CI runs the repo lint)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (not touched)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QsR2jT854xvoBjntpsRE1F
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent ac4715a commit e5a24dd
22 files changed
Lines changed: 824 additions & 64 deletions
File tree
- .changeset
- ui/packages
- app-shell/src/views
- i18n/src/locales
- plugin-detail/src
- __tests__
- renderers
- react/src/context
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
40 | | - | |
41 | | - | |
42 | | - | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
43 | 47 | | |
44 | 48 | | |
45 | 49 | | |
| |||
106 | 110 | | |
107 | 111 | | |
108 | 112 | | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
109 | 119 | | |
110 | 120 | | |
111 | 121 | | |
| |||
294 | 304 | | |
295 | 305 | | |
296 | 306 | | |
297 | | - | |
298 | | - | |
| 307 | + | |
| 308 | + | |
299 | 309 | | |
300 | 310 | | |
301 | 311 | | |
302 | 312 | | |
303 | | - | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
304 | 317 | | |
305 | 318 | | |
306 | 319 | | |
307 | 320 | | |
308 | 321 | | |
309 | 322 | | |
310 | | - | |
| 323 | + | |
311 | 324 | | |
312 | 325 | | |
313 | 326 | | |
| |||
329 | 342 | | |
330 | 343 | | |
331 | 344 | | |
332 | | - | |
| 345 | + | |
333 | 346 | | |
334 | 347 | | |
335 | 348 | | |
| |||
368 | 381 | | |
369 | 382 | | |
370 | 383 | | |
371 | | - | |
372 | | - | |
| 384 | + | |
| 385 | + | |
373 | 386 | | |
374 | 387 | | |
375 | 388 | | |
376 | 389 | | |
377 | | - | |
| 390 | + | |
| 391 | + | |
378 | 392 | | |
379 | 393 | | |
380 | 394 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
260 | 261 | | |
261 | 262 | | |
262 | 263 | | |
263 | | - | |
264 | | - | |
265 | | - | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
266 | 269 | | |
267 | 270 | | |
268 | 271 | | |
| |||
0 commit comments