Skip to content

feat(app-shell): the ban dialog says how many records the user holds and offers an optional hand-over - #30855

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/project-thread-hackce
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/project-thread-hackce

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

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.

维护者速读

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 perf(i18n, app-shell)!: the marketplace pages' strings leave the eager language packs for a lazy marketplace namespace #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 feat(app-shell): the ban dialog says how many records the user holds and offers an optional hand-over #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 Assigned work (#30205): the count and the handover skip the platform's injected owner_id, so a banned user owns nothing on most objects #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 feat(app-shell): the ban dialog says how many records the user holds and offers an optional hand-over #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 feat(app-shell): the ban dialog says how many records the user holds and offers an optional hand-over #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, zh-CN;
    • after, the dialog with the counts: en (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 (a second user who owns 2);
    • after one hand-over to another user: en ("Handed over 5, 0 could not be moved, 0 still held", the recount leaving the 2 multi-valued rows), zh-CN;
    • after Confirm, the banned user's record page: en, zh-CN.
    • 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.

Affected tests
  • 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 perf(i18n, app-shell)!: the marketplace pages' strings leave the eager language packs for a lazy marketplace namespace #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.
Gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 35 derived families, the same list as on feat(app-shell): the ban dialog says how many records the user holds and offers an optional hand-over #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.

Generated by Claude Code

claude added 11 commits November 5, 2026 20:19
…and offers an optional hand-over

The sys_user ban_user dialog gains a host-composed companion above its
params: the user's owned and assigned record counts from
security.assignedWork.count (truncated and unreadable said as the server
answers them), and an optional bulk hand-over through
security.assignedWork.transfer whose report shows what moved, what failed
and what remains. Confirm stays the ban and never waits on the panel.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgV7XEtNA8u7WA2q1XZ
…format-string keys as legitimate identities

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgV7XEtNA8u7WA2q1XZ
…rk vocabulary in its own module

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgV7XEtNA8u7WA2q1XZ
…, removed by the next commit

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgV7XEtNA8u7WA2q1XZ
… that read at 1

Review follow-up on the ban dialog's assigned-work panel (objectstack#30205).

- The panel names each object through `servedObjectLabel` (objectstack#30486):
  the label the server served is drawn as given, and the client bundle is
  asked only when none is served, so a renamed object shows its new name. A
  new case pins a served label that differs from the bundle's.
- en, de, fr, es and pt now say the summary (and, in de, fr, es and pt, the
  hand-over report) as "label: number", the form ru, ko and ja already use, so
  a count of 1 is not a plural noun. No plural keys. The eager English pack
  stays at 60,325 gz, the same +374 B over main as before.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgV7XEtNA8u7WA2q1XZ
The ban dialog's assigned-work panel (objectstack#30205) took every object
definition off `useMetadata().objects`, which pulls the whole object registry
back when the dialog opens and which the open-path census of object readers
did not classify. It now asks `readObjectsByName` for the objects the count
and the hand-over report name, as `ActivityObjectLabel` does, and the census
lists it as a `byName` reader. A new case pins that the counted objects are
asked by name and the whole list is never read.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgV7XEtNA8u7WA2q1XZ
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · os-tesla · session_01L9qibxW6qraU2Z3mwupUGp

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 5290d2701684b0d54d7db31dda3702e0f409c733
Local-runs: none

① Derived judgments

  • The declaration names the whole widening: one new console.assignedWork block of 19 leaves in all ten ui/packages/i18n/src/locales/*.ts packs, +22 lines each, nothing removed or renamed (git diff --stat origin/main...5290d270, +1342/−0). No other published face moves: ParamDialogState.companion is an optional new member of an app-shell view state that only the two in-package hosts (useConsoleActionRuntime, RecordDetailView) fill; actionDialogCompanion.ts, userAssignedWork.ts and UserAssignedWorkPanel.tsx are not re-exported from the package entry.
  • No authored-metadata key is read or added: the companion is picked from the platform action (sys_user + ban_user) by resolveParamDialogCompanion, so spec and the SDUI manifest stay untouched. The counts and hand-over are the existing security.assignedWork.count / .transfer of @objectstack/client, typed off the adapter's client (userAssignedWork.ts), so the panel adds no server contract.

② Semver level

@object-ui/app-shell patch and @object-ui/i18n patch, Clause-②: yes (widening), Metadata-protocol: none: additive keys and an optional state member, no break, matching the lane's patch convention for new pack keys.

③ Boundary flags

Implemented-by: claude/project-thread-hackce
Reviewed-by: session_01L9qibxW6qraU2Z3mwupUGp

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · os-tesla · session_01L9qibxW6qraU2Z3mwupUGp

维护者速读

  • 改了什么:封禁用户的对话框里,封禁原因上方多一块「此人持有的记录」:总数、拥有几条、被指派几条、按对象和字段分行;可选一键把这些记录移交给另一个在职用户(每次最多 500 条,剩下的可再点「移交下一批」)。
  • 为什么改:管理员封禁一个人时看不到他手上还挂着多少客户、商机、任务,封完这些记录就没人跟进了。服务端的计数与移交接口已在,前端一直没接。
  • 风险与回滚:只在 sys_user 的 ban_user 对话框出现;「确认」仍是封禁本身,计数或移交在跑、失败都不挡它。19 条新文案(十种语言)。首屏英文包 57,396 / 60,000 gz,上限未动。回滚即撤本 PR。
  • 先看:ui/packages/app-shell/src/views/UserAssignedWorkPanel.tsx,以及 actionDialogCompanion.ts 里决定哪个对话框带面板的那一个函数。
  • 两分钟验证:cd ui && pnpm vitest run packages/app-shell/src/views/ActionParamDialog.banAssignedWork-30205.test.tsx packages/app-shell/src/views/actionDialogCompanion.test.ts,预期全绿。
  • 需要你做的:无,席位落地。
  • 席位意见:可合。「拥有」数目前在多数对象上读 0,原因在服务端(计数与移交跳过了平台注入的 owner_id),已由 Assigned work (#30205): the count and the handover skip the platform's injected owner_id, so a banned user owns nothing on most objects #30646 承接,本 PR 照服务端回答原样显示、不自己算,不另立卡。

席位核对 — ACCEPT · head 5290d27

  • 形态:24 个文件,+1342/−0(app-shell 视图与两个宿主各几行、一个新面板、五个测试文件、十个语言包各 22 行、一份 changeset)。check-governed-merges --branch origin/claude/project-thread-hackce:NOT governed,1342 行 ≤ 3000。Fixes #30205,目标 main,取代 feat(app-shell): the ban dialog says how many records the user holds and offers an optional hand-over #30652(已关,6678992636)。与认领 6678119224 及修订 6678138571、6679032591(Clause-② 改 yes)一致。本 PR 由本席派出的卡线程实现,席位不写码。
  • 契约:Clause-②: yes (widening),Metadata-protocol: none,两包 patch。TIER_DEFAULT 申报面复核 6679410422 在本 head 上 PASS。
  • 席位自己核对(读 diff):与旧 feat(app-shell): the ban dialog says how many records the user holds and offers an optional hand-over #30652 头逐行比对,文件集与新增行完全相同,只差 changeset 的 Clause-② 一行。resolveParamDialogCompanion 只认 sys_user + ban_user 且行上有 id;两个宿主(useConsoleActionRuntime、RecordDetailView)调同一个函数;面板按 userId 作 key,换人不串旧数。companion 是可选成员,不读任何编写元数据。changeset 正文逐句对 diff:计数来源、移交 500 条一批、确认不等待、十种语言都与代码一致。
  • 前提实测:dev 报告 6678981959(status done)。合入 main(含 perf(i18n, app-shell)!: the marketplace pages' strings leave the eager language packs for a lazy marketplace namespace #30640)后:322 个 ui 测试文件绿(16 个 spec-symbol-parity 红是本地 OS_SKIP_DTS 所致,做 spec 声明构建后 41/41),app-shell type-check exit 0;派生门禁 35/35 exit 0;check:eager-closure exit 0。
  • 门禁 check-runs:本 head 46 success、13 skipped,0 红;mergeable_state clean。
  • 接受的偏差:消融未在合并后的 head 重跑(沿用 feat(app-shell): the ban dialog says how many records the user holds and offers an optional hand-over #30652 在 975b53f 上的结果);main 在被消融文件里只改了 RecordDetailView.tsx,当时变红的套件在本 head 上都绿。
  • 落地条款:维护者常设令 「按 pm-dispatch 审过的pr你可以自主落地」(2026-10-30T14:20Z),本场 「ui 车道跟进一下 3小时内没更新的pr和已派发的issue,接手处理。」。中继 pr_ready + automerge_enable(后者等维护者在席位线程的明确放行)。合并即关卡,同笔摘 pm:dispatched。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 6, 2026 07:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit f84205b Nov 6, 2026
60 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-hackce branch November 6, 2026 14:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Offboarding: banning a user says nothing about their open work, and there is no way to hand it over

1 participant