Skip to content

fix(fields,components): a date typed in a form that the picker cannot read is a field error that refuses submit - #27623

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-t6m105
Oct 29, 2026
Merged

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

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

Requested by Bill · project thread
Fixes #27483
Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:记录表单里手输一个读不懂的日期(如 13/45/2026)时,该字段当场标红并显示提示,点「创建」被拒、对话框不关、不发请求;改对或清空后才能保存。
  • 为什么改:原来日期控件不写入读不懂的文本,表单值保持为空(编辑时保持旧值),照常保存并弹成功提示,用户以为日期已存好。
  • 风险与回滚:只新增一个可选的控件回调和一条表单校验项,只对 date / datetime 控件生效;回滚即还原本 PR,无数据迁移。
  • 先看:ui/packages/components/src/renderers/form/form.tsx(新校验项 invalidInput 与回调)、ui/packages/fields/src/widgets/date-picker/DatePickerField.tsx(上报与撤回)、ui/packages/fields/src/widgets/types.ts(新属性的契约说明)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/fields/src/__tests__/date-picker-invalid-text-blocks-submit-27483.test.tsx,预期 5 条全绿。
  • 需要你做的:无,席位落地。

What changes

The date / datetime picker never writes text that names no date. In a record form that meant the form value stayed what it was (empty on create, the old date on edit), the submit went through and the success toast fired. Per the triage ruling, the unparseable text is now marked invalid through the form's existing field validation, the way a missing required value is.

  • onInvalidInputChange (@object-ui/fields, FieldWidgetComponentProps): a new optional widget prop. A widget that parses typed text fires it with the message to show when the committed text names no value, and with null once the text is valid, cleared, or the widget unmounts. Its doc comment names its producer (the date picker's two faces) and its consumer (the form renderer). It is added to toHostProps' HOST_PLUMBING_KEYS, so the compile-time partition still holds and FieldEditWidget delivers it when a host passes it.
  • DatePickerField / DateTimePickerField: report on change only (not on every render), withdraw on unmount, and report nothing while readonly. A widget given the callback leaves the message text to the form's FormMessage, as the error slot already does; a host that passes none (the grid's inline editor, the action-param and bulk-action dialogs) keeps the picker's own notice, byte for byte. aria-invalid is set either way.
  • form renderer (@object-ui/components): a per-field invalidInputMessages map (a ref, beside requiredMessages, read by the validator at call time) and a first rules.validate entry, invalidInput, that refuses while a message is held. The callback stores the message and runs trigger(name), so the error shows when the user leaves the field, not only at submit; a withdrawal clears only an invalidInput error. stripRegisteredFieldProps forwards the callback to the date / datetime widgets only, so no other widget spreads an unknown handler onto its DOM.
  • The time widget is a native input[type=time] and shares neither the parser nor the commit path, so it is untouched.

Tests

  • ui/packages/fields/src/__tests__/date-picker-invalid-text-blocks-submit-27483.test.tsx (new, real form renderer plus the production registration of the picker): typing 13/45/2026 and leaving the field shows the form message and aria-invalid, with no second notice; pressing Create is refused (the form scrolls to the field) and onSubmit is never called; correcting the text clears the error and saves 2026-10-20; clearing it lets an optional empty date save; a valid typed date saves (control); datetime refuses the same way.
  • ui/packages/components/src/renderers/form/__tests__/form-invalid-input-refuses-submit-27483.test.tsx (new, probe widget): a report becomes the field message at once and refuses submit; a withdrawal clears it and the submit goes through; only field:date receives the callback, field:lookup does not.
  • DatePickerField-12622.test.tsx (updated, the existing "not written, and says so" pin is kept as is): with a host callback the picker reports the message (containing the text and the format), draws no own notice, reports null after a valid commit, on clear and on unmount; datetime reports too.

Acceptance notes

  • Hosts outside the record form (FieldEditWidget for the grid and record-page inline edit, ActionParamDialog, BulkActionDialog) pass no callback today, so there the picker still shows its own notice and does not block the host's save. Not reproduced here and not filed; noted as the remaining surface of the same channel.
  • The message shown is the picker's existing fields.datePicker.invalid string ("Not saved: ... is not a date. Type it as FORMAT, or pick one from the calendar."), so no i18n key was added.
  • The merge of main met the record-currency change in stripRegisteredFieldProps; both allow-list lines are kept.

Verification

Head 81611c8a. Test readings at bc5108de (after the one merge of main); c4826848 only types the two new test files' probe and registrations instead of casting to any (both packages' type-check exit 0 and the four pin files 33/33 there), and 81611c8a only changes the changeset's declaration to Clause-②: yes (widening): FieldWidgetComponentProps is exported from @object-ui/fields, and the new key is optional, so no consumer breaks. The derived gates are re-run at 81611c8a.

Tests and type-check (local, under the verify lock)
  • pnpm --dir ui exec vitest run --maxWorkers=2 packages/fields/ packages/components/: Test Files 734 passed, 2 skipped; Tests 8683 passed, 31 skipped; VERDICT command-exit 0. (Before the merge: 731 files, 8654 tests, exit 0.)
  • pnpm --filter @object-ui/fields type-check exit 0; pnpm --filter @object-ui/components type-check exit 0.
  • eslint on the seven changed ui/ files: 0 errors, and no warning on the lines this PR adds.
  • pnpm check:new-line-citations (ui): 0 new citations.
Ablation (one per tested behaviour, each through scripts/ablation-replace.mjs, restored by blob)
  • The form's invalidInput validator made to pass always: 4 tests red (the two refusal pins in the fields end-to-end file, both renderer pins that need the error), 20 green; restored to blob c3d119a7bf03.
  • The picker's report call removed: 5 tests red (the two end-to-end refusal pins and the three 12622 report pins), 16 green; restored to blob 556b44a4b738.
  • No build was needed: the ui vitest config aliases @object-ui/components to its src.
Gates (dispatch-gates derivation, 34 families)
  • dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN; every recorded exit 0.
  • Two were re-run alone after a first reading under load: pnpm check:type-check-debt (exit 3, queue timeout, then exit 0 under the lock) and pnpm --dir ui exec vitest run scripts/ (one test timed out at 15 s beside the package run, then 4901 passed alone).
  • node scripts/check-sdui-manifest.mjs and its --self-test: exit 0.
  • CI runs the rest of the farm.

Generated by Claude Code

…t refuses submit

The date / datetime picker never writes text that names no date, so the
form kept its old value and saved it as if nothing had been typed. The
picker now reports such text through a new onInvalidInputChange widget
prop, and the form renderer turns the report into the field's
validation error, which refuses submit the way a missing required value
does.

Claude-Session: https://claude.ai/code/session_01Utp6CXAtdK85r4832Kydbf
Co-authored-by: Claude <noreply@anthropic.com>
… in an effect, not during render

Claude-Session: https://claude.ai/code/session_01Utp6CXAtdK85r4832Kydbf
Co-authored-by: Claude <noreply@anthropic.com>
…ations instead of casting to any

Claude-Session: https://claude.ai/code/session_01Utp6CXAtdK85r4832Kydbf
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 29, 2026
…l widget prop

FieldWidgetComponentProps is exported from @object-ui/fields, so the
optional onInvalidInputChange key widens its published surface.

Claude-Session: https://claude.ai/code/session_01Utp6CXAtdK85r4832Kydbf
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 81611c8a5358c3956e5b8b1c19f57f7209b2ed2a
Local-runs: none

Face: only the Clause-②: yes (widening) declaration, which appears in the PR body, the changeset and the corrected claim 6497796776. The PR's eight files include no packages/spec/** path and no governed path, so no CONTRACT_REVIEW_TIER face applies.

① Derived judgments

  • Widened, now correctly declared: @object-ui/fields FieldWidgetComponentProps (ui/packages/fields/src/widgets/types.ts) is re-exported from the package entry (export * from './widgets/types.js', index.tsx). It gains one optional key, onInvalidInputChange?: (message: string | null) => void. The public surface grows, so yes (widening) is the honest line. Nothing becomes required and no existing key changes shape, so no widget or host breaks. The first head declared no; the seat corrected it from the diff and the dev's commit 81611c8a brought the changeset and body into line.
  • Plumbing kept inside the contract: toHostProps.ts lists the key among HOST_PLUMBING_KEYS, beside onUploadingChange, so the declared contract and the forwarded keys agree. form.tsx stripRegisteredFieldProps passes it only to date / datetime, so no other registered widget spreads an unknown handler onto its DOM.
  • Not widened: no metadata key and no spec text change. Metadata-protocol: none holds: what an author writes is unchanged, and the new invalidInput validation key is renderer-internal.
  • Behaviour change: a date form field with unparseable committed text now refuses submit. That is the defect's fix (card Record create: a date typed in an invalid form (13/45/2026) is silently dropped, and the record saves with an empty Due Date and a success toast #27483 Expected: "The date field is marked invalid … and the dialog stays open"). Hosts that pass no callback keep the picker's own notice, which the changeset states.

② Semver level

patch on @object-ui/fields and @object-ui/components. This follows the repo's rule that the level follows Metadata-protocol: (kind none ⇒ patch). The changeset body names the new optional key and the exported type.

③ Boundary flags

  • No open_questions. The dev noted an unreproduced gap: the inline edit (FieldEditWidget), ActionParamDialog and BulkActionDialog pass no callback, so there the notice shows but the host's save is not blocked. It is recorded in the PR's Acceptance notes and is outside this card.
  • The PR author is the relay (objectstack-fleet[bot]).

Implemented-by: claude/project-thread-t6m105
Reviewed-by: session_01Utp6CY2n1JmZZKrHgtp9jC

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

席位核对

ACCEPT · head 81611c8a5358c3956e5b8b1c19f57f7209b2ed2a · 2026-10-29T15:45Z

  1. Shape: draft → main, Fixes #27483 only, attribution block on top. Eight files: the fields DatePickerField.tsx, types.ts and toHostProps.ts, the components form.tsx, two new tests and one extended test, and one changeset. No assignee, no reviewer.
  2. Premise measured: the seat read the diff. The date/datetime picker reports text that names no date through the new optional onInvalidInputChange, and withdraws the report on a valid commit, a clear, or unmount. It stays silent while readonly. The form renderer keeps a per-field message map, puts a first invalidInput entry in rules.validate (so submit is refused the way a missing required value is), calls trigger(name) on a report, and clears only its own error type on withdrawal. stripRegisteredFieldProps forwards the callback to date / datetime only. This matches the card's Expected ("marked invalid … the dialog stays open"). No packages/spec/** path.
  3. Gates (check-runs on this head): 46 success, 13 skipped, 0 red. These include Lint & Repo Gates 123550642344, TypeScript Type Check 123550347050, Test UI 123555289933 and Lint · PM dispatch-gates 123546726217. The dev's derived gates were 34/34 exit 0 at c4826848. The only commit after it, 81611c8a, touches the changeset alone, and its changeset gates exit 0.
  4. Contract review: declaration face, TIER_DEFAULT, PASS on this head (comment 6501038827). The seat corrected the claim's Clause-② line to yes (widening) (6497796776) from the diff. The new optional key sits on the exported FieldWidgetComponentProps, and the changeset and body now say so.
  5. Accepted deviations / out of scope: hosts that pass no callback (the inline FieldEditWidget, ActionParamDialog, BulkActionDialog) keep the picker's own notice and do not block their save. The dev noted this, it is unreproduced, and it is outside this card. The changeset was read sentence by sentence against the diff and matches: FROM → TO, the new key, the hosts that use it and those that do not. time is a native input and untouched.
  6. Landing: not governed (check-governed-merges --pr 27623: NOT governed, 0 of 8 paths), 425 lines < 3000. The seat lands it: ready + auto-merge now.

Generated by Claude Code

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 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.

Record create: a date typed in an invalid form (13/45/2026) is silently dropped, and the record saves with an empty Due Date and a success toast

1 participant