Skip to content

feat(console): name the records a bulk run wrote, hint which required fields to fill, and show CRM expected revenue as money - #28378

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-5ywnd8
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-5ywnd8

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Part of #27520 (rows 8, 11 and 16; every other row stays on the card)

Clause-②: yes (widening: ActionResult gains the optional fieldErrors member, ActionFieldError is a new export of @object-ui/types, and ActionResponseOutcome gains fieldErrors; no accept set moves)
Metadata-protocol: none

维护者速读

  • 改了什么:批量动作(如 Tasks 的「Mark Done」)结果弹窗不再只写「Succeeded 2 / 2」,而是列出成功的记录名;动作因必填字段被拒(如草稿发票「Submit for Sign-off」缺 Issued On)时,提示变成「Issued On is required. Fill in Issued On, then run “Submit for Sign-off” again.」;CRM 示例的 Expected Revenue 公式声明为货币,与旁边的 Deal Value 同样显示为 $16,000.00。
  • 为什么改:18.3.0 RC 走查第 8、11、16 行:用户看不出批量改了哪些记录;被拒后不知道要先去编辑哪个字段;同一表格里两列金额一列带货币一列是普通数字。
  • 风险与回滚:批量名单只在每个失败都对应到具体记录时才显示,服务端只回计数时保持原样;提示只追加在服务端原文之后,只针对 required 字段项;ActionResult 多一个可选成员(放宽,不收紧)。回滚即 revert 本 PR。
  • 先看:ui/packages/app-shell/src/utils/consoleServerAction.ts(withRequiredFieldsHint);ui/packages/core/src/actions/actionResponse.ts(refusalFieldErrors);ui/packages/plugin-grid/src/components/BulkActionDialog.tsx(succeededRows)。
  • 两分钟验证:showcase 打开一张无 Issued On 的草稿发票点「Submit for Sign-off」,预期红色提示以「Fill in Issued On, then run “Submit for Sign-off” again.」结尾;Tasks → Bulk Actions 视图勾两行点 Mark Done,预期结果弹窗列出两条任务标题。
  • 需要你做的:无,席位落地。

What changes

Row 8, bulk result names the records. BulkActionDialog's result step now lists the records the run wrote under the "Succeeded N / M" line, named the way the confirm step names the affected records (affectedRecordName, the object's nameField), five at most plus "… and N more". The list is drawn only when every failure in the result sits under a record of the run, so the rows left over are exactly the successes. When a bulk call answered a bare count (executeBulkBatch's batch_<offset> entry), which records failed is unknown and the result keeps to its count. No server read and no executor change. New key grid.bulk.succeededRecords (the list's accessible name) in all ten packs.

