Repository navigation
Commit f84205b
feat(app-shell): the ban dialog says how many records the user holds and offers an optional hand-over (#30855)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoRihuYgPAMj645pMLAKLevd)_
Fixes #30205
Clause-②: yes (widening) — 19 new `console.assignedWork.*` keys ship in
every published `@object-ui/i18n` language pack, which publishes every
key it carries.
Metadata-protocol: none
Supersedes #30652. Same change, carried onto a branch this seat can
push: cut from `claude/project-thread-4vnhy6` at `6491bc3d` (the
previous account is out of tokens), then `origin/main` merged in at
`fd749a45e`, which includes PR #30640. The merge was clean; the diff
against `main` is the same 24 files, +1342 / -0.
## 维护者速读
-
改了什么:控制台「封禁用户」对话框在封禁原因上方多了一块:显示此人手上你能读到的记录数(总数、拥有、被指派,按对象和字段逐行),读不到的对象和被截断的统计如实写出;可选地选一位在职用户点「交接」,一次最多移交
500 条,结果显示已移交、失败(附每条原因)和剩余,剩余时可继续下一批。封禁按钮照旧,任何时候都能直接确认。
- 为什么改:#30205 的界面半。员工离职被封禁后,他名下的工单无人知晓、无人接手;服务器半(计数与批量转交接口)已由 PR #30546
落地,这里把它接到封禁对话框上。本 PR 接替 #30652(原账号额度用尽),内容不变,只是合入了最新 main。
- 风险与回滚:只改前端;面板只对平台自己的 `sys_user` 的 `ban_user`
动作出现,由宿主判定,不读任何作者元数据,其他动作的对话框不变。首屏英文语言包增加约 370 B gz,PR #30640
落地后余量足够:现读数 57,396 / 60,000,上限未动。回滚即还原本 PR。
-
先看:`ui/packages/app-shell/src/views/UserAssignedWorkPanel.tsx`(面板与交接流程)、`ui/packages/app-shell/src/views/actionDialogCompanion.ts`(哪个对话框带面板)、`ui/packages/app-shell/src/views/ActionParamDialog.tsx`
的 companion 渲染处。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/app-shell/src/views/ActionParamDialog.banAssignedWork-30205.test.tsx`,预期
12 个用例全绿(计数与拥有/指派拆分、读不到与截断、交接完成/部分失败/被拒、交接进行中仍可封禁)。
- 需要你做的:无,席位落地。
## What changes
- **A host-composed companion on the param dialog.** `ParamDialogState`
gains an optional `companion`, drawn at the top of the dialog's
scrolling region. The host decides it from the platform action being
run: the pure `resolveParamDialogCompanion(objectName, actionName, row)`
(`views/actionDialogCompanion.ts`) answers `{ kind:
'user-assigned-work', userId }` for `ban_user` on `sys_user`, with the
user id read from the row the action runs on, and nothing for any other
action. No authored key selects it, so `sys-user.object.ts` and the spec
are untouched. Both param-collection handlers,
`useConsoleActionRuntime`'s and `RecordDetailView`'s, call that one
function, so the two runtimes cannot drift.
- **`UserAssignedWorkPanel`** (app-shell internal, not exported). On
open it calls `security.assignedWork.count(userId)` of
`@objectstack/client` through the console adapter, as the signed-in
administrator, and shows:
- the total, **owned** (rows whose field is `owner_id`) and **assigned**
(every other user field), then one row per object and field with its
count, using the object's and field's translated labels (objects read by
name through `readObjectsByName`, with its `openPathObjectReaders-12811`
row);
- `truncated` as a note that this person may hold more, `unreadable` as
the named objects that were not counted, and `transferable: false` rows
(multi-valued user fields) as counted but not handed over here;
- a refused count inside the panel, without blocking anything.
- **The optional hand-over.** When some row is transferable, a `user`
picker (the platform's person picker, which already leaves out banned
users; the panel also leaves out the user being banned) and a Hand over
button. One click is one `security.assignedWork.transfer(userId, {
toUserId })` call (the server moves at most 500 rows). The report shows
moved / could not be moved / still held, and each failed record's
object, id and reason as the server worded it. When rows remain, the
button reads "Hand over the next batch". The counts are read again after
each hand-over, and the previous counts stay drawn until the new ones
arrive. A target the server refuses as banned (`reference_user_banned`)
is said in the reader's language; any other refusal shows the server's
message.
- **The ban never waits.** The dialog's Confirm is still the ban. It
stays enabled while a count or a hand-over runs, and after either fails.
- **Strings.** 19 `console.assignedWork.*` keys in all ten packs, in the
eager console namespace: none of them is a marketplace-page string, so
none belongs in the lazy `marketplace` namespace PR #30640 created.
`row` and `failure` are pure format strings and are added to
`LEGITIMATE_IDENTITIES` in `untranslated-identity-4376.test.ts` with
their reason.
- **Changeset.** `.changeset/ban-dialog-assigned-work-handover.md`,
`@object-ui/app-shell` and `@object-ui/i18n` patch.
Branch: the harness-assigned `claude/project-thread-hackce`, not
`claude/issue-30205-*`; the card is #30205.
## Tests
- New `views/actionDialogCompanion.test.ts` (5): the ban dialog gets the
companion for its row (string and numeric ids); CONTROL, another
`sys_user` action (`unban_user`, `set_password`) gets none; CONTROL,
`ban_user` on another object or with no object gets none; a row with no
id gets none.
- New
`hooks/__tests__/useConsoleActionRuntime.banCompanion-30205.test.tsx`
(2) and `views/RecordDetailView.banCompanion-30205.test.tsx` (2): each
handler hands the dialog the companion for `ban_user` with
`params._rowRecord`, and none for `unban_user`.
- New `views/ActionParamDialog.banAssignedWork-30205.test.tsx` (12),
with the client's `count` / `transfer` as recording doubles: one count
for the dialog's user and the owned/assigned split; `splitAssignedWork`
counts only `owner_id` as owned; truncated and unreadable notes; nothing
readable means no hand-over offered; a refused count keeps Confirm
enabled and Confirm still resolves; the hand-over button waits for a
pick; a complete hand-over reports and recounts; a partial hand-over
names each failure and offers the next batch, which calls transfer
again; a banned target is the translated sentence; another refusal shows
the server's reason; Confirm stays enabled and bans while a hand-over is
in flight; no panel and no count without the companion.
- `RecordDetailView.paramRuntimeParity-6431.test.tsx`: `companion` joins
`DIALOG_READS`, since both runtimes now write it.
- **Ablations** (from #30652, `scripts/ablation-replace.mjs` on
committed `975b53ff`, each restored by the tool: blob equals HEAD, `git
diff HEAD` empty; not re-run on this head. Of the three files, the merge
brought `main` changes only into `RecordDetailView.tsx` (+40 / -10,
outside the companion line); the handler and panel suites those
ablations turned red are green on `58719174dd` in the run below):
- `useConsoleActionRuntime.tsx`, `companion:
resolveParamDialogCompanion(...)` replaced by `companion: undefined`:
its handler test went red, 1 failed / 2, `expected undefined to deeply
equal { kind: 'user-assigned-work', … }`.
- The same replacement in `RecordDetailView.tsx`: its handler test red,
1 failed / 2, same message.
- `ActionParamDialog.tsx`, the companion render guarded off: the panel
suite red, 10 failed / 12 (the two that stay green are the pure split
and the no-companion case).
## Acceptance notes
- **Owned is 0 on most objects until #30646 lands.** The server's field
list skips `system: true` fields, and the platform-injected `owner_id`
is `system: true`, so only objects that declare `owner_id` themselves
(in the showcase, `showcase_announcement`) count or move owned records.
Measured on the live showcase in #30652: three `showcase_project` rows
owned by the user were not counted. The panel shows the server's answer
as it came and does not compute owned records itself; once the server
counts the injected field, the owned figure appears with no change here.
- **What "owned" means here.** Only rows whose field is `owner_id` are
owned; every other user field (an app's `assignee`, `f_user` in the
showcase) is assigned.
- **Eager English pack, re-measured after the merge.** `CI=true vite
build` of the console on `58719174dd`, `dist/eager-closure.json`:
`i18n-locale-en` 57,396 gz with this branch's `en.ts` and 57,026 gz with
`main`'s `en.ts` on the same tree, so +370 B. `pnpm --dir ui
check:eager-closure` exits 0 (`i18n-locale-en 56.1 KB / 58.6 KB ceiling
(headroom 2.5 KB)`). The ceiling is not raised. The eager chunk count is
103 on both builds.
- **One hand-over per click.** The panel does not loop over batches by
itself. A failed row still holds the user and is retried on the next
call, so an automatic loop would count the same failures again; the
administrator sees each batch's report and asks for the next.
- **A language switch** relabels the panel and does not recount.
- **Screenshots** (from #30652, live showcase, console via vite at 1440
wide, 2x):
- before, the ban dialog on `main` `14c67d44`, with the description and
the ban reason only:
[en](https://github.com/objectstack-ai/objectstack/blob/44ce98c65fa36cc386b4a1f2d4a14e3c0939f357/evidence/30205/before-ban-dialog-en-US-1440.png?raw=true),
[zh-CN](https://github.com/objectstack-ai/objectstack/blob/44ce98c65fa36cc386b4a1f2d4a14e3c0939f357/evidence/30205/before-ban-dialog-zh-CN-1440.png?raw=true);
- after, the dialog with the counts:
[en](https://github.com/objectstack-ai/objectstack/blob/44ce98c65fa36cc386b4a1f2d4a14e3c0939f357/evidence/30205/after-1-ban-dialog-counts-en-US-1440.png?raw=true)
(7 held: owns 3 on Announcement, assigned 2 on Field Zoo's single user
field, plus 2 on its multi-valued field, marked not transferable),
[zh-CN](https://github.com/objectstack-ai/objectstack/blob/44ce98c65fa36cc386b4a1f2d4a14e3c0939f357/evidence/30205/after-1-ban-dialog-counts-zh-CN-1440.png?raw=true)
(a second user who owns 2);
- after one hand-over to another user:
[en](https://github.com/objectstack-ai/objectstack/blob/44ce98c65fa36cc386b4a1f2d4a14e3c0939f357/evidence/30205/after-2-handover-report-en-US-1440.png?raw=true)
("Handed over 5, 0 could not be moved, 0 still held", the recount
leaving the 2 multi-valued rows),
[zh-CN](https://github.com/objectstack-ai/objectstack/blob/44ce98c65fa36cc386b4a1f2d4a14e3c0939f357/evidence/30205/after-2-handover-report-zh-CN-1440.png?raw=true);
- after Confirm, the banned user's record page:
[en](https://github.com/objectstack-ai/objectstack/blob/44ce98c65fa36cc386b4a1f2d4a14e3c0939f357/evidence/30205/after-3-banned-record-en-US-1440.png?raw=true),
[zh-CN](https://github.com/objectstack-ai/objectstack/blob/44ce98c65fa36cc386b4a1f2d4a14e3c0939f357/evidence/30205/after-3-banned-record-zh-CN-1440.png?raw=true).
- Committed at `44ce98c6` (in this branch's history) and removed from
the tree by the next commit.
## Verification
All on head `58719174dd` (the merge of `origin/main` `fd749a45e` into
`6491bc3d`) unless noted. Lock readings are the lock's own VERDICT
lines. Head `5290d27016` changes only the changeset's `Clause-②:` line
(to `yes (widening)`, matching this body); on it the changeset gates
were re-run (below) and the derived list is the same 35.
<details><summary>Affected tests</summary>
- `pnpm --dir ui exec vitest run packages/i18n/
packages/app-shell/src/views/ActionParamDialog
packages/app-shell/src/views/RecordDetailView
packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime
packages/app-shell/src/views/actionDialogCompanion
packages/app-shell/src/__tests__/
packages/app-shell/src/console/__tests__/openPathObjectReaders-12811.test.ts
packages/app-shell/src/console/marketplace/`: 322 files, 321 passed;
3450 tests passed, 13 skipped, 16 failed, all 16 in
`spec-symbol-parity.test.ts`, whose probe reads `@objectstack/spec`'s
`.d.ts` and found none (`expected 0 to be greater than 1000`) because
the dependency build ran with `OS_SKIP_DTS=1`. After a declaration build
of `@objectstack/spec`, that file: 41 / 41. The run covers every i18n
suite walking the ten packs (key parity, untranslated identities), the
product-neutral renderer ratchet and PR #30640's marketplace namespace
suites.
- `pnpm --filter @object-ui/app-shell run type-check` (source and tests,
after a declaration build of `@object-ui/app-shell^...`): VERDICT
command-exit 0.
</details>
<details><summary>Gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 35 derived families, the same list as on
#30652. All 35 exit 0 on `58719174dd`, including `pnpm --dir ui exec
vitest run scripts/` (170 files passed, 2 skipped; 4979 tests passed).
`--ran` reconciles 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN.
- Outside the derived list: `pnpm --dir ui check:eager-closure` exit 0
(above); `pnpm --dir ui run check:i18n-keys`, `check:i18n-dead-keys`,
`check:eager-locale-catalogues`, `check:new-line-citations` exit 0.
- On `5290d27016`: `node scripts/check-adr-0087-registration.mjs --base
origin/main`, `node scripts/check-changeset-no-major.mjs --base
origin/main`, `node scripts/check-empty-changeset.mjs --base
origin/main`, `node scripts/check-closing-keyword-parity.mjs`, `pnpm
check:nul-bytes`, `pnpm check:changeset-gate-self-tests` exit 0.
- Dogfood battery: not run. The title has no `!` and `Clause-②` has no
`(narrowing)` arm.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibzvSNwtTjiRhhyeVt8)_
<!-- signed: domain:ui#3 · os-tesla · session_01L9qibzvSNwtTjiRhhyeVt8
-->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent c1da957 commit f84205b
24 files changed
Lines changed: 1342 additions & 0 deletions
File tree
- .changeset
- ui/packages
- app-shell/src
- console/__tests__
- hooks
- __tests__
- views
- i18n/src
- __tests__
- locales
| 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 | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
| 86 | + | |
86 | 87 | | |
87 | 88 | | |
88 | 89 | | |
| |||
| 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 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| 47 | + | |
47 | 48 | | |
48 | 49 | | |
49 | 50 | | |
| |||
370 | 371 | | |
371 | 372 | | |
372 | 373 | | |
| 374 | + | |
| 375 | + | |
373 | 376 | | |
374 | 377 | | |
375 | 378 | | |
| |||
0 commit comments