Skip to content

fix(console): a refused form submit marks the fields it names; markdown placeholder in sans - #27443

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-3k15gu
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-3k15gu

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #27417

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Console 表单页(公开 /f/:slug、/forms/:name、应用内表单)提交被拒时,逐个标出被拒字段(aria-invalid + 控件下方原因);markdown/html/richtext 编辑器的占位文字改用无衬线字体,输入的源码仍是等宽。
  • 为什么改:HotCRM 18.2.0 浏览器走查(console: suggestions from a full HotCRM browser pass on 18.2.0: clickable path steps, chart labels and empty charts, record-page Delete, list inline edit without edit rights, public web forms, and phone layouts #27355 第 9、10 行):拒绝只给横幅和 toast,不指明哪个输入框;描述框占位文字是等宽字体。
  • 风险与回滚:只改界面标记与一个 CSS 类,横幅与 toast 原样保留;回滚即还原本 PR 两个源文件。
  • 先看:ui/apps/console/src/components/FormPage.tsx(refusedWriteError 与 catch 里的 extractFieldErrors)、ui/packages/fields/src/widgets/RichTextField.tsx、新测试 FormPage.refusalMarks-27417.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run apps/console/src/components/FormPage.refusalMarks-27417.test.tsx,预期 8 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

  • Row 10, field marks (@object-ui/console, FormPage.tsx). Every refusal on the form page used to end in one string on the banner and the outcome toast. Each refused row is now marked: aria-invalid="true" on its control and the reason under the control, which the control names in aria-describedby.
    • Client required check: each empty required row, with the record form's validation.required message ("Name is required").
    • Faulted visibleWhen: the row is marked and points at the banner, which already names the field and the rule, so no sentence is repeated under it.
    • Server refusals: the record dialog's reader of the server's field paths, extractFieldErrors from @object-ui/react, reads the refusal. No second parser was written. submitPublic / submitInternal used to throw a bare Error(text), which lost the field paths. They now throw refusedWriteError, which keeps the same message and attaches the parsed body the way @objectstack/client attaches it (code, and details = wrapped error.details, else the flat body). That covers the flat 400 VALIDATION_FAILED with fields[], the wrapped dispatcher envelope, and the flat 409 UNIQUE_VIOLATION naming its field. Only the rows the form draws are marked.
    • A refusal that names no field marks nothing. On a tabbed or wizard view the first marked row's tab or step opens, through one revealRefused shared by the client and server paths (the client path's existing behaviour, moved, not changed). Editing a marked row clears its mark. The banner and the toast are unchanged.
  • Row 9, placeholder font (@object-ui/fields, RichTextField.tsx). The markdown / html / richtext textarea is font-mono on every host, and its placeholder inherited that. It now carries placeholder:font-sans, so typed source stays mono and the placeholder reads as prose. This widened the claimed file surface; the seat approved it in its thread.
  • Row 9, brand colour: no code change. Measured below: the in-app route already takes the app's colour. The app-less routes know no app, and that half is an open question for the seat.
  • Changeset: @object-ui/console patch, @object-ui/fields patch.

Why

HotCRM's browser pass on 18.2.0 (#27355 rows 9 and 10): a refused public-form submit showed a toast and a banner but marked no field, and the form's description placeholder was monospace.

Tests

  • New ui/apps/console/src/components/FormPage.refusalMarks-27417.test.tsx, 8 tests. A server refusal naming A and B marks both and not the third row, on the public route (flat envelope) and the internal route (wrapped envelope). A 409 UNIQUE_VIOLATION marks its one field. An edit clears one mark and keeps the other. A 400 naming no field marks none. A field the form does not draw marks nothing. The client required refusal marks both empty required rows and writes nothing. A faulted visibleWhen row points at the banner.
  • New ui/packages/fields/src/widgets/__tests__/RichTextField.placeholderFont-27417.test.tsx, 3 tests (markdown, html, richtext): the control keeps font-mono and carries placeholder:font-sans. jsdom computes no placeholder style, so the computed font is the Chromium reading below.
  • Ablation (one-time, through scripts/ablation-replace.mjs, fix committed first): row mark forced to undefined makes 6 of 8 red. The 2 that stay green are the "marks none" pins, as predicted. Restored blob matches HEAD.
  • Regression at head d32c6b1: every FormPage*.test plus the fields RichText, aria-invalid and dom-leak suites, 47 files and 739 tests, all passed. pnpm --filter @object-ui/console --filter @object-ui/fields run type-check exit 0.

Acceptance notes

  • Seat 2's open PR feat(plugin-form,spec)!: ObjectForm lays the form out, the object-form block opens drawer/modal, split retires #27305 also edits FormPage.tsx, in its FORM_SPEC_TYPES hunk only. That hunk is disjoint from these, and whichever lands second merges main.
  • Row 9's app-less half is left open. /forms/:name renders in the app-independent Home layout, and the anonymous GET /forms/:slug payload is slug, object, label, form, objectSchema, with no app and no branding. The app metadata read is authenticated, so no server read was invented. It is reported to the seat as an open question with options.
  • The placeholder takes Tailwind's font-sans stack, which is a system sans stack. The console body sets its own system stack, which differs in its first families. Both are sans.

Verification

Real Chromium (/opt/pw-browsers/chromium) against the showcase backend (objectstack dev --fresh) and the console's Vite dev server. "Before" is FormPage.tsx at main and "after" is this branch.

Reading Before After
/f/contact-us, empty submit: Name / Email / Message aria-invalid false / false / false true / true / true, each described by "X is required"; Company false
/f/contact-us, server 400 VALIDATION_FAILED on email (real server answer) no field marked Email true, described by "Email must be a valid email address"; others false
Submit button vs --primary, /apps/showcase_app/forms/showcase_inquiry.contact rgb(124, 59, 237) = app #7C3AED unchanged
Submit button vs --primary, /forms/... and /f/contact-us rgb(0, 123, 255), the default (no app known) unchanged
Markdown row placeholder font-family ui-monospace stack -apple-system / system sans stack; control stays ui-monospace
Textarea and text rows placeholder body sans unchanged

The server-refusal probe set form.noValidate before submitting, because the browser's own email check would otherwise stop the submit before the server judged it. The server's answer is its real one.

Derived gates at d32c6b1: node scripts/pm/dispatch-gates.mjs --commands derived 34 families. All 34 exit 0, including node scripts/check-sdui-manifest.mjs and the ui scripts/ suite (169 files passed). --ran reconciliation: 34 run, 0 NOT-MEASURED. pnpm --dir ui run check:new-line-citations: 0 new citations.


Generated by Claude Code

A refused submit on the console form page (public /f/:slug and the
internal /forms/:name) now marks each field it names: aria-invalid on
the control and the reason under it, for the client required check, a
faulted visibleWhen (pointed at the banner that names it) and the
server's field-level refusals, read with extractFieldErrors from
@object-ui/react. The banner and the toast are unchanged.

Claude-Session: https://claude.ai/code/session_01JQwxbX1Zhx8ST6DDF1AGvD
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tooling labels Oct 29, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · os-warren · session_01JQwxbXGpVJHZTXXerMoY4C

席位核对

ACCEPT · seat domain:ui#5 · session_01JQwxbXGpVJHZTXXerMoY4C · head d32c6b12 · 2026-10-29T07:15Z

席位意见:网页表单提交被拒时,以前只有一条横幅和一条提示,输入框上没有任何标记。现在每个被拒的字段都会标上 aria-invalid,并在控件下方显示原因。这对客户端必填检查、服务器字段级拒绝(VALIDATION_FAILED 字段列表、UNIQUE_VIOLATION)以及条件显示规则故障都生效。服务器错误用记录对话框同一个读取器 extractFieldErrors 来读,没有写第二个解析器。分页或向导表单会跳到第一个被拒字段所在的页或步骤,字段一被修改,标记就清掉。富文本编辑器的占位文字改用无衬线字体,用户输入的源码仍保持等宽。应用内表单本来就带品牌色;无应用路由的品牌问题留作开放问题。

  1. Shape: Fixes #27417. 5 files, +530/−25.
    • FormPage.tsx +178: refusedWriteError attaches the body the way @objectstack/client does. It also adds fieldErrors state, a refusal prop on FieldInput, the message under the control, and revealRefused, which unifies the tab/step jump.
    • RichTextField.tsx: placeholder:font-sans.
    • Two new test files.
    • patch changesets for @object-ui/console and @object-ui/fields.
  2. Premise measured:
    • Row 10 held. Every refusal path ended in banner plus toast only.
    • Row 9 held only in part. The in-app route was already branded (Chromium shows rgb(124,59,237), the showcase #7C3AED). The app-less /forms/:name and /f/:slug have no app. The monospace placeholder came from RichTextField's font-mono, which applies on every host, not from FormPage.
    • The seat read the diff. refusedWriteError reads no field path itself, so a non-JSON body attaches nothing. Only fields the form draws are marked. A faulted visibleWhen points its control at the banner instead of repeating it. The tab/step jump keeps its earlier behaviour for client refusals.
  3. Gates / check-runs: PR check-runs on d32c6b12: 44 success, 11 skipped (path-filtered), 0 failure; mergeable_state clean. Dev: 34/34 derived gates exit 0. The new pins pass 8 of 8. The ablation turns 6 red, restored afterwards. Readings were taken in Chromium before and after.
  4. Accepted deviations:
    • The surface widened to ui/packages/fields/src/widgets/RichTextField.tsx. The seat approved this in the thread at 2026-10-29T06:21Z: no open PR touched the file, and seats 1 and 2 named no fields-package hot file.
    • @object-ui/console is not private, so it takes a patch changeset.
    • The PR has no assignee, per os-dev.md on main.
  5. Out of scope:
  6. Landing: not Tier H (no governed path), Clause-②: no. Ready for landing (ready + auto-merge).

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/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console public web form: a refused submit marks no field, and the page carries no app branding (blue button beside an orange app, monospace placeholder)

1 participant