Repository navigation
Commit be646ed
feat(console): tell the comment author, in one sentence, which mentioned users will not be notified; draw picked mentions as chips (#28261)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoPzeJHeDfhpMWRZ877JUwQh)_
Fixes #28218
Refs #27703
Clause-②: yes (widening: `@object-ui/data-objectstack`
`ObjectStackAdapter.create` gains an optional third parameter
`opts.onMentionsWithheld`, and `.update`'s existing `opts` gains the
same optional member; both still resolve the record, and no export, key
or route is added)
Metadata-protocol: none
## 维护者速读
- 改了什么:在记录讨论里 @ 了一个看不到这条记录的同事时,评论照常发出,同时弹出一句提示:「Grace Hopper、Linus
不会收到这条评论的通知。」发评论、回复、编辑时新加 @ 三处都会提示;不说原因,没有被拦下的人就不提示。输入框下方把已 @
的人显示为标签,预览里的 @姓名 也显示为标签。
- 为什么改:按 #27703 裁决 A(「27703 同意」),服务端已在 #28174 把被拦下的人的 id
回给作者本人的写入答复(`withheldMentions`),但控制台一直不读,作者以为对方已被通知。
- 风险与回滚:数据适配器的 `create` / `update`
多了一个可选的逐次回调,返回值不变,其他调用方不受影响;回调抛错不会让已保存的评论被当成失败。回滚即 revert 本 PR,服务端不受影响。
-
先看:`ui/packages/app-shell/src/views/RecordDetailView.tsx`(`tellMentionsWithheld`
与三个评论写回调);`ui/packages/data-objectstack/src/index.ts`(`notifyWithheldMentions`);`ui/packages/plugin-detail/src/RichTextCommentInput.tsx`(标签行与预览)。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/app-shell/src/views/RecordDetailView.withheldMentions-28218.test.tsx
packages/data-objectstack/src/withheldMentions-28218.test.ts
packages/plugin-detail/src/__tests__/RichTextCommentInput.mentionChips-28218.test.tsx`,预期
3 个文件、14 个测试全绿。
- 需要你做的:无,席位落地。
## What changes
**The answer reaches the caller**
(`ui/packages/data-objectstack/src/index.ts`). Measured first:
`ObjectStackAdapter.create` / `.update` resolved `result.record` and
dropped every other member of the write answer, so `withheldMentions`
never reached the record page. Both methods now take an optional
per-call listener, `opts.onMentionsWithheld(ids)` (on `update` beside
`ifMatch`, which still reaches the client alone). A module helper,
`notifyWithheldMentions`, reads the answer structurally, keeps the
string ids, and calls the listener once after the write succeeded, only
when at least one id is named. A runtime that predates the key, or a
write that withheld nobody, calls nothing. A throwing listener is
isolated like the subscriber fan-outs, so a stored comment is never
reported as failed. What every caller resolves is unchanged. A per-call
listener was chosen over the global `onWriteWarning` channel because the
sentence needs the picker's names, which only the writing page holds.
**One sentence, no reason**
(`ui/packages/app-shell/src/views/RecordDetailView.tsx`).
`tellMentionsWithheld` maps the ids to names through the mention
picker's suggestions (the list the write's `mentions` were extracted
from) and raises one `toast.warning`: `detail.mentionsWithheld`, "NAMES
will not be notified of this comment.", the names joined with the pack's
`validation.formInvalidJoiner`, the list punctuation the record feed
already uses. The three `sys_comment` writes pass it:
`handleAddComment`, `handleAddReply` and `handleEditComment`. Per the
ruling the sentence gives no reason (one bit per named user), and
nothing probes a colleague's access before the write (B was not taken).
**Mention chips**
(`ui/packages/plugin-detail/src/RichTextCommentInput.tsx`). Under the
editor, the users the text mentions are a list of chips labelled
`detail.mentionedPeople`, resolved with the same `extractMentions` the
write uses, so the row shows exactly whom the comment's `mentions` will
carry, multi-word names whole. In the preview, each picked `@NAME` is
one chip (longest name first, not followed by a letter or digit, as
`extractMentions` matches); any other `@word` keeps the plain highlight
it had. The preview escapes the text before any markup, chips included.
**Copy.** `detail.mentionsWithheld` and `detail.mentionedPeople` in all
ten packs. The sentence is phrased so it needs no plural forms in any
pack. `detail.mentionedPeople` is also in `DETAIL_DEFAULT_TRANSLATIONS`
(the provider-less mirror of the `en` pack).
**File surface.** Within the order's list: `RecordDetailView.tsx` (the
three callbacks and one helper), `RichTextCommentInput.tsx`,
`data-objectstack/src/index.ts`, the ten packs, tests and the root
changeset. Outside it, both small:
`plugin-detail/src/useDetailTranslation.ts` (one row, the chip row's
label, which the defaults-mirror test requires beside the pack) and
`ui/packages/data-objectstack/README.md` (the method list names the new
option, per `ui/AGENTS.md` #2). No `packages/spec/**` and no server
change.
**Serial constraints.** Neither #28116 nor #27713 has landed (both open
at the time of writing); this branch is on `main` `86cf7cef`. The edits
in `RecordDetailView.tsx` are the helper and three call sites, in hunks
neither PR touches as far as a read of their descriptions shows;
whichever lands first, this branch merges `origin/main` and resolves.
## Tests
New:
- `ui/packages/data-objectstack/src/withheldMentions-28218.test.ts` (4):
`create` hands the ids to the listener once and still resolves the plain
record, and the listener is never forwarded to the client; `update`
hands them on and `ifMatch` still reaches the client alone (no options
bag without it); CONTROL: an answer without the key, with an empty list,
or with no string ids calls nothing; a write without the listener
resolves as before, and a throwing listener does not reject a stored
write.
-
`ui/packages/app-shell/src/views/RecordDetailView.withheldMentions-28218.test.tsx`
(5), through the real `RecordDetailView`, its composer, reply input and
edit form, under a real `I18nProvider` with the `en` pack read by key: a
comment names the withheld users by their picker names (a multi-word
name included) and is posted; only the withheld users are named;
CONTROL: a comment whose mentions all reach their users says nothing; a
reply names them; an edit that adds a mention names them.
-
`ui/packages/plugin-detail/src/__tests__/RichTextCommentInput.mentionChips-28218.test.tsx`
(5): the chip row names each picked user once, multi-word names whole;
it follows the text and is absent with no mention; a prefix of a longer
name or an `@word` naming nobody is no chip; the preview draws a picked
`@NAME` as one chip and keeps other `@word`s plain; the preview escapes
the text.
Updated: three assertions that pinned the `sys_comment` create's exact
arguments now include the per-call listener (`commentWriteFailure-10899`
×2, `feedRecordScope`). Their behaviour is unchanged.
**Ablations** (each through `scripts/ablation-replace.mjs`, mutation
proved on disk, restored to the HEAD blob):
- `RecordDetailView.tsx`, the comment write without `onMentionsWithheld`
→ `RecordDetailView.withheldMentions-28218` 2 failed / 3 passed (the two
comment cases); restored, blob `6cf08c0a` = HEAD.
- `data-objectstack/src/index.ts`, `notifyWithheldMentions` not calling
the listener → `withheldMentions-28218` 3 failed / 1 passed (the control
stays green, as it should); restored, blob `20fba8d2` = HEAD.
## Acceptance notes
- **Live, on a stock stack** (`pnpm dev -- --fresh`, showcase, at this
branch's build, 2026-10-30T20:11Z). As the seeded admin, a user created
through `POST /api/v1/auth/admin/create-user` with no membership
(`membershipCreated: false`) cannot read the org's records. `POST
/api/v1/data/sys_comment` on a `showcase_project` record mentioning that
user and `usr_showcase_auditor_demo` answered `withheldMentions` with
both ids; a comment with no mentions answered no such key (`id`,
`object`, `record` only); a `PATCH` adding the user to a comment that
had no mention answered `withheldMentions` with that id. Through the
real `ObjectStackAdapter` (built `dist`) and `@objectstack/client`
against the same server: `create` with the listener heard `[that id]`
and resolved the record without the key; an `update` that added no
mention heard nothing. The server was stopped after the measure.
- The mention chips are drawn in the composer and its preview, as the
ruling names. A posted comment's body in the feed is rendered by the
timeline, not by this file, and is unchanged here.
- The sentence is a `toast.warning` after a successful write: the
comment is posted, so it is not an error.
## Verification
<details><summary>Local runs</summary>
Head `fcdb4c48`, on `main` `86cf7cef`.
- `pnpm exec turbo run build` (whole repo, under the verify lock) →
`Tasks: 116 successful, 116 total`, VERDICT command-exit 0.
- `type-check` of `@object-ui/data-objectstack`,
`@object-ui/plugin-detail`, `@object-ui/app-shell` (each `tsc --noEmit`,
plus `tsconfig.test.json` where the script runs it) → exit 0 each.
- Tests (the app-shell and console files are those under `ui/` that name
`sys_comment`, `mentionSuggestions`, `RichTextCommentInput` or
`extractMentions`):
- Under the verify lock: `pnpm --dir ui exec vitest run
packages/data-objectstack/ packages/plugin-detail/` plus those app-shell
and console test files, the new record-page file and
`defaults-maps-mirror-en-pack` → `Test Files 473 passed | 1 skipped
(474)`, `Tests 4547 passed | 18 skipped (4565)`, VERDICT command-exit 0.
- `pnpm --dir ui exec vitest run packages/i18n/
packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx`
→ `Test Files 93 passed (93)`, exit 0.
- eslint on the 19 changed `ts` / `tsx` files under `ui/` → 0 errors.
`pnpm --dir ui run check:new-line-citations` → 0 new citations, exit 0.
- Main has moved 6 commits past `86cf7cef`; neither #28116 nor #27713 is
among them, and `git merge-tree` with `origin/main` is clean (the ten
packs are shared paths, in other hunks).
</details>
<details><summary>Derived gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `fcdb4c48` → 35 commands, each run with
stdin from /dev/null and its exit code captured before any pipe; `--ran`
reconcile: `35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN`, exit 0. Two
more beyond the derivation, the Type Check UI i18n legs.
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --dir ui run check:i18n-keys :: exit 0
pnpm --dir ui run check:i18n-drift :: exit 0
```
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibvjxjPWDhWVKzkEU2x)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 916531a commit be646ed
21 files changed
Lines changed: 668 additions & 20 deletions
File tree
- .changeset
- ui/packages
- app-shell/src/views
- data-objectstack
- src
- i18n/src/locales
- plugin-detail/src
- __tests__
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
177 | 177 | | |
178 | 178 | | |
179 | 179 | | |
| 180 | + | |
| 181 | + | |
180 | 182 | | |
181 | 183 | | |
182 | 184 | | |
| |||
243 | 245 | | |
244 | 246 | | |
245 | 247 | | |
| 248 | + | |
246 | 249 | | |
247 | 250 | | |
248 | 251 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
450 | 450 | | |
451 | 451 | | |
452 | 452 | | |
| 453 | + | |
| 454 | + | |
453 | 455 | | |
454 | 456 | | |
455 | 457 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3049 | 3049 | | |
3050 | 3050 | | |
3051 | 3051 | | |
| 3052 | + | |
| 3053 | + | |
| 3054 | + | |
| 3055 | + | |
| 3056 | + | |
| 3057 | + | |
| 3058 | + | |
| 3059 | + | |
| 3060 | + | |
| 3061 | + | |
| 3062 | + | |
| 3063 | + | |
| 3064 | + | |
| 3065 | + | |
| 3066 | + | |
| 3067 | + | |
| 3068 | + | |
| 3069 | + | |
| 3070 | + | |
| 3071 | + | |
| 3072 | + | |
| 3073 | + | |
3052 | 3074 | | |
3053 | 3075 | | |
3054 | 3076 | | |
| |||
3078 | 3100 | | |
3079 | 3101 | | |
3080 | 3102 | | |
3081 | | - | |
| 3103 | + | |
3082 | 3104 | | |
3083 | 3105 | | |
3084 | 3106 | | |
| |||
3088 | 3110 | | |
3089 | 3111 | | |
3090 | 3112 | | |
3091 | | - | |
| 3113 | + | |
3092 | 3114 | | |
3093 | 3115 | | |
3094 | 3116 | | |
| |||
3119 | 3141 | | |
3120 | 3142 | | |
3121 | 3143 | | |
3122 | | - | |
| 3144 | + | |
3123 | 3145 | | |
3124 | 3146 | | |
3125 | 3147 | | |
| |||
3139 | 3161 | | |
3140 | 3162 | | |
3141 | 3163 | | |
3142 | | - | |
| 3164 | + | |
3143 | 3165 | | |
3144 | 3166 | | |
3145 | 3167 | | |
| |||
3187 | 3209 | | |
3188 | 3210 | | |
3189 | 3211 | | |
3190 | | - | |
| 3212 | + | |
3191 | 3213 | | |
3192 | 3214 | | |
3193 | 3215 | | |
| |||
3199 | 3221 | | |
3200 | 3222 | | |
3201 | 3223 | | |
3202 | | - | |
| 3224 | + | |
3203 | 3225 | | |
3204 | 3226 | | |
3205 | 3227 | | |
| |||
0 commit comments