feat(contracts): focus-first-invalid on the contract form via FormGuard (PMS-518) #355

Merged
nrupard merged 2 commits from feat/PMS-518-migrate-contracts-form into main 2026-06-25 21:23:02 +02:00
Owner

What

PMS-518 increment: adds focus-first-invalid to the Create/Edit Contract form via FormGuard.

Context

This form already collects every field error at once (MAPPS-211: it sets each field's inline *_err signal and only returns at the end, never short-circuiting), so it already satisfies the "report all failures" half of the unified system. The one missing behaviour was focusing the first invalid field on a blocked submit.

Change

On the failure path, builds a FormGuard from the per-field err signals (read in field order), notes the first non-empty one, and focuses it via guard.blocked(). The ids match each input's name/DOM id. Line-item row errors have no single focusable id, so they fall back to no-focus (still blocked) when no top-level field is also invalid.

Why not a full Rule rewrite

Deliberately keeps the bespoke typed validators (validate_text_required / validate_money / validate_quantity / validate_text_optional): unlike the generic Rule set, they parse-and-return the typed values (Decimal, Option<String>, ...) the submit body consumes. Replacing them with Rule::Number etc. would force a re-parse and regress a form that already works well. FormGuard is adopted only for the focus/block behaviour.

Verification

just check parity via the rust-builder image: clippy --all-targets -- -D warnings, fmt --check, cargo check --target wasm32-unknown-unknown - all green.

Scope: the contract create/edit form. The contracts modals (Contract Item, Rate Card, Rate Card Item, Allotted Hours) are further increments. Part of PMS-518.

## What PMS-518 increment: adds **focus-first-invalid** to the Create/Edit Contract form via `FormGuard`. ## Context This form already collects every field error at once (MAPPS-211: it sets each field's inline `*_err` signal and only returns at the end, never short-circuiting), so it already satisfies the "report all failures" half of the unified system. The one missing behaviour was focusing the first invalid field on a blocked submit. ## Change On the failure path, builds a `FormGuard` from the per-field err signals (read in field order), notes the first non-empty one, and focuses it via `guard.blocked()`. The ids match each input's `name`/DOM id. Line-item row errors have no single focusable id, so they fall back to no-focus (still blocked) when no top-level field is also invalid. ## Why not a full Rule rewrite Deliberately keeps the bespoke typed validators (`validate_text_required` / `validate_money` / `validate_quantity` / `validate_text_optional`): unlike the generic `Rule` set, they parse-and-return the typed values (`Decimal`, `Option<String>`, ...) the submit body consumes. Replacing them with `Rule::Number` etc. would force a re-parse and regress a form that already works well. `FormGuard` is adopted only for the focus/block behaviour. ## Verification `just check` parity via the rust-builder image: `clippy --all-targets -- -D warnings`, `fmt --check`, `cargo check --target wasm32-unknown-unknown` - all green. Scope: the contract create/edit form. The contracts modals (Contract Item, Rate Card, Rate Card Item, Allotted Hours) are further increments. Part of PMS-518.
feat(contracts): add focus-first-invalid to the contract form via FormGuard (PMS-518)
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 21m31s
59d080eb39
The Create/Edit Contract handler already collects every field error at once (MAPPS-211: it sets each field's inline err signal and only returns at the end, never short-circuiting), so it already satisfies the "report all failures" half of the unified system. The one missing behaviour was focusing the first invalid field.

Adds a FormGuard on the failure path that, from the per-field err signals (set in field order), notes the first non-empty one and focuses it via guard.blocked(). The field ids match each input's name/DOM id. Line-item row errors have no single focusable id, so they fall back to no-focus (still blocked) when no top-level field is also invalid.

Deliberately keeps the bespoke typed validators (validate_text_required / validate_money / validate_quantity / validate_text_optional): unlike the generic Rule set, they parse-and-return the typed values (Decimal, Option<String>, ...) the body consumes, so replacing them with Rule::Number etc. would force a re-parse and regress a form that already works well. The shared FormGuard is adopted only for the focus/block behaviour.

just check parity (rust-builder image): clippy --all-targets -D warnings, fmt --check, cargo check --target wasm32-unknown-unknown all green.

#PMS-518
feat(contracts): migrate the contract modals to FormGuard (PMS-518)
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 19m34s
Create release / Create release from merged PR (pull_request) Has been skipped
cc705c40be
Follows the Create/Edit Contract migration on this branch; brings the contract detail modals onto the shared guard (focus-first + per-field inline slots), keeping every bespoke typed parser since their returned Decimals/UUIDs build the request bodies.

- ContractItemFormModal: was short-circuit, converted to accumulate. Per-field slots for Name / Quantity / Unit Price; kept the inline Decimal parsers; focus-first on the first invalid field.
- RateCardItemFormModal: was short-circuit, converted to accumulate. Per-field slots for Work type / Hourly / After-hours / Emergency; kept the UUID + Decimal parsers; the combined "rates cannot be negative" banner split into per-field checks; focus-first.
- AllotmentFormModal: single Allotted Hours field -> per-field slot + focus-first; kept the optional-Decimal parser.
- RateCardFormModal: Name required -> per-field slot + focus-first.

Every modal keeps its banner for server/submit errors. Values sent to the server are unchanged. just check parity (rust-builder image): clippy --all-targets -D warnings, fmt --check, cargo check --target wasm32-unknown-unknown all green.

#PMS-518
nrupard scheduled this pull request to auto merge when all checks succeed 2026-06-25 21:11:58 +02:00
nrupard deleted branch feat/PMS-518-migrate-contracts-form 2026-06-25 21:23:02 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-apps!355
No description provided.