Skip to content

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

Draft
objectstack-fleet[bot] wants to merge 10 commits into
mainfrom
claude/project-thread-4vnhy6
Draft

objectstack-fleet[bot] wants to merge 10 commits into
mainfrom
claude/project-thread-4vnhy6

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #30205

Clause-②: no
Metadata-protocol: none

维护者速读

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;
    • 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. 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-4vnhy6, 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 (scripts/ablation-replace.mjs, on committed 975b53ff, each restored by the tool: blob equals HEAD, git diff HEAD empty):
    • 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: 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. CI=true vite build of the console, dist/eager-closure.json: i18n-locale-en 59,951 gz with main's en.ts and 60,325 gz with this branch's, so +374 B, measured as two builds of the same tree that differ only in that file. That is over the chunk's 60,000 ceiling, and pnpm --dir ui check:eager-closure is red on this branch (i18n-locale-en 58.9 KB / 58.6 KB ceiling (OVER by 0.3 KB)). PR perf(i18n, app-shell)!: the marketplace pages' strings leave the eager language packs for a lazy marketplace namespace #30640 moves the marketplace strings out of the pack (59,951 → 57,010 at its head), so this PR lands after it: merge main once perf(i18n, app-shell)!: the marketplace pages' strings leave the eager language packs for a lazy marketplace namespace #30640 is in, rebuild, and the check clears with about 2.6 KB to spare. The ceiling is not raised here. 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 (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; the target user's own count then answered 3 owned announcements and 2 f_user rows;
    • after Confirm, the banned user's record page: en, zh-CN. No page error was recorded on any run.
    • Committed at 44ce98c6 and removed from the tree by the next commit.

Verification

All on head bfe597f0 (the tree of 3e306eff; the two later commits add and remove the screenshots) unless noted. Lock readings are the lock's own VERDICT lines.

  • 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__/ on bbee3036: 266 files, 2928 passed, 13 skipped, 1 failed (the partial hand-over case, which the next commit 3e306eff fixed by keeping the previous counts drawn during the recount); that file on 3e306eff: 12 / 12. The run includes every i18n suite walking the ten packs (key parity, untranslated identities) and the product-neutral renderer ratchet.
  • pnpm --filter @object-ui/app-shell run type-check (source and tests): exit 0 on bbee3036; the last source commit changes one statement.
  • eslint --no-inline-config on UserAssignedWorkPanel.tsx, userAssignedWork.ts, actionDialogCompanion.ts: 0 problems. On the edited ActionParamDialog.tsx: 0 errors, its existing warnings only.
  • node scripts/pm/dispatch-gates.mjs --commands: 35 derived families, the same list at fb246487 and bfe597f0. All 35 exit 0 on bfe597f0, including pnpm --dir ui exec vitest run scripts/ (170 files passed, 2 skipped; 4970 tests passed). --ran reconciles 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN.
  • Outside the derived list, on bfe597f0: pnpm --dir ui run check:i18n-keys, check:i18n-dead-keys, check:eager-locale-catalogues, check:new-line-citations exit 0. pnpm --dir ui check:eager-closure exit 1, i18n-locale-en over its ceiling by 0.3 KB, as the Acceptance notes say: it clears once perf(i18n, app-shell)!: the marketplace pages' strings leave the eager language packs for a lazy marketplace namespace #30640 is merged in.
  • Dogfood battery: not run. The title has no ! and Clause-② has no (narrowing) arm.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgV7XEtNA8u7WA2q1XZ

…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
@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 5, 2026
… 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

This branch has not been deployed

No deployments
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