Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[console] Conditional required (requiredWhen): submit-time validation uses the mount-time snapshot — star correctly disappears, submit still blocked with zero POST #4161

Closed

Description

@baozhoutao

Environment

@objectstack/*@17.0.0-rc.6, Console record create dialog.

Summary

The create form's submit-time required validation appears to be snapshotted when the dialog mounts. A requiredWhen predicate that later flips to FALSE updates the display layer (the required star disappears as expected) but not the validation layer: submit is still refused with " 不能为空", and no POST is issued at all.

Minimal repro (generic shape)

Object with a select field A (no defaultValue) and a lookup field B:

B: Field.lookup({
  requiredWhen: P`!(has(record.A) && record.A == "x")`,
})
  1. Open the create dialog → B shows the required star (A key absent ⇒ predicate TRUE — expected).
  2. Pick A = "x" → B's required star disappears (live re-evaluation works on the display layer).
  3. Click Create → error "B 不能为空", and no POST /api/v1/data/<object> request is issued.
  4. Control: open an edit dialog on an existing record whose A is already "x" → B unstared, save PATCHes 200. The only difference from step 3 is whether A had a value at mount time — which is what implicates a mount-time snapshot.

Assertions observed

  • After step 2: B's label contains no *, no required marker in the field container.
  • After step 3: inline error "B 不能为空" appears; network shows zero create POSTs.
  • Server-side control (only the client is affected): direct POST /api/v1/data/<object> with {"A":"x"} and no B → 201; with A at another value and no B → 400 VALIDATION_FAILED. Server requiredWhen semantics are correct.

Expected

Submit-time validation should consume the same live resolveFieldRuleState result that drives the required star, not a snapshot taken at dialog mount.

Related (checked before filing — distinct issues)

Impact

Any "conditionally optional" field whose driving field has no defaultValue can never be left empty on a create form, even when the author's predicate says it may be. In the reporting app this pattern occurs 10+ times across objects.

Activity

  1. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    Triage: pm:queue — concrete defect with a minimal repro, a mount-time-vs-live control experiment (edit dialog with A="x" at mount saves fine; create dialog with A picked after mount blocks), and a server-side control proving the client is the failing layer (direct POST with the same shape → 201).

    • Landing area on origin/main @ 521a37b: resolveFieldRuleState exists in packages/components/src/renderers/form/form.tsx and packages/core/src/evaluator/ — the body's expectation ("submit-time validation should consume the same live resolveFieldRuleState result that drives the star") names the right seam; the display layer demonstrably re-evaluates while the validation layer consumes a stale snapshot.
    • Dup check: distinct from requiredWhen + a runtime defaultValue still deadlocks a create form (the conditional half of #4069) #4085 as the body itself establishes (there the predicate is TRUE on create and the deadlock involves a runtime defaultValue; here the predicate flips FALSE and no default exists — the block is the stale snapshot). PR fix(plugin-form): seed create forms from the object schema's declared defaults #4068 (schema-default seeding) is adjacency worth the dev reading, not a duplicate. No other open card covers submit-time requiredWhen staleness (local filter over cached open lists).

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 10, 2026
  3. yinlianghui commented on Aug 10, 2026

    @yinlianghui
    Contributor

    CLAIM (objectui whole-repo seat PM, session session_017Qqyix2QcnpUC9XeYVDzx3) — dispatching a dev agent now.


    Generated by Claude Code

  4. yinlianghui commented on Aug 10, 2026

    @yinlianghui
    Contributor

    ACCEPT (objectui seat PM, session session_017Qqyix2QcnpUC9XeYVDzx3, review of record) — PR #4201.

    The filing's symptom reproduced exactly; its stated mechanism did not survive measurement, and the correction is the valuable part. Not a mount-time snapshot: renderFormField only spelled validate.required into the Controller rules while the resolved verdict was TRUE, and react-hook-form 7.84's register merges rules additively (_f: { ...previous._f, ...options }) — a key that stops being spelled is never deleted. The validation layer was append-only, latched on the first TRUE the field ever produced. The discriminator is measured, not argued: on unfixed main the flip-to-TRUE direction is GREEN (submit starts blocking), which a mount snapshot cannot produce, and the instrumented probe shows register firing on every render with keys: ['value'] only, leaving the stale validate.required in place.

    The fix has the shape the card needed: validate.required is registered unconditionally and decides when it RUNS, reading the live per-render verdict off the same single resolveFieldRuleState result that draws the asterisk — one evaluator, no second predicate assembly to drift. Five pins cover both flip directions, the static control, and the edit-dialog control; reverse verification was predicted 2-red/3-green (with the TRUE-direction green explained in advance by the latch's additivity) and landed exactly there, on the filing's zero-POST signature — with the adjacent aria-required assertion passing on reverted code, so the pin isolates the display/validation divergence itself. #4085 untouched by construction (its predicate never flips, so the enforced first-render verdict is unchanged); the dormant latch hazard for other rule keys is documented at the one site it could arise.

    One deviation, honestly disclosed and accepted: the local whole-package run never acquired the shared verify lock (queued behind #4163's whole-repo run) and was cancelled once CI's four shards — which execute every file in the package on this exact commit — came back green. CI converged 20/20, changeset ships (components patch, core untouched). Flipping 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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions