Skip to content

finding(plugin-form): richtext is missing from ObjectForm's maxLength guard and EmbeddableForm's DEFAULT_MAX_LENGTH — a max_length on a richtext field is enforced at SUBMIT but invisible in the UI, and uncapped by default #8438

Description

@os-zhuang

Finding (observation, awaiting first grading). Filed unlabelled and unassigned — grading and domain:* belong to triage.

Surfaced by the dev on PR #8373 (card #7083) as an out-of-scope note and deliberately not acted on there. ⚠️ Every reading below is this seat's own, re-measured on origin/main ca394272 — none is inherited from that report, and one of the report's three sites did not survive the re-measurement (see 〈The third site, measured and excluded〉).

The asymmetry

richtext is a live registry key: packages/fields/src/index.tsx:2769 resolves 'richtext' to RichTextField, and it is declared in known-schema-types.ts, reports.ts and the field-type alias table. It is one of three registry keys served by ONE widgetmarkdown, html, richtext.

A max_length authored on a richtext field is enforced at submit and invisible everywhere else:

stage reads max_length on a richtext field? site
submit-time validation yes packages/fields/src/index.tsx:2565 buildValidationRules
native maxLength stop + character counter + aria-describedby no packages/plugin-form/src/ObjectForm.tsx:784
default cap when the author declared none no packages/plugin-form/src/EmbeddableForm.tsx:148

So the field silently accepts typing past its own limit, shows no counter, announces nothing to a screen reader — and then fails validation on submit, after the person has written the text. That is the worst ordering of the three possible ones.

Site 1 — ObjectForm.tsx:784, the maxLength-attribute forwarding guard

if (field.type === 'text' || field.type === 'textarea' || field.type === 'markdown' || field.type === 'html') {
  formField.maxLength = (field as any).maxLength ?? field.max_length;
  formField.minLength = (field as any).minLength ?? field.min_length;

richtext is absent. Firing control: markdown and html ARE present in the same literal, so the instrument that reports richtext absent is the same one that reports its two siblings present — the absence is a reading, not an empty search.

formField.maxLength is what packages/components/src/renderers/form/form.tsx turns into three separate consumer-visible things — the native attribute on both textareas (:761, :781, :812, :832), the character counter (:767, gated maxLength ? …), and the aria-describedby wiring (:745, :855). All three are lost together.

Site 2 — EmbeddableForm.tsx:148, DEFAULT_MAX_LENGTH

const DEFAULT_MAX_LENGTH: Record<string, number> = {
  text: 200, email: 254, url: 2048, phone: 32,
  textarea: 5000, markdown: 5000, html: 5000,
};

richtext absent; markdown: 5000 and html: 5000 present as the firing control. The consumer at :187-191 is const cap = DEFAULT_MAX_LENGTH[t]; if (!cap) return f; — so absence is not a zero cap, it is no cap at all, applied silently. A public embeddable form with a richtext field takes unbounded input where its markdown twin stops at 5000.

Why the exclusion is not justified by the control type

The obvious defence would be that an HTML maxlength attribute is meaningless on a rich-text editor. It does not hold here. RichTextField renders a plain <Textarea> — the same control as its siblings:

  • packages/fields/src/widgets/RichTextField.tsx:98 — the <Textarea> itself
  • :24 — "The <Textarea> below is this widget's ONE focusable control"
  • :183 — "For now, this is a simple textarea. A full implementation would use …"

Three registry keys, one widget, one <Textarea>. Two of the three get the cap.

The contrast site, measured rather than assumed

buildValidationRules (packages/fields/src/index.tsx:2565) reads (field as any).maxLength ?? field.max_length with no field-type gate. Measured: the only two field.type comparisons in its whole body are === 'email' and === 'url', both attaching pattern rules, neither touching the length rules. Both form producers call it on every field they build.

⇒ Submit-time enforcement is generic and already correct. The defect is entirely in the two hand-written type lists, which is what makes this cheap to fix and easy to get wrong again.

The third site, measured and excluded

PR #8373's report grouped a third enumeration with these two — DESIGNER_FIELD_TYPES (packages/types/src/designer.ts:734) — as "three pre-existing enumerations that stop at two of the widget's three registry keys". Measured against the widget registry, that grouping does not hold, so it is not part of this card:

  • registry keys ABSENT from DESIGNER_FIELD_TYPES12: auto_number checkboxes grid master_detail multiselect object radio richtext summary tags user vector
  • DESIGNER_FIELD_TYPES entries with NO widget in the registry — 6: address autonumber code color rating slider

That is a curated designer palette on its own axis, not an enumeration of the widget's registry keys. richtext sitting outside it is one of twelve and says nothing about this widget. If there is a question there it is "does the designer palette drift from the registry", which is a 12-key card and a different lane. (Noted in passing, not filed: the registry spells auto_number, the palette autonumber.)

A spelling hypothesis that was checked and came back negative

Recorded so the next reader does not re-derive it. #4831's list uses field:-prefixed spellings (field:textarea, field:markdown, field:richtext) while site 1 uses bare ones, which looks like a second defect. It is not. The two lists sit at different stages of the same file: site 1 (:784) reads the raw object-metadata field.type before mapFieldTypeToFormType, where bare spec spellings are correct; the stamping list (:1448) reads the already-mapped form-field type, where prefixed spellings are correct. Site 1's spelling family is right — it is simply missing one member.

Same class, same file, and a root cause that was named and left standing

#4831 — "mobile.fullscreenLongText 漏掉 field:richtext" — is this defect's twin: the same field type, the same file, the same shape (a hand-written type list omitting the third of one widget's three registry keys). It was fixed by PR #4839, and the fix's own docblock now sits at ObjectForm.tsx:1404-1412 recording the lesson verbatim: "field:richtext was missing from this list until objectui#4831".

⚠️ That docblock is ~660 lines below a list with the same gap. The file carries a written memory of this exact class and the class recurred inside it.

#4831 also asked, in its own body, the question that would have prevented this one:

顺带一并判断(属于同一次决定,不要只补一个字面量):清单是否应该改成「凡是解析到长文本 widget 的类型」而不是四个手写字面量 —— 手写清单正是本条与 #4250 的共同成因。

It was answered by patching one literal. This card is the next instance of the root cause that question named and its fix declined to remove. #4250 (richtext missed in four layout/column sets) is the third member.

⇒ Whoever takes this should decide the list question, not just add 'richtext' twice. Two more hand-written lists with richtext added are two more places for the fourth instance.

Not a duplicate of

All six neighbours are closed, and none covers either site:

For the implementer

Do not pin this by asserting the literals are in the list — that is the 假绿 form #4250 named and #4831 warned about explicitly. Pin it by behaviour: an object field of type: 'richtext' with max_length authored, rendered through ObjectForm, asserting the native attribute and the counter are present; and an EmbeddableForm richtext field with no authored cap, asserting the default applies. Removing 'richtext' from either list must turn both red.

Refs: #4831 / PR #4839 (the twin, same file) · #4250 (the family) · #5201 · #5253 · #3439 · #7083 / PR #8373 (where it surfaced).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions