Skip to content

feat(components): the edit form names the fields a visible update action would require, before it is pressed - #28459

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-3lhyz5
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-3lhyz5

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #28382

Clause-②: yes (widening: @object-ui/i18n gains the form.requiredForAction key in all ten locales; no accept set moves)
Metadata-protocol: none

Branch note: claude/project-thread-3lhyz5 is the branch the platform assigned to this card thread (claim 6546347620 names it), not the repo's claude/issue-N-slug convention.

维护者速读

  • 改了什么:编辑记录时,如果某个可见的「update + 静态 patch」动作执行后会让一个空字段变成必填,表单就在该字段下写一行提示,例如 showcase 草稿发票的「Issued On」下显示 “Required to run “Submit for Sign-off”.”。
  • 为什么改:服务端在动作的 patch 合并后的记录上跑 requiredWhen,而表单只看当前记录,所以第一次点「Submit for Sign-off」总会被拒(Issued On is required),事前没有任何提示。
  • 风险与回滚:纯提示,不拦保存、不改字段的必填状态、不改服务端;只在编辑表单生效,读对象 schema 走已有的数据源缓存。回滚即撤销本 PR。
  • 先看:ui/packages/components/src/renderers/form/form.tsx 的 actionOutcomes 块和 renderFieldDescription 调用处;ui/packages/components/src/renderers/form/actionRequirements.ts(哪些动作计入)。
  • 两分钟验证:cd ui && pnpm exec vitest run packages/components/src/renderers/form/__tests__/form-required-for-action-28382.test.tsx,预期 12 条全过;或在 showcase 打开一张 draft 发票点 Edit,Issued On 下出现该提示。
  • 需要你做的:无,席位落地。

What changes

Measured on main f91ab5a0 before any code, with the showcase (objectstack dev --seed-admin) and the console (vite, proxied):

On main This PR
POST /api/v1/actions/showcase_invoice/showcase_submit_signoff on draft INV-1004 (issued_on empty) 400 VALIDATION_FAILED, "Issued On is required" unchanged: the server stays the source of truth
Edit Invoice dialog on the same draft "Issued On" shows nothing "Issued On" shows "Required to run “Submit for Sign-off”." under the control
  • actionRequirements.ts (new, in the form renderer's directory). patchingUpdateActions keeps a declared action only when its whole write is known before it runs: operation: 'update', a non-empty static patch, no params (an empty list is fine), and at least one location (a locationless action is placed nowhere, per actionRendersAt). usePatchingUpdateActions reads the object's actions once per object through the data source the renderer already has (getObjectSchema, the read every form host makes and the adapter caches), on edit forms only.
  • form.tsx. actionOutcomes is the record each kept action would leave: { ...ruleRecord, ...patch }, the merge the server makes. An action is dropped when it is switched off, when the caller lacks its requiredPermissions (the action:bar gate), or when its visible predicate does not hold over the live record. That predicate fails closed and quietly, because the action's own button reports its fault. In renderFormField, a field with a requiredWhen that is not required now, is editable and is empty gets one line per action whose record makes it required. The line sits inside the field's FormDescription, so the control's aria-describedby announces it.
  • Nothing about Save changes: the field gets no asterisk and no aria-required, and the submit-time required check reads the same live verdict as before.
  • i18n. form.requiredForAction in all ten locales, plus the renderer's provider-less fallback row (byte-identical to en).

Tests

  • form-required-for-action-28382.test.tsx (new, 12 tests) through the real form renderer with a data source in SchemaRendererContext: the note on an edit form and its aria-describedby delivery; Save still writes with the field empty; the note goes when the field is filled; it gives way to the asterisk when the record as it stands requires the field; a create form reads nothing; an action whose visible does not hold, or that collects params, adds nothing; two actions give two lines and an unrelated patch none; and patchingUpdateActions unit cases.
  • Ablation (fix committed first; scripts/ablation-replace.mjs, each leg restored with git diff HEAD empty):
    • actionOutcomes.length > 0 && to < 0: 5 tests red (every case that expects a note).
    • visible !== true && to false &&: 1 red, the visible skip case.
    • !required && to true &&: 1 red, the asterisk case.
  • packages/components/src/renderers/form/ + packages/i18n/ (one run): 177 of 178 files passed; the only red was the German quote census below (2 tests). After moving it, packages/i18n/ reran: 94/94 files, 1438 passed, 13 skipped.
  • Moved pin: ui/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts. The German value „{{action}}“ is one more matched pair, so its census goes from 103 to 104 (both literals), with the history comment extended.
  • turbo run type-check --filter=@object-ui/components --filter=@object-ui/i18n: exit 0. eslint on each changed file: 0 errors. form.tsx has the same warning count as on main (58).

Acceptance notes

  • Heads-up only, as the card asks: Save is never blocked, and the server's refusal stays the source of truth for the action.
  • The renderer reads the actions itself, not through a new FormSchema key, so every form host (modal, drawer, page, tabbed, wizard) gets the note with no host or @object-ui/types change. This is the same pattern the renderer already uses for the record-currency used codes.
  • Not covered, by design: an action that collects params (the dialog's values are unknown before the press), and any non-declarative action (api / flow / script bodies). The optional "say what is missing on the button" half of the card is not done here: the button lives in the record header, which another card holds.
  • Live check: the after screenshot shows the note under "Issued On" on the showcase's Edit Invoice dialog.

Verification

  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 34 commands; on head c56ed27d all 34 ran with exit 0, and --ran reconciles 34 derived, 34 run, 0 NOT-MEASURED (the full list is in the os-dev-report on the card). node scripts/check-sdui-manifest.mjs exit 0. pnpm --dir ui check:i18n-keys, check:i18n-dead-keys and check:i18n-drift exit 0.
  • No open pull request touches ui/packages/components/src/renderers/form/** or ui/packages/i18n/src/locales/** at this head.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (per-file eslint locally; the repo-wide run is CI's)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (the 维护者速读 section is the seat's required Chinese summary)
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgXCPFJQiBaT5tcJurf

…ld require on the edit form

An update action with a static patch writes it over the record, and the
server runs requiredWhen over the result. The edit form judged the record
as it stands, so a field only the action makes required drew no mark and
the first press was refused. The form now reads the object's declarative
update actions, evaluates each field's requiredWhen over the record merged
with the patch, and notes the action under an empty field that comes out
required. A heads-up only: Save is never blocked.

Co-Authored-By: Claude <noreply@anthropic.com>
…n's patch would require

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…s effect setState

Co-Authored-By: Claude <noreply@anthropic.com>
…uiredForAction

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 31, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-sales · session_01L6bhgWqjXeTcS3jtug46Dv

维护者速读

  • 改了什么:编辑记录时,如果某个可见的「更新」动作(operation: 'update',带静态 patch)执行后会让一个字段变成必填,而这个字段现在还空着,字段下方会提前提示:「执行「Submit for Sign-off」时必填。」
  • 为什么改:showcase 里给草稿发票点「Submit for Sign-off」,第一次总被拒,报「Issued On is required」,因为编辑框事先没有任何提示(Mark the fields an update action will require before it is pressed (requiredWhen over the action's patch) #28382)。
  • 风险与回滚:只是提示。不拦保存,不加星号,最终仍以服务端的拒绝为准。带 params 的动作、写入不在 patch 里的动作、已关闭的动作、没有权限的动作、visible 不成立的动作都会跳过;visible 求值失败时按不显示处理。新建表单不读也不标。回滚就是 revert。
  • 先看:components/src/renderers/form/actionRequirements.ts,以及 form.tsx 里的 actionOutcomes。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/components/src/renderers/form/__tests__/form-required-for-action-28382.test.tsx,12 个用例应全部通过。
  • 需要你做的:无。不在受管面上,Tier S,CI 全绿后由席位落地。

席位意见:ACCEPT。按分诊 6541943879 的 A 路线实现:纯 ui,不加 spec 键,不改服务端。动作读取走数据源已有的 getObjectSchema 缓存,求值复用表单已有的 evalFieldPredicate。

席位核对 — ACCEPT · domain:ui#4 · Head-sha c56ed27dd702385f46f487365f72e856bff605be · 2026-10-31T18:00Z

  1. Shape: 15 files, +479 / −5, all inside the claim's surface (6546347620):
    • components/src/renderers/form/: form.tsx, a new actionRequirements.ts, and a new test.
    • One form.requiredForAction row in each of the ten locales.
    • The de quote census pin (de-quote-pairing-3876), moved 103 → 104 by the one new interpolated „{{action}}“ span.
    • A root changeset (components patch, i18n patch).
    • No spec, server, host, plugin-detail, RelatedList, RecordDetailView, plugin-grid or data-table.tsx file is touched.
  2. Premise and change measured (seat's own read):
    • Premise. It holds on main f91ab5a0: the action is refused with 400 "Issued On is required", and the edit form marks nothing (the thread's re-measure).

    • Which actions are read. patchingUpdateActions keeps only actions that meet all of these:

      • operation: 'update',
      • a non-empty plain patch,
      • no params (an empty list counts as none),
      • at least one locations entry.

      Actions are de-duplicated by name.

    • The read itself. usePatchingUpdateActions reads getObjectSchema only while the form is an edit form. A missing method or a failed read yields none. The answer is keyed to its dataSource and objectName, so a switch shows none until its own read lands. The adapter's getObjectSchema is cache-backed (data-objectstack/src/index.ts L5494) and re-fetched on a locale switch, so a served action label follows the locale.

    • Which outcomes count. actionOutcomes drops an action that isActionSwitchedOff reports, or whose requiredPermissions fail useCapabilityGate. It also drops one whose visible is false, or whose predicate fails over the live record (fail-closed, warn: false). For each survivor it builds { ...ruleRecord, ...patch }.

    • When the note shows. It is drawn only when all of these hold:

      • the field declares requiredWhen,
      • the field is not already required,
      • it is neither read-only nor disabled,
      • it is empty by isMissingForRequired,
      • its requiredWhen is true over an outcome record.

      It renders inside the existing FormDescription, which aria-describedby already names. Save is not gated.

  3. Gates and check-runs:
    • On this head at this reading: 11 success, 11 skipped, 30 in progress, 0 failed. Landing waits until every check is green or an expected skip.
    • The thread reported: 12/12 new tests, three ablations red then restored, dispatch-gates 34 commands exit 0 (--ran 0 NOT-MEASURED), type-check and the i18n checks green.
    • check-governed-merges --pr 28459: NOT governed, 484 changed lines.
    • Commit trailers are model-free.
  4. Accepted deviations:
    • (1) The fix commit precedes the test commit. Red is proven by ablation, three legs red on revert.
    • (2) The PR body's attribution line links the card thread.
    • (3) The census pin moved 103 → 104 for exactly the one new matched „…“ pair. The identity assertion close === open + rdq is unchanged.
  5. Out of scope, with destinations: the card's optional half, where the action button itself says what is missing, lives in the record header. That surface belongs to Record page v4: same-shaped related panels with no collapse, a persistent tab row under the header (the enlarged state is the active tab), Activity as the first panel and a tab, one top bar on every face, tiers by container width #27524 ④ (seat 5) and is not done here. A later pass can file it once ④ lands. Skills-docs: n/a (Metadata-protocol: none).
  6. Landing clause: Tier S. On all-green, the seat lands it by relay pr_ready plus automerge_enable.

Clause-② review of record (TIER_DEFAULT, in-seat)

  • Declared: Clause-②: yes (widening), on the claim and the PR.
  • What widens:
    • @object-ui/i18n (published) gains form.requiredForAction in all ten locales, with byte-identical English in the renderer's safe-translation defaults. No key is renamed or removed.
    • actionRequirements.ts (patchingUpdateActions, usePatchingUpdateActions, PatchingUpdateAction) is a module internal to the form renderer. form.tsx imports it, and no barrel re-exports it.
  • Does any accept set widen? No. No schema key is added: FormSchema is unchanged, and ActionSchema.patch / requiredWhen are read as declared. No request body changes, and Save behaves exactly as before. The note is presentation over data the client already reads.
  • Verdict: PASS. The change adds one translation row to a published locale pack and reads declared metadata it already has access to. No accept set in the contract changes.

Generated by Claude Code

…pec export

Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-sales · session_01L6bhgWqjXeTcS3jtug46Dv

维护者速读

  • 改了什么:与上一条 ACCEPT 6547129412 相同,只多一处改动:内部函数 isPlainRecord 改名为 isPatchObject。
  • 为什么改:CI 的 check:spec-symbols 报这个局部函数和 @objectstack/spec/data 导出的名字重名。
  • 风险与回滚:只改了名字,3 行,行为不变。回滚就是 revert。
  • 先看:actionRequirements.ts 的 L52、L71、L74。
  • 两分钟验证:pnpm --dir ui check:spec-symbols 应该 exit 0。
  • 需要你做的:无。

席位意见:ACCEPT,钉到新 head。

席位核对 — ACCEPT · domain:ui#4 · Head-sha e4ae56a2a9f0455f37d060f5e9845b49af08f669 · 2026-10-31T18:03Z

  1. Shape: c56ed27d..e4ae56a2 is one commit, 1 file, +3 / −3. It renames isPlainRecord → isPatchObject in ui/packages/components/src/renderers/form/actionRequirements.ts (the declaration and its two call sites). The trailer is model-free. The file list and every other line are unchanged from the head ACCEPTed in 6547129412.
  2. Why: the CI job "Type Check UI" was red on c56ed27d. Its check:spec-symbols step refuses a local symbol that shadows an @objectstack/spec/data export. The PR introduced the collision and this commit cures it. The thread reports check:spec-symbols and that job's other steps exit 0 locally, except type-check:vitest-setup, which is NOT MEASURED locally and is left to CI. It also reports the new test at 12/12 and eslint at 0.
  3. Everything else carries over from 6547129412 unchanged: the premise reading, the review points, the accepted deviations, and the in-seat TIER_DEFAULT Clause-② PASS. The rename moves no export, key or accept set.
  4. Landing clause: Tier S. When every check is green, the seat lands it by relay pr_ready plus automerge_enable.

Generated by Claude Code

…requiredWhen

With no requiredWhen on the form there is nothing an action's patch could
make required, so the form makes no read beyond the ones its host made.
Keeps the hosts' one-schema-read pins.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l and removed size/m labels Oct 31, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-sales · session_01L6bhgWqjXeTcS3jtug46Dv

维护者速读

  • 改了什么:表单里至少有一个字段声明了 requiredWhen 时,渲染器才去读对象的动作。没有 requiredWhen 的表单不再多读一次。其余与 ACCEPT 6547165406 相同。
  • 为什么改:plugin-form 有两组测试固定了「宿主只读一次 getObjectSchema」,在 e4ae56a 上被这次多出来的一次读取打红了。
  • 风险与回滚:只是收紧了读取条件。没有 requiredWhen,任何 patch 都不可能让字段变成必填,所以对这类表单少读一次不会漏掉提示。回滚就是 revert。
  • 先看:form.tsx 里的 declaresRequiredWhen。
  • 两分钟验证:新测试 13 条全绿,其中新增的一条验证「没有 requiredWhen 时不读」。supersededReads-10712 和 recordFormsErrorClears-10682 原样通过。
  • 需要你做的:无。

席位意见:ACCEPT,钉到新 head。

席位核对 — ACCEPT · domain:ui#4 · Head-sha 09cb02950eb69ba4327431b778716b923b93f71d · 2026-10-31T18:45Z

  1. Shape: e4ae56a2..09cb0295 is one commit, 2 files, +35 / −1. In form.tsx, a memoised declaresRequiredWhen (some field in the rendered fields list carries requiredWhen != null) now gates usePatchingUpdateActions(…, !isCreateForm && declaresRequiredWhen). The test file gains one case, "no requiredWhen, no read", for 13 in all. The trailer is model-free.
  2. Why: on e4ae56a2, Test UI shards 3/16 and 14/16 went red in plugin-form's supersededReads-10712 and recordFormsErrorClears-10682. Each covers 5 hosts, and both pin one getObjectSchema read per host. The renderer's added read broke that pin, and this PR introduced it. The fix leaves those pins untouched: a form without requiredWhen makes no extra read. A form that declares requiredWhen makes one read, which the adapter's schema cache serves (data-objectstack getObjectSchema).
  3. Safety of the gate: fields is the list the renderer draws, and the note is only ever computed for a field that declares requiredWhen. Skipping the read when no field declares one cannot drop a note that would have shown.
  4. Local runs (thread):
    • vitest across components/renderers/form, components/renderers/basic, plugin-form and plugin-detail: 678 files, 677 pass and 1 is skipped; 6434 tests pass; VERDICT exit 0.
    • eslint: 0 errors, and the form.tsx warning count is unchanged at 58.
    • On e4ae56a2, CI was red only on those two shards and their aggregate.
  5. Carries over from 6547129412 / 6547165406, all unchanged: the review points, the accepted deviations and the in-seat TIER_DEFAULT Clause-② PASS. No export, key or accept set moves.
  6. Landing clause: Tier S. On all-green, the seat lands it by relay pr_ready plus automerge_enable.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 19:04
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 31, 2026 19:04
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit 4bc963c Oct 31, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-3lhyz5 branch October 31, 2026 19:22
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mark the fields an update action will require before it is pressed (requiredWhen over the action's patch)

1 participant