feat(orchestrator-form): add field-level validation via ui:validateOn and ui:validateGroup - #4156
Conversation
… and ui:validateGroup Enable per-field async validation triggered on blur/change without waiting for Next/Submit. Fields annotated with ui:validateOn fire their validate:url immediately (blur) or after a 1s debounce (change). Fields sharing a ui:validateGroup name are validated together once all group members have values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Important This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior. Changed Packages
|
PR Summary by QodoAdd field-level async validation via ui:validateOn and ui:validateGroup
AI Description
Diagram
High-Level Assessment
Files changed (18)
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code Review by Qodo
1.
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4156 +/- ##
==========================================
- Coverage 58.05% 58.04% -0.01%
==========================================
Files 2409 2414 +5
Lines 96354 96534 +180
Branches 26803 26851 +48
==========================================
+ Hits 55937 56036 +99
- Misses 40225 40306 +81
Partials 192 192
*This pull request uses carry forward flags. Click here to find out more. Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
- Add try/catch around getExtraErrorsForField to handle network failures - Guard onBlur with optional chaining in ActiveTextInput, ActiveDropdown, ActiveMultiSelect - Update formDataRef synchronously in onChange to prevent stale ref reads Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use optional chaining in triggerFieldValidation - Convert VALIDATABLE_WIDGETS array to Set with .has() lookup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip clearing errors immediately on keystroke when the field has ui:validateOn: "change" — let the debounced validation replace them instead of clearing and re-showing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
karthikjeeyar
left a comment
There was a problem hiding this comment.
Group members validation needs to be debounced.
Update the schema to make validateOn to change in the step 2 and when you type a value immediately the request fires.
ValidationOnGroup.mov
- Debounce group member validation when triggered via change mode - Add try/catch around fetch with user-facing error message - Collect all error messages from response before setting errors - Add tests for multi-key errors and network failure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract debounce-or-immediate logic into scheduleValidation helper, eliminating duplicated nested branches in triggerFieldValidation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
@karthikjeeyar Updated the PR based on review comments, PTAL. |



Story: https://redhat.atlassian.net/browse/RHIDP-15871
Summary
ui:validateOnschema annotation to trigger per-field async validation onblur,change, orblur,changewithout waiting for Next/Submitui:validateGroupschema annotation for dependent field group validation — when all group members have values, validation triggers for all members automaticallyonBlurin ActiveTextInput, ActiveDropdown, and ActiveMultiSelect widgetsNew schema annotations
ui:validateOn"blur","change","blur,change"ui:validateGroupPackages affected
orchestrator-form-api— addedvalidatingFieldsandgetExtraErrorsForFieldtypesorchestrator-form-react— validation orchestration (useFieldValidation,fieldValidationConfig,mergeExtraErrors)orchestrator-form-widgets— single-field validation (validateSingleField,useGetExtraErrorsForField), widgetonBlurwiringVideo:
Screen.Recording.2026-08-04.at.1.24.07.PM.mov
How to test
Add the following proxy to your
app-config.local.yaml:Create the test workflow file at
packages/backend/.devModeTemp/repository/workflows/test-field-validation.sw.yaml:Create the schema file at
packages/backend/.devModeTemp/repository/workflows/schemas/test-field-validation__main-schema.json:{ "$id": "classpath:/schemas/test-field-validation__main-schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Test Field Validation", "description": "Tests ui:validateOn (blur, change, blur+change) and ui:validateGroup annotations", "type": "object", "properties": { "step1": { "type": "object", "title": "Independent Field Validation", "properties": { "blurField": { "title": "Blur-validated field", "description": "Validates on blur only. Tab out or click away to trigger validation.", "type": "string", "ui:widget": "ActiveTextInput", "ui:validateOn": "blur", "ui:props": { "validate:method": "POST", "validate:url": "$${{backend.baseUrl}}/api/proxy/postman-echo/post", "validate:body": { "field": "blurField", "value": "$${{current.step1.blurField}}" } } }, "changeField": { "title": "Change-validated field (debounced)", "description": "Validates on change with 1s debounce. Type and wait to see validation.", "type": "string", "ui:widget": "ActiveTextInput", "ui:validateOn": "change", "ui:props": { "validate:method": "POST", "validate:url": "$${{backend.baseUrl}}/api/proxy/postman-echo/post", "validate:body": { "field": "changeField", "value": "$${{current.step1.changeField}}" } } }, "blurAndChangeField": { "title": "Blur+Change validated field", "description": "Validates on both blur and change. Try both interactions.", "type": "string", "ui:widget": "ActiveTextInput", "ui:validateOn": "blur,change", "ui:props": { "validate:method": "POST", "validate:url": "$${{backend.baseUrl}}/api/proxy/postman-echo/post", "validate:body": { "field": "blurAndChangeField", "value": "$${{current.step1.blurAndChangeField}}" } } }, "noValidation": { "title": "No field-level validation (default)", "description": "No ui:validateOn — validates only on Next/Submit click.", "type": "string", "ui:widget": "ActiveTextInput", "ui:props": { "validate:method": "POST", "validate:url": "$${{backend.baseUrl}}/api/proxy/postman-echo/post", "validate:body": { "field": "noValidation", "value": "$${{current.step1.noValidation}}" } } } }, "required": ["blurField"] }, "step2": { "type": "object", "title": "Dependent Group Validation", "properties": { "namespace": { "title": "Namespace", "description": "Part of 'ns-cluster' group. Both namespace and cluster must be filled to trigger group validation.", "type": "string", "ui:widget": "ActiveTextInput", "ui:validateOn": "blur", "ui:validateGroup": "ns-cluster", "ui:props": { "validate:method": "POST", "validate:url": "$${{backend.baseUrl}}/api/proxy/postman-echo/post", "validate:body": { "field": "namespace", "value": "$${{current.step2.namespace}}", "relatedCluster": "$${{current.step2.cluster}}" } } }, "cluster": { "title": "Cluster", "description": "Part of 'ns-cluster' group. Fill both namespace and cluster, then blur either to validate both.", "type": "string", "ui:widget": "ActiveTextInput", "ui:validateOn": "blur", "ui:validateGroup": "ns-cluster", "ui:props": { "validate:method": "POST", "validate:url": "$${{backend.baseUrl}}/api/proxy/postman-echo/post", "validate:body": { "field": "cluster", "value": "$${{current.step2.cluster}}", "relatedNamespace": "$${{current.step2.namespace}}" } } }, "tags": { "title": "Tags (MultiSelect with change validation)", "description": "Validates on change. Select tags and wait 1s for validation.", "type": "array", "items": { "type": "string" }, "uniqueItems": true, "ui:widget": "ActiveMultiSelect", "ui:validateOn": "change", "ui:props": { "fetch:url": "$${{backend.baseUrl}}/api/proxy/postman-echo/post", "fetch:method": "POST", "fetch:body": { "options": true }, "fetch:response:autocomplete": "['frontend','backend','infra','security','data']", "fetch:retrigger": [], "validate:method": "POST", "validate:url": "$${{backend.baseUrl}}/api/proxy/postman-echo/post", "validate:body": { "field": "tags", "value": "$${{current.step2.tags}}" } } } }, "required": ["namespace", "cluster"] } } }Test scenarios
validate:urltopostman-echo/status/400→ error appears on blur/changeTest plan
fieldValidationConfig(parseValidateOn, getFieldValidationConfig, getGroupMembers, areAllGroupFieldsPopulated)mergeExtraErrors(merge, replace, clear, preserve, immutability)validateSingleField(validate:url handling, error responses, widget type filtering)FormWidgetsApi(getExtraErrorsForField prop passing)npx tsc --noEmitpasses clean