Row 11, a required-field refusal says what to fill in. The action route answers a refused write with error.details.fields (one entry per failed rule, measured live: { field: 'issued_on', code: 'required', message: 'Issued On is required', label: 'Issued On' }). interpretActionResponse kept only the text. Now:

  • interpretActionResponse reads the entries (current envelope error.details.fields, legacy inner envelope fields), drops entries without a string field, and returns them as fieldErrors;
  • createServerActionHandler passes them on the failed ActionResult (fieldErrors?: ActionFieldError[], new optional member in @object-ui/types);
  • the console wrapper createConsoleServerActionHandler follows the server's sentence with console.serverAction.fillRequiredThenRetry ("Fill in {{fields}}, then run “{{action}}” again.") or, for an action with no label, fillRequiredFirst. Only required entries on a field count (_record and every other code keep the server's text alone). Field names are the server's labels, deduplicated and joined with validation.formInvalidJoiner, the key the form's field lists already join with (Intl.ListFormat is outside app-shell's lib target, and a unit list joins zh with no separator). The server's text stays first and whole; a stop is added only when it has none (a CJK one after CJK text). The wrapper's t now takes interpolation values; both call sites (useConsoleActionRuntime, RecordDetailView) pass them through to i18next as { defaultValue, ...values }.
  • An authored errorMessage still replaces the whole text in the runner, hint included, as before.

Row 16, money formula declares money. examples/app-crm/src/objects/opportunity.object.ts: expected_revenue declares returnType: 'currency'. The renderer reads returnType and never infers money from an expression (FormulaCellRenderer, objectui#11683 / #11748), so the undeclared formula drew the number face (16,000) beside the Field.currency Deal Value ($80,000.00). The seat lifted the order's no-examples line for exactly this edit (claim amendment 6539593257). No renderer change.

Tests

  • New ui/packages/plugin-grid/src/__tests__/bulkResultSucceededNames-27520.test.tsx (3): a clean run lists both titles under the accessible name; a run with one refusal lists only the two that landed and keeps the refused one under its refusal; a bulkUpdate answering a bare count names nobody.

  • New ui/packages/core/src/actions/__tests__/actionRefusalFields-27520.test.ts (5): the measured answer yields fieldErrors; an answer that also carries a producer-marked error.userMessage keeps them (the hint keys off the entries, never the text, so it survives a userMessage); field-less entries are dropped; the legacy inner list is read; the dispatch's failed result carries the entries.

  • New ui/packages/app-shell/src/utils/__tests__/consoleServerAction.requiredFieldsHint-27520.test.ts (6): the hint after the server's sentence; several fields deduplicated and joined with the pack joiner (zh 、); a label-less field and a label-less action; min and _record entries left alone; the host t receives the values; the server's own stop kept and a CJK stop added.

  • Adapted ui/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts: the de census counts the new „{{action}}“ span, with the same one-line history note its earlier rises carry. main's feat(app-shell): Setup pages for position holders and permission-set assignment #28310 raised the same pins by one while this was open, so after merging main (6a3bd4e) they read 103 paired spans and 103 / 103 / 0, both rises noted. pnpm --dir ui exec vitest run --maxWorkers=2 packages/i18n/ on the merge head: 93 files passed (1433 tests, 13 skipped).

  • Ablations (one-time, not kept, at 714e376, before the userMessage case was added), through scripts/ablation-replace.mjs, each restored to the HEAD blob with git diff HEAD empty: succeeded list never drawn → 2 failed of 3; count guard removed → 1 failed of 3; core stops returning fieldErrors → 8 failed of 10 (core + wrapper files); wrapper returns the refusal unhinted → 5 failed of 6. The suites resolve @object-ui/* to source, so no dist leg applies.

  • pnpm --dir ui exec vitest run --maxWorkers=2 packages/types/ packages/core/ packages/i18n/ packages/plugin-grid/: at 714e376: 963 files passed, 1 failed (de-quote-pairing-3876, 2 tests: its census counted 101 paired „…“ spans and the new fillRequiredThenRetry adds one). The pins were raised to 102 in 9f34e7d and the file reran 9/9 passed. The new userMessage case in e578f93 ran alone, 5/5 passed.

  • app-shell, the wrapper and both call sites: pnpm --dir ui exec vitest run --maxWorkers=2 packages/app-shell/src/utils/__tests__/consoleServerAction packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime packages/app-shell/src/views/RecordDetailView: 108 files / 761 tests passed (lock VERDICT command-exit 0). The whole packages/app-shell/ run sat 80 minutes under the lock without finishing on this shared box and was stopped; the rest of app-shell is left to CI.

  • type-check of @object-ui/types, core, i18n, plugin-grid, app-shell (full dts closure built first): all Done, 0 error TS. @objectstack/example-crm: typecheck exit 0, test 7 files / 48 tests passed, validate exit 0 (only the pre-existing field-no-consumers warnings on renewal_of / line_total).

  • Dogfood that reads the CRM example's metadata (asked for by the seat for the example edit), against the built examples/app-crm/dist/objectstack.json that carries returnType: 'currency': pnpm --filter @objectstack/dogfood exec vitest run test/install-local-purge-sample-data.dogfood.test.ts test/wall-environment-metadata-refusal.dogfood.test.ts test/guest-grants-channel.dogfood.test.ts → 3 files / 53 tests passed (lock VERDICT command-exit 0). The other dogfood files read only the CRM's flows or views, which this PR leaves alone.

Acceptance notes

  • Row 8's Status half is the example's own declaration, not touched: examples/app-showcase/src/ui/actions/index.ts MarkDoneAction patches done and progress on purpose, because status is governed by task_status_flow (only in_review -> done is legal), and its doc comment says so.
  • Row 11's "mark fields required for submission" half is not done. The rule exists (issued_on requiredWhen: record.status in ['sent', 'paid']), and the edit form evaluates it on the record as it stands, so a draft correctly shows Issued On as optional. Saying "required for Submit for Sign-off" means tying a field's rule to an action's patch, which no spec door declares. The seat files that as a spec card.
  • The bulk path runs a promoted object action per record through the same wrapper, so a required-field refusal there now reads with the hint under the refused record too.
  • The action toast's text comes from actionErrorDetail, which reads error.message and never error.userMessage (a flow-action refusal can carry one). That is unchanged here; the hint rides after whichever text is shown, because it is keyed off details.fields. Noted, not filed.
  • Row 16 also applies to any author: a money formula with no returnType draws a plain number by rule. os validate does not warn about it. Noted, not filed.

Verification

Live before/after (showcase and CRM on --fresh stacks, admin, 1440x900)

Base origin/main b1e1dcf (before) and this branch (after), full dts build plus pnpm console:build, LANG=en_US.UTF-8 pnpm dev -- --fresh -p 38611 (showcase) and pnpm --filter @objectstack/example-crm dev -- --fresh -p 38612, driven with Playwright.

  • Row 8, Tasks → Bulk Actions view, Content backlog + Evidence collection → Mark Done → Run. Before: dialog text Mark Done | Succeeded 2 / 2 | Done. After: Mark Done | Succeeded 2 / 2 | • Evidence collection | • Content backlog | Done.
  • Row 11, INV-1002 (draft, no Issued On) → Submit for Sign-off. Wire both times: 400 {"success":false,"error":{"code":"VALIDATION_FAILED","message":"Issued On is required","httpStatus":400,"details":{"fields":[{"field":"issued_on","code":"required","message":"Issued On is required","label":"Issued On"}]}}}. Toast before: Issued On is required. After: Issued On is required. Fill in Issued On, then run “Submit for Sign-off” again.
  • Row 16, CRM → Opportunities, row Initech — Expansion. Before: Deal Value $80,000.00, Expected Revenue 16,000. After: $80,000.00, $16,000.00.
Derived gates

At 6a3bd4e (this branch merged with main 0f29a88): node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 54 families against the diff. All 54 ran with </dev/null, exit codes recorded (27520-actions-gates-ran-6a3bd4e7.list in the seat's out folder). dispatch-gates --ran: "54 derived famil(ies) accounted for — 54 run, 0 NOT-MEASURED".

  • pnpm check:type-check-debt first exited 3 (not measured): the closure build could not resolve @objectstack/observability, a package main added, because the tree had not been reinstalled after the merge. After pnpm install --frozen-lockfile it reran: exit 0, "0 raw tsc error(s) total, none above its recorded number". The list records the rerun.
  • pnpm --dir ui exec vitest run scripts/ ran under the verify lock: exit 0.
  • Also run: pnpm --dir ui run check:new-line-citations exit 0; node scripts/check-sdui-manifest.mjs exit 0.
  • The same 54 families were derived and all exited 0 at 714e376, before the last test commit and the merge.

Generated by Claude Code

The result step said only 'Succeeded N / M'. It now lists the records the
run wrote, named as the confirm step names them, whenever every failure is
attributed to a record of the run; a count-only shortfall keeps the count.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzF2YJvme2ABRytQ9o
…to fill in

The action route's refusal carries details.fields, and the dispatch kept only
its text. The failed ActionResult now carries those entries (fieldErrors),
and the console's server-action wrapper follows the server's sentence with
'Fill in <fields>, then run <action> again.' The CRM Expected Revenue
formula declares its currency return type, so it reads as money beside
Deal Value.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzF2YJvme2ABRytQ9o
…ation

Intl.ListFormat is outside the app-shell lib target, and the form's field
lists already join with validation.formInvalidJoiner.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzF2YJvme2ABRytQ9o
Both sides raised the de pack's quoted-span census by one; the pins now
count both spans (103).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzF2YJvme2ABRytQ9o
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 6a3bd4e70622cc197527a2dd5959c2bbcb17c164 · 2026-10-31T08:33Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6a3bd4e70622cc197527a2dd5959c2bbcb17c164
Local-runs: none
Scope: ui/packages/types/src/ui-action.ts (ActionFieldError, ActionResult.fieldErrors) and index.ts, ui/packages/core/src/actions/actionResponse.ts (ActionResponseOutcome.fieldErrors, refusalFieldErrors) and serverActionHandler.ts, ui/packages/app-shell/src/utils/consoleServerAction.ts (withRequiredFieldsHint, the translate signature) and its two call sites, ui/packages/plugin-grid/src/components/BulkActionDialog.tsx (succeededRows), examples/app-crm/src/objects/opportunity.object.ts (returnType: 'currency'), the ten i18n packs, the de quote-pairing census, the three new test files, .changeset/27520-bulk-result-names-required-hint.md

Inputs: card #27520 (rows 8, 11 and 16 of the 18.3.0 RC walkthrough; the seat's claim 6539394970 and its amendment 6539593257 widening the surface and allowing the one example edit; the dev report 6541061691 with its deviations), PR #28378 (body with the live before and after on showcase and CRM, the file list of 24 files, the diff of 873 lines against main read whole, the six commits from the merge-base 0f29a88), the 56 check-runs on this head (read 2026-10-31T08:37Z: all 56 runs completed, none failed, none cancelled, none in progress; success: Auto Label, Build & E2E UI, Build Core, Build UI Deps, Check Changeset, Check PR Size, Dogfood Regression Gate, Governed Surface Queue Guard, Lint & Repo Gates, Lint UI, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Test Core, Test Core (all 8), Test UI, Test UI (all 16), Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter; skipped: 18.x upgrade harness (opt-in), Build Docs, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), UI Published Artifacts), and the tree on main for packages/spec/src/api/errors.zod.ts (FieldErrorSchema :301 to :321: field, the closed code catalog of ADR-0114, message, and label the display name of #3957; fields on the error at :461), the semantic migration 17.enhanced-api-error-field-errors-renamed.ts (the wire carries fields, never fieldErrors), ui/packages/plugin-grid BulkActionDialog on main (the confirm step's affectedRecordName) and examples/app-crm opportunity.object.ts (amount is Field.currency, the formula had no returnType). The head is mergeable and clean against today's main (merge-tree clean); the ten locale files moved on main since the merge-base in other keys.

① Derived judgments

  • Row 11 reads the contract the server already serves - right. The action route answers a refused write with error.details.fields, one FieldErrorSchema entry per failed rule; core now carries those entries (string field required, code, message and label kept when strings, the legacy inner fields read too) through ActionResponseOutcome and the handler onto ActionResult.fieldErrors, and the console wrapper appends one pack sentence naming the required entries' labels, after the server's own sentence and only on the failed branch. _record and every other code leave the text alone; an authored errorMessage still replaces the whole text as before. The sentence stop and the joiner follow the pack (validation.formInvalidJoiner, a CJK stop with no space), which the tests pin in zh.
  • Row 8 names only what provably succeeded - right. The list is drawn when every failure maps to a row of the run and the remainder equals the server's succeeded count; a bare-count answer keeps the count alone, so no record is named that may have failed. Names come from the same affectedRecordName the confirm step uses; no server read and no executor change.
  • Row 16 is the example's declaration, by rule - right. The renderer reads returnType and never infers money from an expression; expected_revenue now declares currency beside the Field.currency amount. The one examples/** edit is the one the seat allowed, and the example's typecheck, tests and os validate are green by the dev's account and the CI Dogfood leg.
  • The widening is what the declaration says - right. ActionFieldError is a new export of @object-ui/types, ActionResult and ActionResponseOutcome gain an optional member; no accept set moves, no spec path, no metadata key. Clause-②: yes (widening) and Metadata-protocol: none are right. The exported ConsoleServerActionTranslate gains an optional third parameter, which is the same widening on an app-shell type and needs no further line.
  • Pins hold both directions - right. Fourteen new cases across plugin-grid, core and app-shell, and four one-off ablations each reddening the cases they should and restored byte-identical; the de quote-pairing census is raised with its history note, as feat(app-shell): Setup pages for position holders and permission-set assignment #28310 raised it on main.
  • Check-runs as gate verdicts - read as above; every run on this head completed and none is red, cancelled or in progress at the read.

② Semver level

  • Five @object-ui packages patch - accepted. A non-protocol widening ships patch; the changeset names the consumer-visible member for code that dispatches through createServerActionHandler.

③ Boundary flags

  • The member is named fieldErrors while the wire key is fields - the spec retired EnhancedApiError.fieldErrors (migration 17, the wire never carried it). This PR reads error.details.fields, so the migration's criterion ("no consumer reads error.fieldErrors") holds; the ui member is a consumer-side name the ui already uses elsewhere (FlowRunner, the form renderer). Not owed, and no name approval owed: it is a ui runtime type, not a spec key.
  • Row 11's second half (mark fields required for submission) - not done, as the amendment ruled: no spec door ties requiredWhen to an action's patch; the seat files the spec card after landing. Row 8's Status half is the showcase action's own declaration (task_status_flow), untouched.
  • Two notes the dev left unfiled - a money formula with no returnType draws a plain number and os validate does not warn (a lint candidate with a named producer), and the action toast reads error.message and never error.userMessage. Both are pre-existing and outside the rows; the first is worth a card from the seat if a second example repeats it.
  • The whole app-shell suite was not run locally (stopped after 80 minutes under the lock); Test UI all shards are green on this head, which is the measure that counts.
  • The seat lands - no Tier H path (no docs/adr, no skills/**, 873 lines), no human gate.

④ Design judgment

  • Tried: a refusal whose entries carry label on some and not others (the hint falls back to the API name per entry, deduplicated); a bulk run where one row has no id (it is excluded from landed, the count guard then withholds the list rather than mislabel); an inner-envelope failure (read from the envelope, as the legacy path always was). Keying the hint off the entries rather than the text is what keeps it right under a producer userMessage. Nothing to add.

Verdict reasoning. The three rows are fixed where each one's cause lives, the per-field entries are read from the contract the spec already declares and carried on a declared optional member, the bulk list never names an unproven success, the example declares what the renderer reads, the declarations and the patch level are right, the pins hold both ways, and every check is green or skipped.

Implemented-by: claude/project-thread-5ywnd8
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibzC6RxMfB59EsgAoNN

席位核对

ACCEPT · seat domain:ui#1 · head 6a3bd4e7 · 2026-10-31T08:38Z

席位意见:批量执行操作后,如果部分记录成功、部分失败,结果对话框现在会列出成功的记录名称,而不只是一个数字;只有每条失败都能对应到具体记录时才列出,避免把未成功的记录误列为成功。服务器因必填字段为空而拒绝操作时,提示会在服务器原文后补一句「请填写某某字段,然后再运行该操作」,字段用显示名称,中文界面使用中文标点。CRM 示例里「预期收入」公式字段补上了货币返回类型,现在显示为金额而不是普通数字。

  1. Shape: Part of #27520 (rows 8, 11, 16), draft, base main, 24 files, +479/−14. Changes:
    • Types: ActionFieldError and ActionResult.fieldErrors.
    • core: actionResponse.ts and serverActionHandler.ts carry error.details.fields onto that member.
    • app-shell: withRequiredFieldsHint and its two call sites.
    • plugin-grid: the BulkActionDialog succeeded list.
    • The one allowed example edit: expected_revenue gains returnType: 'currency'.
    • Ten locale packs, the de census, three test files and a changeset.
  2. Seat read: the surface matches amendment 6539593257. Row 11's other half, marking fields required for submission, was ruled out of this PR because no spec door ties requiredWhen to an action's patch. The seat files that spec card after landing.
  3. Contract review: PASS 6541234847 on this head. ② is patch on five @object-ui packages. ③ notes that fieldErrors is a ui runtime name read off the wire's fields, with no spec key and no name approval owed. The two pre-existing notes (a money formula with no returnType draws without a lint warning, and the toast ignores userMessage) stay unfiled until a second example repeats them.
  4. Gates / check-runs: on 6a3bd4e7: 48 success, 8 skipped, 0 failure, including Test UI (all 16 shards). The app-shell suite that was not run locally is green in CI. mergeable_state clean.
  5. Landing: check-governed-merges --pr 28378 reports NOT governed, 493 changed lines ≤ 3000. The seat readies the PR and arms auto-merge, on the maintainer's 「按 pm-dispatch 审过的pr你可以自主落地」 (seat thread, 2026-10-30T14:19Z).

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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant